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
+132 -40
View File
@@ -1,5 +1,6 @@
import { E } from '../../core/math.js';
import { clamp, lerp } from '../../../shared/scalar.js';
import * as THREE from 'three';
/**
* Upper-body authoring for everything done with the stick.
@@ -11,9 +12,9 @@ import { clamp, lerp } from '../../../shared/scalar.js';
*
* Each one is a function of a single phase 0..1 so the animator can drive it
* from a timer, hold it (wind-up), or run it once and blend out (shoot, pass,
* poke). The right arm carries the stick; the left joins it for two-handed
* work and is pinned onto the shaft by IK afterwards, so what is authored here
* for the left side is only a starting guess that the IK refines.
* poke). Poses are authored for a **right** shot (top hand right, lower hand
* left, forehand at X). A left shot runs the same functions and then
* `mirrorStickwork` swaps the arms and flips the coil across the midline.
*/
/**
@@ -36,6 +37,39 @@ export const STICK_SPINE = ['spine1', 'spine2', 'spine3', 'neck', 'head'];
export const STICK_BONES = STICK_ARMS.concat(STICK_SPINE);
/** Left/right arm pairs the mirror swaps. */
const ARM_PAIRS = [
['clavicleL', 'clavicleR'],
['upperArmL', 'upperArmR'],
['forearmL', 'forearmR'],
['handL', 'handR'],
];
const _mirrorL = new THREE.Euler();
const _mirrorR = new THREE.Euler();
/**
* Mirror a right-shot stickwork pose onto a left shot.
*
* Arms swap sides with yaw/roll flipped; spine coil flips the same way. Call
* after any stickwork writer when `shotSign < 0`. Idempotent only if you do
* not call it twice — the animator applies it once per layer write.
*/
export function mirrorStickwork(P) {
for (const [l, r] of ARM_PAIRS) {
if (!P.q[l] || !P.q[r]) continue;
_mirrorL.setFromQuaternion(P.q[l], 'XYZ');
_mirrorR.setFromQuaternion(P.q[r], 'XYZ');
E(P.q[l], _mirrorR.x, -_mirrorR.y, -_mirrorR.z);
E(P.q[r], _mirrorL.x, -_mirrorL.y, -_mirrorL.z);
}
for (const n of STICK_SPINE) {
if (!P.q[n]) continue;
_mirrorL.setFromQuaternion(P.q[n], 'XYZ');
E(P.q[n], _mirrorL.x, -_mirrorL.y, -_mirrorL.z);
}
}
/**
* The neutral carry, and the hustle variant.
*
@@ -102,8 +136,9 @@ export function poseCarry(P, { hustle = 0, reach = 0, lateral = 0 }) {
* Wind-up. `phase` 0..1 is how loaded the shot is, and it is *held* — the
* animator parks here for as long as the Skill Stick is pulled back.
*
* Hands high and back, stick raised behind the head — not hanging blade-down
* from waist height. The torso coils open so the follow-through has something
* Both hands stay on the stick, relatively square, and the whole grip draws
* back and *up* as a unit from the carry — not a golf swing that parks the
* stick behind the head. The torso coils open so the downswing has something
* to spend.
*/
export function poseWindup(P, { phase = 0, aim = 0 }) {
@@ -112,58 +147,115 @@ export function poseWindup(P, { phase = 0, aim = 0 }) {
// left is the opposite sign.
const side = -aim;
// Torso coils open, loading the shot side.
E(P.q.spine1, -0.06 - 0.08 * w, -0.18 - 0.42 * w + side * 0.08, -0.05 * w);
E(P.q.spine2, -0.07 - 0.1 * w, -0.22 - 0.48 * w + side * 0.1, -0.06 * w);
E(P.q.spine3, -0.04 - 0.07 * w, -0.18 - 0.38 * w + side * 0.08, -0.04 * w);
// Torso coils open on the shot side — enough load, not a full pirouette.
E(P.q.spine1, -0.03 - 0.04 * w, -0.1 - 0.18 * w + side * 0.08, -0.02 * w);
E(P.q.spine2, -0.04 - 0.05 * w, -0.12 - 0.22 * w + side * 0.1, -0.03 * w);
E(P.q.spine3, -0.02 - 0.04 * w, -0.08 - 0.16 * w + side * 0.08, -0.02 * w);
// Eyes stay on the target while the body turns away from it.
E(P.q.neck, 0.04, 0.28 + 0.42 * w - side * 0.2, 0);
E(P.q.head, 0.04, 0.22 + 0.32 * w - side * 0.25, 0);
E(P.q.neck, 0.02, 0.12 + 0.18 * w - side * 0.16, 0);
E(P.q.head, 0.02, 0.1 + 0.14 * w - side * 0.18, 0);
// Top hand: high and back, roughly shoulder/head height, so the aimed stick
// can sit up behind the head instead of dangling at the hip.
E(P.q.clavicleR, -0.1 * w, -0.22 * w, -0.1);
E(P.q.upperArmR, 0.15 + 0.65 * w, -0.55 - 0.35 * w + side * 0.15, -0.55 - 0.35 * w);
E(P.q.forearmR, -0.45 - 0.25 * w, 0.22, -0.12);
E(P.q.handR, -0.05, 0.2, 0.22);
// Carry-like arms that lift and pull back *together* on the forehand side.
// Keeping the seed close to the two-handed carry means the lower-hand IK only
// finishes the last few centimetres, and the hands stay square on the shaft.
E(P.q.clavicleR, -0.02 * w, -0.05 * w, -0.05);
E(
P.q.upperArmR,
lerp(-0.42, -0.3, w) + side * 0.03,
lerp(0.42, 0.22, w) + side * 0.1,
lerp(0.68, 0.62, w),
);
E(
P.q.forearmR,
lerp(-1.35, -1.2, w),
lerp(0.02, 0.05, w),
lerp(0.32, 0.28, w),
);
E(P.q.handR, lerp(-0.12, -0.09, w), lerp(0.12, 0.13, w), lerp(0.04, 0.06, w));
// Lower hand comes up with it; IK pins it to the shaft.
E(P.q.clavicleL, 0.04, 0.12 * w, 0.08);
E(P.q.upperArmL, -0.35 - 0.1 * w, 0.45 + 0.2 * w, 0.4 + 0.15 * w);
E(P.q.forearmL, -0.85 - 0.15 * w, -0.18, -0.12);
E(P.q.handL, -0.08, 0, -0.1);
E(P.q.clavicleL, 0.03, lerp(0.06, 0.07, w), lerp(-0.06, -0.03, w));
E(
P.q.upperArmL,
lerp(-0.38, -0.26, w),
lerp(0.1, 0.16, w) + side * 0.06,
lerp(-0.48, -0.4, w),
);
E(
P.q.forearmL,
lerp(-1.32, -1.2, w),
lerp(-0.08, -0.09, w),
lerp(0.18, 0.15, w),
);
E(P.q.handL, -0.08, 0, lerp(0.08, 0.02, w));
}
/**
* Follow-through. `phase` 0..1 runs once, fast.
* Shot swing. `phase` 0..1 runs once, fast.
*
* The coil released: the torso whips through the shot, the stick sweeps across
* and finishes high. Front-loaded easing, so the contact reads at the start of
* the animation rather than in the middle of it.
* Three beats, matching the grip path: still loaded → square through the puck
* with the blade on the ice → follow-through high across the body. Front-loaded
* easing so contact reads early, not in the middle of a slow blend.
*/
export function poseShot(P, { phase = 0, power = 1, aim = 0 }) {
const t = clamp(phase, 0, 1);
// Fast out of the coil, then settle.
// Fast out of the coil, then settle into the follow.
const s = 1 - (1 - t) * (1 - t);
const p = clamp(power, 0.2, 1);
// 0..1 through the "square on the ice" window, then 0..1 into the follow.
// Contact lands around t≈0.28 so the blade is flush when the puck leaves.
const down = clamp(t / 0.28, 0, 1);
const through = clamp((t - 0.28) / 0.55, 0, 1);
const square = 1 - (1 - down) * (1 - down);
const twist = lerp(-0.42 * p, 0.44 * p, s);
E(P.q.spine1, -0.06 + 0.12 * s, twist * 0.9, 0.04 * s);
E(P.q.spine2, -0.07 + 0.14 * s, twist, 0.05 * s);
E(P.q.spine3, -0.05 + 0.1 * s, twist * 0.8, 0.03 * s);
const twist = lerp(-0.28 * p, 0.38 * p, s);
E(P.q.spine1, -0.04 + 0.1 * s, twist * 0.9, 0.03 * s);
E(P.q.spine2, -0.05 + 0.12 * s, twist, 0.04 * s);
E(P.q.spine3, -0.03 + 0.08 * s, twist * 0.8, 0.03 * s);
E(P.q.neck, 0.02, -twist * 0.5 + aim * 0.2, 0);
E(P.q.head, 0.02, -twist * 0.4 + aim * 0.25, 0);
// Top hand drives through and finishes high across the body.
E(P.q.clavicleR, lerp(-0.05, 0.04, s), lerp(-0.14, 0.1, s), -0.06);
E(P.q.upperArmR, lerp(0.3, -1.05 * p, s), lerp(-0.94, 0.3, s), lerp(-0.72, -0.1, s));
E(P.q.forearmR, lerp(-1.12, -0.42, s), 0.16, -0.1);
E(P.q.handR, -0.1, 0.1, 0.16);
// Arms: wind-up square → contact square (carry-like) → follow high.
// The early half is deliberately close to the carry pose so both hands stay
// on the shaft and the stick reads flat through the ice, not rotating off it.
E(
P.q.clavicleR,
lerp(lerp(-0.02, 0.02, square), 0.04, through),
lerp(lerp(-0.06, -0.04, square), 0.1, through),
-0.05,
);
E(
P.q.upperArmR,
lerp(lerp(-0.3, -0.42, square), -1.0 * p, through),
lerp(lerp(0.22, 0.4, square), 0.28, through),
lerp(lerp(0.62, 0.68, square), -0.08, through),
);
E(
P.q.forearmR,
lerp(lerp(-1.2, -1.34, square), -0.4, through),
lerp(0.05, 0.12, through),
lerp(0.28, -0.08, through),
);
E(P.q.handR, -0.1, 0.1, 0.12);
E(P.q.clavicleL, 0.03, lerp(0.1, -0.04, s), 0.06);
E(P.q.upperArmL, lerp(-0.86, -0.3, s), lerp(0.66, 0.12, s), lerp(0.4, 0.5, s));
E(P.q.forearmL, lerp(-1.36, -0.6, s), -0.28, -0.2);
E(P.q.handL, -0.08, 0, -0.14);
E(
P.q.clavicleL,
0.03,
lerp(lerp(0.07, 0.06, square), -0.04, through),
lerp(-0.03, 0.05, through),
);
E(
P.q.upperArmL,
lerp(lerp(-0.26, -0.38, square), -0.28, through),
lerp(lerp(0.16, 0.12, square), 0.1, through),
lerp(lerp(-0.4, -0.48, square), 0.48, through),
);
E(
P.q.forearmL,
lerp(lerp(-1.2, -1.32, square), -0.55, through),
lerp(-0.09, -0.2, through),
lerp(0.15, -0.14, through),
);
E(P.q.handL, -0.08, 0, -0.08);
}
/**