@hyperframes/studio 0.7.89 → 0.7.92
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-DVTnBvSO.js → hyperframes-player-9qOa08Tv.js} +1 -1
- package/dist/assets/{index-Qvx7boWH.js → index-Bw4lNdtO.js} +1 -1
- package/dist/assets/{index-ulQEdWKo.js → index-CDTN2ZVa.js} +1 -1
- package/dist/assets/index-DIntkrQl.js +428 -0
- package/dist/index.d.ts +20 -16
- package/dist/index.html +1 -1
- package/dist/index.js +6495 -5429
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelTypes.ts +1 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/Timeline.test.ts +2 -3
- package/src/player/components/Timeline.tsx +47 -48
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.tsx +6 -3
- package/src/player/components/TimelineClipDiamonds.test.tsx +442 -18
- package/src/player/components/TimelineClipDiamonds.tsx +66 -211
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +0 -1
- package/src/player/components/TimelineLanes.tsx +3 -2
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +5 -15
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineLaneProps.ts +0 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +4 -4
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +11 -4
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/playerStore.test.ts +76 -0
- package/src/player/store/playerStore.ts +19 -11
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-CddKwfQ1.js +0 -428
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.92",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"gsap": "^3.13.0",
|
|
48
48
|
"marked": "^14.1.4",
|
|
49
49
|
"mediabunny": "^1.45.3",
|
|
50
|
-
"@hyperframes/core": "0.7.
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/sdk": "0.7.
|
|
53
|
-
"@hyperframes/
|
|
54
|
-
"@hyperframes/
|
|
50
|
+
"@hyperframes/core": "0.7.92",
|
|
51
|
+
"@hyperframes/player": "0.7.92",
|
|
52
|
+
"@hyperframes/sdk": "0.7.92",
|
|
53
|
+
"@hyperframes/studio-server": "0.7.92",
|
|
54
|
+
"@hyperframes/parsers": "0.7.92"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "19",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"vite": "^6.4.2",
|
|
67
67
|
"vitest": "^3.2.4",
|
|
68
68
|
"zustand": "^5.0.0",
|
|
69
|
-
"@hyperframes/producer": "0.7.
|
|
69
|
+
"@hyperframes/producer": "0.7.92"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"react": "19",
|
|
@@ -401,8 +401,8 @@ export function StudioRightPanel({
|
|
|
401
401
|
onUpdateArcSegment={handleUpdateArcSegment}
|
|
402
402
|
onUnroll={handleUnroll}
|
|
403
403
|
onUpdateKeyframeEase={handleUpdateKeyframeEase}
|
|
404
|
-
onSetAllKeyframeEases={handleSetAllKeyframeEases}
|
|
405
404
|
onUpdateSegmentEase={handleUpdateSegmentEase}
|
|
405
|
+
onSetAllKeyframeEases={handleSetAllKeyframeEases}
|
|
406
406
|
recordingState={recordingState}
|
|
407
407
|
recordingDuration={recordingDuration}
|
|
408
408
|
onToggleRecording={onToggleRecording}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import { useShallow } from "zustand/react/shallow";
|
|
4
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
5
|
+
import { usePlayerStore } from "../../player";
|
|
6
|
+
import { isFocusedEaseRequestCurrent } from "../../player/store/keyframeSlice";
|
|
7
|
+
import { AnimationCard } from "./AnimationCard";
|
|
8
|
+
import { GsapAddAnimationControl } from "./GsapAddAnimationControl";
|
|
9
|
+
import {
|
|
10
|
+
type GsapAnimationEditCallbacks,
|
|
11
|
+
withTrackedGsapAnimationCallbacks,
|
|
12
|
+
} from "./gsapAnimationCallbacks";
|
|
13
|
+
|
|
14
|
+
interface GsapAnimationListProps extends GsapAnimationEditCallbacks {
|
|
15
|
+
elementId: string;
|
|
16
|
+
animations: GsapAnimation[];
|
|
17
|
+
onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void;
|
|
18
|
+
variant: "classic" | "flat";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Shared animation cards, telemetry, and timeline-ease focus ownership for both inspectors. */
|
|
22
|
+
export function GsapAnimationList({
|
|
23
|
+
elementId,
|
|
24
|
+
animations,
|
|
25
|
+
onAddAnimation,
|
|
26
|
+
variant,
|
|
27
|
+
...callbacks
|
|
28
|
+
}: GsapAnimationListProps) {
|
|
29
|
+
const track = useTrackDesignInput();
|
|
30
|
+
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
31
|
+
const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
|
|
32
|
+
const { focusedEaseSegment, timelineProjectId, timelineSessionEpoch, selectedElementId } =
|
|
33
|
+
usePlayerStore(
|
|
34
|
+
useShallow((state) => ({
|
|
35
|
+
focusedEaseSegment: state.focusedEaseSegment,
|
|
36
|
+
timelineProjectId: state.timelineProjectId,
|
|
37
|
+
timelineSessionEpoch: state.timelineSessionEpoch,
|
|
38
|
+
selectedElementId: state.selectedElementId,
|
|
39
|
+
})),
|
|
40
|
+
);
|
|
41
|
+
const focusedHere =
|
|
42
|
+
focusedEaseSegment &&
|
|
43
|
+
focusedEaseSegment.elementId === elementId &&
|
|
44
|
+
isFocusedEaseRequestCurrent(focusedEaseSegment, {
|
|
45
|
+
timelineProjectId,
|
|
46
|
+
timelineSessionEpoch,
|
|
47
|
+
selectedElementId,
|
|
48
|
+
}) &&
|
|
49
|
+
animations.some((animation) => animation.id === focusedEaseSegment.animationId)
|
|
50
|
+
? focusedEaseSegment
|
|
51
|
+
: null;
|
|
52
|
+
// Stable while the request is unchanged: AnimationCard includes this callback
|
|
53
|
+
// in its focus-effect deps, and a fresh closure would replay that effect on
|
|
54
|
+
// unrelated inspector renders. Reading the action lazily also avoids a store
|
|
55
|
+
// subscription for a function whose identity never changes.
|
|
56
|
+
const consumeFocusedEaseSegment = useCallback(() => {
|
|
57
|
+
if (focusedHere) {
|
|
58
|
+
usePlayerStore.getState().clearFocusedEaseSegment(focusedHere.nonce);
|
|
59
|
+
}
|
|
60
|
+
}, [focusedHere]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-2">
|
|
64
|
+
{animations.map((animation, index) => (
|
|
65
|
+
<AnimationCard
|
|
66
|
+
{...trackedCallbacks}
|
|
67
|
+
key={animation.id}
|
|
68
|
+
animation={animation}
|
|
69
|
+
defaultExpanded={index === 0}
|
|
70
|
+
flat={variant === "flat"}
|
|
71
|
+
focusedSegment={focusedHere?.animationId === animation.id ? focusedHere : null}
|
|
72
|
+
onFocusSegmentConsumed={consumeFocusedEaseSegment}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
<GsapAddAnimationControl
|
|
76
|
+
open={addMenuOpen}
|
|
77
|
+
setOpen={setAddMenuOpen}
|
|
78
|
+
onAddAnimation={onAddAnimation}
|
|
79
|
+
track={track}
|
|
80
|
+
variant={variant}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -5,11 +5,15 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
7
7
|
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
8
|
-
import { usePlayerStore } from "../../player";
|
|
8
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
9
9
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
10
10
|
|
|
11
11
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
12
|
|
|
13
|
+
const animationCardMock = vi.hoisted(() => ({
|
|
14
|
+
consumers: [] as Array<{ tweenPercentage: number | null; consume: () => void }>,
|
|
15
|
+
}));
|
|
16
|
+
|
|
13
17
|
vi.mock("./AnimationCard", () => ({
|
|
14
18
|
AnimationCard: ({
|
|
15
19
|
animation,
|
|
@@ -19,22 +23,27 @@ vi.mock("./AnimationCard", () => ({
|
|
|
19
23
|
animation: GsapAnimation;
|
|
20
24
|
focusedSegment: { tweenPercentage: number } | null;
|
|
21
25
|
onFocusSegmentConsumed: () => void;
|
|
22
|
-
}) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
}) => {
|
|
27
|
+
animationCardMock.consumers.push({
|
|
28
|
+
tweenPercentage: focusedSegment?.tweenPercentage ?? null,
|
|
29
|
+
consume: onFocusSegmentConsumed,
|
|
30
|
+
});
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
data-testid={`animation-${animation.id}`}
|
|
35
|
+
data-focused={focusedSegment ? String(focusedSegment.tweenPercentage) : ""}
|
|
36
|
+
onClick={onFocusSegmentConsumed}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
32
40
|
}));
|
|
33
41
|
|
|
34
42
|
vi.mock("./GsapAddAnimationControl", () => ({ GsapAddAnimationControl: () => null }));
|
|
35
43
|
|
|
36
44
|
afterEach(() => {
|
|
37
45
|
document.body.innerHTML = "";
|
|
46
|
+
animationCardMock.consumers = [];
|
|
38
47
|
usePlayerStore.getState().reset();
|
|
39
48
|
});
|
|
40
49
|
|
|
@@ -76,8 +85,22 @@ function renderSection(elementId: string) {
|
|
|
76
85
|
return { host, root, render };
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
function focusSharedAnimation(elementId: string, tweenPercentage: number) {
|
|
89
|
+
const store = usePlayerStore.getState();
|
|
90
|
+
store.beginTimelineSession("project-a");
|
|
91
|
+
store.setSelectedElementId(elementId);
|
|
92
|
+
store.setFocusedEaseSegment({
|
|
93
|
+
elementId,
|
|
94
|
+
animationId: sharedAnimation.id,
|
|
95
|
+
tweenPercentage,
|
|
96
|
+
});
|
|
97
|
+
return store;
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
describe("GsapAnimationSection", () => {
|
|
80
101
|
it("consumes a shared animation id only for the focused element", () => {
|
|
102
|
+
usePlayerStore.getState().beginTimelineSession("project-a");
|
|
103
|
+
usePlayerStore.getState().setSelectedElementId("index.html#second");
|
|
81
104
|
usePlayerStore.getState().setFocusedEaseSegment({
|
|
82
105
|
elementId: "index.html#second",
|
|
83
106
|
animationId: sharedAnimation.id,
|
|
@@ -99,4 +122,70 @@ describe("GsapAnimationSection", () => {
|
|
|
99
122
|
expect(usePlayerStore.getState().focusedEaseSegment).toBeNull();
|
|
100
123
|
act(() => view.root.unmount());
|
|
101
124
|
});
|
|
125
|
+
|
|
126
|
+
it("does not let an older consumer clear a newer request", () => {
|
|
127
|
+
const store = focusSharedAnimation("index.html#first", 25);
|
|
128
|
+
const view = renderSection("index.html#first");
|
|
129
|
+
const staleConsumer = animationCardMock.consumers.at(-1)?.consume;
|
|
130
|
+
if (!staleConsumer) throw new Error("expected first focus consumer");
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
store.setFocusedEaseSegment({
|
|
134
|
+
elementId: "index.html#first",
|
|
135
|
+
animationId: sharedAnimation.id,
|
|
136
|
+
tweenPercentage: 75,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
const current = usePlayerStore.getState().focusedEaseSegment;
|
|
140
|
+
if (!current) throw new Error("expected replacement request");
|
|
141
|
+
|
|
142
|
+
act(() => staleConsumer());
|
|
143
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toBe(current);
|
|
144
|
+
|
|
145
|
+
const currentConsumer = animationCardMock.consumers.at(-1)?.consume;
|
|
146
|
+
if (!currentConsumer) throw new Error("expected replacement focus consumer");
|
|
147
|
+
act(() => currentConsumer());
|
|
148
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toBeNull();
|
|
149
|
+
act(() => view.root.unmount());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("keeps the focus consumer stable across unrelated parent renders", () => {
|
|
153
|
+
focusSharedAnimation("index.html#first", 25);
|
|
154
|
+
const view = renderSection("index.html#first");
|
|
155
|
+
const firstConsumer = animationCardMock.consumers.at(-1)?.consume;
|
|
156
|
+
if (!firstConsumer) throw new Error("expected focus consumer");
|
|
157
|
+
|
|
158
|
+
view.render("index.html#first");
|
|
159
|
+
|
|
160
|
+
expect(animationCardMock.consumers.at(-1)?.consume).toBe(firstConsumer);
|
|
161
|
+
act(() => view.root.unmount());
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("rejects a request from an earlier project session", () => {
|
|
165
|
+
const store = usePlayerStore.getState();
|
|
166
|
+
store.beginTimelineSession("project-a");
|
|
167
|
+
store.setSelectedElementId("index.html#first");
|
|
168
|
+
store.setFocusedEaseSegment({
|
|
169
|
+
elementId: "index.html#first",
|
|
170
|
+
animationId: sharedAnimation.id,
|
|
171
|
+
tweenPercentage: 50,
|
|
172
|
+
});
|
|
173
|
+
const staleRequest = usePlayerStore.getState().focusedEaseSegment;
|
|
174
|
+
if (!staleRequest) throw new Error("expected request");
|
|
175
|
+
|
|
176
|
+
const view = renderSection("index.html#first");
|
|
177
|
+
|
|
178
|
+
act(() => {
|
|
179
|
+
store.beginTimelineSession("project-b");
|
|
180
|
+
store.setSelectedElementId("index.html#first");
|
|
181
|
+
usePlayerStore.setState({ focusedEaseSegment: staleRequest });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const card = view.host.querySelector<HTMLButtonElement>(
|
|
185
|
+
"[data-testid='animation-shared-animation']",
|
|
186
|
+
);
|
|
187
|
+
expect(card?.dataset.focused).toBe("");
|
|
188
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toBe(staleRequest);
|
|
189
|
+
act(() => view.root.unmount());
|
|
190
|
+
});
|
|
102
191
|
});
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import { memo
|
|
1
|
+
import { memo } from "react";
|
|
2
2
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
3
|
import { Film } from "../../icons/SystemIcons";
|
|
4
4
|
import { Section } from "./propertyPanelPrimitives";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
type GsapAnimationEditCallbacks,
|
|
8
|
-
withTrackedGsapAnimationCallbacks,
|
|
9
|
-
clearFocusedEaseSegment,
|
|
10
|
-
} from "./gsapAnimationCallbacks";
|
|
11
|
-
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
12
|
-
import { usePlayerStore } from "../../player";
|
|
13
|
-
import { GsapAddAnimationControl } from "./GsapAddAnimationControl";
|
|
5
|
+
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
|
6
|
+
import { GsapAnimationList } from "./GsapAnimationList";
|
|
14
7
|
|
|
15
8
|
interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
|
|
16
9
|
elementId: string;
|
|
@@ -21,18 +14,13 @@ interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
|
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
24
|
-
animations,
|
|
25
17
|
elementId,
|
|
18
|
+
animations,
|
|
26
19
|
multipleTimelines,
|
|
27
20
|
unsupportedTimelinePattern,
|
|
28
21
|
onAddAnimation,
|
|
29
22
|
...callbacks
|
|
30
23
|
}: GsapAnimationSectionProps) {
|
|
31
|
-
const track = useTrackDesignInput();
|
|
32
|
-
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
33
|
-
const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
|
|
34
|
-
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
35
|
-
|
|
36
24
|
return (
|
|
37
25
|
<Section title="Animation" icon={<Film size={15} />}>
|
|
38
26
|
{multipleTimelines && (
|
|
@@ -49,31 +37,13 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
49
37
|
</p>
|
|
50
38
|
)}
|
|
51
39
|
{multipleTimelines || unsupportedTimelinePattern ? null : (
|
|
52
|
-
<
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
focusedSegment={
|
|
60
|
-
focusedEaseSegment?.elementId === elementId &&
|
|
61
|
-
focusedEaseSegment.animationId === anim.id
|
|
62
|
-
? focusedEaseSegment
|
|
63
|
-
: null
|
|
64
|
-
}
|
|
65
|
-
onFocusSegmentConsumed={clearFocusedEaseSegment}
|
|
66
|
-
/>
|
|
67
|
-
))}
|
|
68
|
-
|
|
69
|
-
<GsapAddAnimationControl
|
|
70
|
-
open={addMenuOpen}
|
|
71
|
-
setOpen={setAddMenuOpen}
|
|
72
|
-
onAddAnimation={onAddAnimation}
|
|
73
|
-
track={track}
|
|
74
|
-
variant="classic"
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
40
|
+
<GsapAnimationList
|
|
41
|
+
{...callbacks}
|
|
42
|
+
elementId={elementId}
|
|
43
|
+
animations={animations}
|
|
44
|
+
onAddAnimation={onAddAnimation}
|
|
45
|
+
variant="classic"
|
|
46
|
+
/>
|
|
77
47
|
)}
|
|
78
48
|
</Section>
|
|
79
49
|
);
|
|
@@ -570,8 +570,8 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
570
570
|
onUpdateArcSegment={onUpdateArcSegment}
|
|
571
571
|
onUnroll={onUnroll}
|
|
572
572
|
onUpdateKeyframeEase={onUpdateKeyframeEase}
|
|
573
|
-
onSetAllKeyframeEases={onSetAllKeyframeEases}
|
|
574
573
|
onUpdateSegmentEase={onUpdateSegmentEase}
|
|
574
|
+
onSetAllKeyframeEases={onSetAllKeyframeEases}
|
|
575
575
|
/>
|
|
576
576
|
)}
|
|
577
577
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scopedElementKey } from "../../hooks/gsapKeyframeCacheHelpers";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useShallow } from "zustand/react/shallow";
|
|
3
4
|
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
4
5
|
import { slugifyDesignInput } from "../../utils/designInputTracking";
|
|
5
6
|
import { isTextEditableSelection } from "./domEditing";
|
|
@@ -18,6 +19,7 @@ import { createGsapLivePreview } from "./gsapLivePreview";
|
|
|
18
19
|
import { formatTextFieldPreview } from "./propertyPanelSections";
|
|
19
20
|
import { useColorGradingController } from "./useColorGradingController";
|
|
20
21
|
import { usePlayerStore } from "../../player";
|
|
22
|
+
import { isFocusedEaseRequestCurrent } from "../../player/store/keyframeSlice";
|
|
21
23
|
import {
|
|
22
24
|
FlatColorGradingAccessory,
|
|
23
25
|
FlatColorGradingSection,
|
|
@@ -31,24 +33,10 @@ import {
|
|
|
31
33
|
deriveMediaOverlayPlacement,
|
|
32
34
|
FlatOverlaysSection,
|
|
33
35
|
} from "./propertyPanelFlatOverlaysSection";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
summary?: string;
|
|
39
|
-
accessory?: ReactNode;
|
|
40
|
-
content: ReactNode;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Required callback shape for the gated-off Motion effect list.
|
|
44
|
-
const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
45
|
-
onAddAnimation: () => {},
|
|
46
|
-
onUpdateProperty: () => {},
|
|
47
|
-
onUpdateMeta: () => {},
|
|
48
|
-
onDeleteAnimation: () => {},
|
|
49
|
-
onAddProperty: () => {},
|
|
50
|
-
onRemoveProperty: () => {},
|
|
51
|
-
};
|
|
36
|
+
import {
|
|
37
|
+
EMPTY_GSAP_EFFECT_HANDLERS,
|
|
38
|
+
type FlatGroupDescriptor,
|
|
39
|
+
} from "./propertyPanelFlatDescriptors";
|
|
52
40
|
|
|
53
41
|
/** The flat inspector shell with one shared open-group state. */
|
|
54
42
|
// fallow-ignore-next-line complexity
|
|
@@ -161,11 +149,18 @@ export function PropertyPanelFlat({
|
|
|
161
149
|
// When the inline timeline ease button focuses a segment on this element,
|
|
162
150
|
// force the Motion group open so its AnimationCard (which only mounts while
|
|
163
151
|
// the group is expanded) can consume the focus and reveal the ease editor.
|
|
164
|
-
const focusedEaseSegment = usePlayerStore(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
152
|
+
const { focusedEaseSegment, timelineProjectId, timelineSessionEpoch } = usePlayerStore(
|
|
153
|
+
useShallow((state) => ({
|
|
154
|
+
focusedEaseSegment: state.focusedEaseSegment,
|
|
155
|
+
timelineProjectId: state.timelineProjectId,
|
|
156
|
+
timelineSessionEpoch: state.timelineSessionEpoch,
|
|
157
|
+
})),
|
|
158
|
+
);
|
|
159
|
+
// Identity of the element THIS panel actually renders (not the store's
|
|
160
|
+
// selectedElementId, which flips synchronously on selection while the panel
|
|
161
|
+
// still renders the previous element during async DOM-selection resolution):
|
|
162
|
+
// a stale panel would otherwise consume a focus request meant for its
|
|
163
|
+
// successor when both share a class-selector animation id.
|
|
169
164
|
const renderedElementId = scopedElementKey(element);
|
|
170
165
|
// Adjusted during render (not an effect) so the card mounts on the same
|
|
171
166
|
// commit the request lands on. Keyed on request identity: a group the user
|
|
@@ -174,8 +169,16 @@ export function PropertyPanelFlat({
|
|
|
174
169
|
if (focusedEaseSegment !== consumedFocus) {
|
|
175
170
|
setConsumedFocus(focusedEaseSegment);
|
|
176
171
|
const focusesThisPanel =
|
|
177
|
-
focusedEaseSegment
|
|
178
|
-
|
|
172
|
+
focusedEaseSegment !== null &&
|
|
173
|
+
// A request from a previous project/session/selection is stale: it must
|
|
174
|
+
// not reopen Motion on whichever panel happens to be mounted now.
|
|
175
|
+
isFocusedEaseRequestCurrent(focusedEaseSegment, {
|
|
176
|
+
timelineProjectId,
|
|
177
|
+
timelineSessionEpoch,
|
|
178
|
+
selectedElementId,
|
|
179
|
+
}) &&
|
|
180
|
+
focusedEaseSegment.elementId === renderedElementId &&
|
|
181
|
+
gsapAnimations.some((animation) => animation.id === focusedEaseSegment.animationId);
|
|
179
182
|
if (focusesThisPanel) setOpenGroupId("motion");
|
|
180
183
|
}
|
|
181
184
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ArcPathSegment } from "@hyperframes/parsers/gsap-parser";
|
|
2
|
-
import { usePlayerStore } from "../../player";
|
|
3
2
|
import type { AnimationKeyframeTarget } from "../../hooks/gsapTweenSynth";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -164,12 +163,3 @@ export function withTrackedGsapAnimationCallbacks(
|
|
|
164
163
|
: undefined,
|
|
165
164
|
};
|
|
166
165
|
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Stable consumer for the store's one-shot ease-focus request. Module-level on
|
|
170
|
-
* purpose: an inline arrow in the section components is a dep of AnimationCard's
|
|
171
|
-
* focus effect, so a fresh identity each render re-runs that effect every render.
|
|
172
|
-
*/
|
|
173
|
-
export function clearFocusedEaseSegment(): void {
|
|
174
|
-
usePlayerStore.getState().setFocusedEaseSegment(null);
|
|
175
|
-
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type FlatGroupDescriptor = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
accessory?: ReactNode;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// FlatMotionSection never calls these while effect cards are hidden; they only
|
|
12
|
+
// provide its required callback shape on the gated-off path.
|
|
13
|
+
export const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
14
|
+
onAddAnimation: () => {},
|
|
15
|
+
onUpdateProperty: () => {},
|
|
16
|
+
onUpdateMeta: () => {},
|
|
17
|
+
onDeleteAnimation: () => {},
|
|
18
|
+
onAddProperty: () => {},
|
|
19
|
+
onRemoveProperty: () => {},
|
|
20
|
+
};
|
|
@@ -273,13 +273,14 @@ describe("FlatMotionSection", () => {
|
|
|
273
273
|
expect(buttons().some((b) => b.textContent === "+ Add effect")).toBe(true);
|
|
274
274
|
act(() => root.unmount());
|
|
275
275
|
});
|
|
276
|
-
});
|
|
277
276
|
|
|
278
|
-
it("forwards focused bulk segment easing through the flat animation card", () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
it("forwards focused bulk segment easing through the flat animation card", () => {
|
|
278
|
+
const onUpdateKeyframeEase = vi.fn();
|
|
279
|
+
const onUpdateSegmentEase = vi.fn();
|
|
280
|
+
const store = usePlayerStore.getState();
|
|
281
|
+
store.beginTimelineSession("project-a");
|
|
282
|
+
store.setSelectedElementId("index.html#hero");
|
|
283
|
+
store.setFocusedEaseSegment({
|
|
283
284
|
elementId: "index.html#hero",
|
|
284
285
|
animationId: "a1",
|
|
285
286
|
tweenPercentage: 50,
|
|
@@ -287,55 +288,55 @@ it("forwards focused bulk segment easing through the flat animation card", () =>
|
|
|
287
288
|
{ animationId: "a1", tweenPercentage: 50 },
|
|
288
289
|
{ animationId: "a2", tweenPercentage: 75 },
|
|
289
290
|
],
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
},
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
);
|
|
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
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
});
|
|
341
342
|
});
|