@hyperframes/core 0.6.90 → 0.6.92
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.
- package/dist/compiler/inlineSubCompositions.d.ts.map +1 -1
- package/dist/compiler/inlineSubCompositions.js +17 -0
- package/dist/compiler/inlineSubCompositions.js.map +1 -1
- package/dist/lint/rules/gsap.d.ts.map +1 -1
- package/dist/lint/rules/gsap.js +42 -1
- package/dist/lint/rules/gsap.js.map +1 -1
- package/dist/parsers/gsapConstants.d.ts +4 -0
- package/dist/parsers/gsapConstants.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.js +28 -0
- package/dist/parsers/gsapConstants.js.map +1 -1
- package/dist/parsers/gsapParser.d.ts +26 -0
- package/dist/parsers/gsapParser.d.ts.map +1 -1
- package/dist/parsers/gsapParser.js +573 -63
- package/dist/parsers/gsapParser.js.map +1 -1
- package/dist/parsers/gsapParser.test-helpers.d.ts +49 -0
- package/dist/parsers/gsapParser.test-helpers.d.ts.map +1 -0
- package/dist/parsers/gsapParser.test-helpers.js +97 -0
- package/dist/parsers/gsapParser.test-helpers.js.map +1 -0
- package/dist/parsers/gsapSerialize.d.ts +8 -0
- package/dist/parsers/gsapSerialize.d.ts.map +1 -1
- package/dist/parsers/gsapSerialize.js +2 -2
- package/dist/parsers/gsapSerialize.js.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.js +122 -5
- package/dist/studio-api/helpers/sourceMutation.js.map +1 -1
- package/dist/studio-api/routes/files.d.ts.map +1 -1
- package/dist/studio-api/routes/files.js +57 -8
- package/dist/studio-api/routes/files.js.map +1 -1
- package/package.json +6 -1
|
@@ -11,6 +11,8 @@
|
|
|
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 { PROPERTY_GROUPS, classifyPropertyGroup, classifyTweenPropertyGroup, } from "./gsapConstants";
|
|
15
|
+
import { classifyPropertyGroup, classifyTweenPropertyGroup } from "./gsapConstants";
|
|
14
16
|
export { generateSpringEaseData, SPRING_PRESETS } from "./springEase";
|
|
15
17
|
const GSAP_METHODS = new Set(["set", "to", "from", "fromTo"]);
|
|
16
18
|
function parseScript(script) {
|
|
@@ -279,15 +281,34 @@ function isGsapTimelineCall(node) {
|
|
|
279
281
|
node.callee.object?.name === "gsap" &&
|
|
280
282
|
node.callee.property?.name === "timeline");
|
|
281
283
|
}
|
|
282
|
-
function
|
|
284
|
+
function extractTimelineDefaults(callNode, scope) {
|
|
285
|
+
const arg = callNode.arguments?.[0];
|
|
286
|
+
if (!arg || arg.type !== "ObjectExpression")
|
|
287
|
+
return undefined;
|
|
288
|
+
const defaultsProp = arg.properties?.find((p) => isObjectProperty(p) && propKeyName(p) === "defaults");
|
|
289
|
+
if (!defaultsProp?.value || defaultsProp.value.type !== "ObjectExpression")
|
|
290
|
+
return undefined;
|
|
291
|
+
const record = objectExpressionToRecord(defaultsProp.value, scope);
|
|
292
|
+
const result = {};
|
|
293
|
+
if (typeof record.ease === "string")
|
|
294
|
+
result.ease = record.ease;
|
|
295
|
+
if (typeof record.duration === "number")
|
|
296
|
+
result.duration = record.duration;
|
|
297
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
298
|
+
}
|
|
299
|
+
function findTimelineVar(ast, scope) {
|
|
283
300
|
let timelineVar = null;
|
|
284
301
|
let timelineCount = 0;
|
|
302
|
+
let defaults;
|
|
303
|
+
const emptyScope = scope ?? new Map();
|
|
285
304
|
recast.types.visit(ast, {
|
|
286
305
|
visitVariableDeclarator(path) {
|
|
287
306
|
if (isGsapTimelineCall(path.node.init)) {
|
|
288
307
|
timelineCount += 1;
|
|
289
|
-
if (!timelineVar)
|
|
308
|
+
if (!timelineVar) {
|
|
290
309
|
timelineVar = path.node.id?.name ?? null;
|
|
310
|
+
defaults = extractTimelineDefaults(path.node.init, emptyScope);
|
|
311
|
+
}
|
|
291
312
|
}
|
|
292
313
|
this.traverse(path);
|
|
293
314
|
},
|
|
@@ -298,12 +319,13 @@ function findTimelineVar(ast) {
|
|
|
298
319
|
const left = path.node.left;
|
|
299
320
|
if (left?.type === "Identifier")
|
|
300
321
|
timelineVar = left.name;
|
|
322
|
+
defaults = extractTimelineDefaults(path.node.right, emptyScope);
|
|
301
323
|
}
|
|
302
324
|
}
|
|
303
325
|
this.traverse(path);
|
|
304
326
|
},
|
|
305
327
|
});
|
|
306
|
-
return { timelineVar, timelineCount };
|
|
328
|
+
return { timelineVar, timelineCount, defaults };
|
|
307
329
|
}
|
|
308
330
|
/**
|
|
309
331
|
* True when the member chain of `callNode.callee` is rooted at the timeline
|
|
@@ -483,6 +505,20 @@ function parsePercentageKeyframes(node, scope) {
|
|
|
483
505
|
...(easeEach ? { easeEach } : {}),
|
|
484
506
|
};
|
|
485
507
|
}
|
|
508
|
+
function computeKeyframesTotalDuration(varsNode, scope) {
|
|
509
|
+
const kfNode = (varsNode.properties ?? []).find((p) => (p.key?.name ?? p.key?.value) === "keyframes")?.value;
|
|
510
|
+
if (!kfNode || kfNode.type !== "ArrayExpression")
|
|
511
|
+
return undefined;
|
|
512
|
+
let total = 0;
|
|
513
|
+
for (const el of kfNode.elements ?? []) {
|
|
514
|
+
if (!el || el.type !== "ObjectExpression")
|
|
515
|
+
continue;
|
|
516
|
+
const r = objectExpressionToRecord(el, scope);
|
|
517
|
+
if (typeof r.duration === "number")
|
|
518
|
+
total += r.duration;
|
|
519
|
+
}
|
|
520
|
+
return total > 0 ? total : undefined;
|
|
521
|
+
}
|
|
486
522
|
// fallow-ignore-next-line complexity
|
|
487
523
|
function parseObjectArrayKeyframes(node, scope) {
|
|
488
524
|
const elements = node.elements ?? [];
|
|
@@ -517,13 +553,13 @@ function parseObjectArrayKeyframes(node, scope) {
|
|
|
517
553
|
if (totalDuration > 0) {
|
|
518
554
|
let cumulative = 0;
|
|
519
555
|
for (const entry of raw) {
|
|
556
|
+
cumulative += entry.duration ?? 0;
|
|
520
557
|
const percentage = Math.round((cumulative / totalDuration) * 100);
|
|
521
558
|
keyframes.push({
|
|
522
559
|
percentage,
|
|
523
560
|
properties: entry.properties,
|
|
524
561
|
...(entry.ease ? { ease: entry.ease } : {}),
|
|
525
562
|
});
|
|
526
|
-
cumulative += entry.duration ?? 0;
|
|
527
563
|
}
|
|
528
564
|
}
|
|
529
565
|
else {
|
|
@@ -745,10 +781,14 @@ function tweenCallToAnimation(call, scope) {
|
|
|
745
781
|
}
|
|
746
782
|
}
|
|
747
783
|
}
|
|
748
|
-
const
|
|
784
|
+
const hasPositionArg = !!call.positionArg;
|
|
785
|
+
const posVal = hasPositionArg ? extractLiteralValue(call.positionArg, scope) : 0;
|
|
749
786
|
const position = typeof posVal === "number" ? posVal : typeof posVal === "string" ? posVal : 0;
|
|
750
|
-
|
|
787
|
+
let duration = typeof vars.duration === "number" ? vars.duration : undefined;
|
|
751
788
|
const ease = typeof vars.ease === "string" ? vars.ease : undefined;
|
|
789
|
+
if (duration === undefined && keyframesData) {
|
|
790
|
+
duration = computeKeyframesTotalDuration(call.varsArg, scope);
|
|
791
|
+
}
|
|
752
792
|
const anim = {
|
|
753
793
|
targetSelector: call.selector,
|
|
754
794
|
method: call.method,
|
|
@@ -758,6 +798,19 @@ function tweenCallToAnimation(call, scope) {
|
|
|
758
798
|
duration,
|
|
759
799
|
ease,
|
|
760
800
|
};
|
|
801
|
+
if (!hasPositionArg)
|
|
802
|
+
anim.implicitPosition = true;
|
|
803
|
+
let group = classifyTweenPropertyGroup(properties);
|
|
804
|
+
if (!group && keyframesData) {
|
|
805
|
+
const kfProps = {};
|
|
806
|
+
for (const kf of keyframesData.keyframes) {
|
|
807
|
+
for (const k of Object.keys(kf.properties))
|
|
808
|
+
kfProps[k] = true;
|
|
809
|
+
}
|
|
810
|
+
group = classifyTweenPropertyGroup(kfProps);
|
|
811
|
+
}
|
|
812
|
+
if (group)
|
|
813
|
+
anim.propertyGroup = group;
|
|
761
814
|
if (Object.keys(extras).length > 0)
|
|
762
815
|
anim.extras = extras;
|
|
763
816
|
if (keyframesData)
|
|
@@ -770,14 +823,101 @@ function tweenCallToAnimation(call, scope) {
|
|
|
770
823
|
anim.hasUnresolvedSelector = true;
|
|
771
824
|
return anim;
|
|
772
825
|
}
|
|
826
|
+
// ── Timeline Position Resolution ──────────────────────────────────────────
|
|
827
|
+
const GSAP_DEFAULT_DURATION = 0.5;
|
|
828
|
+
// NOTE: Label-based positions (e.g. "myLabel+=0.5") are not yet resolved —
|
|
829
|
+
// they fall through to parseFloat which returns null for non-numeric strings.
|
|
830
|
+
function resolvePositionString(pos, cursor, prevStart) {
|
|
831
|
+
const trimmed = pos.trim();
|
|
832
|
+
if (trimmed === "")
|
|
833
|
+
return cursor;
|
|
834
|
+
if (trimmed.startsWith("+=")) {
|
|
835
|
+
const n = Number.parseFloat(trimmed.slice(2));
|
|
836
|
+
return Number.isFinite(n) ? cursor + n : null;
|
|
837
|
+
}
|
|
838
|
+
if (trimmed.startsWith("-=")) {
|
|
839
|
+
const n = Number.parseFloat(trimmed.slice(2));
|
|
840
|
+
return Number.isFinite(n) ? cursor - n : null;
|
|
841
|
+
}
|
|
842
|
+
if (trimmed === "<")
|
|
843
|
+
return prevStart;
|
|
844
|
+
if (trimmed === ">")
|
|
845
|
+
return cursor;
|
|
846
|
+
if (trimmed.startsWith("<")) {
|
|
847
|
+
const n = Number.parseFloat(trimmed.slice(1));
|
|
848
|
+
return Number.isFinite(n) ? prevStart + n : null;
|
|
849
|
+
}
|
|
850
|
+
if (trimmed.startsWith(">")) {
|
|
851
|
+
const n = Number.parseFloat(trimmed.slice(1));
|
|
852
|
+
return Number.isFinite(n) ? cursor + n : null;
|
|
853
|
+
}
|
|
854
|
+
const n = Number.parseFloat(trimmed);
|
|
855
|
+
return Number.isFinite(n) ? n : null;
|
|
856
|
+
}
|
|
857
|
+
function applyTimelineDefaults(anims, defaults) {
|
|
858
|
+
if (!defaults)
|
|
859
|
+
return;
|
|
860
|
+
for (const anim of anims) {
|
|
861
|
+
if (anim.method === "set")
|
|
862
|
+
continue;
|
|
863
|
+
if (anim.duration === undefined && defaults.duration !== undefined) {
|
|
864
|
+
anim.duration = defaults.duration;
|
|
865
|
+
}
|
|
866
|
+
if (anim.ease === undefined && defaults.ease !== undefined) {
|
|
867
|
+
anim.ease = defaults.ease;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function resolveTimelinePositions(anims) {
|
|
872
|
+
let cursor = 0;
|
|
873
|
+
let prevStart = 0;
|
|
874
|
+
for (const anim of anims) {
|
|
875
|
+
const duration = anim.method === "set" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);
|
|
876
|
+
let start;
|
|
877
|
+
if (anim.implicitPosition) {
|
|
878
|
+
start = cursor;
|
|
879
|
+
}
|
|
880
|
+
else if (typeof anim.position === "number") {
|
|
881
|
+
start = anim.position;
|
|
882
|
+
}
|
|
883
|
+
else if (typeof anim.position === "string") {
|
|
884
|
+
start = resolvePositionString(anim.position, cursor, prevStart);
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
start = cursor;
|
|
888
|
+
}
|
|
889
|
+
if (start != null) {
|
|
890
|
+
anim.resolvedStart = Math.max(0, start);
|
|
891
|
+
prevStart = anim.resolvedStart;
|
|
892
|
+
cursor = Math.max(cursor, anim.resolvedStart + duration);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function sortBySourcePosition(calls) {
|
|
897
|
+
calls.sort((a, b) => {
|
|
898
|
+
const aLoc = a.node.callee?.property?.loc?.start;
|
|
899
|
+
const bLoc = b.node.callee?.property?.loc?.start;
|
|
900
|
+
if (!aLoc || !bLoc)
|
|
901
|
+
return 0;
|
|
902
|
+
return aLoc.line - bLoc.line || aLoc.column - bLoc.column;
|
|
903
|
+
});
|
|
904
|
+
}
|
|
773
905
|
// ── Stable ID Generation ───────────────────────────────────────────────────
|
|
906
|
+
/**
|
|
907
|
+
* IDs are transient — recomputed on every parse, never persisted across sessions.
|
|
908
|
+
* They exist only in ephemeral request/response payloads, React component state,
|
|
909
|
+
* and the in-memory keyframe cache (rebuilt on every page load). No database,
|
|
910
|
+
* localStorage, or file stores animation IDs, so changing the ID format (e.g.
|
|
911
|
+
* adding a `-scale`/`-position` suffix) is safe.
|
|
912
|
+
*/
|
|
774
913
|
function assignStableIds(anims) {
|
|
775
914
|
const counts = new Map();
|
|
776
915
|
return anims.map((anim) => {
|
|
777
916
|
const posKey = typeof anim.position === "number"
|
|
778
917
|
? String(Math.round(anim.position * 1000))
|
|
779
918
|
: String(anim.position);
|
|
780
|
-
const
|
|
919
|
+
const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : "";
|
|
920
|
+
const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;
|
|
781
921
|
const count = (counts.get(base) ?? 0) + 1;
|
|
782
922
|
counts.set(base, count);
|
|
783
923
|
const id = count === 1 ? base : `${base}-${count}`;
|
|
@@ -794,10 +934,14 @@ function parseGsapAst(script) {
|
|
|
794
934
|
const ast = parseScript(script);
|
|
795
935
|
const scope = collectScopeBindings(ast);
|
|
796
936
|
const targetBindings = collectTargetBindings(ast, scope);
|
|
797
|
-
const detection = findTimelineVar(ast);
|
|
937
|
+
const detection = findTimelineVar(ast, scope);
|
|
798
938
|
const timelineVar = detection.timelineVar ?? "tl";
|
|
799
939
|
const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);
|
|
800
|
-
|
|
940
|
+
sortBySourcePosition(calls);
|
|
941
|
+
const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope));
|
|
942
|
+
applyTimelineDefaults(rawAnims, detection.defaults);
|
|
943
|
+
resolveTimelinePositions(rawAnims);
|
|
944
|
+
const animations = assignStableIds(rawAnims);
|
|
801
945
|
const located = animations.map((animation, i) => ({
|
|
802
946
|
id: animation.id,
|
|
803
947
|
call: calls[i],
|
|
@@ -885,14 +1029,12 @@ function setVarsKey(varsArg, key, value) {
|
|
|
885
1029
|
}
|
|
886
1030
|
}
|
|
887
1031
|
/**
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
* untouched.
|
|
1032
|
+
* Filter an ObjectExpression's properties, keeping non-editable keys
|
|
1033
|
+
* and delegating the keep/drop decision for editable keys to `shouldKeep`.
|
|
891
1034
|
*/
|
|
892
|
-
function
|
|
1035
|
+
function filterEditableKeys(varsArg, shouldKeep) {
|
|
893
1036
|
if (varsArg?.type !== "ObjectExpression")
|
|
894
1037
|
return;
|
|
895
|
-
// Drop editable props no longer present.
|
|
896
1038
|
varsArg.properties = varsArg.properties.filter((p) => {
|
|
897
1039
|
if (!isObjectProperty(p))
|
|
898
1040
|
return true;
|
|
@@ -901,8 +1043,16 @@ function reconcileEditableProperties(varsArg, newProps) {
|
|
|
901
1043
|
return true;
|
|
902
1044
|
if (!isEditablePropertyKey(key))
|
|
903
1045
|
return true;
|
|
904
|
-
return key
|
|
1046
|
+
return shouldKeep(key);
|
|
905
1047
|
});
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Replace the editable-property keys on an ObjectExpression with `newProps`,
|
|
1051
|
+
* leaving `duration`, `ease`, `stagger`, callbacks and other non-editable keys
|
|
1052
|
+
* untouched.
|
|
1053
|
+
*/
|
|
1054
|
+
function reconcileEditableProperties(varsArg, newProps) {
|
|
1055
|
+
filterEditableKeys(varsArg, (key) => key in newProps);
|
|
906
1056
|
// Upsert each new prop, preserving the order keys first appeared.
|
|
907
1057
|
for (const [key, value] of Object.entries(newProps)) {
|
|
908
1058
|
setVarsKey(varsArg, key, value);
|
|
@@ -980,6 +1130,26 @@ export function updateAnimationInScript(script, animationId, updates) {
|
|
|
980
1130
|
applyUpdatesToCall(target.call, updates);
|
|
981
1131
|
return recast.print(parsed.ast).code;
|
|
982
1132
|
}
|
|
1133
|
+
function updateAnimationSelector(script, animationId, newSelector) {
|
|
1134
|
+
let parsed;
|
|
1135
|
+
try {
|
|
1136
|
+
parsed = parseGsapAst(script);
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
return script;
|
|
1140
|
+
}
|
|
1141
|
+
const target = parsed.located.find((l) => l.id === animationId);
|
|
1142
|
+
if (!target)
|
|
1143
|
+
return script;
|
|
1144
|
+
const selectorArg = target.call.path.node.arguments?.[0];
|
|
1145
|
+
if (selectorArg?.type === "StringLiteral") {
|
|
1146
|
+
selectorArg.value = newSelector;
|
|
1147
|
+
}
|
|
1148
|
+
else if (selectorArg?.type === "Identifier") {
|
|
1149
|
+
target.call.path.node.arguments[0] = { type: "StringLiteral", value: newSelector };
|
|
1150
|
+
}
|
|
1151
|
+
return recast.print(parsed.ast).code;
|
|
1152
|
+
}
|
|
983
1153
|
export function addAnimationToScript(script, animation) {
|
|
984
1154
|
let parsed;
|
|
985
1155
|
try {
|
|
@@ -1094,7 +1264,11 @@ export function removeAnimationFromScript(script, animationId) {
|
|
|
1094
1264
|
console.warn("[gsap-parser] removeAnimationFromScript parse failed:", e);
|
|
1095
1265
|
return script;
|
|
1096
1266
|
}
|
|
1097
|
-
|
|
1267
|
+
let target = parsed.located.find((l) => l.id === animationId);
|
|
1268
|
+
if (!target) {
|
|
1269
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1270
|
+
target = parsed.located.find((l) => l.id === convertedId);
|
|
1271
|
+
}
|
|
1098
1272
|
if (!target)
|
|
1099
1273
|
return script;
|
|
1100
1274
|
const node = target.call.node;
|
|
@@ -1116,7 +1290,150 @@ export function removeAnimationFromScript(script, animationId) {
|
|
|
1116
1290
|
}
|
|
1117
1291
|
return recast.print(parsed.ast).code;
|
|
1118
1292
|
}
|
|
1293
|
+
function insertInheritedStateSet(script, selector, position, properties) {
|
|
1294
|
+
let parsed;
|
|
1295
|
+
try {
|
|
1296
|
+
parsed = parseGsapAst(script);
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
return script;
|
|
1300
|
+
}
|
|
1301
|
+
const tlVar = parsed.timelineVar;
|
|
1302
|
+
const props = Object.entries(properties)
|
|
1303
|
+
.map(([k, v]) => `${k}: ${typeof v === "string" ? JSON.stringify(v) : v}`)
|
|
1304
|
+
.join(", ");
|
|
1305
|
+
const code = `${tlVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;
|
|
1306
|
+
const newStatement = parseScript(code).program.body[0];
|
|
1307
|
+
const anchor = findTimelineDeclarationPath(parsed.ast, tlVar);
|
|
1308
|
+
if (anchor) {
|
|
1309
|
+
anchor.insertAfter(newStatement);
|
|
1310
|
+
}
|
|
1311
|
+
else if (parsed.located.length > 0) {
|
|
1312
|
+
const firstTween = parsed.located[0].call;
|
|
1313
|
+
const stmtPath = findStatementPath(firstTween.path);
|
|
1314
|
+
if (stmtPath)
|
|
1315
|
+
stmtPath.insertBefore(newStatement);
|
|
1316
|
+
else
|
|
1317
|
+
parsed.ast.program.body.unshift(newStatement);
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
parsed.ast.program.body.push(newStatement);
|
|
1321
|
+
}
|
|
1322
|
+
return recast.print(parsed.ast).code;
|
|
1323
|
+
}
|
|
1324
|
+
// fallow-ignore-next-line complexity
|
|
1325
|
+
export function splitAnimationsInScript(script, opts) {
|
|
1326
|
+
const parsed = parseGsapScript(script);
|
|
1327
|
+
const originalSelector = `#${opts.originalId}`;
|
|
1328
|
+
const newSelector = `#${opts.newId}`;
|
|
1329
|
+
const skippedSelectors = [];
|
|
1330
|
+
for (const a of parsed.animations) {
|
|
1331
|
+
if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {
|
|
1332
|
+
skippedSelectors.push(a.targetSelector);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const matching = parsed.animations.filter((a) => a.targetSelector === originalSelector);
|
|
1336
|
+
if (matching.length === 0)
|
|
1337
|
+
return { script, skippedSelectors };
|
|
1338
|
+
let result = script;
|
|
1339
|
+
const newElementStart = opts.splitTime;
|
|
1340
|
+
const inheritedProps = {};
|
|
1341
|
+
// Reverse iteration: updateAnimationSelector mutates selectors in the source
|
|
1342
|
+
// string, which can shift count-based ID suffixes (e.g. "#hero-1" → "#hero-2")
|
|
1343
|
+
// for later animations. Processing last-to-first prevents stale ID collisions.
|
|
1344
|
+
for (let i = matching.length - 1; i >= 0; i--) {
|
|
1345
|
+
const anim = matching[i];
|
|
1346
|
+
const pos = typeof anim.position === "number" ? anim.position : 0;
|
|
1347
|
+
const dur = anim.duration ?? 0;
|
|
1348
|
+
const animEnd = pos + dur;
|
|
1349
|
+
if (anim.keyframes) {
|
|
1350
|
+
if (pos >= opts.splitTime) {
|
|
1351
|
+
result = updateAnimationSelector(result, anim.id, newSelector);
|
|
1352
|
+
}
|
|
1353
|
+
else if (animEnd > opts.splitTime) {
|
|
1354
|
+
// Spanning keyframes can't be correctly split without renormalizing
|
|
1355
|
+
// percentages and durations — leave on original, warn the caller.
|
|
1356
|
+
skippedSelectors.push(`${originalSelector} (keyframes spanning split)`);
|
|
1357
|
+
const kfs = anim.keyframes.keyframes;
|
|
1358
|
+
for (const kf of kfs) {
|
|
1359
|
+
const kfTime = pos + (kf.percentage / 100) * dur;
|
|
1360
|
+
if (kfTime <= opts.splitTime) {
|
|
1361
|
+
for (const [k, v] of Object.entries(kf.properties)) {
|
|
1362
|
+
inheritedProps[k] = v;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
// Entirely before split — extract final keyframe properties
|
|
1369
|
+
const kfs = anim.keyframes.keyframes;
|
|
1370
|
+
if (kfs.length > 0) {
|
|
1371
|
+
for (const [k, v] of Object.entries(kfs[kfs.length - 1].properties)) {
|
|
1372
|
+
inheritedProps[k] = v;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (animEnd <= opts.splitTime) {
|
|
1379
|
+
for (const [k, v] of Object.entries(anim.properties)) {
|
|
1380
|
+
inheritedProps[k] = v;
|
|
1381
|
+
}
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (pos >= opts.splitTime) {
|
|
1385
|
+
result = updateAnimationSelector(result, anim.id, newSelector);
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
// Spans the split — use linear interpolation to compute mid-values,
|
|
1389
|
+
// then .fromTo() on the clone so both halves play the correct range.
|
|
1390
|
+
// For .fromTo() tweens we have explicit from-values; for .to() tweens
|
|
1391
|
+
// we use accumulated state from prior animations, defaulting to 0 for
|
|
1392
|
+
// unknown numeric properties (the standard GSAP transform initial state).
|
|
1393
|
+
const progress = dur > 0 ? (opts.splitTime - pos) / dur : 0;
|
|
1394
|
+
const fromSource = anim.fromProperties ?? inheritedProps;
|
|
1395
|
+
const midProps = {};
|
|
1396
|
+
for (const [k, v] of Object.entries(anim.properties)) {
|
|
1397
|
+
if (typeof v !== "number") {
|
|
1398
|
+
midProps[k] = v;
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const fromVal = typeof fromSource[k] === "number" ? fromSource[k] : 0;
|
|
1402
|
+
midProps[k] = fromVal + (v - fromVal) * progress;
|
|
1403
|
+
}
|
|
1404
|
+
const firstHalfDuration = opts.splitTime - pos;
|
|
1405
|
+
result = updateAnimationInScript(result, anim.id, {
|
|
1406
|
+
duration: firstHalfDuration,
|
|
1407
|
+
properties: midProps,
|
|
1408
|
+
});
|
|
1409
|
+
const secondHalfDuration = animEnd - opts.splitTime;
|
|
1410
|
+
const addResult = addAnimationToScript(result, {
|
|
1411
|
+
targetSelector: newSelector,
|
|
1412
|
+
method: "fromTo",
|
|
1413
|
+
position: newElementStart,
|
|
1414
|
+
duration: secondHalfDuration,
|
|
1415
|
+
properties: { ...anim.properties },
|
|
1416
|
+
fromProperties: { ...midProps },
|
|
1417
|
+
ease: anim.ease,
|
|
1418
|
+
extras: anim.extras,
|
|
1419
|
+
});
|
|
1420
|
+
result = addResult.script;
|
|
1421
|
+
for (const [k, v] of Object.entries(midProps)) {
|
|
1422
|
+
inheritedProps[k] = v;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (Object.keys(inheritedProps).length > 0) {
|
|
1426
|
+
result = insertInheritedStateSet(result, newSelector, newElementStart, inheritedProps);
|
|
1427
|
+
}
|
|
1428
|
+
return { script: result, skippedSelectors };
|
|
1429
|
+
}
|
|
1119
1430
|
// ── Keyframe Mutation Functions ────────────────────────────────────────────
|
|
1431
|
+
function sortedKeyframes(kfs) {
|
|
1432
|
+
return kfs.slice().sort((a, b) => a.percentage - b.percentage);
|
|
1433
|
+
}
|
|
1434
|
+
function keyframePropsToCode(kf) {
|
|
1435
|
+
return Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
1436
|
+
}
|
|
1120
1437
|
/** Remove a named property from an ObjectExpression's properties array. */
|
|
1121
1438
|
function removeVarsKey(varsArg, key) {
|
|
1122
1439
|
if (varsArg?.type !== "ObjectExpression")
|
|
@@ -1128,6 +1445,23 @@ function percentageFromKey(key) {
|
|
|
1128
1445
|
const m = PERCENTAGE_KEY_RE.exec(key);
|
|
1129
1446
|
return m ? Number.parseFloat(m[1]) : Number.NaN;
|
|
1130
1447
|
}
|
|
1448
|
+
const PCT_TOLERANCE = 2;
|
|
1449
|
+
function findKeyframePropByPct(kfNode, percentage) {
|
|
1450
|
+
const props = kfNode.properties;
|
|
1451
|
+
for (let i = 0; i < props.length; i++) {
|
|
1452
|
+
if (!isObjectProperty(props[i]))
|
|
1453
|
+
continue;
|
|
1454
|
+
const key = propKeyName(props[i]);
|
|
1455
|
+
if (typeof key !== "string")
|
|
1456
|
+
continue;
|
|
1457
|
+
const parsed = percentageFromKey(key);
|
|
1458
|
+
if (Number.isNaN(parsed))
|
|
1459
|
+
continue;
|
|
1460
|
+
if (Math.abs(parsed - percentage) <= PCT_TOLERANCE)
|
|
1461
|
+
return { idx: i, prop: props[i] };
|
|
1462
|
+
}
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1131
1465
|
/** Build a keyframe value AST node from properties and optional ease. */
|
|
1132
1466
|
function buildKeyframeValueNode(properties, ease) {
|
|
1133
1467
|
const entries = Object.entries(properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
@@ -1176,6 +1510,24 @@ function collapseKeyframesToFlat(varsArg, record) {
|
|
|
1176
1510
|
removeVarsKey(varsArg, "keyframes");
|
|
1177
1511
|
removeVarsKey(varsArg, "easeEach");
|
|
1178
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Locate an animation's keyframes ObjectExpression and build the percentage key.
|
|
1515
|
+
* Shared preamble for addKeyframeToScript, removeKeyframeFromScript, and
|
|
1516
|
+
* updateKeyframeInScript.
|
|
1517
|
+
*/
|
|
1518
|
+
function locateKeyframeCtx(script, animationId, percentage) {
|
|
1519
|
+
let loc = locateAnimation(script, animationId);
|
|
1520
|
+
if (!loc) {
|
|
1521
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1522
|
+
loc = locateAnimation(script, convertedId);
|
|
1523
|
+
}
|
|
1524
|
+
if (!loc)
|
|
1525
|
+
return null;
|
|
1526
|
+
const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
|
|
1527
|
+
if (!kfNode)
|
|
1528
|
+
return null;
|
|
1529
|
+
return { loc, kfNode, pctKey: `${percentage}%` };
|
|
1530
|
+
}
|
|
1179
1531
|
/**
|
|
1180
1532
|
* Insert a keyframe at the given percentage in an existing percentage-keyframes
|
|
1181
1533
|
* object. If the percentage already exists, its value is replaced.
|
|
@@ -1204,10 +1556,19 @@ export function addKeyframeToScript(script, animationId, percentage, properties,
|
|
|
1204
1556
|
}
|
|
1205
1557
|
const pctKey = `${percentage}%`;
|
|
1206
1558
|
const newValueNode = buildKeyframeValueNode(properties, ease);
|
|
1207
|
-
//
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1559
|
+
// Merge into existing keyframe at this percentage, or insert new
|
|
1560
|
+
const existing = findKeyframePropByPct(kfNode, percentage);
|
|
1561
|
+
if (existing) {
|
|
1562
|
+
if (existing.prop.value?.type === "ObjectExpression") {
|
|
1563
|
+
const existingRecord = objectExpressionToRecord(existing.prop.value, loc.parsed.scope);
|
|
1564
|
+
const merged = { ...existingRecord };
|
|
1565
|
+
for (const [k, v] of Object.entries(properties))
|
|
1566
|
+
merged[k] = v;
|
|
1567
|
+
existing.prop.value = buildKeyframeValueNode(merged, ease ?? (typeof existingRecord.ease === "string" ? existingRecord.ease : undefined));
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
existing.prop.value = newValueNode;
|
|
1571
|
+
}
|
|
1211
1572
|
}
|
|
1212
1573
|
else {
|
|
1213
1574
|
// Build the new property node with a quoted percentage key
|
|
@@ -1285,16 +1646,14 @@ export function addKeyframeToScript(script, animationId, percentage, properties,
|
|
|
1285
1646
|
* remaining keyframe's properties.
|
|
1286
1647
|
*/
|
|
1287
1648
|
export function removeKeyframeFromScript(script, animationId, percentage) {
|
|
1288
|
-
const
|
|
1289
|
-
if (!
|
|
1649
|
+
const ctx = locateKeyframeCtx(script, animationId, percentage);
|
|
1650
|
+
if (!ctx)
|
|
1290
1651
|
return script;
|
|
1291
|
-
const kfNode =
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const pctKey = `${percentage}%`;
|
|
1295
|
-
const removeIdx = kfNode.properties.findIndex((p) => isObjectProperty(p) && propKeyName(p) === pctKey);
|
|
1296
|
-
if (removeIdx === -1)
|
|
1652
|
+
const { loc, kfNode } = ctx;
|
|
1653
|
+
const match = findKeyframePropByPct(kfNode, percentage);
|
|
1654
|
+
if (!match)
|
|
1297
1655
|
return script;
|
|
1656
|
+
const removeIdx = match.idx;
|
|
1298
1657
|
kfNode.properties.splice(removeIdx, 1);
|
|
1299
1658
|
const remainingKfs = filterPercentageProps(kfNode);
|
|
1300
1659
|
if (remainingKfs.length < 2) {
|
|
@@ -1309,17 +1668,14 @@ export function removeKeyframeFromScript(script, animationId, percentage) {
|
|
|
1309
1668
|
* Replace the properties (and optionally ease) at an existing keyframe percentage.
|
|
1310
1669
|
*/
|
|
1311
1670
|
export function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
|
|
1312
|
-
const
|
|
1313
|
-
if (!
|
|
1671
|
+
const ctx = locateKeyframeCtx(script, animationId, percentage);
|
|
1672
|
+
if (!ctx)
|
|
1314
1673
|
return script;
|
|
1315
|
-
const kfNode =
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const pctKey = `${percentage}%`;
|
|
1319
|
-
const existing = kfNode.properties.find((p) => isObjectProperty(p) && propKeyName(p) === pctKey);
|
|
1320
|
-
if (!existing)
|
|
1674
|
+
const { loc, kfNode } = ctx;
|
|
1675
|
+
const match = findKeyframePropByPct(kfNode, percentage);
|
|
1676
|
+
if (!match)
|
|
1321
1677
|
return script;
|
|
1322
|
-
|
|
1678
|
+
match.prop.value = buildKeyframeValueNode(properties, ease);
|
|
1323
1679
|
return recast.print(loc.parsed.ast).code;
|
|
1324
1680
|
}
|
|
1325
1681
|
/** Resolve from/to property maps for a tween being converted to keyframes. */
|
|
@@ -1333,46 +1689,60 @@ const CSS_IDENTITY = {
|
|
|
1333
1689
|
function cssIdentityValue(prop) {
|
|
1334
1690
|
return CSS_IDENTITY[prop] ?? 0;
|
|
1335
1691
|
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Resolve the 0% (from) and 100% (to) property maps for a tween being
|
|
1694
|
+
* converted to percentage keyframes.
|
|
1695
|
+
*
|
|
1696
|
+
* @param resolvedFromValues — Despite the "from" in the name (historical), these
|
|
1697
|
+
* are runtime-captured DOM values that override the conversion endpoint:
|
|
1698
|
+
* - For to(): overrides fromProps (the 0% state / where the element is now).
|
|
1699
|
+
* - For from(): overrides toProps (the 100% state / where the element rests).
|
|
1700
|
+
* - For fromTo(): merges into toProps (the 100% endpoint the user is editing).
|
|
1701
|
+
*/
|
|
1336
1702
|
function resolveConversionProps(anim, resolvedFromValues) {
|
|
1337
1703
|
if (anim.method === "to") {
|
|
1338
|
-
if (resolvedFromValues) {
|
|
1339
|
-
return { fromProps: resolvedFromValues, toProps: { ...anim.properties } };
|
|
1340
|
-
}
|
|
1341
1704
|
const identityFrom = {};
|
|
1342
1705
|
for (const [key, val] of Object.entries(anim.properties)) {
|
|
1343
1706
|
if (val != null)
|
|
1344
1707
|
identityFrom[key] = typeof val === "number" ? cssIdentityValue(key) : val;
|
|
1345
1708
|
}
|
|
1346
|
-
|
|
1709
|
+
const fromProps = resolvedFromValues
|
|
1710
|
+
? { ...identityFrom, ...resolvedFromValues }
|
|
1711
|
+
: identityFrom;
|
|
1712
|
+
return { fromProps, toProps: { ...anim.properties } };
|
|
1347
1713
|
}
|
|
1348
1714
|
if (anim.method === "from") {
|
|
1349
|
-
if (resolvedFromValues) {
|
|
1350
|
-
return { fromProps: { ...anim.properties }, toProps: resolvedFromValues };
|
|
1351
|
-
}
|
|
1352
1715
|
const identityTo = {};
|
|
1353
1716
|
for (const [key, val] of Object.entries(anim.properties)) {
|
|
1354
1717
|
if (val != null)
|
|
1355
1718
|
identityTo[key] = typeof val === "number" ? cssIdentityValue(key) : val;
|
|
1356
1719
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1720
|
+
const toProps = resolvedFromValues ? { ...identityTo, ...resolvedFromValues } : identityTo;
|
|
1721
|
+
return { fromProps: { ...anim.properties }, toProps };
|
|
1722
|
+
}
|
|
1723
|
+
// fromTo(fromVars, toVars): anim.fromProperties = fromVars (0% state),
|
|
1724
|
+
// anim.properties = toVars (100% state). resolvedFromValues contains the
|
|
1725
|
+
// current DOM position from a drag — it represents the NEW destination, so
|
|
1726
|
+
// it merges into toProps (the 100% endpoint the user is editing), NOT into
|
|
1727
|
+
// fromProps. This is intentional and not inverted.
|
|
1728
|
+
const toProps = resolvedFromValues
|
|
1729
|
+
? { ...anim.properties, ...resolvedFromValues }
|
|
1730
|
+
: { ...anim.properties };
|
|
1731
|
+
return { fromProps: { ...(anim.fromProperties ?? {}) }, toProps };
|
|
1361
1732
|
}
|
|
1362
1733
|
/** Strip editable properties and ease/keyframes keys from a varsArg. */
|
|
1363
1734
|
function stripEditableAndEase(varsArg) {
|
|
1735
|
+
// ease is a BUILTIN_VAR_KEY (not editable), so filterEditableKeys won't remove it —
|
|
1736
|
+
// drop it explicitly before filtering, along with keyframes.
|
|
1364
1737
|
if (varsArg?.type !== "ObjectExpression")
|
|
1365
1738
|
return;
|
|
1366
1739
|
varsArg.properties = varsArg.properties.filter((p) => {
|
|
1367
1740
|
if (!isObjectProperty(p))
|
|
1368
1741
|
return true;
|
|
1369
1742
|
const key = propKeyName(p);
|
|
1370
|
-
|
|
1371
|
-
return true;
|
|
1372
|
-
if (key === "ease" || key === "keyframes")
|
|
1373
|
-
return false;
|
|
1374
|
-
return !isEditablePropertyKey(key);
|
|
1743
|
+
return key !== "ease" && key !== "keyframes";
|
|
1375
1744
|
});
|
|
1745
|
+
filterEditableKeys(varsArg, () => false);
|
|
1376
1746
|
}
|
|
1377
1747
|
/** Build and prepend a keyframes property node onto varsArg. */
|
|
1378
1748
|
function insertKeyframesProp(varsArg, fromProps, toProps, easeEach) {
|
|
@@ -1391,7 +1761,11 @@ function insertKeyframesProp(varsArg, fromProps, toProps, easeEach) {
|
|
|
1391
1761
|
* the "to" state for `from()` tweens (the values the DOM would resolve to).
|
|
1392
1762
|
*/
|
|
1393
1763
|
export function convertToKeyframesInScript(script, animationId, resolvedFromValues) {
|
|
1394
|
-
|
|
1764
|
+
let loc = locateAnimation(script, animationId);
|
|
1765
|
+
if (!loc) {
|
|
1766
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1767
|
+
loc = locateAnimation(script, convertedId);
|
|
1768
|
+
}
|
|
1395
1769
|
if (!loc)
|
|
1396
1770
|
return script;
|
|
1397
1771
|
const anim = loc.target.animation;
|
|
@@ -1418,7 +1792,11 @@ export function convertToKeyframesInScript(script, animationId, resolvedFromValu
|
|
|
1418
1792
|
* last keyframe's properties.
|
|
1419
1793
|
*/
|
|
1420
1794
|
export function removeAllKeyframesFromScript(script, animationId) {
|
|
1421
|
-
|
|
1795
|
+
let loc = locateAnimation(script, animationId);
|
|
1796
|
+
if (!loc) {
|
|
1797
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1798
|
+
loc = locateAnimation(script, convertedId);
|
|
1799
|
+
}
|
|
1422
1800
|
if (!loc)
|
|
1423
1801
|
return script;
|
|
1424
1802
|
const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
|
|
@@ -1443,7 +1821,11 @@ export function removeAllKeyframesFromScript(script, animationId) {
|
|
|
1443
1821
|
* Called when the user first edits a dynamically-generated keyframe in the studio.
|
|
1444
1822
|
*/
|
|
1445
1823
|
export function materializeKeyframesInScript(script, animationId, keyframes, easeEach, resolvedSelector) {
|
|
1446
|
-
|
|
1824
|
+
let loc = locateAnimation(script, animationId);
|
|
1825
|
+
if (!loc) {
|
|
1826
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1827
|
+
loc = locateAnimation(script, convertedId);
|
|
1828
|
+
}
|
|
1447
1829
|
if (!loc)
|
|
1448
1830
|
return script;
|
|
1449
1831
|
const varsArg = loc.target.call.varsArg;
|
|
@@ -1452,9 +1834,8 @@ export function materializeKeyframesInScript(script, animationId, keyframes, eas
|
|
|
1452
1834
|
loc.target.call.node.arguments[0] = parseExpr(JSON.stringify(resolvedSelector));
|
|
1453
1835
|
}
|
|
1454
1836
|
const entries = [];
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
const propEntries = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
1837
|
+
for (const kf of sortedKeyframes(keyframes)) {
|
|
1838
|
+
const propEntries = keyframePropsToCode(kf);
|
|
1458
1839
|
if (kf.ease)
|
|
1459
1840
|
propEntries.push(`ease: ${JSON.stringify(kf.ease)}`);
|
|
1460
1841
|
entries.push(`${JSON.stringify(kf.percentage + "%")}: { ${propEntries.join(", ")} }`);
|
|
@@ -1652,6 +2033,136 @@ export function removeArcPathFromScript(script, animationId) {
|
|
|
1652
2033
|
segments: [],
|
|
1653
2034
|
});
|
|
1654
2035
|
}
|
|
2036
|
+
// ── Split Into Property Groups ────────────────────────────────────────────
|
|
2037
|
+
/**
|
|
2038
|
+
* Split a multi-group tween into separate per-group tweens. Each resulting
|
|
2039
|
+
* tween contains only properties belonging to one property group (position,
|
|
2040
|
+
* scale, rotation, visual, etc.). `transformOrigin` stays with the group that
|
|
2041
|
+
* has the most properties. If the tween already belongs to a single group,
|
|
2042
|
+
* returns the script unchanged with the original ID.
|
|
2043
|
+
*/
|
|
2044
|
+
// fallow-ignore-next-line complexity
|
|
2045
|
+
export function splitIntoPropertyGroups(script, animationId) {
|
|
2046
|
+
let loc = locateAnimation(script, animationId);
|
|
2047
|
+
if (!loc) {
|
|
2048
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
2049
|
+
loc = locateAnimation(script, convertedId);
|
|
2050
|
+
}
|
|
2051
|
+
if (!loc)
|
|
2052
|
+
return { script, ids: [animationId] };
|
|
2053
|
+
const anim = loc.target.animation;
|
|
2054
|
+
// Collect the properties to partition. For keyframed tweens, gather the
|
|
2055
|
+
// union of all properties across all keyframes. For flat tweens, use the
|
|
2056
|
+
// tween's own properties map.
|
|
2057
|
+
const allPropKeys = new Set();
|
|
2058
|
+
if (anim.keyframes) {
|
|
2059
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
2060
|
+
for (const k of Object.keys(kf.properties))
|
|
2061
|
+
allPropKeys.add(k);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
for (const k of Object.keys(anim.properties))
|
|
2066
|
+
allPropKeys.add(k);
|
|
2067
|
+
}
|
|
2068
|
+
// Partition properties into groups (excluding transformOrigin — handled below).
|
|
2069
|
+
const groupProps = new Map();
|
|
2070
|
+
for (const key of allPropKeys) {
|
|
2071
|
+
if (key === "transformOrigin")
|
|
2072
|
+
continue;
|
|
2073
|
+
const group = classifyPropertyGroup(key);
|
|
2074
|
+
let arr = groupProps.get(group);
|
|
2075
|
+
if (!arr) {
|
|
2076
|
+
arr = [];
|
|
2077
|
+
groupProps.set(group, arr);
|
|
2078
|
+
}
|
|
2079
|
+
arr.push(key);
|
|
2080
|
+
}
|
|
2081
|
+
// Only one group (or zero) — no split needed.
|
|
2082
|
+
if (groupProps.size <= 1)
|
|
2083
|
+
return { script, ids: [anim.id] };
|
|
2084
|
+
// Assign transformOrigin to the group with the most properties.
|
|
2085
|
+
if (allPropKeys.has("transformOrigin")) {
|
|
2086
|
+
let largestGroup;
|
|
2087
|
+
let largestCount = 0;
|
|
2088
|
+
for (const [group, props] of groupProps) {
|
|
2089
|
+
if (props.length > largestCount) {
|
|
2090
|
+
largestCount = props.length;
|
|
2091
|
+
largestGroup = group;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (largestGroup) {
|
|
2095
|
+
groupProps.get(largestGroup).push("transformOrigin");
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
// Build per-group tweens and insert them, then remove the original.
|
|
2099
|
+
let result = script;
|
|
2100
|
+
// Remove the original tween first.
|
|
2101
|
+
result = removeAnimationFromScript(result, anim.id);
|
|
2102
|
+
// Insert one tween per group. Iteration order of the Map follows insertion
|
|
2103
|
+
// order, which mirrors the order properties were encountered.
|
|
2104
|
+
for (const [, props] of groupProps) {
|
|
2105
|
+
const propSet = new Set(props);
|
|
2106
|
+
if (anim.keyframes) {
|
|
2107
|
+
// Build keyframes containing only this group's properties per keyframe.
|
|
2108
|
+
const groupKeyframes = [];
|
|
2109
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
2110
|
+
const filtered = {};
|
|
2111
|
+
for (const [k, v] of Object.entries(kf.properties)) {
|
|
2112
|
+
if (propSet.has(k))
|
|
2113
|
+
filtered[k] = v;
|
|
2114
|
+
}
|
|
2115
|
+
// Skip keyframes where this group has zero properties.
|
|
2116
|
+
if (Object.keys(filtered).length === 0)
|
|
2117
|
+
continue;
|
|
2118
|
+
groupKeyframes.push({
|
|
2119
|
+
percentage: kf.percentage,
|
|
2120
|
+
properties: filtered,
|
|
2121
|
+
...(kf.ease ? { ease: kf.ease } : {}),
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
if (groupKeyframes.length === 0)
|
|
2125
|
+
continue;
|
|
2126
|
+
const addResult = addAnimationWithKeyframesToScript(result, anim.targetSelector, typeof anim.position === "number" ? anim.position : 0, anim.duration ?? 0.5, groupKeyframes, anim.keyframes.easeEach ?? anim.ease);
|
|
2127
|
+
result = addResult.script;
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
// Flat tween — filter properties to this group.
|
|
2131
|
+
const groupProperties = {};
|
|
2132
|
+
for (const [k, v] of Object.entries(anim.properties)) {
|
|
2133
|
+
if (propSet.has(k))
|
|
2134
|
+
groupProperties[k] = v;
|
|
2135
|
+
}
|
|
2136
|
+
if (Object.keys(groupProperties).length === 0)
|
|
2137
|
+
continue;
|
|
2138
|
+
let fromProperties;
|
|
2139
|
+
if (anim.method === "fromTo" && anim.fromProperties) {
|
|
2140
|
+
fromProperties = {};
|
|
2141
|
+
for (const [k, v] of Object.entries(anim.fromProperties)) {
|
|
2142
|
+
if (propSet.has(k))
|
|
2143
|
+
fromProperties[k] = v;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const addResult = addAnimationToScript(result, {
|
|
2147
|
+
targetSelector: anim.targetSelector,
|
|
2148
|
+
method: anim.method,
|
|
2149
|
+
position: anim.position,
|
|
2150
|
+
duration: anim.duration,
|
|
2151
|
+
ease: anim.ease,
|
|
2152
|
+
properties: groupProperties,
|
|
2153
|
+
fromProperties,
|
|
2154
|
+
extras: anim.extras,
|
|
2155
|
+
});
|
|
2156
|
+
result = addResult.script;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
// Re-parse to collect the new IDs.
|
|
2160
|
+
const reParsed = parseGsapAst(result);
|
|
2161
|
+
const newIds = reParsed.located
|
|
2162
|
+
.filter((l) => l.animation.targetSelector === anim.targetSelector)
|
|
2163
|
+
.map((l) => l.id);
|
|
2164
|
+
return { script: result, ids: newIds };
|
|
2165
|
+
}
|
|
1655
2166
|
/**
|
|
1656
2167
|
* Replace a dynamic loop that generates multiple tween calls with individual
|
|
1657
2168
|
* static `tl.to()` calls — one per element. Finds the loop containing the
|
|
@@ -1698,15 +2209,14 @@ export function unrollDynamicAnimations(script, animationId, elements) {
|
|
|
1698
2209
|
const calls = [];
|
|
1699
2210
|
for (const el of elements) {
|
|
1700
2211
|
const kfEntries = [];
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1703
|
-
const propEntries = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
2212
|
+
for (const kf of sortedKeyframes(el.keyframes)) {
|
|
2213
|
+
const propEntries = keyframePropsToCode(kf);
|
|
1704
2214
|
kfEntries.push(`${JSON.stringify(kf.percentage + "%")}: { ${propEntries.join(", ")} }`);
|
|
1705
2215
|
}
|
|
1706
2216
|
if (el.easeEach) {
|
|
1707
2217
|
kfEntries.push(`easeEach: ${JSON.stringify(el.easeEach)}`);
|
|
1708
2218
|
}
|
|
1709
|
-
calls.push(
|
|
2219
|
+
calls.push(`${loc.parsed.timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: { ${kfEntries.join(", ")} }, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`);
|
|
1710
2220
|
}
|
|
1711
2221
|
const replacement = calls.join("\n ");
|
|
1712
2222
|
if (loopNode) {
|