@hyperframes/studio 0.7.71 → 0.7.72
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-C1X90psg.js → hyperframes-player-Bjf2HPzR.js} +1 -1
- package/dist/assets/index-BgZMle9I.css +1 -0
- package/dist/assets/{index-nY4lLBqM.js → index-Ch1hbJ3e.js} +1 -1
- package/dist/assets/{index-DzxDHPR1.js → index-Ct_pxETK.js} +1 -1
- package/dist/assets/index-DCyWLHnx.js +428 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +2 -2
- package/dist/index.js +5383 -3804
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +10 -4
- package/src/components/editor/AnimationCard.test.tsx +100 -0
- package/src/components/editor/AnimationCard.tsx +5 -2
- package/src/components/editor/EaseCurveSection.test.tsx +348 -0
- package/src/components/editor/EaseCurveSection.tsx +444 -237
- package/src/components/editor/EaseParamFields.test.tsx +188 -0
- package/src/components/editor/EaseParamFields.tsx +246 -0
- package/src/components/editor/PropertyPanel.test.tsx +9 -7
- package/src/components/editor/PropertyPanelFlat.tsx +74 -88
- package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
- package/src/components/editor/easeCurveSvg.tsx +65 -0
- package/src/components/editor/easePresetLibrary.test.ts +114 -0
- package/src/components/editor/easePresetLibrary.ts +49 -0
- package/src/components/editor/gsapAnimationConstants.ts +15 -13
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
- package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
- package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
- package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
- package/src/components/editor/propertyPanelTypes.ts +13 -0
- package/src/components/editor/useColorGradingController.test.ts +215 -10
- package/src/components/editor/useColorGradingController.ts +46 -88
- package/src/components/editor/useColorGradingPreviews.ts +307 -0
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/contexts/PanelLayoutContext.tsx +3 -6
- package/src/hooks/useBlockCatalog.ts +31 -13
- package/src/hooks/useBlockHandlers.ts +22 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
- package/src/hooks/useGsapScriptCommits.ts +14 -1
- package/src/hooks/usePanelLayout.test.ts +68 -1
- package/src/hooks/usePanelLayout.ts +72 -34
- package/src/player/components/PlayerControls.tsx +68 -229
- package/src/player/components/ShortcutsPanel.tsx +2 -4
- package/src/player/components/SpeedMenu.tsx +1 -2
- package/src/telemetry/events.test.ts +9 -0
- package/src/telemetry/events.ts +5 -0
- package/src/utils/blockInstaller.test.ts +67 -0
- package/src/utils/blockInstaller.ts +162 -109
- package/src/utils/studioUiPreferences.test.ts +5 -0
- package/src/utils/studioUiPreferences.ts +8 -0
- package/dist/assets/index-CSCh2Vrp.js +0 -428
- package/dist/assets/index-Ceyz8Qt2.css +0 -1
- package/src/player/components/PlayerControls.test.ts +0 -20
- package/src/player/components/useSeekBarDrag.ts +0 -169
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { RegistryItem } from "@hyperframes/core/registry";
|
|
3
|
+
import { useBlockCatalog } from "../../hooks/useBlockCatalog";
|
|
4
|
+
import { Film, Plus } from "../../icons/SystemIcons";
|
|
5
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
6
|
+
import type { DomEditSelection } from "./domEditing";
|
|
7
|
+
import { FLAT_PREVIEW_GRID } from "./propertyPanelFlatPrimitives";
|
|
8
|
+
import type { ElementTiming } from "./propertyPanelFlatTimingDerivation";
|
|
9
|
+
|
|
10
|
+
export const MEDIA_TREATMENT_OVERLAY_TAG = "media-treatment-overlay";
|
|
11
|
+
|
|
12
|
+
export function filterMediaTreatmentOverlays(items: readonly RegistryItem[]): RegistryItem[] {
|
|
13
|
+
return items.filter(
|
|
14
|
+
(item) =>
|
|
15
|
+
item.type === "hyperframes:block" &&
|
|
16
|
+
item.tags?.includes(MEDIA_TREATMENT_OVERLAY_TAG) === true,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveMediaOverlayPlacement(
|
|
21
|
+
element: Pick<DomEditSelection, "dataAttributes" | "sourceFile">,
|
|
22
|
+
timing: Pick<ElementTiming, "start" | "duration">,
|
|
23
|
+
) {
|
|
24
|
+
const authoredTrack = Number.parseInt(element.dataAttributes["track-index"] ?? "", 10);
|
|
25
|
+
return {
|
|
26
|
+
start: timing.start,
|
|
27
|
+
...(timing.duration > 0 ? { duration: timing.duration } : {}),
|
|
28
|
+
...(Number.isFinite(authoredTrack) ? { track: authoredTrack + 1 } : {}),
|
|
29
|
+
compositionPath: element.sourceFile,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FlatOverlaysSection({
|
|
34
|
+
onAddOverlay,
|
|
35
|
+
}: {
|
|
36
|
+
onAddOverlay: (name: string) => Promise<void>;
|
|
37
|
+
}) {
|
|
38
|
+
const track = useTrackDesignInput();
|
|
39
|
+
const { blocks, loading, error } = useBlockCatalog();
|
|
40
|
+
const overlays = filterMediaTreatmentOverlays(blocks);
|
|
41
|
+
const [adding, setAdding] = useState<string | null>(null);
|
|
42
|
+
const [previewing, setPreviewing] = useState<string | null>(null);
|
|
43
|
+
|
|
44
|
+
if (loading) {
|
|
45
|
+
return <div className="py-4 text-center text-[10px] text-panel-text-4">Loading overlays…</div>;
|
|
46
|
+
}
|
|
47
|
+
if (error) {
|
|
48
|
+
return <div className="py-4 text-center text-[10px] text-red-300">{error}</div>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const busy = adding !== null;
|
|
52
|
+
return (
|
|
53
|
+
<div data-flat-overlays="true" className={FLAT_PREVIEW_GRID}>
|
|
54
|
+
{overlays.map((overlay) => (
|
|
55
|
+
<button
|
|
56
|
+
key={overlay.name}
|
|
57
|
+
type="button"
|
|
58
|
+
data-flat-overlay={overlay.name}
|
|
59
|
+
aria-label={`Add ${overlay.title}`}
|
|
60
|
+
disabled={busy}
|
|
61
|
+
title={overlay.description}
|
|
62
|
+
onPointerEnter={() => setPreviewing(overlay.name)}
|
|
63
|
+
onPointerLeave={() => setPreviewing(null)}
|
|
64
|
+
onFocus={() => setPreviewing(overlay.name)}
|
|
65
|
+
onBlur={() => setPreviewing(null)}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
track("button", `Add ${overlay.title}`);
|
|
68
|
+
setAdding(overlay.name);
|
|
69
|
+
void onAddOverlay(overlay.name).finally(() => setAdding(null));
|
|
70
|
+
}}
|
|
71
|
+
className="group min-w-0 overflow-hidden border border-panel-hairline bg-panel-bg-soft text-left transition-colors hover:border-panel-border-input hover:bg-panel-bg disabled:cursor-wait disabled:opacity-50"
|
|
72
|
+
>
|
|
73
|
+
<span className="relative flex aspect-video w-full items-center justify-center overflow-hidden bg-black/20">
|
|
74
|
+
{previewing === overlay.name && overlay.preview?.video ? (
|
|
75
|
+
<video
|
|
76
|
+
src={overlay.preview.video}
|
|
77
|
+
poster={overlay.preview.poster}
|
|
78
|
+
autoPlay
|
|
79
|
+
muted
|
|
80
|
+
loop
|
|
81
|
+
playsInline
|
|
82
|
+
className="block h-full w-full object-cover"
|
|
83
|
+
/>
|
|
84
|
+
) : overlay.preview?.poster ? (
|
|
85
|
+
<img
|
|
86
|
+
data-flat-overlay-preview={overlay.name}
|
|
87
|
+
src={overlay.preview.poster}
|
|
88
|
+
alt=""
|
|
89
|
+
draggable={false}
|
|
90
|
+
loading="lazy"
|
|
91
|
+
className="block h-full w-full object-cover"
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<Film size={15} className="text-panel-text-4" />
|
|
95
|
+
)}
|
|
96
|
+
<Plus
|
|
97
|
+
size={12}
|
|
98
|
+
className="absolute right-1.5 top-1.5 text-white opacity-70 drop-shadow group-hover:text-panel-accent group-hover:opacity-100"
|
|
99
|
+
/>
|
|
100
|
+
</span>
|
|
101
|
+
<span className="block truncate px-2 py-1.5 text-[10px] text-panel-text-2">
|
|
102
|
+
{adding === overlay.name ? "Adding…" : overlay.title}
|
|
103
|
+
</span>
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type PropertyValueTier,
|
|
9
9
|
} from "./propertyPanelValueTier";
|
|
10
10
|
|
|
11
|
+
export const FLAT_PREVIEW_GRID = "grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-1";
|
|
12
|
+
|
|
11
13
|
/* ------------------------------------------------------------------ */
|
|
12
14
|
/* FlatRow — single-column label/value property row */
|
|
13
15
|
/* ------------------------------------------------------------------ */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NormalizedHfColorGrading } from "@hyperframes/core/color-grading";
|
|
2
|
+
import type { ColorGradingPreviewOptions } from "./useColorGradingController";
|
|
3
|
+
|
|
4
|
+
export function presetPreviewHandlers({
|
|
5
|
+
id,
|
|
6
|
+
label,
|
|
7
|
+
resolve,
|
|
8
|
+
onPreview,
|
|
9
|
+
onCommit,
|
|
10
|
+
onTrack,
|
|
11
|
+
}: {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
resolve: () => NormalizedHfColorGrading;
|
|
15
|
+
onPreview: (
|
|
16
|
+
grading: NormalizedHfColorGrading | null,
|
|
17
|
+
options?: ColorGradingPreviewOptions,
|
|
18
|
+
) => void;
|
|
19
|
+
onCommit: (grading: NormalizedHfColorGrading) => void;
|
|
20
|
+
onTrack: (label: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
return {
|
|
23
|
+
title: `Preview ${label}`,
|
|
24
|
+
onPointerEnter: () =>
|
|
25
|
+
onPreview(resolve(), {
|
|
26
|
+
animatedPreview: { kind: "presets" as const, id },
|
|
27
|
+
}),
|
|
28
|
+
onPointerLeave: () => onPreview(null),
|
|
29
|
+
onFocus: () => onPreview(resolve()),
|
|
30
|
+
onBlur: () => onPreview(null),
|
|
31
|
+
onClick: () => {
|
|
32
|
+
onTrack(label);
|
|
33
|
+
onCommit(resolve());
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -19,6 +19,18 @@ export interface BackgroundRemovalResult {
|
|
|
19
19
|
provider?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface MediaOverlayPlacement {
|
|
23
|
+
start: number;
|
|
24
|
+
duration?: number;
|
|
25
|
+
track?: number;
|
|
26
|
+
compositionPath?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AddMediaOverlayHandler = (
|
|
30
|
+
blockName: string,
|
|
31
|
+
placement: MediaOverlayPlacement,
|
|
32
|
+
) => Promise<void>;
|
|
33
|
+
|
|
22
34
|
export interface PropertyPanelProps {
|
|
23
35
|
projectId: string;
|
|
24
36
|
projectDir: string | null;
|
|
@@ -69,6 +81,7 @@ export interface PropertyPanelProps {
|
|
|
69
81
|
onAskAgent: () => void;
|
|
70
82
|
onToggleElementHidden?: (elementKey: string, hidden: boolean) => void | Promise<void>;
|
|
71
83
|
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
84
|
+
onAddMediaOverlay?: AddMediaOverlayHandler;
|
|
72
85
|
fontAssets?: ImportedFontAsset[];
|
|
73
86
|
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
74
87
|
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
@@ -13,9 +13,9 @@ function brightPopGrading() {
|
|
|
13
13
|
return next;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
const next = normalizeHfColorGrading({ preset: "
|
|
18
|
-
if (!next) throw new Error("expected
|
|
16
|
+
function cleanStudioGrading() {
|
|
17
|
+
const next = normalizeHfColorGrading({ preset: "clean-studio", intensity: 1 });
|
|
18
|
+
if (!next) throw new Error("expected clean-studio preset to normalize");
|
|
19
19
|
return next;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -60,14 +60,17 @@ function HookHost({
|
|
|
60
60
|
onState,
|
|
61
61
|
onSetAttributeLive,
|
|
62
62
|
element,
|
|
63
|
+
previewIframeRef,
|
|
63
64
|
}: {
|
|
64
65
|
onState: (state: ReturnType<typeof useColorGradingController>) => void;
|
|
65
66
|
onSetAttributeLive: (attr: string, value: string | null) => void;
|
|
66
67
|
element: DomEditSelection;
|
|
68
|
+
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
67
69
|
}) {
|
|
68
70
|
const state = useColorGradingController({
|
|
69
71
|
projectId: "proj",
|
|
70
72
|
element,
|
|
73
|
+
previewIframeRef,
|
|
71
74
|
onSetAttributeLive,
|
|
72
75
|
});
|
|
73
76
|
onState(state);
|
|
@@ -77,6 +80,7 @@ function HookHost({
|
|
|
77
80
|
function renderHook(
|
|
78
81
|
onSetAttributeLive: (attr: string, value: string | null) => void,
|
|
79
82
|
initialElement: DomEditSelection = makeElement(),
|
|
83
|
+
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>,
|
|
80
84
|
) {
|
|
81
85
|
const host = document.createElement("div");
|
|
82
86
|
document.body.append(host);
|
|
@@ -89,6 +93,7 @@ function renderHook(
|
|
|
89
93
|
onState: (s: ReturnType<typeof useColorGradingController>) => (latest = s),
|
|
90
94
|
onSetAttributeLive,
|
|
91
95
|
element,
|
|
96
|
+
previewIframeRef,
|
|
92
97
|
}),
|
|
93
98
|
);
|
|
94
99
|
});
|
|
@@ -107,6 +112,43 @@ function renderHook(
|
|
|
107
112
|
};
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
type PreviewWindow = Window & {
|
|
116
|
+
__hf?: {
|
|
117
|
+
colorGrading?: {
|
|
118
|
+
renderPreviews?: ReturnType<typeof vi.fn>;
|
|
119
|
+
startPreviewPlayback?: ReturnType<typeof vi.fn>;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
__player?: { play: ReturnType<typeof vi.fn> };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function createPreviewFrame() {
|
|
126
|
+
const iframe = document.body.appendChild(document.createElement("iframe"));
|
|
127
|
+
const contentWindow = iframe.contentWindow as PreviewWindow | null;
|
|
128
|
+
if (!contentWindow) throw new Error("expected iframe contentWindow");
|
|
129
|
+
return { contentWindow, iframe };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function flushPreviewRequest() {
|
|
133
|
+
act(() => vi.advanceTimersByTime(0));
|
|
134
|
+
await act(async () => {
|
|
135
|
+
await Promise.resolve();
|
|
136
|
+
await Promise.resolve();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function colorGradingMessages(calls: ReadonlyArray<ReadonlyArray<unknown>>) {
|
|
141
|
+
return calls
|
|
142
|
+
.map(([message]) => message)
|
|
143
|
+
.filter(
|
|
144
|
+
(message): message is { action: string; grading: unknown } =>
|
|
145
|
+
typeof message === "object" &&
|
|
146
|
+
message !== null &&
|
|
147
|
+
"action" in message &&
|
|
148
|
+
message.action === "set-color-grading",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
110
152
|
describe("useColorGradingController", () => {
|
|
111
153
|
it("starts with the neutral (inactive) grading and idle compare state", () => {
|
|
112
154
|
const { root, getState } = renderHook(vi.fn());
|
|
@@ -115,6 +157,77 @@ describe("useColorGradingController", () => {
|
|
|
115
157
|
act(() => root.unmount());
|
|
116
158
|
});
|
|
117
159
|
|
|
160
|
+
it("requests one exact preset batch from the selected media runtime", async () => {
|
|
161
|
+
vi.useFakeTimers();
|
|
162
|
+
const devicePixelRatio = vi.spyOn(window, "devicePixelRatio", "get").mockReturnValue(2);
|
|
163
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
164
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
165
|
+
width: 160,
|
|
166
|
+
height: 90,
|
|
167
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,bright" }],
|
|
168
|
+
});
|
|
169
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
170
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
171
|
+
|
|
172
|
+
act(() => getState().requestPresetPreviews());
|
|
173
|
+
await flushPreviewRequest();
|
|
174
|
+
|
|
175
|
+
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(renderPreviews.mock.calls[0]?.[1]).toHaveLength(18);
|
|
177
|
+
expect(renderPreviews.mock.calls[0]?.[2]).toEqual({ maxDimension: 320 });
|
|
178
|
+
expect(getState().presetPreviews).toEqual({
|
|
179
|
+
status: "ready",
|
|
180
|
+
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
181
|
+
width: 160,
|
|
182
|
+
height: 90,
|
|
183
|
+
});
|
|
184
|
+
act(() => root.unmount());
|
|
185
|
+
devicePixelRatio.mockRestore();
|
|
186
|
+
vi.useRealTimers();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("requests exact effect families and retains earlier family images", async () => {
|
|
190
|
+
vi.useFakeTimers();
|
|
191
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
192
|
+
const renderPreviews = vi
|
|
193
|
+
.fn()
|
|
194
|
+
.mockImplementation(async (_target: unknown, candidates: Array<{ id: string }>) => ({
|
|
195
|
+
width: 160,
|
|
196
|
+
height: 90,
|
|
197
|
+
images: candidates.map(({ id }) => ({ id, dataUrl: `data:image/png;base64,${id}` })),
|
|
198
|
+
}));
|
|
199
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
200
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
201
|
+
|
|
202
|
+
act(() => getState().requestEffectPreviews(["blur", "pixelate", "bloom"]));
|
|
203
|
+
await flushPreviewRequest();
|
|
204
|
+
|
|
205
|
+
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
206
|
+
expect(renderPreviews.mock.calls[0]?.[1].map(({ id }: { id: string }) => id)).toEqual([
|
|
207
|
+
"blur",
|
|
208
|
+
"pixelate",
|
|
209
|
+
"bloom",
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
act(() => getState().requestEffectPreviews(["kuwahara"]));
|
|
213
|
+
await flushPreviewRequest();
|
|
214
|
+
|
|
215
|
+
expect(renderPreviews).toHaveBeenCalledTimes(2);
|
|
216
|
+
expect(getState().effectPreviews).toEqual({
|
|
217
|
+
status: "ready",
|
|
218
|
+
images: {
|
|
219
|
+
blur: "data:image/png;base64,blur",
|
|
220
|
+
pixelate: "data:image/png;base64,pixelate",
|
|
221
|
+
bloom: "data:image/png;base64,bloom",
|
|
222
|
+
kuwahara: "data:image/png;base64,kuwahara",
|
|
223
|
+
},
|
|
224
|
+
width: 160,
|
|
225
|
+
height: 90,
|
|
226
|
+
});
|
|
227
|
+
act(() => root.unmount());
|
|
228
|
+
vi.useRealTimers();
|
|
229
|
+
});
|
|
230
|
+
|
|
118
231
|
it("commitColorGrading updates grading state synchronously and schedules a debounced persist", async () => {
|
|
119
232
|
vi.useFakeTimers();
|
|
120
233
|
const onSetAttributeLive = vi.fn();
|
|
@@ -135,6 +248,86 @@ describe("useColorGradingController", () => {
|
|
|
135
248
|
vi.useRealTimers();
|
|
136
249
|
});
|
|
137
250
|
|
|
251
|
+
it("previews through the runtime only and restores the committed grade without persisting", () => {
|
|
252
|
+
vi.useFakeTimers();
|
|
253
|
+
const onSetAttributeLive = vi.fn();
|
|
254
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
255
|
+
const postMessage = vi.spyOn(contentWindow, "postMessage");
|
|
256
|
+
const { root, getState } = renderHook(onSetAttributeLive, makeElement(), {
|
|
257
|
+
current: iframe,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
act(() => getState().previewColorGrading(brightPopGrading()));
|
|
261
|
+
act(() => getState().previewColorGrading(null));
|
|
262
|
+
const gradingMessages = colorGradingMessages(postMessage.mock.calls);
|
|
263
|
+
expect(gradingMessages).toHaveLength(2);
|
|
264
|
+
expect(gradingMessages[0]?.grading).toMatchObject({ preset: "bright-pop" });
|
|
265
|
+
expect(gradingMessages[1]?.grading).toBeNull();
|
|
266
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
267
|
+
act(() => vi.advanceTimersByTime(500));
|
|
268
|
+
expect(onSetAttributeLive).not.toHaveBeenCalled();
|
|
269
|
+
|
|
270
|
+
act(() => root.unmount());
|
|
271
|
+
vi.useRealTimers();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("animates only the selected video card without starting the project player", async () => {
|
|
275
|
+
vi.useFakeTimers();
|
|
276
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
277
|
+
const stopPlayback = vi.fn();
|
|
278
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
279
|
+
width: 320,
|
|
280
|
+
height: 180,
|
|
281
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,animated" }],
|
|
282
|
+
});
|
|
283
|
+
const startPreviewPlayback = vi.fn(() => stopPlayback);
|
|
284
|
+
contentWindow.__hf = { colorGrading: { renderPreviews, startPreviewPlayback } };
|
|
285
|
+
contentWindow.__player = { play: vi.fn() };
|
|
286
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
287
|
+
|
|
288
|
+
act(() =>
|
|
289
|
+
getState().previewColorGrading(brightPopGrading(), {
|
|
290
|
+
animatedPreview: { kind: "presets", id: "bright-pop" },
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
await act(async () => {
|
|
294
|
+
await Promise.resolve();
|
|
295
|
+
await Promise.resolve();
|
|
296
|
+
});
|
|
297
|
+
expect(startPreviewPlayback).toHaveBeenCalledTimes(1);
|
|
298
|
+
expect(renderPreviews).toHaveBeenCalledWith(
|
|
299
|
+
expect.anything(),
|
|
300
|
+
[{ id: "bright-pop", grading: expect.objectContaining({ preset: "bright-pop" }) }],
|
|
301
|
+
{ maxDimension: 160, useMediaTime: true },
|
|
302
|
+
);
|
|
303
|
+
expect(contentWindow.__player.play).not.toHaveBeenCalled();
|
|
304
|
+
expect(getState().presetPreviews.images["bright-pop"]).toBe("data:image/png;base64,animated");
|
|
305
|
+
|
|
306
|
+
act(() => getState().previewColorGrading(null));
|
|
307
|
+
expect(stopPlayback).toHaveBeenCalledTimes(1);
|
|
308
|
+
|
|
309
|
+
act(() => root.unmount());
|
|
310
|
+
vi.useRealTimers();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("restores a just-committed look even before React renders the new state", () => {
|
|
314
|
+
vi.useFakeTimers();
|
|
315
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
316
|
+
const postMessage = vi.spyOn(contentWindow, "postMessage");
|
|
317
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
318
|
+
const brightPop = brightPopGrading();
|
|
319
|
+
|
|
320
|
+
act(() => {
|
|
321
|
+
getState().commitColorGrading(brightPop);
|
|
322
|
+
getState().previewColorGrading(null);
|
|
323
|
+
});
|
|
324
|
+
const gradingMessages = colorGradingMessages(postMessage.mock.calls);
|
|
325
|
+
expect(gradingMessages.at(-1)?.grading).toMatchObject({ preset: "bright-pop" });
|
|
326
|
+
|
|
327
|
+
act(() => root.unmount());
|
|
328
|
+
vi.useRealTimers();
|
|
329
|
+
});
|
|
330
|
+
|
|
138
331
|
it("reverts to the last confirmed-good grading via the real onSettled(false) signal (matches runDomEditCommit, which never rejects)", async () => {
|
|
139
332
|
// The actual Studio commit runner (runDomEditCommit) catches persist
|
|
140
333
|
// failures internally and always resolves — it reports outcome only
|
|
@@ -253,7 +446,7 @@ describe("useColorGradingController", () => {
|
|
|
253
446
|
});
|
|
254
447
|
},
|
|
255
448
|
)
|
|
256
|
-
// Edit B (
|
|
449
|
+
// Edit B (clean-studio): settles immediately and successfully.
|
|
257
450
|
.mockImplementationOnce(
|
|
258
451
|
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
259
452
|
onSettled?.(true);
|
|
@@ -272,13 +465,13 @@ describe("useColorGradingController", () => {
|
|
|
272
465
|
|
|
273
466
|
// B commits on the SAME element before A's persist has settled.
|
|
274
467
|
act(() => {
|
|
275
|
-
getState().commitColorGrading(
|
|
468
|
+
getState().commitColorGrading(cleanStudioGrading());
|
|
276
469
|
});
|
|
277
470
|
act(() => {
|
|
278
471
|
vi.advanceTimersByTime(400);
|
|
279
472
|
});
|
|
280
473
|
expect(onSetAttributeLive).toHaveBeenCalledTimes(2); // B's persist has already settled (mock resolves sync)
|
|
281
|
-
expect(getState().grading.preset).toBe("
|
|
474
|
+
expect(getState().grading.preset).toBe("clean-studio");
|
|
282
475
|
|
|
283
476
|
// NOW A's stale persist finally settles as a FAILURE — must not revert
|
|
284
477
|
// `grading` (which now correctly shows B's newer edit) back to the
|
|
@@ -292,20 +485,32 @@ describe("useColorGradingController", () => {
|
|
|
292
485
|
await Promise.resolve();
|
|
293
486
|
await Promise.resolve();
|
|
294
487
|
});
|
|
295
|
-
expect(getState().grading.preset).toBe("
|
|
488
|
+
expect(getState().grading.preset).toBe("clean-studio");
|
|
296
489
|
act(() => root.unmount());
|
|
297
490
|
vi.useRealTimers();
|
|
298
491
|
});
|
|
299
492
|
|
|
300
|
-
it("resetGrading
|
|
493
|
+
it("resetGrading resets Grade fields without clearing Effects or Palette", () => {
|
|
301
494
|
const { root, getState } = renderHook(vi.fn());
|
|
495
|
+
const grading = normalizeHfColorGrading({
|
|
496
|
+
preset: "bright-pop",
|
|
497
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
|
|
498
|
+
effects: { pixelate: 0.5 },
|
|
499
|
+
palette: ["#112233", "#ffffff"],
|
|
500
|
+
});
|
|
501
|
+
if (!grading) throw new Error("expected grading to normalize");
|
|
302
502
|
act(() => {
|
|
303
|
-
getState().commitColorGrading(
|
|
503
|
+
getState().commitColorGrading(grading);
|
|
304
504
|
});
|
|
305
505
|
act(() => {
|
|
306
506
|
getState().resetGrading();
|
|
307
507
|
});
|
|
308
|
-
expect(getState().grading
|
|
508
|
+
expect(getState().grading).toMatchObject({
|
|
509
|
+
preset: "neutral",
|
|
510
|
+
lut: null,
|
|
511
|
+
effects: { pixelate: 0.5 },
|
|
512
|
+
palette: ["#112233", "#ffffff"],
|
|
513
|
+
});
|
|
309
514
|
act(() => root.unmount());
|
|
310
515
|
});
|
|
311
516
|
|