From 43827d16f77b4d151f2b9e6ba120d5fffbfb18ba Mon Sep 17 00:00:00 2001 From: a-baran-orhan Date: Tue, 14 Jul 2026 13:32:18 +0300 Subject: [PATCH] Smooth root travel with Hermite interpolation and update choreography --- packages/posecode-render/src/timeline.ts | 62 ++++++++++++++--- packages/posecode-render/test/render.test.ts | 32 +++++++++ spec/examples/box-step.posecode | 57 +++++++++------- spec/examples/chasse.posecode | 70 ++++++++++++++------ spec/examples/dance-phrase.posecode | 4 +- spec/examples/demi-plie.posecode | 4 +- spec/examples/grapevine.posecode | 63 ++++++++++++++---- spec/examples/pirouette.posecode | 23 +++++-- spec/examples/port-de-bras.posecode | 14 +++- spec/examples/releve.posecode | 4 +- spec/examples/tendu.posecode | 4 +- spec/examples/waltz-box.posecode | 58 +++++++++++----- 12 files changed, 293 insertions(+), 102 deletions(-) diff --git a/packages/posecode-render/src/timeline.ts b/packages/posecode-render/src/timeline.ts index 852e729..ba1d8a2 100644 --- a/packages/posecode-render/src/timeline.ts +++ b/packages/posecode-render/src/timeline.ts @@ -106,6 +106,33 @@ const REST_MODE: Record = { 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( @@ -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 } : {}), diff --git a/packages/posecode-render/test/render.test.ts b/packages/posecode-render/test/render.test.ts index b9b45a5..ba8711c 100644 --- a/packages/posecode-render/test/render.test.ts +++ b/packages/posecode-render/test/render.test.ts @@ -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"', diff --git a/spec/examples/box-step.posecode b/spec/examples/box-step.posecode index f145b7d..d8ccd19 100644 --- a/spec/examples/box-step.posecode +++ b/spec/examples/box-step.posecode @@ -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 diff --git a/spec/examples/chasse.posecode b/spec/examples/chasse.posecode index 54ea937..0d93db9 100644 --- a/spec/examples/chasse.posecode +++ b/spec/examples/chasse.posecode @@ -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 diff --git a/spec/examples/dance-phrase.posecode b/spec/examples/dance-phrase.posecode index 6216a99..625c6a8 100644 --- a/spec/examples/dance-phrase.posecode +++ b/spec/examples/dance-phrase.posecode @@ -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 @@ -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 diff --git a/spec/examples/demi-plie.posecode b/spec/examples/demi-plie.posecode index 18f4ada..c0e7e0f 100644 --- a/spec/examples/demi-plie.posecode +++ b/spec/examples/demi-plie.posecode @@ -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 @@ -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 diff --git a/spec/examples/grapevine.posecode b/spec/examples/grapevine.posecode index ff179eb..84effd5 100644 --- a/spec/examples/grapevine.posecode +++ b/spec/examples/grapevine.posecode @@ -2,39 +2,76 @@ posecode exercise "Grapevine" rig humanoid pose start = standing - step "Step out" 0.7s flow: - hip_left: abduct 30 + step "Left step side" 0.65s flow: + hip_left: abduct 28 shoulders: abduct 40 - travel: 0.4 0 + travel: 0.3 0 pin: foot_right floor reach: foot_left floor - cue "Step the leading foot out to the side" + cue "Step the left foot to the side and begin travelling" - step "Cross behind" 0.7s flow: + step "Right crosses behind" 0.65s flow: hip_left: abduct 0 hip_right: rotate-in 20 knee_right: flex 25 ankle_right: plantarflex 25 - travel: 0.8 0 + travel: 0.6 0 pin: foot_left floor reach: foot_right floor - cue "Cross the trailing foot behind and keep travelling" + cue "Cross the right foot behind without stopping the sideways momentum" - step "Step out again" 0.7s flow: + step "Left step side again" 0.65s flow: hip_right: rotate-in 0 knee_right: flex 0 ankle_right: plantarflex 0 - hip_left: abduct 30 - travel: 1.2 0 + hip_left: abduct 28 + travel: 0.9 0 pin: foot_right floor reach: foot_left floor - cue "Step out to the side once more" + cue "Step left again and keep the shoulders quiet" - step "Return home" 1s settle: + step "Right touch" 0.55s flow: hip_left: abduct 0 + knee_right: flex 16 + ankle_right: plantarflex 20 + travel: 1 0 + ground-lock: feet + cue "Touch the right foot in lightly before reversing" + + step "Right step side" 0.65s flow: + knee_right: flex 0 + ankle_right: plantarflex 0 + hip_right: abduct 28 + travel: 0.7 0 + pin: foot_left floor + reach: foot_right floor + cue "Step the right foot to the side to travel back" + + step "Left crosses behind" 0.65s flow: + hip_right: abduct 0 + hip_left: rotate-in 20 + knee_left: flex 25 + ankle_left: plantarflex 25 + travel: 0.4 0 + pin: foot_right floor + reach: foot_left floor + cue "Cross the left foot behind and continue through the beat" + + step "Right step side again" 0.65s flow: + hip_left: rotate-in 0 + knee_left: flex 0 + ankle_left: plantarflex 0 + hip_right: abduct 28 + travel: 0.1 0 + pin: foot_left floor + reach: foot_right floor + cue "Step right and arrive back near the starting place" + + step "Close home" 0.65s settle: + hip_right: abduct 0 shoulders: abduct 0 travel: 0 0 ground-lock: feet - cue "Travel back the other way to the starting spot" + cue "Close the left foot under the body and settle the phrase" repeat 3 diff --git a/spec/examples/pirouette.posecode b/spec/examples/pirouette.posecode index 6f21f75..b63fe00 100644 --- a/spec/examples/pirouette.posecode +++ b/spec/examples/pirouette.posecode @@ -5,7 +5,7 @@ posecode exercise "Pirouette" step "Prep - plié" 1.2s flow: hips: rotate-out 25 knees: flex 45 - ankles: plantarflex 45 + ankles: dorsiflex 12 shoulders: flex 40 elbows: flex 35 ground-lock: feet @@ -24,14 +24,27 @@ posecode exercise "Pirouette" pin: foot_left floor cue "Push up to relevé, pull the arms in, and spin a full turn" - step "Land" 1s settle: + step "Land through plié" 0.9s settle: + hip_right: flex 0 + hip_right: rotate-out 0 + knee_right: flex 28 + ankle_right: dorsiflex 8 + knee_left: flex 28 + ankle_left: dorsiflex 8 + shoulders: abduct 55 + shoulders: flex 0 + elbows: flex 18 + ground-lock: feet + cue "Finish the turn and absorb the landing through a soft plié" + + step "Recover" 1.1s settle: hips: flex 0 hips: rotate-out 0 knees: flex 0 - ankles: plantarflex 0 - shoulders: flex 0 + ankles: dorsiflex 0 + shoulders: abduct 0 elbows: flex 0 ground-lock: feet - cue "Land softly through the feet, arms floating back to the sides" + cue "Lengthen to standing and let the arms float back to the sides" repeat 3 diff --git a/spec/examples/port-de-bras.posecode b/spec/examples/port-de-bras.posecode index af47938..e67c951 100644 --- a/spec/examples/port-de-bras.posecode +++ b/spec/examples/port-de-bras.posecode @@ -16,18 +16,26 @@ posecode stretch "Port de bras" ground-lock: feet cue "Open the arms out to the sides: second position, wrists lifted" - step "Fifth en haut" 2.2s flow: + step "Fifth en haut" 2.4s flow: shoulders: abduct 0 shoulders: flex 160 elbows: flex 20 ground-lock: feet cue "Float the arms into a rounded frame overhead: fifth position" - step "Lower" 2s settle: + step "Open down through second" 2.2s flow: + shoulders: flex 0 + shoulders: abduct 85 + elbows: flex 16 + ground-lock: feet + cue "Open from overhead through a wide second position, shoulders released" + + step "Close low" 2s settle: + shoulders: abduct 0 shoulders: flex 0 elbows: flex 0 hips: rotate-out 0 ground-lock: feet - cue "Lower through second back to the start, eyes following the hands" + cue "Float the arms down to the start, eyes following the hands" repeat 3 diff --git a/spec/examples/releve.posecode b/spec/examples/releve.posecode index aee5f32..44eb079 100644 --- a/spec/examples/releve.posecode +++ b/spec/examples/releve.posecode @@ -2,7 +2,7 @@ posecode exercise "Relevé" rig humanoid pose start = standing - step "Rise" 1.6s settle: + step "Rise" 1.8s settle: ankles: plantarflex 30 hips: rotate-out 25 knees: extend 0 @@ -12,7 +12,7 @@ posecode exercise "Relevé" ground-lock: feet cue "Turn out and rise onto the balls of the feet: lengthen up tall" - step "Lower" 1.8s drive: + step "Lower" 2s settle: ankles: plantarflex 0 hips: rotate-out 0 shoulders: abduct 0 diff --git a/spec/examples/tendu.posecode b/spec/examples/tendu.posecode index 2dce22a..3857154 100644 --- a/spec/examples/tendu.posecode +++ b/spec/examples/tendu.posecode @@ -2,7 +2,7 @@ posecode exercise "Tendu" rig humanoid pose start = standing - step "Point front" 1.6s flow: + step "Brush and point" 1.8s settle: hip_right: flex 22 hip_right: rotate-out 30 knee_right: extend 0 @@ -12,7 +12,7 @@ posecode exercise "Tendu" pin: foot_left floor cue "Brush the right foot forward to a fully pointed tendu, leg turned out" - step "Close" 1.6s settle: + step "Close through the floor" 1.8s settle: hip_right: flex 0 hip_right: rotate-out 0 ankle_right: plantarflex 0 diff --git a/spec/examples/waltz-box.posecode b/spec/examples/waltz-box.posecode index 05577b2..65ef9b7 100644 --- a/spec/examples/waltz-box.posecode +++ b/spec/examples/waltz-box.posecode @@ -2,40 +2,62 @@ posecode stretch "Waltz box step" rig humanoid pose start = standing - step "1 - forward, rise" 1s flow: - hip_right: flex 25 - knee_right: flex 20 - ankles: plantarflex 12 + step "1 - right foot forward" 0.95s flow: + hip_right: flex 24 + knee_right: flex 24 + ankle_right: plantarflex 18 shoulders: abduct 55 elbows: flex 20 - travel: 0 0.4 + travel: 0 0.32 pin: foot_left floor - cue "Step forward onto the right foot and rise, arms in a soft frame" + cue "Step forward onto the right foot and begin the waltz rise" - step "2 - side, lower" 1s flow: + step "2 - left foot side" 0.95s flow: hip_right: flex 0 knee_right: flex 0 + ankle_right: plantarflex 0 + hip_left: abduct 22 + knee_left: flex 16 + ankles: plantarflex 22 + travel: -0.32 0.32 + pin: foot_right floor + cue "Step left to the side and continue rising through count two" + + step "3 - close and lower" 1.05s flow: + hip_left: abduct 0 + knee_left: flex 0 ankles: plantarflex 0 - travel: -0.4 0.4 + travel: -0.32 0.32 ground-lock: feet - cue "Side step to the corner and lower through the heels" + cue "Close the right foot to the left and lower softly through count three" - step "3 - back, rise" 1s flow: - hip_left: flex 25 - knee_left: flex 20 - ankles: plantarflex 12 - travel: -0.4 0 + step "4 - left foot back" 0.95s flow: + hip_left: extend 18 + knee_left: flex 24 + ankle_left: plantarflex 18 + travel: -0.32 0 pin: foot_right floor - cue "Step back onto the left foot and rise again" + cue "Step back onto the left foot and begin the second rise" - step "4 - close home" 1s settle: - hip_left: flex 0 + step "5 - right foot side" 0.95s flow: + hip_left: extend 0 knee_left: flex 0 + ankle_left: plantarflex 0 + hip_right: abduct 22 + knee_right: flex 16 + ankles: plantarflex 22 + travel: 0 0 + pin: foot_left floor + cue "Step right to the side, staying buoyant through count five" + + step "6 - close and lower" 1.05s 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 to the start, completing the box" + cue "Close the left foot and lower with control to complete the six-count box" repeat 3