Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions packages/posecode-render/src/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,33 @@ const REST_MODE: Record<TimingMode, boolean> = {
linear: false,
};

/** Cubic Hermite interpolation with endpoint velocities expressed per second. */
function hermite(a: number, b: number, va: number, vb: number, span: number, t: number): number {
const t2 = t * t;
const t3 = t2 * t;
return (
(2 * t3 - 3 * t2 + 1) * a +
(t3 - 2 * t2 + t) * span * va +
(-2 * t3 + 3 * t2) * b +
(t3 - t2) * span * vb
);
}

/**
* Time-aware centered velocity at an interior root keyframe. Rest keyframes
* deliberately have zero velocity; flowing keyframes carry momentum through.
*/
function rootVelocity(
prev: Keyframe,
current: Keyframe,
next: Keyframe,
read: (keyframe: Keyframe) => number,
): number {
if (current.rest) return 0;
const span = next.time - prev.time;
return span > 1e-6 ? (read(next) - read(prev)) / span : 0;
}

export function buildTimeline(ir: PosecodeIR): BuiltTimeline {
const basePose = poseFor(ir.startPose);
const baseJoints = new Map<string, EulerDegTuple>(
Expand Down Expand Up @@ -248,14 +275,33 @@ export function buildTimeline(ir: PosecodeIR): BuiltTimeline {
: squadControl(q0, q1, kNext.quats.get(bone)!);
squad(q0, s0, s1, q1, eased, node.quaternion);
}
// Root facing/position: linear interpolation of the raw values so a large
// turn (e.g. 360°) sweeps the whole way round rather than taking a short
// arc. Uses the same eased param as the joints so everything moves as one.
const rootYaw = a.yaw + (b.yaw - a.yaw) * eased;
const rootOffset = {
x: a.pos.x + (b.pos.x - a.pos.x) * eased,
z: a.pos.z + (b.pos.z - a.pos.z) * eased,
};
// Root facing/position use the scalar analogue of the joint squad spline:
// cubic Hermite with time-aware centered tangents. This carries velocity
// through `flow` waypoints instead of hitting every travel point with a
// visible direction/speed kink (box steps, grapevines, waltz, chassé).
// Yaw remains a raw scalar rather than a quaternion so a 360° turn still
// sweeps the full revolution. Rest points receive zero tangent.
const yawA = rootVelocity(kPrev, a, b, (kf) => kf.yaw);
const yawB = rootVelocity(a, b, kNext, (kf) => kf.yaw);
const xA = rootVelocity(kPrev, a, b, (kf) => kf.pos.x);
const xB = rootVelocity(a, b, kNext, (kf) => kf.pos.x);
const zA = rootVelocity(kPrev, a, b, (kf) => kf.pos.z);
const zB = rootVelocity(a, b, kNext, (kf) => kf.pos.z);
// `linear` is an explicit authoring promise, so preserve a literal
// straight interpolation for that mode. The spline applies to the
// expressive timing modes, especially continuous `flow` choreography.
const rootYaw = b.easing === "linear"
? a.yaw + (b.yaw - a.yaw) * eased
: hermite(a.yaw, b.yaw, yawA, yawB, span, eased);
const rootOffset = b.easing === "linear"
? {
x: a.pos.x + (b.pos.x - a.pos.x) * eased,
z: a.pos.z + (b.pos.z - a.pos.z) * eased,
}
: {
x: hermite(a.pos.x, b.pos.x, xA, xB, span, eased),
z: hermite(a.pos.z, b.pos.z, zA, zB, span, eased),
};
return {
phaseName: b.name,
...(b.cue ? { cue: b.cue } : {}),
Expand Down
32 changes: 32 additions & 0 deletions packages/posecode-render/test/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,38 @@ describe("spatial choreography (turn & travel)", () => {
expect(tl.travelExtent).toBeCloseTo(Math.hypot(0.5, 0.4), 3);
});

it("carries root velocity smoothly through flow travel waypoints", () => {
const src = [
'posecode exercise "Corner"',
" rig humanoid",
" pose start = standing",
' step "Across" 1s flow:',
" travel: 1 0",
' step "Forward" 1s flow:',
" travel: 1 1",
" repeat 1",
].join("\n");
const { ir } = parse(src);
const tl = buildTimeline(ir!);
const m = buildMannequin();
const eps = 1e-3;
const before = tl.sample(1 - eps, m.bones).rootOffset;
const at = tl.sample(1, m.bones).rootOffset;
const after = tl.sample(1 + eps, m.bones).rootOffset;
const velocityBefore = {
x: (at.x - before.x) / eps,
z: (at.z - before.z) / eps,
};
const velocityAfter = {
x: (after.x - at.x) / eps,
z: (after.z - at.z) / eps,
};

expect(at).toEqual({ x: 1, z: 0 });
expect(velocityBefore.x).toBeCloseTo(velocityAfter.x, 2);
expect(velocityBefore.z).toBeCloseTo(velocityAfter.z, 2);
});

it("leaves yaw and offset at home for movements that never turn/travel", () => {
const src = [
'posecode exercise "Curl"',
Expand Down
57 changes: 32 additions & 25 deletions spec/examples/box-step.posecode
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,49 @@ posecode exercise "Box step"
rig humanoid
pose start = standing

step "Step forward-right" 0.9s flow:
hip_right: flex 35
knee_right: flex 40
ankle_right: plantarflex 40
shoulder_left: flex 25
shoulder_right: extend 15
travel: -0.35 0.35
step "Right foot forward" 0.9s flow:
hip_right: flex 30
knee_right: flex 32
ankle_right: plantarflex 24
shoulder_left: flex 22
shoulder_right: extend 12
travel: 0 0.32
pin: foot_left floor
cue "Step the right foot forward and out to the corner"
cue "Step forward onto the right foot with an easy contralateral arm swing"

step "Close the feet" 0.7s flow:
step "Left foot side" 0.85s flow:
hip_right: flex 0
knee_right: flex 0
ankle_right: plantarflex 0
shoulders: flex 0
travel: -0.35 0.35
ground-lock: feet
cue "Bring the left foot to meet it, standing tall on the corner"

step "Step back-left" 0.9s flow:
hip_left: flex 35
knee_left: flex 40
ankle_left: plantarflex 40
shoulder_right: flex 25
shoulder_left: extend 15
travel: 0 0
hip_left: abduct 24
knee_left: flex 22
ankle_left: plantarflex 20
shoulder_left: flex 0
shoulder_right: flex 20
travel: -0.32 0.32
pin: foot_right floor
cue "Step the left foot back to the start on the diagonal"
cue "Transfer left to the side, keeping the torso lifted"

step "Close home" 0.7s settle:
hip_left: flex 0
step "Right foot back" 0.9s flow:
hip_left: abduct 0
knee_left: flex 0
ankle_left: plantarflex 0
hip_right: extend 18
knee_right: flex 30
ankle_right: plantarflex 24
shoulder_right: flex 0
shoulder_left: flex 22
travel: -0.32 0
pin: foot_left floor
cue "Step back onto the right foot to trace the third side of the box"

step "Left foot closes home" 0.85s settle:
hip_right: extend 0
knee_right: flex 0
ankle_right: plantarflex 0
shoulders: flex 0
travel: 0 0
ground-lock: feet
cue "Close the feet together, back home and ready to repeat"
cue "Close the left foot and settle over both feet, completing the square"

repeat 4
70 changes: 48 additions & 22 deletions spec/examples/chasse.posecode
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,72 @@ posecode exercise "Chassé"
rig humanoid
pose start = standing

step "Reach & push" 0.8s settle:
hip_right: flex 30
knee_right: flex 25
ankle_right: plantarflex 25
step "Right foot reaches" 0.65s flow:
hip_right: abduct 26
knee_right: flex 20
ankle_right: plantarflex 24
ankle_left: plantarflex 20
shoulders: abduct 60
elbows: flex 18
travel: 0.5 0
travel: 0.34 0
pin: foot_left floor
reach: foot_right floor
cue "Reach the lead foot out and push off to travel sideways"
cue "Reach the right foot sideways and push away from the left leg"

step "Gallop close" 0.6s drive:
hip_right: flex 0
step "Left foot chases" 0.5s flow:
hip_right: abduct 0
knee_right: flex 0
ankle_right: plantarflex 0
ankle_left: plantarflex 0
travel: 0.9 0
ground-lock: feet
cue "Chase the trailing foot to the lead: feet meet in the air"
hip_left: abduct 18
knee_left: flex 22
ankle_left: plantarflex 24
travel: 0.66 0
pin: foot_right floor
cue "Let the left foot chase under the body without breaking the sideways flow"

step "Right foot reaches again" 0.65s flow:
hip_left: abduct 0
knee_left: flex 0
ankle_left: plantarflex 18
hip_right: abduct 26
knee_right: flex 20
ankle_right: plantarflex 24
travel: 1 0
pin: foot_left floor
reach: foot_right floor
cue "Reach right once more and finish the outward chassé"

step "Reach & push back" 0.8s settle:
hip_left: flex 30
knee_left: flex 25
ankle_left: plantarflex 25
step "Left foot reaches back" 0.65s flow:
hip_right: abduct 0
knee_right: flex 0
ankle_right: plantarflex 20
travel: 0.4 0
hip_left: abduct 26
knee_left: flex 20
ankle_left: plantarflex 24
travel: 0.66 0
pin: foot_right floor
reach: foot_left floor
cue "Change direction and travel back the other way"
cue "Reverse cleanly and reach the left foot back across the floor"

step "Gallop home" 0.6s drive:
hip_left: flex 0
step "Right foot chases" 0.5s flow:
hip_left: abduct 0
knee_left: flex 0
ankle_left: plantarflex 0
ankle_right: plantarflex 0
hip_right: abduct 18
knee_right: flex 22
ankle_right: plantarflex 24
travel: 0.34 0
pin: foot_left floor
cue "Let the right foot chase under the body on the return"

step "Left foot closes home" 0.7s settle:
hip_right: abduct 0
knee_right: flex 0
ankles: plantarflex 0
shoulders: abduct 0
elbows: flex 0
travel: 0 0
ground-lock: feet
cue "Close home, ready to chassé again"
cue "Close home over both feet and let the momentum resolve"

repeat 4
4 changes: 2 additions & 2 deletions spec/examples/dance-phrase.posecode
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ posecode exercise "Dance phrase (8-count)"
step "3-4 - demi-plié" 2s flow:
hips: flex 18
knees: flex 50
ankles: plantarflex 50
ankles: dorsiflex 12
shoulders: flex 28
elbows: flex 30
ground-lock: feet
Expand All @@ -29,7 +29,7 @@ posecode exercise "Dance phrase (8-count)"
ground-lock: feet
cue "Press up to relevé and lift the arms into a frame overhead"

step "7-8 - close" 2s settle:
step "7-8 - close" 2.2s settle:
ankles: plantarflex 0
hips: rotate-out 0
shoulders: flex 0
Expand Down
4 changes: 2 additions & 2 deletions spec/examples/demi-plie.posecode
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ posecode exercise "Demi-plié"
rig humanoid
pose start = standing

step "Plié" 2s flow:
step "Plié" 2.2s settle:
hips: flex 20
hips: rotate-out 30
knees: flex 55
Expand All @@ -13,7 +13,7 @@ posecode exercise "Demi-plié"
ground-lock: feet
cue "Turn out from the hips and bend the knees over the toes: heels down"

step "Straighten" 2s settle:
step "Straighten" 2.2s settle:
hips: flex 0
hips: rotate-out 0
knees: flex 0
Expand Down
Loading
Loading