@hyperframes/parsers 0.7.70 → 0.7.72

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.
@@ -45,10 +45,9 @@ declare function removeKeyframeFromScript(script: string, animationId: string, p
45
45
  /**
46
46
  * Retime a keyframe: move the keyframe at `fromPercentage` to `toPercentage`,
47
47
  * PRESERVING its properties and per-keyframe ease (the Studio "Move to Playhead"
48
- * gesture). Re-sorts keyframes by percentage. If a keyframe already exists at
49
- * `toPercentage`, it is overwritten by the moved one (no duplicate). No-op when
50
- * the animation/keyframe isn't found, the tween has no object-form keyframes, or
51
- * the move resolves onto the same keyframe.
48
+ * gesture). Re-sorts keyframes by percentage. No-op when the animation/keyframe
49
+ * isn't found, the tween has no object-form keyframes, the move resolves onto the
50
+ * same keyframe, or the destination is occupied.
52
51
  */
53
52
  declare function moveKeyframeInScript(script: string, animationId: string, fromPercentage: number, toPercentage: number): string;
54
53
  /**
@@ -29,6 +29,23 @@ function classifyTweenPropertyGroup(properties) {
29
29
  }
30
30
 
31
31
  // src/gsapSerialize.ts
32
+ function mergePercentageKeyframes(keyframes) {
33
+ const byPercentage = /* @__PURE__ */ new Map();
34
+ for (const keyframe of keyframes) {
35
+ const existing = byPercentage.get(keyframe.percentage);
36
+ if (!existing) {
37
+ byPercentage.set(keyframe.percentage, {
38
+ ...keyframe,
39
+ properties: { ...keyframe.properties }
40
+ });
41
+ continue;
42
+ }
43
+ existing.properties = { ...existing.properties, ...keyframe.properties };
44
+ if (keyframe.ease !== void 0) existing.ease = keyframe.ease;
45
+ if (keyframe.auto !== void 0) existing.auto = keyframe.auto;
46
+ }
47
+ return [...byPercentage.values()].sort((a, b) => a.percentage - b.percentage);
48
+ }
32
49
  function buildArcPath(coords, curviness, autoRotate, isCubic) {
33
50
  const first = coords[0];
34
51
  if (coords.length < 2 || !first) return void 0;
@@ -159,6 +176,61 @@ function readProvenance(node) {
159
176
  return node?.__hfProvenance;
160
177
  }
161
178
 
179
+ // src/gsapObjectArrayTiming.ts
180
+ var roundPercentage = (percentage) => Math.round(percentage * 10) / 10;
181
+ var OBJECT_ARRAY_PERCENTAGE_TOLERANCE = 2;
182
+ function getObjectArrayKeyframeTiming(durations) {
183
+ const hasAuthoredDuration = durations.some((duration) => duration !== void 0);
184
+ if (hasAuthoredDuration) {
185
+ if (!durations.every(
186
+ (duration) => typeof duration === "number" && Number.isFinite(duration) && duration > 0
187
+ )) {
188
+ return null;
189
+ }
190
+ const totalDuration = durations.reduce((sum, duration) => sum + duration, 0);
191
+ let cumulative = 0;
192
+ return {
193
+ percentages: durations.map((duration) => {
194
+ cumulative += duration;
195
+ return roundPercentage(cumulative / totalDuration * 100);
196
+ }),
197
+ totalDuration
198
+ };
199
+ }
200
+ const lastIndex = durations.length - 1;
201
+ return {
202
+ percentages: durations.map(
203
+ (_, index) => lastIndex > 0 ? roundPercentage(index / lastIndex * 100) : 0
204
+ )
205
+ };
206
+ }
207
+ function getCompatibleObjectArrayKeyframeTiming(durations, outerDuration) {
208
+ const timing = getObjectArrayKeyframeTiming(durations);
209
+ if (!timing) return null;
210
+ if (timing.totalDuration === void 0 || outerDuration === void 0) return timing;
211
+ if (typeof outerDuration === "number" && Math.abs(outerDuration - timing.totalDuration) <= Number.EPSILON) {
212
+ return timing;
213
+ }
214
+ return null;
215
+ }
216
+ function findObjectArrayKeyframeIndex(durations, percentage, options) {
217
+ if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) return null;
218
+ const timing = getObjectArrayKeyframeTiming(durations);
219
+ if (!timing) return null;
220
+ const { percentages } = timing;
221
+ let match = null;
222
+ let bestDistance = Number.POSITIVE_INFINITY;
223
+ for (let index = 0; index < percentages.length; index++) {
224
+ const distance = Math.abs(percentages[index] - percentage);
225
+ if (distance < bestDistance) {
226
+ match = index;
227
+ bestDistance = distance;
228
+ }
229
+ }
230
+ const tolerance = options?.tolerance ?? OBJECT_ARRAY_PERCENTAGE_TOLERANCE;
231
+ return bestDistance <= tolerance || options?.fallbackToNearest ? match : null;
232
+ }
233
+
162
234
  // src/gsapParserAcorn.ts
163
235
  var GSAP_METHODS = /* @__PURE__ */ new Set(["set", "to", "from", "fromTo"]);
164
236
  var QUERY_METHODS = /* @__PURE__ */ new Set(["querySelector", "querySelectorAll"]);
@@ -729,6 +801,8 @@ function parsePercentageKeyframes(node, scope, source) {
729
801
  for (const [k, v] of Object.entries(record)) {
730
802
  if (k === "ease" && typeof v === "string") {
731
803
  kfEase = v;
804
+ } else if (k === "duration") {
805
+ continue;
732
806
  } else if (typeof v === "number" || typeof v === "string") {
733
807
  properties[k] = v;
734
808
  }
@@ -753,13 +827,13 @@ function computeKeyframesTotalDuration(varsNode, scope, source) {
753
827
  (p) => (p.key?.name ?? p.key?.value) === "keyframes"
754
828
  )?.value;
755
829
  if (!kfNode || kfNode.type !== "ArrayExpression") return void 0;
756
- let total = 0;
830
+ const durations = [];
757
831
  for (const el of kfNode.elements ?? []) {
758
832
  if (!el || el.type !== "ObjectExpression") continue;
759
833
  const r = objectExpressionToRecord(el, scope, source);
760
- if (typeof r.duration === "number") total += r.duration;
834
+ durations.push(r.duration);
761
835
  }
762
- return total > 0 ? total : void 0;
836
+ return getObjectArrayKeyframeTiming(durations)?.totalDuration;
763
837
  }
764
838
  function parseObjectArrayKeyframes(node, scope, source) {
765
839
  const elements = node.elements ?? [];
@@ -771,7 +845,7 @@ function parseObjectArrayKeyframes(node, scope, source) {
771
845
  let duration;
772
846
  let ease;
773
847
  for (const [k, v] of Object.entries(record)) {
774
- if (k === "duration" && typeof v === "number") {
848
+ if (k === "duration") {
775
849
  duration = v;
776
850
  } else if (k === "ease" && typeof v === "string") {
777
851
  ease = v;
@@ -781,31 +855,13 @@ function parseObjectArrayKeyframes(node, scope, source) {
781
855
  }
782
856
  raw.push({ properties, duration, ease });
783
857
  }
784
- const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);
785
- const keyframes = [];
786
- if (totalDuration > 0) {
787
- let cumulative = 0;
788
- for (const entry of raw) {
789
- cumulative += entry.duration ?? 0;
790
- const percentage = Math.round(cumulative / totalDuration * 100);
791
- keyframes.push({
792
- percentage,
793
- properties: entry.properties,
794
- ...entry.ease ? { ease: entry.ease } : {}
795
- });
796
- }
797
- } else {
798
- for (let i = 0; i < raw.length; i++) {
799
- const entry = raw[i];
800
- if (!entry) continue;
801
- const percentage = raw.length > 1 ? Math.round(i / (raw.length - 1) * 100) : 0;
802
- keyframes.push({
803
- percentage,
804
- properties: entry.properties,
805
- ...entry.ease ? { ease: entry.ease } : {}
806
- });
807
- }
808
- }
858
+ const timing = getObjectArrayKeyframeTiming(raw.map((entry) => entry.duration));
859
+ if (!timing) return void 0;
860
+ const keyframes = raw.map((entry, index) => ({
861
+ percentage: timing.percentages[index],
862
+ properties: entry.properties,
863
+ ...entry.ease ? { ease: entry.ease } : {}
864
+ }));
809
865
  return { format: "object-array", keyframes };
810
866
  }
811
867
  function parseSimpleArrayKeyframes(node, scope) {
@@ -1698,43 +1754,85 @@ function findKfPropByPct(kfNode, percentage) {
1698
1754
  }
1699
1755
  return best;
1700
1756
  }
1757
+ function updateMotionPathPosition(script, target, percentage, properties) {
1758
+ const waypoints = extractArcWaypoints(target.animation);
1759
+ if (waypoints.length < 2) return void 0;
1760
+ const pointIndex = Math.max(
1761
+ 0,
1762
+ Math.min(waypoints.length - 1, Math.round(percentage / 100 * (waypoints.length - 1)))
1763
+ );
1764
+ const current = waypoints[pointIndex];
1765
+ if (!current) return void 0;
1766
+ const x = properties.x ?? current.x;
1767
+ const y = properties.y ?? current.y;
1768
+ if (typeof x !== "number" || typeof y !== "number") return void 0;
1769
+ return updateMotionPathPointInScript(script, target.id, pointIndex, { x, y });
1770
+ }
1771
+ function updateTweenEase(script, animationId, ease) {
1772
+ const reparsed = parseGsapScriptAcornForWrite(script);
1773
+ const target = reparsed?.located.find((entry) => entry.id === animationId);
1774
+ if (!target) return void 0;
1775
+ const ms = new MagicString(script);
1776
+ upsertProp(ms, target.call.varsArg, "ease", ease);
1777
+ return ms.toString();
1778
+ }
1779
+ function updateMotionPathKeyframe(script, target, percentage, properties, ease) {
1780
+ if (!target.animation.arcPath?.enabled || !findPropertyNode2(target.call.varsArg, "motionPath")) {
1781
+ return void 0;
1782
+ }
1783
+ const propertyKeys = Object.keys(properties);
1784
+ if (propertyKeys.some((key) => key !== "x" && key !== "y")) return void 0;
1785
+ const next = propertyKeys.length === 0 ? script : updateMotionPathPosition(script, target, percentage, properties);
1786
+ if (next === void 0 || ease === void 0) return next;
1787
+ return updateTweenEase(next, target.id, ease);
1788
+ }
1789
+ function updateObjectKeyframe(script, prop, properties, ease) {
1790
+ const ms = new MagicString(script);
1791
+ if (prop.value?.type === "ObjectExpression") {
1792
+ for (const [key, value] of Object.entries(properties)) {
1793
+ upsertProp(ms, prop.value, key, value);
1794
+ }
1795
+ if (ease !== void 0) upsertProp(ms, prop.value, "ease", ease);
1796
+ } else {
1797
+ const record = { ...properties };
1798
+ if (ease) record.ease = ease;
1799
+ ms.overwrite(prop.value.start, prop.value.end, recordToCode(record));
1800
+ }
1801
+ return ms.toString();
1802
+ }
1701
1803
  function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
1702
1804
  const parsed = parseGsapScriptAcornForWrite(script);
1703
1805
  if (!parsed) return script;
1704
1806
  const target = parsed.located.find((l) => l.id === animationId);
1705
1807
  if (!target) return script;
1706
1808
  const kfPropNode = findPropertyNode2(target.call.varsArg, "keyframes");
1707
- if (!kfPropNode) return script;
1809
+ if (!kfPropNode) {
1810
+ return updateMotionPathKeyframe(script, target, percentage, properties, ease) ?? script;
1811
+ }
1708
1812
  if (kfPropNode.value?.type === "ArrayExpression") {
1709
1813
  return updateArrayKeyframeByPct(script, kfPropNode.value, percentage, properties, ease);
1710
1814
  }
1711
1815
  if (kfPropNode.value?.type !== "ObjectExpression") return script;
1712
1816
  const match = findKfPropByPct(kfPropNode.value, percentage);
1713
1817
  if (!match) return script;
1714
- const ms = new MagicString(script);
1715
- if (match.prop.value?.type === "ObjectExpression") {
1716
- for (const [k, v] of Object.entries(properties)) {
1717
- upsertProp(ms, match.prop.value, k, v);
1718
- }
1719
- if (ease !== void 0) upsertProp(ms, match.prop.value, "ease", ease);
1720
- } else {
1721
- const record = { ...properties };
1722
- if (ease) record.ease = ease;
1723
- ms.overwrite(match.prop.value.start, match.prop.value.end, recordToCode(record));
1724
- }
1725
- return ms.toString();
1818
+ return updateObjectKeyframe(script, match.prop, properties, ease);
1726
1819
  }
1727
1820
  function updateArrayKeyframeByPct(script, arrayNode, percentage, properties, ease) {
1728
1821
  const elements = (arrayNode.elements ?? []).filter(
1729
1822
  (el2) => !!el2 && el2.type === "ObjectExpression"
1730
1823
  );
1731
- const n = elements.length;
1732
- if (n === 0) return script;
1733
- const idx = n > 1 ? Math.round(percentage / 100 * (n - 1)) : 0;
1734
- const el = elements[Math.max(0, Math.min(n - 1, idx))];
1824
+ if (elements.length === 0) return script;
1825
+ const records = elements.map((element) => valueNodeToRecord(element, script));
1826
+ const idx = findObjectArrayKeyframeIndex(
1827
+ records.map((record) => record.duration),
1828
+ percentage,
1829
+ { fallbackToNearest: true }
1830
+ );
1831
+ if (idx === null) return script;
1832
+ const el = elements[idx];
1735
1833
  if (!el) return script;
1736
1834
  const merged = {
1737
- ...valueNodeToRecord(el, script),
1835
+ ...records[idx],
1738
1836
  ...properties
1739
1837
  };
1740
1838
  if (ease) merged.ease = ease;
@@ -1785,14 +1883,23 @@ function convertArrayKeyframesToObject(script, target) {
1785
1883
  const els = (kfPropNode.value.elements ?? []).filter(
1786
1884
  (el) => !!el && el.type === "ObjectExpression"
1787
1885
  );
1788
- const n = els.length;
1789
- if (n === 0) return script;
1886
+ if (els.length === 0) return script;
1887
+ const records = els.map((element) => valueNodeToRecord(element, script));
1888
+ const outerDuration = valueNodeToRecord(target.call.varsArg, script).duration;
1889
+ const timing = getCompatibleObjectArrayKeyframeTiming(
1890
+ records.map((record) => record.duration),
1891
+ outerDuration
1892
+ );
1893
+ if (!timing) return script;
1790
1894
  const entries = els.map((el, i) => {
1791
- const pct = n > 1 ? Math.round(i / (n - 1) * 1e3) / 10 : 0;
1792
- return `${JSON.stringify(`${pct}%`)}: ${script.slice(el.start, el.end)}`;
1895
+ const { duration: _duration, ...record } = records[i];
1896
+ return `${JSON.stringify(`${timing.percentages[i]}%`)}: ${recordToCode(record)}`;
1793
1897
  });
1794
1898
  const ms = new MagicString(script);
1795
1899
  ms.overwrite(kfPropNode.value.start, kfPropNode.value.end, `{ ${entries.join(", ")} }`);
1900
+ if (timing.totalDuration !== void 0 && findPropertyNode2(target.call.varsArg, "duration") === void 0) {
1901
+ upsertProp(ms, target.call.varsArg, "duration", timing.totalDuration);
1902
+ }
1796
1903
  return ms.toString();
1797
1904
  }
1798
1905
  function ensureKeyframesNode(script, animationId) {
@@ -1877,25 +1984,17 @@ function collapseKeyframesToFlat(ms, varsNode, source, remainingRecord) {
1877
1984
  }
1878
1985
  ms.overwrite(varsNode.start, varsNode.end, `{ ${entries.join(", ")} }`);
1879
1986
  }
1880
- function arrayKeyframePct(i, n) {
1881
- return n > 1 ? i / (n - 1) * 100 : 0;
1882
- }
1883
1987
  function removeArrayKeyframe(ms, varsArg, arrNode, script, percentage) {
1884
1988
  const elements = (arrNode.elements ?? []).filter(
1885
1989
  (e) => !!e && e.type === "ObjectExpression"
1886
1990
  );
1887
- const n = elements.length;
1888
- if (n === 0) return false;
1889
- let matchIdx = -1;
1890
- let bestDist = Number.POSITIVE_INFINITY;
1891
- for (let i = 0; i < n; i++) {
1892
- const dist = Math.abs(arrayKeyframePct(i, n) - percentage);
1893
- if (dist <= PCT_TOLERANCE && dist < bestDist) {
1894
- matchIdx = i;
1895
- bestDist = dist;
1896
- }
1897
- }
1898
- if (matchIdx === -1) return false;
1991
+ if (elements.length === 0) return false;
1992
+ const records = elements.map((element) => valueNodeToRecord(element, script));
1993
+ const matchIdx = findObjectArrayKeyframeIndex(
1994
+ records.map((record) => record.duration),
1995
+ percentage
1996
+ );
1997
+ if (matchIdx === null) return false;
1899
1998
  const remaining = elements.filter((_, i) => i !== matchIdx);
1900
1999
  if (remaining.length < 2) {
1901
2000
  const sole = remaining[0];
@@ -1941,11 +2040,10 @@ function moveKeyframeInScript(script, animationId, fromPercentage, toPercentage)
1941
2040
  if (!match) return src;
1942
2041
  if (Math.abs(fromPercentage - toPercentage) < MOVE_NOOP_EPSILON_PCT) return src;
1943
2042
  const dest = findKfPropByPct(kfNode, toPercentage);
1944
- const collision = dest && dest.prop !== match.prop ? dest : null;
2043
+ if (dest && dest.prop !== match.prop) return script;
1945
2044
  const entries = [];
1946
2045
  for (const prop of percentagePropsOf(kfNode)) {
1947
2046
  if (prop === match.prop) continue;
1948
- if (collision && prop === collision.prop) continue;
1949
2047
  const pct = percentageFromKey(propKeyName2(prop) ?? "");
1950
2048
  if (Number.isNaN(pct)) continue;
1951
2049
  entries.push({ pct, record: valueNodeToRecord(prop.value, src) });
@@ -2085,7 +2183,7 @@ function convertToKeyframesFromScript(script, animationId, resolvedFromValues, s
2085
2183
  return ms.toString();
2086
2184
  }
2087
2185
  function buildKeyframeObjectCode(keyframes, easeEach) {
2088
- const entries = keyframes.map((kf) => {
2186
+ const entries = mergePercentageKeyframes(keyframes).map((kf) => {
2089
2187
  const props = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
2090
2188
  if (kf.ease) props.push(`ease: ${JSON.stringify(kf.ease)}`);
2091
2189
  if (kf.auto) props.push(`_auto: 1`);