@hyperframes/studio 0.7.79 → 0.7.80
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-mFah2TZE.js → hyperframes-player-Csai0_vh.js} +1 -1
- package/dist/assets/{index-Bqj3h_1a.js → index-B5aYTg-4.js} +1 -1
- package/dist/assets/{index-DlZMDyYs.js → index-BhdJN49y.js} +200 -200
- package/dist/assets/{index-hmnoiSEV.js → index-By-YiRxq.js} +1 -1
- package/dist/assets/index-D379YOKT.css +1 -0
- package/dist/index.d.ts +45 -3
- package/dist/index.html +2 -2
- package/dist/index.js +652 -465
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +2 -1
- package/src/components/editor/AnimationCard.test.tsx +225 -18
- package/src/components/editor/AnimationCard.tsx +24 -3
- package/src/components/editor/EaseCurveSection.test.tsx +147 -2
- package/src/components/editor/EaseCurveSection.tsx +77 -15
- package/src/components/editor/GsapAnimationSection.test.tsx +102 -0
- package/src/components/editor/GsapAnimationSection.tsx +6 -1
- package/src/components/editor/KeyframeEaseList.tsx +4 -0
- package/src/components/editor/MotionPathOverlay.tsx +54 -19
- package/src/components/editor/PropertyPanel.tsx +4 -0
- package/src/components/editor/PropertyPanelFlat.tsx +6 -91
- package/src/components/editor/gsapAnimationCallbacks.test.ts +8 -1
- package/src/components/editor/gsapAnimationCallbacks.ts +8 -0
- package/src/components/editor/holdEaseSeek.test.ts +37 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -1
- package/src/components/editor/propertyPanelFlatProps.ts +91 -0
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +132 -43
- package/src/hooks/gsapKeyframeCacheHelpers.ts +150 -50
- package/src/hooks/gsapTweenSynth.test.ts +70 -15
- package/src/hooks/gsapTweenSynth.ts +50 -23
- package/src/hooks/keyframeCacheAstLoad.ts +4 -12
- package/src/hooks/useDomEditSession.test.tsx +152 -3
- package/src/hooks/useDomEditSession.ts +4 -53
- package/src/hooks/useGsapTweenCache.ts +40 -34
- package/src/hooks/useKeyframeEaseCommits.ts +78 -0
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +21 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -12
- package/src/player/components/TimelineClipDiamonds.test.tsx +18 -6
- package/src/player/components/TimelineDiamondConnectors.tsx +118 -85
- package/src/player/components/timelineDiamondTypes.ts +4 -3
- package/src/player/components/timelineKeyframeIdentity.ts +3 -0
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +52 -32
- package/src/player/components/useTimelineKeyframeHandlers.ts +1 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +29 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +24 -0
- package/src/player/store/keyframeSlice.ts +15 -5
- package/dist/assets/index-gGVKuFg5.css +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Real gsap, not a parseEase stub: this pins the seek behaviour of the `hold`
|
|
2
|
+
// ease against the engine that actually runs it, and gsap is a studio
|
|
3
|
+
// dependency. core's own customEase.test.ts covers the resolver in isolation.
|
|
4
|
+
import { installStudioCustomEase } from "@hyperframes/core/runtime/custom-ease";
|
|
5
|
+
import { gsap } from "gsap";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
describe("Studio hold ease", () => {
|
|
9
|
+
it("holds the start value under seek until the destination time", () => {
|
|
10
|
+
const runtimeGsap = { parseEase: gsap.parseEase.bind(gsap) };
|
|
11
|
+
expect(installStudioCustomEase(runtimeGsap)).toBe(true);
|
|
12
|
+
const hold = runtimeGsap.parseEase("hold");
|
|
13
|
+
expect(hold).toBeTypeOf("function");
|
|
14
|
+
if (typeof hold !== "function") return;
|
|
15
|
+
|
|
16
|
+
const target = { value: 0 };
|
|
17
|
+
const timeline = gsap.timeline({ paused: true }).to(
|
|
18
|
+
target,
|
|
19
|
+
{
|
|
20
|
+
value: 100,
|
|
21
|
+
duration: 2,
|
|
22
|
+
ease: hold,
|
|
23
|
+
},
|
|
24
|
+
0,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
timeline.seek(0.5);
|
|
28
|
+
expect(target.value).toBe(0);
|
|
29
|
+
timeline.seek(1);
|
|
30
|
+
expect(target.value).toBe(0);
|
|
31
|
+
timeline.seek(1.99);
|
|
32
|
+
expect(target.value).toBe(0);
|
|
33
|
+
timeline.seek(2);
|
|
34
|
+
expect(target.value).toBe(100);
|
|
35
|
+
timeline.kill();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -5,11 +5,15 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { FlatMotionSection, FlatTimingRow } from "./propertyPanelFlatMotionSection";
|
|
7
7
|
import type { DomEditSelection } from "./domEditing";
|
|
8
|
+
import { usePlayerStore } from "../../player";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
11
|
|
|
11
12
|
afterEach(() => {
|
|
12
13
|
document.body.innerHTML = "";
|
|
14
|
+
// The store is module-global, so a test that parks a focused ease segment
|
|
15
|
+
// would otherwise leak it into every test that runs after it.
|
|
16
|
+
usePlayerStore.getState().reset();
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
function baseElement(overrides: Partial<DomEditSelection> = {}): DomEditSelection {
|
|
@@ -270,3 +274,68 @@ describe("FlatMotionSection", () => {
|
|
|
270
274
|
act(() => root.unmount());
|
|
271
275
|
});
|
|
272
276
|
});
|
|
277
|
+
|
|
278
|
+
it("forwards focused bulk segment easing through the flat animation card", () => {
|
|
279
|
+
const onUpdateKeyframeEase = vi.fn();
|
|
280
|
+
const onUpdateSegmentEase = vi.fn();
|
|
281
|
+
usePlayerStore.setState({
|
|
282
|
+
focusedEaseSegment: {
|
|
283
|
+
elementId: "index.html#hero",
|
|
284
|
+
animationId: "a1",
|
|
285
|
+
tweenPercentage: 50,
|
|
286
|
+
collidingAnimationTargets: [
|
|
287
|
+
{ animationId: "a1", tweenPercentage: 50 },
|
|
288
|
+
{ animationId: "a2", tweenPercentage: 75 },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
const { host, root } = renderInto(
|
|
293
|
+
<FlatMotionSection
|
|
294
|
+
element={baseElement()}
|
|
295
|
+
animations={[
|
|
296
|
+
{
|
|
297
|
+
id: "a1",
|
|
298
|
+
method: "to",
|
|
299
|
+
position: 0,
|
|
300
|
+
duration: 1,
|
|
301
|
+
properties: { x: 100 },
|
|
302
|
+
keyframes: {
|
|
303
|
+
format: "percentage",
|
|
304
|
+
keyframes: [
|
|
305
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
306
|
+
{ percentage: 50, properties: { x: 100 } },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
} as never,
|
|
310
|
+
]}
|
|
311
|
+
showTiming={false}
|
|
312
|
+
showEffects
|
|
313
|
+
onSetAttribute={vi.fn()}
|
|
314
|
+
onAddAnimation={vi.fn()}
|
|
315
|
+
onUpdateProperty={vi.fn()}
|
|
316
|
+
onUpdateMeta={vi.fn()}
|
|
317
|
+
onDeleteAnimation={vi.fn()}
|
|
318
|
+
onAddProperty={vi.fn()}
|
|
319
|
+
onRemoveProperty={vi.fn()}
|
|
320
|
+
onUpdateKeyframeEase={onUpdateKeyframeEase}
|
|
321
|
+
onUpdateSegmentEase={onUpdateSegmentEase}
|
|
322
|
+
/>,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const dropdown = host.querySelector<HTMLButtonElement>("[data-ease-type-dropdown]");
|
|
326
|
+
expect(dropdown).not.toBeNull();
|
|
327
|
+
act(() => dropdown?.click());
|
|
328
|
+
const preset = host.querySelector<HTMLButtonElement>('[data-ease-preset-id="quad-out"]');
|
|
329
|
+
expect(preset).not.toBeNull();
|
|
330
|
+
act(() => preset?.click());
|
|
331
|
+
|
|
332
|
+
expect(onUpdateSegmentEase).toHaveBeenCalledExactlyOnceWith(
|
|
333
|
+
[
|
|
334
|
+
{ animationId: "a1", tweenPercentage: 50 },
|
|
335
|
+
{ animationId: "a2", tweenPercentage: 75 },
|
|
336
|
+
],
|
|
337
|
+
"power2.out",
|
|
338
|
+
);
|
|
339
|
+
expect(onUpdateKeyframeEase).not.toHaveBeenCalled();
|
|
340
|
+
act(() => root.unmount());
|
|
341
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scopedElementKey } from "../../hooks/gsapKeyframeCacheHelpers";
|
|
1
2
|
import { useState } from "react";
|
|
2
3
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
4
|
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
@@ -143,7 +144,7 @@ export function FlatMotionSection({
|
|
|
143
144
|
// the store's selectedElementId, which flips synchronously during async
|
|
144
145
|
// selection resolution), so a shared class-selector animation id can't open
|
|
145
146
|
// the wrong element's editor.
|
|
146
|
-
const renderedElementId =
|
|
147
|
+
const renderedElementId = scopedElementKey(element);
|
|
147
148
|
const focusedHere =
|
|
148
149
|
focusedEaseSegment && focusedEaseSegment.elementId === renderedElementId
|
|
149
150
|
? focusedEaseSegment
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { resolveEditingSections } from "@hyperframes/core/editing";
|
|
2
|
+
import type { DomEditSelection } from "./domEditing";
|
|
3
|
+
import type { PropertyPanelProps } from "./propertyPanelHelpers";
|
|
4
|
+
import type { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
|
5
|
+
|
|
6
|
+
export type PropertyPanelFlatProps = Pick<
|
|
7
|
+
PropertyPanelProps,
|
|
8
|
+
| "projectId"
|
|
9
|
+
| "projectDir"
|
|
10
|
+
| "assets"
|
|
11
|
+
| "previewIframeRef"
|
|
12
|
+
| "onClearSelection"
|
|
13
|
+
| "onUngroup"
|
|
14
|
+
| "onSetStyle"
|
|
15
|
+
| "onPreviewStyle"
|
|
16
|
+
| "onSetAttribute"
|
|
17
|
+
| "onSetAttributes"
|
|
18
|
+
| "onSetAttributeLive"
|
|
19
|
+
| "onApplyColorGradingScope"
|
|
20
|
+
| "onSetHtmlAttribute"
|
|
21
|
+
| "onRemoveBackground"
|
|
22
|
+
| "onSetText"
|
|
23
|
+
| "onSetTextFieldStyle"
|
|
24
|
+
| "onPreviewTextFieldStyle"
|
|
25
|
+
| "onAddTextField"
|
|
26
|
+
| "onRemoveTextField"
|
|
27
|
+
| "onAskAgent"
|
|
28
|
+
| "onToggleElementHidden"
|
|
29
|
+
| "onImportAssets"
|
|
30
|
+
| "onAddMediaOverlay"
|
|
31
|
+
| "onImportFonts"
|
|
32
|
+
| "fontAssets"
|
|
33
|
+
| "gsapAnimations"
|
|
34
|
+
| "gsapMultipleTimelines"
|
|
35
|
+
| "gsapUnsupportedTimelinePattern"
|
|
36
|
+
| "onUpdateGsapProperty"
|
|
37
|
+
| "onUpdateGsapMeta"
|
|
38
|
+
| "onDeleteGsapAnimation"
|
|
39
|
+
| "onAddGsapProperty"
|
|
40
|
+
| "onRemoveGsapProperty"
|
|
41
|
+
| "onUpdateGsapFromProperty"
|
|
42
|
+
| "onAddGsapFromProperty"
|
|
43
|
+
| "onRemoveGsapFromProperty"
|
|
44
|
+
| "onAddGsapAnimation"
|
|
45
|
+
| "onSetArcPath"
|
|
46
|
+
| "onUpdateArcSegment"
|
|
47
|
+
| "onUnroll"
|
|
48
|
+
| "onUpdateKeyframeEase"
|
|
49
|
+
| "onSetAllKeyframeEases"
|
|
50
|
+
| "onUpdateSegmentEase"
|
|
51
|
+
| "recordingState"
|
|
52
|
+
| "recordingDuration"
|
|
53
|
+
| "onToggleRecording"
|
|
54
|
+
> &
|
|
55
|
+
Pick<
|
|
56
|
+
Parameters<typeof FlatLayoutSection>[0],
|
|
57
|
+
| "displayX"
|
|
58
|
+
| "displayY"
|
|
59
|
+
| "displayW"
|
|
60
|
+
| "displayH"
|
|
61
|
+
| "displayR"
|
|
62
|
+
| "manualOffsetEditingDisabled"
|
|
63
|
+
| "manualSizeEditingDisabled"
|
|
64
|
+
| "manualRotationEditingDisabled"
|
|
65
|
+
| "commitManualOffset"
|
|
66
|
+
| "commitManualSize"
|
|
67
|
+
| "commitManualRotation"
|
|
68
|
+
| "gsapAnimId"
|
|
69
|
+
| "navKeyframes"
|
|
70
|
+
| "animIdForProp"
|
|
71
|
+
| "gsapRuntimeValues"
|
|
72
|
+
| "elStart"
|
|
73
|
+
| "elDuration"
|
|
74
|
+
| "onCommitAnimatedProperty"
|
|
75
|
+
| "onCommitAnimatedProperties"
|
|
76
|
+
| "onSeekToTime"
|
|
77
|
+
| "onRemoveKeyframe"
|
|
78
|
+
| "onConvertToKeyframes"
|
|
79
|
+
> & {
|
|
80
|
+
element: DomEditSelection;
|
|
81
|
+
styles: Record<string, string>;
|
|
82
|
+
sections: ReturnType<typeof resolveEditingSections>;
|
|
83
|
+
sourceLabel: string;
|
|
84
|
+
gsapBorderRadius: { tl: number; tr: number; br: number; bl: number } | null;
|
|
85
|
+
showEditableSections: boolean;
|
|
86
|
+
selectedElementHidden: boolean;
|
|
87
|
+
selectedElementId: string | null;
|
|
88
|
+
clipboardCopied: boolean;
|
|
89
|
+
onCopyElementInfo: () => void;
|
|
90
|
+
currentTime: number;
|
|
91
|
+
};
|
|
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
|
|
|
2
2
|
import type { ArcPathSegment, GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
3
3
|
import type { DomEditSelection } from "./domEditing";
|
|
4
4
|
import type { ImportedFontAsset } from "./fontAssets";
|
|
5
|
+
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
|
5
6
|
|
|
6
7
|
export interface BackgroundRemovalProgress {
|
|
7
8
|
status: "processing" | "complete" | "failed";
|
|
@@ -123,6 +124,7 @@ export interface PropertyPanelProps {
|
|
|
123
124
|
onRemoveKeyframe?: (animationId: string, percentage: number) => void;
|
|
124
125
|
onUpdateKeyframeEase?: (animationId: string, percentage: number, ease: string) => void;
|
|
125
126
|
onSetAllKeyframeEases?: (animationId: string, ease: string) => void;
|
|
127
|
+
onUpdateSegmentEase?: NonNullable<GsapAnimationEditCallbacks["onUpdateSegmentEase"]>;
|
|
126
128
|
onConvertToKeyframes?: (animationId: string, duration?: number) => void;
|
|
127
129
|
onCommitAnimatedProperty?: (
|
|
128
130
|
selection: DomEditSelection,
|
|
@@ -71,6 +71,7 @@ export interface DomEditActionsValue extends Pick<
|
|
|
71
71
|
| "commitMutation"
|
|
72
72
|
| "applyMarqueeSelection"
|
|
73
73
|
| "handleUpdateKeyframeEase"
|
|
74
|
+
| "handleUpdateSegmentEase"
|
|
74
75
|
| "handleSetAllKeyframeEases"
|
|
75
76
|
> {}
|
|
76
77
|
|
|
@@ -200,6 +201,7 @@ export function DomEditProvider({
|
|
|
200
201
|
applyMarqueeSelection,
|
|
201
202
|
handleUpdateKeyframeEase,
|
|
202
203
|
handleSetAllKeyframeEases,
|
|
204
|
+
handleUpdateSegmentEase,
|
|
203
205
|
},
|
|
204
206
|
children,
|
|
205
207
|
}: {
|
|
@@ -281,6 +283,7 @@ export function DomEditProvider({
|
|
|
281
283
|
commitMutation: stableCommitMutation,
|
|
282
284
|
applyMarqueeSelection,
|
|
283
285
|
handleUpdateKeyframeEase,
|
|
286
|
+
handleUpdateSegmentEase,
|
|
284
287
|
handleSetAllKeyframeEases,
|
|
285
288
|
}),
|
|
286
289
|
[
|
|
@@ -349,6 +352,7 @@ export function DomEditProvider({
|
|
|
349
352
|
stableCommitMutation,
|
|
350
353
|
applyMarqueeSelection,
|
|
351
354
|
handleUpdateKeyframeEase,
|
|
355
|
+
handleUpdateSegmentEase,
|
|
352
356
|
handleSetAllKeyframeEases,
|
|
353
357
|
],
|
|
354
358
|
);
|
|
@@ -3,8 +3,8 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
3
3
|
import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
|
|
4
4
|
import {
|
|
5
5
|
clearKeyframeCacheForElement,
|
|
6
|
-
clearKeyframeCacheForFile,
|
|
7
6
|
pruneKeyframeCacheToFiles,
|
|
7
|
+
replaceKeyframeCacheForFile,
|
|
8
8
|
updateKeyframeCacheFromParsed,
|
|
9
9
|
} from "./gsapKeyframeCacheHelpers";
|
|
10
10
|
|
|
@@ -70,48 +70,6 @@ describe("clearKeyframeCacheForElement", () => {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
describe("clearKeyframeCacheForFile", () => {
|
|
74
|
-
it("clears the prefixed, fallback, and bare keys for every element of the file", () => {
|
|
75
|
-
seed("comp.html#a");
|
|
76
|
-
seed("index.html#a");
|
|
77
|
-
seed("a");
|
|
78
|
-
seed("comp.html#b");
|
|
79
|
-
seed("b");
|
|
80
|
-
|
|
81
|
-
clearKeyframeCacheForFile("comp.html");
|
|
82
|
-
|
|
83
|
-
for (const key of ["comp.html#a", "index.html#a", "a", "comp.html#b", "b"]) {
|
|
84
|
-
expect(cache().has(key)).toBe(false);
|
|
85
|
-
}
|
|
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
|
-
|
|
102
|
-
it("leaves entries that belong to a different source file", () => {
|
|
103
|
-
seed("comp.html#a");
|
|
104
|
-
seed("a");
|
|
105
|
-
seed("other.html#z");
|
|
106
|
-
seed("z");
|
|
107
|
-
|
|
108
|
-
clearKeyframeCacheForFile("comp.html");
|
|
109
|
-
|
|
110
|
-
expect(cache().has("other.html#z")).toBe(true);
|
|
111
|
-
expect(cache().has("z")).toBe(true);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
73
|
describe("pruneKeyframeCacheToFiles", () => {
|
|
116
74
|
// Switching composition leaves the previous comp's elements cached with no
|
|
117
75
|
// owner left to clear them: each file only ever clears its own entries.
|
|
@@ -152,9 +110,140 @@ describe("pruneKeyframeCacheToFiles", () => {
|
|
|
152
110
|
expect(cache().has("index.html#hero")).toBe(true);
|
|
153
111
|
expect(cache().has("comp.html#a")).toBe(true);
|
|
154
112
|
});
|
|
113
|
+
|
|
114
|
+
// The prune runs immediately before the atomic repopulate on every
|
|
115
|
+
// composition switch. One notification per stale element put every subscriber
|
|
116
|
+
// through a cache that was progressively emptier, which is the flash the
|
|
117
|
+
// atomic repopulate exists to avoid.
|
|
118
|
+
it("drops every stale file in one notification", () => {
|
|
119
|
+
seed("old.html#a");
|
|
120
|
+
seed("old.html#b");
|
|
121
|
+
seed("older.html#c");
|
|
122
|
+
seed("kept.html#k");
|
|
123
|
+
let notifications = 0;
|
|
124
|
+
const unsubscribe = usePlayerStore.subscribe(() => {
|
|
125
|
+
notifications += 1;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
pruneKeyframeCacheToFiles(["kept.html"]);
|
|
129
|
+
unsubscribe();
|
|
130
|
+
|
|
131
|
+
expect(notifications).toBe(1);
|
|
132
|
+
expect([...cache().keys()]).toEqual(["kept.html#k"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// A prune that finds nothing stale must not hand subscribers a fresh Map
|
|
136
|
+
// identity: every keyframe consumer re-renders on cache identity alone.
|
|
137
|
+
it("does not publish when nothing is stale", () => {
|
|
138
|
+
seed("kept.html#k");
|
|
139
|
+
const before = cache();
|
|
140
|
+
let notifications = 0;
|
|
141
|
+
const unsubscribe = usePlayerStore.subscribe(() => {
|
|
142
|
+
notifications += 1;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
pruneKeyframeCacheToFiles(["kept.html"]);
|
|
146
|
+
unsubscribe();
|
|
147
|
+
|
|
148
|
+
expect(notifications).toBe(0);
|
|
149
|
+
expect(cache()).toBe(before);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("replaceKeyframeCacheForFile", () => {
|
|
154
|
+
it("publishes complete keyframe and animation maps in one notification", () => {
|
|
155
|
+
const staleEntry = entry();
|
|
156
|
+
const otherEntry = entry();
|
|
157
|
+
const staleAnimation = animWithKeyframes("stale");
|
|
158
|
+
const freshAnimation = animWithKeyframes("fresh");
|
|
159
|
+
usePlayerStore.setState({
|
|
160
|
+
keyframeCache: new Map([
|
|
161
|
+
["scene.html#stale", staleEntry],
|
|
162
|
+
["index.html#stale", staleEntry],
|
|
163
|
+
["stale", staleEntry],
|
|
164
|
+
["other.html#other", otherEntry],
|
|
165
|
+
["other", otherEntry],
|
|
166
|
+
]),
|
|
167
|
+
gsapAnimations: new Map([
|
|
168
|
+
["scene.html#stale", [staleAnimation]],
|
|
169
|
+
["index.html#stale", [staleAnimation]],
|
|
170
|
+
["stale", [staleAnimation]],
|
|
171
|
+
["other.html#other", [staleAnimation]],
|
|
172
|
+
["other", [staleAnimation]],
|
|
173
|
+
]),
|
|
174
|
+
});
|
|
175
|
+
const snapshots: Array<{ cacheKeys: string[]; animationKeys: string[] }> = [];
|
|
176
|
+
const unsubscribe = usePlayerStore.subscribe((state) => {
|
|
177
|
+
snapshots.push({
|
|
178
|
+
cacheKeys: [...state.keyframeCache.keys()].sort(),
|
|
179
|
+
animationKeys: [...state.gsapAnimations.keys()].sort(),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
replaceKeyframeCacheForFile(
|
|
184
|
+
"scene.html",
|
|
185
|
+
new Map([["fresh", entry()]]),
|
|
186
|
+
new Map([["fresh", [freshAnimation]]]),
|
|
187
|
+
);
|
|
188
|
+
unsubscribe();
|
|
189
|
+
|
|
190
|
+
expect(snapshots).toEqual([
|
|
191
|
+
{
|
|
192
|
+
cacheKeys: ["fresh", "index.html#fresh", "other", "other.html#other", "scene.html#fresh"],
|
|
193
|
+
animationKeys: [
|
|
194
|
+
"fresh",
|
|
195
|
+
"index.html#fresh",
|
|
196
|
+
"other",
|
|
197
|
+
"other.html#other",
|
|
198
|
+
"scene.html#fresh",
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#fresh")).toEqual([
|
|
203
|
+
freshAnimation,
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
155
206
|
});
|
|
156
207
|
|
|
157
208
|
describe("updateKeyframeCacheFromParsed", () => {
|
|
209
|
+
it("records colliding animation targets with their own tween percentages", () => {
|
|
210
|
+
const animation = (
|
|
211
|
+
id: string,
|
|
212
|
+
propertyGroup: string,
|
|
213
|
+
properties: Record<string, number>,
|
|
214
|
+
percentage: number,
|
|
215
|
+
resolvedStart: number,
|
|
216
|
+
): GsapAnimation => ({
|
|
217
|
+
...animWithKeyframes(id),
|
|
218
|
+
targetSelector: "#hero",
|
|
219
|
+
propertyGroup,
|
|
220
|
+
resolvedStart,
|
|
221
|
+
keyframes: { format: "percentage", keyframes: [{ percentage, properties }] },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
usePlayerStore.setState({
|
|
225
|
+
elements: [{ id: "hero", domId: "hero", tag: "div", start: 0, duration: 4, track: 0 }],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
updateKeyframeCacheFromParsed(
|
|
229
|
+
[
|
|
230
|
+
animation("hero-position", "position", { x: 100 }, 50, 0.5),
|
|
231
|
+
animation("hero-visual", "visual", { opacity: 1 }, 80, 0.2),
|
|
232
|
+
animation("hero-position", "position", { y: 50 }, 25, 0.75),
|
|
233
|
+
animation("hero-scale", "scale", { scale: 2 }, 60, 0.4),
|
|
234
|
+
],
|
|
235
|
+
"scene.html",
|
|
236
|
+
"hero",
|
|
237
|
+
{},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(cache().get("scene.html#hero")?.keyframes[0]?.collidingAnimationTargets).toEqual([
|
|
241
|
+
{ animationId: "hero-position", tweenPercentage: 50 },
|
|
242
|
+
{ animationId: "hero-visual", tweenPercentage: 80 },
|
|
243
|
+
{ animationId: "hero-scale", tweenPercentage: 60 },
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
158
247
|
it("serializes a multi-keyframe tween with a stable shape and animation identity", () => {
|
|
159
248
|
const animation: GsapAnimation = {
|
|
160
249
|
...animWithKeyframes("hero"),
|