@@ -18,6 +18,7 @@ const PROJECT_SIGNATURE_META = "hyperframes-project-signature";
1818const GSAP_CDN_VERSION = "3.15.0" ;
1919const GSAP_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${ GSAP_CDN_VERSION } /dist/gsap.min.js"></script>` ;
2020const GSAP_CUSTOM_EASE_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${ GSAP_CDN_VERSION } /dist/CustomEase.min.js"></script>` ;
21+ const GSAP_MOTION_PATH_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${ GSAP_CDN_VERSION } /dist/MotionPathPlugin.min.js"></script>` ;
2122
2223function resolveProjectSignature ( adapter : StudioApiAdapter , projectDir : string ) : string {
2324 return adapter . getProjectSignature ?.( projectDir ) ?? createProjectSignature ( projectDir ) ;
@@ -86,6 +87,42 @@ function htmlHasCustomEase(html: string): boolean {
8687 ) ;
8788}
8889
90+ // A composition that drives motion via GSAP's `motionPath` (e.g. a studio-created
91+ // motion path written into the single-source timeline) needs MotionPathPlugin
92+ // registered before the timeline first renders — otherwise the initial seek
93+ // throws "Invalid property motionPath ... Missing plugin?". Detect it anywhere in
94+ // the bundle (the plugin registers globally, so sub-composition usage counts too).
95+ function htmlUsesMotionPath ( html : string ) : boolean {
96+ return / m o t i o n P a t h \s * [: { ] / . test ( html ) ;
97+ }
98+
99+ function htmlHasMotionPathPlugin ( html : string ) : boolean {
100+ return (
101+ / < s c r i p t \b [ ^ > ] * s r c = [ " ' ] [ ^ " ' ] * M o t i o n P a t h P l u g i n / i. test ( html ) ||
102+ / \b w i n d o w \. M o t i o n P a t h P l u g i n \b / . test ( html ) ||
103+ / \b M o t i o n P a t h P l u g i n \s * = \s * / . test ( html )
104+ ) ;
105+ }
106+
107+ function injectMotionPathPluginIfNeeded ( html : string ) : string {
108+ if ( ! htmlUsesMotionPath ( html ) || htmlHasMotionPathPlugin ( html ) ) return html ;
109+ // The plugin registers onto an already-loaded gsap, so it must come AFTER the
110+ // core gsap script — which often lives at body-end, not <head>. Insert it
111+ // directly after the gsap script tag; only fall back to <head> if none is found
112+ // (e.g. gsap is inlined).
113+ const gsapScript = / < s c r i p t \b [ ^ > ] * \b s r c = [ " ' ] [ ^ " ' ] * \/ g s a p ( \. m i n ) ? \. j s [ " ' ] [ ^ > ] * > \s * < \/ s c r i p t > / i;
114+ const match = html . match ( gsapScript ) ;
115+ if ( match ) {
116+ // Match the plugin version to the composition's own gsap so the plugin
117+ // registers cleanly (a minor-version skew triggers a GSAP compatibility warning).
118+ const version = match [ 0 ] . match ( / g s a p @ ( [ \d . ] + ) / ) ?. [ 1 ] ?? GSAP_CDN_VERSION ;
119+ const pluginTag = `<script src="https://cdn.jsdelivr.net/npm/gsap@${ version } /dist/MotionPathPlugin.min.js"></script>` ;
120+ const end = html . indexOf ( match [ 0 ] ) + match [ 0 ] . length ;
121+ return html . slice ( 0 , end ) + "\n" + pluginTag + html . slice ( end ) ;
122+ }
123+ return injectScriptTagIntoHead ( html , GSAP_MOTION_PATH_CDN_SCRIPT ) ;
124+ }
125+
89126function injectStudioMotionDependencies ( html : string , manifestContent : string ) : string {
90127 const manifest = parseStudioMotionManifestContent ( manifestContent ) ;
91128 if ( ! manifest . hasMotion ) return html ;
@@ -149,8 +186,10 @@ function injectStudioPreviewAugmentations(
149186 activeCompositionPath : string ,
150187) : string {
151188 return injectStudioMotionScript (
152- injectGsapCdnFallback (
153- injectProjectSignature ( html , resolveProjectSignature ( adapter , projectDir ) ) ,
189+ injectMotionPathPluginIfNeeded (
190+ injectGsapCdnFallback (
191+ injectProjectSignature ( html , resolveProjectSignature ( adapter , projectDir ) ) ,
192+ ) ,
154193 ) ,
155194 projectDir ,
156195 activeCompositionPath ,
0 commit comments