@hyperframes/studio 0.7.72 → 0.7.74
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-Bjf2HPzR.js → hyperframes-player-wiJqS2i-.js} +1 -1
- package/dist/assets/index-B37rWXo4.css +1 -0
- package/dist/assets/{index-Ct_pxETK.js → index-BXepgTJb.js} +1 -1
- package/dist/assets/index-CBDJuOGW.js +428 -0
- package/dist/assets/{index-Ch1hbJ3e.js → index-DGVNG1dd.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +4203 -2281
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/PropertyPanelFlat.tsx +1 -0
- package/src/components/editor/colorGradingFrameAnalysis.test.ts +101 -0
- package/src/components/editor/colorGradingFrameAnalysis.ts +203 -0
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +549 -0
- package/src/components/editor/propertyPanelColorCurves.test.tsx +158 -0
- package/src/components/editor/propertyPanelColorCurves.tsx +185 -0
- package/src/components/editor/propertyPanelColorGradingControls.tsx +115 -77
- package/src/components/editor/propertyPanelColorScopes.tsx +258 -0
- package/src/components/editor/propertyPanelColorSecondary.test.tsx +181 -0
- package/src/components/editor/propertyPanelColorSecondary.tsx +448 -0
- package/src/components/editor/propertyPanelColorWheels.test.tsx +119 -0
- package/src/components/editor/propertyPanelColorWheels.tsx +334 -0
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +109 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +168 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +184 -243
- package/src/components/editor/propertyPanelGradingNumberField.test.tsx +79 -0
- package/src/components/editor/propertyPanelGradingNumberField.tsx +116 -0
- package/src/components/editor/useColorGradingController.test.ts +133 -17
- package/src/components/editor/useColorGradingController.ts +11 -2
- package/src/components/editor/useColorGradingPreviews.ts +54 -11
- package/src/components/editor/useColorGradingScopes.test.tsx +143 -0
- package/src/components/editor/useColorGradingScopes.ts +62 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +30 -1
- package/src/icons/SystemIcons.tsx +4 -0
- package/dist/assets/index-BgZMle9I.css +0 -1
- package/dist/assets/index-DCyWLHnx.js +0 -428
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { clampNumber } from "../../utils/studioHelpers";
|
|
3
|
+
|
|
4
|
+
export function GradingNumberField({
|
|
5
|
+
label,
|
|
6
|
+
ariaLabel = label,
|
|
7
|
+
value,
|
|
8
|
+
min,
|
|
9
|
+
max,
|
|
10
|
+
disabled,
|
|
11
|
+
formatValue = String,
|
|
12
|
+
labelClassName = "min-w-0",
|
|
13
|
+
labelTextClassName = "block",
|
|
14
|
+
inputClassName = "w-full",
|
|
15
|
+
onBegin,
|
|
16
|
+
onPreview,
|
|
17
|
+
onSettle,
|
|
18
|
+
onCancel,
|
|
19
|
+
}: {
|
|
20
|
+
label: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
value: number;
|
|
23
|
+
min: number;
|
|
24
|
+
max: number;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
formatValue?: (value: number) => string;
|
|
27
|
+
labelClassName?: string;
|
|
28
|
+
labelTextClassName?: string;
|
|
29
|
+
inputClassName?: string;
|
|
30
|
+
onBegin: () => void;
|
|
31
|
+
onPreview: (value: number) => void;
|
|
32
|
+
onSettle: () => void;
|
|
33
|
+
onCancel: () => void;
|
|
34
|
+
}) {
|
|
35
|
+
const [draft, setDraft] = useState(() => formatValue(value));
|
|
36
|
+
const focusedRef = useRef(false);
|
|
37
|
+
const cancelBlurRef = useRef(false);
|
|
38
|
+
const baselineRef = useRef(value);
|
|
39
|
+
const dirtyRef = useRef(false);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!focusedRef.current) setDraft(formatValue(value));
|
|
43
|
+
}, [formatValue, value]);
|
|
44
|
+
|
|
45
|
+
const settle = () => {
|
|
46
|
+
focusedRef.current = false;
|
|
47
|
+
if (cancelBlurRef.current) {
|
|
48
|
+
cancelBlurRef.current = false;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const parsed = Number(draft);
|
|
52
|
+
if (draft.trim() === "" || !Number.isFinite(parsed)) {
|
|
53
|
+
setDraft(formatValue(value));
|
|
54
|
+
if (dirtyRef.current) onCancel();
|
|
55
|
+
dirtyRef.current = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const next = clampNumber(parsed, min, max);
|
|
59
|
+
setDraft(formatValue(next));
|
|
60
|
+
if (!dirtyRef.current || Object.is(next, baselineRef.current)) {
|
|
61
|
+
if (dirtyRef.current) onCancel();
|
|
62
|
+
dirtyRef.current = false;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
onPreview(next);
|
|
66
|
+
onSettle();
|
|
67
|
+
dirtyRef.current = false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<label className={labelClassName}>
|
|
72
|
+
<span className={labelTextClassName}>{label}</span>
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
inputMode="decimal"
|
|
76
|
+
aria-label={ariaLabel}
|
|
77
|
+
value={draft}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
onFocus={() => {
|
|
80
|
+
focusedRef.current = true;
|
|
81
|
+
baselineRef.current = value;
|
|
82
|
+
dirtyRef.current = false;
|
|
83
|
+
setDraft(formatValue(value));
|
|
84
|
+
}}
|
|
85
|
+
onChange={(event) => {
|
|
86
|
+
const next = event.target.value;
|
|
87
|
+
setDraft(next);
|
|
88
|
+
if (next.trim() === "") return;
|
|
89
|
+
const parsed = Number(next);
|
|
90
|
+
if (!Number.isFinite(parsed)) return;
|
|
91
|
+
const clamped = clampNumber(parsed, min, max);
|
|
92
|
+
if (!Object.is(clamped, baselineRef.current) && !dirtyRef.current) {
|
|
93
|
+
dirtyRef.current = true;
|
|
94
|
+
onBegin();
|
|
95
|
+
}
|
|
96
|
+
if (dirtyRef.current) onPreview(clamped);
|
|
97
|
+
}}
|
|
98
|
+
onBlur={settle}
|
|
99
|
+
onKeyDown={(event) => {
|
|
100
|
+
if (event.key === "Escape") {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
cancelBlurRef.current = true;
|
|
103
|
+
focusedRef.current = false;
|
|
104
|
+
setDraft(formatValue(value));
|
|
105
|
+
if (dirtyRef.current) onCancel();
|
|
106
|
+
dirtyRef.current = false;
|
|
107
|
+
event.currentTarget.blur();
|
|
108
|
+
} else if (event.key === "Enter") {
|
|
109
|
+
event.currentTarget.blur();
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
className={`border-b border-panel-border-input/50 bg-transparent py-0.5 text-right font-mono text-[9px] text-panel-text-2 outline-none focus:border-panel-accent disabled:opacity-40 ${inputClassName}`}
|
|
113
|
+
/>
|
|
114
|
+
</label>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -56,22 +56,30 @@ function makeElement(overrides: Partial<DomEditSelection> = {}): DomEditSelectio
|
|
|
56
56
|
} as DomEditSelection;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
type ApplyScope = (
|
|
60
|
+
scope: "source-file" | "project",
|
|
61
|
+
value: string | null,
|
|
62
|
+
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
63
|
+
|
|
59
64
|
function HookHost({
|
|
60
65
|
onState,
|
|
61
66
|
onSetAttributeLive,
|
|
62
67
|
element,
|
|
63
68
|
previewIframeRef,
|
|
69
|
+
onApplyScope,
|
|
64
70
|
}: {
|
|
65
71
|
onState: (state: ReturnType<typeof useColorGradingController>) => void;
|
|
66
72
|
onSetAttributeLive: (attr: string, value: string | null) => void;
|
|
67
73
|
element: DomEditSelection;
|
|
68
74
|
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
75
|
+
onApplyScope?: ApplyScope;
|
|
69
76
|
}) {
|
|
70
77
|
const state = useColorGradingController({
|
|
71
78
|
projectId: "proj",
|
|
72
79
|
element,
|
|
73
80
|
previewIframeRef,
|
|
74
81
|
onSetAttributeLive,
|
|
82
|
+
onApplyScope,
|
|
75
83
|
});
|
|
76
84
|
onState(state);
|
|
77
85
|
return null;
|
|
@@ -81,6 +89,7 @@ function renderHook(
|
|
|
81
89
|
onSetAttributeLive: (attr: string, value: string | null) => void,
|
|
82
90
|
initialElement: DomEditSelection = makeElement(),
|
|
83
91
|
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>,
|
|
92
|
+
onApplyScope?: ApplyScope,
|
|
84
93
|
) {
|
|
85
94
|
const host = document.createElement("div");
|
|
86
95
|
document.body.append(host);
|
|
@@ -94,6 +103,7 @@ function renderHook(
|
|
|
94
103
|
onSetAttributeLive,
|
|
95
104
|
element,
|
|
96
105
|
previewIframeRef,
|
|
106
|
+
onApplyScope,
|
|
97
107
|
}),
|
|
98
108
|
);
|
|
99
109
|
});
|
|
@@ -129,6 +139,21 @@ function createPreviewFrame() {
|
|
|
129
139
|
return { contentWindow, iframe };
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
function installPreviewRenderer(contentWindow: PreviewWindow) {
|
|
143
|
+
const renderPreviews = vi
|
|
144
|
+
.fn()
|
|
145
|
+
.mockImplementation(async (_target: unknown, candidates: Array<{ id: string }>) => ({
|
|
146
|
+
width: 160,
|
|
147
|
+
height: 90,
|
|
148
|
+
images: candidates.map(({ id }) => ({
|
|
149
|
+
id,
|
|
150
|
+
dataUrl: `data:image/png;base64,${id}`,
|
|
151
|
+
})),
|
|
152
|
+
}));
|
|
153
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
154
|
+
return renderPreviews;
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
async function flushPreviewRequest() {
|
|
133
158
|
act(() => vi.advanceTimersByTime(0));
|
|
134
159
|
await act(async () => {
|
|
@@ -161,23 +186,37 @@ describe("useColorGradingController", () => {
|
|
|
161
186
|
vi.useFakeTimers();
|
|
162
187
|
const devicePixelRatio = vi.spyOn(window, "devicePixelRatio", "get").mockReturnValue(2);
|
|
163
188
|
const { contentWindow, iframe } = createPreviewFrame();
|
|
164
|
-
const renderPreviews =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
const renderPreviews = installPreviewRenderer(contentWindow);
|
|
190
|
+
const initialElement = makeElement({
|
|
191
|
+
dataAttributes: {
|
|
192
|
+
"color-grading": JSON.stringify({
|
|
193
|
+
wheels: { shadows: { hue: 210, amount: 0.2 } },
|
|
194
|
+
effects: { pixelate: 0.4 },
|
|
195
|
+
palette: ["#112233", "#ffffff"],
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
168
198
|
});
|
|
169
|
-
|
|
170
|
-
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
199
|
+
const { root, getState } = renderHook(vi.fn(), initialElement, { current: iframe });
|
|
171
200
|
|
|
172
201
|
act(() => getState().requestPresetPreviews());
|
|
173
202
|
await flushPreviewRequest();
|
|
174
203
|
|
|
175
204
|
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
176
205
|
expect(renderPreviews.mock.calls[0]?.[1]).toHaveLength(18);
|
|
206
|
+
expect(renderPreviews.mock.calls[0]?.[1]).toContainEqual({
|
|
207
|
+
id: "bright-pop",
|
|
208
|
+
grading: expect.objectContaining({
|
|
209
|
+
wheels: expect.objectContaining({
|
|
210
|
+
shadows: expect.objectContaining({ amount: 0 }),
|
|
211
|
+
}),
|
|
212
|
+
effects: expect.objectContaining({ pixelate: 0.4 }),
|
|
213
|
+
palette: ["#112233", "#ffffff"],
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
177
216
|
expect(renderPreviews.mock.calls[0]?.[2]).toEqual({ maxDimension: 320 });
|
|
178
217
|
expect(getState().presetPreviews).toEqual({
|
|
179
218
|
status: "ready",
|
|
180
|
-
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
219
|
+
images: expect.objectContaining({ "bright-pop": "data:image/png;base64,bright-pop" }),
|
|
181
220
|
width: 160,
|
|
182
221
|
height: 90,
|
|
183
222
|
});
|
|
@@ -186,19 +225,96 @@ describe("useColorGradingController", () => {
|
|
|
186
225
|
vi.useRealTimers();
|
|
187
226
|
});
|
|
188
227
|
|
|
189
|
-
it("
|
|
228
|
+
it("retains partial preset previews and exposes retry after the batch times out", async () => {
|
|
190
229
|
vi.useFakeTimers();
|
|
191
230
|
const { contentWindow, iframe } = createPreviewFrame();
|
|
192
|
-
const renderPreviews = vi
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
images: candidates.map(({ id }) => ({ id, dataUrl: `data:image/png;base64,${id}` })),
|
|
198
|
-
}));
|
|
231
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
232
|
+
width: 160,
|
|
233
|
+
height: 90,
|
|
234
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,bright" }],
|
|
235
|
+
});
|
|
199
236
|
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
200
237
|
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
201
238
|
|
|
239
|
+
act(() => getState().requestPresetPreviews());
|
|
240
|
+
await flushPreviewRequest();
|
|
241
|
+
expect(getState().presetPreviews).toMatchObject({
|
|
242
|
+
status: "loading",
|
|
243
|
+
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await act(async () => {
|
|
247
|
+
await vi.advanceTimersByTimeAsync(1700);
|
|
248
|
+
});
|
|
249
|
+
expect(getState().presetPreviews).toMatchObject({
|
|
250
|
+
status: "unavailable",
|
|
251
|
+
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
252
|
+
});
|
|
253
|
+
expect(renderPreviews.mock.calls.length).toBeGreaterThan(1);
|
|
254
|
+
act(() => root.unmount());
|
|
255
|
+
vi.useRealTimers();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("persists a disabled secondary even though it does not render pixels", () => {
|
|
259
|
+
vi.useFakeTimers();
|
|
260
|
+
const onSetAttributeLive = vi.fn();
|
|
261
|
+
const grading = normalizeHfColorGrading({
|
|
262
|
+
secondaries: [
|
|
263
|
+
{
|
|
264
|
+
enabled: false,
|
|
265
|
+
key: { hue: { center: 215, range: 25 } },
|
|
266
|
+
correction: { saturation: 0.15 },
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
if (!grading) throw new Error("expected secondary grading");
|
|
271
|
+
const { root, getState } = renderHook(onSetAttributeLive);
|
|
272
|
+
|
|
273
|
+
act(() => getState().commitColorGrading(grading));
|
|
274
|
+
act(() => vi.advanceTimersByTime(400));
|
|
275
|
+
|
|
276
|
+
expect(onSetAttributeLive.mock.calls[0]?.[0]).toBe("color-grading");
|
|
277
|
+
expect(onSetAttributeLive.mock.calls[0]?.[1]).toContain('"enabled":false');
|
|
278
|
+
act(() => root.unmount());
|
|
279
|
+
vi.useRealTimers();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("copies a disabled authored secondary to a broader scope", async () => {
|
|
283
|
+
const onApplyScope = vi.fn<ApplyScope>().mockResolvedValue({
|
|
284
|
+
changedFiles: 1,
|
|
285
|
+
changedElements: 1,
|
|
286
|
+
});
|
|
287
|
+
const grading = {
|
|
288
|
+
secondaries: [
|
|
289
|
+
{
|
|
290
|
+
enabled: false,
|
|
291
|
+
key: { hue: { center: 215, range: 25 } },
|
|
292
|
+
correction: { saturation: 0.15 },
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
const { root, getState } = renderHook(
|
|
297
|
+
vi.fn(),
|
|
298
|
+
makeElement({ dataAttributes: { "color-grading": JSON.stringify(grading) } }),
|
|
299
|
+
undefined,
|
|
300
|
+
onApplyScope,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
await act(async () => getState().applyToScope());
|
|
304
|
+
|
|
305
|
+
expect(onApplyScope).toHaveBeenCalledWith(
|
|
306
|
+
"source-file",
|
|
307
|
+
expect.stringContaining('"enabled":false'),
|
|
308
|
+
);
|
|
309
|
+
act(() => root.unmount());
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("requests exact effect families and retains earlier family images", async () => {
|
|
313
|
+
vi.useFakeTimers();
|
|
314
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
315
|
+
const renderPreviews = installPreviewRenderer(contentWindow);
|
|
316
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
317
|
+
|
|
202
318
|
act(() => getState().requestEffectPreviews(["blur", "pixelate", "bloom"]));
|
|
203
319
|
await flushPreviewRequest();
|
|
204
320
|
|
|
@@ -490,7 +606,7 @@ describe("useColorGradingController", () => {
|
|
|
490
606
|
vi.useRealTimers();
|
|
491
607
|
});
|
|
492
608
|
|
|
493
|
-
it("resetGrading resets Grade fields without clearing Effects or Palette", () => {
|
|
609
|
+
it("resetGrading resets Grade fields without clearing LUT, Effects, or Palette", () => {
|
|
494
610
|
const { root, getState } = renderHook(vi.fn());
|
|
495
611
|
const grading = normalizeHfColorGrading({
|
|
496
612
|
preset: "bright-pop",
|
|
@@ -507,7 +623,7 @@ describe("useColorGradingController", () => {
|
|
|
507
623
|
});
|
|
508
624
|
expect(getState().grading).toMatchObject({
|
|
509
625
|
preset: "neutral",
|
|
510
|
-
lut:
|
|
626
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
|
|
511
627
|
effects: { pixelate: 0.5 },
|
|
512
628
|
palette: ["#112233", "#ffffff"],
|
|
513
629
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
2
|
import {
|
|
3
3
|
HF_COLOR_GRADING_ATTR,
|
|
4
|
+
hasHfColorGradingAuthoredValues,
|
|
4
5
|
isHfColorGradingActive,
|
|
5
6
|
normalizeHfColorGrading,
|
|
6
7
|
serializeHfColorGrading,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
} from "../../player/lib/runtimeProtocol";
|
|
22
23
|
import {
|
|
23
24
|
useColorGradingPreviews,
|
|
25
|
+
type ColorGradingCapturedFrame,
|
|
24
26
|
type ColorGradingPresetPreviews,
|
|
25
27
|
type ColorGradingPreviewOptions,
|
|
26
28
|
} from "./useColorGradingPreviews";
|
|
@@ -169,6 +171,9 @@ export interface ColorGradingControllerState {
|
|
|
169
171
|
next: NormalizedHfColorGrading | null,
|
|
170
172
|
options?: ColorGradingPreviewOptions,
|
|
171
173
|
) => void;
|
|
174
|
+
captureGradedFrame: (options?: {
|
|
175
|
+
grading?: NormalizedHfColorGrading;
|
|
176
|
+
}) => Promise<ColorGradingCapturedFrame | null>;
|
|
172
177
|
commitCompare: (enabled: boolean) => void;
|
|
173
178
|
setApplyScope: (scope: "source-file" | "project") => void;
|
|
174
179
|
applyToScope: () => Promise<void>;
|
|
@@ -472,7 +477,7 @@ export function useColorGradingController({
|
|
|
472
477
|
}
|
|
473
478
|
scheduleRuntimeStatusRefresh();
|
|
474
479
|
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
475
|
-
pendingPersistValueRef.current =
|
|
480
|
+
pendingPersistValueRef.current = hasHfColorGradingAuthoredValues(nextGrading)
|
|
476
481
|
? serializeHfColorGrading(nextGrading)
|
|
477
482
|
: null;
|
|
478
483
|
pendingPersistGradingRef.current = nextGrading;
|
|
@@ -519,7 +524,9 @@ export function useColorGradingController({
|
|
|
519
524
|
if (!onApplyScope || applyBusy) return;
|
|
520
525
|
setApplyBusy(true);
|
|
521
526
|
try {
|
|
522
|
-
const value =
|
|
527
|
+
const value = hasHfColorGradingAuthoredValues(grading)
|
|
528
|
+
? serializeHfColorGrading(grading)
|
|
529
|
+
: null;
|
|
523
530
|
await onApplyScope(applyScope, value);
|
|
524
531
|
} finally {
|
|
525
532
|
setApplyBusy(false);
|
|
@@ -539,6 +546,7 @@ export function useColorGradingController({
|
|
|
539
546
|
requestEffectPreviews: previewController.requestEffectPreviews,
|
|
540
547
|
commitColorGrading,
|
|
541
548
|
previewColorGrading: previewController.previewColorGrading,
|
|
549
|
+
captureGradedFrame: previewController.captureGradedFrame,
|
|
542
550
|
commitCompare,
|
|
543
551
|
setApplyScope,
|
|
544
552
|
applyToScope,
|
|
@@ -546,6 +554,7 @@ export function useColorGradingController({
|
|
|
546
554
|
const neutral = defaultColorGrading();
|
|
547
555
|
commitColorGrading({
|
|
548
556
|
...neutral,
|
|
557
|
+
lut: latestGradingRef.current.lut,
|
|
549
558
|
effects: latestGradingRef.current.effects,
|
|
550
559
|
palette: latestGradingRef.current.palette,
|
|
551
560
|
});
|
|
@@ -16,6 +16,12 @@ export interface ColorGradingPresetPreviews {
|
|
|
16
16
|
height: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface ColorGradingCapturedFrame {
|
|
20
|
+
dataUrl: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
type ColorGradingPreviewKind = "presets" | "effects";
|
|
20
26
|
|
|
21
27
|
export interface ColorGradingPreviewOptions {
|
|
@@ -78,12 +84,21 @@ function toPreviewColorGrading(grading: NormalizedHfColorGrading): unknown {
|
|
|
78
84
|
|
|
79
85
|
function previewCandidates(
|
|
80
86
|
request: PreviewRequest,
|
|
81
|
-
|
|
87
|
+
grading: Pick<NormalizedHfColorGrading, "effects" | "lut" | "palette">,
|
|
82
88
|
): Array<{ id: string; grading: unknown }> {
|
|
83
89
|
if (request.kind === "presets") {
|
|
84
90
|
return HF_COLOR_GRADING_PRESETS.map((preset) => {
|
|
85
|
-
const resolved = normalizeHfColorGrading({ preset: preset.id, lut });
|
|
86
|
-
return {
|
|
91
|
+
const resolved = normalizeHfColorGrading({ preset: preset.id, lut: grading.lut });
|
|
92
|
+
return {
|
|
93
|
+
id: preset.id,
|
|
94
|
+
grading: resolved
|
|
95
|
+
? toPreviewColorGrading({
|
|
96
|
+
...resolved,
|
|
97
|
+
effects: grading.effects,
|
|
98
|
+
palette: grading.palette,
|
|
99
|
+
})
|
|
100
|
+
: null,
|
|
101
|
+
};
|
|
87
102
|
});
|
|
88
103
|
}
|
|
89
104
|
return (request.effects ?? HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS).map((effect) => {
|
|
@@ -98,16 +113,19 @@ async function renderRequestedPreviews(
|
|
|
98
113
|
runtime: RuntimeColorGradingPreview,
|
|
99
114
|
target: HfColorGradingTarget,
|
|
100
115
|
request: PreviewRequest,
|
|
101
|
-
|
|
116
|
+
grading: Pick<NormalizedHfColorGrading, "effects" | "lut" | "palette">,
|
|
102
117
|
) {
|
|
103
|
-
const
|
|
118
|
+
const candidates = previewCandidates(request, grading);
|
|
119
|
+
const batch = await runtime.renderPreviews(target, candidates, {
|
|
104
120
|
maxDimension: previewMaxDimension(),
|
|
105
121
|
});
|
|
106
122
|
if (!batch) return null;
|
|
107
123
|
const images = Object.fromEntries(
|
|
108
124
|
batch.images.flatMap((image) => (image.dataUrl ? [[image.id, image.dataUrl]] : [])),
|
|
109
125
|
);
|
|
110
|
-
return Object.keys(images).length
|
|
126
|
+
return Object.keys(images).length
|
|
127
|
+
? { ...batch, images, complete: Object.keys(images).length === candidates.length }
|
|
128
|
+
: null;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
export function useColorGradingPreviews({
|
|
@@ -154,6 +172,7 @@ export function useColorGradingPreviews({
|
|
|
154
172
|
let cancelled = false;
|
|
155
173
|
let complete = false;
|
|
156
174
|
let inFlight = false;
|
|
175
|
+
let timedOut = false;
|
|
157
176
|
const timers: number[] = [];
|
|
158
177
|
setState((current) => ({
|
|
159
178
|
...current,
|
|
@@ -169,15 +188,19 @@ export function useColorGradingPreviews({
|
|
|
169
188
|
if (!runtime) return;
|
|
170
189
|
inFlight = true;
|
|
171
190
|
try {
|
|
172
|
-
const result = await renderRequestedPreviews(runtime, target, request,
|
|
191
|
+
const result = await renderRequestedPreviews(runtime, target, request, {
|
|
192
|
+
effects: grading.effects,
|
|
193
|
+
lut: grading.lut,
|
|
194
|
+
palette: grading.palette,
|
|
195
|
+
});
|
|
173
196
|
if (cancelled || !result) return;
|
|
174
|
-
complete =
|
|
197
|
+
complete = result.complete;
|
|
175
198
|
setState((current) => ({
|
|
176
199
|
...current,
|
|
177
200
|
previews: {
|
|
178
201
|
...current.previews,
|
|
179
202
|
[kind]: {
|
|
180
|
-
status: "ready",
|
|
203
|
+
status: result.complete ? "ready" : timedOut ? "unavailable" : "loading",
|
|
181
204
|
images:
|
|
182
205
|
kind === "effects"
|
|
183
206
|
? { ...current.previews[kind].images, ...result.images }
|
|
@@ -206,11 +229,12 @@ export function useColorGradingPreviews({
|
|
|
206
229
|
timers.push(
|
|
207
230
|
window.setTimeout(() => {
|
|
208
231
|
if (cancelled || complete) return;
|
|
232
|
+
timedOut = true;
|
|
209
233
|
setState((current) => ({
|
|
210
234
|
...current,
|
|
211
235
|
previews: {
|
|
212
236
|
...current.previews,
|
|
213
|
-
[kind]: { status: "unavailable"
|
|
237
|
+
[kind]: { ...current.previews[kind], status: "unavailable" },
|
|
214
238
|
},
|
|
215
239
|
}));
|
|
216
240
|
}, 1600),
|
|
@@ -221,7 +245,7 @@ export function useColorGradingPreviews({
|
|
|
221
245
|
iframe.removeEventListener("load", attempt);
|
|
222
246
|
window.removeEventListener("message", onMessage);
|
|
223
247
|
};
|
|
224
|
-
}, [grading.lut, previewIframeRef, request, target]);
|
|
248
|
+
}, [grading.effects, grading.lut, grading.palette, previewIframeRef, request, target]);
|
|
225
249
|
|
|
226
250
|
const stopAnimatedPreview = useCallback(() => {
|
|
227
251
|
const session = animatedRef.current;
|
|
@@ -296,6 +320,24 @@ export function useColorGradingPreviews({
|
|
|
296
320
|
(effects: readonly HfColorGradingActiveEffectKey[]) => requestPreviews("effects", effects),
|
|
297
321
|
[requestPreviews],
|
|
298
322
|
);
|
|
323
|
+
const captureGradedFrame = useCallback(
|
|
324
|
+
async ({
|
|
325
|
+
grading: requestedGrading = gradingRef.current,
|
|
326
|
+
}: {
|
|
327
|
+
grading?: NormalizedHfColorGrading;
|
|
328
|
+
} = {}): Promise<ColorGradingCapturedFrame | null> => {
|
|
329
|
+
const runtime = readRuntime(previewIframeRef?.current);
|
|
330
|
+
if (!runtime) return null;
|
|
331
|
+
const batch = await runtime.renderPreviews(
|
|
332
|
+
target,
|
|
333
|
+
[{ id: "capture", grading: toPreviewColorGrading(requestedGrading) }],
|
|
334
|
+
{ maxDimension: 320, useMediaTime: true },
|
|
335
|
+
);
|
|
336
|
+
const dataUrl = batch?.images[0]?.dataUrl;
|
|
337
|
+
return batch && dataUrl ? { dataUrl, width: batch.width, height: batch.height } : null;
|
|
338
|
+
},
|
|
339
|
+
[gradingRef, previewIframeRef, target],
|
|
340
|
+
);
|
|
299
341
|
|
|
300
342
|
return {
|
|
301
343
|
presetPreviews: previews.presets,
|
|
@@ -303,5 +345,6 @@ export function useColorGradingPreviews({
|
|
|
303
345
|
requestPresetPreviews,
|
|
304
346
|
requestEffectPreviews,
|
|
305
347
|
previewColorGrading,
|
|
348
|
+
captureGradedFrame,
|
|
306
349
|
};
|
|
307
350
|
}
|