88 */
99
1010import * as THREE from "three" ;
11- import type { PosecodeIR , ReachTarget , PinTarget } from "posecode-parser" ;
11+ import type { PosecodeIR , ReachTarget , PinTarget , TimingMode } from "posecode-parser" ;
1212import { poseFor , type PoseSpec } from "./poses.js" ;
13+ import { squad , squadControl } from "./squad.js" ;
1314
1415const DEG = Math . PI / 180 ;
1516
1617type EulerDegTuple = [ number , number , number ] ;
17- type Easing = "linear" | "ease-in" | "ease-out" | "ease-in-out" ;
1818
1919interface Keyframe {
2020 time : number ;
2121 name : string ;
2222 cue ?: string ;
23- easing : Easing ;
23+ easing : TimingMode ;
2424 quats : Map < string , THREE . Quaternion > ;
2525 groundLock : string [ ] ;
2626 reaches : ReachTarget [ ] ;
@@ -71,11 +71,27 @@ function eulerToQuat([x, y, z]: EulerDegTuple): THREE.Quaternion {
7171 ) ;
7272}
7373
74- const EASE : Record < Easing , ( t : number ) => number > = {
74+ /**
75+ * Per-mode remap of the normalized segment parameter (arrival shaping). `flow`
76+ * and `linear` are even; `settle`/`snap` decelerate into rest; `drive`
77+ * accelerates from rest. The spline (squad) carries velocity across keyframes;
78+ * this only shapes the timing within a segment.
79+ */
80+ const MODE_EASE : Record < TimingMode , ( t : number ) => number > = {
81+ flow : ( t ) => t ,
82+ settle : ( t ) => 1 - ( 1 - t ) * ( 1 - t ) ,
83+ drive : ( t ) => t * t ,
84+ snap : ( t ) => 1 - ( 1 - t ) * ( 1 - t ) * ( 1 - t ) ,
7585 linear : ( t ) => t ,
76- "ease-in" : ( t ) => t * t ,
77- "ease-out" : ( t ) => 1 - ( 1 - t ) * ( 1 - t ) ,
78- "ease-in-out" : ( t ) => t * t * ( 3 - 2 * t ) ,
86+ } ;
87+
88+ /** A keyframe is a rest-point (zero boundary velocity) for these modes. */
89+ const REST_MODE : Record < TimingMode , boolean > = {
90+ flow : false ,
91+ settle : true ,
92+ drive : false ,
93+ snap : true ,
94+ linear : false ,
7995} ;
8096
8197export function buildTimeline ( ir : PosecodeIR ) : BuiltTimeline {
@@ -96,7 +112,7 @@ export function buildTimeline(ir: PosecodeIR): BuiltTimeline {
96112 keyframes . push ( {
97113 time : 0 ,
98114 name : ir . startPose ?? "start" ,
99- easing : "linear " ,
115+ easing : "flow " ,
100116 quats : snapshot ( curr ) ,
101117 groundLock : [ ] ,
102118 reaches : [ ] ,
@@ -139,7 +155,7 @@ export function buildTimeline(ir: PosecodeIR): BuiltTimeline {
139155 keyframes . push ( {
140156 time : t ,
141157 name : "reset" ,
142- easing : "ease-in-out " ,
158+ easing : "flow " ,
143159 quats : snapshot ( new Map ( baseJoints ) ) ,
144160 groundLock : [ ] ,
145161 reaches : [ ] ,
@@ -179,23 +195,40 @@ export function buildTimeline(ir: PosecodeIR): BuiltTimeline {
179195 travelExtent,
180196 sample ( time , bones ) {
181197 const tt = duration > 0 ? ( ( time % duration ) + duration ) % duration : 0 ;
198+ let i = 0 ;
182199 let a = keyframes [ 0 ] ! ;
183200 let b = keyframes [ keyframes . length - 1 ] ! ;
184- for ( let i = 0 ; i < keyframes . length - 1 ; i ++ ) {
185- if ( tt >= keyframes [ i ] ! . time && tt < keyframes [ i + 1 ] ! . time ) {
186- a = keyframes [ i ] ! ;
187- b = keyframes [ i + 1 ] ! ;
201+ for ( let k = 0 ; k < keyframes . length - 1 ; k ++ ) {
202+ if ( tt >= keyframes [ k ] ! . time && tt < keyframes [ k + 1 ] ! . time ) {
203+ i = k ;
204+ a = keyframes [ k ] ! ;
205+ b = keyframes [ k + 1 ] ! ;
188206 break ;
189207 }
190208 }
191209 const span = Math . max ( 1e-6 , b . time - a . time ) ;
192210 const local = THREE . MathUtils . clamp ( ( tt - a . time ) / span , 0 , 1 ) ;
193- const eased = EASE [ b . easing ] ( local ) ;
211+ const eased = MODE_EASE [ b . easing ] ( local ) ;
194212
213+ // Neighbors for the squad control quaternions (clamp at the ends → the
214+ // segment endpoint itself, giving a one-sided tangent).
215+ const kPrev = keyframes [ Math . max ( 0 , i - 1 ) ] ! ;
216+ const kNext = keyframes [ Math . min ( keyframes . length - 1 , i + 2 ) ] ! ;
195217 for ( const bone of bonesUsed ) {
196218 const node = bones . get ( bone ) ;
197219 if ( ! node ) continue ;
198- node . quaternion . slerpQuaternions ( a . quats . get ( bone ) ! , b . quats . get ( bone ) ! , eased ) ;
220+ const q0 = a . quats . get ( bone ) ! ;
221+ const q1 = b . quats . get ( bone ) ! ;
222+ // A rest-point keyframe uses its own value as the control (zero tangent
223+ // → the spline comes to / leaves from rest there); otherwise the
224+ // Shoemake control from the neighboring keyframe carries velocity.
225+ const s0 = REST_MODE [ a . easing ]
226+ ? q0 . clone ( )
227+ : squadControl ( kPrev . quats . get ( bone ) ! , q0 , q1 ) ;
228+ const s1 = REST_MODE [ b . easing ]
229+ ? q1 . clone ( )
230+ : squadControl ( q0 , q1 , kNext . quats . get ( bone ) ! ) ;
231+ squad ( q0 , s0 , s1 , q1 , eased , node . quaternion ) ;
199232 }
200233 // Root facing/position: linear interpolation of the raw values so a large
201234 // turn (e.g. 360°) sweeps the whole way round rather than taking a short
0 commit comments