@hyperframes/studio 0.7.37 → 0.7.39
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-BBrKzTGd.js +459 -0
- package/dist/assets/index-0P10SwC_.css +1 -0
- package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
- package/dist/assets/index-B9YvRJz1.js +396 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
- package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
- package/dist/chunk-RCLGSZ7C.js.map +1 -0
- package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
- package/dist/index.d.ts +48 -10
- package/dist/index.html +2 -2
- package/dist/index.js +6045 -3507
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -24
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioPreviewArea.tsx +8 -0
- package/src/components/StudioRightPanel.tsx +160 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/DomEditCropHandles.tsx +238 -0
- package/src/components/editor/DomEditOverlay.tsx +111 -108
- package/src/components/editor/DomEditRotateHandle.tsx +41 -0
- package/src/components/editor/PropertyPanel.test.ts +48 -0
- package/src/components/editor/PropertyPanel.tsx +27 -47
- package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
- package/src/components/editor/SnapToolbar.tsx +20 -1
- package/src/components/editor/clipPathHelpers.ts +113 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
- package/src/components/editor/domEditOverlayCrop.ts +115 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayShape.ts +39 -0
- package/src/components/editor/domEditing.test.ts +2 -0
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/marqueeCommit.ts +3 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +32 -125
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
- package/src/components/editor/propertyPanelTypes.ts +113 -0
- package/src/components/editor/snapTargetCollection.ts +2 -3
- package/src/components/editor/useDomEditCompositionRect.ts +68 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
- package/src/components/editor/useDomEditOverlayRects.ts +5 -3
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/sidebar/AssetsTab.test.ts +87 -0
- package/src/components/sidebar/AssetsTab.tsx +113 -15
- package/src/components/sidebar/BlocksTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/domSelectionTestHarness.ts +1 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useCropMode.ts +91 -0
- package/src/hooks/useDomEditCommits.test.tsx +2 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/player/components/ShortcutsPanel.tsx +9 -0
- package/src/player/store/playerStore.ts +14 -0
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
- package/dist/chunk-JND3XUJL.js.map +0 -1
- /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
HF_COLOR_GRADING_PRESETS,
|
|
4
4
|
normalizeHfColorGrading,
|
|
5
5
|
type HfColorGradingAdjustKey,
|
|
6
|
+
type HfColorGradingDetailKey,
|
|
7
|
+
type HfColorGradingEffectKey,
|
|
6
8
|
type NormalizedHfColorGrading,
|
|
7
9
|
} from "@hyperframes/core/color-grading";
|
|
8
|
-
import {
|
|
10
|
+
import { ChevronDown, ChevronRight, Plus, X } from "../../icons/SystemIcons";
|
|
9
11
|
import { LUT_EXT } from "../../utils/mediaTypes";
|
|
10
12
|
import { LABEL } from "./propertyPanelHelpers";
|
|
13
|
+
import { ColorGradingSliderControl } from "./propertyPanelColorGradingSlider";
|
|
11
14
|
|
|
12
15
|
const LUT_UPLOAD_DIR = "assets/luts";
|
|
13
|
-
const SLIDER_THUMB_SIZE = 10;
|
|
14
|
-
const SLIDER_THUMB_RADIUS = SLIDER_THUMB_SIZE / 2;
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
+
const ADJUST_SLIDERS: Array<{
|
|
17
18
|
key: HfColorGradingAdjustKey;
|
|
18
19
|
label: string;
|
|
19
20
|
min: number;
|
|
@@ -34,254 +35,133 @@ const SLIDERS: Array<{
|
|
|
34
35
|
suffix: "%",
|
|
35
36
|
},
|
|
36
37
|
{ key: "shadows", label: "Shadows", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
37
|
-
{
|
|
38
|
-
|
|
38
|
+
{
|
|
39
|
+
key: "whites",
|
|
40
|
+
label: "White Point",
|
|
41
|
+
min: -100,
|
|
42
|
+
max: 100,
|
|
43
|
+
step: 1,
|
|
44
|
+
scale: 100,
|
|
45
|
+
suffix: "%",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "blacks",
|
|
49
|
+
label: "Black Point",
|
|
50
|
+
min: -100,
|
|
51
|
+
max: 100,
|
|
52
|
+
step: 1,
|
|
53
|
+
scale: 100,
|
|
54
|
+
suffix: "%",
|
|
55
|
+
},
|
|
39
56
|
{ key: "temperature", label: "Warmth", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
40
57
|
{ key: "tint", label: "Tint", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
58
|
+
{ key: "vibrance", label: "Vibrance", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
41
59
|
{ key: "saturation", label: "Saturation", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
42
60
|
];
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
}: {
|
|
62
|
+
const DETAIL_SLIDERS: Array<{
|
|
63
|
+
key: HfColorGradingDetailKey;
|
|
79
64
|
label: string;
|
|
80
|
-
value: number;
|
|
81
65
|
min: number;
|
|
82
66
|
max: number;
|
|
83
67
|
step: number;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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]);
|
|
68
|
+
scale: number;
|
|
69
|
+
suffix: string;
|
|
70
|
+
defaultValue?: number;
|
|
71
|
+
}> = [
|
|
72
|
+
{ key: "vignette", label: "Vignette", min: 0, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
73
|
+
{
|
|
74
|
+
key: "vignetteMidpoint",
|
|
75
|
+
label: "Midpoint",
|
|
76
|
+
min: 0,
|
|
77
|
+
max: 100,
|
|
78
|
+
step: 1,
|
|
79
|
+
scale: 100,
|
|
80
|
+
suffix: "%",
|
|
81
|
+
defaultValue: 50,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "vignetteRoundness",
|
|
85
|
+
label: "Roundness",
|
|
86
|
+
min: -100,
|
|
87
|
+
max: 100,
|
|
88
|
+
step: 1,
|
|
89
|
+
scale: 100,
|
|
90
|
+
suffix: "%",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "vignetteFeather",
|
|
94
|
+
label: "Feather",
|
|
95
|
+
min: 0,
|
|
96
|
+
max: 100,
|
|
97
|
+
step: 1,
|
|
98
|
+
scale: 100,
|
|
99
|
+
suffix: "%",
|
|
100
|
+
defaultValue: 65,
|
|
101
|
+
},
|
|
102
|
+
{ key: "grain", label: "Grain", min: 0, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
103
|
+
{
|
|
104
|
+
key: "grainSize",
|
|
105
|
+
label: "Grain Size",
|
|
106
|
+
min: 0,
|
|
107
|
+
max: 100,
|
|
108
|
+
step: 1,
|
|
109
|
+
scale: 100,
|
|
110
|
+
suffix: "%",
|
|
111
|
+
defaultValue: 25,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: "grainRoughness",
|
|
115
|
+
label: "Roughness",
|
|
116
|
+
min: 0,
|
|
117
|
+
max: 100,
|
|
118
|
+
step: 1,
|
|
119
|
+
scale: 100,
|
|
120
|
+
suffix: "%",
|
|
121
|
+
defaultValue: 50,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
153
124
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
125
|
+
type DetailSlider = (typeof DETAIL_SLIDERS)[number];
|
|
126
|
+
type SliderSettings = {
|
|
127
|
+
active?: boolean;
|
|
128
|
+
label: string;
|
|
129
|
+
onClick: () => void;
|
|
130
|
+
};
|
|
160
131
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
const EFFECT_SLIDERS: Array<{
|
|
133
|
+
key: HfColorGradingEffectKey;
|
|
134
|
+
label: string;
|
|
135
|
+
min: number;
|
|
136
|
+
max: number;
|
|
137
|
+
step: number;
|
|
138
|
+
scale: number;
|
|
139
|
+
suffix: string;
|
|
140
|
+
}> = [
|
|
141
|
+
{ key: "blur", label: "Blur", min: 0, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
142
|
+
{ key: "pixelate", label: "Pixelate", min: 0, max: 100, step: 1, scale: 100, suffix: "%" },
|
|
143
|
+
];
|
|
167
144
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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>
|
|
145
|
+
const AMOUNT_DETAIL_SLIDERS = DETAIL_SLIDERS.filter(
|
|
146
|
+
(slider) => slider.key === "vignette" || slider.key === "grain",
|
|
147
|
+
);
|
|
148
|
+
const VIGNETTE_TUNE_SLIDERS = DETAIL_SLIDERS.filter(
|
|
149
|
+
(slider) =>
|
|
150
|
+
slider.key === "vignetteMidpoint" ||
|
|
151
|
+
slider.key === "vignetteRoundness" ||
|
|
152
|
+
slider.key === "vignetteFeather",
|
|
153
|
+
);
|
|
154
|
+
const GRAIN_TUNE_SLIDERS = DETAIL_SLIDERS.filter(
|
|
155
|
+
(slider) => slider.key === "grainSize" || slider.key === "grainRoughness",
|
|
156
|
+
);
|
|
188
157
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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>
|
|
158
|
+
function normalizedDefaultValue(slider: { defaultValue?: number; scale: number }): number {
|
|
159
|
+
return (slider.defaultValue ?? 0) / slider.scale;
|
|
160
|
+
}
|
|
226
161
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
);
|
|
162
|
+
function visibleIntensity(grading: NormalizedHfColorGrading): number {
|
|
163
|
+
// Earlier drafts could persist 0% strength; the next manual edit should revive visible grading.
|
|
164
|
+
return grading.intensity === 0 ? 1 : grading.intensity;
|
|
285
165
|
}
|
|
286
166
|
|
|
287
167
|
export function ColorGradingControls({
|
|
@@ -296,21 +176,37 @@ export function ColorGradingControls({
|
|
|
296
176
|
onCommitColorGrading: (nextGrading: NormalizedHfColorGrading) => void;
|
|
297
177
|
}) {
|
|
298
178
|
const lutInputRef = useRef<HTMLInputElement>(null);
|
|
179
|
+
const [lutOpen, setLutOpen] = useState(false);
|
|
180
|
+
const [detailSettings, setDetailSettings] = useState<"vignette" | "grain" | null>(null);
|
|
299
181
|
const lutAssets = useMemo(
|
|
300
182
|
() => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
|
|
301
183
|
[assets],
|
|
302
184
|
);
|
|
303
185
|
const selectedLut = grading.lut?.src ?? "";
|
|
304
186
|
const selectedProjectLut = selectedLut ? (selectedLut.split("/").pop() ?? selectedLut) : null;
|
|
187
|
+
const detailSettingsSliders =
|
|
188
|
+
detailSettings === "vignette" ? VIGNETTE_TUNE_SLIDERS : GRAIN_TUNE_SLIDERS;
|
|
189
|
+
const vignetteSettingsActive = VIGNETTE_TUNE_SLIDERS.some(
|
|
190
|
+
(slider) => Math.abs(grading.details[slider.key] - normalizedDefaultValue(slider)) > 0.0001,
|
|
191
|
+
);
|
|
192
|
+
const grainSettingsActive = GRAIN_TUNE_SLIDERS.some(
|
|
193
|
+
(slider) => Math.abs(grading.details[slider.key] - normalizedDefaultValue(slider)) > 0.0001,
|
|
194
|
+
);
|
|
305
195
|
|
|
306
196
|
const applyPreset = (preset: string) => {
|
|
307
|
-
const next = normalizeHfColorGrading({ preset, intensity: 1 });
|
|
197
|
+
const next = normalizeHfColorGrading({ preset, intensity: 1, lut: grading.lut });
|
|
308
198
|
if (next) onCommitColorGrading(next);
|
|
309
199
|
};
|
|
200
|
+
const updateFilterIntensity = (value: number) => {
|
|
201
|
+
onCommitColorGrading({
|
|
202
|
+
...grading,
|
|
203
|
+
intensity: value / 100,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
310
206
|
const applyLut = (src: string | null, intensity = 1) => {
|
|
311
207
|
onCommitColorGrading({
|
|
312
208
|
...grading,
|
|
313
|
-
intensity:
|
|
209
|
+
intensity: visibleIntensity(grading),
|
|
314
210
|
lut: src ? { src, intensity } : null,
|
|
315
211
|
});
|
|
316
212
|
};
|
|
@@ -324,9 +220,48 @@ export function ColorGradingControls({
|
|
|
324
220
|
const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
|
|
325
221
|
if (firstLut) applyLut(firstLut, 1);
|
|
326
222
|
};
|
|
223
|
+
const commitDetailSlider = (slider: DetailSlider, next: number) => {
|
|
224
|
+
onCommitColorGrading({
|
|
225
|
+
...grading,
|
|
226
|
+
intensity: visibleIntensity(grading),
|
|
227
|
+
details: {
|
|
228
|
+
...grading.details,
|
|
229
|
+
[slider.key]: next / slider.scale,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const resetDetailSlider = (slider: DetailSlider) => {
|
|
234
|
+
onCommitColorGrading({
|
|
235
|
+
...grading,
|
|
236
|
+
intensity: visibleIntensity(grading),
|
|
237
|
+
details: {
|
|
238
|
+
...grading.details,
|
|
239
|
+
[slider.key]: normalizedDefaultValue(slider),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
const renderDetailSlider = (slider: DetailSlider, settings?: SliderSettings) => {
|
|
244
|
+
const value = Math.round(grading.details[slider.key] * slider.scale);
|
|
245
|
+
return (
|
|
246
|
+
<ColorGradingSliderControl
|
|
247
|
+
key={slider.key}
|
|
248
|
+
label={slider.label}
|
|
249
|
+
value={value}
|
|
250
|
+
min={slider.min}
|
|
251
|
+
max={slider.max}
|
|
252
|
+
step={slider.step}
|
|
253
|
+
neutral={slider.defaultValue ?? 0}
|
|
254
|
+
suffix={slider.suffix}
|
|
255
|
+
displayValue={`${value}%`}
|
|
256
|
+
settings={settings}
|
|
257
|
+
onCommit={(next) => commitDetailSlider(slider, next)}
|
|
258
|
+
onReset={() => resetDetailSlider(slider)}
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
327
262
|
|
|
328
263
|
return (
|
|
329
|
-
<div className="space-y-
|
|
264
|
+
<div className="space-y-3">
|
|
330
265
|
<label className="grid min-w-0 gap-1.5">
|
|
331
266
|
<span className={LABEL}>Preset</span>
|
|
332
267
|
<select
|
|
@@ -341,97 +276,125 @@ export function ColorGradingControls({
|
|
|
341
276
|
))}
|
|
342
277
|
</select>
|
|
343
278
|
</label>
|
|
279
|
+
<ColorGradingSliderControl
|
|
280
|
+
label="Preset strength"
|
|
281
|
+
value={Math.round(grading.intensity * 100)}
|
|
282
|
+
min={0}
|
|
283
|
+
max={100}
|
|
284
|
+
step={1}
|
|
285
|
+
neutral={0}
|
|
286
|
+
suffix="%"
|
|
287
|
+
displayValue={`${Math.round(grading.intensity * 100)}%`}
|
|
288
|
+
onCommit={updateFilterIntensity}
|
|
289
|
+
onReset={() => updateFilterIntensity(100)}
|
|
290
|
+
/>
|
|
344
291
|
|
|
345
|
-
<div className="
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
292
|
+
<div className="min-w-0 rounded-md border border-panel-border/70 bg-panel-input/15">
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
className="flex h-8 w-full min-w-0 items-center gap-1.5 px-2 text-left text-[11px] font-medium text-panel-text-3 transition-colors hover:bg-panel-hover/60 hover:text-panel-text-1"
|
|
296
|
+
onClick={() => setLutOpen((value) => !value)}
|
|
297
|
+
aria-expanded={lutOpen}
|
|
298
|
+
>
|
|
299
|
+
{lutOpen ? (
|
|
300
|
+
<ChevronDown size={11} className="flex-shrink-0 text-panel-text-5" />
|
|
301
|
+
) : (
|
|
302
|
+
<ChevronRight size={11} className="flex-shrink-0 text-panel-text-5" />
|
|
303
|
+
)}
|
|
304
|
+
<span className="min-w-0 flex-1 truncate">Custom LUT</span>
|
|
305
|
+
{grading.lut && (
|
|
306
|
+
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-studio-accent" />
|
|
307
|
+
)}
|
|
308
|
+
</button>
|
|
309
|
+
{lutOpen && (
|
|
310
|
+
<div className="grid gap-1.5 border-t border-panel-border/60 p-1.5">
|
|
311
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_28px] gap-2">
|
|
312
|
+
<select
|
|
313
|
+
value={selectedLut}
|
|
314
|
+
onChange={(event) => {
|
|
315
|
+
const nextSrc = event.target.value;
|
|
316
|
+
applyLut(
|
|
317
|
+
nextSrc || null,
|
|
318
|
+
nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1,
|
|
319
|
+
);
|
|
320
|
+
}}
|
|
321
|
+
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"
|
|
322
|
+
title="Uploaded .cube LUT"
|
|
323
|
+
>
|
|
324
|
+
<option value="">None</option>
|
|
325
|
+
{lutAssets.length > 0 && (
|
|
326
|
+
<optgroup label="Uploaded LUTs">
|
|
327
|
+
{lutAssets.map((asset) => (
|
|
328
|
+
<option key={asset} value={asset}>
|
|
329
|
+
{asset.split("/").pop() ?? asset}
|
|
330
|
+
</option>
|
|
331
|
+
))}
|
|
332
|
+
</optgroup>
|
|
333
|
+
)}
|
|
334
|
+
</select>
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
disabled={!onImportAssets}
|
|
338
|
+
onClick={(event) => {
|
|
339
|
+
event.stopPropagation();
|
|
340
|
+
lutInputRef.current?.click();
|
|
341
|
+
}}
|
|
342
|
+
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"
|
|
343
|
+
title="Import .cube LUT"
|
|
344
|
+
aria-label="Import .cube LUT"
|
|
345
|
+
>
|
|
346
|
+
<Plus size={13} />
|
|
347
|
+
</button>
|
|
348
|
+
<input
|
|
349
|
+
ref={lutInputRef}
|
|
350
|
+
type="file"
|
|
351
|
+
accept=".cube"
|
|
352
|
+
multiple
|
|
353
|
+
className="hidden"
|
|
354
|
+
onChange={(event) => {
|
|
355
|
+
void importLuts(event.currentTarget.files);
|
|
356
|
+
event.currentTarget.value = "";
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
{grading.lut && (
|
|
361
|
+
<div className="grid gap-2">
|
|
362
|
+
{selectedProjectLut && (
|
|
363
|
+
<div className="flex min-w-0 items-start gap-2 text-[10px] leading-4 text-panel-text-3">
|
|
364
|
+
<span className="mt-[5px] h-1.5 w-1.5 flex-shrink-0 rounded-full bg-studio-accent" />
|
|
365
|
+
<span className="min-w-0 flex-1 truncate" title={selectedProjectLut}>
|
|
366
|
+
<span className="font-medium text-panel-text-2">Uploaded LUT</span>
|
|
367
|
+
{` · ${selectedProjectLut}`}
|
|
368
|
+
</span>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
<ColorGradingSliderControl
|
|
372
|
+
label="LUT Strength"
|
|
373
|
+
value={Math.round((grading.lut.intensity ?? 1) * 100)}
|
|
374
|
+
min={0}
|
|
375
|
+
max={100}
|
|
376
|
+
step={1}
|
|
377
|
+
neutral={0}
|
|
378
|
+
suffix="%"
|
|
379
|
+
displayValue={`${Math.round((grading.lut.intensity ?? 1) * 100)}%`}
|
|
380
|
+
onCommit={updateLutIntensity}
|
|
381
|
+
onReset={() => updateLutIntensity(100)}
|
|
382
|
+
/>
|
|
405
383
|
</div>
|
|
406
384
|
)}
|
|
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
385
|
</div>
|
|
420
386
|
)}
|
|
421
387
|
</div>
|
|
422
388
|
|
|
423
|
-
<div className="grid min-w-0
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
className={
|
|
431
|
-
SLIDERS.length % 2 === 1 && index === SLIDERS.length - 1 ? "col-span-2" : ""
|
|
432
|
-
}
|
|
433
|
-
>
|
|
389
|
+
<div className="grid min-w-0 gap-1.5">
|
|
390
|
+
<span className={LABEL}>Adjust</span>
|
|
391
|
+
<div className="grid min-w-0 grid-cols-2 gap-1.5">
|
|
392
|
+
{ADJUST_SLIDERS.map((slider) => {
|
|
393
|
+
const value = grading.adjust[slider.key] * slider.scale;
|
|
394
|
+
const isExposure = slider.key === "exposure";
|
|
395
|
+
return (
|
|
434
396
|
<ColorGradingSliderControl
|
|
397
|
+
key={slider.key}
|
|
435
398
|
label={slider.label}
|
|
436
399
|
value={Math.round(value)}
|
|
437
400
|
min={slider.min}
|
|
@@ -448,7 +411,7 @@ export function ColorGradingControls({
|
|
|
448
411
|
onCommit={(next) => {
|
|
449
412
|
onCommitColorGrading({
|
|
450
413
|
...grading,
|
|
451
|
-
intensity:
|
|
414
|
+
intensity: visibleIntensity(grading),
|
|
452
415
|
adjust: {
|
|
453
416
|
...grading.adjust,
|
|
454
417
|
[slider.key]: next / slider.scale,
|
|
@@ -458,7 +421,7 @@ export function ColorGradingControls({
|
|
|
458
421
|
onReset={() => {
|
|
459
422
|
onCommitColorGrading({
|
|
460
423
|
...grading,
|
|
461
|
-
intensity:
|
|
424
|
+
intensity: visibleIntensity(grading),
|
|
462
425
|
adjust: {
|
|
463
426
|
...grading.adjust,
|
|
464
427
|
[slider.key]: 0,
|
|
@@ -466,9 +429,88 @@ export function ColorGradingControls({
|
|
|
466
429
|
});
|
|
467
430
|
}}
|
|
468
431
|
/>
|
|
432
|
+
);
|
|
433
|
+
})}
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div className="grid min-w-0 gap-1.5">
|
|
438
|
+
<span className={LABEL}>Finishing</span>
|
|
439
|
+
<div className="grid min-w-0 grid-cols-2 gap-1.5">
|
|
440
|
+
{AMOUNT_DETAIL_SLIDERS.map((slider) =>
|
|
441
|
+
renderDetailSlider(slider, {
|
|
442
|
+
active: slider.key === "vignette" ? vignetteSettingsActive : grainSettingsActive,
|
|
443
|
+
label: `${slider.label} settings`,
|
|
444
|
+
onClick: () =>
|
|
445
|
+
setDetailSettings((current) =>
|
|
446
|
+
current === slider.key ? null : (slider.key as "vignette" | "grain"),
|
|
447
|
+
),
|
|
448
|
+
}),
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
{detailSettings && (
|
|
452
|
+
<div className="grid min-w-0 gap-1.5 rounded-md border border-panel-border bg-panel-input/40 p-1.5 shadow-xl shadow-black/20">
|
|
453
|
+
<div className="flex min-w-0 items-center gap-2 px-0.5">
|
|
454
|
+
<span className={`${LABEL} min-w-0 flex-1 truncate`}>
|
|
455
|
+
{detailSettings === "vignette" ? "Vignette settings" : "Grain settings"}
|
|
456
|
+
</span>
|
|
457
|
+
<button
|
|
458
|
+
type="button"
|
|
459
|
+
aria-label="Close settings"
|
|
460
|
+
title="Close settings"
|
|
461
|
+
onClick={() => setDetailSettings(null)}
|
|
462
|
+
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded text-panel-text-5 transition-colors hover:bg-panel-hover hover:text-panel-text-1"
|
|
463
|
+
>
|
|
464
|
+
<X size={11} />
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="grid min-w-0 grid-cols-2 gap-1.5">
|
|
468
|
+
{detailSettingsSliders.map((slider) => renderDetailSlider(slider))}
|
|
469
469
|
</div>
|
|
470
|
-
|
|
471
|
-
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div className="grid min-w-0 gap-1.5">
|
|
475
|
+
<span className={LABEL}>Effects</span>
|
|
476
|
+
<div className="grid min-w-0 grid-cols-2 gap-1.5">
|
|
477
|
+
{EFFECT_SLIDERS.map((slider) => {
|
|
478
|
+
const value = grading.effects[slider.key] * slider.scale;
|
|
479
|
+
return (
|
|
480
|
+
<ColorGradingSliderControl
|
|
481
|
+
key={slider.key}
|
|
482
|
+
label={slider.label}
|
|
483
|
+
value={Math.round(value)}
|
|
484
|
+
min={slider.min}
|
|
485
|
+
max={slider.max}
|
|
486
|
+
step={slider.step}
|
|
487
|
+
neutral={0}
|
|
488
|
+
suffix={slider.suffix}
|
|
489
|
+
displayValue={`${Math.round(value)}%`}
|
|
490
|
+
onCommit={(next) => {
|
|
491
|
+
onCommitColorGrading({
|
|
492
|
+
...grading,
|
|
493
|
+
intensity: visibleIntensity(grading),
|
|
494
|
+
effects: {
|
|
495
|
+
...grading.effects,
|
|
496
|
+
[slider.key]: next / slider.scale,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}}
|
|
500
|
+
onReset={() => {
|
|
501
|
+
onCommitColorGrading({
|
|
502
|
+
...grading,
|
|
503
|
+
intensity: visibleIntensity(grading),
|
|
504
|
+
effects: {
|
|
505
|
+
...grading.effects,
|
|
506
|
+
[slider.key]: 0,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}}
|
|
510
|
+
/>
|
|
511
|
+
);
|
|
512
|
+
})}
|
|
513
|
+
</div>
|
|
472
514
|
</div>
|
|
473
515
|
</div>
|
|
474
516
|
);
|