@hyperframes/studio 0.7.76 → 0.7.78
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-DsRdxz7A.js → hyperframes-player-uiAAPKvw.js} +1 -1
- package/dist/assets/index-BSZrK0bx.js +428 -0
- package/dist/assets/{index-Du1RoLiB.js → index-CDSTMtUs.js} +1 -1
- package/dist/assets/index-DpkO2Hvw.css +1 -0
- package/dist/assets/{index-BLICKger.js → index-r1tKKlU7.js} +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.html +2 -2
- package/dist/index.js +25030 -23214
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +16 -17
- package/src/components/EditorShell.tsx +3 -0
- package/src/components/TimelineToolbar.test.tsx +48 -3
- package/src/components/TimelineToolbar.tsx +89 -36
- package/src/components/editor/AnimationCard.test.tsx +18 -75
- package/src/components/editor/AnimationCard.tsx +26 -2
- package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
- package/src/components/editor/GsapAnimationSection.tsx +18 -157
- package/src/components/editor/KeyframeEaseList.tsx +5 -1
- package/src/components/editor/KeyframeNavigation.tsx +46 -14
- package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
- package/src/components/editor/MotionPathOverlay.tsx +32 -10
- package/src/components/editor/PropertyPanel.tsx +15 -5
- package/src/components/editor/PropertyPanelFlat.tsx +48 -23
- package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
- package/src/components/editor/keyframeRetime.test.ts +25 -22
- package/src/components/editor/keyframeRetime.ts +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
- package/src/components/nle/PreviewOverlays.tsx +3 -1
- package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +41 -0
- package/src/components/sidebar/CompositionsTab.tsx +66 -32
- package/src/contexts/TimelineEditContext.tsx +1 -1
- package/src/hooks/deleteSelectedKeyframes.ts +24 -8
- package/src/hooks/gsapDragCommit.ts +2 -2
- package/src/hooks/gsapDragPositionCommit.ts +84 -5
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
- package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
- package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
- package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
- package/src/hooks/gsapScriptCommitTypes.ts +2 -0
- package/src/hooks/gsapShared.test.ts +109 -1
- package/src/hooks/gsapShared.ts +142 -17
- package/src/hooks/gsapTweenSynth.test.ts +30 -1
- package/src/hooks/gsapTweenSynth.ts +61 -14
- package/src/hooks/keyframeCacheAstLoad.ts +181 -0
- package/src/hooks/timelineEditingHelpers.test.ts +80 -0
- package/src/hooks/timelineMoveAdapter.ts +9 -2
- package/src/hooks/useDomEditWiring.ts +2 -2
- package/src/hooks/useDomSelection.ts +4 -0
- package/src/hooks/useEnableKeyframes.test.ts +122 -0
- package/src/hooks/useEnableKeyframes.ts +116 -56
- package/src/hooks/useGestureCommit.ts +2 -2
- package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
- package/src/hooks/useGsapKeyframeOps.ts +30 -13
- package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
- package/src/hooks/useGsapScriptCommits.ts +2 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
- package/src/hooks/useGsapSelectionHandlers.ts +90 -65
- package/src/hooks/useGsapTweenCache.test.ts +16 -0
- package/src/hooks/useGsapTweenCache.ts +64 -178
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
- package/src/hooks/useStudioContextValue.test.ts +93 -0
- package/src/hooks/useStudioContextValue.ts +17 -3
- package/src/hooks/useStudioTestHooks.ts +63 -0
- package/src/hooks/useTimelineEditing.test.tsx +187 -22
- package/src/hooks/useTimelineEditing.ts +43 -32
- package/src/hooks/useTimelineEditingTypes.ts +2 -0
- package/src/hooks/useTimelineGroupEditing.ts +106 -82
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
- package/src/player/components/LayerDisclosureRow.tsx +76 -0
- package/src/player/components/Player.test.ts +143 -12
- package/src/player/components/Player.tsx +84 -43
- package/src/player/components/Timeline.test.ts +420 -90
- package/src/player/components/Timeline.tsx +80 -78
- package/src/player/components/TimelineCanvas.tsx +33 -21
- package/src/player/components/TimelineClip.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
- package/src/player/components/TimelineClipDiamonds.tsx +320 -117
- package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
- package/src/player/components/TimelineLanes.tsx +153 -79
- package/src/player/components/TimelineOverlays.tsx +4 -18
- package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
- package/src/player/components/TimelinePropertyLanes.tsx +191 -0
- package/src/player/components/TimelineRazorInteraction.tsx +60 -0
- package/src/player/components/TimelineRuler.tsx +11 -21
- package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
- package/src/player/components/TimelineTrackHeader.tsx +363 -0
- package/src/player/components/TrackClipCount.tsx +17 -0
- package/src/player/components/timelineCallbacks.ts +21 -6
- package/src/player/components/timelineClipDragPreview.test.ts +50 -3
- package/src/player/components/timelineClipDragPreview.ts +33 -15
- package/src/player/components/timelineCollision.ts +4 -13
- package/src/player/components/timelineDiamondTypes.ts +120 -0
- package/src/player/components/timelineDragDrop.ts +8 -3
- package/src/player/components/timelineEditing.test.ts +19 -27
- package/src/player/components/timelineEditing.ts +7 -8
- package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
- package/src/player/components/timelineKeyframeIdentity.ts +66 -0
- package/src/player/components/timelineLayout.test.ts +162 -9
- package/src/player/components/timelineLayout.ts +198 -38
- package/src/player/components/timelineMarquee.test.ts +76 -21
- package/src/player/components/timelineMarquee.ts +17 -14
- package/src/player/components/trackHeaderLaneState.ts +121 -0
- package/src/player/components/trackHeaderLaneValues.ts +139 -0
- package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
- package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
- package/src/player/components/useTimelineClipDrag.ts +4 -1
- package/src/player/components/useTimelineGeometry.ts +4 -1
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
- package/src/player/components/useTimelinePlayhead.ts +21 -12
- package/src/player/components/useTimelineRangeSelection.ts +35 -14
- package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
- package/src/player/components/useTimelineTrackLayout.ts +162 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
- package/src/player/lib/timelineElementHelpers.ts +26 -11
- package/src/player/store/keyframeSlice.ts +122 -0
- package/src/player/store/playerStore.test.ts +25 -0
- package/src/player/store/playerStore.ts +7 -43
- package/src/telemetry/events.test.ts +9 -3
- package/src/telemetry/events.ts +16 -7
- package/src/utils/globalTimeCompiler.test.ts +10 -0
- package/src/utils/globalTimeCompiler.ts +3 -3
- package/dist/assets/index-B37rWXo4.css +0 -1
- package/dist/assets/index-BdOfFsR8.js +0 -428
- package/src/player/components/TimelineDragGhost.tsx +0 -59
- package/src/utils/keyframeSelection.test.ts +0 -45
- package/src/utils/keyframeSelection.ts +0 -29
|
@@ -2,6 +2,7 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
2
2
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
3
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
4
4
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
5
|
+
import { KEYFRAME_PCT_MATCH, resolveEditableTweenDuration } from "./gsapShared";
|
|
5
6
|
import { roundTo3 } from "../utils/rounding";
|
|
6
7
|
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
7
8
|
import {
|
|
@@ -11,6 +12,31 @@ import {
|
|
|
11
12
|
materializeIfDynamic,
|
|
12
13
|
} from "./gsapDragCommit";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The tween's keyframes with one inserted at `percentage`. Any existing keyframe
|
|
17
|
+
* within {@link KEYFRAME_PCT_MATCH} of the insert is REPLACED, not kept: the
|
|
18
|
+
* server takes a replace-with-keyframes list verbatim, so an append-only build
|
|
19
|
+
* could hand it two keyframes a fraction of a percent apart. The invariant lives
|
|
20
|
+
* here rather than in each caller's own pre-check, which is how the two callers
|
|
21
|
+
* ended up with different tolerances in the first place.
|
|
22
|
+
*/
|
|
23
|
+
export function buildTemporalArcKeyframes(
|
|
24
|
+
anim: GsapAnimation,
|
|
25
|
+
percentage: number,
|
|
26
|
+
properties: Record<string, number>,
|
|
27
|
+
) {
|
|
28
|
+
return [
|
|
29
|
+
...(anim.keyframes?.keyframes ?? [])
|
|
30
|
+
.filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH)
|
|
31
|
+
.map((keyframe) => ({
|
|
32
|
+
percentage: keyframe.percentage,
|
|
33
|
+
properties: { ...keyframe.properties },
|
|
34
|
+
...(keyframe.ease ? { ease: keyframe.ease } : {}),
|
|
35
|
+
})),
|
|
36
|
+
{ percentage, properties },
|
|
37
|
+
].sort((a, b) => a.percentage - b.percentage);
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
async function extendTweenAndAddKeyframe(
|
|
15
41
|
selection: DomEditSelection,
|
|
16
42
|
anim: GsapAnimation,
|
|
@@ -143,7 +169,7 @@ async function commitFlatViaKeyframes(
|
|
|
143
169
|
): Promise<void> {
|
|
144
170
|
const ct = usePlayerStore.getState().currentTime;
|
|
145
171
|
const ts = resolveTweenStart(anim);
|
|
146
|
-
const td =
|
|
172
|
+
const td = resolveEditableTweenDuration(anim, selection);
|
|
147
173
|
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
148
174
|
const outsideRange =
|
|
149
175
|
activeKeyframePct == null && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
@@ -184,8 +210,13 @@ async function commitFlatViaKeyframes(
|
|
|
184
210
|
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
|
185
211
|
);
|
|
186
212
|
const fresh = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
213
|
+
// By id first: a target with several tweens (two `to`s on the same selector)
|
|
214
|
+
// matches the selector lookup on whichever one happens to be first, and the
|
|
215
|
+
// extend-and-add below would then rewrite a tween the drag never touched.
|
|
187
216
|
const converted =
|
|
188
|
-
fresh.find((a) => a.
|
|
217
|
+
fresh.find((a) => a.id === anim.id && a.keyframes) ??
|
|
218
|
+
fresh.find((a) => a.targetSelector === anim.targetSelector && a.keyframes) ??
|
|
219
|
+
anim;
|
|
189
220
|
const convertedStart = resolveTweenStart(converted) ?? ts;
|
|
190
221
|
const convertedDur = resolveTweenDuration(converted) || td;
|
|
191
222
|
await extendTweenAndAddKeyframe(
|
|
@@ -259,13 +290,61 @@ export async function commitGsapPositionFromDrag(
|
|
|
259
290
|
|
|
260
291
|
const backfillDefaults: Record<string, number> = { x: baseGsapX, y: baseGsapY };
|
|
261
292
|
const ct = usePlayerStore.getState().currentTime;
|
|
293
|
+
if (anim.arcPath?.enabled) {
|
|
294
|
+
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
295
|
+
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
296
|
+
const keyframes = anim.keyframes?.keyframes ?? [];
|
|
297
|
+
// Same tolerance as applyArcKeyframeAtPlayhead and isMotionPathEndpoint. A
|
|
298
|
+
// tighter one here meant a drag that landed a fraction of a percent off an
|
|
299
|
+
// authored waypoint skipped the update-point branch and appended instead.
|
|
300
|
+
const pointIndex = keyframes.findIndex(
|
|
301
|
+
(kf) => Math.abs(kf.percentage - pct) <= KEYFRAME_PCT_MATCH,
|
|
302
|
+
);
|
|
303
|
+
if (pointIndex >= 0) {
|
|
304
|
+
await callbacks.commitMutation(
|
|
305
|
+
selection,
|
|
306
|
+
{
|
|
307
|
+
type: "update-motion-path-point",
|
|
308
|
+
animationId: anim.id,
|
|
309
|
+
pointIndex,
|
|
310
|
+
x: newX,
|
|
311
|
+
y: newY,
|
|
312
|
+
},
|
|
313
|
+
{ label: "Move layer (waypoint)", softReload: true, beforeReload: restoreOffset },
|
|
314
|
+
);
|
|
315
|
+
setActiveKeyframePct(null);
|
|
316
|
+
parkPlayheadOnKeyframe(anim, pct);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const tweenStart = resolveTweenStart(anim);
|
|
321
|
+
// Clip-wide, same as applyArcKeyframeAtPlayhead: authoring GSAP's 0.5s
|
|
322
|
+
// default here would collapse a duration-less arc's window on drag.
|
|
323
|
+
const tweenDuration = resolveEditableTweenDuration(anim, selection);
|
|
324
|
+
if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2) return;
|
|
325
|
+
const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
|
|
326
|
+
await callbacks.commitMutation(
|
|
327
|
+
selection,
|
|
328
|
+
{
|
|
329
|
+
type: "replace-with-keyframes",
|
|
330
|
+
animationId: anim.id,
|
|
331
|
+
targetSelector: anim.targetSelector,
|
|
332
|
+
position: roundTo3(tweenStart),
|
|
333
|
+
duration: roundTo3(tweenDuration),
|
|
334
|
+
keyframes: temporalKeyframes,
|
|
335
|
+
ease: "none",
|
|
336
|
+
},
|
|
337
|
+
{ label: "Move layer (new keyframe)", softReload: true, beforeReload: restoreOffset },
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
262
341
|
if (anim.keyframes) {
|
|
263
342
|
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
264
343
|
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
265
344
|
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
266
345
|
|
|
267
346
|
const ts = resolveTweenStart(effectiveAnim);
|
|
268
|
-
const td =
|
|
347
|
+
const td = resolveEditableTweenDuration(effectiveAnim, selection);
|
|
269
348
|
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
270
349
|
const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
|
|
271
350
|
if (outsideRange && !hasSelectedKeyframe) {
|
|
@@ -293,7 +372,7 @@ export async function commitGsapPositionFromDrag(
|
|
|
293
372
|
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
294
373
|
const ct = usePlayerStore.getState().currentTime;
|
|
295
374
|
const ts = resolveTweenStart(anim);
|
|
296
|
-
const td =
|
|
375
|
+
const td = resolveEditableTweenDuration(anim, selection);
|
|
297
376
|
const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
|
|
298
377
|
const outsideRange =
|
|
299
378
|
!hasSelectedKeyframe && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
@@ -313,7 +392,7 @@ export async function commitGsapPositionFromDrag(
|
|
|
313
392
|
|
|
314
393
|
if (existingPosAnim?.keyframes) {
|
|
315
394
|
const posTs = resolveTweenStart(existingPosAnim);
|
|
316
|
-
const posTd =
|
|
395
|
+
const posTd = resolveEditableTweenDuration(existingPosAnim, selection);
|
|
317
396
|
if (posTs !== null) {
|
|
318
397
|
await extendTweenAndAddKeyframe(
|
|
319
398
|
selection,
|
|
@@ -4,6 +4,7 @@ import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerS
|
|
|
4
4
|
import {
|
|
5
5
|
clearKeyframeCacheForElement,
|
|
6
6
|
clearKeyframeCacheForFile,
|
|
7
|
+
pruneKeyframeCacheToFiles,
|
|
7
8
|
updateKeyframeCacheFromParsed,
|
|
8
9
|
} from "./gsapKeyframeCacheHelpers";
|
|
9
10
|
|
|
@@ -28,7 +29,7 @@ const animWithKeyframes = (id: string): GsapAnimation => ({
|
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
beforeEach(() => {
|
|
31
|
-
usePlayerStore.setState({ keyframeCache: new Map(), elements: [] });
|
|
32
|
+
usePlayerStore.setState({ keyframeCache: new Map(), gsapAnimations: new Map(), elements: [] });
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
describe("clearKeyframeCacheForElement", () => {
|
|
@@ -84,6 +85,20 @@ describe("clearKeyframeCacheForFile", () => {
|
|
|
84
85
|
}
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
// Several composition files re-scan concurrently, so a clear that walked the
|
|
89
|
+
// index.html alias would delete rows a sibling file had just written.
|
|
90
|
+
it("leaves an index.html-owned element alone when another file re-scans", () => {
|
|
91
|
+
seed("index.html#title");
|
|
92
|
+
seed("title");
|
|
93
|
+
seed("comp.html#a");
|
|
94
|
+
|
|
95
|
+
clearKeyframeCacheForFile("comp.html");
|
|
96
|
+
|
|
97
|
+
expect(cache().has("index.html#title")).toBe(true);
|
|
98
|
+
expect(cache().has("title")).toBe(true);
|
|
99
|
+
expect(cache().has("comp.html#a")).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
87
102
|
it("leaves entries that belong to a different source file", () => {
|
|
88
103
|
seed("comp.html#a");
|
|
89
104
|
seed("a");
|
|
@@ -97,7 +112,84 @@ describe("clearKeyframeCacheForFile", () => {
|
|
|
97
112
|
});
|
|
98
113
|
});
|
|
99
114
|
|
|
115
|
+
describe("pruneKeyframeCacheToFiles", () => {
|
|
116
|
+
// Switching composition leaves the previous comp's elements cached with no
|
|
117
|
+
// owner left to clear them: each file only ever clears its own entries.
|
|
118
|
+
it("drops every element of a file the next scan no longer covers", () => {
|
|
119
|
+
seed("index.html#stress-1");
|
|
120
|
+
seed("stress-1");
|
|
121
|
+
seed("index.html#stress-2");
|
|
122
|
+
seed("stress-2");
|
|
123
|
+
seed("kf200.html#kf200");
|
|
124
|
+
seed("index.html#kf200");
|
|
125
|
+
seed("kf200");
|
|
126
|
+
|
|
127
|
+
pruneKeyframeCacheToFiles(["kf200.html"]);
|
|
128
|
+
|
|
129
|
+
for (const key of ["index.html#stress-1", "stress-1", "index.html#stress-2", "stress-2"]) {
|
|
130
|
+
expect(cache().has(key)).toBe(false);
|
|
131
|
+
}
|
|
132
|
+
expect(cache().has("kf200.html#kf200")).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("prunes gsapAnimations alongside keyframeCache", () => {
|
|
136
|
+
usePlayerStore.getState().setGsapAnimations("index.html#stress-1", [animWithKeyframes("t")]);
|
|
137
|
+
usePlayerStore.getState().setGsapAnimations("kf200.html#kf200", [animWithKeyframes("u")]);
|
|
138
|
+
|
|
139
|
+
pruneKeyframeCacheToFiles(["kf200.html"]);
|
|
140
|
+
|
|
141
|
+
const animations = usePlayerStore.getState().gsapAnimations;
|
|
142
|
+
expect(animations.has("index.html#stress-1")).toBe(false);
|
|
143
|
+
expect(animations.has("kf200.html#kf200")).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("keeps everything when every cached file is still covered", () => {
|
|
147
|
+
seed("index.html#hero");
|
|
148
|
+
seed("comp.html#a");
|
|
149
|
+
|
|
150
|
+
pruneKeyframeCacheToFiles(["index.html", "comp.html"]);
|
|
151
|
+
|
|
152
|
+
expect(cache().has("index.html#hero")).toBe(true);
|
|
153
|
+
expect(cache().has("comp.html#a")).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
100
157
|
describe("updateKeyframeCacheFromParsed", () => {
|
|
158
|
+
it("serializes a multi-keyframe tween with a stable shape and animation identity", () => {
|
|
159
|
+
const animation: GsapAnimation = {
|
|
160
|
+
...animWithKeyframes("hero"),
|
|
161
|
+
duration: 2,
|
|
162
|
+
resolvedStart: 3,
|
|
163
|
+
keyframes: {
|
|
164
|
+
format: "percentage",
|
|
165
|
+
keyframes: [
|
|
166
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
167
|
+
{ percentage: 50, properties: { x: 100 }, ease: "power1.inOut" },
|
|
168
|
+
{ percentage: 100, properties: { x: 200 } },
|
|
169
|
+
],
|
|
170
|
+
easeEach: "power1.inOut",
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
usePlayerStore.setState({
|
|
174
|
+
elements: [
|
|
175
|
+
{
|
|
176
|
+
id: "hero-clip",
|
|
177
|
+
domId: "hero",
|
|
178
|
+
tag: "div",
|
|
179
|
+
start: 2,
|
|
180
|
+
duration: 4,
|
|
181
|
+
track: 0,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
updateKeyframeCacheFromParsed([animation], "scene.html", "hero", {});
|
|
187
|
+
|
|
188
|
+
expect(JSON.stringify(cache().get("scene.html#hero"))).toBe(
|
|
189
|
+
'{"format":"percentage","keyframes":[{"percentage":25,"properties":{"x":0},"tweenPercentage":0,"propertyGroup":"position","animationId":"hero"},{"percentage":50,"properties":{"x":100},"ease":"power1.inOut","tweenPercentage":50,"propertyGroup":"position","animationId":"hero"},{"percentage":75,"properties":{"x":200},"tweenPercentage":100,"propertyGroup":"position","animationId":"hero"}],"easeEach":"power1.inOut"}',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
101
193
|
it("clears the bare key when the selected element no longer has keyframes", () => {
|
|
102
194
|
// Element previously had keyframes, so a bare entry exists (writes set both).
|
|
103
195
|
seed("index.html#box");
|
|
@@ -118,4 +210,87 @@ describe("updateKeyframeCacheFromParsed", () => {
|
|
|
118
210
|
expect(cache().has("index.html#hero")).toBe(true);
|
|
119
211
|
expect(cache().has("hero")).toBe(true);
|
|
120
212
|
});
|
|
213
|
+
|
|
214
|
+
it("caches flat tweens as clip-relative start and end keyframes", () => {
|
|
215
|
+
const animation: GsapAnimation = {
|
|
216
|
+
id: "flat-box",
|
|
217
|
+
targetSelector: "#box",
|
|
218
|
+
method: "to",
|
|
219
|
+
position: 1,
|
|
220
|
+
properties: { x: 420 },
|
|
221
|
+
duration: 2,
|
|
222
|
+
resolvedStart: 1,
|
|
223
|
+
ease: "power2.out",
|
|
224
|
+
propertyGroup: "position",
|
|
225
|
+
};
|
|
226
|
+
usePlayerStore.setState({
|
|
227
|
+
elements: [{ id: "box-clip", domId: "box", tag: "div", start: 1, duration: 2, track: 0 }],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
updateKeyframeCacheFromParsed([animation], "scene.html", "box", {});
|
|
231
|
+
|
|
232
|
+
expect(cache().get("scene.html#box")).toEqual({
|
|
233
|
+
format: "percentage",
|
|
234
|
+
keyframes: [
|
|
235
|
+
{
|
|
236
|
+
percentage: 0,
|
|
237
|
+
properties: { x: 0 },
|
|
238
|
+
tweenPercentage: 0,
|
|
239
|
+
propertyGroup: "position",
|
|
240
|
+
animationId: "flat-box",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
percentage: 100,
|
|
244
|
+
properties: { x: 420 },
|
|
245
|
+
ease: "power2.out",
|
|
246
|
+
tweenPercentage: 100,
|
|
247
|
+
propertyGroup: "position",
|
|
248
|
+
animationId: "flat-box",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#box")).toEqual([animation]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("records an ungrouped tween in gsapAnimations too, so the two stores agree", () => {
|
|
256
|
+
// `{ x, opacity }` spans two property groups, so the parser leaves
|
|
257
|
+
// propertyGroup undefined. Skipping it here used to cache diamonds with no
|
|
258
|
+
// source animation behind them: the collapsed row drew keyframes the
|
|
259
|
+
// expanded lanes could not render.
|
|
260
|
+
const animation: GsapAnimation = {
|
|
261
|
+
id: "mixed-box",
|
|
262
|
+
targetSelector: "#box",
|
|
263
|
+
method: "to",
|
|
264
|
+
position: 0,
|
|
265
|
+
properties: { x: 100, opacity: 0 },
|
|
266
|
+
duration: 1,
|
|
267
|
+
resolvedStart: 0,
|
|
268
|
+
};
|
|
269
|
+
usePlayerStore.setState({
|
|
270
|
+
elements: [{ id: "box-clip", domId: "box", tag: "div", start: 0, duration: 1, track: 0 }],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
updateKeyframeCacheFromParsed([animation], "scene.html", "box", {});
|
|
274
|
+
|
|
275
|
+
expect(cache().has("scene.html#box")).toBe(true);
|
|
276
|
+
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#box")).toEqual([animation]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("does not cache a flat tween without animatable numeric properties", () => {
|
|
280
|
+
const animation: GsapAnimation = {
|
|
281
|
+
id: "flat-box",
|
|
282
|
+
targetSelector: "#box",
|
|
283
|
+
method: "to",
|
|
284
|
+
position: 0,
|
|
285
|
+
properties: { backgroundColor: "#fff" },
|
|
286
|
+
duration: 1,
|
|
287
|
+
propertyGroup: "visual",
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
updateKeyframeCacheFromParsed([animation], "scene.html", "box", {});
|
|
291
|
+
|
|
292
|
+
expect(cache().has("scene.html#box")).toBe(false);
|
|
293
|
+
expect(cache().has("box")).toBe(false);
|
|
294
|
+
expect(usePlayerStore.getState().gsapAnimations.has("scene.html#box")).toBe(false);
|
|
295
|
+
});
|
|
121
296
|
});
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
|
|
7
|
-
import {
|
|
7
|
+
import { idFromSelector, toClipKeyframes } from "./gsapShared";
|
|
8
|
+
import { deduplicateKeyframes, synthesizeFlatTweenKeyframes } from "./gsapTweenSynth";
|
|
8
9
|
|
|
9
10
|
export function updateKeyframeCacheFromParsed(
|
|
10
11
|
animations: GsapAnimation[],
|
|
@@ -15,57 +16,52 @@ export function updateKeyframeCacheFromParsed(
|
|
|
15
16
|
const { setKeyframeCache, elements } = usePlayerStore.getState();
|
|
16
17
|
const idsWithKeyframes = new Set<string>();
|
|
17
18
|
const merged = new Map<string, KeyframeCacheEntry>();
|
|
19
|
+
const sourceAnimations = new Map<string, GsapAnimation[]>();
|
|
18
20
|
for (const anim of animations) {
|
|
19
|
-
const id = anim.targetSelector
|
|
20
|
-
|
|
21
|
+
const id = idFromSelector(anim.targetSelector);
|
|
22
|
+
const kfSource =
|
|
23
|
+
anim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(anim)?.keyframes ?? [];
|
|
24
|
+
if (!id || kfSource.length === 0) continue;
|
|
21
25
|
idsWithKeyframes.add(id);
|
|
26
|
+
// Every tween that fed keyframeCache also lands in gsapAnimations, group or
|
|
27
|
+
// not: a mixed-group tween (`{ x, opacity }` classifies to undefined) used to
|
|
28
|
+
// cache diamonds with no source animation behind them, so the collapsed row
|
|
29
|
+
// drew keyframes the expanded lanes couldn't render. Lane consumers do the
|
|
30
|
+
// group filtering themselves (animationContributesLane).
|
|
31
|
+
sourceAnimations.set(id, [...(sourceAnimations.get(id) ?? []), anim]);
|
|
22
32
|
|
|
23
33
|
// Convert tween-relative percentages to clip-relative so diamonds
|
|
24
34
|
// render at the correct position within the timeline clip.
|
|
25
|
-
const tweenPos = anim.resolvedStart ?? (typeof anim.position === "number" ? anim.position : 0);
|
|
26
|
-
const tweenDur = anim.duration ?? 1;
|
|
27
35
|
const timelineEl = elements.find(
|
|
28
36
|
(el) => el.domId === id || (el.key ?? el.id) === `${targetPath}#${id}`,
|
|
29
37
|
);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
...kf,
|
|
38
|
-
percentage: clipPct,
|
|
39
|
-
tweenPercentage: kf.percentage,
|
|
40
|
-
propertyGroup: anim.propertyGroup,
|
|
41
|
-
};
|
|
42
|
-
});
|
|
38
|
+
const clipKeyframes = toClipKeyframes(
|
|
39
|
+
kfSource,
|
|
40
|
+
anim,
|
|
41
|
+
timelineEl?.start ?? 0,
|
|
42
|
+
timelineEl?.duration ?? 1,
|
|
43
|
+
);
|
|
43
44
|
|
|
44
45
|
const existing = merged.get(id);
|
|
45
46
|
if (existing) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
prev.properties = { ...prev.properties, ...kf.properties };
|
|
51
|
-
if (kf.ease) prev.ease = kf.ease;
|
|
52
|
-
} else {
|
|
53
|
-
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
existing.keyframes = Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
|
|
47
|
+
// deduplicateKeyframes owns the same-% merge (including the easeAmbiguous
|
|
48
|
+
// flag downstream lanes read); a second copy of that rule here is how the
|
|
49
|
+
// two writers drift.
|
|
50
|
+
existing.keyframes = deduplicateKeyframes([...existing.keyframes, ...clipKeyframes]);
|
|
57
51
|
} else {
|
|
58
|
-
merged.set(id, {
|
|
52
|
+
merged.set(id, {
|
|
53
|
+
...anim.keyframes,
|
|
54
|
+
format: anim.keyframes?.format ?? "percentage",
|
|
55
|
+
keyframes: clipKeyframes,
|
|
56
|
+
});
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
for (const [id, entry] of merged) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (targetPath !== "index.html") setKeyframeCache(`index.html#${id}`, entry);
|
|
60
|
+
for (const key of elementCacheKeys(targetPath, id)) setKeyframeCache(key, entry);
|
|
61
|
+
writeGsapAnimationsForElement(targetPath, id, sourceAnimations.get(id));
|
|
65
62
|
}
|
|
66
63
|
const targetId =
|
|
67
|
-
(mutation as { targetSelector?: string }).targetSelector
|
|
68
|
-
selectionId;
|
|
64
|
+
idFromSelector((mutation as { targetSelector?: string }).targetSelector) ?? selectionId;
|
|
69
65
|
if (targetId && !idsWithKeyframes.has(targetId)) {
|
|
70
66
|
clearKeyframeCacheForElement(targetPath, targetId);
|
|
71
67
|
}
|
|
@@ -84,40 +80,85 @@ export function updateKeyframeCacheFromParsed(
|
|
|
84
80
|
* a new cache map and re-render every subscriber.
|
|
85
81
|
*/
|
|
86
82
|
export function clearKeyframeCacheForElement(sourceFile: string, elementId: string): void {
|
|
87
|
-
const { keyframeCache, setKeyframeCache } =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
? [`index.html#${elementId}`, elementId]
|
|
91
|
-
: [`${sourceFile}#${elementId}`, `index.html#${elementId}`, elementId];
|
|
83
|
+
const { keyframeCache, setKeyframeCache, gsapAnimations, setGsapAnimations } =
|
|
84
|
+
usePlayerStore.getState();
|
|
85
|
+
const keys = elementCacheKeys(sourceFile, elementId);
|
|
92
86
|
for (const key of keys) {
|
|
93
87
|
if (keyframeCache.has(key)) setKeyframeCache(key, undefined);
|
|
88
|
+
if (gsapAnimations.has(key)) setGsapAnimations(key, undefined);
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
/**
|
|
98
93
|
* Clear every cached element of `sourceFile` before a full re-scan repopulates
|
|
99
|
-
* it.
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
94
|
+
* it. Only the file's OWN prefixed keys name the ids to clear: every write sets
|
|
95
|
+
* the prefixed key (see elementCacheKeys), so the file's elements are all
|
|
96
|
+
* reachable that way, and clearKeyframeCacheForElement then takes the
|
|
97
|
+
* index.html alias and the bare key with them — an element whose keyframes were
|
|
98
|
+
* removed (and so is absent from the re-scan) leaves no stale bare entry
|
|
99
|
+
* behind. Reading the alias prefix here instead would collect ids owned by
|
|
100
|
+
* OTHER files, and several files re-scan concurrently, so this file's clear
|
|
101
|
+
* would wipe the entries a sibling file had just written.
|
|
103
102
|
*/
|
|
104
103
|
export function clearKeyframeCacheForFile(sourceFile: string): void {
|
|
105
|
-
const { keyframeCache } = usePlayerStore.getState();
|
|
104
|
+
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
|
|
106
105
|
const sfPrefix = `${sourceFile}#`;
|
|
107
|
-
const fallbackPrefix = "index.html#";
|
|
108
106
|
const ids = new Set<string>();
|
|
109
|
-
for (const key of keyframeCache.keys()) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!matchesFile) continue;
|
|
113
|
-
const hashIdx = key.indexOf("#");
|
|
114
|
-
if (hashIdx !== -1) ids.add(key.slice(hashIdx + 1));
|
|
107
|
+
for (const key of [...keyframeCache.keys(), ...gsapAnimations.keys()]) {
|
|
108
|
+
if (!key.startsWith(sfPrefix)) continue;
|
|
109
|
+
ids.add(key.slice(sfPrefix.length));
|
|
115
110
|
}
|
|
116
111
|
for (const id of ids) {
|
|
117
112
|
clearKeyframeCacheForElement(sourceFile, id);
|
|
118
113
|
}
|
|
119
114
|
}
|
|
120
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Drop every cached element owned by a file that is no longer on screen. Each
|
|
118
|
+
* file only ever clears its OWN entries (see clearKeyframeCacheForFile), so
|
|
119
|
+
* switching composition left the previous composition's elements cached forever
|
|
120
|
+
* — 240 entries per switch on a 120-clip comp, in both keyframeCache and
|
|
121
|
+
* gsapAnimations, with nothing to evict them. Called once before a re-scan, with
|
|
122
|
+
* the full set of files that scan covers.
|
|
123
|
+
*/
|
|
124
|
+
export function pruneKeyframeCacheToFiles(files: readonly string[]): void {
|
|
125
|
+
const keep = new Set(files);
|
|
126
|
+
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
|
|
127
|
+
const stale = new Map<string, Set<string>>();
|
|
128
|
+
for (const key of [...keyframeCache.keys(), ...gsapAnimations.keys()]) {
|
|
129
|
+
const hash = key.indexOf("#");
|
|
130
|
+
// Bare-id aliases carry no owner; clearKeyframeCacheForElement takes them
|
|
131
|
+
// with their prefixed key, so skipping them here loses nothing.
|
|
132
|
+
if (hash < 0) continue;
|
|
133
|
+
const sourceFile = key.slice(0, hash);
|
|
134
|
+
if (keep.has(sourceFile)) continue;
|
|
135
|
+
const ids = stale.get(sourceFile) ?? new Set<string>();
|
|
136
|
+
ids.add(key.slice(hash + 1));
|
|
137
|
+
stale.set(sourceFile, ids);
|
|
138
|
+
}
|
|
139
|
+
for (const [sourceFile, ids] of stale) {
|
|
140
|
+
for (const id of ids) clearKeyframeCacheForElement(sourceFile, id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every cache key a write for this element sets, in read-preference order. */
|
|
145
|
+
export function elementCacheKeys(sourceFile: string, elementId: string): string[] {
|
|
146
|
+
return sourceFile === "index.html"
|
|
147
|
+
? [`index.html#${elementId}`, elementId]
|
|
148
|
+
: [`${sourceFile}#${elementId}`, `index.html#${elementId}`, elementId];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function writeGsapAnimationsForElement(
|
|
152
|
+
sourceFile: string,
|
|
153
|
+
elementId: string,
|
|
154
|
+
animations: GsapAnimation[] | undefined,
|
|
155
|
+
): void {
|
|
156
|
+
const { setGsapAnimations } = usePlayerStore.getState();
|
|
157
|
+
for (const key of elementCacheKeys(sourceFile, elementId)) {
|
|
158
|
+
setGsapAnimations(key, animations);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
121
162
|
function buildCacheKey(sourceFile: string, elementId: string): string {
|
|
122
163
|
return `${sourceFile}#${elementId}`;
|
|
123
164
|
}
|
|
@@ -280,3 +280,103 @@ describe("tryGsapDragIntercept — autoKeyframeEnabled toggle (#1808)", () => {
|
|
|
280
280
|
expect(types).not.toContain("replace-with-keyframes");
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
|
+
|
|
284
|
+
describe("tryGsapDragIntercept — motion paths", () => {
|
|
285
|
+
const motionPathAnim = {
|
|
286
|
+
id: "#puck-b-to-12170-position",
|
|
287
|
+
targetSelector: "#puck-b",
|
|
288
|
+
propertyGroup: "position",
|
|
289
|
+
method: "to",
|
|
290
|
+
position: 12.17,
|
|
291
|
+
resolvedStart: 12.17,
|
|
292
|
+
duration: 16.055,
|
|
293
|
+
ease: "power1.inOut",
|
|
294
|
+
properties: {},
|
|
295
|
+
keyframes: {
|
|
296
|
+
keyframes: [
|
|
297
|
+
{ percentage: 0, properties: { x: -184, y: 326 } },
|
|
298
|
+
{ percentage: 50, properties: { x: 416, y: 804 } },
|
|
299
|
+
{ percentage: 100, properties: { x: 796, y: 237 } },
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
arcPath: {
|
|
303
|
+
enabled: true,
|
|
304
|
+
autoRotate: false,
|
|
305
|
+
segments: [{ curviness: 1 }, { curviness: 1 }],
|
|
306
|
+
},
|
|
307
|
+
} as unknown as GsapAnimation;
|
|
308
|
+
const liveTween = {
|
|
309
|
+
targets: () => [{ id: "puck-b" }],
|
|
310
|
+
vars: { motionPath: { path: [] }, duration: 16.055 },
|
|
311
|
+
duration: () => 16.055,
|
|
312
|
+
startTime: () => 12.17,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
async function dragMotionPath(activeKeyframePct: number | null) {
|
|
316
|
+
usePlayerStore.setState({
|
|
317
|
+
autoKeyframeEnabled: true,
|
|
318
|
+
activeKeyframePct,
|
|
319
|
+
currentTime: 15.9,
|
|
320
|
+
});
|
|
321
|
+
const commitMutation = vi.fn();
|
|
322
|
+
const handled = await tryGsapDragIntercept(
|
|
323
|
+
selection,
|
|
324
|
+
{ x: -50, y: 30 },
|
|
325
|
+
[motionPathAnim],
|
|
326
|
+
fakeIframe("puck-b", [liveTween]),
|
|
327
|
+
commitMutation,
|
|
328
|
+
);
|
|
329
|
+
return { commitMutation, handled };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
afterEach(() => {
|
|
333
|
+
usePlayerStore.setState({ activeKeyframePct: null });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("creates a temporal keyframe at the exact playhead instead of redistributing path waypoints", async () => {
|
|
337
|
+
const { commitMutation, handled } = await dragMotionPath(null);
|
|
338
|
+
|
|
339
|
+
expect(handled).toBe(true);
|
|
340
|
+
expect(commitMutation).toHaveBeenCalledWith(
|
|
341
|
+
selection,
|
|
342
|
+
{
|
|
343
|
+
type: "replace-with-keyframes",
|
|
344
|
+
animationId: motionPathAnim.id,
|
|
345
|
+
targetSelector: "#puck-b",
|
|
346
|
+
position: 12.17,
|
|
347
|
+
duration: 16.055,
|
|
348
|
+
keyframes: [
|
|
349
|
+
{ percentage: 0, properties: { x: -184, y: 326 } },
|
|
350
|
+
{ percentage: 23.233, properties: { x: -50, y: 30 } },
|
|
351
|
+
{ percentage: 50, properties: { x: 416, y: 804 } },
|
|
352
|
+
{ percentage: 100, properties: { x: 796, y: 237 } },
|
|
353
|
+
],
|
|
354
|
+
ease: "none",
|
|
355
|
+
},
|
|
356
|
+
expect.objectContaining({ label: "Move layer (new keyframe)", softReload: true }),
|
|
357
|
+
);
|
|
358
|
+
expect(commitMutation.mock.calls.map(([, mutation]) => mutation.type)).not.toContain(
|
|
359
|
+
"add-motion-path-point",
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("keeps an explicitly selected path waypoint as a spatial edit", async () => {
|
|
364
|
+
const { commitMutation, handled } = await dragMotionPath(50);
|
|
365
|
+
|
|
366
|
+
expect(handled).toBe(true);
|
|
367
|
+
expect(commitMutation).toHaveBeenCalledWith(
|
|
368
|
+
selection,
|
|
369
|
+
{
|
|
370
|
+
type: "update-motion-path-point",
|
|
371
|
+
animationId: motionPathAnim.id,
|
|
372
|
+
pointIndex: 1,
|
|
373
|
+
x: -50,
|
|
374
|
+
y: 30,
|
|
375
|
+
},
|
|
376
|
+
expect.objectContaining({ label: "Move layer (waypoint)", softReload: true }),
|
|
377
|
+
);
|
|
378
|
+
expect(commitMutation.mock.calls.map(([, mutation]) => mutation.type)).not.toContain(
|
|
379
|
+
"replace-with-keyframes",
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
@@ -2,12 +2,13 @@ import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mu
|
|
|
2
2
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
3
|
|
|
4
4
|
export { PROPERTY_DEFAULTS } from "./gsapShared";
|
|
5
|
+
import { idSelector } from "./gsapShared";
|
|
5
6
|
|
|
6
7
|
export function ensureElementAddressable(selection: DomEditSelection): {
|
|
7
8
|
selector: string;
|
|
8
9
|
autoId?: string;
|
|
9
10
|
} {
|
|
10
|
-
if (selection.id) return { selector:
|
|
11
|
+
if (selection.id) return { selector: idSelector(selection.id) };
|
|
11
12
|
if (selection.selector) return { selector: selection.selector };
|
|
12
13
|
|
|
13
14
|
const el = selection.element;
|
|
@@ -20,7 +21,7 @@ export function ensureElementAddressable(selection: DomEditSelection): {
|
|
|
20
21
|
id = `${tag}-${n}`;
|
|
21
22
|
}
|
|
22
23
|
el.setAttribute("id", id);
|
|
23
|
-
return { selector:
|
|
24
|
+
return { selector: idSelector(id), autoId: id };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export class GsapMutationHttpError extends Error {
|