@hyperframes/studio 0.7.77 → 0.7.79
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/assets/{hyperframes-player-CEggaaxR.js → hyperframes-player-mFah2TZE.js} +1 -1
- package/dist/assets/{index-B1Tjjdse.js → index-Bqj3h_1a.js} +1 -1
- package/dist/assets/{index-Bp4jAYZG.js → index-DlZMDyYs.js} +194 -194
- package/dist/assets/index-gGVKuFg5.css +1 -0
- package/dist/assets/{index-B1INQNmj.js → index-hmnoiSEV.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +1432 -1181
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioLeftSidebar.tsx +10 -5
- package/src/components/StudioRightPanel.tsx +5 -4
- package/src/components/TimelineToolbar.tsx +3 -1
- package/src/components/editor/KeyframeNavigation.tsx +5 -0
- package/src/components/editor/MotionPathOverlay.tsx +4 -1
- package/src/components/editor/PropertyPanel.tsx +3 -7
- package/src/components/editor/keyframeRetime.test.ts +42 -0
- package/src/components/editor/keyframeRetime.ts +4 -1
- package/src/components/editor/motionPathSelection.test.ts +65 -0
- package/src/components/editor/motionPathSelection.ts +13 -3
- package/src/components/editor/propertyPanelColor.test.tsx +181 -12
- package/src/components/editor/propertyPanelColor.tsx +32 -26
- package/src/components/editor/propertyPanelPrimitives.tsx +3 -1
- package/src/components/nle/TimelineResizeDivider.tsx +9 -4
- package/src/components/pointerTargetSize.test.tsx +65 -0
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +41 -0
- package/src/components/sidebar/CompositionsTab.tsx +70 -33
- package/src/hooks/gsapDragCommit.ts +43 -10
- package/src/hooks/gsapDragStaticSetHelpers.ts +16 -6
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +63 -0
- package/src/hooks/gsapKeyframeCacheHelpers.ts +45 -38
- package/src/hooks/gsapKeyframeCommit.ts +11 -2
- package/src/hooks/gsapResizeIntercept.ts +2 -2
- package/src/hooks/gsapRuntimeBridge.ts +3 -2
- package/src/hooks/gsapScriptCommitHelpers.ts +17 -2
- package/src/hooks/gsapShared.test.ts +144 -0
- package/src/hooks/gsapShared.ts +264 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +289 -0
- package/src/hooks/keyframeCacheAstLoad.ts +5 -71
- package/src/hooks/newTweenTarget.test.ts +364 -0
- package/src/hooks/newTweenTargetHooks.test.tsx +255 -0
- package/src/hooks/timelineTrackVisibility.test.ts +61 -1
- package/src/hooks/timelineTrackVisibility.ts +11 -1
- package/src/hooks/useAnimatedPropertyCommit.ts +24 -8
- package/src/hooks/useEnableKeyframes.test.ts +44 -0
- package/src/hooks/useEnableKeyframes.ts +18 -5
- package/src/hooks/useGestureCommit.ts +21 -4
- package/src/hooks/useGsapScriptCommits.ts +3 -0
- package/src/hooks/useGsapTweenCache.test.ts +11 -3
- package/src/hooks/useGsapTweenCache.ts +4 -11
- package/src/player/components/LayerDisclosureRow.tsx +4 -1
- package/src/player/components/Player.test.ts +143 -12
- package/src/player/components/Player.tsx +84 -43
- package/src/player/components/ShortcutsPanel.test.tsx +158 -0
- package/src/player/components/ShortcutsPanel.tsx +11 -15
- package/src/player/components/Timeline.test.ts +3 -1
- package/src/player/components/TimelineCanvas.tsx +2 -1
- package/src/player/components/TimelineClipDiamonds.test.tsx +17 -2
- package/src/player/components/TimelineClipDiamonds.tsx +11 -1
- package/src/player/components/TimelineDiamondConnectors.tsx +14 -1
- package/src/player/components/TimelineLanes.test.tsx +281 -0
- package/src/player/components/TimelineLanes.tsx +43 -97
- package/src/player/components/TimelinePropertyLanes.test.tsx +217 -0
- package/src/player/components/TimelinePropertyLanes.tsx +80 -11
- package/src/player/components/TimelineTrackHeader.test.tsx +75 -5
- package/src/player/components/TimelineTrackHeader.tsx +52 -22
- package/src/player/components/timelineLaneProps.ts +86 -0
- package/src/player/components/timelineTrackDisplay.test.ts +35 -0
- package/src/player/components/timelineTrackDisplay.ts +38 -0
- package/src/player/components/useTimelineRangeSelection.ts +5 -0
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +127 -0
- package/src/player/components/useTimelineTrackLayout.test.ts +35 -0
- package/src/player/components/useTimelineTrackLayout.ts +4 -4
- package/src/player/hooks/useExpandedTimelineElements.test.ts +151 -2
- package/src/player/hooks/useExpandedTimelineElements.ts +46 -7
- package/src/telemetry/client.test.ts +12 -3
- package/src/telemetry/client.ts +3 -2
- package/dist/assets/index-CBAfprvx.css +0 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { parseGsapScript } from "@hyperframes/core/gsap-parser";
|
|
4
|
+
import { addAnimationWithKeyframesToScript } from "@hyperframes/parsers/gsap-writer-acorn";
|
|
5
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
6
|
+
import { buildStableSelector, getSelectorIndex } from "../components/editor/domEditingDom";
|
|
7
|
+
import { resolveSelectorElementIds, tweenTargetsElement, writeTargetSelector } from "./gsapShared";
|
|
8
|
+
import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit";
|
|
9
|
+
import { promoteSetToKeyframes } from "./useEnableKeyframes";
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.body.innerHTML = "";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A selection built the way production builds it (getDomLayerPatchTarget), so
|
|
17
|
+
* the class-only case under test is the real one: buildStableSelector hands back
|
|
18
|
+
* a BARE class for an element with no id / hf-id / composition id.
|
|
19
|
+
*/
|
|
20
|
+
function selectionFor(el: HTMLElement): DomEditSelection {
|
|
21
|
+
const selector = buildStableSelector(el);
|
|
22
|
+
return {
|
|
23
|
+
element: el,
|
|
24
|
+
id: el.id || undefined,
|
|
25
|
+
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
26
|
+
selector,
|
|
27
|
+
selectorIndex: getSelectorIndex(document, el, selector, "index.html", null),
|
|
28
|
+
sourceFile: "index.html",
|
|
29
|
+
dataAttributes: { start: "0", duration: "2" },
|
|
30
|
+
} as unknown as DomEditSelection;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Five class-only siblings — the shape that made one add collapse the timeline. */
|
|
34
|
+
function mountGroupSiblings(): HTMLElement[] {
|
|
35
|
+
document.body.innerHTML = `
|
|
36
|
+
<div id="scene" class="clip" data-start="0" data-duration="2">
|
|
37
|
+
<div class="group"></div>
|
|
38
|
+
<div class="group"></div>
|
|
39
|
+
<div class="group"></div>
|
|
40
|
+
<div class="group"></div>
|
|
41
|
+
<div class="group"></div>
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
44
|
+
return Array.from(document.querySelectorAll<HTMLElement>(".group"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const BASE_SCRIPT = `
|
|
48
|
+
const tl = gsap.timeline({ paused: true });
|
|
49
|
+
tl.to("#scene", { opacity: 1, duration: 0.5 }, 0);
|
|
50
|
+
`.trim();
|
|
51
|
+
|
|
52
|
+
const KEYFRAMES = [
|
|
53
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
54
|
+
{ percentage: 100, properties: { x: 40 } },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** Author the selector into a real script and read the tween's targets back. */
|
|
58
|
+
function roundTripTargets(selector: string): { script: string; targets: string[] } {
|
|
59
|
+
const { script } = addAnimationWithKeyframesToScript(BASE_SCRIPT, selector, 0, 1, KEYFRAMES);
|
|
60
|
+
const added = parseGsapScript(script).animations.find((a) => a.targetSelector === selector);
|
|
61
|
+
if (!added) throw new Error(`written selector did not re-parse: ${selector}`);
|
|
62
|
+
return { script, targets: resolveSelectorElementIds(added.targetSelector, document) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("writeTargetSelector", () => {
|
|
66
|
+
it("addresses exactly one element when the only identity is a shared class", () => {
|
|
67
|
+
const groups = mountGroupSiblings();
|
|
68
|
+
const selection = selectionFor(groups[2]);
|
|
69
|
+
|
|
70
|
+
// Precondition: this is the defect's input — a bare class hitting all five.
|
|
71
|
+
expect(selection.selector).toBe(".group");
|
|
72
|
+
expect(document.querySelectorAll(".group")).toHaveLength(5);
|
|
73
|
+
|
|
74
|
+
const written = writeTargetSelector(selection);
|
|
75
|
+
|
|
76
|
+
expect(written).toBeTruthy();
|
|
77
|
+
expect(document.querySelectorAll(written!)).toHaveLength(1);
|
|
78
|
+
expect(document.querySelector(written!)).toBe(groups[2]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps a unique #id target", () => {
|
|
82
|
+
document.body.innerHTML = `<div id="box" class="card"></div>`;
|
|
83
|
+
const el = document.querySelector<HTMLElement>("#box")!;
|
|
84
|
+
|
|
85
|
+
expect(writeTargetSelector(selectionFor(el))).toBe("#box");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("prefers data-hf-id over a generated structural selector", () => {
|
|
89
|
+
document.body.innerHTML = `
|
|
90
|
+
<div id="scene"><div class="group"></div><div class="group" data-hf-id="hf-42"></div></div>
|
|
91
|
+
`;
|
|
92
|
+
const el = document.querySelectorAll<HTMLElement>(".group")[1]!;
|
|
93
|
+
|
|
94
|
+
const written = writeTargetSelector(selectionFor(el));
|
|
95
|
+
|
|
96
|
+
expect(written).toBe('[data-hf-id="hf-42"]');
|
|
97
|
+
expect(document.querySelector(written!)).toBe(el);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps an already-unique class selector as authored", () => {
|
|
101
|
+
document.body.innerHTML = `<div id="scene"><div class="header"></div></div>`;
|
|
102
|
+
const el = document.querySelector<HTMLElement>(".header")!;
|
|
103
|
+
|
|
104
|
+
expect(writeTargetSelector(selectionFor(el))).toBe(".header");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("hands identity back to a data-hf-id ancestor part way up a deep chain", () => {
|
|
108
|
+
// Mixed identity: the walk must step past the id-less <section>, stop at the
|
|
109
|
+
// data-hf-id row, and NOT keep climbing to #outer.
|
|
110
|
+
document.body.innerHTML = `
|
|
111
|
+
<div id="outer">
|
|
112
|
+
<header></header>
|
|
113
|
+
<section>
|
|
114
|
+
<div data-hf-id="mid">
|
|
115
|
+
<div class="cell"></div>
|
|
116
|
+
<div class="cell"></div>
|
|
117
|
+
<div class="cell"></div>
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
`;
|
|
122
|
+
const el = document.querySelectorAll<HTMLElement>(".cell")[2]!;
|
|
123
|
+
|
|
124
|
+
const written = writeTargetSelector(selectionFor(el));
|
|
125
|
+
|
|
126
|
+
expect(written).toBe('[data-hf-id="mid"] > div:nth-child(3)');
|
|
127
|
+
expect(document.querySelectorAll(written!)).toHaveLength(1);
|
|
128
|
+
expect(document.querySelector(written!)).toBe(el);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("falls through to the structural walk when the selection's selector is not valid CSS", () => {
|
|
132
|
+
document.body.innerHTML = `<div id="scene"><div></div><div></div></div>`;
|
|
133
|
+
const el = document.querySelectorAll<HTMLElement>("#scene > div")[1]!;
|
|
134
|
+
// querySelectorAll throws on this, so the "does it match exactly one?" rung
|
|
135
|
+
// must read as a miss rather than crashing the add.
|
|
136
|
+
const selection = { ...selectionFor(el), selector: "div[unclosed" } as DomEditSelection;
|
|
137
|
+
|
|
138
|
+
expect(writeTargetSelector(selection)).toBe("#scene > div:nth-child(2)");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("returns null when a live DOM is present and no rung addresses one element", () => {
|
|
142
|
+
document.body.innerHTML = `<div id="scene"><div class="group"></div><div class="group"></div></div>`;
|
|
143
|
+
const el = document.querySelectorAll<HTMLElement>(".group")[1]!;
|
|
144
|
+
const selection = selectionFor(el);
|
|
145
|
+
expect(selection.selector).toBe(".group");
|
|
146
|
+
// Detached between selecting and committing: ownerDocument still resolves,
|
|
147
|
+
// but there is no parent to count a :nth-child position in. Returning
|
|
148
|
+
// ".group" here would re-author the very selector this function replaces.
|
|
149
|
+
el.remove();
|
|
150
|
+
|
|
151
|
+
expect(writeTargetSelector(selection)).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("still returns the selection's own selector when there is no DOM to check against", () => {
|
|
155
|
+
const selection = {
|
|
156
|
+
selector: ".group",
|
|
157
|
+
sourceFile: "index.html",
|
|
158
|
+
} as unknown as DomEditSelection;
|
|
159
|
+
|
|
160
|
+
expect(writeTargetSelector(selection)).toBe(".group");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("writeTargetSelector — write/read round trip", () => {
|
|
165
|
+
it("re-parses and attributes the new tween to the one element it targeted", () => {
|
|
166
|
+
const groups = mountGroupSiblings();
|
|
167
|
+
// Ids let resolveSelectorElementIds name the attributed elements; the
|
|
168
|
+
// SELECTION still has none, so the write path still faces the bare class.
|
|
169
|
+
groups.forEach((el, i) => el.setAttribute("id", `group-${i}`));
|
|
170
|
+
const selection = { ...selectionFor(groups[2]), id: undefined } as DomEditSelection;
|
|
171
|
+
|
|
172
|
+
const written = writeTargetSelector(selection);
|
|
173
|
+
const { targets } = roundTripTargets(written!);
|
|
174
|
+
|
|
175
|
+
expect(targets).toEqual(["group-2"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not widen the add to the element's four class siblings", () => {
|
|
179
|
+
const groups = mountGroupSiblings();
|
|
180
|
+
groups.forEach((el, i) => el.setAttribute("id", `group-${i}`));
|
|
181
|
+
const selection = { ...selectionFor(groups[0]), id: undefined } as DomEditSelection;
|
|
182
|
+
|
|
183
|
+
// The old bare-class write attributed the one add to every sibling — the
|
|
184
|
+
// attribution blow-up behind "one add collapsed the timeline to a single row".
|
|
185
|
+
expect(roundTripTargets(".group").targets).toHaveLength(5);
|
|
186
|
+
|
|
187
|
+
expect(roundTripTargets(writeTargetSelector(selection)!).targets).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("existingTweenTargetSelector", () => {
|
|
192
|
+
it("keeps the narrowed target when a replace re-authors the tween", async () => {
|
|
193
|
+
const groups = mountGroupSiblings();
|
|
194
|
+
const selection = selectionFor(groups[2]);
|
|
195
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
196
|
+
// A tween a previous add already narrowed to one sibling. Re-deriving the
|
|
197
|
+
// target from the selection would widen it back to all five.
|
|
198
|
+
const setAnim = {
|
|
199
|
+
id: "a1",
|
|
200
|
+
targetSelector: "#scene > div:nth-child(3)",
|
|
201
|
+
method: "set",
|
|
202
|
+
position: 0,
|
|
203
|
+
properties: { x: 10 },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await promoteSetToKeyframes({ commitMutation } as never, selection, setAnim as never, 0, null);
|
|
207
|
+
|
|
208
|
+
const mutation = commitMutation.mock.calls[0]?.[0] as { targetSelector: string };
|
|
209
|
+
expect(mutation.targetSelector).toBe("#scene > div:nth-child(3)");
|
|
210
|
+
expect(document.querySelectorAll(mutation.targetSelector)).toHaveLength(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("falls back to the selection when the tween's own target did not resolve", async () => {
|
|
214
|
+
const groups = mountGroupSiblings();
|
|
215
|
+
const selection = selectionFor(groups[2]);
|
|
216
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
217
|
+
const setAnim = {
|
|
218
|
+
id: "a1",
|
|
219
|
+
targetSelector: "targets[i]",
|
|
220
|
+
hasUnresolvedSelector: true,
|
|
221
|
+
method: "set",
|
|
222
|
+
position: 0,
|
|
223
|
+
properties: { x: 10 },
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
await promoteSetToKeyframes({ commitMutation } as never, selection, setAnim as never, 0, null);
|
|
227
|
+
|
|
228
|
+
const mutation = commitMutation.mock.calls[0]?.[0] as { targetSelector: string };
|
|
229
|
+
expect(mutation.targetSelector).toBe(".group");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("commitKeyframeAtTimeImpl — new-tween target", () => {
|
|
234
|
+
it("authors a one-element selector when no tween exists at the playhead", async () => {
|
|
235
|
+
const groups = mountGroupSiblings();
|
|
236
|
+
const selection = selectionFor(groups[3]);
|
|
237
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
238
|
+
|
|
239
|
+
await commitKeyframeAtTimeImpl(selection, 1, [], { x: 12 }, commitMutation);
|
|
240
|
+
|
|
241
|
+
const mutation = commitMutation.mock.calls[0]?.[1] as { targetSelector: string };
|
|
242
|
+
expect(mutation.targetSelector).not.toBe(".group");
|
|
243
|
+
expect(document.querySelectorAll(mutation.targetSelector)).toHaveLength(1);
|
|
244
|
+
expect(document.querySelector(mutation.targetSelector)).toBe(groups[3]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("tweenTargetsElement", () => {
|
|
249
|
+
it("matches a tween narrowed to this element by a selector the selection does not use", () => {
|
|
250
|
+
const groups = mountGroupSiblings();
|
|
251
|
+
groups[2]!.id = "narrowed";
|
|
252
|
+
|
|
253
|
+
// The write half authored "#narrowed"; the read half still has ".group".
|
|
254
|
+
expect(tweenTargetsElement("#narrowed", ".group", groups[2])).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("does not hand an individual element the group tween it merely inherits", () => {
|
|
258
|
+
const groups = mountGroupSiblings();
|
|
259
|
+
|
|
260
|
+
// Selecting one sibling by its own address must not let an edit mutate the
|
|
261
|
+
// ".group" tween: that write moves all five, not the one being nudged.
|
|
262
|
+
expect(tweenTargetsElement(".group", "#scene > div:nth-child(3)", groups[2])).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("still edits a group tween when the group itself is the selection", () => {
|
|
266
|
+
const groups = mountGroupSiblings();
|
|
267
|
+
|
|
268
|
+
expect(tweenTargetsElement(".group", ".group", groups[0])).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("does not match a target that is not a selector matches() understands", () => {
|
|
272
|
+
const groups = mountGroupSiblings();
|
|
273
|
+
|
|
274
|
+
expect(tweenTargetsElement("div[unclosed", ".group", groups[0])).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("commitKeyframeAtTimeImpl — no one-element target", () => {
|
|
279
|
+
it("drops the keyframe rather than authoring the bare class", async () => {
|
|
280
|
+
const groups = mountGroupSiblings();
|
|
281
|
+
const selection = selectionFor(groups[3]!);
|
|
282
|
+
groups[3]!.remove();
|
|
283
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
284
|
+
|
|
285
|
+
await commitKeyframeAtTimeImpl(selection, 1, [], { x: 12 }, commitMutation);
|
|
286
|
+
|
|
287
|
+
expect(commitMutation).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reading a composition file's GSAP tweens into the keyframe cache: fetch
|
|
3
|
-
* selector -> element id resolution
|
|
2
|
+
* Reading a composition file's GSAP tweens into the keyframe cache: fetch and
|
|
3
|
+
* selector -> element id resolution.
|
|
4
4
|
* Split from useGsapTweenCache to keep that file under the 600-line limit.
|
|
5
5
|
*/
|
|
6
6
|
import type { GsapAnimation, GsapKeyframesData, ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
@@ -11,50 +11,15 @@ import {
|
|
|
11
11
|
elementCacheKeys,
|
|
12
12
|
writeGsapAnimationsForElement,
|
|
13
13
|
} from "./gsapKeyframeCacheHelpers";
|
|
14
|
-
import {
|
|
14
|
+
import { resolveClipTimingBasis, resolveSelectorElementIds, toClipKeyframes } from "./gsapShared";
|
|
15
15
|
import {
|
|
16
16
|
deduplicateKeyframes,
|
|
17
17
|
isStaticPositionHold,
|
|
18
18
|
synthesizeFlatTweenKeyframes,
|
|
19
19
|
} from "./gsapTweenSynth";
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* A bare `#id` resolves directly; anything else (a class like `.dot`, a group
|
|
24
|
-
* `.a, .b`, or a descendant selector) is matched against the live preview DOM so
|
|
25
|
-
* class/selector tweens (e.g. `gsap.from(".dot", {stagger})`) attribute to every
|
|
26
|
-
* element they animate — not just one parsed from the string. Falls back to a
|
|
27
|
-
* leading `#id` when there's no DOM (so the cache still populates pre-iframe).
|
|
28
|
-
*/
|
|
29
|
-
// fallow-ignore-next-line complexity
|
|
30
|
-
export function resolveSelectorElementIds(
|
|
31
|
-
selector: string,
|
|
32
|
-
doc: Document | null | undefined,
|
|
33
|
-
): string[] {
|
|
34
|
-
// A whole-selector id match (either shape) addresses exactly one element.
|
|
35
|
-
const bareId = /^(#[\w-]+|\[id="(?:\\.|[^"\\])*"\])$/.test(selector)
|
|
36
|
-
? idFromSelector(selector)
|
|
37
|
-
: null;
|
|
38
|
-
if (bareId) return [bareId];
|
|
39
|
-
if (!doc) {
|
|
40
|
-
const lead = idFromSelector(selector);
|
|
41
|
-
return lead ? [lead] : [];
|
|
42
|
-
}
|
|
43
|
-
const ids = new Set<string>();
|
|
44
|
-
for (const part of selector.split(",")) {
|
|
45
|
-
const sel = part.trim();
|
|
46
|
-
if (!sel) continue;
|
|
47
|
-
try {
|
|
48
|
-
for (const el of Array.from(doc.querySelectorAll(sel))) {
|
|
49
|
-
if (el.id) ids.add(el.id);
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
const lead = idFromSelector(sel);
|
|
53
|
-
if (lead) ids.add(lead);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return Array.from(ids);
|
|
57
|
-
}
|
|
21
|
+
export { resolveSelectorElementIds };
|
|
22
|
+
|
|
58
23
|
/**
|
|
59
24
|
* The slice of the parse response callers actually read. The endpoint returns
|
|
60
25
|
* the full `ParsedGsap` (preamble/postamble and all), but nothing downstream of
|
|
@@ -103,37 +68,6 @@ export async function fetchParsedAnimations(
|
|
|
103
68
|
}
|
|
104
69
|
}
|
|
105
70
|
|
|
106
|
-
/**
|
|
107
|
-
* Clip-relative timing basis for an element. Sub-composition internals (e.g. pills
|
|
108
|
-
* inside a scene) aren't timeline clips themselves — they're derived at expand time
|
|
109
|
-
* — so they're absent from `elements`. Without a basis, elDuration defaulted to 1
|
|
110
|
-
* and clip-relative keyframe percentages blew past 100% (rendering off the clip).
|
|
111
|
-
* Fall back to the sub-comp HOST's bounds, resolved via domClipChildren (the host's
|
|
112
|
-
* data-composition-src is stripped in the rendered DOM, so we can't query it).
|
|
113
|
-
*/
|
|
114
|
-
export function resolveClipTimingBasis(
|
|
115
|
-
elementId: string,
|
|
116
|
-
sourceFile: string,
|
|
117
|
-
elements: ReadonlyArray<{
|
|
118
|
-
domId?: string;
|
|
119
|
-
key?: string;
|
|
120
|
-
id: string;
|
|
121
|
-
start: number;
|
|
122
|
-
duration: number;
|
|
123
|
-
}>,
|
|
124
|
-
domClipChildren: ReadonlyArray<{ id: string; hostId: string }>,
|
|
125
|
-
): { elStart: number; elDuration: number } {
|
|
126
|
-
const direct = elements.find(
|
|
127
|
-
(el) => el.domId === elementId || (el.key ?? el.id) === `${sourceFile}#${elementId}`,
|
|
128
|
-
);
|
|
129
|
-
if (direct) return { elStart: direct.start, elDuration: direct.duration };
|
|
130
|
-
const hostId = domClipChildren.find((c) => c.id === elementId)?.hostId;
|
|
131
|
-
const host = hostId
|
|
132
|
-
? elements.find((el) => el.domId === hostId || (el.key ?? el.id) === `index.html#${hostId}`)
|
|
133
|
-
: undefined;
|
|
134
|
-
return { elStart: host?.start ?? 0, elDuration: host?.duration ?? 1 };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
71
|
/**
|
|
138
72
|
* Read one composition file's tweens into the keyframe cache. Split out of the
|
|
139
73
|
* hook so the effect can run it per file without re-nesting the whole body.
|