@hyperframes/parsers 0.7.24 → 0.7.26

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/index.d.ts CHANGED
@@ -46,6 +46,66 @@ interface CompositionMetadata {
46
46
  declare function extractCompositionMetadata(html: string): CompositionMetadata;
47
47
  declare function validateCompositionHtml(html: string): ValidationResult;
48
48
 
49
+ /**
50
+ * Shared "is this `outputResolution` preset compatible with the composition?"
51
+ * check.
52
+ *
53
+ * The `--resolution` render flag (a `CanvasResolution` preset) is chosen
54
+ * independently of the composition it renders, so a portrait composition
55
+ * (1080×1920) rendered with `--resolution landscape` (1920×1080) is a common
56
+ * mistake — especially from AI agents that pick a preset by habit rather than
57
+ * by inspecting the composition. Historically this surfaced as a cryptic
58
+ * `Error` thrown deep inside the render compiler (`resolveDeviceScaleFactor`
59
+ * in `@hyperframes/producer`), after the browser and ffmpeg had already spun
60
+ * up, with a message that named the mismatch but not the fix.
61
+ *
62
+ * This module gives every consumer (the render pre-flight in the CLI, the
63
+ * producer's `resolveDeviceScaleFactor`, and any future lint rule) a single,
64
+ * dependency-free definition of "is this preset usable for this composition"
65
+ * — including a suggested preset when there's an unambiguously-correct swap —
66
+ * so the same check can run *before* a render is attempted (loud, actionable,
67
+ * cheap) and again as defense-in-depth inside the pipeline.
68
+ *
69
+ * It lives in `@hyperframes/parsers` (rather than `@hyperframes/core`) because
70
+ * both `core`/`producer` and `lint` may need it, and `lint` cannot depend on
71
+ * `core`. The geometry it needs (`CANVAS_DIMENSIONS`) already lives here.
72
+ */
73
+
74
+ type OutputResolutionIssueKind = "hdr-incompatible" | "alpha-incompatible" | "aspect-mismatch" | "downsampling" | "non-integer-scale";
75
+ interface OutputResolutionCompatibility {
76
+ ok: boolean;
77
+ /** Present when `ok` is false. */
78
+ kind?: OutputResolutionIssueKind;
79
+ /** Human-readable, actionable message suitable for direct display. */
80
+ message?: string;
81
+ /**
82
+ * A preset whose orientation/aspect ratio matches the composition, when one
83
+ * exists and is an unambiguous swap for the user's intent. Present only for
84
+ * `aspect-mismatch`. Consumers may surface this as a suggestion ("did you
85
+ * mean `--resolution portrait`?"); it is intentionally *not* auto-applied —
86
+ * silently swapping a user-supplied flag changes their stated intent.
87
+ */
88
+ suggestedResolution?: CanvasResolution;
89
+ }
90
+ /**
91
+ * Check whether rendering a composition of the given dimensions with the given
92
+ * `outputResolution` preset (and alpha/HDR modes) is supported.
93
+ *
94
+ * Pure and dependency-free — the single source of truth for the constraints
95
+ * `resolveDeviceScaleFactor` enforces, so the CLI can run the exact same check
96
+ * as a pre-flight before any browser/ffmpeg work.
97
+ *
98
+ * @param outputResolution The chosen preset, or `undefined` when the render
99
+ * uses the composition's native dimensions (always compatible).
100
+ */
101
+ declare function checkOutputResolutionCompatibility(input: {
102
+ compositionWidth: number;
103
+ compositionHeight: number;
104
+ outputResolution: CanvasResolution | undefined;
105
+ alphaRequested?: boolean;
106
+ hdrRequested?: boolean;
107
+ }): OutputResolutionCompatibility;
108
+
49
109
  /**
50
110
  * Rewrite `script` so top-level helper calls / loops that build the timeline
51
111
  * become explicit literal tweens. Returns the original script unchanged when
@@ -55,4 +115,4 @@ declare function unrollComputedTimeline(script: string): string;
55
115
 
56
116
  declare function queryByAttr(root: ParentNode, attr: string, value: string, tag?: string): Element | null;
57
117
 
58
- export { CanvasResolution, CompositionHtmlParseError, type CompositionMetadata, CompositionVariable, Keyframe, type ParsedHtml, StageZoomKeyframe, TimelineElement, ValidationResult, addElementToHtml, extractCompositionMetadata, parseHtml, queryByAttr, removeElementFromHtml, unrollComputedTimeline, updateElementInHtml, validateCompositionHtml };
118
+ export { CanvasResolution, CompositionHtmlParseError, type CompositionMetadata, CompositionVariable, Keyframe, type OutputResolutionCompatibility, type OutputResolutionIssueKind, type ParsedHtml, StageZoomKeyframe, TimelineElement, ValidationResult, addElementToHtml, checkOutputResolutionCompatibility, extractCompositionMetadata, parseHtml, queryByAttr, removeElementFromHtml, unrollComputedTimeline, updateElementInHtml, validateCompositionHtml };
package/dist/index.js CHANGED
@@ -850,6 +850,49 @@ var SCOPE_NODE_TYPES = /* @__PURE__ */ new Set([
850
850
  "FunctionExpression",
851
851
  "ArrowFunctionExpression"
852
852
  ]);
853
+ var CONST_NODES = /* @__PURE__ */ Symbol("hf.constNodes");
854
+ function constNodesOf(scope) {
855
+ return scope[CONST_NODES];
856
+ }
857
+ var MATH_FNS = /* @__PURE__ */ new Set(["min", "max", "round", "floor", "ceil", "abs", "sqrt", "sign", "trunc"]);
858
+ var MATH_CONSTS = { PI: Math.PI, E: Math.E, SQRT2: Math.SQRT2 };
859
+ function resolveMemberNode(node, scope) {
860
+ if (node.object?.type === "Identifier" && node.object.name === "Math") {
861
+ const key = node.property?.name;
862
+ return typeof key === "string" ? MATH_CONSTS[key] : void 0;
863
+ }
864
+ const objNode = resolveConstNode(node.object, scope);
865
+ if (!objNode) return void 0;
866
+ let valueNode;
867
+ if (node.computed) {
868
+ const idx = resolveNode(node.property, scope);
869
+ if (objNode.type === "ArrayExpression" && typeof idx === "number") {
870
+ valueNode = objNode.elements?.[idx];
871
+ } else if (objNode.type === "ObjectExpression" && (typeof idx === "string" || typeof idx === "number")) {
872
+ valueNode = findPropertyNode(objNode, String(idx));
873
+ }
874
+ } else if (objNode.type === "ObjectExpression") {
875
+ valueNode = findPropertyNode(objNode, node.property?.name ?? node.property?.value);
876
+ }
877
+ return valueNode ? resolveNode(valueNode, scope) : void 0;
878
+ }
879
+ function resolveConstMember(objNode, node, scope) {
880
+ if (!node.computed) {
881
+ return objNode.type === "ObjectExpression" ? findPropertyNode(objNode, node.property?.name ?? node.property?.value) : void 0;
882
+ }
883
+ const idx = resolveNode(node.property, scope);
884
+ if (objNode.type === "ArrayExpression" && typeof idx === "number") return objNode.elements?.[idx];
885
+ if (objNode.type === "ObjectExpression") return findPropertyNode(objNode, String(idx));
886
+ return void 0;
887
+ }
888
+ function resolveConstNode(node, scope) {
889
+ if (!node) return void 0;
890
+ if (node.type === "ArrayExpression" || node.type === "ObjectExpression") return node;
891
+ if (node.type === "Identifier") return constNodesOf(scope)?.get(node.name);
892
+ if (node.type !== "MemberExpression") return void 0;
893
+ const objNode = resolveConstNode(node.object, scope);
894
+ return objNode ? resolveConstMember(objNode, node, scope) : void 0;
895
+ }
853
896
  function resolveNode(node, scope) {
854
897
  if (!node) return void 0;
855
898
  if (node.type === "NumericLiteral" || node.type === "Literal" && typeof node.value === "number")
@@ -886,6 +929,15 @@ function resolveNode(node, scope) {
886
929
  if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
887
930
  return node.quasis?.[0]?.value?.cooked ?? void 0;
888
931
  }
932
+ if (node.type === "MemberExpression") {
933
+ return resolveMemberNode(node, scope);
934
+ }
935
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "Math" && MATH_FNS.has(node.callee.property?.name)) {
936
+ const args = (node.arguments ?? []).map((a) => resolveNode(a, scope));
937
+ if (args.every((a) => typeof a === "number")) {
938
+ return Math[node.callee.property.name](...args);
939
+ }
940
+ }
889
941
  return void 0;
890
942
  }
891
943
  function extractLiteralValue(node, scope) {
@@ -943,14 +995,19 @@ function resolveCollectionSelector(node, ancestors, scope, bindings) {
943
995
  }
944
996
  function collectScopeBindings(ast) {
945
997
  const bindings = /* @__PURE__ */ new Map();
998
+ const constNodes = /* @__PURE__ */ new Map();
999
+ Object.defineProperty(bindings, CONST_NODES, { value: constNodes, enumerable: false });
946
1000
  acornWalk.simple(ast, {
947
1001
  VariableDeclarator(node) {
948
1002
  const name = node.id?.name;
949
1003
  const init = node.init;
950
- if (name && init) {
951
- const val = resolveNode(init, bindings);
952
- if (val !== void 0) bindings.set(name, val);
1004
+ if (!name || !init) return;
1005
+ if (init.type === "ArrayExpression" || init.type === "ObjectExpression") {
1006
+ constNodes.set(name, init);
1007
+ return;
953
1008
  }
1009
+ const val = resolveNode(init, bindings);
1010
+ if (val !== void 0) bindings.set(name, val);
954
1011
  }
955
1012
  });
956
1013
  return bindings;
@@ -992,6 +1049,25 @@ function collectTargetBindings(ast, scope) {
992
1049
  }
993
1050
  }
994
1051
  });
1052
+ const COLLECTION_ALIAS_METHODS = /* @__PURE__ */ new Set(["slice", "filter", "concat", "reverse"]);
1053
+ acornWalk.ancestor(ast, {
1054
+ // fallow-ignore-next-line complexity
1055
+ VariableDeclarator(node, _, ancestors) {
1056
+ const name = node.id?.name;
1057
+ const init = node.init;
1058
+ if (!name || !init) return;
1059
+ let sourceVar;
1060
+ if (init.type === "MemberExpression" && init.object?.type === "Identifier") {
1061
+ sourceVar = init.object.name;
1062
+ } else if (init.type === "CallExpression" && init.callee?.type === "MemberExpression" && init.callee.object?.type === "Identifier" && init.callee.property?.type === "Identifier" && COLLECTION_ALIAS_METHODS.has(init.callee.property.name)) {
1063
+ sourceVar = init.callee.object.name;
1064
+ }
1065
+ if (!sourceVar) return;
1066
+ const selector = lookupBindingFromAncestors(sourceVar, ancestors, bindings);
1067
+ if (selector)
1068
+ addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), name, selector);
1069
+ }
1070
+ });
995
1071
  return bindings;
996
1072
  }
997
1073
  function resolveTargetSelector(node, ancestors, scope, bindings) {
@@ -1014,6 +1090,32 @@ function resolveTargetSelector(node, ancestors, scope, bindings) {
1014
1090
  }
1015
1091
  return null;
1016
1092
  }
1093
+ function describeProxyTarget(targetNode, varsNode, scope) {
1094
+ const objNode = targetNode?.type === "ObjectExpression" ? targetNode : targetNode?.type === "Identifier" ? resolveConstNode(targetNode, scope) : void 0;
1095
+ if (objNode?.type !== "ObjectExpression") return null;
1096
+ const onUpdate = findPropertyNode(varsNode, "onUpdate");
1097
+ const driven = onUpdate ? drivenDomChannel(onUpdate) : void 0;
1098
+ if (driven) return `proxy \u2192 ${driven}`;
1099
+ return "dwell/hold";
1100
+ }
1101
+ function isStyleAssignmentTarget(left) {
1102
+ return left?.type === "MemberExpression" && left.object?.type === "MemberExpression" && left.object.property?.name === "style" && !!left.property?.name;
1103
+ }
1104
+ function drivenDomChannel(fnNode) {
1105
+ let found;
1106
+ acornWalk.simple(fnNode, {
1107
+ CallExpression(node) {
1108
+ if (node.callee?.type === "MemberExpression" && node.callee.property?.name === "setAttribute" && typeof node.arguments?.[0]?.value === "string") {
1109
+ found ??= node.arguments[0].value;
1110
+ }
1111
+ },
1112
+ AssignmentExpression(node) {
1113
+ const left = node.left;
1114
+ if (isStyleAssignmentTarget(left)) found ??= `style.${left.property.name}`;
1115
+ }
1116
+ });
1117
+ return found;
1118
+ }
1017
1119
  function isObjectProperty(prop) {
1018
1120
  return prop?.type === "ObjectProperty" || prop?.type === "Property";
1019
1121
  }
@@ -1482,8 +1584,13 @@ function tweenCallToAnimation(call, scope, source) {
1482
1584
  if (duration === void 0 && keyframesData) {
1483
1585
  duration = computeKeyframesTotalDuration(call.varsArg, scope, source);
1484
1586
  }
1587
+ let selector = call.selector;
1588
+ if (selector === "__unresolved__") {
1589
+ const proxyLabel = describeProxyTarget(call.node.arguments?.[0], call.varsArg, scope);
1590
+ if (proxyLabel) selector = proxyLabel;
1591
+ }
1485
1592
  const anim = {
1486
- targetSelector: call.selector,
1593
+ targetSelector: selector,
1487
1594
  method: call.method,
1488
1595
  position,
1489
1596
  properties,
@@ -1506,11 +1613,52 @@ function tweenCallToAnimation(call, scope, source) {
1506
1613
  if (keyframesData) anim.keyframes = keyframesData;
1507
1614
  if (motionPathResult) anim.arcPath = motionPathResult.arcPath;
1508
1615
  if (hasUnresolvedKeyframes) anim.hasUnresolvedKeyframes = true;
1509
- if (call.selector === "__unresolved__") anim.hasUnresolvedSelector = true;
1616
+ if (selector === "__unresolved__") anim.hasUnresolvedSelector = true;
1510
1617
  const provenance = readProvenance(call.node);
1511
1618
  if (provenance) anim.provenance = provenance;
1512
1619
  return anim;
1513
1620
  }
1621
+ function staggerAmount(raw) {
1622
+ if (typeof raw === "number") return raw;
1623
+ if (typeof raw !== "string") return void 0;
1624
+ const src = raw.startsWith("__raw:") ? raw.slice(6) : raw;
1625
+ const m = /(?:each\s*:\s*)?(-?\d+(?:\.\d+)?)/.exec(src);
1626
+ if (!m) return void 0;
1627
+ const n = Number.parseFloat(m[1]);
1628
+ return Number.isFinite(n) ? n : void 0;
1629
+ }
1630
+ function restValue(prop) {
1631
+ return prop === "opacity" || prop.startsWith("scale") ? 1 : 0;
1632
+ }
1633
+ function staggeredKeyframes(anim, each) {
1634
+ const vars = { ...anim.properties };
1635
+ let from;
1636
+ let to;
1637
+ if (anim.method === "fromTo") {
1638
+ from = { ...anim.fromProperties ?? {} };
1639
+ to = vars;
1640
+ } else if (anim.method === "from") {
1641
+ from = vars;
1642
+ to = {};
1643
+ for (const k of Object.keys(vars)) to[k] = restValue(k);
1644
+ } else {
1645
+ from = { ...anim.fromProperties ?? {} };
1646
+ for (const k of Object.keys(vars)) if (from[k] === void 0) from[k] = restValue(k);
1647
+ to = vars;
1648
+ }
1649
+ return [
1650
+ { percentage: 0, properties: { ...from, stagger: each } },
1651
+ { percentage: 100, properties: { ...to, stagger: each } }
1652
+ ];
1653
+ }
1654
+ function annotateStaggeredCollections(anims) {
1655
+ for (const anim of anims) {
1656
+ if (anim.keyframes || anim.arcPath) continue;
1657
+ const each = staggerAmount(anim.extras?.stagger);
1658
+ if (each === void 0) continue;
1659
+ anim.keyframes = { format: "percentage", keyframes: staggeredKeyframes(anim, each) };
1660
+ }
1661
+ }
1514
1662
  var GSAP_DEFAULT_DURATION = 0.5;
1515
1663
  function resolvePositionString(pos, cursor, prevStart) {
1516
1664
  const trimmed = pos.trim();
@@ -1536,6 +1684,55 @@ function resolvePositionString(pos, cursor, prevStart) {
1536
1684
  const n = Number.parseFloat(trimmed);
1537
1685
  return Number.isFinite(n) ? n : null;
1538
1686
  }
1687
+ function collectGsapSetStates(ast, scope, bindings, source) {
1688
+ const states = /* @__PURE__ */ new Map();
1689
+ acornWalk.ancestor(ast, {
1690
+ // fallow-ignore-next-line complexity
1691
+ CallExpression(node, _, ancestors) {
1692
+ const callee = node.callee;
1693
+ if (callee?.type !== "MemberExpression" || callee.object?.name !== "gsap" || callee.property?.name !== "set")
1694
+ return;
1695
+ const selector = resolveTargetSelector(node.arguments?.[0], ancestors, scope, bindings);
1696
+ if (!selector) return;
1697
+ const rec = objectExpressionToRecord(node.arguments?.[1], scope, source);
1698
+ const props = states.get(selector) ?? {};
1699
+ for (const [k, v] of Object.entries(rec)) {
1700
+ if (typeof v === "number" || typeof v === "string") props[k] = v;
1701
+ }
1702
+ states.set(selector, props);
1703
+ }
1704
+ });
1705
+ return states;
1706
+ }
1707
+ function mergeProps(target, props) {
1708
+ for (const [k, v] of Object.entries(props)) target[k] = v;
1709
+ return target;
1710
+ }
1711
+ function seedFromPreState(anim, cur) {
1712
+ const from = { ...anim.fromProperties ?? {} };
1713
+ let seeded = false;
1714
+ for (const prop of Object.keys(anim.properties)) {
1715
+ if (from[prop] === void 0 && cur[prop] !== void 0) {
1716
+ from[prop] = cur[prop];
1717
+ seeded = true;
1718
+ }
1719
+ }
1720
+ if (seeded) anim.fromProperties = from;
1721
+ }
1722
+ function seedSetStates(anims, initial) {
1723
+ const state = /* @__PURE__ */ new Map();
1724
+ for (const [sel, props] of initial) state.set(sel, { ...props });
1725
+ for (const anim of anims) {
1726
+ const sel = anim.targetSelector;
1727
+ if (anim.method === "set") {
1728
+ state.set(sel, mergeProps(state.get(sel) ?? {}, anim.properties));
1729
+ continue;
1730
+ }
1731
+ const cur = state.get(sel);
1732
+ if (anim.method === "to" && cur) seedFromPreState(anim, cur);
1733
+ state.set(sel, mergeProps(state.get(sel) ?? {}, anim.properties));
1734
+ }
1735
+ }
1539
1736
  function applyTimelineDefaults(anims, defaults) {
1540
1737
  if (!defaults) return;
1541
1738
  for (const anim of anims) {
@@ -1548,31 +1745,89 @@ function applyTimelineDefaults(anims, defaults) {
1548
1745
  }
1549
1746
  }
1550
1747
  }
1551
- function resolveTimelinePositions(anims) {
1748
+ function resolveLabelPosition(pos, labels, cursor) {
1749
+ const m = /^([A-Za-z_$][\w$]*)\s*(?:([+-])=\s*([\d.]+))?$/.exec(pos.trim());
1750
+ if (!m) return null;
1751
+ const name = m[1];
1752
+ let base = labels.get(name);
1753
+ if (base === void 0) {
1754
+ base = cursor;
1755
+ labels.set(name, base);
1756
+ }
1757
+ if (m[2] && m[3]) {
1758
+ const n = Number.parseFloat(m[3]);
1759
+ if (Number.isFinite(n)) return m[2] === "+" ? base + n : base - n;
1760
+ }
1761
+ return base;
1762
+ }
1763
+ function resolveAnimStart(anim, cursor, prevStart, labels) {
1764
+ if (anim.implicitPosition) return cursor;
1765
+ if (typeof anim.position === "number") return anim.position;
1766
+ if (typeof anim.position === "string") {
1767
+ return resolveLabelPosition(anim.position, labels, cursor) ?? resolvePositionString(anim.position, cursor, prevStart);
1768
+ }
1769
+ return cursor;
1770
+ }
1771
+ function resolveTimelinePositions(anims, labelDefs = []) {
1552
1772
  let cursor = 0;
1553
1773
  let prevStart = 0;
1554
- for (const anim of anims) {
1774
+ const labels = /* @__PURE__ */ new Map();
1775
+ let labelIdx = 0;
1776
+ const sortedLabels = [...labelDefs].sort((a, b) => a.order - b.order);
1777
+ const defineLabel = (def) => {
1778
+ let value;
1779
+ if (typeof def.position === "number") value = def.position;
1780
+ else if (typeof def.position === "string") {
1781
+ value = resolveLabelPosition(def.position, labels, cursor) ?? cursor;
1782
+ } else value = cursor;
1783
+ labels.set(def.name, Math.max(0, value));
1784
+ };
1785
+ anims.forEach((anim, i) => {
1786
+ while (labelIdx < sortedLabels.length && sortedLabels[labelIdx].order <= i) {
1787
+ defineLabel(sortedLabels[labelIdx]);
1788
+ labelIdx++;
1789
+ }
1555
1790
  if (anim.method === "set" && anim.global) {
1556
1791
  anim.resolvedStart = 0;
1557
- continue;
1792
+ return;
1558
1793
  }
1559
1794
  const duration = anim.method === "set" ? 0 : anim.duration ?? GSAP_DEFAULT_DURATION;
1560
- let start;
1561
- if (anim.implicitPosition) {
1562
- start = cursor;
1563
- } else if (typeof anim.position === "number") {
1564
- start = anim.position;
1565
- } else if (typeof anim.position === "string") {
1566
- start = resolvePositionString(anim.position, cursor, prevStart);
1567
- } else {
1568
- start = cursor;
1569
- }
1795
+ const start = resolveAnimStart(anim, cursor, prevStart, labels);
1570
1796
  if (start != null) {
1571
1797
  anim.resolvedStart = Math.max(0, start);
1572
1798
  prevStart = anim.resolvedStart;
1573
1799
  cursor = Math.max(cursor, anim.resolvedStart + duration);
1574
1800
  }
1575
- }
1801
+ });
1802
+ while (labelIdx < sortedLabels.length) defineLabel(sortedLabels[labelIdx++]);
1803
+ }
1804
+ function collectAddLabelDefs(ast, ref, scope, sortedCalls) {
1805
+ const callLocs = sortedCalls.map((c) => c.node.callee?.property?.loc?.start);
1806
+ const defs = [];
1807
+ acornWalk.simple(ast, {
1808
+ // fallow-ignore-next-line complexity
1809
+ CallExpression(node) {
1810
+ const callee = node.callee;
1811
+ const objMatches = ref.kind === "identifier" ? callee.object?.type === "Identifier" && callee.object.name === ref.name : sameMemberAccess(callee.object, ref.node);
1812
+ if (callee?.type !== "MemberExpression" || !objMatches || callee.property?.name !== "addLabel")
1813
+ return;
1814
+ const nameNode = node.arguments?.[0];
1815
+ const name = typeof nameNode?.value === "string" ? nameNode.value : void 0;
1816
+ if (!name) return;
1817
+ const posVal = resolveNode(node.arguments?.[1], scope);
1818
+ const position = typeof posVal === "number" || typeof posVal === "string" ? posVal : void 0;
1819
+ const labelLoc = callee.property?.loc?.start;
1820
+ let order = sortedCalls.length;
1821
+ if (labelLoc) {
1822
+ order = callLocs.findIndex(
1823
+ (l) => l && (l.line > labelLoc.line || l.line === labelLoc.line && l.column > labelLoc.column)
1824
+ );
1825
+ if (order === -1) order = sortedCalls.length;
1826
+ }
1827
+ defs.push({ name, position, order });
1828
+ }
1829
+ });
1830
+ return defs;
1576
1831
  }
1577
1832
  function compareByLoc(a, b) {
1578
1833
  const aLoc = a.node.callee?.property?.loc?.start;
@@ -1653,7 +1908,10 @@ function parseGsapScriptAcorn(script) {
1653
1908
  sortBySourcePosition(calls);
1654
1909
  const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));
1655
1910
  applyTimelineDefaults(rawAnims, detection.defaults);
1656
- resolveTimelinePositions(rawAnims);
1911
+ seedSetStates(rawAnims, collectGsapSetStates(ast, scope, targetBindings, script));
1912
+ const labelDefs = collectAddLabelDefs(ast, ref, scope, calls);
1913
+ resolveTimelinePositions(rawAnims, labelDefs);
1914
+ annotateStaggeredCollections(rawAnims);
1657
1915
  const animations = assignStableIds(rawAnims);
1658
1916
  const declPattern = ref.kind === "identifier" ? `(?:const|let|var)\\s+${timelineVar}\\s*=\\s*gsap\\.timeline\\s*\\([^)]*\\)\\s*;?` : `${escapeRegExp(timelineVar)}\\s*=\\s*gsap\\.timeline\\s*\\([^)]*\\)\\s*;?`;
1659
1917
  const timelineMatch = script.match(new RegExp(`^[\\s\\S]*?${declPattern}`));
@@ -2491,6 +2749,76 @@ function checkSubCompositionUsability(html, parseHtml2) {
2491
2749
  return { ok: true };
2492
2750
  }
2493
2751
 
2752
+ // src/outputResolutionCompatibility.ts
2753
+ var OK = { ok: true };
2754
+ function suggestMatchingPreset(compositionWidth, compositionHeight, chosen) {
2755
+ const aspectMatches = VALID_CANVAS_RESOLUTIONS.filter((preset) => {
2756
+ const { width, height } = CANVAS_DIMENSIONS[preset];
2757
+ return width * compositionHeight === height * compositionWidth;
2758
+ });
2759
+ if (aspectMatches.length === 0) return void 0;
2760
+ const chosenIs4k = chosen.endsWith("-4k");
2761
+ const sameTier = aspectMatches.find((preset) => preset.endsWith("-4k") === chosenIs4k);
2762
+ return sameTier ?? aspectMatches[0];
2763
+ }
2764
+ function describeOrientation(width, height) {
2765
+ if (width > height) return "landscape";
2766
+ if (width < height) return "portrait";
2767
+ return "square";
2768
+ }
2769
+ function buildAspectMismatch(compositionWidth, compositionHeight, outputResolution, target) {
2770
+ const suggestedResolution = suggestMatchingPreset(
2771
+ compositionWidth,
2772
+ compositionHeight,
2773
+ outputResolution
2774
+ );
2775
+ const suggestion = suggestedResolution ? ` The composition is ${describeOrientation(compositionWidth, compositionHeight)} \u2014 use --resolution ${suggestedResolution} instead.` : ` Pick a preset whose orientation matches, or omit --resolution to render at the composition's native dimensions.`;
2776
+ return {
2777
+ ok: false,
2778
+ kind: "aspect-mismatch",
2779
+ suggestedResolution,
2780
+ message: `outputResolution ${outputResolution} (${target.width}\xD7${target.height}) does not match the aspect ratio of the composition (${compositionWidth}\xD7${compositionHeight}).` + suggestion
2781
+ };
2782
+ }
2783
+ function checkOutputResolutionCompatibility(input) {
2784
+ const { compositionWidth, compositionHeight, outputResolution } = input;
2785
+ if (!outputResolution) return OK;
2786
+ if (input.hdrRequested) {
2787
+ return {
2788
+ ok: false,
2789
+ kind: "hdr-incompatible",
2790
+ message: `outputResolution cannot be combined with hdrMode='force-hdr'. HDR rendering composites at composition dimensions and does not yet support supersampling. Pick one or render in two passes.`
2791
+ };
2792
+ }
2793
+ if (input.alphaRequested) {
2794
+ return {
2795
+ ok: false,
2796
+ kind: "alpha-incompatible",
2797
+ message: `outputResolution cannot be combined with alpha output (--format webm|mov|png-sequence). The alpha screenshot path does not yet apply deviceScaleFactor and would silently produce composition-resolution frames. Render alpha at composition resolution and upscale separately, or use --format mp4.`
2798
+ };
2799
+ }
2800
+ const target = CANVAS_DIMENSIONS[outputResolution];
2801
+ if (target.width * compositionHeight !== target.height * compositionWidth) {
2802
+ return buildAspectMismatch(compositionWidth, compositionHeight, outputResolution, target);
2803
+ }
2804
+ const widthRatio = target.width / compositionWidth;
2805
+ if (widthRatio < 1) {
2806
+ return {
2807
+ ok: false,
2808
+ kind: "downsampling",
2809
+ message: `outputResolution ${outputResolution} (${target.width}\xD7${target.height}) is smaller than the composition (${compositionWidth}\xD7${compositionHeight}). Downsampling via --resolution is not supported.`
2810
+ };
2811
+ }
2812
+ if (!Number.isInteger(widthRatio)) {
2813
+ return {
2814
+ ok: false,
2815
+ kind: "non-integer-scale",
2816
+ message: `outputResolution ${outputResolution} requires a non-integer device scale factor (${widthRatio}\xD7) to upsample from ${compositionWidth}\xD7${compositionHeight}. Pick a preset that's an integer multiple, or rescale the composition.`
2817
+ };
2818
+ }
2819
+ return OK;
2820
+ }
2821
+
2494
2822
  // src/gsapUnroll.ts
2495
2823
  import * as acorn2 from "acorn";
2496
2824
  import MagicString2 from "magic-string";
@@ -2718,6 +3046,7 @@ export {
2718
3046
  TIMELINE_COLORS,
2719
3047
  VALID_CANVAS_RESOLUTIONS,
2720
3048
  addElementToHtml,
3049
+ checkOutputResolutionCompatibility,
2721
3050
  checkSubCompositionUsability,
2722
3051
  classifyPropertyGroup,
2723
3052
  classifyTweenPropertyGroup,