@@ -22,10 +22,11 @@ interface RuntimeTween {
2222interface RuntimeTimeline {
2323 getChildren ?: ( deep : boolean ) => RuntimeTween [ ] ;
2424 duration ?: ( ) => number ;
25+ time ?: ( ) => number ;
2526}
2627
2728type Pct = { percentage : number ; properties : Record < string , number | string > } ;
28- type ReadTween = { keyframes : Pct [ ] ; easeEach ?: string ; arcPath ?: ArcPathConfig } ;
29+ export type ReadTween = { keyframes : Pct [ ] ; easeEach ?: string ; arcPath ?: ArcPathConfig } ;
2930
3031export interface RuntimeKeyframeEntry {
3132 keyframes : Pct [ ] ;
@@ -160,7 +161,11 @@ export function readRuntimeKeyframes(
160161) : ReadTween | null {
161162 const timelines = timelinesOf ( iframe ) ;
162163 if ( ! timelines ) return null ;
163- const tlId = compositionId || Object . keys ( timelines ) [ 0 ] ;
164+ // Skip non-timeline markers (e.g. the studio's `__proxied` flag) when no
165+ // explicit composition id is given — picking those yields no getChildren.
166+ const tlId =
167+ compositionId ||
168+ Object . keys ( timelines ) . find ( ( k ) => typeof timelines [ k ] ?. getChildren === "function" ) ;
164169 if ( ! tlId ) return null ;
165170 const timeline = timelines [ tlId ] ;
166171 if ( ! timeline ?. getChildren ) return null ;
@@ -173,12 +178,32 @@ export function readRuntimeKeyframes(
173178 }
174179 if ( ! targetEl ) return null ;
175180
181+ // The element can have MORE THAN ONE keyframed tween at disjoint time ranges
182+ // (e.g. two non-overlapping gesture recordings → two separate `to()`s). The
183+ // overlay must draw the segment under the PLAYHEAD, not blindly the first one
184+ // — otherwise recording a second gesture leaves the path stuck on the first.
185+ const now = typeof timeline . time === "function" ? timeline . time ( ) : null ;
186+ let firstRead : ReadTween | null = null ;
176187 for ( const tween of timeline . getChildren ( true ) ) {
177188 if ( ! tween . vars || ! matchesElement ( tween , targetEl ) ) continue ;
189+ // Skip zero-duration tweens (`tl.set(...)`, incl. the studio position-hold
190+ // `data:"hf-hold"`). They sit before the real keyframed tween and otherwise
191+ // shadow it — `readTween` falls back to a degenerate 2-point flat path from
192+ // the set's values, hiding the actual multi-keyframe motion.
193+ const dur = typeof tween . duration === "function" ? tween . duration ( ) : 0 ;
194+ if ( ! ( dur > 0 ) ) continue ;
178195 const read = readTween ( tween . vars ) ;
179- if ( read ) return read ;
196+ if ( ! read ) continue ;
197+ if ( firstRead === null ) firstRead = read ;
198+ // Prefer the tween whose [start, start+dur] contains the playhead.
199+ if ( now != null ) {
200+ const start = typeof tween . startTime === "function" ? tween . startTime ( ) : 0 ;
201+ if ( now >= start - 1e-3 && now <= start + dur + 1e-3 ) return read ;
202+ }
180203 }
181- return null ;
204+ // Playhead outside every tween's range (or timeline has no clock): the element
205+ // still has motion, so fall back to the first keyframed tween.
206+ return firstRead ;
182207}
183208
184209/** Convert tween-relative keyframes to clip-relative % using the element's clip dims. */
@@ -217,9 +242,12 @@ function addScanEntry(
217242 clipById ?: ClipDims ,
218243) : void {
219244 if ( ! tween . targets || ! tween . vars ) return ;
245+ const { start, duration } = tweenTiming ( tween ) ;
246+ // Skip zero-duration sets/holds — they shadow the real keyframed tween (see
247+ // readRuntimeKeyframes).
248+ if ( ! ( duration > 0 ) ) return ;
220249 const read = readTween ( tween . vars ) ;
221250 if ( ! read ) return ;
222- const { start, duration } = tweenTiming ( tween ) ;
223251 for ( const target of tween . targets ( ) ) {
224252 const id = ( target as HTMLElement ) . id ;
225253 if ( id && ! result . has ( id ) ) result . set ( id , buildEntry ( read , start , duration , clipById ?. get ( id ) ) ) ;
0 commit comments