@hyperframes/core 0.6.72 → 0.6.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@
11
11
  import * as recast from "recast";
12
12
  import { parse as babelParse } from "@babel/parser";
13
13
  export { serializeGsapAnimations, getAnimationsForElementId, validateCompositionGsap, keyframesToGsapAnimations, gsapAnimationsToKeyframes, SUPPORTED_PROPS, SUPPORTED_EASES, } from "./gsapSerialize";
14
+ export { generateSpringEaseData, SPRING_PRESETS } from "./springEase";
14
15
  const GSAP_METHODS = new Set(["set", "to", "from", "fromTo"]);
15
16
  function parseScript(script) {
16
17
  return recast.parse(script, {
@@ -334,11 +335,7 @@ function findAllTweenCalls(ast, timelineVar, scope, targetBindings) {
334
335
  this.traverse(path);
335
336
  return;
336
337
  }
337
- const selectorValue = resolveTargetSelector(args[0], path, scope, targetBindings);
338
- if (!selectorValue) {
339
- this.traverse(path);
340
- return;
341
- }
338
+ const selectorValue = resolveTargetSelector(args[0], path, scope, targetBindings) ?? "__unresolved__";
342
339
  if (method === "fromTo") {
343
340
  results.push({
344
341
  path,
@@ -369,7 +366,7 @@ function findAllTweenCalls(ast, timelineVar, scope, targetBindings) {
369
366
  /** Keys that are stored on dedicated GsapAnimation fields (not in properties/extras). */
370
367
  const BUILTIN_VAR_KEYS = new Set(["duration", "ease", "delay"]);
371
368
  /** Keys that are never preserved (callbacks / advanced patterns). */
372
- const DROPPED_VAR_KEYS = new Set(["keyframes", "onComplete", "onStart", "onUpdate", "onRepeat"]);
369
+ const DROPPED_VAR_KEYS = new Set(["onComplete", "onStart", "onUpdate", "onRepeat"]);
373
370
  /** Keys that belong in `extras` — non-editable GSAP config that must survive round-trips. */
374
371
  const EXTRAS_KEYS = new Set([
375
372
  "stagger",
@@ -385,27 +382,234 @@ const EXTRAS_KEYS = new Set([
385
382
  * Returns the printed source of the value node, suitable for verbatim re-emission.
386
383
  */
387
384
  function extractRawPropertySource(varsArgNode, key) {
385
+ const node = findPropertyNode(varsArgNode, key);
386
+ return node ? recast.print(node).code : undefined;
387
+ }
388
+ /** Find the raw AST node for a named property inside an ObjectExpression. */
389
+ function findPropertyNode(varsArgNode, key) {
388
390
  if (varsArgNode?.type !== "ObjectExpression")
389
391
  return undefined;
390
392
  for (const prop of varsArgNode.properties ?? []) {
393
+ if (!isObjectProperty(prop))
394
+ continue;
395
+ if (propKeyName(prop) === key)
396
+ return prop.value;
397
+ }
398
+ return undefined;
399
+ }
400
+ // ── Native GSAP Keyframes Parsing ──────────────────────────────────────────
401
+ const PERCENTAGE_KEY_RE = /^(\d+(?:\.\d+)?)%$/;
402
+ /** Extract a string-valued ease or easeEach from an AST property node. */
403
+ function tryResolveStringProp(propValue, scope) {
404
+ const val = resolveNode(propValue, scope);
405
+ return typeof val === "string" ? val : undefined;
406
+ }
407
+ /**
408
+ * Parse a `keyframes` property value from a tween vars AST node into a
409
+ * normalized `GsapKeyframesData` structure. Handles all three GSAP formats:
410
+ * percentage objects, object arrays, and simple (property-array) objects.
411
+ */
412
+ // fallow-ignore-next-line complexity
413
+ function parseKeyframesNode(node, scope) {
414
+ if (!node)
415
+ return undefined;
416
+ // ── Object array format: keyframes: [ { x: 0, duration: 0.5 }, ... ] ──
417
+ if (node.type === "ArrayExpression") {
418
+ return parseObjectArrayKeyframes(node, scope);
419
+ }
420
+ if (node.type !== "ObjectExpression")
421
+ return undefined;
422
+ // Distinguish percentage vs simple-array by inspecting property keys/values.
423
+ const props = node.properties ?? [];
424
+ let hasPercentageKey = false;
425
+ let hasArrayValue = false;
426
+ for (const prop of props) {
391
427
  if (prop.type !== "ObjectProperty" && prop.type !== "Property")
392
428
  continue;
393
- const propKey = prop.key?.name ?? prop.key?.value;
394
- if (propKey === key) {
395
- return recast.print(prop.value).code;
429
+ const key = prop.key?.value ?? prop.key?.name;
430
+ if (typeof key === "string" && PERCENTAGE_KEY_RE.test(key)) {
431
+ hasPercentageKey = true;
432
+ break;
433
+ }
434
+ if (prop.value?.type === "ArrayExpression") {
435
+ hasArrayValue = true;
396
436
  }
397
437
  }
438
+ if (hasPercentageKey)
439
+ return parsePercentageKeyframes(node, scope);
440
+ if (hasArrayValue)
441
+ return parseSimpleArrayKeyframes(node, scope);
398
442
  return undefined;
399
443
  }
444
+ // fallow-ignore-next-line complexity
445
+ function parsePercentageKeyframes(node, scope) {
446
+ const keyframes = [];
447
+ let ease;
448
+ let easeEach;
449
+ for (const prop of node.properties ?? []) {
450
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property")
451
+ continue;
452
+ const key = prop.key?.value ?? prop.key?.name;
453
+ if (typeof key !== "string")
454
+ continue;
455
+ const pctMatch = PERCENTAGE_KEY_RE.exec(key);
456
+ if (pctMatch) {
457
+ const percentage = Number.parseFloat(pctMatch[1]);
458
+ const record = objectExpressionToRecord(prop.value, scope);
459
+ const properties = {};
460
+ let kfEase;
461
+ for (const [k, v] of Object.entries(record)) {
462
+ if (k === "ease" && typeof v === "string") {
463
+ kfEase = v;
464
+ }
465
+ else if (typeof v === "number" || typeof v === "string") {
466
+ properties[k] = v;
467
+ }
468
+ }
469
+ keyframes.push({ percentage, properties, ...(kfEase ? { ease: kfEase } : {}) });
470
+ }
471
+ else if (key === "ease") {
472
+ ease = tryResolveStringProp(prop.value, scope) ?? ease;
473
+ }
474
+ else if (key === "easeEach") {
475
+ easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;
476
+ }
477
+ }
478
+ keyframes.sort((a, b) => a.percentage - b.percentage);
479
+ return {
480
+ format: "percentage",
481
+ keyframes,
482
+ ...(ease ? { ease } : {}),
483
+ ...(easeEach ? { easeEach } : {}),
484
+ };
485
+ }
486
+ // fallow-ignore-next-line complexity
487
+ function parseObjectArrayKeyframes(node, scope) {
488
+ const elements = node.elements ?? [];
489
+ const raw = [];
490
+ for (const el of elements) {
491
+ if (!el || (el.type !== "ObjectExpression" && el.type !== "ObjectProperty")) {
492
+ // Skip non-object elements
493
+ if (el?.type !== "ObjectExpression")
494
+ continue;
495
+ }
496
+ const record = objectExpressionToRecord(el, scope);
497
+ const properties = {};
498
+ let duration;
499
+ let ease;
500
+ for (const [k, v] of Object.entries(record)) {
501
+ if (k === "duration" && typeof v === "number") {
502
+ duration = v;
503
+ }
504
+ else if (k === "ease" && typeof v === "string") {
505
+ ease = v;
506
+ }
507
+ else if (typeof v === "number" || typeof v === "string") {
508
+ properties[k] = v;
509
+ }
510
+ }
511
+ raw.push({ properties, duration, ease });
512
+ }
513
+ // Convert durations to percentage positions. If durations are present, use
514
+ // cumulative ratios; otherwise distribute evenly.
515
+ const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);
516
+ const keyframes = [];
517
+ if (totalDuration > 0) {
518
+ let cumulative = 0;
519
+ for (const entry of raw) {
520
+ const percentage = Math.round((cumulative / totalDuration) * 100);
521
+ keyframes.push({
522
+ percentage,
523
+ properties: entry.properties,
524
+ ...(entry.ease ? { ease: entry.ease } : {}),
525
+ });
526
+ cumulative += entry.duration ?? 0;
527
+ }
528
+ }
529
+ else {
530
+ for (let i = 0; i < raw.length; i++) {
531
+ const entry = raw[i];
532
+ const percentage = raw.length > 1 ? Math.round((i / (raw.length - 1)) * 100) : 0;
533
+ keyframes.push({
534
+ percentage,
535
+ properties: entry.properties,
536
+ ...(entry.ease ? { ease: entry.ease } : {}),
537
+ });
538
+ }
539
+ }
540
+ return { format: "object-array", keyframes };
541
+ }
542
+ // fallow-ignore-next-line complexity
543
+ function parseSimpleArrayKeyframes(node, scope) {
544
+ const arrayProps = new Map();
545
+ let ease;
546
+ let easeEach;
547
+ for (const prop of node.properties ?? []) {
548
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property")
549
+ continue;
550
+ const key = prop.key?.name ?? prop.key?.value;
551
+ if (typeof key !== "string")
552
+ continue;
553
+ if (prop.value?.type === "ArrayExpression") {
554
+ const values = [];
555
+ for (const el of prop.value.elements ?? []) {
556
+ const val = resolveNode(el, scope);
557
+ if (typeof val === "number" || typeof val === "string") {
558
+ values.push(val);
559
+ }
560
+ }
561
+ if (values.length > 0)
562
+ arrayProps.set(key, values);
563
+ }
564
+ else if (key === "ease") {
565
+ ease = tryResolveStringProp(prop.value, scope) ?? ease;
566
+ }
567
+ else if (key === "easeEach") {
568
+ easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;
569
+ }
570
+ }
571
+ // Zip arrays into percentage keyframes (evenly spaced).
572
+ const maxLen = Math.max(...[...arrayProps.values()].map((a) => a.length), 0);
573
+ const keyframes = [];
574
+ for (let i = 0; i < maxLen; i++) {
575
+ const percentage = maxLen > 1 ? Math.round((i / (maxLen - 1)) * 100) : 0;
576
+ const properties = {};
577
+ for (const [key, values] of arrayProps) {
578
+ if (i < values.length)
579
+ properties[key] = values[i];
580
+ }
581
+ keyframes.push({ percentage, properties });
582
+ }
583
+ return {
584
+ format: "simple-array",
585
+ keyframes,
586
+ ...(ease ? { ease } : {}),
587
+ ...(easeEach ? { easeEach } : {}),
588
+ };
589
+ }
590
+ // fallow-ignore-next-line complexity
400
591
  function tweenCallToAnimation(call, scope) {
401
592
  const vars = objectExpressionToRecord(call.varsArg, scope);
402
593
  const properties = {};
403
594
  const extras = {};
595
+ let keyframesData;
596
+ let hasUnresolvedKeyframes = false;
404
597
  for (const [key, val] of Object.entries(vars)) {
405
598
  if (BUILTIN_VAR_KEYS.has(key))
406
599
  continue;
407
600
  if (DROPPED_VAR_KEYS.has(key))
408
601
  continue;
602
+ if (key === "keyframes") {
603
+ const kfNode = findPropertyNode(call.varsArg, "keyframes");
604
+ keyframesData = parseKeyframesNode(kfNode, scope);
605
+ if (!keyframesData && kfNode)
606
+ hasUnresolvedKeyframes = true;
607
+ continue;
608
+ }
609
+ if (key === "easeEach") {
610
+ // easeEach is only meaningful alongside keyframes — handled below.
611
+ continue;
612
+ }
409
613
  if (EXTRAS_KEYS.has(key)) {
410
614
  // For extras, prefer the raw AST source so complex objects like
411
615
  // `stagger: { each: 0.15, from: "start" }` survive verbatim.
@@ -422,6 +626,10 @@ function tweenCallToAnimation(call, scope) {
422
626
  properties[key] = val;
423
627
  }
424
628
  }
629
+ // Apply tween-level easeEach to keyframes data.
630
+ if (keyframesData && typeof vars.easeEach === "string") {
631
+ keyframesData.easeEach = vars.easeEach;
632
+ }
425
633
  let fromProperties;
426
634
  if (call.method === "fromTo" && call.fromArg) {
427
635
  fromProperties = {};
@@ -447,6 +655,12 @@ function tweenCallToAnimation(call, scope) {
447
655
  };
448
656
  if (Object.keys(extras).length > 0)
449
657
  anim.extras = extras;
658
+ if (keyframesData)
659
+ anim.keyframes = keyframesData;
660
+ if (hasUnresolvedKeyframes)
661
+ anim.hasUnresolvedKeyframes = true;
662
+ if (call.selector === "__unresolved__")
663
+ anim.hasUnresolvedSelector = true;
450
664
  return anim;
451
665
  }
452
666
  // ── Stable ID Generation ───────────────────────────────────────────────────
@@ -744,4 +958,393 @@ export function removeAnimationFromScript(script, animationId) {
744
958
  }
745
959
  return recast.print(parsed.ast).code;
746
960
  }
961
+ // ── Keyframe Mutation Functions ────────────────────────────────────────────
962
+ /** Remove a named property from an ObjectExpression's properties array. */
963
+ function removeVarsKey(varsArg, key) {
964
+ if (varsArg?.type !== "ObjectExpression")
965
+ return;
966
+ varsArg.properties = varsArg.properties.filter((p) => !(isObjectProperty(p) && propKeyName(p) === key));
967
+ }
968
+ /** Extract the numeric percentage from a key like "50%". Returns NaN for non-percentage keys. */
969
+ function percentageFromKey(key) {
970
+ const m = PERCENTAGE_KEY_RE.exec(key);
971
+ return m ? Number.parseFloat(m[1]) : Number.NaN;
972
+ }
973
+ /** Build a keyframe value AST node from properties and optional ease. */
974
+ function buildKeyframeValueNode(properties, ease) {
975
+ const entries = Object.entries(properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
976
+ if (ease)
977
+ entries.push(`ease: ${JSON.stringify(ease)}`);
978
+ return parseExpr(`{ ${entries.join(", ")} }`);
979
+ }
980
+ /** Parse + locate a target animation, returning null on failure. */
981
+ function locateAnimation(script, animationId) {
982
+ let parsed;
983
+ try {
984
+ parsed = parseGsapAst(script);
985
+ }
986
+ catch {
987
+ return null;
988
+ }
989
+ const target = parsed.located.find((l) => l.id === animationId);
990
+ return target ? { parsed, target } : null;
991
+ }
992
+ /** Find the keyframes ObjectExpression node on a tween's varsArg, or null. */
993
+ function findKeyframesObjectNode(varsArg) {
994
+ const node = findPropertyNode(varsArg, "keyframes");
995
+ return node?.type === "ObjectExpression" ? node : null;
996
+ }
997
+ /** Filter percentage-keyed properties from a keyframes ObjectExpression. */
998
+ function filterPercentageProps(kfNode) {
999
+ return kfNode.properties.filter((p) => {
1000
+ if (!isObjectProperty(p))
1001
+ return false;
1002
+ const key = propKeyName(p);
1003
+ return typeof key === "string" && PERCENTAGE_KEY_RE.test(key);
1004
+ });
1005
+ }
1006
+ /**
1007
+ * Collapse a keyframes node to flat tween: apply `record` entries as vars keys,
1008
+ * then remove `keyframes` and `easeEach` from varsArg. Skips the `ease` key
1009
+ * from the record (per-keyframe ease, not a tween ease).
1010
+ */
1011
+ function collapseKeyframesToFlat(varsArg, record) {
1012
+ for (const [k, v] of Object.entries(record)) {
1013
+ if (k === "ease")
1014
+ continue;
1015
+ if (typeof v === "number" || typeof v === "string")
1016
+ setVarsKey(varsArg, k, v);
1017
+ }
1018
+ removeVarsKey(varsArg, "keyframes");
1019
+ removeVarsKey(varsArg, "easeEach");
1020
+ }
1021
+ /**
1022
+ * Insert a keyframe at the given percentage in an existing percentage-keyframes
1023
+ * object. If the percentage already exists, its value is replaced.
1024
+ */
1025
+ export function addKeyframeToScript(script, animationId, percentage, properties, ease, backfillDefaults) {
1026
+ const loc = locateAnimation(script, animationId);
1027
+ if (!loc)
1028
+ return script;
1029
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
1030
+ if (!kfNode)
1031
+ return script;
1032
+ const pctKey = `${percentage}%`;
1033
+ const newValueNode = buildKeyframeValueNode(properties, ease);
1034
+ // Replace if this percentage already exists
1035
+ const existingIdx = kfNode.properties.findIndex((p) => isObjectProperty(p) && propKeyName(p) === pctKey);
1036
+ if (existingIdx !== -1) {
1037
+ kfNode.properties[existingIdx].value = newValueNode;
1038
+ }
1039
+ else {
1040
+ // Build the new property node with a quoted percentage key
1041
+ const newProp = parseExpr(`{ ${JSON.stringify(pctKey)}: {} }`).properties[0];
1042
+ newProp.value = newValueNode;
1043
+ // Insert in sorted order by percentage
1044
+ let insertIdx = kfNode.properties.length;
1045
+ for (let i = 0; i < kfNode.properties.length; i++) {
1046
+ const key = isObjectProperty(kfNode.properties[i])
1047
+ ? propKeyName(kfNode.properties[i])
1048
+ : undefined;
1049
+ if (typeof key === "string" && percentageFromKey(key) > percentage) {
1050
+ insertIdx = i;
1051
+ break;
1052
+ }
1053
+ }
1054
+ kfNode.properties.splice(insertIdx, 0, newProp);
1055
+ }
1056
+ // Backfill: when the new keyframe introduces properties absent from other
1057
+ // keyframes, add default values so GSAP can interpolate them.
1058
+ if (backfillDefaults) {
1059
+ const newPropKeys = Object.keys(properties);
1060
+ const pctProps = filterPercentageProps(kfNode);
1061
+ for (const prop of pctProps) {
1062
+ const key = propKeyName(prop);
1063
+ if (key === pctKey)
1064
+ continue;
1065
+ const valObj = prop.value;
1066
+ if (!valObj || valObj.type !== "ObjectExpression")
1067
+ continue;
1068
+ const existingKeys = new Set(valObj.properties.filter((p) => isObjectProperty(p)).map((p) => propKeyName(p)));
1069
+ for (const pk of newPropKeys) {
1070
+ if (existingKeys.has(pk))
1071
+ continue;
1072
+ const defaultVal = backfillDefaults[pk];
1073
+ if (defaultVal == null)
1074
+ continue;
1075
+ const fillProp = parseExpr(`{ ${safeKey(pk)}: ${valueToCode(defaultVal)} }`).properties[0];
1076
+ valObj.properties.push(fillProp);
1077
+ }
1078
+ }
1079
+ }
1080
+ return recast.print(loc.parsed.ast).code;
1081
+ }
1082
+ /**
1083
+ * Remove a keyframe at the given percentage. If fewer than 2 keyframes remain
1084
+ * after removal, collapse the keyframes object to a flat tween using the
1085
+ * remaining keyframe's properties.
1086
+ */
1087
+ export function removeKeyframeFromScript(script, animationId, percentage) {
1088
+ const loc = locateAnimation(script, animationId);
1089
+ if (!loc)
1090
+ return script;
1091
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
1092
+ if (!kfNode)
1093
+ return script;
1094
+ const pctKey = `${percentage}%`;
1095
+ const removeIdx = kfNode.properties.findIndex((p) => isObjectProperty(p) && propKeyName(p) === pctKey);
1096
+ if (removeIdx === -1)
1097
+ return script;
1098
+ kfNode.properties.splice(removeIdx, 1);
1099
+ const remainingKfs = filterPercentageProps(kfNode);
1100
+ if (remainingKfs.length < 2) {
1101
+ const record = remainingKfs.length === 1
1102
+ ? objectExpressionToRecord(remainingKfs[0].value, loc.parsed.scope)
1103
+ : {};
1104
+ collapseKeyframesToFlat(loc.target.call.varsArg, record);
1105
+ }
1106
+ return recast.print(loc.parsed.ast).code;
1107
+ }
1108
+ /**
1109
+ * Replace the properties (and optionally ease) at an existing keyframe percentage.
1110
+ */
1111
+ export function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
1112
+ const loc = locateAnimation(script, animationId);
1113
+ if (!loc)
1114
+ return script;
1115
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
1116
+ if (!kfNode)
1117
+ return script;
1118
+ const pctKey = `${percentage}%`;
1119
+ const existing = kfNode.properties.find((p) => isObjectProperty(p) && propKeyName(p) === pctKey);
1120
+ if (!existing)
1121
+ return script;
1122
+ existing.value = buildKeyframeValueNode(properties, ease);
1123
+ return recast.print(loc.parsed.ast).code;
1124
+ }
1125
+ /** Resolve from/to property maps for a tween being converted to keyframes. */
1126
+ const CSS_IDENTITY = {
1127
+ opacity: 1,
1128
+ autoAlpha: 1,
1129
+ scale: 1,
1130
+ scaleX: 1,
1131
+ scaleY: 1,
1132
+ };
1133
+ function cssIdentityValue(prop) {
1134
+ return CSS_IDENTITY[prop] ?? 0;
1135
+ }
1136
+ function resolveConversionProps(anim, resolvedFromValues) {
1137
+ if (anim.method === "to") {
1138
+ if (resolvedFromValues) {
1139
+ return { fromProps: resolvedFromValues, toProps: { ...anim.properties } };
1140
+ }
1141
+ const identityFrom = {};
1142
+ for (const [key, val] of Object.entries(anim.properties)) {
1143
+ if (val != null)
1144
+ identityFrom[key] = typeof val === "number" ? cssIdentityValue(key) : val;
1145
+ }
1146
+ return { fromProps: identityFrom, toProps: { ...anim.properties } };
1147
+ }
1148
+ if (anim.method === "from") {
1149
+ if (resolvedFromValues) {
1150
+ return { fromProps: { ...anim.properties }, toProps: resolvedFromValues };
1151
+ }
1152
+ const identityTo = {};
1153
+ for (const [key, val] of Object.entries(anim.properties)) {
1154
+ if (val != null)
1155
+ identityTo[key] = typeof val === "number" ? cssIdentityValue(key) : val;
1156
+ }
1157
+ return { fromProps: { ...anim.properties }, toProps: identityTo };
1158
+ }
1159
+ // fromTo
1160
+ return { fromProps: { ...(anim.fromProperties ?? {}) }, toProps: { ...anim.properties } };
1161
+ }
1162
+ /** Strip editable properties and ease/keyframes keys from a varsArg. */
1163
+ function stripEditableAndEase(varsArg) {
1164
+ if (varsArg?.type !== "ObjectExpression")
1165
+ return;
1166
+ varsArg.properties = varsArg.properties.filter((p) => {
1167
+ if (!isObjectProperty(p))
1168
+ return true;
1169
+ const key = propKeyName(p);
1170
+ if (typeof key !== "string")
1171
+ return true;
1172
+ if (key === "ease" || key === "keyframes")
1173
+ return false;
1174
+ return !isEditablePropertyKey(key);
1175
+ });
1176
+ }
1177
+ /** Build and prepend a keyframes property node onto varsArg. */
1178
+ function insertKeyframesProp(varsArg, fromProps, toProps, easeEach) {
1179
+ const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
1180
+ const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
1181
+ const easeEntry = easeEach ? `, easeEach: ${JSON.stringify(easeEach)}` : "";
1182
+ const kfCode = `{ "0%": { ${fromEntries.join(", ")} }, "100%": { ${toEntries.join(", ")} }${easeEntry} }`;
1183
+ const kfProp = parseExpr(`{ keyframes: {} }`).properties[0];
1184
+ kfProp.value = parseExpr(kfCode);
1185
+ if (varsArg?.type === "ObjectExpression")
1186
+ varsArg.properties.unshift(kfProp);
1187
+ }
1188
+ /**
1189
+ * Convert a flat tween (to/from/fromTo) to percentage-keyframes format.
1190
+ * `resolvedFromValues` supplies the "from" state for `to()` tweens or
1191
+ * the "to" state for `from()` tweens (the values the DOM would resolve to).
1192
+ */
1193
+ export function convertToKeyframesInScript(script, animationId, resolvedFromValues) {
1194
+ const loc = locateAnimation(script, animationId);
1195
+ if (!loc)
1196
+ return script;
1197
+ const anim = loc.target.animation;
1198
+ if (anim.keyframes || anim.method === "set")
1199
+ return script;
1200
+ const { fromProps, toProps } = resolveConversionProps(anim, resolvedFromValues);
1201
+ const varsArg = loc.target.call.varsArg;
1202
+ const originalEase = anim.ease;
1203
+ stripEditableAndEase(varsArg);
1204
+ insertKeyframesProp(varsArg, fromProps, toProps, originalEase || undefined);
1205
+ if (originalEase) {
1206
+ setVarsKey(varsArg, "ease", "none");
1207
+ }
1208
+ // For from() or fromTo(), convert to to()
1209
+ if (anim.method === "from" || anim.method === "fromTo") {
1210
+ loc.target.call.node.callee.property.name = "to";
1211
+ if (anim.method === "fromTo")
1212
+ loc.target.call.node.arguments.splice(1, 1);
1213
+ }
1214
+ return recast.print(loc.parsed.ast).code;
1215
+ }
1216
+ /**
1217
+ * Remove all keyframes from a tween, collapsing to a flat tween with the
1218
+ * last keyframe's properties.
1219
+ */
1220
+ export function removeAllKeyframesFromScript(script, animationId) {
1221
+ const loc = locateAnimation(script, animationId);
1222
+ if (!loc)
1223
+ return script;
1224
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
1225
+ if (!kfNode)
1226
+ return script;
1227
+ // Collect all percentage keyframe entries, sorted
1228
+ const kfEntries = filterPercentageProps(kfNode)
1229
+ .map((p) => ({ pct: percentageFromKey(propKeyName(p)), prop: p }))
1230
+ .filter((e) => !Number.isNaN(e.pct))
1231
+ .sort((a, b) => a.pct - b.pct);
1232
+ if (kfEntries.length === 0)
1233
+ return script;
1234
+ const lastRecord = objectExpressionToRecord(kfEntries[kfEntries.length - 1].prop.value, loc.parsed.scope);
1235
+ collapseKeyframesToFlat(loc.target.call.varsArg, lastRecord);
1236
+ return recast.print(loc.parsed.ast).code;
1237
+ }
1238
+ /**
1239
+ * Replace a dynamic `keyframes: <expr>` with a static percentage-keyframes object.
1240
+ * Called when the user first edits a dynamically-generated keyframe in the studio.
1241
+ */
1242
+ export function materializeKeyframesInScript(script, animationId, keyframes, easeEach, resolvedSelector) {
1243
+ const loc = locateAnimation(script, animationId);
1244
+ if (!loc)
1245
+ return script;
1246
+ const varsArg = loc.target.call.varsArg;
1247
+ // Replace dynamic selector with resolved static string
1248
+ if (resolvedSelector && loc.target.call.node.arguments[0]) {
1249
+ loc.target.call.node.arguments[0] = parseExpr(JSON.stringify(resolvedSelector));
1250
+ }
1251
+ const entries = [];
1252
+ const sorted = keyframes.slice().sort((a, b) => a.percentage - b.percentage);
1253
+ for (const kf of sorted) {
1254
+ const propEntries = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
1255
+ if (kf.ease)
1256
+ propEntries.push(`ease: ${JSON.stringify(kf.ease)}`);
1257
+ entries.push(`${JSON.stringify(kf.percentage + "%")}: { ${propEntries.join(", ")} }`);
1258
+ }
1259
+ if (easeEach) {
1260
+ entries.push(`easeEach: ${JSON.stringify(easeEach)}`);
1261
+ }
1262
+ const kfObjCode = `{ ${entries.join(", ")} }`;
1263
+ const kfParent = varsArg.properties.find((p) => isObjectProperty(p) && propKeyName(p) === "keyframes");
1264
+ if (kfParent) {
1265
+ kfParent.value = parseExpr(kfObjCode);
1266
+ }
1267
+ else {
1268
+ const kfProp = parseExpr(`{ keyframes: ${kfObjCode} }`).properties[0];
1269
+ varsArg.properties.unshift(kfProp);
1270
+ }
1271
+ removeVarsKey(varsArg, "easeEach");
1272
+ return recast.print(loc.parsed.ast).code;
1273
+ }
1274
+ /**
1275
+ * Replace a dynamic loop that generates multiple tween calls with individual
1276
+ * static `tl.to()` calls — one per element. Finds the loop containing the
1277
+ * animation and replaces the entire loop body with unrolled static calls.
1278
+ */
1279
+ export function unrollDynamicAnimations(script, animationId, elements) {
1280
+ const loc = locateAnimation(script, animationId);
1281
+ if (!loc)
1282
+ return script;
1283
+ const varsArg = loc.target.call.varsArg;
1284
+ // Read duration and ease from the original tween vars
1285
+ const durationVal = extractLiteralValue(findPropertyNode(varsArg, "duration"), loc.parsed.scope);
1286
+ const easeVal = extractLiteralValue(findPropertyNode(varsArg, "ease"), loc.parsed.scope);
1287
+ const duration = typeof durationVal === "number" ? durationVal : 8;
1288
+ const ease = typeof easeVal === "string" ? easeVal : "none";
1289
+ const posArg = loc.target.call.positionArg;
1290
+ const position = posArg ? extractLiteralValue(posArg, loc.parsed.scope) : 0;
1291
+ const posCode = typeof position === "number"
1292
+ ? String(position)
1293
+ : typeof position === "string"
1294
+ ? JSON.stringify(position)
1295
+ : "0";
1296
+ // Find the enclosing loop (for/forEach) by walking up the AST path
1297
+ let loopNode = null;
1298
+ let current = loc.target.call.path;
1299
+ while (current) {
1300
+ const node = current.node ?? current.value;
1301
+ if (node?.type === "ForStatement" ||
1302
+ node?.type === "ForInStatement" ||
1303
+ node?.type === "ForOfStatement" ||
1304
+ node?.type === "WhileStatement") {
1305
+ loopNode = node;
1306
+ break;
1307
+ }
1308
+ if (node?.type === "ExpressionStatement" &&
1309
+ node.expression?.type === "CallExpression" &&
1310
+ node.expression.callee?.property?.name === "forEach") {
1311
+ loopNode = node;
1312
+ break;
1313
+ }
1314
+ current = current.parent ?? current.parentPath;
1315
+ }
1316
+ // Build replacement code: individual tl.to() calls for each element
1317
+ const calls = [];
1318
+ for (const el of elements) {
1319
+ const kfEntries = [];
1320
+ const sorted = el.keyframes.slice().sort((a, b) => a.percentage - b.percentage);
1321
+ for (const kf of sorted) {
1322
+ const propEntries = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
1323
+ kfEntries.push(`${JSON.stringify(kf.percentage + "%")}: { ${propEntries.join(", ")} }`);
1324
+ }
1325
+ if (el.easeEach) {
1326
+ kfEntries.push(`easeEach: ${JSON.stringify(el.easeEach)}`);
1327
+ }
1328
+ calls.push(`tl.to(${JSON.stringify(el.selector)}, { keyframes: { ${kfEntries.join(", ")} }, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`);
1329
+ }
1330
+ const replacement = calls.join("\n ");
1331
+ if (loopNode) {
1332
+ // Replace the entire loop with the unrolled calls
1333
+ const start = loopNode.start ?? loopNode.range?.[0];
1334
+ const end = loopNode.end ?? loopNode.range?.[1];
1335
+ if (typeof start === "number" && typeof end === "number") {
1336
+ return script.slice(0, start) + replacement + script.slice(end);
1337
+ }
1338
+ }
1339
+ // Fallback: replace just the tween call's enclosing expression statement
1340
+ const stmtNode = loc.target.call.path?.parent?.node ?? loc.target.call.path?.parentPath?.node;
1341
+ if (stmtNode?.type === "ExpressionStatement") {
1342
+ const start = stmtNode.start ?? stmtNode.range?.[0];
1343
+ const end = stmtNode.end ?? stmtNode.range?.[1];
1344
+ if (typeof start === "number" && typeof end === "number") {
1345
+ return script.slice(0, start) + replacement + script.slice(end);
1346
+ }
1347
+ }
1348
+ return script;
1349
+ }
747
1350
  //# sourceMappingURL=gsapParser.js.map