@hyperframes/core 0.7.14 → 0.7.15
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/staticGuard.js +1 -1
- package/dist/compiler/staticGuard.js.map +1 -1
- package/dist/core.types.d.ts +2 -369
- package/dist/core.types.d.ts.map +1 -1
- package/dist/core.types.js +1 -84
- package/dist/core.types.js.map +1 -1
- package/dist/generators/hyperframes.d.ts +1 -1
- package/dist/generators/hyperframes.d.ts.map +1 -1
- package/dist/generators/hyperframes.js +1 -1
- package/dist/generators/hyperframes.js.map +1 -1
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/lint/index.d.ts +2 -2
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint/index.js +2 -1
- package/dist/lint/index.js.map +1 -1
- package/dist/parsers/gsapConstants.d.ts +2 -12
- package/dist/parsers/gsapConstants.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.js +2 -110
- package/dist/parsers/gsapConstants.js.map +1 -1
- package/dist/parsers/gsapParserAcorn.d.ts +2 -48
- package/dist/parsers/gsapParserAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapParserAcorn.js +2 -1057
- package/dist/parsers/gsapParserAcorn.js.map +1 -1
- package/dist/parsers/gsapParserExports.d.ts +2 -20
- package/dist/parsers/gsapParserExports.d.ts.map +1 -1
- package/dist/parsers/gsapParserExports.js +2 -8
- package/dist/parsers/gsapParserExports.js.map +1 -1
- package/dist/parsers/gsapWriterAcorn.d.ts +2 -90
- package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapWriterAcorn.js +2 -2016
- package/dist/parsers/gsapWriterAcorn.js.map +1 -1
- package/dist/parsers/hfIds.d.ts +2 -16
- package/dist/parsers/hfIds.d.ts.map +1 -1
- package/dist/parsers/hfIds.js +2 -127
- package/dist/parsers/hfIds.js.map +1 -1
- package/dist/parsers/springEase.d.ts +2 -28
- package/dist/parsers/springEase.d.ts.map +1 -1
- package/dist/parsers/springEase.js +2 -69
- package/dist/parsers/springEase.js.map +1 -1
- package/dist/slideshow/index.d.ts +1 -0
- package/dist/slideshow/index.d.ts.map +1 -1
- package/dist/slideshow/index.js +1 -0
- package/dist/slideshow/index.js.map +1 -1
- package/dist/studio-api/helpers/draftMarkers.d.ts +2 -10
- package/dist/studio-api/helpers/draftMarkers.d.ts.map +1 -1
- package/dist/studio-api/helpers/draftMarkers.js +2 -10
- package/dist/studio-api/helpers/draftMarkers.js.map +1 -1
- package/dist/studio-api/helpers/finiteMutation.d.ts +2 -10
- package/dist/studio-api/helpers/finiteMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/finiteMutation.js +2 -20
- package/dist/studio-api/helpers/finiteMutation.js.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts +2 -12
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.js +2 -632
- package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/studio-api/helpers/screenshotClip.d.ts +2 -7
- package/dist/studio-api/helpers/screenshotClip.d.ts.map +1 -1
- package/dist/studio-api/helpers/screenshotClip.js +2 -21
- package/dist/studio-api/helpers/screenshotClip.js.map +1 -1
- package/dist/studio-api/helpers/studioMotionRenderScript.d.ts +2 -8
- package/dist/studio-api/helpers/studioMotionRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/studioMotionRenderScript.js +2 -207
- package/dist/studio-api/helpers/studioMotionRenderScript.js.map +1 -1
- package/dist/studio-api/index.d.ts +2 -9
- package/dist/studio-api/index.d.ts.map +1 -1
- package/dist/studio-api/index.js +2 -8
- package/dist/studio-api/index.js.map +1 -1
- package/package.json +16 -18
- package/dist/generated/runtime-inline.js +0 -11
- package/dist/hyperframe.manifest.json +0 -22
- package/dist/hyperframe.runtime.mjs +0 -325
- package/dist/lint/context.d.ts +0 -17
- package/dist/lint/context.d.ts.map +0 -1
- package/dist/lint/context.js +0 -37
- package/dist/lint/context.js.map +0 -1
- package/dist/lint/hyperframeLinter.d.ts +0 -14
- package/dist/lint/hyperframeLinter.d.ts.map +0 -1
- package/dist/lint/hyperframeLinter.js +0 -132
- package/dist/lint/hyperframeLinter.js.map +0 -1
- package/dist/lint/rules/adapters.d.ts +0 -3
- package/dist/lint/rules/adapters.d.ts.map +0 -1
- package/dist/lint/rules/adapters.js +0 -40
- package/dist/lint/rules/adapters.js.map +0 -1
- package/dist/lint/rules/captions.d.ts +0 -3
- package/dist/lint/rules/captions.d.ts.map +0 -1
- package/dist/lint/rules/captions.js +0 -242
- package/dist/lint/rules/captions.js.map +0 -1
- package/dist/lint/rules/composition.d.ts +0 -5
- package/dist/lint/rules/composition.d.ts.map +0 -1
- package/dist/lint/rules/composition.js +0 -694
- package/dist/lint/rules/composition.js.map +0 -1
- package/dist/lint/rules/core.d.ts +0 -3
- package/dist/lint/rules/core.d.ts.map +0 -1
- package/dist/lint/rules/core.js +0 -432
- package/dist/lint/rules/core.js.map +0 -1
- package/dist/lint/rules/fonts.d.ts +0 -3
- package/dist/lint/rules/fonts.d.ts.map +0 -1
- package/dist/lint/rules/fonts.js +0 -205
- package/dist/lint/rules/fonts.js.map +0 -1
- package/dist/lint/rules/gsap.d.ts +0 -4
- package/dist/lint/rules/gsap.d.ts.map +0 -1
- package/dist/lint/rules/gsap.js +0 -1039
- package/dist/lint/rules/gsap.js.map +0 -1
- package/dist/lint/rules/media.d.ts +0 -3
- package/dist/lint/rules/media.d.ts.map +0 -1
- package/dist/lint/rules/media.js +0 -491
- package/dist/lint/rules/media.js.map +0 -1
- package/dist/lint/rules/slideshow.d.ts +0 -4
- package/dist/lint/rules/slideshow.d.ts.map +0 -1
- package/dist/lint/rules/slideshow.js +0 -74
- package/dist/lint/rules/slideshow.js.map +0 -1
- package/dist/lint/rules/textures.d.ts +0 -3
- package/dist/lint/rules/textures.d.ts.map +0 -1
- package/dist/lint/rules/textures.js +0 -192
- package/dist/lint/rules/textures.js.map +0 -1
- package/dist/lint/types.d.ts +0 -36
- package/dist/lint/types.d.ts.map +0 -1
- package/dist/lint/types.js +0 -2
- package/dist/lint/types.js.map +0 -1
- package/dist/lint/utils.d.ts +0 -55
- package/dist/lint/utils.d.ts.map +0 -1
- package/dist/lint/utils.js +0 -264
- package/dist/lint/utils.js.map +0 -1
- package/dist/parsers/gsapInline.d.ts +0 -47
- package/dist/parsers/gsapInline.d.ts.map +0 -1
- package/dist/parsers/gsapInline.js +0 -513
- package/dist/parsers/gsapInline.js.map +0 -1
- package/dist/parsers/gsapParser.d.ts +0 -158
- package/dist/parsers/gsapParser.d.ts.map +0 -1
- package/dist/parsers/gsapParser.js +0 -2634
- package/dist/parsers/gsapParser.js.map +0 -1
- package/dist/parsers/gsapParser.test-helpers.d.ts +0 -49
- package/dist/parsers/gsapParser.test-helpers.d.ts.map +0 -1
- package/dist/parsers/gsapParser.test-helpers.js +0 -97
- package/dist/parsers/gsapParser.test-helpers.js.map +0 -1
- package/dist/parsers/gsapSerialize.d.ts +0 -191
- package/dist/parsers/gsapSerialize.d.ts.map +0 -1
- package/dist/parsers/gsapSerialize.js +0 -410
- package/dist/parsers/gsapSerialize.js.map +0 -1
- package/dist/parsers/gsapUnroll.d.ts +0 -7
- package/dist/parsers/gsapUnroll.d.ts.map +0 -1
- package/dist/parsers/gsapUnroll.js +0 -137
- package/dist/parsers/gsapUnroll.js.map +0 -1
- package/dist/parsers/htmlParser.d.ts +0 -27
- package/dist/parsers/htmlParser.d.ts.map +0 -1
- package/dist/parsers/htmlParser.js +0 -731
- package/dist/parsers/htmlParser.js.map +0 -1
- package/dist/studio-api/createStudioApi.d.ts +0 -10
- package/dist/studio-api/createStudioApi.d.ts.map +0 -1
- package/dist/studio-api/createStudioApi.js +0 -32
- package/dist/studio-api/createStudioApi.js.map +0 -1
- package/dist/studio-api/helpers/backupJournal.d.ts +0 -9
- package/dist/studio-api/helpers/backupJournal.d.ts.map +0 -1
- package/dist/studio-api/helpers/backupJournal.js +0 -81
- package/dist/studio-api/helpers/backupJournal.js.map +0 -1
- package/dist/studio-api/helpers/hfIdPersist.d.ts +0 -11
- package/dist/studio-api/helpers/hfIdPersist.d.ts.map +0 -1
- package/dist/studio-api/helpers/hfIdPersist.js +0 -39
- package/dist/studio-api/helpers/hfIdPersist.js.map +0 -1
- package/dist/studio-api/helpers/mediaValidation.d.ts +0 -20
- package/dist/studio-api/helpers/mediaValidation.d.ts.map +0 -1
- package/dist/studio-api/helpers/mediaValidation.js +0 -56
- package/dist/studio-api/helpers/mediaValidation.js.map +0 -1
- package/dist/studio-api/helpers/mime.d.ts +0 -4
- package/dist/studio-api/helpers/mime.d.ts.map +0 -1
- package/dist/studio-api/helpers/mime.js +0 -39
- package/dist/studio-api/helpers/mime.js.map +0 -1
- package/dist/studio-api/helpers/previewAdapter.d.ts +0 -43
- package/dist/studio-api/helpers/previewAdapter.d.ts.map +0 -1
- package/dist/studio-api/helpers/previewAdapter.js +0 -118
- package/dist/studio-api/helpers/previewAdapter.js.map +0 -1
- package/dist/studio-api/helpers/projectSignature.d.ts +0 -5
- package/dist/studio-api/helpers/projectSignature.d.ts.map +0 -1
- package/dist/studio-api/helpers/projectSignature.js +0 -154
- package/dist/studio-api/helpers/projectSignature.js.map +0 -1
- package/dist/studio-api/helpers/safePath.d.ts +0 -12
- package/dist/studio-api/helpers/safePath.d.ts.map +0 -1
- package/dist/studio-api/helpers/safePath.js +0 -38
- package/dist/studio-api/helpers/safePath.js.map +0 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts +0 -28
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +0 -1
- package/dist/studio-api/helpers/sourceMutation.js +0 -339
- package/dist/studio-api/helpers/sourceMutation.js.map +0 -1
- package/dist/studio-api/helpers/subComposition.d.ts +0 -20
- package/dist/studio-api/helpers/subComposition.d.ts.map +0 -1
- package/dist/studio-api/helpers/subComposition.js +0 -289
- package/dist/studio-api/helpers/subComposition.js.map +0 -1
- package/dist/studio-api/helpers/waveform.d.ts +0 -4
- package/dist/studio-api/helpers/waveform.d.ts.map +0 -1
- package/dist/studio-api/helpers/waveform.js +0 -80
- package/dist/studio-api/helpers/waveform.js.map +0 -1
- package/dist/studio-api/routes/files.d.ts +0 -4
- package/dist/studio-api/routes/files.d.ts.map +0 -1
- package/dist/studio-api/routes/files.js +0 -1200
- package/dist/studio-api/routes/files.js.map +0 -1
- package/dist/studio-api/routes/fonts.d.ts +0 -3
- package/dist/studio-api/routes/fonts.d.ts.map +0 -1
- package/dist/studio-api/routes/fonts.js +0 -157
- package/dist/studio-api/routes/fonts.js.map +0 -1
- package/dist/studio-api/routes/lint.d.ts +0 -4
- package/dist/studio-api/routes/lint.d.ts.map +0 -1
- package/dist/studio-api/routes/lint.js +0 -29
- package/dist/studio-api/routes/lint.js.map +0 -1
- package/dist/studio-api/routes/preview.d.ts +0 -4
- package/dist/studio-api/routes/preview.d.ts.map +0 -1
- package/dist/studio-api/routes/preview.js +0 -331
- package/dist/studio-api/routes/preview.js.map +0 -1
- package/dist/studio-api/routes/projects.d.ts +0 -4
- package/dist/studio-api/routes/projects.d.ts.map +0 -1
- package/dist/studio-api/routes/projects.js +0 -45
- package/dist/studio-api/routes/projects.js.map +0 -1
- package/dist/studio-api/routes/registry.d.ts +0 -4
- package/dist/studio-api/routes/registry.d.ts.map +0 -1
- package/dist/studio-api/routes/registry.js +0 -31
- package/dist/studio-api/routes/registry.js.map +0 -1
- package/dist/studio-api/routes/render.d.ts +0 -4
- package/dist/studio-api/routes/render.d.ts.map +0 -1
- package/dist/studio-api/routes/render.js +0 -269
- package/dist/studio-api/routes/render.js.map +0 -1
- package/dist/studio-api/routes/storyboard.d.ts +0 -4
- package/dist/studio-api/routes/storyboard.d.ts.map +0 -1
- package/dist/studio-api/routes/storyboard.js +0 -65
- package/dist/studio-api/routes/storyboard.js.map +0 -1
- package/dist/studio-api/routes/thumbnail.d.ts +0 -4
- package/dist/studio-api/routes/thumbnail.d.ts.map +0 -1
- package/dist/studio-api/routes/thumbnail.js +0 -107
- package/dist/studio-api/routes/thumbnail.js.map +0 -1
- package/dist/studio-api/routes/waveform.d.ts +0 -4
- package/dist/studio-api/routes/waveform.d.ts.map +0 -1
- package/dist/studio-api/routes/waveform.js +0 -41
- package/dist/studio-api/routes/waveform.js.map +0 -1
- package/dist/studio-api/types.d.ts +0 -120
- package/dist/studio-api/types.d.ts.map +0 -1
- package/dist/studio-api/types.js +0 -2
- package/dist/studio-api/types.js.map +0 -1
|
@@ -1,2017 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Browser-safe GSAP write path — magic-string offset-splice.
|
|
4
|
-
*
|
|
5
|
-
* T6c: edits GSAP scripts by overwriting/removing byte ranges in the original
|
|
6
|
-
* source. Every byte outside the edited span is preserved verbatim — no
|
|
7
|
-
* pretty-printer churn. Consumes ParsedGsapAcornForWrite from gsapParserAcorn.ts.
|
|
8
|
-
*/
|
|
9
|
-
import MagicString from "magic-string";
|
|
10
|
-
import { resolveConversionProps, extractArcWaypoints, buildMotionPathObjectCode, } from "./gsapSerialize.js";
|
|
11
|
-
import { parseGsapScriptAcornForWrite, } from "./gsapParserAcorn.js";
|
|
12
|
-
import { classifyPropertyGroup } from "./gsapConstants.js";
|
|
13
|
-
import * as acornWalk from "acorn-walk";
|
|
14
|
-
// ── Code generation helpers ──────────────────────────────────────────────────
|
|
15
|
-
// Local serializer for the tween-statement path, which may carry boolean/object
|
|
16
|
-
// extras (stagger config). serializeValue stringifies objects to "[object
|
|
17
|
-
// Object]", so keep this richer JSON fallback for that path. Keyframe values are
|
|
18
|
-
// always number|string and use the shared serializeValue (recast parity).
|
|
19
|
-
function valueToCode(value) {
|
|
20
|
-
if (typeof value === "string" && value.startsWith("__raw:"))
|
|
21
|
-
return value.slice(6);
|
|
22
|
-
if (typeof value === "string")
|
|
23
|
-
return JSON.stringify(value);
|
|
24
|
-
if (typeof value === "number")
|
|
25
|
-
return Number.isNaN(value) ? "0" : String(value);
|
|
26
|
-
if (typeof value === "boolean")
|
|
27
|
-
return String(value);
|
|
28
|
-
return JSON.stringify(value);
|
|
29
|
-
}
|
|
30
|
-
function safeKey(key) {
|
|
31
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
32
|
-
}
|
|
33
|
-
// fallow-ignore-next-line complexity
|
|
34
|
-
function buildTweenStatementCode(timelineVar, anim) {
|
|
35
|
-
const selector = JSON.stringify(anim.targetSelector);
|
|
36
|
-
const props = { ...anim.properties };
|
|
37
|
-
if (anim.method !== "set" && anim.duration !== undefined)
|
|
38
|
-
props.duration = anim.duration;
|
|
39
|
-
if (anim.ease)
|
|
40
|
-
props.ease = anim.ease;
|
|
41
|
-
const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
42
|
-
if (anim.extras) {
|
|
43
|
-
for (const [k, v] of Object.entries(anim.extras)) {
|
|
44
|
-
entries.push(`${safeKey(k)}: ${valueToCode(v)}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const objCode = `{ ${entries.join(", ")} }`;
|
|
48
|
-
const posCode = valueToCode(typeof anim.position === "number" ? anim.position : (anim.position ?? 0));
|
|
49
|
-
if (anim.method === "fromTo") {
|
|
50
|
-
const fromEntries = Object.entries(anim.fromProperties ?? {}).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
51
|
-
return `${timelineVar}.fromTo(${selector}, { ${fromEntries.join(", ")} }, ${objCode}, ${posCode});`;
|
|
52
|
-
}
|
|
53
|
-
// A base `gsap.set` is off the timeline: no timeline var, no position arg.
|
|
54
|
-
if (anim.method === "set" && anim.global) {
|
|
55
|
-
return `gsap.set(${selector}, ${objCode});`;
|
|
56
|
-
}
|
|
57
|
-
return `${timelineVar}.${anim.method}(${selector}, ${objCode}, ${posCode});`;
|
|
58
|
-
}
|
|
59
|
-
// ── AST node helpers ─────────────────────────────────────────────────────────
|
|
60
|
-
function isObjectProperty(prop) {
|
|
61
|
-
return prop?.type === "ObjectProperty" || prop?.type === "Property";
|
|
62
|
-
}
|
|
63
|
-
function propKeyName(prop) {
|
|
64
|
-
return prop?.key?.name ?? prop?.key?.value;
|
|
65
|
-
}
|
|
66
|
-
function findPropertyNode(varsArgNode, key) {
|
|
67
|
-
if (varsArgNode?.type !== "ObjectExpression")
|
|
68
|
-
return undefined;
|
|
69
|
-
for (const prop of varsArgNode.properties ?? []) {
|
|
70
|
-
if (!isObjectProperty(prop))
|
|
71
|
-
continue;
|
|
72
|
-
if (propKeyName(prop) === key)
|
|
73
|
-
return prop;
|
|
74
|
-
}
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
/** The `keyframes` property's ObjectExpression value, or null when not a keyframe tween. */
|
|
78
|
-
function keyframesObjectNode(varsNode) {
|
|
79
|
-
const kfProp = findPropertyNode(varsNode, "keyframes");
|
|
80
|
-
return kfProp?.value?.type === "ObjectExpression" ? kfProp.value : null;
|
|
81
|
-
}
|
|
82
|
-
function findEnclosingExpressionStatement(ancestors) {
|
|
83
|
-
for (let i = ancestors.length - 2; i >= 0; i--) {
|
|
84
|
-
if (ancestors[i]?.type === "ExpressionStatement")
|
|
85
|
-
return ancestors[i];
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
/** Find the VariableDeclaration statement for `tl = gsap.timeline(...)`. */
|
|
90
|
-
function findTimelineDeclarationStatement(ast, timelineVar) {
|
|
91
|
-
let found = null;
|
|
92
|
-
acornWalk.simple(ast, {
|
|
93
|
-
// fallow-ignore-next-line complexity
|
|
94
|
-
VariableDeclaration(node) {
|
|
95
|
-
if (found)
|
|
96
|
-
return;
|
|
97
|
-
for (const decl of node.declarations ?? []) {
|
|
98
|
-
if (decl.id?.name === timelineVar &&
|
|
99
|
-
decl.init?.type === "CallExpression" &&
|
|
100
|
-
decl.init.callee?.type === "MemberExpression" &&
|
|
101
|
-
decl.init.callee.object?.name === "gsap" &&
|
|
102
|
-
decl.init.callee.property?.name === "timeline") {
|
|
103
|
-
found = node;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
return found;
|
|
109
|
-
}
|
|
110
|
-
// ── Property splice helpers ───────────────────────────────────────────────────
|
|
111
|
-
/**
|
|
112
|
-
* Remove a property from a properties array, handling its comma.
|
|
113
|
-
* `editableProps` must be the isObjectProperty-filtered subset in source order.
|
|
114
|
-
*/
|
|
115
|
-
function removeProp(ms, propNode, editableProps) {
|
|
116
|
-
const idx = editableProps.indexOf(propNode);
|
|
117
|
-
if (idx === -1)
|
|
118
|
-
return;
|
|
119
|
-
if (editableProps.length === 1) {
|
|
120
|
-
ms.remove(propNode.start, propNode.end);
|
|
121
|
-
}
|
|
122
|
-
else if (idx === 0) {
|
|
123
|
-
// First prop: remove from its start to next prop start (drops trailing ", ")
|
|
124
|
-
ms.remove(editableProps[0].start, editableProps[1].start);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
// Non-first: remove from prev prop end to this prop end (drops leading ", ")
|
|
128
|
-
ms.remove(editableProps[idx - 1].end, propNode.end);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/** Serialize a vars record to an object-literal source: `{ k: v, ... }`. */
|
|
132
|
-
function buildVarsObjectCode(record) {
|
|
133
|
-
const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
134
|
-
return entries.length > 0 ? `{ ${entries.join(", ")} }` : "{}";
|
|
135
|
-
}
|
|
136
|
-
/** Overwrite a tween call's vars ObjectExpression with freshly-built source. */
|
|
137
|
-
function overwriteVarsArg(ms, call, objCode) {
|
|
138
|
-
if (!call.varsArg)
|
|
139
|
-
return;
|
|
140
|
-
ms.overwrite(call.varsArg.start, call.varsArg.end, objCode);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Update a property value if it exists, or append a new key: val before the
|
|
144
|
-
* closing `}`. Call with the full ObjectExpression node.
|
|
145
|
-
*/
|
|
146
|
-
function upsertProp(ms, objNode, key, value) {
|
|
147
|
-
if (objNode?.type !== "ObjectExpression")
|
|
148
|
-
return;
|
|
149
|
-
const existing = findPropertyNode(objNode, key);
|
|
150
|
-
if (existing) {
|
|
151
|
-
ms.overwrite(existing.value.start, existing.value.end, valueToCode(value));
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
const sep = objNode.properties.length > 0 ? ", " : "";
|
|
155
|
-
ms.appendLeft(objNode.end - 1, `${sep}${safeKey(key)}: ${valueToCode(value)}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Vars keys that are NOT editable transform/style props: builtins
|
|
160
|
-
* (duration/ease/delay), dropped callbacks, and extras (stagger/yoyo/repeat/…).
|
|
161
|
-
* The exact union of recast's BUILTIN_VAR_KEYS + DROPPED_VAR_KEYS + EXTRAS_KEYS,
|
|
162
|
-
* so both writers classify vars keys identically. (Distinct from the keyframe-
|
|
163
|
-
* conversion NON_EDITABLE_VAR_KEYS below, which intentionally omits `ease`
|
|
164
|
-
* because that path re-emits ease separately.)
|
|
165
|
-
*/
|
|
166
|
-
const NON_EDITABLE_PROP_KEYS = new Set([
|
|
167
|
-
"duration",
|
|
168
|
-
"ease",
|
|
169
|
-
"delay",
|
|
170
|
-
"onComplete",
|
|
171
|
-
"onStart",
|
|
172
|
-
"onUpdate",
|
|
173
|
-
"onRepeat",
|
|
174
|
-
"stagger",
|
|
175
|
-
"yoyo",
|
|
176
|
-
"repeat",
|
|
177
|
-
"repeatDelay",
|
|
178
|
-
"snap",
|
|
179
|
-
"overwrite",
|
|
180
|
-
"immediateRender",
|
|
181
|
-
]);
|
|
182
|
-
/**
|
|
183
|
-
* Editable transform/style key test: anything NOT a builtin, dropped callback, or
|
|
184
|
-
* extras key. Mirrors recast's isEditablePropertyKey so both writers classify
|
|
185
|
-
* vars keys identically.
|
|
186
|
-
*/
|
|
187
|
-
function isEditableVarKey(key) {
|
|
188
|
-
return !NON_EDITABLE_PROP_KEYS.has(key);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Collect verbatim `key: value` entries to PRESERVE from a vars/keyframe
|
|
192
|
-
* ObjectExpression: every property whose key `drop` does not reject, sliced from
|
|
193
|
-
* source — except keys present in `overrides`, whose value is replaced. Returns
|
|
194
|
-
* the entries plus the set of keys it kept, so callers can append new keys.
|
|
195
|
-
*/
|
|
196
|
-
function preservedEntries(objNode, source, drop, overrides) {
|
|
197
|
-
const entries = [];
|
|
198
|
-
const keys = new Set();
|
|
199
|
-
for (const prop of objNode.properties ?? []) {
|
|
200
|
-
if (!isObjectProperty(prop))
|
|
201
|
-
continue;
|
|
202
|
-
const key = propKeyName(prop);
|
|
203
|
-
if (typeof key !== "string" || drop(key))
|
|
204
|
-
continue;
|
|
205
|
-
keys.add(key);
|
|
206
|
-
const code = key in overrides
|
|
207
|
-
? valueToCode(overrides[key])
|
|
208
|
-
: source.slice(prop.value.start, prop.value.end);
|
|
209
|
-
entries.push(`${safeKey(key)}: ${code}`);
|
|
210
|
-
}
|
|
211
|
-
return { entries, keys };
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Replace the editable-property keys on a vars ObjectExpression with exactly
|
|
215
|
-
* `newProps`, leaving non-editable keys (duration/ease/stagger/callbacks/…)
|
|
216
|
-
* untouched unless overridden in `nonEditableOverrides`. Mirrors recast's
|
|
217
|
-
* reconcileEditableProperties: editable keys absent from `newProps` are DROPPED,
|
|
218
|
-
* not merged. Rebuilt in a single ms.overwrite so the splice can never overlap a
|
|
219
|
-
* sibling edit — non-editable updates that also target this node (duration/ease/
|
|
220
|
-
* extras) are folded into the same rebuild rather than spliced separately.
|
|
221
|
-
*/
|
|
222
|
-
function reconcileEditableProps(ms, objNode, source, newProps, nonEditableOverrides) {
|
|
223
|
-
if (objNode?.type !== "ObjectExpression")
|
|
224
|
-
return;
|
|
225
|
-
const overrides = nonEditableOverrides ?? {};
|
|
226
|
-
const { entries, keys } = preservedEntries(objNode, source, isEditableVarKey, overrides);
|
|
227
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
228
|
-
if (!keys.has(key))
|
|
229
|
-
entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
|
|
230
|
-
}
|
|
231
|
-
for (const [key, value] of Object.entries(newProps)) {
|
|
232
|
-
entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
|
|
233
|
-
}
|
|
234
|
-
ms.overwrite(objNode.start, objNode.end, `{ ${entries.join(", ")} }`);
|
|
235
|
-
}
|
|
236
|
-
// ── Insertion helpers ─────────────────────────────────────────────────────────
|
|
237
|
-
/** Traverse callee.object chain to check if a call ultimately roots at timelineVar. */
|
|
238
|
-
function isTimelineRooted(node, timelineVar) {
|
|
239
|
-
if (node?.type === "Identifier")
|
|
240
|
-
return node.name === timelineVar;
|
|
241
|
-
if (node?.type === "CallExpression")
|
|
242
|
-
return isTimelineRooted(node.callee?.object, timelineVar);
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Find the byte offset after which to insert a new statement (tween or label).
|
|
247
|
-
* Returns null when no timeline declaration exists in the script — callers must
|
|
248
|
-
* not emit `tl.xxx()` calls in that case as `tl` would be undefined at render.
|
|
249
|
-
*/
|
|
250
|
-
function findInsertionPoint(parsed) {
|
|
251
|
-
const lastLocated = parsed.located[parsed.located.length - 1];
|
|
252
|
-
if (lastLocated) {
|
|
253
|
-
const lastCall = lastLocated.call;
|
|
254
|
-
const exprStmt = findEnclosingExpressionStatement(lastCall.ancestors);
|
|
255
|
-
return exprStmt?.end ?? lastCall.node.end;
|
|
256
|
-
}
|
|
257
|
-
if (!parsed.hasTimeline)
|
|
258
|
-
return null;
|
|
259
|
-
const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);
|
|
260
|
-
return tlDecl?.end ?? parsed.ast.end;
|
|
261
|
-
}
|
|
262
|
-
// ── Public write API ─────────────────────────────────────────────────────────
|
|
263
|
-
// fallow-ignore-next-line complexity
|
|
264
|
-
export function updateAnimationInScript(script, animationId, updates) {
|
|
265
|
-
if (!Object.keys(updates).length)
|
|
266
|
-
return script;
|
|
267
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
268
|
-
if (!parsed)
|
|
269
|
-
return script;
|
|
270
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
271
|
-
if (!target)
|
|
272
|
-
return script;
|
|
273
|
-
const ms = new MagicString(script);
|
|
274
|
-
const { call } = target;
|
|
275
|
-
// When `properties` is present we REPLACE the editable set (recast parity:
|
|
276
|
-
// editable keys absent from the update are dropped). Fold any concurrent
|
|
277
|
-
// non-editable updates (duration/ease/extras) into the single varsArg rebuild
|
|
278
|
-
// so their splices can't overlap the rebuild's overwrite of the whole node.
|
|
279
|
-
if (updates.properties) {
|
|
280
|
-
const overrides = {};
|
|
281
|
-
if (updates.duration !== undefined)
|
|
282
|
-
overrides.duration = updates.duration;
|
|
283
|
-
if (updates.ease !== undefined)
|
|
284
|
-
overrides.ease = updates.ease;
|
|
285
|
-
if (updates.extras)
|
|
286
|
-
Object.assign(overrides, updates.extras);
|
|
287
|
-
reconcileEditableProps(ms, call.varsArg, script, updates.properties, overrides);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
if (updates.duration !== undefined) {
|
|
291
|
-
upsertProp(ms, call.varsArg, "duration", updates.duration);
|
|
292
|
-
}
|
|
293
|
-
const easeValue = updates.easeEach ?? updates.ease;
|
|
294
|
-
if (easeValue !== undefined) {
|
|
295
|
-
const kfNode = keyframesObjectNode(call.varsArg);
|
|
296
|
-
if (kfNode) {
|
|
297
|
-
upsertProp(ms, kfNode, "easeEach", easeValue);
|
|
298
|
-
// "Apply to all segments": drop every per-keyframe `ease` override so the
|
|
299
|
-
// single easeEach governs all segments uniformly (AE select-all + F9).
|
|
300
|
-
if (updates.resetKeyframeEases) {
|
|
301
|
-
for (const kfEntry of kfNode.properties ?? []) {
|
|
302
|
-
if (!isObjectProperty(kfEntry))
|
|
303
|
-
continue;
|
|
304
|
-
const val = kfEntry.value;
|
|
305
|
-
if (val?.type !== "ObjectExpression")
|
|
306
|
-
continue;
|
|
307
|
-
const easeNode = findPropertyNode(val, "ease");
|
|
308
|
-
if (easeNode)
|
|
309
|
-
removeProp(ms, easeNode, val.properties);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
upsertProp(ms, call.varsArg, "ease", easeValue);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (updates.extras) {
|
|
318
|
-
for (const [key, value] of Object.entries(updates.extras)) {
|
|
319
|
-
upsertProp(ms, call.varsArg, key, value);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (updates.fromProperties && call.method === "fromTo" && call.fromArg) {
|
|
324
|
-
// fromTo's from-vars carry only editable props — REPLACE them too (recast
|
|
325
|
-
// parity). fromArg is a distinct node from varsArg, so this rebuild never
|
|
326
|
-
// overlaps the varsArg edits above.
|
|
327
|
-
reconcileEditableProps(ms, call.fromArg, script, updates.fromProperties);
|
|
328
|
-
}
|
|
329
|
-
if (updates.position !== undefined) {
|
|
330
|
-
overwritePosition(ms, call, updates.position);
|
|
331
|
-
}
|
|
332
|
-
return ms.toString();
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Overwrite a tween call's numeric position argument (the positionArg the parser
|
|
336
|
-
* located: 3rd arg for fromTo, else 2nd), or append one when the call has no
|
|
337
|
-
* explicit position. Shared by updateAnimationInScript and the
|
|
338
|
-
* shift/scalePositionsInScript timeline ops.
|
|
339
|
-
*/
|
|
340
|
-
function overwritePosition(ms, call, position) {
|
|
341
|
-
if (call.positionArg) {
|
|
342
|
-
ms.overwrite(call.positionArg.start, call.positionArg.end, valueToCode(position));
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
ms.appendLeft(call.node.end - 1, `, ${valueToCode(position)}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Shift every tween targeting `targetSelector` by `delta` seconds (clamped ≥0),
|
|
350
|
-
* rewriting each call's position argument. Mirrors recast's shiftPositionsInScript
|
|
351
|
-
* (used by timeline clip-move to keep GSAP positions in sync with the clip start).
|
|
352
|
-
*/
|
|
353
|
-
export function shiftPositionsInScript(script, targetSelector, delta) {
|
|
354
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
355
|
-
if (!parsed)
|
|
356
|
-
return script;
|
|
357
|
-
const ms = new MagicString(script);
|
|
358
|
-
let changed = false;
|
|
359
|
-
for (const entry of parsed.located) {
|
|
360
|
-
if (entry.animation.targetSelector !== targetSelector)
|
|
361
|
-
continue;
|
|
362
|
-
if (typeof entry.animation.position !== "number")
|
|
363
|
-
continue;
|
|
364
|
-
const newPos = Math.max(0, Math.round((entry.animation.position + delta) * 1000) / 1000);
|
|
365
|
-
overwritePosition(ms, entry.call, newPos);
|
|
366
|
-
changed = true;
|
|
367
|
-
}
|
|
368
|
-
return changed ? ms.toString() : script;
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Linearly remap every tween targeting `targetSelector` from the old clip
|
|
372
|
-
* [oldStart, oldDuration] onto the new [newStart, newDuration] (position and,
|
|
373
|
-
* when present, duration scaled by the duration ratio). Mirrors recast's
|
|
374
|
-
* scalePositionsInScript (used by timeline clip-resize).
|
|
375
|
-
*/
|
|
376
|
-
export function scalePositionsInScript(script, targetSelector, oldStart, oldDuration, newStart, newDuration) {
|
|
377
|
-
if (oldDuration <= 0 || newDuration <= 0)
|
|
378
|
-
return script;
|
|
379
|
-
const ratio = newDuration / oldDuration;
|
|
380
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
381
|
-
if (!parsed)
|
|
382
|
-
return script;
|
|
383
|
-
const ms = new MagicString(script);
|
|
384
|
-
let changed = false;
|
|
385
|
-
for (const entry of parsed.located) {
|
|
386
|
-
if (entry.animation.targetSelector !== targetSelector)
|
|
387
|
-
continue;
|
|
388
|
-
if (typeof entry.animation.position !== "number")
|
|
389
|
-
continue;
|
|
390
|
-
const newPos = Math.max(0, Math.round((newStart + (entry.animation.position - oldStart) * ratio) * 1000) / 1000);
|
|
391
|
-
overwritePosition(ms, entry.call, newPos);
|
|
392
|
-
if (typeof entry.animation.duration === "number" && entry.animation.duration > 0) {
|
|
393
|
-
const newDur = Math.max(0.001, Math.round(entry.animation.duration * ratio * 1000) / 1000);
|
|
394
|
-
upsertProp(ms, entry.call.varsArg, "duration", newDur);
|
|
395
|
-
}
|
|
396
|
-
changed = true;
|
|
397
|
-
}
|
|
398
|
-
return changed ? ms.toString() : script;
|
|
399
|
-
}
|
|
400
|
-
export function addAnimationToScript(script, animation) {
|
|
401
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
402
|
-
if (!parsed)
|
|
403
|
-
return { script, id: "" };
|
|
404
|
-
const insertionPoint = findInsertionPoint(parsed);
|
|
405
|
-
if (insertionPoint === null)
|
|
406
|
-
return { script, id: "" };
|
|
407
|
-
const ms = new MagicString(script);
|
|
408
|
-
const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);
|
|
409
|
-
ms.appendLeft(insertionPoint, "\n" + statementCode);
|
|
410
|
-
const result = ms.toString();
|
|
411
|
-
const reParsed = parseGsapScriptAcornForWrite(result);
|
|
412
|
-
const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? "";
|
|
413
|
-
return { script: result, id: newId };
|
|
414
|
-
}
|
|
415
|
-
export function removeAnimationFromScript(script, animationId) {
|
|
416
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
417
|
-
if (!parsed)
|
|
418
|
-
return script;
|
|
419
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
420
|
-
if (!target)
|
|
421
|
-
return script;
|
|
422
|
-
const ms = new MagicString(script);
|
|
423
|
-
const N = target.call.node;
|
|
424
|
-
const exprStmt = findEnclosingExpressionStatement(target.call.ancestors);
|
|
425
|
-
if (N.callee?.object?.type !== "CallExpression" && exprStmt?.expression === N) {
|
|
426
|
-
// Standalone `tl.method(...)` — remove the whole ExpressionStatement
|
|
427
|
-
const end = exprStmt.end < script.length && script[exprStmt.end] === "\n"
|
|
428
|
-
? exprStmt.end + 1
|
|
429
|
-
: exprStmt.end;
|
|
430
|
-
ms.remove(exprStmt.start, end);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
// Chain link — splice out `.method(args)` from N.callee.object.end to N.end
|
|
434
|
-
ms.remove(N.callee.object.end, N.end);
|
|
435
|
-
}
|
|
436
|
-
return ms.toString();
|
|
437
|
-
}
|
|
438
|
-
// ── Flat-tween → keyframes conversion ──────────────────────────────────────────
|
|
439
|
-
//
|
|
440
|
-
// Mirror recast's convertToKeyframesInScript: when the first keyframe op lands
|
|
441
|
-
// on a flat to()/from()/fromTo() tween, rewrite its vars object to
|
|
442
|
-
// `{ keyframes: { "0%": {from}, "100%": {to} }, <preserved non-editable keys>,
|
|
443
|
-
// ease: "none"? }` and convert from()/fromTo() to to(). We rebuild the whole
|
|
444
|
-
// vars ObjectExpression in one ms.overwrite (single-edit-per-node), so the next
|
|
445
|
-
// keyframe-add re-parses cleanly.
|
|
446
|
-
// Identity value for an editable transform/style prop (recast's CSS_IDENTITY).
|
|
447
|
-
const CSS_IDENTITY = {
|
|
448
|
-
opacity: 1,
|
|
449
|
-
autoAlpha: 1,
|
|
450
|
-
scale: 1,
|
|
451
|
-
scaleX: 1,
|
|
452
|
-
scaleY: 1,
|
|
453
|
-
};
|
|
454
|
-
function cssIdentityValue(prop) {
|
|
455
|
-
return CSS_IDENTITY[prop] ?? 0;
|
|
456
|
-
}
|
|
457
|
-
// Keys NOT in the editable set — preserved verbatim on the converted vars object
|
|
458
|
-
// (matches the parser's classification: builtin/dropped/extras keys).
|
|
459
|
-
const NON_EDITABLE_VAR_KEYS = new Set([
|
|
460
|
-
"duration",
|
|
461
|
-
"delay",
|
|
462
|
-
"onComplete",
|
|
463
|
-
"onStart",
|
|
464
|
-
"onUpdate",
|
|
465
|
-
"onRepeat",
|
|
466
|
-
"stagger",
|
|
467
|
-
"yoyo",
|
|
468
|
-
"repeat",
|
|
469
|
-
"repeatDelay",
|
|
470
|
-
"snap",
|
|
471
|
-
"overwrite",
|
|
472
|
-
"immediateRender",
|
|
473
|
-
]);
|
|
474
|
-
/** The CSS-identity counterpart of a props record (numbers → identity value). */
|
|
475
|
-
function identityProps(properties) {
|
|
476
|
-
const identity = {};
|
|
477
|
-
for (const [k, v] of Object.entries(properties)) {
|
|
478
|
-
if (v != null)
|
|
479
|
-
identity[k] = typeof v === "number" ? cssIdentityValue(k) : v;
|
|
480
|
-
}
|
|
481
|
-
return identity;
|
|
482
|
-
}
|
|
483
|
-
/** Resolve the 0%/100% endpoint records for a tween being converted. */
|
|
484
|
-
function conversionEndpoints(animation) {
|
|
485
|
-
if (animation.method === "from") {
|
|
486
|
-
return { fromProps: { ...animation.properties }, toProps: identityProps(animation.properties) };
|
|
487
|
-
}
|
|
488
|
-
if (animation.method === "fromTo") {
|
|
489
|
-
return {
|
|
490
|
-
fromProps: { ...(animation.fromProperties ?? {}) },
|
|
491
|
-
toProps: { ...animation.properties },
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
// to(): 0% is the CSS identity state, 100% is the authored props.
|
|
495
|
-
return { fromProps: identityProps(animation.properties), toProps: { ...animation.properties } };
|
|
496
|
-
}
|
|
497
|
-
/** Collect preserved (non-editable) `key: value` entries from the original vars node. */
|
|
498
|
-
function preservedVarsEntries(varsNode, source) {
|
|
499
|
-
const entries = [];
|
|
500
|
-
if (varsNode?.type !== "ObjectExpression")
|
|
501
|
-
return entries;
|
|
502
|
-
for (const prop of varsNode.properties ?? []) {
|
|
503
|
-
if (!isObjectProperty(prop))
|
|
504
|
-
continue;
|
|
505
|
-
const key = propKeyName(prop);
|
|
506
|
-
if (typeof key !== "string" || !NON_EDITABLE_VAR_KEYS.has(key))
|
|
507
|
-
continue;
|
|
508
|
-
entries.push(`${safeKey(key)}: ${source.slice(prop.value.start, prop.value.end)}`);
|
|
509
|
-
}
|
|
510
|
-
return entries;
|
|
511
|
-
}
|
|
512
|
-
/** Build the rebuilt vars-object code for a converted flat tween. */
|
|
513
|
-
function buildConvertedVarsCode(animation, varsNode, source) {
|
|
514
|
-
const { fromProps, toProps } = conversionEndpoints(animation);
|
|
515
|
-
const easeEach = animation.ease;
|
|
516
|
-
const easeEachEntry = easeEach ? `, easeEach: ${JSON.stringify(easeEach)}` : "";
|
|
517
|
-
const kfCode = `{ "0%": ${recordToCode(fromProps)}, "100%": ${recordToCode(toProps)}${easeEachEntry} }`;
|
|
518
|
-
const entries = [`keyframes: ${kfCode}`, ...preservedVarsEntries(varsNode, source)];
|
|
519
|
-
if (easeEach)
|
|
520
|
-
entries.push(`ease: "none"`);
|
|
521
|
-
return `{ ${entries.join(", ")} }`;
|
|
522
|
-
}
|
|
523
|
-
/** Rename a from()/fromTo() call to to(), dropping fromTo's leading from-vars arg. */
|
|
524
|
-
function convertMethodToTo(ms, animation, call, varsNode) {
|
|
525
|
-
if (animation.method !== "from" && animation.method !== "fromTo")
|
|
526
|
-
return;
|
|
527
|
-
const calleeProp = call.node.callee?.property;
|
|
528
|
-
if (calleeProp)
|
|
529
|
-
ms.overwrite(calleeProp.start, calleeProp.end, "to");
|
|
530
|
-
// Remove the from-vars arg and its trailing separator up to the to-vars arg.
|
|
531
|
-
if (animation.method === "fromTo" && call.fromArg)
|
|
532
|
-
ms.remove(call.fromArg.start, varsNode.start);
|
|
533
|
-
}
|
|
534
|
-
function convertFlatTweenToKeyframes(script, target) {
|
|
535
|
-
const animation = target.animation;
|
|
536
|
-
if (animation.keyframes || animation.method === "set")
|
|
537
|
-
return script;
|
|
538
|
-
const call = target.call;
|
|
539
|
-
const varsNode = call.varsArg;
|
|
540
|
-
if (varsNode?.type !== "ObjectExpression")
|
|
541
|
-
return script;
|
|
542
|
-
const ms = new MagicString(script);
|
|
543
|
-
ms.overwrite(varsNode.start, varsNode.end, buildConvertedVarsCode(animation, varsNode, script));
|
|
544
|
-
convertMethodToTo(ms, animation, call, varsNode);
|
|
545
|
-
return ms.toString();
|
|
546
|
-
}
|
|
547
|
-
// ── Keyframe write ops ────────────────────────────────────────────────────────
|
|
548
|
-
//
|
|
549
|
-
// Design: mirror the recast writer's rebuild-the-node model. The recast writer
|
|
550
|
-
// mutates AST nodes in place and re-prints, so it never has an offset-overlap
|
|
551
|
-
// problem. Here we instead compute the FINAL property record for every keyframe
|
|
552
|
-
// value node that must change (the target merge, `_auto` endpoint sync, and
|
|
553
|
-
// backfilled siblings) against the ORIGINAL parsed AST, then emit exactly ONE
|
|
554
|
-
// `ms.overwrite(valueNode.start, valueNode.end, code)` per changed node (and a
|
|
555
|
-
// single insert for a brand-new key). No node is ever both overwritten and
|
|
556
|
-
// appended into, so the splices can never overlap.
|
|
557
|
-
const PERCENTAGE_KEY_RE = /^(\d+(?:\.\d+)?)%$/;
|
|
558
|
-
// Matches recast's PCT_TOLERANCE: percentages within 2 of an existing key are
|
|
559
|
-
// treated as the same keyframe (merge), not a new insert.
|
|
560
|
-
const PCT_TOLERANCE = 2;
|
|
561
|
-
function percentageFromKey(key) {
|
|
562
|
-
const m = PERCENTAGE_KEY_RE.exec(key);
|
|
563
|
-
return m ? Number.parseFloat(m[1] ?? "0") : Number.NaN;
|
|
564
|
-
}
|
|
565
|
-
/** Serialize a final keyframe property record (number|string values) to code. */
|
|
566
|
-
function recordToCode(record) {
|
|
567
|
-
const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
568
|
-
return `{ ${entries.join(", ")} }`;
|
|
569
|
-
}
|
|
570
|
-
/** Percentage-keyed property nodes of a keyframes ObjectExpression, in source order. */
|
|
571
|
-
function percentagePropsOf(kfNode) {
|
|
572
|
-
return (kfNode.properties ?? []).filter((p) => {
|
|
573
|
-
if (!isObjectProperty(p))
|
|
574
|
-
return false;
|
|
575
|
-
const key = propKeyName(p);
|
|
576
|
-
return typeof key === "string" && PERCENTAGE_KEY_RE.test(key);
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
const LITERAL_NODE_TYPES = new Set(["Literal", "NumericLiteral", "StringLiteral"]);
|
|
580
|
-
/** Read one value node: a number/string literal, a negative number, or raw source. */
|
|
581
|
-
// fallow-ignore-next-line complexity
|
|
582
|
-
function readValueNode(v, source) {
|
|
583
|
-
if (LITERAL_NODE_TYPES.has(v?.type) &&
|
|
584
|
-
(typeof v.value === "number" || typeof v.value === "string")) {
|
|
585
|
-
return v.value;
|
|
586
|
-
}
|
|
587
|
-
if (v?.type === "UnaryExpression" &&
|
|
588
|
-
v.operator === "-" &&
|
|
589
|
-
typeof v.argument?.value === "number") {
|
|
590
|
-
return -v.argument.value;
|
|
591
|
-
}
|
|
592
|
-
return `__raw:${source.slice(v.start, v.end)}`;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Read a keyframe value ObjectExpression into a record, mirroring the parser's
|
|
596
|
-
* `objectExpressionToRecord`: literals resolve to their value; anything else is
|
|
597
|
-
* preserved as `__raw:<source>` so serializeValue round-trips it verbatim.
|
|
598
|
-
* Keyframe values are literals in practice, so the raw fallback is rarely hit.
|
|
599
|
-
*/
|
|
600
|
-
function valueNodeToRecord(valueNode, source) {
|
|
601
|
-
const record = {};
|
|
602
|
-
if (valueNode?.type !== "ObjectExpression")
|
|
603
|
-
return record;
|
|
604
|
-
for (const prop of valueNode.properties ?? []) {
|
|
605
|
-
if (!isObjectProperty(prop))
|
|
606
|
-
continue;
|
|
607
|
-
const key = propKeyName(prop);
|
|
608
|
-
if (typeof key !== "string")
|
|
609
|
-
continue;
|
|
610
|
-
record[key] = readValueNode(prop.value, source);
|
|
611
|
-
}
|
|
612
|
-
return record;
|
|
613
|
-
}
|
|
614
|
-
/** True when a keyframe value record carries the synthetic `_auto` marker. */
|
|
615
|
-
function recordHasAuto(record) {
|
|
616
|
-
return "_auto" in record;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Compute `_auto` endpoint overwrites: when the new keyframe is the immediate
|
|
620
|
-
* neighbor of an `_auto` 0% or 100% endpoint, that endpoint is rewritten to
|
|
621
|
-
* `{ ...newProps, _auto: 1 }`. Only fires for interior keyframes. Returns the
|
|
622
|
-
* percentage→overwrite map so the caller can fold these into the per-node final
|
|
623
|
-
* records (never a separate splice).
|
|
624
|
-
*/
|
|
625
|
-
function autoEndpointOverwrites(kfNode, source, percentage, properties) {
|
|
626
|
-
const result = new Map();
|
|
627
|
-
if (percentage <= 0 || percentage >= 100)
|
|
628
|
-
return result;
|
|
629
|
-
const pctProps = percentagePropsOf(kfNode);
|
|
630
|
-
const allPcts = pctProps
|
|
631
|
-
.map((p) => percentageFromKey(propKeyName(p) ?? ""))
|
|
632
|
-
.filter((n) => !Number.isNaN(n) && n !== percentage)
|
|
633
|
-
.sort((a, b) => a - b);
|
|
634
|
-
const leftNeighbor = allPcts.filter((p) => p < percentage).pop();
|
|
635
|
-
const rightNeighbor = allPcts.find((p) => p > percentage);
|
|
636
|
-
for (const endPct of [0, 100]) {
|
|
637
|
-
const isNeighbor = endPct === 0 ? leftNeighbor === 0 : rightNeighbor === 100;
|
|
638
|
-
if (!isNeighbor)
|
|
639
|
-
continue;
|
|
640
|
-
const endProp = pctProps.find((p) => percentageFromKey(propKeyName(p) ?? "") === endPct);
|
|
641
|
-
if (!endProp)
|
|
642
|
-
continue;
|
|
643
|
-
const rec = valueNodeToRecord(endProp.value, source);
|
|
644
|
-
if (!recordHasAuto(rec))
|
|
645
|
-
continue;
|
|
646
|
-
result.set(endProp, { ...properties, _auto: 1 });
|
|
647
|
-
}
|
|
648
|
-
return result;
|
|
649
|
-
}
|
|
650
|
-
function findKfPropByPct(kfNode, percentage) {
|
|
651
|
-
// Match the CLOSEST keyframe within tolerance, not the first one within range.
|
|
652
|
-
// Keyframes at e.g. 0/49/50/100 are all valid (the SDK dedups to a unique
|
|
653
|
-
// match at TOLERANCE=0.001 upstream); picking the first-within-PCT_TOLERANCE=2
|
|
654
|
-
// would hit 49% when the caller meant 50%. Tie-break on the earliest index so
|
|
655
|
-
// the choice stays deterministic.
|
|
656
|
-
const props = kfNode.properties ?? [];
|
|
657
|
-
let best = null;
|
|
658
|
-
let bestDist = Number.POSITIVE_INFINITY;
|
|
659
|
-
for (let i = 0; i < props.length; i++) {
|
|
660
|
-
const prop = props[i];
|
|
661
|
-
if (!isObjectProperty(prop))
|
|
662
|
-
continue;
|
|
663
|
-
const key = propKeyName(prop);
|
|
664
|
-
if (typeof key !== "string")
|
|
665
|
-
continue;
|
|
666
|
-
const dist = Math.abs(percentageFromKey(key) - percentage);
|
|
667
|
-
if (dist <= PCT_TOLERANCE && dist < bestDist) {
|
|
668
|
-
best = { prop, idx: i };
|
|
669
|
-
bestDist = dist;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
return best;
|
|
673
|
-
}
|
|
674
|
-
export function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
|
|
675
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
676
|
-
if (!parsed)
|
|
677
|
-
return script;
|
|
678
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
679
|
-
if (!target)
|
|
680
|
-
return script;
|
|
681
|
-
const kfPropNode = findPropertyNode(target.call.varsArg, "keyframes");
|
|
682
|
-
if (!kfPropNode)
|
|
683
|
-
return script;
|
|
684
|
-
// Array-form keyframes (`keyframes: [{x,y}, ...]`) carry no explicit percentages
|
|
685
|
-
// — GSAP distributes them evenly, and the runtime read assigns even percentages
|
|
686
|
-
// (0, 100/(n-1), …). Map the percentage back to an array index and overwrite that
|
|
687
|
-
// element in place (preserving the array form). Without this the function bailed
|
|
688
|
-
// on the ObjectExpression check, so dragging a motion-path node on an array-form
|
|
689
|
-
// tween committed nothing (server no-op).
|
|
690
|
-
if (kfPropNode.value?.type === "ArrayExpression") {
|
|
691
|
-
return updateArrayKeyframeByPct(script, kfPropNode.value, percentage, properties, ease);
|
|
692
|
-
}
|
|
693
|
-
if (kfPropNode.value?.type !== "ObjectExpression")
|
|
694
|
-
return script;
|
|
695
|
-
const match = findKfPropByPct(kfPropNode.value, percentage);
|
|
696
|
-
if (!match)
|
|
697
|
-
return script;
|
|
698
|
-
const record = { ...properties };
|
|
699
|
-
if (ease)
|
|
700
|
-
record.ease = ease;
|
|
701
|
-
const ms = new MagicString(script);
|
|
702
|
-
ms.overwrite(match.prop.value.start, match.prop.value.end, recordToCode(record));
|
|
703
|
-
return ms.toString();
|
|
704
|
-
}
|
|
705
|
-
// ponytail: even-spacing index map; if array keyframes ever carry per-element
|
|
706
|
-
// `duration`, switch to matching the closest cumulative position.
|
|
707
|
-
function updateArrayKeyframeByPct(script, arrayNode, percentage, properties, ease) {
|
|
708
|
-
const elements = (arrayNode.elements ?? []).filter((el) => !!el && el.type === "ObjectExpression");
|
|
709
|
-
const n = elements.length;
|
|
710
|
-
if (n === 0)
|
|
711
|
-
return script;
|
|
712
|
-
const idx = n > 1 ? Math.round((percentage / 100) * (n - 1)) : 0;
|
|
713
|
-
const el = elements[Math.max(0, Math.min(n - 1, idx))];
|
|
714
|
-
if (!el)
|
|
715
|
-
return script;
|
|
716
|
-
const merged = {
|
|
717
|
-
...valueNodeToRecord(el, script),
|
|
718
|
-
...properties,
|
|
719
|
-
};
|
|
720
|
-
if (ease)
|
|
721
|
-
merged.ease = ease;
|
|
722
|
-
const ms = new MagicString(script);
|
|
723
|
-
ms.overwrite(el.start, el.end, recordToCode(merged));
|
|
724
|
-
return ms.toString();
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Build the final property record for the keyframe at `percentage`. If a
|
|
728
|
-
* keyframe already exists there, MERGE the new props over the existing record
|
|
729
|
-
* (preserve untouched props, preserve `_auto`, preserve the existing per-keyframe
|
|
730
|
-
* ease when the op omits one); otherwise it's just the new props.
|
|
731
|
-
*/
|
|
732
|
-
function buildTargetRecord(existing, source, properties, ease) {
|
|
733
|
-
if (!existing || existing.prop.value?.type !== "ObjectExpression") {
|
|
734
|
-
const record = { ...properties };
|
|
735
|
-
if (ease)
|
|
736
|
-
record.ease = ease;
|
|
737
|
-
return record;
|
|
738
|
-
}
|
|
739
|
-
const existingRecord = valueNodeToRecord(existing.prop.value, source);
|
|
740
|
-
const existingEase = typeof existingRecord.ease === "string" ? existingRecord.ease : undefined;
|
|
741
|
-
const merged = { ...existingRecord };
|
|
742
|
-
for (const [k, v] of Object.entries(properties))
|
|
743
|
-
merged[k] = v;
|
|
744
|
-
const finalEase = ease ?? existingEase;
|
|
745
|
-
if (finalEase)
|
|
746
|
-
merged.ease = finalEase;
|
|
747
|
-
else
|
|
748
|
-
delete merged.ease;
|
|
749
|
-
return merged;
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* Compute the backfilled final record for one sibling keyframe: append any of
|
|
753
|
-
* `newPropKeys` it's missing, using the backfill default. Returns null when
|
|
754
|
-
* nothing changes (so the caller emits no overwrite for it).
|
|
755
|
-
*/
|
|
756
|
-
function backfilledSiblingRecord(valueNode, source, newPropKeys, backfillDefaults) {
|
|
757
|
-
if (valueNode?.type !== "ObjectExpression")
|
|
758
|
-
return null;
|
|
759
|
-
const record = valueNodeToRecord(valueNode, source);
|
|
760
|
-
let changed = false;
|
|
761
|
-
for (const pk of newPropKeys) {
|
|
762
|
-
const defaultVal = backfillDefaults[pk];
|
|
763
|
-
if (pk in record || defaultVal == null)
|
|
764
|
-
continue;
|
|
765
|
-
record[pk] = defaultVal;
|
|
766
|
-
changed = true;
|
|
767
|
-
}
|
|
768
|
-
return changed ? record : null;
|
|
769
|
-
}
|
|
770
|
-
/** A located tween whose varsArg has a static keyframes ObjectExpression, or null. */
|
|
771
|
-
function locateWithKeyframes(script, animationId) {
|
|
772
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
773
|
-
if (!parsed)
|
|
774
|
-
return null;
|
|
775
|
-
// Converting from()/fromTo() to to() rewrites the content-derived id; match
|
|
776
|
-
// recast's locateAnimationWithFallback by remapping the method segment.
|
|
777
|
-
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
778
|
-
const target = parsed.located.find((l) => l.id === animationId) ??
|
|
779
|
-
parsed.located.find((l) => l.id === convertedId);
|
|
780
|
-
if (!target)
|
|
781
|
-
return null;
|
|
782
|
-
const kfPropNode = findPropertyNode(target.call.varsArg, "keyframes");
|
|
783
|
-
if (!kfPropNode || kfPropNode.value?.type !== "ObjectExpression")
|
|
784
|
-
return null;
|
|
785
|
-
return { script, parsed, target, kfNode: kfPropNode.value };
|
|
786
|
-
}
|
|
787
|
-
/** Locate a tween's keyframes object, converting a flat tween first if absent. */
|
|
788
|
-
// Array-form keyframes (`keyframes: [{x,y}, …]`) → even-percentage object form
|
|
789
|
-
// (`{ "0%": {…}, "33.3%": {…}, … }`). Inserting a keyframe needs percentage keys,
|
|
790
|
-
// which an even array can't host. Runtime-identical; mirrors the recast path.
|
|
791
|
-
function convertArrayKeyframesToObject(script, target) {
|
|
792
|
-
const kfPropNode = findPropertyNode(target.call.varsArg, "keyframes");
|
|
793
|
-
if (!kfPropNode || kfPropNode.value?.type !== "ArrayExpression")
|
|
794
|
-
return script;
|
|
795
|
-
const els = (kfPropNode.value.elements ?? []).filter((el) => !!el && el.type === "ObjectExpression");
|
|
796
|
-
const n = els.length;
|
|
797
|
-
if (n === 0)
|
|
798
|
-
return script;
|
|
799
|
-
const entries = els.map((el, i) => {
|
|
800
|
-
const pct = n > 1 ? Math.round((i / (n - 1)) * 1000) / 10 : 0;
|
|
801
|
-
return `${JSON.stringify(`${pct}%`)}: ${script.slice(el.start, el.end)}`;
|
|
802
|
-
});
|
|
803
|
-
const ms = new MagicString(script);
|
|
804
|
-
ms.overwrite(kfPropNode.value.start, kfPropNode.value.end, `{ ${entries.join(", ")} }`);
|
|
805
|
-
return ms.toString();
|
|
806
|
-
}
|
|
807
|
-
function ensureKeyframesNode(script, animationId) {
|
|
808
|
-
const direct = locateWithKeyframes(script, animationId);
|
|
809
|
-
if (direct)
|
|
810
|
-
return direct;
|
|
811
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
812
|
-
const target = parsed?.located.find((l) => l.id === animationId);
|
|
813
|
-
if (!target)
|
|
814
|
-
return null;
|
|
815
|
-
// Array-form keyframes → normalize to object form, then re-locate.
|
|
816
|
-
const kfProp = findPropertyNode(target.call.varsArg, "keyframes");
|
|
817
|
-
if (kfProp?.value?.type === "ArrayExpression") {
|
|
818
|
-
const normalized = convertArrayKeyframesToObject(script, target);
|
|
819
|
-
if (normalized !== script)
|
|
820
|
-
return locateWithKeyframes(normalized, animationId);
|
|
821
|
-
return null;
|
|
822
|
-
}
|
|
823
|
-
// No static keyframes object — convert the flat tween, then re-locate.
|
|
824
|
-
const converted = convertFlatTweenToKeyframes(script, target);
|
|
825
|
-
if (converted === script)
|
|
826
|
-
return null;
|
|
827
|
-
return locateWithKeyframes(converted, animationId);
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* Compute the sibling keyframe nodes that need a backfilled prop, excluding the
|
|
831
|
-
* target keyframe and any node already being overwritten as an `_auto` endpoint.
|
|
832
|
-
*/
|
|
833
|
-
function collectBackfillOverwrites(kfNode, src, properties, backfillDefaults, skip) {
|
|
834
|
-
const result = new Map();
|
|
835
|
-
if (!backfillDefaults)
|
|
836
|
-
return result;
|
|
837
|
-
const newPropKeys = Object.keys(properties);
|
|
838
|
-
for (const prop of percentagePropsOf(kfNode)) {
|
|
839
|
-
if (prop === skip.existingProp || skip.endpoints.has(prop))
|
|
840
|
-
continue;
|
|
841
|
-
const rec = backfilledSiblingRecord(prop.value, src, newPropKeys, backfillDefaults);
|
|
842
|
-
if (rec)
|
|
843
|
-
result.set(prop, rec);
|
|
844
|
-
}
|
|
845
|
-
return result;
|
|
846
|
-
}
|
|
847
|
-
export function addKeyframeToScript(script, animationId, percentage, properties, ease, backfillDefaults) {
|
|
848
|
-
const located = ensureKeyframesNode(script, animationId);
|
|
849
|
-
if (!located)
|
|
850
|
-
return script;
|
|
851
|
-
const { script: src, kfNode } = located;
|
|
852
|
-
const existing = findKfPropByPct(kfNode, percentage);
|
|
853
|
-
// Final record for the target keyframe (merge if it already exists).
|
|
854
|
-
const targetRecord = buildTargetRecord(existing, src, properties, ease);
|
|
855
|
-
// `_auto` endpoint syncs fire only on new inserts; a merge landing ON an
|
|
856
|
-
// endpoint already preserves `_auto` via buildTargetRecord.
|
|
857
|
-
const endpointOverwrites = existing
|
|
858
|
-
? new Map()
|
|
859
|
-
: autoEndpointOverwrites(kfNode, src, percentage, properties);
|
|
860
|
-
// Backfilled siblings (each node changes at most once).
|
|
861
|
-
const backfillOverwrites = collectBackfillOverwrites(kfNode, src, properties, backfillDefaults, {
|
|
862
|
-
existingProp: existing?.prop,
|
|
863
|
-
endpoints: endpointOverwrites,
|
|
864
|
-
});
|
|
865
|
-
// Emit exactly one overwrite per changed node, plus one insert for a new key.
|
|
866
|
-
const ms = new MagicString(src);
|
|
867
|
-
if (existing) {
|
|
868
|
-
// Merge into the existing keyframe at this percentage, preserving sibling
|
|
869
|
-
// properties — overwrite only the given keys. (A whole-value overwrite here
|
|
870
|
-
// would silently drop other properties already keyframed at this percent.)
|
|
871
|
-
if (existing.prop.value?.type === "ObjectExpression") {
|
|
872
|
-
for (const [k, v] of Object.entries(properties)) {
|
|
873
|
-
upsertProp(ms, existing.prop.value, k, v);
|
|
874
|
-
}
|
|
875
|
-
if (ease !== undefined)
|
|
876
|
-
upsertProp(ms, existing.prop.value, "ease", ease);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
ms.overwrite(existing.prop.value.start, existing.prop.value.end, recordToCode(targetRecord));
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
insertNewKeyframe(ms, kfNode, percentage, `${percentage}%`, recordToCode(targetRecord));
|
|
884
|
-
}
|
|
885
|
-
for (const [prop, rec] of [...endpointOverwrites, ...backfillOverwrites]) {
|
|
886
|
-
ms.overwrite(prop.value.start, prop.value.end, recordToCode(rec));
|
|
887
|
-
}
|
|
888
|
-
return ms.toString();
|
|
889
|
-
}
|
|
890
|
-
/** Insert a brand-new `"pct%": {...}` property in sorted order. */
|
|
891
|
-
function insertNewKeyframe(ms, kfNode, percentage, pctKey, valueCode) {
|
|
892
|
-
const allProps = (kfNode.properties ?? []).filter((p) => isObjectProperty(p));
|
|
893
|
-
let insertBeforeProp = null;
|
|
894
|
-
for (const prop of allProps) {
|
|
895
|
-
const key = propKeyName(prop);
|
|
896
|
-
if (typeof key === "string" && percentageFromKey(key) > percentage) {
|
|
897
|
-
insertBeforeProp = prop;
|
|
898
|
-
break;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
if (insertBeforeProp) {
|
|
902
|
-
ms.appendLeft(insertBeforeProp.start, `${JSON.stringify(pctKey)}: ${valueCode}, `);
|
|
903
|
-
}
|
|
904
|
-
else {
|
|
905
|
-
const sep = allProps.length > 0 ? ", " : "";
|
|
906
|
-
ms.appendLeft(kfNode.end - 1, `${sep}${JSON.stringify(pctKey)}: ${valueCode}`);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
/**
|
|
910
|
-
* Rebuild a vars ObjectExpression that has just dropped below two keyframes,
|
|
911
|
-
* collapsing `keyframes: {…}` back to a flat tween. Mirrors recast's
|
|
912
|
-
* collapseKeyframesToFlat: drop the `keyframes` + `easeEach` keys, preserve every
|
|
913
|
-
* other vars key verbatim, and splice the remaining keyframe's properties (minus
|
|
914
|
-
* its per-keyframe `ease`) in as flat vars keys. Single ms.overwrite of the whole
|
|
915
|
-
* vars node so the splice can't overlap the keyframe removal.
|
|
916
|
-
*/
|
|
917
|
-
function collapseKeyframesToFlat(ms, varsNode, source, remainingRecord) {
|
|
918
|
-
if (varsNode?.type !== "ObjectExpression")
|
|
919
|
-
return;
|
|
920
|
-
const dropKeyframeKeys = (key) => key === "keyframes" || key === "easeEach";
|
|
921
|
-
const { entries } = preservedEntries(varsNode, source, dropKeyframeKeys, {});
|
|
922
|
-
for (const [k, v] of Object.entries(remainingRecord)) {
|
|
923
|
-
if (k !== "ease")
|
|
924
|
-
entries.push(`${safeKey(k)}: ${valueToCode(v)}`);
|
|
925
|
-
}
|
|
926
|
-
ms.overwrite(varsNode.start, varsNode.end, `{ ${entries.join(", ")} }`);
|
|
927
|
-
}
|
|
928
|
-
/** Implicit tween-relative percentage of array-form keyframe index `i` of `n`
|
|
929
|
-
* (GSAP distributes array keyframes evenly: 0%, 1/(n-1), …, 100%). */
|
|
930
|
-
function arrayKeyframePct(i, n) {
|
|
931
|
-
return n > 1 ? (i / (n - 1)) * 100 : 0;
|
|
932
|
-
}
|
|
933
|
-
// Array-form keyframes (`keyframes: [{x,y}, …]`) carry no explicit percentages —
|
|
934
|
-
// GSAP distributes them evenly. removeKeyframeFromScript only handled the
|
|
935
|
-
// object-form (`keyframes: { "50%": {…} }`), so removing from an array-form tween
|
|
936
|
-
// was a silent no-op (and the downstream hold-sync then stranded an `hf-hold`).
|
|
937
|
-
// Resolve the element by its implicit percentage and splice it out; collapse to a
|
|
938
|
-
// flat tween when fewer than two remain (parity with the object-form path).
|
|
939
|
-
function removeArrayKeyframe(ms, varsArg, arrNode, script, percentage) {
|
|
940
|
-
const elements = (arrNode.elements ?? []).filter((e) => !!e && e.type === "ObjectExpression");
|
|
941
|
-
const n = elements.length;
|
|
942
|
-
if (n === 0)
|
|
943
|
-
return false;
|
|
944
|
-
let matchIdx = -1;
|
|
945
|
-
let bestDist = Number.POSITIVE_INFINITY;
|
|
946
|
-
for (let i = 0; i < n; i++) {
|
|
947
|
-
const dist = Math.abs(arrayKeyframePct(i, n) - percentage);
|
|
948
|
-
if (dist <= PCT_TOLERANCE && dist < bestDist) {
|
|
949
|
-
matchIdx = i;
|
|
950
|
-
bestDist = dist;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (matchIdx === -1)
|
|
954
|
-
return false;
|
|
955
|
-
const remaining = elements.filter((_, i) => i !== matchIdx);
|
|
956
|
-
if (remaining.length < 2) {
|
|
957
|
-
const sole = remaining[0];
|
|
958
|
-
const record = sole ? valueNodeToRecord(sole, script) : {};
|
|
959
|
-
collapseKeyframesToFlat(ms, varsArg, script, record);
|
|
960
|
-
return true;
|
|
961
|
-
}
|
|
962
|
-
removeProp(ms, elements[matchIdx], elements);
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
export function removeKeyframeFromScript(script, animationId, percentage) {
|
|
966
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
967
|
-
if (!parsed)
|
|
968
|
-
return script;
|
|
969
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
970
|
-
if (!target)
|
|
971
|
-
return script;
|
|
972
|
-
const kfPropNode = findPropertyNode(target.call.varsArg, "keyframes");
|
|
973
|
-
if (!kfPropNode)
|
|
974
|
-
return script;
|
|
975
|
-
if (kfPropNode.value?.type === "ArrayExpression") {
|
|
976
|
-
const ms = new MagicString(script);
|
|
977
|
-
return removeArrayKeyframe(ms, target.call.varsArg, kfPropNode.value, script, percentage)
|
|
978
|
-
? ms.toString()
|
|
979
|
-
: script;
|
|
980
|
-
}
|
|
981
|
-
if (kfPropNode.value?.type !== "ObjectExpression")
|
|
982
|
-
return script;
|
|
983
|
-
const kfNode = kfPropNode.value;
|
|
984
|
-
const match = findKfPropByPct(kfNode, percentage);
|
|
985
|
-
if (!match)
|
|
986
|
-
return script;
|
|
987
|
-
const ms = new MagicString(script);
|
|
988
|
-
// If removing this keyframe leaves fewer than two, collapse the keyframes
|
|
989
|
-
// object back to a flat tween (recast parity) instead of leaving a lone
|
|
990
|
-
// keyframe. We rebuild the whole vars node, so we never also splice the kf
|
|
991
|
-
// node — the two edits would overlap.
|
|
992
|
-
const remaining = percentagePropsOf(kfNode).filter((p) => p !== match.prop);
|
|
993
|
-
if (remaining.length < 2) {
|
|
994
|
-
const sole = remaining[0];
|
|
995
|
-
const record = sole ? valueNodeToRecord(sole.value, script) : {};
|
|
996
|
-
collapseKeyframesToFlat(ms, target.call.varsArg, script, record);
|
|
997
|
-
return ms.toString();
|
|
998
|
-
}
|
|
999
|
-
const allProps = (kfNode.properties ?? []).filter((p) => isObjectProperty(p));
|
|
1000
|
-
removeProp(ms, match.prop, allProps);
|
|
1001
|
-
return ms.toString();
|
|
1002
|
-
}
|
|
1003
|
-
export function removePropertyFromAnimation(script, animationId, property, from = false) {
|
|
1004
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1005
|
-
if (!parsed)
|
|
1006
|
-
return script;
|
|
1007
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1008
|
-
if (!target)
|
|
1009
|
-
return script;
|
|
1010
|
-
const { call } = target;
|
|
1011
|
-
const objNode = from ? (call.method === "fromTo" ? call.fromArg : null) : call.varsArg;
|
|
1012
|
-
if (!objNode)
|
|
1013
|
-
return script;
|
|
1014
|
-
const propNode = findPropertyNode(objNode, property);
|
|
1015
|
-
if (!propNode)
|
|
1016
|
-
return script;
|
|
1017
|
-
const allProps = (objNode.properties ?? []).filter((p) => isObjectProperty(p));
|
|
1018
|
-
const ms = new MagicString(script);
|
|
1019
|
-
removeProp(ms, propNode, allProps);
|
|
1020
|
-
return ms.toString();
|
|
1021
|
-
}
|
|
1022
|
-
/**
|
|
1023
|
-
* Remove all keyframes from a tween, collapsing to a flat tween with one
|
|
1024
|
-
* keyframe's properties: the first for `from()`, the last otherwise (the
|
|
1025
|
-
* destination = the visible resting state).
|
|
1026
|
-
*/
|
|
1027
|
-
export function removeAllKeyframesFromScript(script, animationId) {
|
|
1028
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1029
|
-
if (!parsed)
|
|
1030
|
-
return script;
|
|
1031
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1032
|
-
if (!target)
|
|
1033
|
-
return script;
|
|
1034
|
-
const kfs = target.animation.keyframes?.keyframes;
|
|
1035
|
-
if (!kfs || kfs.length === 0)
|
|
1036
|
-
return script;
|
|
1037
|
-
const sorted = [...kfs].sort((a, b) => a.percentage - b.percentage);
|
|
1038
|
-
const collapse = target.call.method === "from" ? sorted[0] : sorted[sorted.length - 1];
|
|
1039
|
-
if (!collapse)
|
|
1040
|
-
return script;
|
|
1041
|
-
const ms = new MagicString(script);
|
|
1042
|
-
overwriteVarsArg(ms, target.call, buildVarsObjectCode(buildCollapsedFlatVars(target.animation, collapse)));
|
|
1043
|
-
return ms.toString();
|
|
1044
|
-
}
|
|
1045
|
-
// Flat vars for a tween collapsing its keyframes onto one stop: existing
|
|
1046
|
-
// top-level props, then the collapse keyframe's props (skip per-keyframe
|
|
1047
|
-
// `ease`), then duration/ease/extras. Drops keyframes + easeEach by omission.
|
|
1048
|
-
function buildCollapsedFlatVars(animation, collapse) {
|
|
1049
|
-
const flat = { ...animation.properties };
|
|
1050
|
-
for (const [k, v] of Object.entries(collapse.properties)) {
|
|
1051
|
-
if (k !== "ease")
|
|
1052
|
-
flat[k] = v;
|
|
1053
|
-
}
|
|
1054
|
-
if (animation.duration !== undefined)
|
|
1055
|
-
flat.duration = animation.duration;
|
|
1056
|
-
if (animation.ease)
|
|
1057
|
-
flat.ease = animation.ease;
|
|
1058
|
-
for (const [k, v] of Object.entries(animation.extras ?? {})) {
|
|
1059
|
-
if (typeof v === "number" || typeof v === "string")
|
|
1060
|
-
flat[k] = v;
|
|
1061
|
-
}
|
|
1062
|
-
return flat;
|
|
1063
|
-
}
|
|
1064
|
-
/** Build the full replacement vars object for a tween being converted to keyframes. */
|
|
1065
|
-
function buildKeyframesVarsCode(animation, fromProps, toProps, varsNode, source, setDuration) {
|
|
1066
|
-
const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
1067
|
-
const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
1068
|
-
const easeEntry = animation.ease ? `, easeEach: ${JSON.stringify(animation.ease)}` : "";
|
|
1069
|
-
const kfCode = `{ "0%": { ${fromEntries.join(", ")} }, "100%": { ${toEntries.join(", ")} }${easeEntry} }`;
|
|
1070
|
-
// Preserve every non-editable key (duration/delay/callbacks/stagger/yoyo/…)
|
|
1071
|
-
// verbatim from source — rebuilding from the animation object alone dropped
|
|
1072
|
-
// `delay` (not a GsapAnimation field), shifting the tween's start time.
|
|
1073
|
-
let preserved = preservedVarsEntries(varsNode, source);
|
|
1074
|
-
// Converting a static `set` → drop its hold markers and give it a real duration
|
|
1075
|
-
// so the keyframes span time.
|
|
1076
|
-
if (setDuration !== undefined) {
|
|
1077
|
-
preserved = preserved.filter((e) => !/^\s*(immediateRender|data|duration)\s*:/.test(e));
|
|
1078
|
-
}
|
|
1079
|
-
const parts = [`keyframes: ${kfCode}`, ...preserved];
|
|
1080
|
-
if (setDuration !== undefined)
|
|
1081
|
-
parts.push(`duration: ${Math.max(0.001, setDuration)}`);
|
|
1082
|
-
if (animation.ease)
|
|
1083
|
-
parts.push(`ease: "none"`);
|
|
1084
|
-
return `{ ${parts.join(", ")} }`;
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Convert a flat tween (to/from/fromTo) to percentage-keyframes format.
|
|
1088
|
-
* `resolvedFromValues` supplies the current DOM state: overrides the 0% endpoint
|
|
1089
|
-
* for `to()`, the 100% endpoint for `from()`, or merges into toProps for `fromTo()`.
|
|
1090
|
-
*/
|
|
1091
|
-
export function convertToKeyframesFromScript(script, animationId, resolvedFromValues, setDuration = 1) {
|
|
1092
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1093
|
-
if (!parsed)
|
|
1094
|
-
return script;
|
|
1095
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1096
|
-
if (!target)
|
|
1097
|
-
return script;
|
|
1098
|
-
const { animation, call } = target;
|
|
1099
|
-
if (animation.keyframes)
|
|
1100
|
-
return script;
|
|
1101
|
-
const isSet = call.method === "set";
|
|
1102
|
-
const { fromProps, toProps } = resolveConversionProps(animation, resolvedFromValues);
|
|
1103
|
-
const ms = new MagicString(script);
|
|
1104
|
-
// A GLOBAL `gsap.set(...)` is off-timeline; rewriting only the method emits
|
|
1105
|
-
// `gsap.to(...)`, which fires once at load and isn't on the paused master
|
|
1106
|
-
// timeline (the engine can't seek/render it). Re-root onto the timeline var
|
|
1107
|
-
// and add the position arg the set lacks so the converted tween is seekable.
|
|
1108
|
-
if (isSet && animation.global) {
|
|
1109
|
-
const calleeObj = call.node.callee.object;
|
|
1110
|
-
if (calleeObj?.type === "Identifier") {
|
|
1111
|
-
ms.overwrite(calleeObj.start, calleeObj.end, parsed.timelineVar);
|
|
1112
|
-
}
|
|
1113
|
-
const args = call.node.arguments;
|
|
1114
|
-
if (args.length > 0 && args.length < 3) {
|
|
1115
|
-
ms.appendLeft(args[args.length - 1].end, ", 0");
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
// set/from/fromTo all become `to`; fromTo also drops its `from` argument.
|
|
1119
|
-
if (call.method === "from" || call.method === "fromTo" || isSet) {
|
|
1120
|
-
ms.overwrite(call.node.callee.property.start, call.node.callee.property.end, "to");
|
|
1121
|
-
}
|
|
1122
|
-
if (call.method === "fromTo" && call.fromArg) {
|
|
1123
|
-
ms.remove(call.fromArg.start, call.varsArg.start);
|
|
1124
|
-
}
|
|
1125
|
-
overwriteVarsArg(ms, call, buildKeyframesVarsCode(animation, fromProps, toProps, call.varsArg, script, isSet ? setDuration : undefined));
|
|
1126
|
-
return ms.toString();
|
|
1127
|
-
}
|
|
1128
|
-
// ── Keyframe-object code builder ─────────────────────────────────────────────
|
|
1129
|
-
/** Build a percentage-keyframes object literal: `{ "0%": { x: 0 }, "100%": { x: 100 } }`. */
|
|
1130
|
-
function buildKeyframeObjectCode(keyframes, easeEach) {
|
|
1131
|
-
const entries = keyframes.map((kf) => {
|
|
1132
|
-
const props = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
1133
|
-
if (kf.ease)
|
|
1134
|
-
props.push(`ease: ${JSON.stringify(kf.ease)}`);
|
|
1135
|
-
if (kf.auto)
|
|
1136
|
-
props.push(`_auto: 1`);
|
|
1137
|
-
return `${JSON.stringify(`${kf.percentage}%`)}: { ${props.join(", ")} }`;
|
|
1138
|
-
});
|
|
1139
|
-
if (easeEach)
|
|
1140
|
-
entries.push(`easeEach: ${JSON.stringify(easeEach)}`);
|
|
1141
|
-
return `{ ${entries.join(", ")} }`;
|
|
1142
|
-
}
|
|
1143
|
-
// ── Materialize keyframes ────────────────────────────────────────────────────
|
|
1144
|
-
/**
|
|
1145
|
-
* Replace a dynamic or static keyframes expression with a fully-resolved
|
|
1146
|
-
* percentage-keyframes object. Called when a user first edits a dynamically-
|
|
1147
|
-
* generated keyframe in the studio so it becomes statically editable.
|
|
1148
|
-
*/
|
|
1149
|
-
export function materializeKeyframesFromScript(script, animationId, keyframes, easeEach, resolvedSelector) {
|
|
1150
|
-
// An empty keyframe list has no materialized form — rebuilding vars with an
|
|
1151
|
-
// empty keyframes object would empty the animation. No-op instead.
|
|
1152
|
-
if (keyframes.length === 0)
|
|
1153
|
-
return script;
|
|
1154
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1155
|
-
if (!parsed)
|
|
1156
|
-
return script;
|
|
1157
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1158
|
-
if (!target)
|
|
1159
|
-
return script;
|
|
1160
|
-
const { call } = target;
|
|
1161
|
-
const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);
|
|
1162
|
-
const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);
|
|
1163
|
-
const ms = new MagicString(script);
|
|
1164
|
-
if (resolvedSelector) {
|
|
1165
|
-
const selectorArg = call.node.arguments[0];
|
|
1166
|
-
if (selectorArg)
|
|
1167
|
-
ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(resolvedSelector));
|
|
1168
|
-
}
|
|
1169
|
-
const kfProp = findPropertyNode(call.varsArg, "keyframes");
|
|
1170
|
-
if (kfProp) {
|
|
1171
|
-
ms.overwrite(kfProp.value.start, kfProp.value.end, kfObjCode);
|
|
1172
|
-
}
|
|
1173
|
-
else if (call.varsArg?.type === "ObjectExpression") {
|
|
1174
|
-
const vars = call.varsArg;
|
|
1175
|
-
if (vars.properties.length > 0) {
|
|
1176
|
-
ms.prependLeft(vars.properties[0].start, `keyframes: ${kfObjCode}, `);
|
|
1177
|
-
}
|
|
1178
|
-
else {
|
|
1179
|
-
ms.appendLeft(vars.end - 1, `keyframes: ${kfObjCode}`);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
const eachProp = findPropertyNode(call.varsArg, "easeEach");
|
|
1183
|
-
if (eachProp) {
|
|
1184
|
-
const allProps = (call.varsArg.properties ?? []).filter((p) => isObjectProperty(p));
|
|
1185
|
-
removeProp(ms, eachProp, allProps);
|
|
1186
|
-
}
|
|
1187
|
-
return ms.toString();
|
|
1188
|
-
}
|
|
1189
|
-
// ── Add animation with keyframes ──────────────────────────────────────────────
|
|
1190
|
-
/** Insert a new keyframed `to()` call and return the new animation ID. */
|
|
1191
|
-
export function addAnimationWithKeyframesToScript(script, targetSelector, position, duration, keyframes, ease, easeEach) {
|
|
1192
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1193
|
-
if (!parsed)
|
|
1194
|
-
return { script, id: "" };
|
|
1195
|
-
const insertionPoint = findInsertionPoint(parsed);
|
|
1196
|
-
if (insertionPoint === null)
|
|
1197
|
-
return { script, id: "" };
|
|
1198
|
-
const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);
|
|
1199
|
-
const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);
|
|
1200
|
-
const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`];
|
|
1201
|
-
if (ease)
|
|
1202
|
-
varParts.push(`ease: ${JSON.stringify(ease)}`);
|
|
1203
|
-
const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(", ")} }, ${valueToCode(position)});`;
|
|
1204
|
-
const ms = new MagicString(script);
|
|
1205
|
-
ms.appendLeft(insertionPoint, "\n" + stmtCode);
|
|
1206
|
-
const result = ms.toString();
|
|
1207
|
-
const reParsed = parseGsapScriptAcornForWrite(result);
|
|
1208
|
-
const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? "";
|
|
1209
|
-
return { script: result, id: newId };
|
|
1210
|
-
}
|
|
1211
|
-
// ── Split into property groups ────────────────────────────────────────────────
|
|
1212
|
-
function collectPropertyKeys(anim) {
|
|
1213
|
-
const keys = new Set();
|
|
1214
|
-
if (anim.keyframes) {
|
|
1215
|
-
for (const kf of anim.keyframes.keyframes) {
|
|
1216
|
-
for (const k of Object.keys(kf.properties))
|
|
1217
|
-
keys.add(k);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
else {
|
|
1221
|
-
for (const k of Object.keys(anim.properties))
|
|
1222
|
-
keys.add(k);
|
|
1223
|
-
}
|
|
1224
|
-
return keys;
|
|
1225
|
-
}
|
|
1226
|
-
function partitionPropertyGroups(keys) {
|
|
1227
|
-
const groups = new Map();
|
|
1228
|
-
for (const key of keys) {
|
|
1229
|
-
if (key === "transformOrigin")
|
|
1230
|
-
continue;
|
|
1231
|
-
const group = classifyPropertyGroup(key);
|
|
1232
|
-
let arr = groups.get(group);
|
|
1233
|
-
if (!arr) {
|
|
1234
|
-
arr = [];
|
|
1235
|
-
groups.set(group, arr);
|
|
1236
|
-
}
|
|
1237
|
-
arr.push(key);
|
|
1238
|
-
}
|
|
1239
|
-
return groups;
|
|
1240
|
-
}
|
|
1241
|
-
function assignTransformOrigin(groupProps) {
|
|
1242
|
-
let largestGroup;
|
|
1243
|
-
let largestCount = 0;
|
|
1244
|
-
for (const [group, props] of groupProps) {
|
|
1245
|
-
if (props.length > largestCount) {
|
|
1246
|
-
largestCount = props.length;
|
|
1247
|
-
largestGroup = group;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
const largest = largestGroup ? groupProps.get(largestGroup) : undefined;
|
|
1251
|
-
if (largest)
|
|
1252
|
-
largest.push("transformOrigin");
|
|
1253
|
-
}
|
|
1254
|
-
function filterGroupKeyframes(kfs, propSet) {
|
|
1255
|
-
const result = [];
|
|
1256
|
-
for (const kf of kfs) {
|
|
1257
|
-
const filtered = {};
|
|
1258
|
-
for (const [k, v] of Object.entries(kf.properties)) {
|
|
1259
|
-
if (propSet.has(k))
|
|
1260
|
-
filtered[k] = v;
|
|
1261
|
-
}
|
|
1262
|
-
if (Object.keys(filtered).length > 0) {
|
|
1263
|
-
result.push({
|
|
1264
|
-
percentage: kf.percentage,
|
|
1265
|
-
properties: filtered,
|
|
1266
|
-
...(kf.ease ? { ease: kf.ease } : {}),
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
return result;
|
|
1271
|
-
}
|
|
1272
|
-
function filterGroupProperties(properties, propSet) {
|
|
1273
|
-
const result = {};
|
|
1274
|
-
for (const [k, v] of Object.entries(properties)) {
|
|
1275
|
-
if (propSet.has(k))
|
|
1276
|
-
result[k] = v;
|
|
1277
|
-
}
|
|
1278
|
-
return result;
|
|
1279
|
-
}
|
|
1280
|
-
function addGroupAnimToScript(script, anim, propSet) {
|
|
1281
|
-
if (anim.keyframes) {
|
|
1282
|
-
const groupKeyframes = filterGroupKeyframes(anim.keyframes.keyframes, propSet);
|
|
1283
|
-
if (groupKeyframes.length === 0)
|
|
1284
|
-
return { script, id: "" };
|
|
1285
|
-
const pos = typeof anim.position === "number" ? anim.position : 0;
|
|
1286
|
-
return addAnimationWithKeyframesToScript(script, anim.targetSelector, pos, anim.duration ?? 0.5, groupKeyframes, anim.keyframes.easeEach ?? anim.ease);
|
|
1287
|
-
}
|
|
1288
|
-
const groupProperties = filterGroupProperties(anim.properties, propSet);
|
|
1289
|
-
if (Object.keys(groupProperties).length === 0)
|
|
1290
|
-
return { script, id: "" };
|
|
1291
|
-
const fromProperties = anim.method === "fromTo" && anim.fromProperties
|
|
1292
|
-
? filterGroupProperties(anim.fromProperties, propSet)
|
|
1293
|
-
: undefined;
|
|
1294
|
-
return addAnimationToScript(script, {
|
|
1295
|
-
targetSelector: anim.targetSelector,
|
|
1296
|
-
method: anim.method,
|
|
1297
|
-
position: anim.position,
|
|
1298
|
-
duration: anim.duration,
|
|
1299
|
-
ease: anim.ease,
|
|
1300
|
-
properties: groupProperties,
|
|
1301
|
-
fromProperties,
|
|
1302
|
-
extras: anim.extras,
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Split a mixed-property tween into one tween per property group (position,
|
|
1307
|
-
* scale, visual, etc.) so each group can be edited independently.
|
|
1308
|
-
* Returns the updated script and the IDs of the newly-created tweens.
|
|
1309
|
-
*/
|
|
1310
|
-
export function splitIntoPropertyGroupsFromScript(script, animationId) {
|
|
1311
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1312
|
-
if (!parsed)
|
|
1313
|
-
return { script, ids: [animationId] };
|
|
1314
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1315
|
-
if (!target)
|
|
1316
|
-
return { script, ids: [animationId] };
|
|
1317
|
-
const { animation } = target;
|
|
1318
|
-
const allPropKeys = collectPropertyKeys(animation);
|
|
1319
|
-
const groupProps = partitionPropertyGroups(allPropKeys);
|
|
1320
|
-
if (groupProps.size <= 1)
|
|
1321
|
-
return { script, ids: [animationId] };
|
|
1322
|
-
if (allPropKeys.has("transformOrigin"))
|
|
1323
|
-
assignTransformOrigin(groupProps);
|
|
1324
|
-
let result = removeAnimationFromScript(script, animationId);
|
|
1325
|
-
for (const [, props] of groupProps) {
|
|
1326
|
-
const { script: next, id } = addGroupAnimToScript(result, animation, new Set(props));
|
|
1327
|
-
if (id)
|
|
1328
|
-
result = next;
|
|
1329
|
-
}
|
|
1330
|
-
const reParsed = parseGsapScriptAcornForWrite(result);
|
|
1331
|
-
const newIds = (reParsed?.located ?? [])
|
|
1332
|
-
.filter((l) => l.animation.targetSelector === animation.targetSelector)
|
|
1333
|
-
.map((l) => l.id);
|
|
1334
|
-
return { script: result, ids: newIds };
|
|
1335
|
-
}
|
|
1336
|
-
// ── Label write ops ───────────────────────────────────────────────────────────
|
|
1337
|
-
/** True when `expr` is `tl.<method>(…)` rooted at the timeline var. */
|
|
1338
|
-
function isTimelineMethodCall(expr, timelineVar, method) {
|
|
1339
|
-
return (expr?.type === "CallExpression" &&
|
|
1340
|
-
expr.callee?.type === "MemberExpression" &&
|
|
1341
|
-
isTimelineRooted(expr.callee.object, timelineVar) &&
|
|
1342
|
-
expr.callee.property?.name === method);
|
|
1343
|
-
}
|
|
1344
|
-
/** True when `expr` is `tl.addLabel("<name>", …)` rooted at the timeline var. */
|
|
1345
|
-
function isAddLabelCall(expr, timelineVar, name) {
|
|
1346
|
-
const firstArg = expr?.arguments?.[0];
|
|
1347
|
-
return (isTimelineMethodCall(expr, timelineVar, "addLabel") &&
|
|
1348
|
-
firstArg?.type === "Literal" &&
|
|
1349
|
-
firstArg.value === name);
|
|
1350
|
-
}
|
|
1351
|
-
/** Every `tl.addLabel("<name>", …)` ExpressionStatement in the script. */
|
|
1352
|
-
function findLabelStatements(parsed, name) {
|
|
1353
|
-
const targets = [];
|
|
1354
|
-
acornWalk.simple(parsed.ast, {
|
|
1355
|
-
ExpressionStatement(node) {
|
|
1356
|
-
if (isAddLabelCall(node.expression, parsed.timelineVar, name))
|
|
1357
|
-
targets.push(node);
|
|
1358
|
-
},
|
|
1359
|
-
});
|
|
1360
|
-
return targets;
|
|
1361
|
-
}
|
|
1362
|
-
export function addLabelToScript(script, name, position) {
|
|
1363
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1364
|
-
if (!parsed)
|
|
1365
|
-
return script;
|
|
1366
|
-
// If the label already exists, MOVE it (overwrite its position) rather than
|
|
1367
|
-
// appending a duplicate. Two same-named addLabel statements make removeLabel
|
|
1368
|
-
// over-remove — it deletes every match, including a pre-existing label the
|
|
1369
|
-
// user never touched.
|
|
1370
|
-
const existing = findLabelStatements(parsed, name)[0];
|
|
1371
|
-
if (existing) {
|
|
1372
|
-
const ms = new MagicString(script);
|
|
1373
|
-
const posArg = existing.expression.arguments?.[1];
|
|
1374
|
-
if (posArg)
|
|
1375
|
-
ms.overwrite(posArg.start, posArg.end, valueToCode(position));
|
|
1376
|
-
else
|
|
1377
|
-
ms.appendLeft(existing.expression.end - 1, `, ${valueToCode(position)}`);
|
|
1378
|
-
return ms.toString();
|
|
1379
|
-
}
|
|
1380
|
-
const insertionPoint = findInsertionPoint(parsed);
|
|
1381
|
-
if (insertionPoint === null)
|
|
1382
|
-
return script;
|
|
1383
|
-
const ms = new MagicString(script);
|
|
1384
|
-
const labelCode = `${parsed.timelineVar}.addLabel(${JSON.stringify(name)}, ${valueToCode(position)});`;
|
|
1385
|
-
ms.appendLeft(insertionPoint, "\n" + labelCode);
|
|
1386
|
-
return ms.toString();
|
|
1387
|
-
}
|
|
1388
|
-
export function removeLabelFromScript(script, name) {
|
|
1389
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1390
|
-
if (!parsed)
|
|
1391
|
-
return script;
|
|
1392
|
-
const targets = findLabelStatements(parsed, name);
|
|
1393
|
-
if (!targets.length)
|
|
1394
|
-
return script;
|
|
1395
|
-
const ms = new MagicString(script);
|
|
1396
|
-
for (const target of targets) {
|
|
1397
|
-
const end = target.end < script.length && script[target.end] === "\n" ? target.end + 1 : target.end;
|
|
1398
|
-
ms.remove(target.start, end);
|
|
1399
|
-
}
|
|
1400
|
-
return ms.toString();
|
|
1401
|
-
}
|
|
1402
|
-
// ── Arc path helpers ─────────────────────────────────────────────────────────
|
|
1403
|
-
/**
|
|
1404
|
-
* Remove a set of properties from an ObjectExpression in a single pass.
|
|
1405
|
-
* Groups consecutive marked props into blocks to avoid overlapping remove ranges.
|
|
1406
|
-
*/
|
|
1407
|
-
function removePropsByKey(ms, objNode, keys) {
|
|
1408
|
-
if (objNode?.type !== "ObjectExpression")
|
|
1409
|
-
return;
|
|
1410
|
-
const allProps = (objNode.properties ?? []).filter(isObjectProperty);
|
|
1411
|
-
const marked = allProps.map((p) => keys.has(propKeyName(p) ?? ""));
|
|
1412
|
-
let i = 0;
|
|
1413
|
-
while (i < allProps.length) {
|
|
1414
|
-
if (!marked[i]) {
|
|
1415
|
-
i++;
|
|
1416
|
-
continue;
|
|
1417
|
-
}
|
|
1418
|
-
const blockStart = i;
|
|
1419
|
-
while (i < allProps.length && marked[i])
|
|
1420
|
-
i++;
|
|
1421
|
-
ms.remove(...blockRemoveRange(allProps, blockStart, i));
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
function blockRemoveRange(allProps, blockStart, blockEnd) {
|
|
1425
|
-
if (blockStart === 0 && blockEnd === allProps.length)
|
|
1426
|
-
return [allProps[0].start, allProps[allProps.length - 1].end];
|
|
1427
|
-
if (blockStart === 0)
|
|
1428
|
-
return [allProps[0].start, allProps[blockEnd].start];
|
|
1429
|
-
return [allProps[blockStart - 1].end, allProps[blockEnd - 1].end];
|
|
1430
|
-
}
|
|
1431
|
-
// fallow-ignore-next-line complexity
|
|
1432
|
-
function readLastWaypointXY(mpVal) {
|
|
1433
|
-
if (mpVal?.type !== "ObjectExpression")
|
|
1434
|
-
return { x: null, y: null };
|
|
1435
|
-
const pathProp = findPropertyNode(mpVal, "path");
|
|
1436
|
-
if (pathProp?.value?.type !== "ArrayExpression")
|
|
1437
|
-
return { x: null, y: null };
|
|
1438
|
-
const elems = pathProp.value.elements ?? [];
|
|
1439
|
-
const last = elems[elems.length - 1];
|
|
1440
|
-
if (last?.type !== "ObjectExpression")
|
|
1441
|
-
return { x: null, y: null };
|
|
1442
|
-
return {
|
|
1443
|
-
x: readNumericLiteralNode(findPropertyNode(last, "x")?.value),
|
|
1444
|
-
y: readNumericLiteralNode(findPropertyNode(last, "y")?.value),
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Read a numeric value node — a plain numeric literal or a unary-minus negative
|
|
1449
|
-
* literal (e.g. `-120`). Returns null for anything non-numeric. Without the
|
|
1450
|
-
* UnaryExpression branch, negative waypoint coords (parsed as a UnaryExpression
|
|
1451
|
-
* with no `.value`) would be lost when disabling an arc path.
|
|
1452
|
-
*/
|
|
1453
|
-
function readNumericLiteralNode(v) {
|
|
1454
|
-
if (LITERAL_NODE_TYPES.has(v?.type) && typeof v.value === "number")
|
|
1455
|
-
return v.value;
|
|
1456
|
-
if (v?.type === "UnaryExpression" &&
|
|
1457
|
-
v.operator === "-" &&
|
|
1458
|
-
typeof v.argument?.value === "number") {
|
|
1459
|
-
return -v.argument.value;
|
|
1460
|
-
}
|
|
1461
|
-
return null;
|
|
1462
|
-
}
|
|
1463
|
-
function disableArcPath(ms, call) {
|
|
1464
|
-
const mpProp = findPropertyNode(call.varsArg, "motionPath");
|
|
1465
|
-
if (!mpProp)
|
|
1466
|
-
return false;
|
|
1467
|
-
const { x, y } = readLastWaypointXY(mpProp.value);
|
|
1468
|
-
if (x === null && y === null) {
|
|
1469
|
-
const allProps = (call.varsArg.properties ?? []).filter(isObjectProperty);
|
|
1470
|
-
removeProp(ms, mpProp, allProps);
|
|
1471
|
-
return true;
|
|
1472
|
-
}
|
|
1473
|
-
// Overwrite the entire motionPath property with the recovered x/y pair — avoids
|
|
1474
|
-
// the appendLeft+remove range-boundary issue in MagicString.
|
|
1475
|
-
const parts = [];
|
|
1476
|
-
if (x !== null)
|
|
1477
|
-
parts.push(`x: ${x}`);
|
|
1478
|
-
if (y !== null)
|
|
1479
|
-
parts.push(`y: ${y}`);
|
|
1480
|
-
ms.overwrite(mpProp.start, mpProp.end, parts.join(", "));
|
|
1481
|
-
return true;
|
|
1482
|
-
}
|
|
1483
|
-
function stripXYFromKeyframes(ms, kfPropNode) {
|
|
1484
|
-
if (kfPropNode?.value?.type !== "ObjectExpression")
|
|
1485
|
-
return;
|
|
1486
|
-
const xyKeys = new Set(["x", "y"]);
|
|
1487
|
-
for (const pctProp of (kfPropNode.value.properties ?? []).filter(isObjectProperty)) {
|
|
1488
|
-
const k = propKeyName(pctProp);
|
|
1489
|
-
if (typeof k === "string" && k.endsWith("%") && pctProp.value?.type === "ObjectExpression") {
|
|
1490
|
-
removePropsByKey(ms, pctProp.value, xyKeys);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
function enableArcPath(ms, call, animation, config) {
|
|
1495
|
-
const waypoints = extractArcWaypoints(animation);
|
|
1496
|
-
if (waypoints.length < 2)
|
|
1497
|
-
return false;
|
|
1498
|
-
const segments = config.segments.length === waypoints.length - 1
|
|
1499
|
-
? config.segments
|
|
1500
|
-
: Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 }));
|
|
1501
|
-
const motionPathCode = buildMotionPathObjectCode({
|
|
1502
|
-
waypoints,
|
|
1503
|
-
segments,
|
|
1504
|
-
autoRotate: config.autoRotate,
|
|
1505
|
-
});
|
|
1506
|
-
const vars = call.varsArg;
|
|
1507
|
-
if (vars?.type !== "ObjectExpression")
|
|
1508
|
-
return false;
|
|
1509
|
-
// Insert motionPath right after the opening `{` (appendRight at start+1) so the
|
|
1510
|
-
// insertion point can never coincide with the end boundary of the x/y removal
|
|
1511
|
-
// range. upsertProp would appendLeft at `end - 1`, which collides with a
|
|
1512
|
-
// remove-range that ends at the same offset when x/y are the only props —
|
|
1513
|
-
// MagicString then discards the append and the output loses everything.
|
|
1514
|
-
const editable = (vars.properties ?? []).filter(isObjectProperty);
|
|
1515
|
-
const survivesRemoval = editable.some((p) => {
|
|
1516
|
-
const k = propKeyName(p);
|
|
1517
|
-
return k !== "x" && k !== "y";
|
|
1518
|
-
});
|
|
1519
|
-
const sep = survivesRemoval ? ", " : "";
|
|
1520
|
-
ms.appendRight(vars.start + 1, ` motionPath: ${motionPathCode}${sep}`);
|
|
1521
|
-
stripXYFromKeyframes(ms, findPropertyNode(call.varsArg, "keyframes"));
|
|
1522
|
-
removePropsByKey(ms, call.varsArg, new Set(["x", "y"]));
|
|
1523
|
-
return true;
|
|
1524
|
-
}
|
|
1525
|
-
export function setArcPathInScript(script, animationId, config) {
|
|
1526
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1527
|
-
if (!parsed)
|
|
1528
|
-
return script;
|
|
1529
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1530
|
-
if (!target)
|
|
1531
|
-
return script;
|
|
1532
|
-
const ms = new MagicString(script);
|
|
1533
|
-
const handled = config.enabled
|
|
1534
|
-
? enableArcPath(ms, target.call, target.animation, config)
|
|
1535
|
-
: disableArcPath(ms, target.call);
|
|
1536
|
-
return handled ? ms.toString() : script;
|
|
1537
|
-
}
|
|
1538
|
-
export function updateArcSegmentInScript(script, animationId, segmentIndex, update) {
|
|
1539
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1540
|
-
if (!parsed)
|
|
1541
|
-
return script;
|
|
1542
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1543
|
-
if (!target)
|
|
1544
|
-
return script;
|
|
1545
|
-
const { call, animation } = target;
|
|
1546
|
-
if (!animation.arcPath?.enabled)
|
|
1547
|
-
return script;
|
|
1548
|
-
const segments = [...animation.arcPath.segments];
|
|
1549
|
-
const existingSeg = segments[segmentIndex];
|
|
1550
|
-
if (segmentIndex < 0 || segmentIndex >= segments.length || !existingSeg)
|
|
1551
|
-
return script;
|
|
1552
|
-
segments[segmentIndex] = { ...existingSeg, ...update };
|
|
1553
|
-
const waypoints = extractArcWaypoints(animation);
|
|
1554
|
-
if (waypoints.length < 2)
|
|
1555
|
-
return script;
|
|
1556
|
-
const motionPathCode = buildMotionPathObjectCode({
|
|
1557
|
-
waypoints,
|
|
1558
|
-
segments,
|
|
1559
|
-
autoRotate: animation.arcPath.autoRotate,
|
|
1560
|
-
});
|
|
1561
|
-
const mpProp = findPropertyNode(call.varsArg, "motionPath");
|
|
1562
|
-
if (!mpProp)
|
|
1563
|
-
return script;
|
|
1564
|
-
const ms = new MagicString(script);
|
|
1565
|
-
ms.overwrite(mpProp.value.start, mpProp.value.end, motionPathCode);
|
|
1566
|
-
return ms.toString();
|
|
1567
|
-
}
|
|
1568
|
-
export function removeArcPathFromScript(script, animationId) {
|
|
1569
|
-
return setArcPathInScript(script, animationId, {
|
|
1570
|
-
enabled: false,
|
|
1571
|
-
autoRotate: false,
|
|
1572
|
-
segments: [],
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
// ── splitAnimationsInScript helpers ──────────────────────────────────────────
|
|
1576
|
-
/** Overwrite the selector (first arg) of a tween call. */
|
|
1577
|
-
function updateAnimationSelectorInScript(script, animationId, newSelector) {
|
|
1578
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1579
|
-
if (!parsed)
|
|
1580
|
-
return script;
|
|
1581
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1582
|
-
if (!target)
|
|
1583
|
-
return script;
|
|
1584
|
-
const selectorArg = target.call.node.arguments?.[0];
|
|
1585
|
-
if (!selectorArg)
|
|
1586
|
-
return script;
|
|
1587
|
-
const ms = new MagicString(script);
|
|
1588
|
-
ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(newSelector));
|
|
1589
|
-
return ms.toString();
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* Insert a `tl.set()` call immediately after the timeline declaration
|
|
1593
|
-
* (before existing tweens) to establish inherited state on a new element.
|
|
1594
|
-
*/
|
|
1595
|
-
function insertInheritedStateSetInScript(script, selector, position, properties) {
|
|
1596
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1597
|
-
if (!parsed)
|
|
1598
|
-
return script;
|
|
1599
|
-
const props = Object.entries(properties)
|
|
1600
|
-
.map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`)
|
|
1601
|
-
.join(", ");
|
|
1602
|
-
const code = `${parsed.timelineVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;
|
|
1603
|
-
const ms = new MagicString(script);
|
|
1604
|
-
const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);
|
|
1605
|
-
const firstLocated = parsed.located[0];
|
|
1606
|
-
if (tlDecl) {
|
|
1607
|
-
ms.appendLeft(tlDecl.end, "\n" + code);
|
|
1608
|
-
}
|
|
1609
|
-
else if (firstLocated) {
|
|
1610
|
-
const firstCall = firstLocated.call;
|
|
1611
|
-
const exprStmt = findEnclosingExpressionStatement(firstCall.ancestors);
|
|
1612
|
-
const insertAt = exprStmt?.start ?? firstCall.node.start;
|
|
1613
|
-
ms.prependLeft(insertAt, code + "\n");
|
|
1614
|
-
}
|
|
1615
|
-
else {
|
|
1616
|
-
ms.append("\n" + code);
|
|
1617
|
-
}
|
|
1618
|
-
return ms.toString();
|
|
1619
|
-
}
|
|
1620
|
-
/**
|
|
1621
|
-
* Compute, in forward (timeline) order, the inherited-props baseline available
|
|
1622
|
-
* BEFORE each matching tween, plus the final cumulative state at the split point.
|
|
1623
|
-
* A tween contributes to later baselines when it ends at/before the split (full
|
|
1624
|
-
* props or last keyframe), spans the split via keyframes (kfs at/before split),
|
|
1625
|
-
* or spans the split as a flat tween (its interpolated midpoint). Decoupled from
|
|
1626
|
-
* the reverse write loop so the spanning-tween midpoint reads earlier tweens.
|
|
1627
|
-
*/
|
|
1628
|
-
// fallow-ignore-next-line complexity
|
|
1629
|
-
function computeForwardBaselines(matching, splitTime) {
|
|
1630
|
-
const before = [];
|
|
1631
|
-
const acc = {};
|
|
1632
|
-
for (const anim of matching) {
|
|
1633
|
-
before.push({ ...acc });
|
|
1634
|
-
const pos = typeof anim.position === "number" ? anim.position : 0;
|
|
1635
|
-
const dur = anim.duration ?? 0;
|
|
1636
|
-
const animEnd = pos + dur;
|
|
1637
|
-
if (anim.keyframes) {
|
|
1638
|
-
const kfs = anim.keyframes.keyframes;
|
|
1639
|
-
if (pos >= splitTime) {
|
|
1640
|
-
// Moves wholly to the new element — contributes nothing to the baseline.
|
|
1641
|
-
}
|
|
1642
|
-
else if (animEnd > splitTime) {
|
|
1643
|
-
for (const kf of kfs) {
|
|
1644
|
-
const kfTime = pos + (kf.percentage / 100) * dur;
|
|
1645
|
-
if (kfTime <= splitTime) {
|
|
1646
|
-
for (const [k, v] of Object.entries(kf.properties))
|
|
1647
|
-
acc[k] = v;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
else {
|
|
1652
|
-
const lastKf = kfs[kfs.length - 1];
|
|
1653
|
-
if (lastKf) {
|
|
1654
|
-
for (const [k, v] of Object.entries(lastKf.properties))
|
|
1655
|
-
acc[k] = v;
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
continue;
|
|
1659
|
-
}
|
|
1660
|
-
if (animEnd <= splitTime) {
|
|
1661
|
-
for (const [k, v] of Object.entries(anim.properties))
|
|
1662
|
-
acc[k] = v;
|
|
1663
|
-
continue;
|
|
1664
|
-
}
|
|
1665
|
-
if (pos >= splitTime)
|
|
1666
|
-
continue;
|
|
1667
|
-
// Flat tween spanning the split — its midpoint becomes the inherited value.
|
|
1668
|
-
const progress = dur > 0 ? (splitTime - pos) / dur : 0;
|
|
1669
|
-
const fromSource = anim.fromProperties ?? acc;
|
|
1670
|
-
for (const [k, v] of Object.entries(anim.properties)) {
|
|
1671
|
-
if (typeof v !== "number") {
|
|
1672
|
-
acc[k] = v;
|
|
1673
|
-
continue;
|
|
1674
|
-
}
|
|
1675
|
-
const fromVal = typeof fromSource[k] === "number" ? fromSource[k] : 0;
|
|
1676
|
-
acc[k] = fromVal + (v - fromVal) * progress;
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
return { before, final: { ...acc } };
|
|
1680
|
-
}
|
|
1681
|
-
// Split one tween that straddles the split point: trim the original to the
|
|
1682
|
-
// first half (interpolated midpoint as its new end) and add a fromTo for the
|
|
1683
|
-
// second half on the new element. `fromSource` is the forward baseline.
|
|
1684
|
-
function buildSpanningSplit(result, anim, pos, dur, fromSource, ctx) {
|
|
1685
|
-
const progress = dur > 0 ? (ctx.splitTime - pos) / dur : 0;
|
|
1686
|
-
const midProps = {};
|
|
1687
|
-
for (const [k, v] of Object.entries(anim.properties)) {
|
|
1688
|
-
if (typeof v !== "number") {
|
|
1689
|
-
midProps[k] = v;
|
|
1690
|
-
continue;
|
|
1691
|
-
}
|
|
1692
|
-
const fromVal = typeof fromSource[k] === "number" ? fromSource[k] : 0;
|
|
1693
|
-
midProps[k] = fromVal + (v - fromVal) * progress;
|
|
1694
|
-
}
|
|
1695
|
-
const trimmed = updateAnimationInScript(result, anim.id, {
|
|
1696
|
-
duration: ctx.splitTime - pos,
|
|
1697
|
-
properties: midProps,
|
|
1698
|
-
});
|
|
1699
|
-
return addAnimationToScript(trimmed, {
|
|
1700
|
-
targetSelector: ctx.newSelector,
|
|
1701
|
-
method: "fromTo",
|
|
1702
|
-
position: ctx.newElementStart,
|
|
1703
|
-
duration: pos + dur - ctx.splitTime,
|
|
1704
|
-
properties: { ...anim.properties },
|
|
1705
|
-
fromProperties: { ...midProps },
|
|
1706
|
-
ease: anim.ease,
|
|
1707
|
-
extras: anim.extras,
|
|
1708
|
-
}).script;
|
|
1709
|
-
}
|
|
1710
|
-
// Decide what one matching tween does at the split point: move to the new
|
|
1711
|
-
// element (wholly after), stay (wholly before / keyframes before), get skipped
|
|
1712
|
-
// (keyframes spanning), or get interpolated in half (spanning). Returns the
|
|
1713
|
-
// updated script; pushes any skip reason into `skippedSelectors`.
|
|
1714
|
-
function applyTweenSplit(result, anim, baselineBefore, ctx, skippedSelectors) {
|
|
1715
|
-
const pos = typeof anim.position === "number" ? anim.position : 0;
|
|
1716
|
-
const dur = anim.duration ?? 0;
|
|
1717
|
-
const animEnd = pos + dur;
|
|
1718
|
-
if (anim.keyframes) {
|
|
1719
|
-
if (pos >= ctx.splitTime)
|
|
1720
|
-
return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);
|
|
1721
|
-
if (animEnd > ctx.splitTime) {
|
|
1722
|
-
skippedSelectors.push(`${ctx.originalSelector} (keyframes spanning split)`);
|
|
1723
|
-
}
|
|
1724
|
-
// Inherited-state for kf tweens is handled by computeForwardBaselines.
|
|
1725
|
-
return result;
|
|
1726
|
-
}
|
|
1727
|
-
// Wholly before the split — kept on the original element.
|
|
1728
|
-
if (animEnd <= ctx.splitTime)
|
|
1729
|
-
return result;
|
|
1730
|
-
// Wholly after — move to the new element.
|
|
1731
|
-
if (pos >= ctx.splitTime)
|
|
1732
|
-
return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);
|
|
1733
|
-
// Spans the split — interpolate the midpoint from the FORWARD baseline.
|
|
1734
|
-
const fromSource = anim.fromProperties ?? baselineBefore;
|
|
1735
|
-
return buildSpanningSplit(result, anim, pos, dur, fromSource, ctx);
|
|
1736
|
-
}
|
|
1737
|
-
export function splitAnimationsInScript(script, opts) {
|
|
1738
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1739
|
-
if (!parsed)
|
|
1740
|
-
return { script, skippedSelectors: [] };
|
|
1741
|
-
const originalSelector = `#${opts.originalId}`;
|
|
1742
|
-
const newSelector = `#${opts.newId}`;
|
|
1743
|
-
const animations = parsed.located.map((l) => l.animation);
|
|
1744
|
-
const skippedSelectors = [];
|
|
1745
|
-
for (const a of animations) {
|
|
1746
|
-
if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {
|
|
1747
|
-
skippedSelectors.push(a.targetSelector);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
const matching = animations.filter((a) => a.targetSelector === originalSelector);
|
|
1751
|
-
if (matching.length === 0)
|
|
1752
|
-
return { script, skippedSelectors };
|
|
1753
|
-
let result = script;
|
|
1754
|
-
const newElementStart = opts.splitTime;
|
|
1755
|
-
// Forward pre-pass: compute the inherited-props baseline available BEFORE each
|
|
1756
|
-
// matching tween, in source/timeline order. The write loop below runs in
|
|
1757
|
-
// REVERSE (so updateAnimationSelectorInScript's selector edits can't shift the
|
|
1758
|
-
// count-based IDs of not-yet-processed tweens), but the spanning-tween midpoint
|
|
1759
|
-
// interpolation needs the baseline from EARLIER tweens — which a reverse
|
|
1760
|
-
// accumulator hasn't seen yet. Decoupling the two fixes the wrong midpoint.
|
|
1761
|
-
const { before: baselineBefore, final: finalInheritedProps } = computeForwardBaselines(matching, opts.splitTime);
|
|
1762
|
-
// Reverse iteration: updateAnimationSelectorInScript mutates selectors which
|
|
1763
|
-
// can shift count-based ID suffixes for later animations.
|
|
1764
|
-
const ctx = { splitTime: opts.splitTime, originalSelector, newSelector, newElementStart };
|
|
1765
|
-
for (let i = matching.length - 1; i >= 0; i--) {
|
|
1766
|
-
const anim = matching[i];
|
|
1767
|
-
if (!anim)
|
|
1768
|
-
continue;
|
|
1769
|
-
result = applyTweenSplit(result, anim, baselineBefore[i] ?? {}, ctx, skippedSelectors);
|
|
1770
|
-
}
|
|
1771
|
-
if (Object.keys(finalInheritedProps).length > 0) {
|
|
1772
|
-
result = insertInheritedStateSetInScript(result, newSelector, newElementStart, finalInheritedProps);
|
|
1773
|
-
}
|
|
1774
|
-
return { script: result, skippedSelectors };
|
|
1775
|
-
}
|
|
1776
|
-
// ── Unroll dynamic animations ────────────────────────────────────────────────
|
|
1777
|
-
function isLoopNode(node) {
|
|
1778
|
-
const t = node?.type;
|
|
1779
|
-
return (t === "ForStatement" ||
|
|
1780
|
-
t === "ForInStatement" ||
|
|
1781
|
-
t === "ForOfStatement" ||
|
|
1782
|
-
t === "WhileStatement");
|
|
1783
|
-
}
|
|
1784
|
-
function isForEachStatement(node) {
|
|
1785
|
-
return (node?.type === "ExpressionStatement" &&
|
|
1786
|
-
node.expression?.type === "CallExpression" &&
|
|
1787
|
-
node.expression.callee?.property?.name === "forEach");
|
|
1788
|
-
}
|
|
1789
|
-
/** The nearest enclosing loop / forEach AST node (not just its byte range). */
|
|
1790
|
-
function findEnclosingLoopNode(ancestors) {
|
|
1791
|
-
for (let i = ancestors.length - 2; i >= 0; i--) {
|
|
1792
|
-
const node = ancestors[i];
|
|
1793
|
-
if (isLoopNode(node) || isForEachStatement(node))
|
|
1794
|
-
return node;
|
|
1795
|
-
}
|
|
1796
|
-
return null;
|
|
1797
|
-
}
|
|
1798
|
-
/** Statements making up a loop's body block, or null when not a simple block. */
|
|
1799
|
-
function loopBodyStatements(loopNode) {
|
|
1800
|
-
let body;
|
|
1801
|
-
if (loopNode?.type === "ExpressionStatement") {
|
|
1802
|
-
// forEach(cb): body is the callback's block.
|
|
1803
|
-
const cb = loopNode.expression?.arguments?.[0];
|
|
1804
|
-
body = cb?.body;
|
|
1805
|
-
}
|
|
1806
|
-
else {
|
|
1807
|
-
body = loopNode?.body;
|
|
1808
|
-
}
|
|
1809
|
-
if (body?.type !== "BlockStatement")
|
|
1810
|
-
return null;
|
|
1811
|
-
return (body.body ?? []).filter((s) => s?.type === "ExpressionStatement");
|
|
1812
|
-
}
|
|
1813
|
-
/** The loop's index identifier name (`for (let i …)`), used for per-iteration substitution. */
|
|
1814
|
-
function loopIndexVarName(loopNode) {
|
|
1815
|
-
if (loopNode?.type === "ForStatement") {
|
|
1816
|
-
const decl = loopNode.init?.declarations?.[0];
|
|
1817
|
-
return typeof decl?.id?.name === "string" ? decl.id.name : null;
|
|
1818
|
-
}
|
|
1819
|
-
return null;
|
|
1820
|
-
}
|
|
1821
|
-
/**
|
|
1822
|
-
* Rewrite one body statement's source for iteration `idx`: replace USES of the
|
|
1823
|
-
* loop index variable (AST Identifier nodes) with the literal index. AST-based,
|
|
1824
|
-
* not a text regex, so the index name appearing inside a string literal (e.g. a
|
|
1825
|
-
* selector ".row-i") or as a non-computed member/key (`obj.i`, `{ i: … }`) is
|
|
1826
|
-
* left untouched — only real references to the variable are substituted.
|
|
1827
|
-
*/
|
|
1828
|
-
// An identifier in "binding position" is a name, not a value reference: a
|
|
1829
|
-
// non-computed member property (`obj.i`) or object-literal key (`{ i: … }`).
|
|
1830
|
-
// Those must NOT be substituted with the iteration index.
|
|
1831
|
-
function isIndexBindingPosition(node, parent) {
|
|
1832
|
-
if (parent?.type === "MemberExpression")
|
|
1833
|
-
return parent.property === node && !parent.computed;
|
|
1834
|
-
if (parent?.type === "Property" || parent?.type === "ObjectProperty") {
|
|
1835
|
-
return parent.key === node && !parent.computed;
|
|
1836
|
-
}
|
|
1837
|
-
return false;
|
|
1838
|
-
}
|
|
1839
|
-
function substituteLoopIndex(stmt, indexVar, idx, script) {
|
|
1840
|
-
const base = stmt.start;
|
|
1841
|
-
const src = script.slice(base, stmt.end);
|
|
1842
|
-
const ranges = [];
|
|
1843
|
-
acornWalk.ancestor(stmt, {
|
|
1844
|
-
Identifier(node, _state, ancestors) {
|
|
1845
|
-
if (node.name !== indexVar)
|
|
1846
|
-
return;
|
|
1847
|
-
if (isIndexBindingPosition(node, ancestors[ancestors.length - 2]))
|
|
1848
|
-
return;
|
|
1849
|
-
ranges.push([node.start - base, node.end - base]);
|
|
1850
|
-
},
|
|
1851
|
-
});
|
|
1852
|
-
if (ranges.length === 0)
|
|
1853
|
-
return src;
|
|
1854
|
-
ranges.sort((a, b) => b[0] - a[0]);
|
|
1855
|
-
let out = src;
|
|
1856
|
-
for (const [s, e] of ranges)
|
|
1857
|
-
out = out.slice(0, s) + String(idx) + out.slice(e);
|
|
1858
|
-
return out;
|
|
1859
|
-
}
|
|
1860
|
-
function buildUnrollReplacement(timelineVar, animation, elements) {
|
|
1861
|
-
const duration = typeof animation.duration === "number" ? animation.duration : 8;
|
|
1862
|
-
const ease = typeof animation.ease === "string" ? animation.ease : "none";
|
|
1863
|
-
const pos = animation.position ?? 0;
|
|
1864
|
-
const posCode = typeof pos === "number" ? String(pos) : JSON.stringify(pos);
|
|
1865
|
-
const calls = elements.map((el) => {
|
|
1866
|
-
const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);
|
|
1867
|
-
const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);
|
|
1868
|
-
return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;
|
|
1869
|
-
});
|
|
1870
|
-
return calls.join("\n ");
|
|
1871
|
-
}
|
|
1872
|
-
/** Build one element's unrolled `tl.to(...)` call from the target animation. */
|
|
1873
|
-
function buildUnrollCallForElement(timelineVar, animation, el) {
|
|
1874
|
-
const duration = typeof animation.duration === "number" ? animation.duration : 8;
|
|
1875
|
-
const ease = typeof animation.ease === "string" ? animation.ease : "none";
|
|
1876
|
-
const pos = animation.position ?? 0;
|
|
1877
|
-
const posCode = typeof pos === "number" ? String(pos) : JSON.stringify(pos);
|
|
1878
|
-
const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);
|
|
1879
|
-
const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);
|
|
1880
|
-
return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;
|
|
1881
|
-
}
|
|
1882
|
-
/** Sentinel: the unroll cannot safely reproduce the loop body — caller no-ops. */
|
|
1883
|
-
const REFUSE_UNROLL = Symbol("refuse-unroll");
|
|
1884
|
-
/** Every statement in a loop's body block (unfiltered), or [] when not a block. */
|
|
1885
|
-
function loopBodyRawStatements(loopNode) {
|
|
1886
|
-
const body = loopNode?.type === "ExpressionStatement"
|
|
1887
|
-
? loopNode.expression?.arguments?.[0]?.body
|
|
1888
|
-
: loopNode?.body;
|
|
1889
|
-
return body?.type === "BlockStatement" ? (body.body ?? []) : [];
|
|
1890
|
-
}
|
|
1891
|
-
/** A node that re-binds `indexVar`: a re-declaration or a function param. */
|
|
1892
|
-
function rebindsIndex(node, indexVar) {
|
|
1893
|
-
if (node.type === "VariableDeclarator")
|
|
1894
|
-
return node.id?.name === indexVar;
|
|
1895
|
-
if (node.type === "FunctionExpression" ||
|
|
1896
|
-
node.type === "FunctionDeclaration" ||
|
|
1897
|
-
node.type === "ArrowFunctionExpression") {
|
|
1898
|
-
return (node.params ?? []).some((p) => p?.name === indexVar);
|
|
1899
|
-
}
|
|
1900
|
-
return false;
|
|
1901
|
-
}
|
|
1902
|
-
/** Object shorthand `{ i }` — substituting the value would yield invalid `{ 0 }`. */
|
|
1903
|
-
function isShorthandIndexUse(node, indexVar) {
|
|
1904
|
-
return ((node.type === "Property" || node.type === "ObjectProperty") &&
|
|
1905
|
-
node.shorthand === true &&
|
|
1906
|
-
propKeyName(node) === indexVar);
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* A sibling statement can't be safely index-substituted when it re-binds the
|
|
1910
|
-
* loop index (shadowing — a nested `for (let i …)`, a callback param `i`) or
|
|
1911
|
-
* uses it in object shorthand (`{ i }`, which would splice to the invalid
|
|
1912
|
-
* `{ 0 }`). substituteLoopIndex has no scope analysis, so in these cases it
|
|
1913
|
-
* would emit broken or wrong code — the unroll must refuse instead.
|
|
1914
|
-
*/
|
|
1915
|
-
function hasUnsafeLoopIndexUse(stmt, indexVar) {
|
|
1916
|
-
let unsafe = false;
|
|
1917
|
-
acornWalk.full(stmt, (node) => {
|
|
1918
|
-
if (!unsafe && (isShorthandIndexUse(node, indexVar) || rebindsIndex(node, indexVar))) {
|
|
1919
|
-
unsafe = true;
|
|
1920
|
-
}
|
|
1921
|
-
});
|
|
1922
|
-
return unsafe;
|
|
1923
|
-
}
|
|
1924
|
-
/** How to handle the loop body's non-target siblings when unrolling. */
|
|
1925
|
-
function unrollSiblingStrategy(loopNode, targetStmt, stmts, indexVar) {
|
|
1926
|
-
const siblings = stmts.filter((s) => s !== targetStmt);
|
|
1927
|
-
// A sibling the filtered statement list doesn't model (non-ExpressionStatement)
|
|
1928
|
-
// would be silently lost by either path — refuse if any exists.
|
|
1929
|
-
const hasUnmodeledSibling = loopBodyRawStatements(loopNode).some((s) => s !== targetStmt && !stmts.includes(s));
|
|
1930
|
-
if (siblings.length === 0 && !hasUnmodeledSibling)
|
|
1931
|
-
return "blanket";
|
|
1932
|
-
if (hasUnmodeledSibling || !indexVar)
|
|
1933
|
-
return "refuse";
|
|
1934
|
-
return siblings.some((s) => hasUnsafeLoopIndexUse(s, indexVar)) ? "refuse" : "preserve";
|
|
1935
|
-
}
|
|
1936
|
-
/** Emit the per-iteration unrolled lines (target → static tl.to, siblings → index-substituted). */
|
|
1937
|
-
function emitUnrolledLines(stmts, targetStmt, elements, timelineVar, animation, indexVar, script) {
|
|
1938
|
-
const lines = [];
|
|
1939
|
-
for (let idx = 0; idx < elements.length; idx++) {
|
|
1940
|
-
const el = elements[idx];
|
|
1941
|
-
if (!el)
|
|
1942
|
-
continue;
|
|
1943
|
-
for (const stmt of stmts) {
|
|
1944
|
-
lines.push(stmt === targetStmt
|
|
1945
|
-
? buildUnrollCallForElement(timelineVar, animation, el)
|
|
1946
|
-
: substituteLoopIndex(stmt, indexVar, idx, script));
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
return lines.join("\n ");
|
|
1950
|
-
}
|
|
1951
|
-
/**
|
|
1952
|
-
* Unroll the loop body, preserving every statement that is NOT the target tween.
|
|
1953
|
-
* For each iteration, emit each non-target statement with the loop index
|
|
1954
|
-
* substituted (e.g. `tl.set(items[i], …)` → `tl.set(items[0], …)`), and replace
|
|
1955
|
-
* the target tween statement with that element's static `tl.to()` call.
|
|
1956
|
-
*
|
|
1957
|
-
* Returns null when a blanket overwrite is lossless (no sibling statements), and
|
|
1958
|
-
* REFUSE_UNROLL when siblings exist but can't be safely reproduced — a non-`for`
|
|
1959
|
-
* loop (no numeric index to splice), a statement we don't model, or an unsafe
|
|
1960
|
-
* index use (shadowing / shorthand). Refusing no-ops the unroll, which is safe:
|
|
1961
|
-
* the dynamic loop keeps rendering correctly, just un-flattened.
|
|
1962
|
-
*/
|
|
1963
|
-
function buildLoopUnrollPreserving(script, timelineVar, animation, elements, loopNode, targetStmt) {
|
|
1964
|
-
const stmts = loopBodyStatements(loopNode);
|
|
1965
|
-
if (!stmts || !stmts.includes(targetStmt))
|
|
1966
|
-
return null;
|
|
1967
|
-
const indexVar = loopIndexVarName(loopNode);
|
|
1968
|
-
const strategy = unrollSiblingStrategy(loopNode, targetStmt, stmts, indexVar);
|
|
1969
|
-
if (strategy === "blanket")
|
|
1970
|
-
return null;
|
|
1971
|
-
if (strategy === "refuse" || !indexVar)
|
|
1972
|
-
return REFUSE_UNROLL;
|
|
1973
|
-
return emitUnrolledLines(stmts, targetStmt, elements, timelineVar, animation, indexVar, script);
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Replace a dynamic loop that generates multiple tween calls with individual
|
|
1977
|
-
* static `tl.to()` calls — one per element. Finds the loop containing the
|
|
1978
|
-
* animation and replaces the loop with unrolled static calls, preserving every
|
|
1979
|
-
* non-target statement in the loop body per iteration.
|
|
1980
|
-
*/
|
|
1981
|
-
export function unrollDynamicAnimations(script, animationId, elements) {
|
|
1982
|
-
// An empty element list has no unrolled form — replacing the loop/statement
|
|
1983
|
-
// with zero calls would silently delete the animation. No-op instead.
|
|
1984
|
-
if (elements.length === 0)
|
|
1985
|
-
return script;
|
|
1986
|
-
const parsed = parseGsapScriptAcornForWrite(script);
|
|
1987
|
-
if (!parsed)
|
|
1988
|
-
return script;
|
|
1989
|
-
const target = parsed.located.find((l) => l.id === animationId);
|
|
1990
|
-
if (!target)
|
|
1991
|
-
return script;
|
|
1992
|
-
const ms = new MagicString(script);
|
|
1993
|
-
const loopNode = findEnclosingLoopNode(target.call.ancestors);
|
|
1994
|
-
if (loopNode) {
|
|
1995
|
-
const targetStmt = findEnclosingExpressionStatement(target.call.ancestors);
|
|
1996
|
-
const preserving = targetStmt
|
|
1997
|
-
? buildLoopUnrollPreserving(script, parsed.timelineVar, target.animation, elements, loopNode, targetStmt)
|
|
1998
|
-
: null;
|
|
1999
|
-
// Siblings exist but can't be safely reproduced — leave the loop untouched
|
|
2000
|
-
// rather than drop or corrupt them. The op no-ops (before === after).
|
|
2001
|
-
if (preserving === REFUSE_UNROLL)
|
|
2002
|
-
return script;
|
|
2003
|
-
// Fall back to the simple whole-body replacement when the body isn't a plain
|
|
2004
|
-
// block of statements we can preserve.
|
|
2005
|
-
const replacement = preserving ?? buildUnrollReplacement(parsed.timelineVar, target.animation, elements);
|
|
2006
|
-
ms.overwrite(loopNode.start, loopNode.end, replacement);
|
|
2007
|
-
}
|
|
2008
|
-
else {
|
|
2009
|
-
const stmt = findEnclosingExpressionStatement(target.call.ancestors);
|
|
2010
|
-
if (!stmt)
|
|
2011
|
-
return script;
|
|
2012
|
-
const replacement = buildUnrollReplacement(parsed.timelineVar, target.animation, elements);
|
|
2013
|
-
ms.overwrite(stmt.start, stmt.end, replacement);
|
|
2014
|
-
}
|
|
2015
|
-
return ms.toString();
|
|
2016
|
-
}
|
|
1
|
+
/** @deprecated Import from @hyperframes/parsers/gsap-writer-acorn */
|
|
2
|
+
export * from "@hyperframes/parsers/gsap-writer-acorn";
|
|
2017
3
|
//# sourceMappingURL=gsapWriterAcorn.js.map
|