@hyperframes/core 0.6.91 → 0.6.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler/inlineSubCompositions.d.ts.map +1 -1
- package/dist/compiler/inlineSubCompositions.js +17 -0
- package/dist/compiler/inlineSubCompositions.js.map +1 -1
- package/dist/lint/rules/gsap.d.ts.map +1 -1
- package/dist/lint/rules/gsap.js +42 -1
- package/dist/lint/rules/gsap.js.map +1 -1
- package/dist/parsers/gsapConstants.d.ts +4 -0
- package/dist/parsers/gsapConstants.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.js +28 -0
- package/dist/parsers/gsapConstants.js.map +1 -1
- package/dist/parsers/gsapParser.d.ts +13 -0
- package/dist/parsers/gsapParser.d.ts.map +1 -1
- package/dist/parsers/gsapParser.js +355 -36
- package/dist/parsers/gsapParser.js.map +1 -1
- package/dist/parsers/gsapSerialize.d.ts +8 -0
- package/dist/parsers/gsapSerialize.d.ts.map +1 -1
- package/dist/parsers/gsapSerialize.js +2 -2
- package/dist/parsers/gsapSerialize.js.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.js +71 -5
- package/dist/studio-api/helpers/sourceMutation.js.map +1 -1
- package/dist/studio-api/routes/files.d.ts.map +1 -1
- package/dist/studio-api/routes/files.js +26 -3
- package/dist/studio-api/routes/files.js.map +1 -1
- package/package.json +5 -1
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import * as recast from "recast";
|
|
12
12
|
import { parse as babelParse } from "@babel/parser";
|
|
13
13
|
export { serializeGsapAnimations, getAnimationsForElementId, validateCompositionGsap, keyframesToGsapAnimations, gsapAnimationsToKeyframes, SUPPORTED_PROPS, SUPPORTED_EASES, } from "./gsapSerialize";
|
|
14
|
+
export { PROPERTY_GROUPS, classifyPropertyGroup, classifyTweenPropertyGroup, } from "./gsapConstants";
|
|
15
|
+
import { classifyPropertyGroup, classifyTweenPropertyGroup } from "./gsapConstants";
|
|
14
16
|
export { generateSpringEaseData, SPRING_PRESETS } from "./springEase";
|
|
15
17
|
const GSAP_METHODS = new Set(["set", "to", "from", "fromTo"]);
|
|
16
18
|
function parseScript(script) {
|
|
@@ -279,15 +281,34 @@ function isGsapTimelineCall(node) {
|
|
|
279
281
|
node.callee.object?.name === "gsap" &&
|
|
280
282
|
node.callee.property?.name === "timeline");
|
|
281
283
|
}
|
|
282
|
-
function
|
|
284
|
+
function extractTimelineDefaults(callNode, scope) {
|
|
285
|
+
const arg = callNode.arguments?.[0];
|
|
286
|
+
if (!arg || arg.type !== "ObjectExpression")
|
|
287
|
+
return undefined;
|
|
288
|
+
const defaultsProp = arg.properties?.find((p) => isObjectProperty(p) && propKeyName(p) === "defaults");
|
|
289
|
+
if (!defaultsProp?.value || defaultsProp.value.type !== "ObjectExpression")
|
|
290
|
+
return undefined;
|
|
291
|
+
const record = objectExpressionToRecord(defaultsProp.value, scope);
|
|
292
|
+
const result = {};
|
|
293
|
+
if (typeof record.ease === "string")
|
|
294
|
+
result.ease = record.ease;
|
|
295
|
+
if (typeof record.duration === "number")
|
|
296
|
+
result.duration = record.duration;
|
|
297
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
298
|
+
}
|
|
299
|
+
function findTimelineVar(ast, scope) {
|
|
283
300
|
let timelineVar = null;
|
|
284
301
|
let timelineCount = 0;
|
|
302
|
+
let defaults;
|
|
303
|
+
const emptyScope = scope ?? new Map();
|
|
285
304
|
recast.types.visit(ast, {
|
|
286
305
|
visitVariableDeclarator(path) {
|
|
287
306
|
if (isGsapTimelineCall(path.node.init)) {
|
|
288
307
|
timelineCount += 1;
|
|
289
|
-
if (!timelineVar)
|
|
308
|
+
if (!timelineVar) {
|
|
290
309
|
timelineVar = path.node.id?.name ?? null;
|
|
310
|
+
defaults = extractTimelineDefaults(path.node.init, emptyScope);
|
|
311
|
+
}
|
|
291
312
|
}
|
|
292
313
|
this.traverse(path);
|
|
293
314
|
},
|
|
@@ -298,12 +319,13 @@ function findTimelineVar(ast) {
|
|
|
298
319
|
const left = path.node.left;
|
|
299
320
|
if (left?.type === "Identifier")
|
|
300
321
|
timelineVar = left.name;
|
|
322
|
+
defaults = extractTimelineDefaults(path.node.right, emptyScope);
|
|
301
323
|
}
|
|
302
324
|
}
|
|
303
325
|
this.traverse(path);
|
|
304
326
|
},
|
|
305
327
|
});
|
|
306
|
-
return { timelineVar, timelineCount };
|
|
328
|
+
return { timelineVar, timelineCount, defaults };
|
|
307
329
|
}
|
|
308
330
|
/**
|
|
309
331
|
* True when the member chain of `callNode.callee` is rooted at the timeline
|
|
@@ -531,13 +553,13 @@ function parseObjectArrayKeyframes(node, scope) {
|
|
|
531
553
|
if (totalDuration > 0) {
|
|
532
554
|
let cumulative = 0;
|
|
533
555
|
for (const entry of raw) {
|
|
556
|
+
cumulative += entry.duration ?? 0;
|
|
534
557
|
const percentage = Math.round((cumulative / totalDuration) * 100);
|
|
535
558
|
keyframes.push({
|
|
536
559
|
percentage,
|
|
537
560
|
properties: entry.properties,
|
|
538
561
|
...(entry.ease ? { ease: entry.ease } : {}),
|
|
539
562
|
});
|
|
540
|
-
cumulative += entry.duration ?? 0;
|
|
541
563
|
}
|
|
542
564
|
}
|
|
543
565
|
else {
|
|
@@ -759,7 +781,8 @@ function tweenCallToAnimation(call, scope) {
|
|
|
759
781
|
}
|
|
760
782
|
}
|
|
761
783
|
}
|
|
762
|
-
const
|
|
784
|
+
const hasPositionArg = !!call.positionArg;
|
|
785
|
+
const posVal = hasPositionArg ? extractLiteralValue(call.positionArg, scope) : 0;
|
|
763
786
|
const position = typeof posVal === "number" ? posVal : typeof posVal === "string" ? posVal : 0;
|
|
764
787
|
let duration = typeof vars.duration === "number" ? vars.duration : undefined;
|
|
765
788
|
const ease = typeof vars.ease === "string" ? vars.ease : undefined;
|
|
@@ -775,6 +798,19 @@ function tweenCallToAnimation(call, scope) {
|
|
|
775
798
|
duration,
|
|
776
799
|
ease,
|
|
777
800
|
};
|
|
801
|
+
if (!hasPositionArg)
|
|
802
|
+
anim.implicitPosition = true;
|
|
803
|
+
let group = classifyTweenPropertyGroup(properties);
|
|
804
|
+
if (!group && keyframesData) {
|
|
805
|
+
const kfProps = {};
|
|
806
|
+
for (const kf of keyframesData.keyframes) {
|
|
807
|
+
for (const k of Object.keys(kf.properties))
|
|
808
|
+
kfProps[k] = true;
|
|
809
|
+
}
|
|
810
|
+
group = classifyTweenPropertyGroup(kfProps);
|
|
811
|
+
}
|
|
812
|
+
if (group)
|
|
813
|
+
anim.propertyGroup = group;
|
|
778
814
|
if (Object.keys(extras).length > 0)
|
|
779
815
|
anim.extras = extras;
|
|
780
816
|
if (keyframesData)
|
|
@@ -787,14 +823,101 @@ function tweenCallToAnimation(call, scope) {
|
|
|
787
823
|
anim.hasUnresolvedSelector = true;
|
|
788
824
|
return anim;
|
|
789
825
|
}
|
|
826
|
+
// ── Timeline Position Resolution ──────────────────────────────────────────
|
|
827
|
+
const GSAP_DEFAULT_DURATION = 0.5;
|
|
828
|
+
// NOTE: Label-based positions (e.g. "myLabel+=0.5") are not yet resolved —
|
|
829
|
+
// they fall through to parseFloat which returns null for non-numeric strings.
|
|
830
|
+
function resolvePositionString(pos, cursor, prevStart) {
|
|
831
|
+
const trimmed = pos.trim();
|
|
832
|
+
if (trimmed === "")
|
|
833
|
+
return cursor;
|
|
834
|
+
if (trimmed.startsWith("+=")) {
|
|
835
|
+
const n = Number.parseFloat(trimmed.slice(2));
|
|
836
|
+
return Number.isFinite(n) ? cursor + n : null;
|
|
837
|
+
}
|
|
838
|
+
if (trimmed.startsWith("-=")) {
|
|
839
|
+
const n = Number.parseFloat(trimmed.slice(2));
|
|
840
|
+
return Number.isFinite(n) ? cursor - n : null;
|
|
841
|
+
}
|
|
842
|
+
if (trimmed === "<")
|
|
843
|
+
return prevStart;
|
|
844
|
+
if (trimmed === ">")
|
|
845
|
+
return cursor;
|
|
846
|
+
if (trimmed.startsWith("<")) {
|
|
847
|
+
const n = Number.parseFloat(trimmed.slice(1));
|
|
848
|
+
return Number.isFinite(n) ? prevStart + n : null;
|
|
849
|
+
}
|
|
850
|
+
if (trimmed.startsWith(">")) {
|
|
851
|
+
const n = Number.parseFloat(trimmed.slice(1));
|
|
852
|
+
return Number.isFinite(n) ? cursor + n : null;
|
|
853
|
+
}
|
|
854
|
+
const n = Number.parseFloat(trimmed);
|
|
855
|
+
return Number.isFinite(n) ? n : null;
|
|
856
|
+
}
|
|
857
|
+
function applyTimelineDefaults(anims, defaults) {
|
|
858
|
+
if (!defaults)
|
|
859
|
+
return;
|
|
860
|
+
for (const anim of anims) {
|
|
861
|
+
if (anim.method === "set")
|
|
862
|
+
continue;
|
|
863
|
+
if (anim.duration === undefined && defaults.duration !== undefined) {
|
|
864
|
+
anim.duration = defaults.duration;
|
|
865
|
+
}
|
|
866
|
+
if (anim.ease === undefined && defaults.ease !== undefined) {
|
|
867
|
+
anim.ease = defaults.ease;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function resolveTimelinePositions(anims) {
|
|
872
|
+
let cursor = 0;
|
|
873
|
+
let prevStart = 0;
|
|
874
|
+
for (const anim of anims) {
|
|
875
|
+
const duration = anim.method === "set" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);
|
|
876
|
+
let start;
|
|
877
|
+
if (anim.implicitPosition) {
|
|
878
|
+
start = cursor;
|
|
879
|
+
}
|
|
880
|
+
else if (typeof anim.position === "number") {
|
|
881
|
+
start = anim.position;
|
|
882
|
+
}
|
|
883
|
+
else if (typeof anim.position === "string") {
|
|
884
|
+
start = resolvePositionString(anim.position, cursor, prevStart);
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
start = cursor;
|
|
888
|
+
}
|
|
889
|
+
if (start != null) {
|
|
890
|
+
anim.resolvedStart = Math.max(0, start);
|
|
891
|
+
prevStart = anim.resolvedStart;
|
|
892
|
+
cursor = Math.max(cursor, anim.resolvedStart + duration);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function sortBySourcePosition(calls) {
|
|
897
|
+
calls.sort((a, b) => {
|
|
898
|
+
const aLoc = a.node.callee?.property?.loc?.start;
|
|
899
|
+
const bLoc = b.node.callee?.property?.loc?.start;
|
|
900
|
+
if (!aLoc || !bLoc)
|
|
901
|
+
return 0;
|
|
902
|
+
return aLoc.line - bLoc.line || aLoc.column - bLoc.column;
|
|
903
|
+
});
|
|
904
|
+
}
|
|
790
905
|
// ── Stable ID Generation ───────────────────────────────────────────────────
|
|
906
|
+
/**
|
|
907
|
+
* IDs are transient — recomputed on every parse, never persisted across sessions.
|
|
908
|
+
* They exist only in ephemeral request/response payloads, React component state,
|
|
909
|
+
* and the in-memory keyframe cache (rebuilt on every page load). No database,
|
|
910
|
+
* localStorage, or file stores animation IDs, so changing the ID format (e.g.
|
|
911
|
+
* adding a `-scale`/`-position` suffix) is safe.
|
|
912
|
+
*/
|
|
791
913
|
function assignStableIds(anims) {
|
|
792
914
|
const counts = new Map();
|
|
793
915
|
return anims.map((anim) => {
|
|
794
916
|
const posKey = typeof anim.position === "number"
|
|
795
917
|
? String(Math.round(anim.position * 1000))
|
|
796
918
|
: String(anim.position);
|
|
797
|
-
const
|
|
919
|
+
const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : "";
|
|
920
|
+
const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;
|
|
798
921
|
const count = (counts.get(base) ?? 0) + 1;
|
|
799
922
|
counts.set(base, count);
|
|
800
923
|
const id = count === 1 ? base : `${base}-${count}`;
|
|
@@ -811,10 +934,14 @@ function parseGsapAst(script) {
|
|
|
811
934
|
const ast = parseScript(script);
|
|
812
935
|
const scope = collectScopeBindings(ast);
|
|
813
936
|
const targetBindings = collectTargetBindings(ast, scope);
|
|
814
|
-
const detection = findTimelineVar(ast);
|
|
937
|
+
const detection = findTimelineVar(ast, scope);
|
|
815
938
|
const timelineVar = detection.timelineVar ?? "tl";
|
|
816
939
|
const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);
|
|
817
|
-
|
|
940
|
+
sortBySourcePosition(calls);
|
|
941
|
+
const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope));
|
|
942
|
+
applyTimelineDefaults(rawAnims, detection.defaults);
|
|
943
|
+
resolveTimelinePositions(rawAnims);
|
|
944
|
+
const animations = assignStableIds(rawAnims);
|
|
818
945
|
const located = animations.map((animation, i) => ({
|
|
819
946
|
id: animation.id,
|
|
820
947
|
call: calls[i],
|
|
@@ -1137,7 +1264,11 @@ export function removeAnimationFromScript(script, animationId) {
|
|
|
1137
1264
|
console.warn("[gsap-parser] removeAnimationFromScript parse failed:", e);
|
|
1138
1265
|
return script;
|
|
1139
1266
|
}
|
|
1140
|
-
|
|
1267
|
+
let target = parsed.located.find((l) => l.id === animationId);
|
|
1268
|
+
if (!target) {
|
|
1269
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1270
|
+
target = parsed.located.find((l) => l.id === convertedId);
|
|
1271
|
+
}
|
|
1141
1272
|
if (!target)
|
|
1142
1273
|
return script;
|
|
1143
1274
|
const node = target.call.node;
|
|
@@ -1314,6 +1445,23 @@ function percentageFromKey(key) {
|
|
|
1314
1445
|
const m = PERCENTAGE_KEY_RE.exec(key);
|
|
1315
1446
|
return m ? Number.parseFloat(m[1]) : Number.NaN;
|
|
1316
1447
|
}
|
|
1448
|
+
const PCT_TOLERANCE = 2;
|
|
1449
|
+
function findKeyframePropByPct(kfNode, percentage) {
|
|
1450
|
+
const props = kfNode.properties;
|
|
1451
|
+
for (let i = 0; i < props.length; i++) {
|
|
1452
|
+
if (!isObjectProperty(props[i]))
|
|
1453
|
+
continue;
|
|
1454
|
+
const key = propKeyName(props[i]);
|
|
1455
|
+
if (typeof key !== "string")
|
|
1456
|
+
continue;
|
|
1457
|
+
const parsed = percentageFromKey(key);
|
|
1458
|
+
if (Number.isNaN(parsed))
|
|
1459
|
+
continue;
|
|
1460
|
+
if (Math.abs(parsed - percentage) <= PCT_TOLERANCE)
|
|
1461
|
+
return { idx: i, prop: props[i] };
|
|
1462
|
+
}
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1317
1465
|
/** Build a keyframe value AST node from properties and optional ease. */
|
|
1318
1466
|
function buildKeyframeValueNode(properties, ease) {
|
|
1319
1467
|
const entries = Object.entries(properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
@@ -1368,7 +1516,11 @@ function collapseKeyframesToFlat(varsArg, record) {
|
|
|
1368
1516
|
* updateKeyframeInScript.
|
|
1369
1517
|
*/
|
|
1370
1518
|
function locateKeyframeCtx(script, animationId, percentage) {
|
|
1371
|
-
|
|
1519
|
+
let loc = locateAnimation(script, animationId);
|
|
1520
|
+
if (!loc) {
|
|
1521
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1522
|
+
loc = locateAnimation(script, convertedId);
|
|
1523
|
+
}
|
|
1372
1524
|
if (!loc)
|
|
1373
1525
|
return null;
|
|
1374
1526
|
const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
|
|
@@ -1404,10 +1556,19 @@ export function addKeyframeToScript(script, animationId, percentage, properties,
|
|
|
1404
1556
|
}
|
|
1405
1557
|
const pctKey = `${percentage}%`;
|
|
1406
1558
|
const newValueNode = buildKeyframeValueNode(properties, ease);
|
|
1407
|
-
//
|
|
1408
|
-
const
|
|
1409
|
-
if (
|
|
1410
|
-
|
|
1559
|
+
// Merge into existing keyframe at this percentage, or insert new
|
|
1560
|
+
const existing = findKeyframePropByPct(kfNode, percentage);
|
|
1561
|
+
if (existing) {
|
|
1562
|
+
if (existing.prop.value?.type === "ObjectExpression") {
|
|
1563
|
+
const existingRecord = objectExpressionToRecord(existing.prop.value, loc.parsed.scope);
|
|
1564
|
+
const merged = { ...existingRecord };
|
|
1565
|
+
for (const [k, v] of Object.entries(properties))
|
|
1566
|
+
merged[k] = v;
|
|
1567
|
+
existing.prop.value = buildKeyframeValueNode(merged, ease ?? (typeof existingRecord.ease === "string" ? existingRecord.ease : undefined));
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
existing.prop.value = newValueNode;
|
|
1571
|
+
}
|
|
1411
1572
|
}
|
|
1412
1573
|
else {
|
|
1413
1574
|
// Build the new property node with a quoted percentage key
|
|
@@ -1488,10 +1649,11 @@ export function removeKeyframeFromScript(script, animationId, percentage) {
|
|
|
1488
1649
|
const ctx = locateKeyframeCtx(script, animationId, percentage);
|
|
1489
1650
|
if (!ctx)
|
|
1490
1651
|
return script;
|
|
1491
|
-
const { loc, kfNode
|
|
1492
|
-
const
|
|
1493
|
-
if (
|
|
1652
|
+
const { loc, kfNode } = ctx;
|
|
1653
|
+
const match = findKeyframePropByPct(kfNode, percentage);
|
|
1654
|
+
if (!match)
|
|
1494
1655
|
return script;
|
|
1656
|
+
const removeIdx = match.idx;
|
|
1495
1657
|
kfNode.properties.splice(removeIdx, 1);
|
|
1496
1658
|
const remainingKfs = filterPercentageProps(kfNode);
|
|
1497
1659
|
if (remainingKfs.length < 2) {
|
|
@@ -1509,11 +1671,11 @@ export function updateKeyframeInScript(script, animationId, percentage, properti
|
|
|
1509
1671
|
const ctx = locateKeyframeCtx(script, animationId, percentage);
|
|
1510
1672
|
if (!ctx)
|
|
1511
1673
|
return script;
|
|
1512
|
-
const { loc, kfNode
|
|
1513
|
-
const
|
|
1514
|
-
if (!
|
|
1674
|
+
const { loc, kfNode } = ctx;
|
|
1675
|
+
const match = findKeyframePropByPct(kfNode, percentage);
|
|
1676
|
+
if (!match)
|
|
1515
1677
|
return script;
|
|
1516
|
-
|
|
1678
|
+
match.prop.value = buildKeyframeValueNode(properties, ease);
|
|
1517
1679
|
return recast.print(loc.parsed.ast).code;
|
|
1518
1680
|
}
|
|
1519
1681
|
/** Resolve from/to property maps for a tween being converted to keyframes. */
|
|
@@ -1527,31 +1689,46 @@ const CSS_IDENTITY = {
|
|
|
1527
1689
|
function cssIdentityValue(prop) {
|
|
1528
1690
|
return CSS_IDENTITY[prop] ?? 0;
|
|
1529
1691
|
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Resolve the 0% (from) and 100% (to) property maps for a tween being
|
|
1694
|
+
* converted to percentage keyframes.
|
|
1695
|
+
*
|
|
1696
|
+
* @param resolvedFromValues — Despite the "from" in the name (historical), these
|
|
1697
|
+
* are runtime-captured DOM values that override the conversion endpoint:
|
|
1698
|
+
* - For to(): overrides fromProps (the 0% state / where the element is now).
|
|
1699
|
+
* - For from(): overrides toProps (the 100% state / where the element rests).
|
|
1700
|
+
* - For fromTo(): merges into toProps (the 100% endpoint the user is editing).
|
|
1701
|
+
*/
|
|
1530
1702
|
function resolveConversionProps(anim, resolvedFromValues) {
|
|
1531
1703
|
if (anim.method === "to") {
|
|
1532
|
-
if (resolvedFromValues) {
|
|
1533
|
-
return { fromProps: resolvedFromValues, toProps: { ...anim.properties } };
|
|
1534
|
-
}
|
|
1535
1704
|
const identityFrom = {};
|
|
1536
1705
|
for (const [key, val] of Object.entries(anim.properties)) {
|
|
1537
1706
|
if (val != null)
|
|
1538
1707
|
identityFrom[key] = typeof val === "number" ? cssIdentityValue(key) : val;
|
|
1539
1708
|
}
|
|
1540
|
-
|
|
1709
|
+
const fromProps = resolvedFromValues
|
|
1710
|
+
? { ...identityFrom, ...resolvedFromValues }
|
|
1711
|
+
: identityFrom;
|
|
1712
|
+
return { fromProps, toProps: { ...anim.properties } };
|
|
1541
1713
|
}
|
|
1542
1714
|
if (anim.method === "from") {
|
|
1543
|
-
if (resolvedFromValues) {
|
|
1544
|
-
return { fromProps: { ...anim.properties }, toProps: resolvedFromValues };
|
|
1545
|
-
}
|
|
1546
1715
|
const identityTo = {};
|
|
1547
1716
|
for (const [key, val] of Object.entries(anim.properties)) {
|
|
1548
1717
|
if (val != null)
|
|
1549
1718
|
identityTo[key] = typeof val === "number" ? cssIdentityValue(key) : val;
|
|
1550
1719
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1720
|
+
const toProps = resolvedFromValues ? { ...identityTo, ...resolvedFromValues } : identityTo;
|
|
1721
|
+
return { fromProps: { ...anim.properties }, toProps };
|
|
1722
|
+
}
|
|
1723
|
+
// fromTo(fromVars, toVars): anim.fromProperties = fromVars (0% state),
|
|
1724
|
+
// anim.properties = toVars (100% state). resolvedFromValues contains the
|
|
1725
|
+
// current DOM position from a drag — it represents the NEW destination, so
|
|
1726
|
+
// it merges into toProps (the 100% endpoint the user is editing), NOT into
|
|
1727
|
+
// fromProps. This is intentional and not inverted.
|
|
1728
|
+
const toProps = resolvedFromValues
|
|
1729
|
+
? { ...anim.properties, ...resolvedFromValues }
|
|
1730
|
+
: { ...anim.properties };
|
|
1731
|
+
return { fromProps: { ...(anim.fromProperties ?? {}) }, toProps };
|
|
1555
1732
|
}
|
|
1556
1733
|
/** Strip editable properties and ease/keyframes keys from a varsArg. */
|
|
1557
1734
|
function stripEditableAndEase(varsArg) {
|
|
@@ -1584,7 +1761,11 @@ function insertKeyframesProp(varsArg, fromProps, toProps, easeEach) {
|
|
|
1584
1761
|
* the "to" state for `from()` tweens (the values the DOM would resolve to).
|
|
1585
1762
|
*/
|
|
1586
1763
|
export function convertToKeyframesInScript(script, animationId, resolvedFromValues) {
|
|
1587
|
-
|
|
1764
|
+
let loc = locateAnimation(script, animationId);
|
|
1765
|
+
if (!loc) {
|
|
1766
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1767
|
+
loc = locateAnimation(script, convertedId);
|
|
1768
|
+
}
|
|
1588
1769
|
if (!loc)
|
|
1589
1770
|
return script;
|
|
1590
1771
|
const anim = loc.target.animation;
|
|
@@ -1611,7 +1792,11 @@ export function convertToKeyframesInScript(script, animationId, resolvedFromValu
|
|
|
1611
1792
|
* last keyframe's properties.
|
|
1612
1793
|
*/
|
|
1613
1794
|
export function removeAllKeyframesFromScript(script, animationId) {
|
|
1614
|
-
|
|
1795
|
+
let loc = locateAnimation(script, animationId);
|
|
1796
|
+
if (!loc) {
|
|
1797
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1798
|
+
loc = locateAnimation(script, convertedId);
|
|
1799
|
+
}
|
|
1615
1800
|
if (!loc)
|
|
1616
1801
|
return script;
|
|
1617
1802
|
const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
|
|
@@ -1636,7 +1821,11 @@ export function removeAllKeyframesFromScript(script, animationId) {
|
|
|
1636
1821
|
* Called when the user first edits a dynamically-generated keyframe in the studio.
|
|
1637
1822
|
*/
|
|
1638
1823
|
export function materializeKeyframesInScript(script, animationId, keyframes, easeEach, resolvedSelector) {
|
|
1639
|
-
|
|
1824
|
+
let loc = locateAnimation(script, animationId);
|
|
1825
|
+
if (!loc) {
|
|
1826
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1827
|
+
loc = locateAnimation(script, convertedId);
|
|
1828
|
+
}
|
|
1640
1829
|
if (!loc)
|
|
1641
1830
|
return script;
|
|
1642
1831
|
const varsArg = loc.target.call.varsArg;
|
|
@@ -1844,6 +2033,136 @@ export function removeArcPathFromScript(script, animationId) {
|
|
|
1844
2033
|
segments: [],
|
|
1845
2034
|
});
|
|
1846
2035
|
}
|
|
2036
|
+
// ── Split Into Property Groups ────────────────────────────────────────────
|
|
2037
|
+
/**
|
|
2038
|
+
* Split a multi-group tween into separate per-group tweens. Each resulting
|
|
2039
|
+
* tween contains only properties belonging to one property group (position,
|
|
2040
|
+
* scale, rotation, visual, etc.). `transformOrigin` stays with the group that
|
|
2041
|
+
* has the most properties. If the tween already belongs to a single group,
|
|
2042
|
+
* returns the script unchanged with the original ID.
|
|
2043
|
+
*/
|
|
2044
|
+
// fallow-ignore-next-line complexity
|
|
2045
|
+
export function splitIntoPropertyGroups(script, animationId) {
|
|
2046
|
+
let loc = locateAnimation(script, animationId);
|
|
2047
|
+
if (!loc) {
|
|
2048
|
+
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
2049
|
+
loc = locateAnimation(script, convertedId);
|
|
2050
|
+
}
|
|
2051
|
+
if (!loc)
|
|
2052
|
+
return { script, ids: [animationId] };
|
|
2053
|
+
const anim = loc.target.animation;
|
|
2054
|
+
// Collect the properties to partition. For keyframed tweens, gather the
|
|
2055
|
+
// union of all properties across all keyframes. For flat tweens, use the
|
|
2056
|
+
// tween's own properties map.
|
|
2057
|
+
const allPropKeys = new Set();
|
|
2058
|
+
if (anim.keyframes) {
|
|
2059
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
2060
|
+
for (const k of Object.keys(kf.properties))
|
|
2061
|
+
allPropKeys.add(k);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
for (const k of Object.keys(anim.properties))
|
|
2066
|
+
allPropKeys.add(k);
|
|
2067
|
+
}
|
|
2068
|
+
// Partition properties into groups (excluding transformOrigin — handled below).
|
|
2069
|
+
const groupProps = new Map();
|
|
2070
|
+
for (const key of allPropKeys) {
|
|
2071
|
+
if (key === "transformOrigin")
|
|
2072
|
+
continue;
|
|
2073
|
+
const group = classifyPropertyGroup(key);
|
|
2074
|
+
let arr = groupProps.get(group);
|
|
2075
|
+
if (!arr) {
|
|
2076
|
+
arr = [];
|
|
2077
|
+
groupProps.set(group, arr);
|
|
2078
|
+
}
|
|
2079
|
+
arr.push(key);
|
|
2080
|
+
}
|
|
2081
|
+
// Only one group (or zero) — no split needed.
|
|
2082
|
+
if (groupProps.size <= 1)
|
|
2083
|
+
return { script, ids: [anim.id] };
|
|
2084
|
+
// Assign transformOrigin to the group with the most properties.
|
|
2085
|
+
if (allPropKeys.has("transformOrigin")) {
|
|
2086
|
+
let largestGroup;
|
|
2087
|
+
let largestCount = 0;
|
|
2088
|
+
for (const [group, props] of groupProps) {
|
|
2089
|
+
if (props.length > largestCount) {
|
|
2090
|
+
largestCount = props.length;
|
|
2091
|
+
largestGroup = group;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (largestGroup) {
|
|
2095
|
+
groupProps.get(largestGroup).push("transformOrigin");
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
// Build per-group tweens and insert them, then remove the original.
|
|
2099
|
+
let result = script;
|
|
2100
|
+
// Remove the original tween first.
|
|
2101
|
+
result = removeAnimationFromScript(result, anim.id);
|
|
2102
|
+
// Insert one tween per group. Iteration order of the Map follows insertion
|
|
2103
|
+
// order, which mirrors the order properties were encountered.
|
|
2104
|
+
for (const [, props] of groupProps) {
|
|
2105
|
+
const propSet = new Set(props);
|
|
2106
|
+
if (anim.keyframes) {
|
|
2107
|
+
// Build keyframes containing only this group's properties per keyframe.
|
|
2108
|
+
const groupKeyframes = [];
|
|
2109
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
2110
|
+
const filtered = {};
|
|
2111
|
+
for (const [k, v] of Object.entries(kf.properties)) {
|
|
2112
|
+
if (propSet.has(k))
|
|
2113
|
+
filtered[k] = v;
|
|
2114
|
+
}
|
|
2115
|
+
// Skip keyframes where this group has zero properties.
|
|
2116
|
+
if (Object.keys(filtered).length === 0)
|
|
2117
|
+
continue;
|
|
2118
|
+
groupKeyframes.push({
|
|
2119
|
+
percentage: kf.percentage,
|
|
2120
|
+
properties: filtered,
|
|
2121
|
+
...(kf.ease ? { ease: kf.ease } : {}),
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
if (groupKeyframes.length === 0)
|
|
2125
|
+
continue;
|
|
2126
|
+
const addResult = addAnimationWithKeyframesToScript(result, anim.targetSelector, typeof anim.position === "number" ? anim.position : 0, anim.duration ?? 0.5, groupKeyframes, anim.keyframes.easeEach ?? anim.ease);
|
|
2127
|
+
result = addResult.script;
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
// Flat tween — filter properties to this group.
|
|
2131
|
+
const groupProperties = {};
|
|
2132
|
+
for (const [k, v] of Object.entries(anim.properties)) {
|
|
2133
|
+
if (propSet.has(k))
|
|
2134
|
+
groupProperties[k] = v;
|
|
2135
|
+
}
|
|
2136
|
+
if (Object.keys(groupProperties).length === 0)
|
|
2137
|
+
continue;
|
|
2138
|
+
let fromProperties;
|
|
2139
|
+
if (anim.method === "fromTo" && anim.fromProperties) {
|
|
2140
|
+
fromProperties = {};
|
|
2141
|
+
for (const [k, v] of Object.entries(anim.fromProperties)) {
|
|
2142
|
+
if (propSet.has(k))
|
|
2143
|
+
fromProperties[k] = v;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const addResult = addAnimationToScript(result, {
|
|
2147
|
+
targetSelector: anim.targetSelector,
|
|
2148
|
+
method: anim.method,
|
|
2149
|
+
position: anim.position,
|
|
2150
|
+
duration: anim.duration,
|
|
2151
|
+
ease: anim.ease,
|
|
2152
|
+
properties: groupProperties,
|
|
2153
|
+
fromProperties,
|
|
2154
|
+
extras: anim.extras,
|
|
2155
|
+
});
|
|
2156
|
+
result = addResult.script;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
// Re-parse to collect the new IDs.
|
|
2160
|
+
const reParsed = parseGsapAst(result);
|
|
2161
|
+
const newIds = reParsed.located
|
|
2162
|
+
.filter((l) => l.animation.targetSelector === anim.targetSelector)
|
|
2163
|
+
.map((l) => l.id);
|
|
2164
|
+
return { script: result, ids: newIds };
|
|
2165
|
+
}
|
|
1847
2166
|
/**
|
|
1848
2167
|
* Replace a dynamic loop that generates multiple tween calls with individual
|
|
1849
2168
|
* static `tl.to()` calls — one per element. Finds the loop containing the
|
|
@@ -1897,7 +2216,7 @@ export function unrollDynamicAnimations(script, animationId, elements) {
|
|
|
1897
2216
|
if (el.easeEach) {
|
|
1898
2217
|
kfEntries.push(`easeEach: ${JSON.stringify(el.easeEach)}`);
|
|
1899
2218
|
}
|
|
1900
|
-
calls.push(
|
|
2219
|
+
calls.push(`${loc.parsed.timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: { ${kfEntries.join(", ")} }, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`);
|
|
1901
2220
|
}
|
|
1902
2221
|
const replacement = calls.join("\n ");
|
|
1903
2222
|
if (loopNode) {
|