@@ -16,6 +16,7 @@ import {
1616} from "./gsapParserAcorn.js" ;
1717import { classifyPropertyGroup } from "./gsapConstants.js" ;
1818import type { PropertyGroupName } from "./gsapConstants.js" ;
19+ import type { SplitAnimationsOptions , SplitAnimationsResult } from "./gsapParser.js" ;
1920import * as acornWalk from "acorn-walk" ;
2021
2122// ── Code generation helpers ──────────────────────────────────────────────────
@@ -1172,3 +1173,169 @@ export function removeLabelFromScript(script: string, name: string): string {
11721173 }
11731174 return ms . toString ( ) ;
11741175}
1176+
1177+ // ── splitAnimationsInScript helpers ──────────────────────────────────────────
1178+
1179+ /** Overwrite the selector (first arg) of a tween call. */
1180+ function updateAnimationSelectorInScript (
1181+ script : string ,
1182+ animationId : string ,
1183+ newSelector : string ,
1184+ ) : string {
1185+ const parsed = parseGsapScriptAcornForWrite ( script ) ;
1186+ if ( ! parsed ) return script ;
1187+ const target = parsed . located . find ( ( l ) => l . id === animationId ) ;
1188+ if ( ! target ) return script ;
1189+ const selectorArg = target . call . node . arguments ?. [ 0 ] ;
1190+ if ( ! selectorArg ) return script ;
1191+ const ms = new MagicString ( script ) ;
1192+ ms . overwrite ( selectorArg . start , selectorArg . end , JSON . stringify ( newSelector ) ) ;
1193+ return ms . toString ( ) ;
1194+ }
1195+
1196+ /**
1197+ * Insert a `tl.set()` call immediately after the timeline declaration
1198+ * (before existing tweens) to establish inherited state on a new element.
1199+ */
1200+ function insertInheritedStateSetInScript (
1201+ script : string ,
1202+ selector : string ,
1203+ position : number ,
1204+ properties : Record < string , number | string > ,
1205+ ) : string {
1206+ const parsed = parseGsapScriptAcornForWrite ( script ) ;
1207+ if ( ! parsed ) return script ;
1208+ const props = Object . entries ( properties )
1209+ . map ( ( [ k , v ] ) => `${ safeKey ( k ) } : ${ valueToCode ( v ) } ` )
1210+ . join ( ", " ) ;
1211+ const code = `${ parsed . timelineVar } .set(${ JSON . stringify ( selector ) } , { ${ props } }, ${ position } );` ;
1212+ const ms = new MagicString ( script ) ;
1213+ const tlDecl = findTimelineDeclarationStatement ( parsed . ast , parsed . timelineVar ) ;
1214+ if ( tlDecl ) {
1215+ ms . appendLeft ( tlDecl . end , "\n" + code ) ;
1216+ } else if ( parsed . located . length > 0 ) {
1217+ const firstCall = parsed . located [ 0 ] ! . call ;
1218+ const exprStmt = findEnclosingExpressionStatement ( firstCall . ancestors ) ;
1219+ const insertAt = exprStmt ?. start ?? firstCall . node . start ;
1220+ ms . prependLeft ( insertAt , code + "\n" ) ;
1221+ } else {
1222+ ms . append ( "\n" + code ) ;
1223+ }
1224+ return ms . toString ( ) ;
1225+ }
1226+
1227+ // fallow-ignore-next-line complexity
1228+ export function splitAnimationsInScript (
1229+ script : string ,
1230+ opts : SplitAnimationsOptions ,
1231+ ) : SplitAnimationsResult {
1232+ const parsed = parseGsapScriptAcornForWrite ( script ) ;
1233+ if ( ! parsed ) return { script, skippedSelectors : [ ] } ;
1234+
1235+ const originalSelector = `#${ opts . originalId } ` ;
1236+ const newSelector = `#${ opts . newId } ` ;
1237+
1238+ const animations = parsed . located . map ( ( l ) => l . animation ) ;
1239+ const skippedSelectors : string [ ] = [ ] ;
1240+
1241+ for ( const a of animations ) {
1242+ if ( a . targetSelector !== originalSelector && a . targetSelector . includes ( opts . originalId ) ) {
1243+ skippedSelectors . push ( a . targetSelector ) ;
1244+ }
1245+ }
1246+
1247+ const matching = animations . filter ( ( a ) => a . targetSelector === originalSelector ) ;
1248+ if ( matching . length === 0 ) return { script, skippedSelectors } ;
1249+
1250+ let result = script ;
1251+ const newElementStart = opts . splitTime ;
1252+ const inheritedProps : Record < string , number | string > = { } ;
1253+
1254+ // Reverse iteration: updateAnimationSelectorInScript mutates selectors which
1255+ // can shift count-based ID suffixes for later animations.
1256+ for ( let i = matching . length - 1 ; i >= 0 ; i -- ) {
1257+ const anim = matching [ i ] ! ;
1258+ const pos = typeof anim . position === "number" ? anim . position : 0 ;
1259+ const dur = anim . duration ?? 0 ;
1260+ const animEnd = pos + dur ;
1261+
1262+ if ( anim . keyframes ) {
1263+ if ( pos >= opts . splitTime ) {
1264+ result = updateAnimationSelectorInScript ( result , anim . id , newSelector ) ;
1265+ } else if ( animEnd > opts . splitTime ) {
1266+ skippedSelectors . push ( `${ originalSelector } (keyframes spanning split)` ) ;
1267+ const kfs = anim . keyframes . keyframes ;
1268+ for ( const kf of kfs ) {
1269+ const kfTime = pos + ( kf . percentage / 100 ) * dur ;
1270+ if ( kfTime <= opts . splitTime ) {
1271+ for ( const [ k , v ] of Object . entries ( kf . properties ) ) {
1272+ inheritedProps [ k ] = v ;
1273+ }
1274+ }
1275+ }
1276+ } else {
1277+ const kfs = anim . keyframes . keyframes ;
1278+ if ( kfs . length > 0 ) {
1279+ for ( const [ k , v ] of Object . entries ( kfs [ kfs . length - 1 ] ! . properties ) ) {
1280+ inheritedProps [ k ] = v ;
1281+ }
1282+ }
1283+ }
1284+ continue ;
1285+ }
1286+
1287+ if ( animEnd <= opts . splitTime ) {
1288+ for ( const [ k , v ] of Object . entries ( anim . properties ) ) {
1289+ inheritedProps [ k ] = v ;
1290+ }
1291+ continue ;
1292+ }
1293+
1294+ if ( pos >= opts . splitTime ) {
1295+ result = updateAnimationSelectorInScript ( result , anim . id , newSelector ) ;
1296+ continue ;
1297+ }
1298+
1299+ // Spans the split — linear interpolation to compute mid-values.
1300+ const progress = dur > 0 ? ( opts . splitTime - pos ) / dur : 0 ;
1301+ const fromSource = anim . fromProperties ?? inheritedProps ;
1302+ const midProps : Record < string , number | string > = { } ;
1303+ for ( const [ k , v ] of Object . entries ( anim . properties ) ) {
1304+ if ( typeof v !== "number" ) {
1305+ midProps [ k ] = v ;
1306+ continue ;
1307+ }
1308+ const fromVal = typeof fromSource [ k ] === "number" ? ( fromSource [ k ] as number ) : 0 ;
1309+ midProps [ k ] = fromVal + ( v - fromVal ) * progress ;
1310+ }
1311+
1312+ const firstHalfDuration = opts . splitTime - pos ;
1313+ result = updateAnimationInScript ( result , anim . id , {
1314+ duration : firstHalfDuration ,
1315+ properties : midProps ,
1316+ } ) ;
1317+
1318+ const secondHalfDuration = animEnd - opts . splitTime ;
1319+ const addResult = addAnimationToScript ( result , {
1320+ targetSelector : newSelector ,
1321+ method : "fromTo" ,
1322+ position : newElementStart ,
1323+ duration : secondHalfDuration ,
1324+ properties : { ...anim . properties } ,
1325+ fromProperties : { ...midProps } ,
1326+ ease : anim . ease ,
1327+ extras : anim . extras ,
1328+ } ) ;
1329+ result = addResult . script ;
1330+
1331+ for ( const [ k , v ] of Object . entries ( midProps ) ) {
1332+ inheritedProps [ k ] = v ;
1333+ }
1334+ }
1335+
1336+ if ( Object . keys ( inheritedProps ) . length > 0 ) {
1337+ result = insertInheritedStateSetInScript ( result , newSelector , newElementStart , inheritedProps ) ;
1338+ }
1339+
1340+ return { script : result , skippedSelectors } ;
1341+ }
0 commit comments