@hyperframes/studio 0.6.107 → 0.6.109
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-DgsMQSvV.js → hyperframes-player-67pq7USK.js} +2 -2
- package/dist/assets/index-BVqybwMG.css +1 -0
- package/dist/assets/{index-zIK7PWvk.js → index-CFCe0Xvn.js} +1 -1
- package/dist/assets/{index-P52mbTRb.js → index-SlMJloLR.js} +1 -1
- package/dist/assets/index-ho_f4axK.js +296 -0
- package/dist/index.html +2 -2
- package/package.json +5 -5
- package/src/App.tsx +1 -0
- package/src/components/StudioRightPanel.tsx +161 -75
- package/src/components/editor/PropertyPanel.tsx +26 -1
- package/src/components/editor/domEditOverlayGeometry.ts +2 -11
- package/src/components/editor/domEditingDom.ts +21 -0
- package/src/components/editor/domEditingElement.ts +2 -11
- package/src/components/editor/manualEditingAvailability.test.ts +12 -0
- package/src/components/editor/manualEditingAvailability.ts +6 -0
- package/src/components/editor/propertyPanelColorGradingControls.tsx +475 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +355 -0
- package/src/components/editor/propertyPanelHelpers.ts +2 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +32 -37
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/PanelLayoutContext.tsx +6 -0
- package/src/hooks/useDomEditCommits.ts +7 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.ts +82 -26
- package/src/hooks/useDomEditWiring.ts +1 -0
- package/src/hooks/useDomSelection.ts +2 -10
- package/src/hooks/usePanelLayout.ts +26 -1
- package/src/hooks/useStudioContextValue.ts +8 -3
- package/src/icons/SystemIcons.tsx +4 -0
- package/src/player/lib/timelineDOM.test.ts +36 -0
- package/src/player/lib/timelineDOM.ts +2 -0
- package/src/player/lib/timelineElementHelpers.ts +18 -1
- package/src/styles/studio.css +81 -0
- package/src/utils/mediaTypes.ts +1 -0
- package/src/utils/studioHelpers.ts +6 -0
- package/dist/assets/index-BrhJl2JY.css +0 -1
- package/dist/assets/index-CVHV-S-B.js +0 -296
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
HF_COLOR_GRADING_PRESETS,
|
|
4
|
+
normalizeHfColorGrading,
|
|
5
|
+
type HfColorGradingAdjustKey,
|
|
6
|
+
type NormalizedHfColorGrading,
|
|
7
|
+
} from "@hyperframes/core/color-grading";
|
|
8
|
+
import { Minus, Plus, RotateCcw } from "../../icons/SystemIcons";
|
|
9
|
+
import { LUT_EXT } from "../../utils/mediaTypes";
|
|
10
|
+
import { LABEL } from "./propertyPanelHelpers";
|
|
11
|
+
|
|
12
|
+
const LUT_UPLOAD_DIR = "assets/luts";
|
|
13
|
+
const SLIDER_THUMB_SIZE = 10;
|
|
14
|
+
const SLIDER_THUMB_RADIUS = SLIDER_THUMB_SIZE / 2;
|
|
15
|
+
|
|
16
|
+
const SLIDERS: Array<{
|
|
17
|
+
key: HfColorGradingAdjustKey;
|
|
18
|
+
label: string;
|
|
19
|
+
min: number;
|
|
20
|
+
max: number;
|
|
21
|
+
step: number;
|
|
22
|
+
scale: number;
|
|
23
|
+
suffix: string;
|
|
24
|
+
}> = [
|
|
25
|
+
{ key: "exposure", label: "Exposure", min: -200, max: 200, step: 5, scale: 100, suffix: "" },
|
|
26
|
+
{ key: "contrast", label: "Contrast", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
27
|
+
{
|
|
28
|
+
key: "highlights",
|
|
29
|
+
label: "Highlights",
|
|
30
|
+
min: -100,
|
|
31
|
+
max: 100,
|
|
32
|
+
step: 1,
|
|
33
|
+
scale: 100,
|
|
34
|
+
suffix: "%",
|
|
35
|
+
},
|
|
36
|
+
{ key: "shadows", label: "Shadows", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
37
|
+
{ key: "whites", label: "Whites", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
38
|
+
{ key: "blacks", label: "Blacks", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
39
|
+
{ key: "temperature", label: "Warmth", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
40
|
+
{ key: "tint", label: "Tint", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
41
|
+
{ key: "saturation", label: "Saturation", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function clampNumber(value: number, min: number, max: number): number {
|
|
45
|
+
if (!Number.isFinite(value)) return min;
|
|
46
|
+
return Math.min(max, Math.max(min, value));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatNumericInput(value: number, scale: number): string {
|
|
50
|
+
const scaled = value / scale;
|
|
51
|
+
return scale === 100 ? scaled.toFixed(2) : String(Math.round(scaled));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseNumericInput(value: string, scale: number): number | null {
|
|
55
|
+
const parsed = Number(value);
|
|
56
|
+
if (!Number.isFinite(parsed)) return null;
|
|
57
|
+
return parsed * scale;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function tickPercent(value: number, min: number, max: number): number {
|
|
61
|
+
if (max <= min) return 0;
|
|
62
|
+
return ((value - min) / (max - min)) * 100;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ColorGradingSliderControl({
|
|
66
|
+
label,
|
|
67
|
+
value,
|
|
68
|
+
min,
|
|
69
|
+
max,
|
|
70
|
+
step,
|
|
71
|
+
neutral = min,
|
|
72
|
+
scale = 1,
|
|
73
|
+
suffix = "",
|
|
74
|
+
displayValue,
|
|
75
|
+
disabled,
|
|
76
|
+
onCommit,
|
|
77
|
+
onReset,
|
|
78
|
+
}: {
|
|
79
|
+
label: string;
|
|
80
|
+
value: number;
|
|
81
|
+
min: number;
|
|
82
|
+
max: number;
|
|
83
|
+
step: number;
|
|
84
|
+
neutral?: number;
|
|
85
|
+
scale?: number;
|
|
86
|
+
suffix?: string;
|
|
87
|
+
displayValue: string;
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
onCommit: (nextValue: number) => void;
|
|
90
|
+
onReset?: () => void;
|
|
91
|
+
}) {
|
|
92
|
+
const [draftState, setDraftState] = useState<{ value: number; source: number } | null>(null);
|
|
93
|
+
const [inputDraft, setInputDraft] = useState<{ value: string; source: number } | null>(null);
|
|
94
|
+
const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
95
|
+
const valueRef = useRef(value);
|
|
96
|
+
valueRef.current = value;
|
|
97
|
+
|
|
98
|
+
useEffect(
|
|
99
|
+
() => () => {
|
|
100
|
+
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
101
|
+
},
|
|
102
|
+
[],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const clampDraft = useCallback(
|
|
106
|
+
(nextValue: number) => clampNumber(nextValue, min, max),
|
|
107
|
+
[max, min],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const setLocalDraft = useCallback(
|
|
111
|
+
(nextValue: number) => {
|
|
112
|
+
const clamped = clampDraft(nextValue);
|
|
113
|
+
const source = valueRef.current;
|
|
114
|
+
setDraftState({ value: clamped, source });
|
|
115
|
+
setInputDraft({ value: formatNumericInput(clamped, scale), source });
|
|
116
|
+
return clamped;
|
|
117
|
+
},
|
|
118
|
+
[clampDraft, scale],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const commitDraft = useCallback(
|
|
122
|
+
(nextValue: number) => {
|
|
123
|
+
const clamped = setLocalDraft(nextValue);
|
|
124
|
+
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
125
|
+
if (clamped !== valueRef.current) onCommit(clamped);
|
|
126
|
+
},
|
|
127
|
+
[onCommit, setLocalDraft],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const scheduleCommit = useCallback(
|
|
131
|
+
(nextValue: number) => {
|
|
132
|
+
const clamped = setLocalDraft(nextValue);
|
|
133
|
+
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
134
|
+
commitTimerRef.current = setTimeout(() => {
|
|
135
|
+
if (clamped !== valueRef.current) onCommit(clamped);
|
|
136
|
+
}, 40);
|
|
137
|
+
},
|
|
138
|
+
[onCommit, setLocalDraft],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const draft = draftState?.source === value ? draftState.value : value;
|
|
142
|
+
const inputValue =
|
|
143
|
+
inputDraft?.source === value ? inputDraft.value : formatNumericInput(draft, scale);
|
|
144
|
+
|
|
145
|
+
const commitInputDraft = useCallback(() => {
|
|
146
|
+
const parsed = parseNumericInput(inputValue, scale);
|
|
147
|
+
if (parsed === null) {
|
|
148
|
+
setInputDraft(null);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
commitDraft(parsed);
|
|
152
|
+
}, [commitDraft, inputValue, scale]);
|
|
153
|
+
|
|
154
|
+
const nudge = useCallback(
|
|
155
|
+
(direction: -1 | 1) => {
|
|
156
|
+
commitDraft(draft + step * direction);
|
|
157
|
+
},
|
|
158
|
+
[commitDraft, draft, step],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const range = max - min;
|
|
162
|
+
const valuePercent = range === 0 ? 0 : ((draft - min) / range) * 100;
|
|
163
|
+
const neutralPercent = range === 0 ? 0 : ((neutral - min) / range) * 100;
|
|
164
|
+
const fillLeft = Math.min(valuePercent, neutralPercent);
|
|
165
|
+
const fillWidth = Math.abs(valuePercent - neutralPercent);
|
|
166
|
+
const ticks = Array.from(new Set([min, neutral, max])).sort((a, b) => a - b);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="grid min-w-0 gap-1.5 rounded-md bg-panel-input/30 p-2">
|
|
170
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
171
|
+
<span className={`${LABEL} min-w-0 flex-1 truncate`}>{label}</span>
|
|
172
|
+
{onReset && (
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
aria-label={`Reset ${label}`}
|
|
177
|
+
onClick={(event) => {
|
|
178
|
+
event.stopPropagation();
|
|
179
|
+
onReset();
|
|
180
|
+
}}
|
|
181
|
+
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-panel-text-5 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
182
|
+
title={`Reset ${label}`}
|
|
183
|
+
>
|
|
184
|
+
<RotateCcw size={11} />
|
|
185
|
+
</button>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className="relative h-7 min-w-0">
|
|
190
|
+
<div
|
|
191
|
+
data-color-grading-slider-track="true"
|
|
192
|
+
className="pointer-events-none absolute inset-y-0 z-0"
|
|
193
|
+
style={{ left: SLIDER_THUMB_RADIUS, right: SLIDER_THUMB_RADIUS }}
|
|
194
|
+
>
|
|
195
|
+
{ticks.map((tick) => (
|
|
196
|
+
<div
|
|
197
|
+
key={tick}
|
|
198
|
+
data-color-grading-slider-tick="true"
|
|
199
|
+
className="absolute top-1/2 h-3 w-px -translate-y-1/2 bg-panel-text-3"
|
|
200
|
+
style={{ left: `${tickPercent(tick, min, max)}%` }}
|
|
201
|
+
title={String(tick / scale)}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
<div className="absolute left-0 right-0 top-1/2 z-10 h-0.5 -translate-y-1/2 rounded-full bg-panel-border" />
|
|
205
|
+
<div
|
|
206
|
+
className="absolute top-1/2 z-20 h-0.5 -translate-y-1/2 rounded-full bg-studio-accent"
|
|
207
|
+
style={{ left: `${fillLeft}%`, width: `${fillWidth}%` }}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<input
|
|
211
|
+
type="range"
|
|
212
|
+
min={min}
|
|
213
|
+
max={max}
|
|
214
|
+
step={step}
|
|
215
|
+
value={draft}
|
|
216
|
+
disabled={disabled}
|
|
217
|
+
aria-label={label}
|
|
218
|
+
onChange={(event) => scheduleCommit(Number(event.currentTarget.value))}
|
|
219
|
+
onMouseUp={() => commitDraft(draft)}
|
|
220
|
+
onTouchEnd={() => commitDraft(draft)}
|
|
221
|
+
onBlur={() => commitDraft(draft)}
|
|
222
|
+
className="hf-color-grading-range absolute left-0 right-0 top-1/2 z-30 min-w-0 w-full -translate-y-1/2"
|
|
223
|
+
title={displayValue}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div className="flex min-w-0 items-center justify-end gap-1.5">
|
|
228
|
+
<div className="flex flex-shrink-0 items-center rounded-md bg-panel-input px-1.5 py-1">
|
|
229
|
+
<input
|
|
230
|
+
type="number"
|
|
231
|
+
value={inputValue}
|
|
232
|
+
min={min / scale}
|
|
233
|
+
max={max / scale}
|
|
234
|
+
step={step / scale}
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
onChange={(event) =>
|
|
237
|
+
setInputDraft({ value: event.currentTarget.value, source: valueRef.current })
|
|
238
|
+
}
|
|
239
|
+
onBlur={commitInputDraft}
|
|
240
|
+
onKeyDown={(event) => {
|
|
241
|
+
if (event.key === "Enter") {
|
|
242
|
+
event.currentTarget.blur();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (event.key === "ArrowUp") {
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
nudge(1);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (event.key === "ArrowDown") {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
nudge(-1);
|
|
253
|
+
}
|
|
254
|
+
}}
|
|
255
|
+
className="hf-color-grading-number h-5 w-[38px] bg-transparent text-right text-[11px] font-medium tabular-nums text-panel-text-1 outline-none disabled:cursor-not-allowed"
|
|
256
|
+
title={displayValue}
|
|
257
|
+
/>
|
|
258
|
+
{suffix && <span className="ml-0.5 text-[10px] text-panel-text-5">{suffix}</span>}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="flex flex-shrink-0 overflow-hidden rounded-md bg-panel-input">
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
disabled={disabled}
|
|
264
|
+
aria-label={`Decrease ${label}`}
|
|
265
|
+
onClick={() => nudge(-1)}
|
|
266
|
+
className="flex h-7 w-5 items-center justify-center text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
267
|
+
title={`Decrease ${label}`}
|
|
268
|
+
>
|
|
269
|
+
<Minus size={11} />
|
|
270
|
+
</button>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
disabled={disabled}
|
|
274
|
+
aria-label={`Increase ${label}`}
|
|
275
|
+
onClick={() => nudge(1)}
|
|
276
|
+
className="flex h-7 w-5 items-center justify-center border-l border-panel-border text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
277
|
+
title={`Increase ${label}`}
|
|
278
|
+
>
|
|
279
|
+
<Plus size={11} />
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function ColorGradingControls({
|
|
288
|
+
grading,
|
|
289
|
+
assets,
|
|
290
|
+
onImportAssets,
|
|
291
|
+
onCommitColorGrading,
|
|
292
|
+
}: {
|
|
293
|
+
grading: NormalizedHfColorGrading;
|
|
294
|
+
assets: string[];
|
|
295
|
+
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
296
|
+
onCommitColorGrading: (nextGrading: NormalizedHfColorGrading) => void;
|
|
297
|
+
}) {
|
|
298
|
+
const lutInputRef = useRef<HTMLInputElement>(null);
|
|
299
|
+
const lutAssets = useMemo(
|
|
300
|
+
() => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
|
|
301
|
+
[assets],
|
|
302
|
+
);
|
|
303
|
+
const selectedLut = grading.lut?.src ?? "";
|
|
304
|
+
const selectedProjectLut = selectedLut ? (selectedLut.split("/").pop() ?? selectedLut) : null;
|
|
305
|
+
|
|
306
|
+
const applyPreset = (preset: string) => {
|
|
307
|
+
const next = normalizeHfColorGrading({ preset, intensity: 1 });
|
|
308
|
+
if (next) onCommitColorGrading(next);
|
|
309
|
+
};
|
|
310
|
+
const applyLut = (src: string | null, intensity = 1) => {
|
|
311
|
+
onCommitColorGrading({
|
|
312
|
+
...grading,
|
|
313
|
+
intensity: 1,
|
|
314
|
+
lut: src ? { src, intensity } : null,
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
const updateLutIntensity = (value: number) => {
|
|
318
|
+
if (!grading.lut) return;
|
|
319
|
+
applyLut(grading.lut.src, value / 100);
|
|
320
|
+
};
|
|
321
|
+
const importLuts = async (files: FileList | null) => {
|
|
322
|
+
if (!files?.length || !onImportAssets) return;
|
|
323
|
+
const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR);
|
|
324
|
+
const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
|
|
325
|
+
if (firstLut) applyLut(firstLut, 1);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div className="space-y-4">
|
|
330
|
+
<label className="grid min-w-0 gap-1.5">
|
|
331
|
+
<span className={LABEL}>Preset</span>
|
|
332
|
+
<select
|
|
333
|
+
value={String(grading.preset ?? "neutral")}
|
|
334
|
+
onChange={(event) => applyPreset(event.target.value)}
|
|
335
|
+
className="w-full min-w-0 rounded-md bg-panel-input px-3 py-2 text-[11px] font-medium text-panel-text-1 outline-none"
|
|
336
|
+
>
|
|
337
|
+
{HF_COLOR_GRADING_PRESETS.map((preset) => (
|
|
338
|
+
<option key={preset.id} value={preset.id}>
|
|
339
|
+
{preset.label}
|
|
340
|
+
</option>
|
|
341
|
+
))}
|
|
342
|
+
</select>
|
|
343
|
+
</label>
|
|
344
|
+
|
|
345
|
+
<div className="grid min-w-0 gap-1.5">
|
|
346
|
+
<span className={LABEL}>LUT Filter</span>
|
|
347
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_28px] gap-2">
|
|
348
|
+
<select
|
|
349
|
+
value={selectedLut}
|
|
350
|
+
onChange={(event) => {
|
|
351
|
+
const nextSrc = event.target.value;
|
|
352
|
+
applyLut(
|
|
353
|
+
nextSrc || null,
|
|
354
|
+
nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1,
|
|
355
|
+
);
|
|
356
|
+
}}
|
|
357
|
+
className="w-full min-w-0 rounded-md bg-panel-input px-3 py-2 text-[11px] font-medium text-panel-text-1 outline-none"
|
|
358
|
+
title="Uploaded .cube LUT filter"
|
|
359
|
+
>
|
|
360
|
+
<option value="">None</option>
|
|
361
|
+
{lutAssets.length > 0 && (
|
|
362
|
+
<optgroup label="Uploaded LUTs">
|
|
363
|
+
{lutAssets.map((asset) => (
|
|
364
|
+
<option key={asset} value={asset}>
|
|
365
|
+
{asset.split("/").pop() ?? asset}
|
|
366
|
+
</option>
|
|
367
|
+
))}
|
|
368
|
+
</optgroup>
|
|
369
|
+
)}
|
|
370
|
+
</select>
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
disabled={!onImportAssets}
|
|
374
|
+
onClick={(event) => {
|
|
375
|
+
event.stopPropagation();
|
|
376
|
+
lutInputRef.current?.click();
|
|
377
|
+
}}
|
|
378
|
+
className="flex h-8 w-8 items-center justify-center rounded-md bg-panel-input text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
379
|
+
title="Import .cube LUT"
|
|
380
|
+
aria-label="Import .cube LUT"
|
|
381
|
+
>
|
|
382
|
+
<Plus size={13} />
|
|
383
|
+
</button>
|
|
384
|
+
<input
|
|
385
|
+
ref={lutInputRef}
|
|
386
|
+
type="file"
|
|
387
|
+
accept=".cube"
|
|
388
|
+
multiple
|
|
389
|
+
className="hidden"
|
|
390
|
+
onChange={(event) => {
|
|
391
|
+
void importLuts(event.currentTarget.files);
|
|
392
|
+
event.currentTarget.value = "";
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
{grading.lut && (
|
|
397
|
+
<div className="grid gap-2">
|
|
398
|
+
{selectedProjectLut && (
|
|
399
|
+
<div className="flex min-w-0 items-start gap-2 text-[10px] leading-4 text-panel-text-3">
|
|
400
|
+
<span className="mt-[5px] h-1.5 w-1.5 flex-shrink-0 rounded-full bg-studio-accent" />
|
|
401
|
+
<span className="min-w-0">
|
|
402
|
+
<span className="font-medium text-panel-text-2">Uploaded LUT</span>
|
|
403
|
+
{` · ${selectedProjectLut}`}
|
|
404
|
+
</span>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
<ColorGradingSliderControl
|
|
408
|
+
label="LUT Strength"
|
|
409
|
+
value={Math.round((grading.lut.intensity ?? 1) * 100)}
|
|
410
|
+
min={0}
|
|
411
|
+
max={100}
|
|
412
|
+
step={1}
|
|
413
|
+
neutral={0}
|
|
414
|
+
suffix="%"
|
|
415
|
+
displayValue={`${Math.round((grading.lut.intensity ?? 1) * 100)}%`}
|
|
416
|
+
onCommit={updateLutIntensity}
|
|
417
|
+
onReset={() => updateLutIntensity(100)}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<div className="grid min-w-0 grid-cols-2 gap-3">
|
|
424
|
+
{SLIDERS.map((slider, index) => {
|
|
425
|
+
const value = grading.adjust[slider.key] * slider.scale;
|
|
426
|
+
const isExposure = slider.key === "exposure";
|
|
427
|
+
return (
|
|
428
|
+
<div
|
|
429
|
+
key={slider.key}
|
|
430
|
+
className={
|
|
431
|
+
SLIDERS.length % 2 === 1 && index === SLIDERS.length - 1 ? "col-span-2" : ""
|
|
432
|
+
}
|
|
433
|
+
>
|
|
434
|
+
<ColorGradingSliderControl
|
|
435
|
+
label={slider.label}
|
|
436
|
+
value={Math.round(value)}
|
|
437
|
+
min={slider.min}
|
|
438
|
+
max={slider.max}
|
|
439
|
+
step={slider.step}
|
|
440
|
+
neutral={0}
|
|
441
|
+
scale={isExposure ? 100 : 1}
|
|
442
|
+
suffix={isExposure ? "" : slider.suffix}
|
|
443
|
+
displayValue={
|
|
444
|
+
isExposure
|
|
445
|
+
? `${value > 0 ? "+" : ""}${(value / 100).toFixed(2)}`
|
|
446
|
+
: `${Math.round(value)}%`
|
|
447
|
+
}
|
|
448
|
+
onCommit={(next) => {
|
|
449
|
+
onCommitColorGrading({
|
|
450
|
+
...grading,
|
|
451
|
+
intensity: 1,
|
|
452
|
+
adjust: {
|
|
453
|
+
...grading.adjust,
|
|
454
|
+
[slider.key]: next / slider.scale,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}}
|
|
458
|
+
onReset={() => {
|
|
459
|
+
onCommitColorGrading({
|
|
460
|
+
...grading,
|
|
461
|
+
intensity: 1,
|
|
462
|
+
adjust: {
|
|
463
|
+
...grading.adjust,
|
|
464
|
+
[slider.key]: 0,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}}
|
|
468
|
+
/>
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|