animations

This commit is contained in:
ryanfitzpatrickio
2026-08-03 16:04:46 -05:00
parent 8b28e299e6
commit fb1ebeed05
28 changed files with 5246 additions and 105 deletions
+121
View File
@@ -0,0 +1,121 @@
/**
* Player definition — identity traits that are not sim state.
*
* Appearance (body style, jersey) and locomotion (velocity, effort) live
* elsewhere. This bag is the stable "who is this skater" record: how they hold
* a stick, which side they shoot from, and anything else that picks a pose set
* rather than a frame of motion.
*
* Kept plain and three.js-free so the same record can travel over the wire.
*/
/** Which side of the body the stick lives on, and which way they shoot. */
export const SHOT_SIDE = Object.freeze({
LEFT: 'left',
RIGHT: 'right',
});
/**
* Defaults for a new player bag.
*
* Authored stickwork (GRIP targets, arm poses) is written for a **right**
* shot: top hand on the right, lower hand on the left, forehand at X in
* skater space. A **left** shot mirrors that set across the body midline.
*/
export const PLAYER_DEFAULTS = Object.freeze({
shotSide: SHOT_SIDE.RIGHT,
});
/**
* Accept the spellings we are likely to see and fold them to `'left' | 'right'`.
*
* @param {unknown} v
* @returns {'left' | 'right'}
*/
export function normalizeShotSide(v) {
if (v === SHOT_SIDE.LEFT || v === 'L' || v === 'l' || v === -1 || v === false) {
return SHOT_SIDE.LEFT;
}
if (v === SHOT_SIDE.RIGHT || v === 'R' || v === 'r' || v === 1 || v === true) {
return SHOT_SIDE.RIGHT;
}
return PLAYER_DEFAULTS.shotSide;
}
/**
* Sign used to mirror authored right-shot content onto a left shot.
* `+1` = as authored (right), `-1` = mirror across the sagittal plane (left).
*
* @param {unknown} side
* @returns {1 | -1}
*/
export function shotSign(side) {
return normalizeShotSide(side) === SHOT_SIDE.LEFT ? -1 : 1;
}
/**
* Top hand on the stick for this shot side.
* Right shot: right hand on top. Left shot: left hand on top.
*
* @param {unknown} side
* @returns {'L' | 'R'}
*/
export function topHandFor(side) {
return normalizeShotSide(side) === SHOT_SIDE.LEFT ? 'L' : 'R';
}
/**
* Lower (blade-side) hand — the one IK pins to the shaft.
*
* @param {unknown} side
* @returns {'L' | 'R'}
*/
export function lowerHandFor(side) {
return normalizeShotSide(side) === SHOT_SIDE.LEFT ? 'R' : 'L';
}
/**
* Rough real-world split (~62% left shot in the NHL). Used when a roster
* entry does not specify a side, so a lineup is not all clones.
*
* @param {number} u01 uniform in [0, 1)
* @returns {'left' | 'right'}
*/
export function rollShotSide(u01) {
const u = Number.isFinite(u01) ? u01 : 0.5;
return u < 0.62 ? SHOT_SIDE.LEFT : SHOT_SIDE.RIGHT;
}
/**
* Normalize a partial player bag. Missing keys take the defaults.
*
* @param {unknown} raw
* @returns {{ shotSide: 'left' | 'right' }}
*/
export function normalizePlayer(raw) {
if (!raw || typeof raw !== 'object') {
return { shotSide: PLAYER_DEFAULTS.shotSide };
}
return {
shotSide: normalizeShotSide(/** @type {{ shotSide?: unknown }} */ (raw).shotSide),
};
}
/**
* Compact wire form for roster snapshots.
* @returns {{ ss: 'L' | 'R' }}
*/
export function packPlayer(player) {
const p = normalizePlayer(player);
return { ss: p.shotSide === SHOT_SIDE.LEFT ? 'L' : 'R' };
}
/** Inverse of packPlayer — also accepts a full `{ shotSide }` bag. */
export function unpackPlayer(raw) {
if (!raw || typeof raw !== 'object') return normalizePlayer(null);
if ('shotSide' in raw) return normalizePlayer(raw);
const ss = /** @type {{ ss?: unknown }} */ (raw).ss;
if (ss === 'L' || ss === 'l') return { shotSide: SHOT_SIDE.LEFT };
if (ss === 'R' || ss === 'r') return { shotSide: SHOT_SIDE.RIGHT };
return normalizePlayer(null);
}