animations
This commit is contained in:
+132
-40
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user