@@ -30,11 +30,14 @@ function studioPositionSeekReapplyRuntime(): void {
3030 const ROTATION_ATTR = "data-hf-studio-rotation" ;
3131 const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate" ;
3232 const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate" ;
33+ const MOTION_ATTR = "data-hf-studio-motion" ;
34+ const MOTION_TL_KEY = "studio-motion" ;
3335 const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped" ;
3436
3537 if (
3638 ! document . querySelector ( "[" + PATH_OFFSET_ATTR + '="true"]' ) &&
37- ! document . querySelector ( "[" + ROTATION_ATTR + '="true"]' )
39+ ! document . querySelector ( "[" + ROTATION_ATTR + '="true"]' ) &&
40+ ! document . querySelector ( "[" + MOTION_ATTR + "]" )
3841 )
3942 return ;
4043
@@ -77,6 +80,87 @@ function studioPositionSeekReapplyRuntime(): void {
7780 return "calc(" + original + " + " + rotationValue + ")" ;
7881 } ;
7982
83+ let lastSeekTime = 0 ;
84+
85+ const finiteNum = ( v : unknown ) : number | null =>
86+ typeof v === "number" && Number . isFinite ( v ) ? v : null ;
87+
88+ const reapplyMotionTimeline = ( ) : void => {
89+ const motionEls = document . querySelectorAll ( "[" + MOTION_ATTR + "]" ) ;
90+ if ( motionEls . length === 0 ) return ;
91+ const win = window as Window & {
92+ gsap ?: {
93+ timeline ?: ( opts : Record < string , unknown > ) => Record < string , unknown > ;
94+ set ?: ( el : HTMLElement , vars : Record < string , unknown > ) => void ;
95+ registerPlugin ?: ( plugin : unknown ) => void ;
96+ } ;
97+ CustomEase ?: { create ?: ( id : string , data : string ) => void } ;
98+ __timelines ?: Record < string , Record < string , unknown > > ;
99+ } ;
100+ const gsap = win . gsap ;
101+ if ( ! gsap || typeof gsap . timeline !== "function" ) return ;
102+ win . __timelines = win . __timelines || { } ;
103+ const existing = win . __timelines [ MOTION_TL_KEY ] ;
104+ if ( existing && typeof existing . kill === "function" ) ( existing . kill as ( ) => void ) ( ) ;
105+ const tl = gsap . timeline ( { paused : true , defaults : { overwrite : "auto" } } ) ;
106+ const fromTo = tl . fromTo as (
107+ el : HTMLElement ,
108+ from : Record < string , unknown > ,
109+ to : Record < string , unknown > ,
110+ pos : number ,
111+ ) => void ;
112+ if ( typeof fromTo !== "function" ) return ;
113+ let applied = 0 ;
114+ for ( let i = 0 ; i < motionEls . length ; i ++ ) {
115+ const el = motionEls [ i ] as HTMLElement ;
116+ if ( ! ( el instanceof HTMLElement ) ) continue ;
117+ const json = el . getAttribute ( MOTION_ATTR ) ;
118+ if ( ! json ) continue ;
119+ try {
120+ const m = JSON . parse ( json ) as Record < string , unknown > ;
121+ const start = finiteNum ( m . start ) ;
122+ const duration = finiteNum ( m . duration ) ;
123+ if ( start == null || duration == null || duration <= 0 ) continue ;
124+ const ease = typeof m . ease === "string" ? m . ease : "none" ;
125+ const from = ( m . from && typeof m . from === "object" ? m . from : { } ) as Record <
126+ string ,
127+ unknown
128+ > ;
129+ const to = ( m . to && typeof m . to === "object" ? m . to : { } ) as Record < string , unknown > ;
130+ const customEase = m . customEase as { id ?: string ; data ?: string } | null | undefined ;
131+ let resolvedEase = ease ;
132+ if ( customEase ?. id && customEase ?. data && win . CustomEase ?. create ) {
133+ try {
134+ gsap . registerPlugin ?.( win . CustomEase ) ;
135+ win . CustomEase . create ( customEase . id , customEase . data ) ;
136+ resolvedEase = customEase . id ;
137+ } catch {
138+ /* use default ease */
139+ }
140+ }
141+ fromTo . call (
142+ tl ,
143+ el ,
144+ { ...from } ,
145+ { ...to , duration, ease : resolvedEase , overwrite : "auto" , immediateRender : false } ,
146+ start ,
147+ ) ;
148+ applied += 1 ;
149+ } catch {
150+ /* malformed JSON — skip */
151+ }
152+ }
153+ if ( applied === 0 ) {
154+ if ( typeof ( tl as { kill ?: ( ) => void } ) . kill === "function" )
155+ ( tl as { kill : ( ) => void } ) . kill ( ) ;
156+ return ;
157+ }
158+ win . __timelines [ MOTION_TL_KEY ] = tl ;
159+ if ( typeof tl . pause === "function" ) ( tl . pause as ( ) => void ) ( ) ;
160+ if ( typeof tl . totalTime === "function" )
161+ ( tl . totalTime as ( t : number , s : boolean ) => void ) ( lastSeekTime , false ) ;
162+ } ;
163+
80164 const reapplyAll = ( ) : void => {
81165 const offsetEls = document . querySelectorAll ( "[" + PATH_OFFSET_ATTR + '="true"]' ) ;
82166 for ( let i = 0 ; i < offsetEls . length ; i ++ ) {
@@ -104,6 +188,7 @@ function studioPositionSeekReapplyRuntime(): void {
104188 el . style . setProperty ( "rotate" , composeRotation ( el , "var(" + ROTATION_PROP + ", 0deg)" ) ) ;
105189 }
106190 }
191+ reapplyMotionTimeline ( ) ;
107192 } ;
108193
109194 const runtimeWindow = window as Window & {
@@ -139,6 +224,7 @@ function studioPositionSeekReapplyRuntime(): void {
139224 return true ;
140225 }
141226 const wrapped = function ( this : unknown , time : number ) : unknown {
227+ lastSeekTime = typeof time === "number" && Number . isFinite ( time ) ? Math . max ( 0 , time ) : 0 ;
142228 const result = seek . call ( this , time ) ;
143229 reapplyAll ( ) ;
144230 return result ;
0 commit comments