@hyperframes/core 0.6.122-alpha.0 → 0.7.0
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/generated/runtime-inline.js +1 -1
- package/dist/generated/runtime-inline.js.map +1 -1
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +21 -21
- package/dist/hyperframe.runtime.mjs +21 -21
- package/dist/lint/rules/gsap.d.ts.map +1 -1
- package/dist/lint/rules/gsap.js +39 -34
- package/dist/lint/rules/gsap.js.map +1 -1
- package/dist/parsers/gsapConstants.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.js +4 -1
- package/dist/parsers/gsapConstants.js.map +1 -1
- package/dist/parsers/gsapParser.d.ts +54 -0
- package/dist/parsers/gsapParser.d.ts.map +1 -1
- package/dist/parsers/gsapParser.js +348 -4
- package/dist/parsers/gsapParser.js.map +1 -1
- package/dist/parsers/gsapParserExports.d.ts +1 -0
- package/dist/parsers/gsapParserExports.d.ts.map +1 -1
- package/dist/parsers/gsapParserExports.js +4 -0
- package/dist/parsers/gsapParserExports.js.map +1 -1
- package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapWriterAcorn.js +107 -3
- package/dist/parsers/gsapWriterAcorn.js.map +1 -1
- package/dist/runtime/clipTree.d.ts +9 -0
- package/dist/runtime/clipTree.d.ts.map +1 -1
- package/dist/runtime/clipTree.js +12 -1
- package/dist/runtime/clipTree.js.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts +4 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.js +19 -3
- package/dist/studio-api/helpers/sourceMutation.js.map +1 -1
- package/dist/studio-api/helpers/subComposition.d.ts.map +1 -1
- package/dist/studio-api/helpers/subComposition.js +144 -5
- package/dist/studio-api/helpers/subComposition.js.map +1 -1
- package/dist/studio-api/routes/files.d.ts.map +1 -1
- package/dist/studio-api/routes/files.js +141 -16
- package/dist/studio-api/routes/files.js.map +1 -1
- package/dist/studio-api/routes/preview.d.ts.map +1 -1
- package/dist/studio-api/routes/preview.js +39 -3
- package/dist/studio-api/routes/preview.js.map +1 -1
- package/package.json +1 -1
|
@@ -1350,6 +1350,71 @@ function insertInheritedStateSet(script, selector, position, properties) {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
return recast.print(parsed.ast).code;
|
|
1352
1352
|
}
|
|
1353
|
+
/** Marker on Studio-emitted pre-keyframe hold `set`s. `data` is a GSAP-reserved
|
|
1354
|
+
* config key (attached to the tween, never applied to the target), so it carries
|
|
1355
|
+
* the tag without triggering GSAP's "Invalid property" warning. */
|
|
1356
|
+
const STUDIO_HOLD_MARKER = "hf-hold";
|
|
1357
|
+
/** True for a `tl.set(...)` this module emitted to hold a keyframe before its tween.
|
|
1358
|
+
* The Studio filters these out so they never appear as user keyframes/diamonds. */
|
|
1359
|
+
export function isStudioHoldSet(anim) {
|
|
1360
|
+
return anim.method === "set" && anim.properties?.data === STUDIO_HOLD_MARKER;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Keep a `tl.set(selector, {x,y}, 0)` "hold" in front of every position-keyframed
|
|
1364
|
+
* tween that starts after t=0, so the element holds its first keyframe's position
|
|
1365
|
+
* BEFORE the tween plays instead of snapping to its CSS base (the universal NLE
|
|
1366
|
+
* "hold before first keyframe" behavior). The set is tagged with `data: "hf-hold"`
|
|
1367
|
+
* so this pass owns it: every call wipes the prior holds and recomputes from the
|
|
1368
|
+
* current keyframes, keeping them in sync as keyframes are added/moved/deleted.
|
|
1369
|
+
*
|
|
1370
|
+
* Idempotent. Only position props (x/y/xPercent/yPercent) are held — opacity/scale
|
|
1371
|
+
* keep their authored pre-tween behavior. A tween already starting at 0 needs no
|
|
1372
|
+
* hold (no gap before it).
|
|
1373
|
+
*/
|
|
1374
|
+
export function syncPositionHoldsBeforeKeyframes(script) {
|
|
1375
|
+
let parsed;
|
|
1376
|
+
try {
|
|
1377
|
+
parsed = parseGsapScript(script);
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
return script;
|
|
1381
|
+
}
|
|
1382
|
+
// 1. Drop every hold this pass previously emitted, so we recompute fresh.
|
|
1383
|
+
let result = script;
|
|
1384
|
+
const staleHoldIds = parsed.animations.filter(isStudioHoldSet).map((a) => a.id);
|
|
1385
|
+
for (const id of staleHoldIds)
|
|
1386
|
+
result = removeAnimationFromScript(result, id);
|
|
1387
|
+
// 2. Re-add a hold for each position-keyframed tween that starts after t=0.
|
|
1388
|
+
let reparsed;
|
|
1389
|
+
try {
|
|
1390
|
+
reparsed = parseGsapScript(result);
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
1395
|
+
for (const anim of reparsed.animations) {
|
|
1396
|
+
if (!anim.keyframes)
|
|
1397
|
+
continue;
|
|
1398
|
+
const start = anim.resolvedStart ?? (typeof anim.position === "number" ? anim.position : 0);
|
|
1399
|
+
if (!(start > 0.001))
|
|
1400
|
+
continue;
|
|
1401
|
+
const firstKf = [...anim.keyframes.keyframes].sort((a, b) => a.percentage - b.percentage)[0];
|
|
1402
|
+
if (!firstKf)
|
|
1403
|
+
continue;
|
|
1404
|
+
const posProps = {};
|
|
1405
|
+
for (const [k, v] of Object.entries(firstKf.properties)) {
|
|
1406
|
+
if (classifyPropertyGroup(k) === "position" && typeof v === "number")
|
|
1407
|
+
posProps[k] = v;
|
|
1408
|
+
}
|
|
1409
|
+
if (Object.keys(posProps).length === 0)
|
|
1410
|
+
continue;
|
|
1411
|
+
result = insertInheritedStateSet(result, anim.targetSelector, 0, {
|
|
1412
|
+
...posProps,
|
|
1413
|
+
data: STUDIO_HOLD_MARKER,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return result;
|
|
1417
|
+
}
|
|
1353
1418
|
// fallow-ignore-next-line complexity
|
|
1354
1419
|
export function splitAnimationsInScript(script, opts) {
|
|
1355
1420
|
const parsed = parseGsapScript(script);
|
|
@@ -1404,9 +1469,27 @@ export function splitAnimationsInScript(script, opts) {
|
|
|
1404
1469
|
}
|
|
1405
1470
|
continue;
|
|
1406
1471
|
}
|
|
1472
|
+
// `<=` (not `<`) is deliberate: a tween whose end coincides exactly with
|
|
1473
|
+
// the split boundary has fully played by splitTime, so it belongs to the
|
|
1474
|
+
// first half and contributes its resting state to the clone. The spanning
|
|
1475
|
+
// branch below handles only strictly-mid-flight tweens (pos < split < end).
|
|
1407
1476
|
if (animEnd <= opts.splitTime) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1477
|
+
// Only a completed .from() reverts the element to its natural state, so
|
|
1478
|
+
// its recorded properties are the HIDDEN start (e.g. opacity:0), not the
|
|
1479
|
+
// resting state — clearing them keeps the clone at its natural value
|
|
1480
|
+
// instead of pinning it to the from-values (which made it invisible).
|
|
1481
|
+
// .fromTo() and .to() both END at their to-values (no revert), so they
|
|
1482
|
+
// fall through to `else` and inherit `anim.properties` (the to-values) —
|
|
1483
|
+
// .fromTo() must NOT join the .from() clear-branch or the clone would
|
|
1484
|
+
// drop the very state the fromTo just established.
|
|
1485
|
+
if (anim.method === "from") {
|
|
1486
|
+
for (const k of Object.keys(anim.properties))
|
|
1487
|
+
delete inheritedProps[k];
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
for (const [k, v] of Object.entries(anim.properties)) {
|
|
1491
|
+
inheritedProps[k] = v;
|
|
1492
|
+
}
|
|
1410
1493
|
}
|
|
1411
1494
|
continue;
|
|
1412
1495
|
}
|
|
@@ -1523,20 +1606,81 @@ function locateAnimation(script, animationId) {
|
|
|
1523
1606
|
const target = parsed.located.find((l) => l.id === animationId);
|
|
1524
1607
|
return target ? { parsed, target } : null;
|
|
1525
1608
|
}
|
|
1609
|
+
// Animation ids encode the tween's timeline position in ms
|
|
1610
|
+
// (`#puck-a-to-1200-position`). A gesture/convert can re-emit a tween at a
|
|
1611
|
+
// different position, changing its id — so a client that cached the old id (its
|
|
1612
|
+
// selectedGsapAnimations hasn't refreshed) edits a now-nonexistent id and the op
|
|
1613
|
+
// no-ops. Parse `{selector}-{method}-{posMs}-{group}` so we can fall back to the
|
|
1614
|
+
// same selector+method+group tween nearest the requested position.
|
|
1615
|
+
const ANIM_ID_RE = /^(.*)-(fromTo|from|to|set)-(\d+)-([a-z]+)$/;
|
|
1526
1616
|
function locateAnimationWithFallback(script, animationId) {
|
|
1527
1617
|
const loc = locateAnimation(script, animationId);
|
|
1528
1618
|
if (loc)
|
|
1529
1619
|
return loc;
|
|
1530
1620
|
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
1531
|
-
if (convertedId
|
|
1621
|
+
if (convertedId !== animationId) {
|
|
1622
|
+
const converted = locateAnimation(script, convertedId);
|
|
1623
|
+
if (converted)
|
|
1624
|
+
return converted;
|
|
1625
|
+
}
|
|
1626
|
+
// Position-drift fallback: match by stable identity (selector+method+group),
|
|
1627
|
+
// disambiguating by the position closest to the one the caller asked for.
|
|
1628
|
+
const want = ANIM_ID_RE.exec(animationId);
|
|
1629
|
+
if (!want)
|
|
1532
1630
|
return null;
|
|
1533
|
-
|
|
1631
|
+
const [, sel, method, wantPosStr, group] = want;
|
|
1632
|
+
const wantPos = Number(wantPosStr);
|
|
1633
|
+
let parsed;
|
|
1634
|
+
try {
|
|
1635
|
+
parsed = parseGsapAst(script);
|
|
1636
|
+
}
|
|
1637
|
+
catch {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
let best = null;
|
|
1641
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
1642
|
+
for (const l of parsed.located) {
|
|
1643
|
+
const m = ANIM_ID_RE.exec(l.id);
|
|
1644
|
+
if (!m || m[1] !== sel || m[2] !== method || m[4] !== group)
|
|
1645
|
+
continue;
|
|
1646
|
+
const dist = Math.abs(Number(m[3]) - wantPos);
|
|
1647
|
+
if (dist < bestDist) {
|
|
1648
|
+
best = l;
|
|
1649
|
+
bestDist = dist;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return best ? { parsed, target: best } : null;
|
|
1534
1653
|
}
|
|
1535
1654
|
/** Find the keyframes ObjectExpression node on a tween's varsArg, or null. */
|
|
1536
1655
|
function findKeyframesObjectNode(varsArg) {
|
|
1537
1656
|
const node = findPropertyNode(varsArg, "keyframes");
|
|
1538
1657
|
return node?.type === "ObjectExpression" ? node : null;
|
|
1539
1658
|
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Convert array-form keyframes (`keyframes: [{x,y}, …]`) to even-percentage object
|
|
1661
|
+
* form (`{ "0%": {…}, "33.3%": {…}, … }`) IN PLACE, returning the new object node
|
|
1662
|
+
* (or null if not array-form). GSAP distributes an array evenly, so this is
|
|
1663
|
+
* runtime-identical — but it gives the percentage-keyed write ops something to
|
|
1664
|
+
* target. Needed before INSERTING a keyframe at an arbitrary percentage, which an
|
|
1665
|
+
* even array can't host.
|
|
1666
|
+
*/
|
|
1667
|
+
function convertArrayKeyframesToObjectNode(varsArg) {
|
|
1668
|
+
if (varsArg?.type !== "ObjectExpression")
|
|
1669
|
+
return null;
|
|
1670
|
+
const prop = (varsArg.properties ?? []).find((p) => isObjectProperty(p) && propKeyName(p) === "keyframes");
|
|
1671
|
+
if (!prop || prop.value?.type !== "ArrayExpression")
|
|
1672
|
+
return null;
|
|
1673
|
+
const els = (prop.value.elements ?? []).filter((e) => !!e && e.type === "ObjectExpression");
|
|
1674
|
+
const n = els.length;
|
|
1675
|
+
if (n === 0)
|
|
1676
|
+
return null;
|
|
1677
|
+
const entries = els.map((el, i) => {
|
|
1678
|
+
const pct = n > 1 ? Math.round((i / (n - 1)) * 1000) / 10 : 0;
|
|
1679
|
+
return `${JSON.stringify(`${pct}%`)}: ${recast.print(el).code}`;
|
|
1680
|
+
});
|
|
1681
|
+
prop.value = parseExpr(`{ ${entries.join(", ")} }`);
|
|
1682
|
+
return prop.value;
|
|
1683
|
+
}
|
|
1540
1684
|
/** Filter percentage-keyed properties from a keyframes ObjectExpression. */
|
|
1541
1685
|
function filterPercentageProps(kfNode) {
|
|
1542
1686
|
return kfNode.properties.filter((p) => {
|
|
@@ -1584,6 +1728,11 @@ export function addKeyframeToScript(script, animationId, percentage, properties,
|
|
|
1584
1728
|
if (!loc)
|
|
1585
1729
|
return script;
|
|
1586
1730
|
let kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
|
|
1731
|
+
// Array-form keyframes can't host an arbitrary new percentage — normalize to
|
|
1732
|
+
// object form in place first. (convertToKeyframesInScript below only converts
|
|
1733
|
+
// FLAT tweens; it early-returns when keyframes already exist.)
|
|
1734
|
+
if (!kfNode)
|
|
1735
|
+
kfNode = convertArrayKeyframesToObjectNode(loc.target.call.varsArg);
|
|
1587
1736
|
if (!kfNode) {
|
|
1588
1737
|
script = convertToKeyframesInScript(script, animationId);
|
|
1589
1738
|
loc = locateAnimationWithFallback(script, animationId);
|
|
@@ -1687,6 +1836,43 @@ export function addKeyframeToScript(script, animationId, percentage, properties,
|
|
|
1687
1836
|
* remaining keyframe's properties.
|
|
1688
1837
|
*/
|
|
1689
1838
|
export function removeKeyframeFromScript(script, animationId, percentage) {
|
|
1839
|
+
// Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages —
|
|
1840
|
+
// GSAP distributes them evenly. The object-form path below can't see them
|
|
1841
|
+
// (findKeyframesObjectNode only matches ObjectExpression), so removing from an
|
|
1842
|
+
// array-form tween silently no-op'd. Resolve the element by its implicit
|
|
1843
|
+
// percentage and splice it; collapse to a flat tween when fewer than two remain.
|
|
1844
|
+
const arrLoc = locateAnimationWithFallback(script, animationId);
|
|
1845
|
+
// findPropertyNode here returns the property's VALUE node directly.
|
|
1846
|
+
const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, "keyframes");
|
|
1847
|
+
if (arrLoc && arrVal?.type === "ArrayExpression") {
|
|
1848
|
+
const elements = (arrVal.elements ?? []).filter((e) => !!e && e.type === "ObjectExpression");
|
|
1849
|
+
const n = elements.length;
|
|
1850
|
+
if (n === 0)
|
|
1851
|
+
return script;
|
|
1852
|
+
let matchIdx = -1;
|
|
1853
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
1854
|
+
for (let i = 0; i < n; i++) {
|
|
1855
|
+
const pct = n > 1 ? (i / (n - 1)) * 100 : 0;
|
|
1856
|
+
const dist = Math.abs(pct - percentage);
|
|
1857
|
+
if (dist <= PCT_TOLERANCE && dist < bestDist) {
|
|
1858
|
+
matchIdx = i;
|
|
1859
|
+
bestDist = dist;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (matchIdx === -1)
|
|
1863
|
+
return script;
|
|
1864
|
+
const remaining = elements.filter((_, i) => i !== matchIdx);
|
|
1865
|
+
if (remaining.length < 2) {
|
|
1866
|
+
const sole = remaining[0];
|
|
1867
|
+
const record = sole ? objectExpressionToRecord(sole, arrLoc.parsed.scope) : {};
|
|
1868
|
+
collapseKeyframesToFlat(arrLoc.target.call.varsArg, record);
|
|
1869
|
+
}
|
|
1870
|
+
else {
|
|
1871
|
+
const realIdx = arrVal.elements.indexOf(elements[matchIdx]);
|
|
1872
|
+
arrVal.elements.splice(realIdx, 1);
|
|
1873
|
+
}
|
|
1874
|
+
return recast.print(arrLoc.parsed.ast).code;
|
|
1875
|
+
}
|
|
1690
1876
|
const ctx = locateKeyframeCtx(script, animationId, percentage);
|
|
1691
1877
|
if (!ctx)
|
|
1692
1878
|
return script;
|
|
@@ -1709,6 +1895,35 @@ export function removeKeyframeFromScript(script, animationId, percentage) {
|
|
|
1709
1895
|
* Replace the properties (and optionally ease) at an existing keyframe percentage.
|
|
1710
1896
|
*/
|
|
1711
1897
|
export function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
|
|
1898
|
+
// Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages —
|
|
1899
|
+
// GSAP distributes them evenly. The percentage-keyed object path below can't
|
|
1900
|
+
// match them (findKeyframesObjectNode only matches ObjectExpression), so dragging
|
|
1901
|
+
// a motion-path node on an array-authored tween silently no-op'd. Resolve the
|
|
1902
|
+
// element by its implicit percentage and replace it in place. Mirrors the array
|
|
1903
|
+
// branch in removeKeyframeFromScript.
|
|
1904
|
+
const arrLoc = locateAnimationWithFallback(script, animationId);
|
|
1905
|
+
const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, "keyframes");
|
|
1906
|
+
if (arrLoc && arrVal?.type === "ArrayExpression") {
|
|
1907
|
+
const elements = (arrVal.elements ?? []).filter((e) => !!e && e.type === "ObjectExpression");
|
|
1908
|
+
const n = elements.length;
|
|
1909
|
+
if (n === 0)
|
|
1910
|
+
return script;
|
|
1911
|
+
let matchIdx = -1;
|
|
1912
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
1913
|
+
for (let i = 0; i < n; i++) {
|
|
1914
|
+
const pct = n > 1 ? (i / (n - 1)) * 100 : 0;
|
|
1915
|
+
const dist = Math.abs(pct - percentage);
|
|
1916
|
+
if (dist <= PCT_TOLERANCE && dist < bestDist) {
|
|
1917
|
+
matchIdx = i;
|
|
1918
|
+
bestDist = dist;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (matchIdx === -1)
|
|
1922
|
+
return script;
|
|
1923
|
+
const realIdx = arrVal.elements.indexOf(elements[matchIdx]);
|
|
1924
|
+
arrVal.elements[realIdx] = buildKeyframeValueNode(properties, ease);
|
|
1925
|
+
return recast.print(arrLoc.parsed.ast).code;
|
|
1926
|
+
}
|
|
1712
1927
|
const ctx = locateKeyframeCtx(script, animationId, percentage);
|
|
1713
1928
|
if (!ctx)
|
|
1714
1929
|
return script;
|
|
@@ -1991,6 +2206,135 @@ export function updateArcSegmentInScript(script, animationId, segmentIndex, upda
|
|
|
1991
2206
|
}
|
|
1992
2207
|
return recast.print(loc.parsed.ast).code;
|
|
1993
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Move a single motionPath waypoint (anchor) to a new position. The waypoint
|
|
2211
|
+
* list is normalized to anchors for both straight and cubic paths, so
|
|
2212
|
+
* `pointIndex` matches the node order the studio overlay renders; cubic control
|
|
2213
|
+
* points are preserved. No-op when the animation/arc is missing or the index is
|
|
2214
|
+
* out of range.
|
|
2215
|
+
*/
|
|
2216
|
+
export function updateMotionPathPointInScript(script, animationId, pointIndex, point) {
|
|
2217
|
+
const loc = locateAnimation(script, animationId);
|
|
2218
|
+
if (!loc)
|
|
2219
|
+
return script;
|
|
2220
|
+
const anim = loc.target.animation;
|
|
2221
|
+
if (!anim.arcPath?.enabled)
|
|
2222
|
+
return script;
|
|
2223
|
+
const waypoints = extractArcWaypoints(anim);
|
|
2224
|
+
if (pointIndex < 0 || pointIndex >= waypoints.length || waypoints.length < 2)
|
|
2225
|
+
return script;
|
|
2226
|
+
const nextWaypoints = waypoints.map((wp, i) => i === pointIndex ? { x: point.x, y: point.y } : wp);
|
|
2227
|
+
const motionPathCode = buildMotionPathObjectCode({
|
|
2228
|
+
waypoints: nextWaypoints,
|
|
2229
|
+
segments: anim.arcPath.segments,
|
|
2230
|
+
autoRotate: anim.arcPath.autoRotate,
|
|
2231
|
+
});
|
|
2232
|
+
const varsArg = loc.target.call.varsArg;
|
|
2233
|
+
const existingProp = varsArg.properties.find((p) => isObjectProperty(p) && propKeyName(p) === "motionPath");
|
|
2234
|
+
if (existingProp) {
|
|
2235
|
+
existingProp.value = parseExpr(motionPathCode);
|
|
2236
|
+
}
|
|
2237
|
+
return recast.print(loc.parsed.ast).code;
|
|
2238
|
+
}
|
|
2239
|
+
/** True when any segment carries explicit cubic control points. Add/remove are
|
|
2240
|
+
* restricted to curviness (non-cubic) paths — synthesizing control points for
|
|
2241
|
+
* an inserted cubic anchor is out of scope. */
|
|
2242
|
+
function hasCubicSegments(segments) {
|
|
2243
|
+
return segments.some((s) => s.cp1 != null || s.cp2 != null);
|
|
2244
|
+
}
|
|
2245
|
+
function writeMotionPathValue(loc, waypoints, segments, autoRotate) {
|
|
2246
|
+
const motionPathCode = buildMotionPathObjectCode({ waypoints, segments, autoRotate });
|
|
2247
|
+
const varsArg = loc.target.call.varsArg;
|
|
2248
|
+
const existingProp = varsArg.properties.find((p) => isObjectProperty(p) && propKeyName(p) === "motionPath");
|
|
2249
|
+
if (existingProp)
|
|
2250
|
+
existingProp.value = parseExpr(motionPathCode);
|
|
2251
|
+
return recast.print(loc.parsed.ast).code;
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Insert a waypoint at `index` (between existing anchors), splitting the segment
|
|
2255
|
+
* it lands on so the new neighbor inherits its curviness. Non-cubic paths only.
|
|
2256
|
+
* No-op for missing animation/arc, out-of-range index, or cubic paths.
|
|
2257
|
+
*/
|
|
2258
|
+
export function addMotionPathPointInScript(script, animationId, index, point) {
|
|
2259
|
+
const loc = locateAnimation(script, animationId);
|
|
2260
|
+
if (!loc)
|
|
2261
|
+
return script;
|
|
2262
|
+
const anim = loc.target.animation;
|
|
2263
|
+
if (!anim.arcPath?.enabled || hasCubicSegments(anim.arcPath.segments))
|
|
2264
|
+
return script;
|
|
2265
|
+
const waypoints = extractArcWaypoints(anim);
|
|
2266
|
+
// Insert strictly between two anchors: index 1..length-1.
|
|
2267
|
+
if (index < 1 || index > waypoints.length - 1)
|
|
2268
|
+
return script;
|
|
2269
|
+
const segments = [...anim.arcPath.segments];
|
|
2270
|
+
waypoints.splice(index, 0, { x: point.x, y: point.y });
|
|
2271
|
+
const splitCurviness = segments[index - 1]?.curviness ?? 1;
|
|
2272
|
+
segments.splice(index - 1, 0, { curviness: splitCurviness });
|
|
2273
|
+
return writeMotionPathValue(loc, waypoints, segments, anim.arcPath.autoRotate);
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Remove the waypoint at `index`. Refuses to drop below two anchors (a path
|
|
2277
|
+
* can't have fewer). Non-cubic paths only. No-op for missing animation/arc,
|
|
2278
|
+
* out-of-range index, cubic paths, or a 2-point path.
|
|
2279
|
+
*/
|
|
2280
|
+
export function removeMotionPathPointInScript(script, animationId, index) {
|
|
2281
|
+
const loc = locateAnimation(script, animationId);
|
|
2282
|
+
if (!loc)
|
|
2283
|
+
return script;
|
|
2284
|
+
const anim = loc.target.animation;
|
|
2285
|
+
if (!anim.arcPath?.enabled || hasCubicSegments(anim.arcPath.segments))
|
|
2286
|
+
return script;
|
|
2287
|
+
const waypoints = extractArcWaypoints(anim);
|
|
2288
|
+
if (waypoints.length <= 2 || index < 0 || index >= waypoints.length)
|
|
2289
|
+
return script;
|
|
2290
|
+
const segments = [...anim.arcPath.segments];
|
|
2291
|
+
waypoints.splice(index, 1);
|
|
2292
|
+
// Drop the segment on the side that still exists (last anchor → preceding segment).
|
|
2293
|
+
segments.splice(Math.min(index, segments.length - 1), 1);
|
|
2294
|
+
return writeMotionPathValue(loc, waypoints, segments, anim.arcPath.autoRotate);
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Author a fresh 2-anchor motionPath tween on a target element: a straight line
|
|
2298
|
+
* from the element's home (0,0) to `point`, gentle ease, ready for waypoint
|
|
2299
|
+
* editing. Mirrors `addAnimationWithKeyframesToScript`.
|
|
2300
|
+
*/
|
|
2301
|
+
export function addMotionPathToScript(script, targetSelector, position, duration, point, ease = "power1.inOut") {
|
|
2302
|
+
// `id: null` on the failure paths is a deliberate sentinel: callers must
|
|
2303
|
+
// null-check before chaining (e.g. locating the new tween). An empty string
|
|
2304
|
+
// would silently flow into selector/locate calls and match nothing.
|
|
2305
|
+
let parsed;
|
|
2306
|
+
try {
|
|
2307
|
+
parsed = parseGsapAst(script);
|
|
2308
|
+
}
|
|
2309
|
+
catch (e) {
|
|
2310
|
+
console.warn("[gsap-parser] addMotionPathToScript parse failed:", e);
|
|
2311
|
+
return { script, id: null };
|
|
2312
|
+
}
|
|
2313
|
+
if (parsed.located.length === 0 && parsed.detection.timelineVar === null) {
|
|
2314
|
+
return { script, id: null };
|
|
2315
|
+
}
|
|
2316
|
+
const motionPathCode = buildMotionPathObjectCode({
|
|
2317
|
+
waypoints: [
|
|
2318
|
+
{ x: 0, y: 0 },
|
|
2319
|
+
{ x: point.x, y: point.y },
|
|
2320
|
+
],
|
|
2321
|
+
segments: [{ curviness: 1 }],
|
|
2322
|
+
autoRotate: false,
|
|
2323
|
+
});
|
|
2324
|
+
const selector = JSON.stringify(targetSelector);
|
|
2325
|
+
const varEntries = [
|
|
2326
|
+
`motionPath: ${motionPathCode}`,
|
|
2327
|
+
`duration: ${valueToCode(duration)}`,
|
|
2328
|
+
`ease: ${JSON.stringify(ease)}`,
|
|
2329
|
+
];
|
|
2330
|
+
const stmtCode = `${parsed.timelineVar}.to(${selector}, { ${varEntries.join(", ")} }, ${valueToCode(position)});`;
|
|
2331
|
+
const newStatement = parseScript(stmtCode).program.body[0];
|
|
2332
|
+
insertAfterAnchor(parsed, newStatement);
|
|
2333
|
+
const result = recast.print(parsed.ast).code;
|
|
2334
|
+
const reParsed = parseGsapAst(result);
|
|
2335
|
+
const newId = reParsed.located[reParsed.located.length - 1]?.id ?? null;
|
|
2336
|
+
return { script: result, id: newId };
|
|
2337
|
+
}
|
|
1994
2338
|
export function removeArcPathFromScript(script, animationId) {
|
|
1995
2339
|
return setArcPathInScript(script, animationId, {
|
|
1996
2340
|
enabled: false,
|