@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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
HF_COLOR_GRADING_GRADE_PRESETS,
|
|
4
|
-
isHfColorGradingActive,
|
|
5
4
|
normalizeHfColorGrading,
|
|
5
|
+
serializeHfColorGrading,
|
|
6
6
|
type HfColorGradingAdjustKey,
|
|
7
7
|
type HfColorGradingDetailKey,
|
|
8
8
|
type NormalizedHfColorGrading,
|
|
9
9
|
} from "@hyperframes/core/color-grading";
|
|
10
|
-
import {
|
|
10
|
+
import { Plus, Settings } from "../../icons/SystemIcons";
|
|
11
11
|
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
12
12
|
import { LUT_EXT } from "../../utils/mediaTypes";
|
|
13
13
|
import { FLAT_PREVIEW_GRID, FlatSlider } from "./propertyPanelFlatPrimitives";
|
|
@@ -18,138 +18,23 @@ import type {
|
|
|
18
18
|
MediaMetadata,
|
|
19
19
|
} from "./useColorGradingController";
|
|
20
20
|
import { presetPreviewHandlers } from "./propertyPanelPresetPreview";
|
|
21
|
+
import { ColorCurves } from "./propertyPanelColorCurves";
|
|
22
|
+
import {
|
|
23
|
+
COLOR_GRADING_ADJUST_SLIDERS,
|
|
24
|
+
COLOR_GRADING_DETAIL_SLIDERS,
|
|
25
|
+
colorGradingWithAdjust,
|
|
26
|
+
colorGradingWithDetail,
|
|
27
|
+
createColorGradingActions,
|
|
28
|
+
GRAIN_TUNE_SLIDERS,
|
|
29
|
+
normalizedColorGradingDefault,
|
|
30
|
+
VIGNETTE_TUNE_SLIDERS,
|
|
31
|
+
visibleColorGradingIntensity,
|
|
32
|
+
} from "./propertyPanelColorGradingControls";
|
|
33
|
+
import { PropertyPanelColorScopes } from "./propertyPanelColorScopes";
|
|
34
|
+
import { PropertyPanelColorSecondary } from "./propertyPanelColorSecondary";
|
|
35
|
+
import { ColorWheels } from "./propertyPanelColorWheels";
|
|
21
36
|
|
|
22
|
-
|
|
23
|
-
active: "bg-emerald-400",
|
|
24
|
-
pending: "bg-amber-300",
|
|
25
|
-
unavailable: "bg-red-400",
|
|
26
|
-
missing: "bg-panel-text-5",
|
|
27
|
-
inactive: "bg-panel-text-5",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function FlatColorGradingAccessory({
|
|
31
|
-
state,
|
|
32
|
-
}: {
|
|
33
|
-
state: Pick<
|
|
34
|
-
ColorGradingControllerState,
|
|
35
|
-
"grading" | "compareEnabled" | "runtimeStatus" | "commitCompare" | "resetGrading"
|
|
36
|
-
>;
|
|
37
|
-
}) {
|
|
38
|
-
const track = useTrackDesignInput();
|
|
39
|
-
const { grading, compareEnabled, runtimeStatus, commitCompare, resetGrading } = state;
|
|
40
|
-
const gradingActive = isHfColorGradingActive(grading);
|
|
41
|
-
// Tracks the active hold's cleanup so it can be torn down on unmount too —
|
|
42
|
-
// without this, switching selection away mid-hold (unmounting this
|
|
43
|
-
// accessory) leaves the pointerup/pointercancel/blur listeners registered
|
|
44
|
-
// on `window` forever, each holding a closure over the old commitCompare.
|
|
45
|
-
const releaseRef = useRef<(() => void) | null>(null);
|
|
46
|
-
useEffect(
|
|
47
|
-
() => () => {
|
|
48
|
-
releaseRef.current?.();
|
|
49
|
-
releaseRef.current = null;
|
|
50
|
-
},
|
|
51
|
-
[],
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<span className="flex items-center gap-2.5">
|
|
56
|
-
<button
|
|
57
|
-
type="button"
|
|
58
|
-
aria-pressed={compareEnabled}
|
|
59
|
-
aria-label="Hold to show original"
|
|
60
|
-
disabled={!gradingActive}
|
|
61
|
-
onPointerDown={(e) => {
|
|
62
|
-
if (!gradingActive) return;
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
e.stopPropagation();
|
|
65
|
-
track("button", "Compare original");
|
|
66
|
-
commitCompare(true);
|
|
67
|
-
const release = () => {
|
|
68
|
-
commitCompare(false);
|
|
69
|
-
window.removeEventListener("pointerup", release);
|
|
70
|
-
window.removeEventListener("pointercancel", release);
|
|
71
|
-
window.removeEventListener("blur", release);
|
|
72
|
-
releaseRef.current = null;
|
|
73
|
-
};
|
|
74
|
-
releaseRef.current = release;
|
|
75
|
-
window.addEventListener("pointerup", release);
|
|
76
|
-
window.addEventListener("pointercancel", release);
|
|
77
|
-
window.addEventListener("blur", release);
|
|
78
|
-
}}
|
|
79
|
-
onBlur={() => {
|
|
80
|
-
if (compareEnabled) commitCompare(false);
|
|
81
|
-
}}
|
|
82
|
-
onKeyDown={(e) => {
|
|
83
|
-
if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return;
|
|
84
|
-
e.preventDefault();
|
|
85
|
-
if (!compareEnabled) {
|
|
86
|
-
track("button", "Compare original");
|
|
87
|
-
commitCompare(true);
|
|
88
|
-
}
|
|
89
|
-
}}
|
|
90
|
-
onKeyUp={(e) => {
|
|
91
|
-
if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return;
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
commitCompare(false);
|
|
94
|
-
}}
|
|
95
|
-
title="Hold to show original"
|
|
96
|
-
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
97
|
-
>
|
|
98
|
-
<Compare size={12} />
|
|
99
|
-
</button>
|
|
100
|
-
<span className="flex min-w-0 items-center gap-1" title={runtimeStatus.message}>
|
|
101
|
-
<span
|
|
102
|
-
data-flat-grade-status-dot="true"
|
|
103
|
-
title={runtimeStatus.message}
|
|
104
|
-
className={`h-[5px] w-[5px] flex-shrink-0 rounded-full ${STATUS_DOT_CLASS[runtimeStatus.state]}`}
|
|
105
|
-
/>
|
|
106
|
-
<span
|
|
107
|
-
data-flat-grade-status-message="true"
|
|
108
|
-
className="max-w-[84px] truncate text-[9px] text-panel-text-4"
|
|
109
|
-
>
|
|
110
|
-
{runtimeStatus.message}
|
|
111
|
-
</span>
|
|
112
|
-
</span>
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
data-flat-grade-reset="true"
|
|
116
|
-
title="Reset color grading"
|
|
117
|
-
onClick={(e) => {
|
|
118
|
-
e.stopPropagation();
|
|
119
|
-
track("button", "Reset color grading");
|
|
120
|
-
resetGrading();
|
|
121
|
-
}}
|
|
122
|
-
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1"
|
|
123
|
-
>
|
|
124
|
-
<RotateCcw size={12} />
|
|
125
|
-
</button>
|
|
126
|
-
</span>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const ADJUST_SLIDERS: Array<{
|
|
131
|
-
key: HfColorGradingAdjustKey;
|
|
132
|
-
label: string;
|
|
133
|
-
min: number;
|
|
134
|
-
max: number;
|
|
135
|
-
step: number;
|
|
136
|
-
}> = [
|
|
137
|
-
{ key: "exposure", label: "Exposure", min: -200, max: 200, step: 5 },
|
|
138
|
-
{ key: "contrast", label: "Contrast", min: -100, max: 100, step: 1 },
|
|
139
|
-
{ key: "highlights", label: "Highlights", min: -100, max: 100, step: 1 },
|
|
140
|
-
{ key: "shadows", label: "Shadows", min: -100, max: 100, step: 1 },
|
|
141
|
-
{ key: "whites", label: "White Point", min: -100, max: 100, step: 1 },
|
|
142
|
-
{ key: "blacks", label: "Black Point", min: -100, max: 100, step: 1 },
|
|
143
|
-
{ key: "temperature", label: "Warmth", min: -100, max: 100, step: 1 },
|
|
144
|
-
{ key: "tint", label: "Tint", min: -100, max: 100, step: 1 },
|
|
145
|
-
{ key: "vibrance", label: "Vibrance", min: -100, max: 100, step: 1 },
|
|
146
|
-
{ key: "saturation", label: "Saturation", min: -100, max: 100, step: 1 },
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
function visibleIntensity(grading: NormalizedHfColorGrading): number {
|
|
150
|
-
// Earlier drafts could persist 0% strength; the next manual edit should revive visible grading.
|
|
151
|
-
return grading.intensity === 0 ? 1 : grading.intensity;
|
|
152
|
-
}
|
|
37
|
+
export { FlatColorGradingAccessory } from "./propertyPanelFlatColorGradingAccessory";
|
|
153
38
|
|
|
154
39
|
function formatAdjustValue(key: HfColorGradingAdjustKey, rawPercent: number): string {
|
|
155
40
|
if (key === "exposure") {
|
|
@@ -159,30 +44,17 @@ function formatAdjustValue(key: HfColorGradingAdjustKey, rawPercent: number): st
|
|
|
159
44
|
return `${Math.round(rawPercent)}%`;
|
|
160
45
|
}
|
|
161
46
|
|
|
162
|
-
const DETAIL_SLIDERS: Array<{
|
|
163
|
-
key: HfColorGradingDetailKey;
|
|
164
|
-
label: string;
|
|
165
|
-
defaultValue: number;
|
|
166
|
-
}> = [
|
|
167
|
-
{ key: "vignette", label: "Vignette", defaultValue: 0 },
|
|
168
|
-
{ key: "vignetteMidpoint", label: "Midpoint", defaultValue: 0.5 },
|
|
169
|
-
{ key: "vignetteRoundness", label: "Roundness", defaultValue: 0 },
|
|
170
|
-
{ key: "vignetteFeather", label: "Feather", defaultValue: 0.65 },
|
|
171
|
-
{ key: "grain", label: "Grain", defaultValue: 0 },
|
|
172
|
-
{ key: "grainSize", label: "Grain Size", defaultValue: 0.25 },
|
|
173
|
-
{ key: "grainRoughness", label: "Roughness", defaultValue: 0.5 },
|
|
174
|
-
];
|
|
175
47
|
const detailByKey = (key: HfColorGradingDetailKey) => {
|
|
176
|
-
const spec =
|
|
48
|
+
const spec = COLOR_GRADING_DETAIL_SLIDERS.find((candidate) => candidate.key === key);
|
|
177
49
|
if (!spec) throw new Error(`Unknown color grading detail key: ${key}`);
|
|
178
50
|
return spec;
|
|
179
51
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
|
|
52
|
+
|
|
53
|
+
function resolveColorGrading(grading: Parameters<typeof normalizeHfColorGrading>[0]) {
|
|
54
|
+
const resolved = normalizeHfColorGrading(grading);
|
|
55
|
+
if (!resolved) throw new Error("Missing resolved color grading");
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
186
58
|
|
|
187
59
|
function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
|
|
188
60
|
if (metadata?.color.dynamicRange !== "hdr") return null;
|
|
@@ -237,6 +109,7 @@ export function FlatColorGradingSection({
|
|
|
237
109
|
mediaMetadata,
|
|
238
110
|
presetPreviews,
|
|
239
111
|
onRequestPresetPreviews,
|
|
112
|
+
captureGradedFrame,
|
|
240
113
|
}: {
|
|
241
114
|
grading: NormalizedHfColorGrading;
|
|
242
115
|
assets: string[];
|
|
@@ -254,6 +127,7 @@ export function FlatColorGradingSection({
|
|
|
254
127
|
mediaMetadata: MediaMetadata | null;
|
|
255
128
|
presetPreviews: ColorGradingPresetPreviews;
|
|
256
129
|
onRequestPresetPreviews: () => void;
|
|
130
|
+
captureGradedFrame: ColorGradingControllerState["captureGradedFrame"];
|
|
257
131
|
}) {
|
|
258
132
|
const track = useTrackDesignInput();
|
|
259
133
|
const lutInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -265,70 +139,65 @@ export function FlatColorGradingSection({
|
|
|
265
139
|
);
|
|
266
140
|
const lut = grading.lut;
|
|
267
141
|
const selectedLutName = lut?.src ? (lut.src.split("/").pop() ?? lut.src) : null;
|
|
142
|
+
const resolvedGrading = useMemo(() => resolveColorGrading(grading), [grading]);
|
|
143
|
+
const secondaryInputGrading = useMemo(
|
|
144
|
+
() =>
|
|
145
|
+
resolveColorGrading({
|
|
146
|
+
intensity: 1,
|
|
147
|
+
adjust: resolvedGrading.adjust,
|
|
148
|
+
wheels: resolvedGrading.wheels,
|
|
149
|
+
curves: resolvedGrading.curves,
|
|
150
|
+
hueCurves: resolvedGrading.hueCurves,
|
|
151
|
+
colorSpace: resolvedGrading.colorSpace,
|
|
152
|
+
}),
|
|
153
|
+
[
|
|
154
|
+
resolvedGrading.adjust,
|
|
155
|
+
resolvedGrading.colorSpace,
|
|
156
|
+
resolvedGrading.curves,
|
|
157
|
+
resolvedGrading.hueCurves,
|
|
158
|
+
resolvedGrading.wheels,
|
|
159
|
+
],
|
|
160
|
+
);
|
|
161
|
+
const actions = createColorGradingActions(grading, onCommitColorGrading);
|
|
162
|
+
const scopesRefreshKey = useMemo(() => serializeHfColorGrading(grading), [grading]);
|
|
268
163
|
|
|
269
164
|
useEffect(() => {
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
HF_COLOR_GRADING_GRADE_PRESETS.some((preset) => !presetPreviews.images[preset.id])
|
|
273
|
-
) {
|
|
274
|
-
onRequestPresetPreviews();
|
|
275
|
-
}
|
|
276
|
-
}, [onRequestPresetPreviews, presetPreviews.images, presetPreviews.status]);
|
|
165
|
+
if (presetPreviews.status === "idle") onRequestPresetPreviews();
|
|
166
|
+
}, [onRequestPresetPreviews, presetPreviews.status]);
|
|
277
167
|
|
|
278
168
|
const resolvePreset = (presetId: string) => {
|
|
279
169
|
const resolved = normalizeHfColorGrading({ preset: presetId, lut: grading.lut });
|
|
280
|
-
return resolved
|
|
170
|
+
return resolved
|
|
171
|
+
? {
|
|
172
|
+
...resolved,
|
|
173
|
+
effects: grading.effects,
|
|
174
|
+
palette: grading.palette,
|
|
175
|
+
}
|
|
176
|
+
: grading;
|
|
281
177
|
};
|
|
282
178
|
|
|
283
179
|
useEffect(() => () => onPreviewColorGrading(null), [onPreviewColorGrading]);
|
|
284
|
-
const updateIntensity = (value: number) => {
|
|
285
|
-
onCommitColorGrading({ ...grading, intensity: value / 100 });
|
|
286
|
-
};
|
|
287
|
-
const applyLut = (src: string | null, intensity = 1) => {
|
|
288
|
-
onCommitColorGrading({
|
|
289
|
-
...grading,
|
|
290
|
-
intensity: visibleIntensity(grading),
|
|
291
|
-
lut: src ? { src, intensity } : null,
|
|
292
|
-
});
|
|
293
|
-
};
|
|
294
|
-
const importLuts = async (files: FileList | null) => {
|
|
295
|
-
if (!files?.length || !onImportAssets) return;
|
|
296
|
-
const uploaded = await onImportAssets(files, "assets/luts");
|
|
297
|
-
const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
|
|
298
|
-
if (firstLut) {
|
|
299
|
-
track("button", "Import LUT");
|
|
300
|
-
applyLut(firstLut, 1);
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
180
|
|
|
304
181
|
const renderDetailSlider = (key: HfColorGradingDetailKey) => {
|
|
305
182
|
const spec = detailByKey(key);
|
|
306
183
|
const value = grading.details[key];
|
|
307
|
-
const
|
|
184
|
+
const defaultValue = normalizedColorGradingDefault(spec);
|
|
185
|
+
const isSet = Math.abs(value - defaultValue) > 1e-4;
|
|
308
186
|
return (
|
|
309
187
|
<FlatSlider
|
|
310
188
|
key={key}
|
|
311
189
|
label={spec.label}
|
|
312
|
-
value={Math.round(value *
|
|
313
|
-
min={
|
|
314
|
-
max={
|
|
190
|
+
value={Math.round(value * spec.scale)}
|
|
191
|
+
min={spec.min}
|
|
192
|
+
max={spec.max}
|
|
193
|
+
step={spec.step}
|
|
315
194
|
tier={isSet ? "explicitCustom" : "default"}
|
|
316
|
-
displayValue={`${Math.round(value *
|
|
195
|
+
displayValue={`${Math.round(value * spec.scale)}${spec.suffix}`}
|
|
317
196
|
centerTick={key === "vignetteRoundness"}
|
|
318
197
|
onCommit={(next) =>
|
|
319
|
-
onCommitColorGrading(
|
|
320
|
-
...grading,
|
|
321
|
-
intensity: visibleIntensity(grading),
|
|
322
|
-
details: { ...grading.details, [key]: next / 100 },
|
|
323
|
-
})
|
|
324
|
-
}
|
|
325
|
-
onReset={() =>
|
|
326
|
-
onCommitColorGrading({
|
|
327
|
-
...grading,
|
|
328
|
-
intensity: visibleIntensity(grading),
|
|
329
|
-
details: { ...grading.details, [key]: spec.defaultValue },
|
|
330
|
-
})
|
|
198
|
+
onCommitColorGrading(colorGradingWithDetail(grading, key, next / spec.scale))
|
|
331
199
|
}
|
|
200
|
+
onReset={() => onCommitColorGrading(colorGradingWithDetail(grading, key, defaultValue))}
|
|
332
201
|
/>
|
|
333
202
|
);
|
|
334
203
|
};
|
|
@@ -336,7 +205,20 @@ export function FlatColorGradingSection({
|
|
|
336
205
|
return (
|
|
337
206
|
<div className="space-y-1.5">
|
|
338
207
|
<HdrBanner metadata={mediaMetadata} />
|
|
208
|
+
<PropertyPanelColorScopes
|
|
209
|
+
captureFrame={() => captureGradedFrame()}
|
|
210
|
+
refreshKey={scopesRefreshKey}
|
|
211
|
+
/>
|
|
339
212
|
<div data-flat-grade-presets="true" className="space-y-1.5">
|
|
213
|
+
{presetPreviews.status === "unavailable" && (
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={onRequestPresetPreviews}
|
|
217
|
+
className="text-[10px] font-medium text-panel-accent hover:text-panel-accent/80"
|
|
218
|
+
>
|
|
219
|
+
Retry look previews
|
|
220
|
+
</button>
|
|
221
|
+
)}
|
|
340
222
|
<div data-flat-grade-preset-group="presets" className={FLAT_PREVIEW_GRID}>
|
|
341
223
|
{HF_COLOR_GRADING_GRADE_PRESETS.map((preset) => {
|
|
342
224
|
const label = preset.label;
|
|
@@ -395,10 +277,105 @@ export function FlatColorGradingSection({
|
|
|
395
277
|
max={100}
|
|
396
278
|
tier={grading.intensity === 1 ? "default" : "explicitCustom"}
|
|
397
279
|
displayValue={`${Math.round(grading.intensity * 100)}%`}
|
|
398
|
-
onCommit={
|
|
399
|
-
onReset={() =>
|
|
280
|
+
onCommit={actions.setIntensityPercent}
|
|
281
|
+
onReset={() => actions.setIntensityPercent(100)}
|
|
400
282
|
/>
|
|
401
283
|
|
|
284
|
+
<div className="space-y-0.5 border-t border-panel-hairline pt-1.5">
|
|
285
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
286
|
+
Primary
|
|
287
|
+
</div>
|
|
288
|
+
{COLOR_GRADING_ADJUST_SLIDERS.map((slider) => {
|
|
289
|
+
const rawPercent = grading.adjust[slider.key] * slider.scale;
|
|
290
|
+
const isSet = Math.abs(grading.adjust[slider.key]) > 1e-6;
|
|
291
|
+
return (
|
|
292
|
+
<div key={slider.key} data-flat-grade-adjust="true">
|
|
293
|
+
<FlatSlider
|
|
294
|
+
label={slider.label}
|
|
295
|
+
value={rawPercent}
|
|
296
|
+
min={slider.min}
|
|
297
|
+
max={slider.max}
|
|
298
|
+
step={slider.step}
|
|
299
|
+
tier={isSet ? "explicitCustom" : "default"}
|
|
300
|
+
displayValue={formatAdjustValue(slider.key, rawPercent)}
|
|
301
|
+
centerTick
|
|
302
|
+
onCommit={(next) =>
|
|
303
|
+
onCommitColorGrading(
|
|
304
|
+
colorGradingWithAdjust(grading, slider.key, next / slider.scale),
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
onReset={() => onCommitColorGrading(colorGradingWithAdjust(grading, slider.key, 0))}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
})}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div className="space-y-1.5 border-t border-panel-hairline pt-1.5">
|
|
315
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
316
|
+
Color wheels
|
|
317
|
+
</div>
|
|
318
|
+
<ColorWheels
|
|
319
|
+
value={resolvedGrading.wheels}
|
|
320
|
+
onPreview={(wheels) =>
|
|
321
|
+
onPreviewColorGrading({
|
|
322
|
+
...grading,
|
|
323
|
+
intensity: visibleColorGradingIntensity(grading),
|
|
324
|
+
wheels,
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
onCommit={(wheels) =>
|
|
328
|
+
onCommitColorGrading({
|
|
329
|
+
...grading,
|
|
330
|
+
intensity: visibleColorGradingIntensity(grading),
|
|
331
|
+
wheels,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div className="space-y-1.5 border-t border-panel-hairline pt-1.5">
|
|
338
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
339
|
+
Curves
|
|
340
|
+
</div>
|
|
341
|
+
<ColorCurves
|
|
342
|
+
value={{ curves: resolvedGrading.curves, hueCurves: resolvedGrading.hueCurves }}
|
|
343
|
+
onPreview={({ curves, hueCurves }) =>
|
|
344
|
+
onPreviewColorGrading({
|
|
345
|
+
...grading,
|
|
346
|
+
intensity: visibleColorGradingIntensity(grading),
|
|
347
|
+
curves,
|
|
348
|
+
hueCurves,
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
onCommit={({ curves, hueCurves }) =>
|
|
352
|
+
onCommitColorGrading({
|
|
353
|
+
...grading,
|
|
354
|
+
intensity: visibleColorGradingIntensity(grading),
|
|
355
|
+
curves,
|
|
356
|
+
hueCurves,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<div className="space-y-1.5 border-t border-panel-hairline pt-1.5">
|
|
363
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
364
|
+
Secondary color
|
|
365
|
+
</div>
|
|
366
|
+
<PropertyPanelColorSecondary
|
|
367
|
+
secondaries={resolvedGrading.secondaries}
|
|
368
|
+
captureFrame={() => captureGradedFrame({ grading: secondaryInputGrading })}
|
|
369
|
+
onCommit={(secondaries) =>
|
|
370
|
+
onCommitColorGrading({
|
|
371
|
+
...grading,
|
|
372
|
+
intensity: visibleColorGradingIntensity(grading),
|
|
373
|
+
secondaries,
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
402
379
|
<div className="border-t border-panel-hairline pt-1.5">
|
|
403
380
|
<button
|
|
404
381
|
type="button"
|
|
@@ -430,7 +407,7 @@ export function FlatColorGradingSection({
|
|
|
430
407
|
onChange={(e) => {
|
|
431
408
|
const src = e.target.value;
|
|
432
409
|
track("select", "Custom LUT");
|
|
433
|
-
applyLut(src || null, src && lut?.src === src ? lut.intensity : 1);
|
|
410
|
+
actions.applyLut(src || null, src && lut?.src === src ? lut.intensity : 1);
|
|
434
411
|
}}
|
|
435
412
|
className="border-b border-panel-border-input/50 bg-transparent font-mono text-[10px] text-panel-text-3 outline-none hover:border-panel-border-input"
|
|
436
413
|
>
|
|
@@ -456,7 +433,9 @@ export function FlatColorGradingSection({
|
|
|
456
433
|
accept=".cube"
|
|
457
434
|
className="hidden"
|
|
458
435
|
onChange={(e) => {
|
|
459
|
-
void
|
|
436
|
+
void actions.importLut(e.currentTarget.files, onImportAssets, () =>
|
|
437
|
+
track("button", "Import LUT"),
|
|
438
|
+
);
|
|
460
439
|
e.currentTarget.value = "";
|
|
461
440
|
}}
|
|
462
441
|
/>
|
|
@@ -469,52 +448,14 @@ export function FlatColorGradingSection({
|
|
|
469
448
|
max={100}
|
|
470
449
|
tier={lut.intensity === 1 ? "default" : "explicitCustom"}
|
|
471
450
|
displayValue={`${Math.round((lut.intensity ?? 1) * 100)}%`}
|
|
472
|
-
onCommit={(v) => applyLut(lut.src, v / 100)}
|
|
473
|
-
onReset={() => applyLut(lut.src, 1)}
|
|
451
|
+
onCommit={(v) => actions.applyLut(lut.src, v / 100)}
|
|
452
|
+
onReset={() => actions.applyLut(lut.src, 1)}
|
|
474
453
|
/>
|
|
475
454
|
)}
|
|
476
455
|
</div>
|
|
477
456
|
)}
|
|
478
457
|
</div>
|
|
479
458
|
|
|
480
|
-
<div className="space-y-0.5 border-t border-panel-hairline pt-1.5">
|
|
481
|
-
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
482
|
-
Adjust
|
|
483
|
-
</div>
|
|
484
|
-
{ADJUST_SLIDERS.map((slider) => {
|
|
485
|
-
const rawPercent = grading.adjust[slider.key] * 100;
|
|
486
|
-
const isSet = Math.abs(grading.adjust[slider.key]) > 1e-6;
|
|
487
|
-
return (
|
|
488
|
-
<div key={slider.key} data-flat-grade-adjust="true">
|
|
489
|
-
<FlatSlider
|
|
490
|
-
label={slider.label}
|
|
491
|
-
value={rawPercent}
|
|
492
|
-
min={slider.min}
|
|
493
|
-
max={slider.max}
|
|
494
|
-
step={slider.step}
|
|
495
|
-
tier={isSet ? "explicitCustom" : "default"}
|
|
496
|
-
displayValue={formatAdjustValue(slider.key, rawPercent)}
|
|
497
|
-
centerTick
|
|
498
|
-
onCommit={(next) =>
|
|
499
|
-
onCommitColorGrading({
|
|
500
|
-
...grading,
|
|
501
|
-
intensity: visibleIntensity(grading),
|
|
502
|
-
adjust: { ...grading.adjust, [slider.key]: next / 100 },
|
|
503
|
-
})
|
|
504
|
-
}
|
|
505
|
-
onReset={() =>
|
|
506
|
-
onCommitColorGrading({
|
|
507
|
-
...grading,
|
|
508
|
-
intensity: visibleIntensity(grading),
|
|
509
|
-
adjust: { ...grading.adjust, [slider.key]: 0 },
|
|
510
|
-
})
|
|
511
|
-
}
|
|
512
|
-
/>
|
|
513
|
-
</div>
|
|
514
|
-
);
|
|
515
|
-
})}
|
|
516
|
-
</div>
|
|
517
|
-
|
|
518
459
|
<div className="space-y-1.5 border-t border-panel-hairline pt-1.5">
|
|
519
460
|
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
520
461
|
Finish
|
|
@@ -545,8 +486,8 @@ export function FlatColorGradingSection({
|
|
|
545
486
|
</div>
|
|
546
487
|
{detailSettingsOpen && (
|
|
547
488
|
<div className="space-y-0.5 border-l-2 border-panel-border-input pl-2.5">
|
|
548
|
-
{(detailSettingsOpen === "vignette" ?
|
|
549
|
-
renderDetailSlider,
|
|
489
|
+
{(detailSettingsOpen === "vignette" ? VIGNETTE_TUNE_SLIDERS : GRAIN_TUNE_SLIDERS).map(
|
|
490
|
+
(slider) => renderDetailSlider(slider.key),
|
|
550
491
|
)}
|
|
551
492
|
</div>
|
|
552
493
|
)}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { GradingNumberField } from "./propertyPanelGradingNumberField";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
document.body.innerHTML = "";
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function changeInput(input: HTMLInputElement, value: string) {
|
|
15
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
16
|
+
if (!setter) throw new Error("Expected the native input setter");
|
|
17
|
+
setter.call(input, value);
|
|
18
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderField() {
|
|
22
|
+
const callbacks = {
|
|
23
|
+
onBegin: vi.fn(),
|
|
24
|
+
onPreview: vi.fn(),
|
|
25
|
+
onSettle: vi.fn(),
|
|
26
|
+
onCancel: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
const host = document.body.appendChild(document.createElement("div"));
|
|
29
|
+
const root = createRoot(host);
|
|
30
|
+
act(() =>
|
|
31
|
+
root.render(
|
|
32
|
+
<GradingNumberField label="Level" value={10} min={-100} max={100} {...callbacks} />,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
const input = host.querySelector("input");
|
|
36
|
+
if (!input) throw new Error("Expected a grading field");
|
|
37
|
+
return { root, input, callbacks };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("GradingNumberField", () => {
|
|
41
|
+
it("does not begin or settle an untouched focus and blur", () => {
|
|
42
|
+
const { root, input, callbacks } = renderField();
|
|
43
|
+
act(() => input.focus());
|
|
44
|
+
act(() => input.blur());
|
|
45
|
+
|
|
46
|
+
expect(callbacks.onBegin).not.toHaveBeenCalled();
|
|
47
|
+
expect(callbacks.onPreview).not.toHaveBeenCalled();
|
|
48
|
+
expect(callbacks.onSettle).not.toHaveBeenCalled();
|
|
49
|
+
expect(callbacks.onCancel).not.toHaveBeenCalled();
|
|
50
|
+
act(() => root.unmount());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("cancels a real edit that returns to its baseline", () => {
|
|
54
|
+
const { root, input, callbacks } = renderField();
|
|
55
|
+
act(() => input.focus());
|
|
56
|
+
act(() => changeInput(input, "25"));
|
|
57
|
+
act(() => changeInput(input, "10"));
|
|
58
|
+
act(() => input.blur());
|
|
59
|
+
|
|
60
|
+
expect(callbacks.onBegin).toHaveBeenCalledOnce();
|
|
61
|
+
expect(callbacks.onSettle).not.toHaveBeenCalled();
|
|
62
|
+
expect(callbacks.onCancel).toHaveBeenCalledOnce();
|
|
63
|
+
act(() => root.unmount());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("previews valid text and settles once on Enter", () => {
|
|
67
|
+
const { root, input, callbacks } = renderField();
|
|
68
|
+
act(() => input.focus());
|
|
69
|
+
act(() => changeInput(input, "-25"));
|
|
70
|
+
act(() => {
|
|
71
|
+
input.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(callbacks.onBegin).toHaveBeenCalledOnce();
|
|
75
|
+
expect(callbacks.onPreview).toHaveBeenLastCalledWith(-25);
|
|
76
|
+
expect(callbacks.onSettle).toHaveBeenCalledOnce();
|
|
77
|
+
act(() => root.unmount());
|
|
78
|
+
});
|
|
79
|
+
});
|