@hyperframes/studio 0.7.58 → 0.7.59
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-CtTDO63S.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13514 -8528
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.ts +29 -175
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +17 -5
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +16 -10
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTimelineGroupEditing.ts +16 -2
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
HF_COLOR_GRADING_PRESETS,
|
|
4
|
+
isHfColorGradingActive,
|
|
5
|
+
normalizeHfColorGrading,
|
|
6
|
+
type HfColorGradingAdjustKey,
|
|
7
|
+
type HfColorGradingDetailKey,
|
|
8
|
+
type HfColorGradingEffectKey,
|
|
9
|
+
type NormalizedHfColorGrading,
|
|
10
|
+
} from "@hyperframes/core/color-grading";
|
|
11
|
+
import { Compare, Plus, RotateCcw, Settings } from "../../icons/SystemIcons";
|
|
12
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
13
|
+
import { LUT_EXT } from "../../utils/mediaTypes";
|
|
14
|
+
import { FlatSelectRow, FlatSlider } from "./propertyPanelFlatPrimitives";
|
|
15
|
+
import { resolveValueTier } from "./propertyPanelValueTier";
|
|
16
|
+
import type { ColorGradingControllerState, MediaMetadata } from "./useColorGradingController";
|
|
17
|
+
|
|
18
|
+
const STATUS_DOT_CLASS: Record<ColorGradingControllerState["runtimeStatus"]["state"], string> = {
|
|
19
|
+
active: "bg-emerald-400",
|
|
20
|
+
pending: "bg-amber-300",
|
|
21
|
+
unavailable: "bg-red-400",
|
|
22
|
+
missing: "bg-panel-text-5",
|
|
23
|
+
inactive: "bg-panel-text-5",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function FlatColorGradingAccessory({
|
|
27
|
+
state,
|
|
28
|
+
}: {
|
|
29
|
+
state: Pick<
|
|
30
|
+
ColorGradingControllerState,
|
|
31
|
+
"grading" | "compareEnabled" | "runtimeStatus" | "commitCompare" | "resetGrading"
|
|
32
|
+
>;
|
|
33
|
+
}) {
|
|
34
|
+
const track = useTrackDesignInput();
|
|
35
|
+
const { grading, compareEnabled, runtimeStatus, commitCompare, resetGrading } = state;
|
|
36
|
+
const gradingActive = isHfColorGradingActive(grading);
|
|
37
|
+
// Tracks the active hold's cleanup so it can be torn down on unmount too —
|
|
38
|
+
// without this, switching selection away mid-hold (unmounting this
|
|
39
|
+
// accessory) leaves the pointerup/pointercancel/blur listeners registered
|
|
40
|
+
// on `window` forever, each holding a closure over the old commitCompare.
|
|
41
|
+
const releaseRef = useRef<(() => void) | null>(null);
|
|
42
|
+
useEffect(
|
|
43
|
+
() => () => {
|
|
44
|
+
releaseRef.current?.();
|
|
45
|
+
releaseRef.current = null;
|
|
46
|
+
},
|
|
47
|
+
[],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<span className="flex items-center gap-2.5">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
aria-pressed={compareEnabled}
|
|
55
|
+
aria-label="Hold to show original"
|
|
56
|
+
disabled={!gradingActive}
|
|
57
|
+
onPointerDown={(e) => {
|
|
58
|
+
if (!gradingActive) return;
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
track("button", "Compare original");
|
|
62
|
+
commitCompare(true);
|
|
63
|
+
const release = () => {
|
|
64
|
+
commitCompare(false);
|
|
65
|
+
window.removeEventListener("pointerup", release);
|
|
66
|
+
window.removeEventListener("pointercancel", release);
|
|
67
|
+
window.removeEventListener("blur", release);
|
|
68
|
+
releaseRef.current = null;
|
|
69
|
+
};
|
|
70
|
+
releaseRef.current = release;
|
|
71
|
+
window.addEventListener("pointerup", release);
|
|
72
|
+
window.addEventListener("pointercancel", release);
|
|
73
|
+
window.addEventListener("blur", release);
|
|
74
|
+
}}
|
|
75
|
+
onBlur={() => {
|
|
76
|
+
if (compareEnabled) commitCompare(false);
|
|
77
|
+
}}
|
|
78
|
+
onKeyDown={(e) => {
|
|
79
|
+
if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return;
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
if (!compareEnabled) {
|
|
82
|
+
track("button", "Compare original");
|
|
83
|
+
commitCompare(true);
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
onKeyUp={(e) => {
|
|
87
|
+
if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return;
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
commitCompare(false);
|
|
90
|
+
}}
|
|
91
|
+
title="Hold to show original"
|
|
92
|
+
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
93
|
+
>
|
|
94
|
+
<Compare size={12} />
|
|
95
|
+
</button>
|
|
96
|
+
<span className="flex min-w-0 items-center gap-1" title={runtimeStatus.message}>
|
|
97
|
+
<span
|
|
98
|
+
data-flat-grade-status-dot="true"
|
|
99
|
+
title={runtimeStatus.message}
|
|
100
|
+
className={`h-[5px] w-[5px] flex-shrink-0 rounded-full ${STATUS_DOT_CLASS[runtimeStatus.state]}`}
|
|
101
|
+
/>
|
|
102
|
+
<span
|
|
103
|
+
data-flat-grade-status-message="true"
|
|
104
|
+
className="max-w-[84px] truncate text-[9px] text-panel-text-4"
|
|
105
|
+
>
|
|
106
|
+
{runtimeStatus.message}
|
|
107
|
+
</span>
|
|
108
|
+
</span>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
data-flat-grade-reset="true"
|
|
112
|
+
title="Reset color grading"
|
|
113
|
+
onClick={(e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
track("button", "Reset color grading");
|
|
116
|
+
resetGrading();
|
|
117
|
+
}}
|
|
118
|
+
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1"
|
|
119
|
+
>
|
|
120
|
+
<RotateCcw size={12} />
|
|
121
|
+
</button>
|
|
122
|
+
</span>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const PRESET_OPTIONS = HF_COLOR_GRADING_PRESETS.map((p) => ({ value: p.id, label: p.label }));
|
|
127
|
+
|
|
128
|
+
const ADJUST_SLIDERS: Array<{
|
|
129
|
+
key: HfColorGradingAdjustKey;
|
|
130
|
+
label: string;
|
|
131
|
+
min: number;
|
|
132
|
+
max: number;
|
|
133
|
+
step: number;
|
|
134
|
+
}> = [
|
|
135
|
+
{ key: "exposure", label: "Exposure", min: -200, max: 200, step: 5 },
|
|
136
|
+
{ key: "contrast", label: "Contrast", min: -100, max: 100, step: 1 },
|
|
137
|
+
{ key: "highlights", label: "Highlights", min: -100, max: 100, step: 1 },
|
|
138
|
+
{ key: "shadows", label: "Shadows", min: -100, max: 100, step: 1 },
|
|
139
|
+
{ key: "whites", label: "White Point", min: -100, max: 100, step: 1 },
|
|
140
|
+
{ key: "blacks", label: "Black Point", min: -100, max: 100, step: 1 },
|
|
141
|
+
{ key: "temperature", label: "Warmth", min: -100, max: 100, step: 1 },
|
|
142
|
+
{ key: "tint", label: "Tint", min: -100, max: 100, step: 1 },
|
|
143
|
+
{ key: "vibrance", label: "Vibrance", min: -100, max: 100, step: 1 },
|
|
144
|
+
{ key: "saturation", label: "Saturation", min: -100, max: 100, step: 1 },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
function visibleIntensity(grading: NormalizedHfColorGrading): number {
|
|
148
|
+
// Earlier drafts could persist 0% strength; the next manual edit should revive visible grading.
|
|
149
|
+
return grading.intensity === 0 ? 1 : grading.intensity;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatAdjustValue(key: HfColorGradingAdjustKey, rawPercent: number): string {
|
|
153
|
+
if (key === "exposure") {
|
|
154
|
+
const stops = rawPercent / 100;
|
|
155
|
+
return `${stops >= 0 ? "+" : ""}${stops.toFixed(2)}`;
|
|
156
|
+
}
|
|
157
|
+
return `${Math.round(rawPercent)}%`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const DETAIL_SLIDERS: Array<{
|
|
161
|
+
key: HfColorGradingDetailKey;
|
|
162
|
+
label: string;
|
|
163
|
+
defaultValue: number;
|
|
164
|
+
}> = [
|
|
165
|
+
{ key: "vignette", label: "Vignette", defaultValue: 0 },
|
|
166
|
+
{ key: "vignetteMidpoint", label: "Midpoint", defaultValue: 0.5 },
|
|
167
|
+
{ key: "vignetteRoundness", label: "Roundness", defaultValue: 0 },
|
|
168
|
+
{ key: "vignetteFeather", label: "Feather", defaultValue: 0.65 },
|
|
169
|
+
{ key: "grain", label: "Grain", defaultValue: 0 },
|
|
170
|
+
{ key: "grainSize", label: "Grain Size", defaultValue: 0.25 },
|
|
171
|
+
{ key: "grainRoughness", label: "Roughness", defaultValue: 0.5 },
|
|
172
|
+
];
|
|
173
|
+
const detailByKey = (key: HfColorGradingDetailKey) => {
|
|
174
|
+
const spec = DETAIL_SLIDERS.find((d) => d.key === key);
|
|
175
|
+
if (!spec) throw new Error(`Unknown color grading detail key: ${key}`);
|
|
176
|
+
return spec;
|
|
177
|
+
};
|
|
178
|
+
const VIGNETTE_TUNE_KEYS: HfColorGradingDetailKey[] = [
|
|
179
|
+
"vignetteMidpoint",
|
|
180
|
+
"vignetteRoundness",
|
|
181
|
+
"vignetteFeather",
|
|
182
|
+
];
|
|
183
|
+
const GRAIN_TUNE_KEYS: HfColorGradingDetailKey[] = ["grainSize", "grainRoughness"];
|
|
184
|
+
|
|
185
|
+
const EFFECT_SLIDERS: Array<{ key: HfColorGradingEffectKey; label: string }> = [
|
|
186
|
+
{ key: "blur", label: "Blur" },
|
|
187
|
+
{ key: "pixelate", label: "Pixelate" },
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
|
|
191
|
+
if (metadata?.color.dynamicRange !== "hdr") return null;
|
|
192
|
+
const details = [
|
|
193
|
+
metadata.color.codecName,
|
|
194
|
+
metadata.color.profile,
|
|
195
|
+
metadata.color.pixelFormat,
|
|
196
|
+
metadata.color.colorPrimaries,
|
|
197
|
+
metadata.color.colorTransfer,
|
|
198
|
+
]
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.join(" · ");
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
data-flat-grade-hdr-banner="true"
|
|
204
|
+
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1.5 text-[10px] leading-4 text-amber-100"
|
|
205
|
+
>
|
|
206
|
+
<div className="mb-0.5 flex items-center justify-between gap-2">
|
|
207
|
+
<span className="font-semibold">{metadata.color.label} source</span>
|
|
208
|
+
<span className="rounded bg-amber-400/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
|
|
209
|
+
SDR preview
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
<p className="text-amber-100/80">
|
|
213
|
+
These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
|
|
214
|
+
is not true HDR color grading yet.
|
|
215
|
+
</p>
|
|
216
|
+
{details && (
|
|
217
|
+
<p
|
|
218
|
+
data-flat-grade-hdr-detail="true"
|
|
219
|
+
className="mt-0.5 truncate text-[9px] text-amber-100/55"
|
|
220
|
+
>
|
|
221
|
+
{details}
|
|
222
|
+
</p>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// fallow-ignore-next-line complexity
|
|
229
|
+
export function FlatColorGradingSection({
|
|
230
|
+
grading,
|
|
231
|
+
assets,
|
|
232
|
+
onImportAssets,
|
|
233
|
+
onCommitColorGrading,
|
|
234
|
+
applyScope,
|
|
235
|
+
applyBusy,
|
|
236
|
+
onSetApplyScope,
|
|
237
|
+
onApplyToScope,
|
|
238
|
+
onApplyScopeAvailable,
|
|
239
|
+
mediaMetadata,
|
|
240
|
+
}: {
|
|
241
|
+
grading: NormalizedHfColorGrading;
|
|
242
|
+
assets: string[];
|
|
243
|
+
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
244
|
+
onCommitColorGrading: (next: NormalizedHfColorGrading) => void;
|
|
245
|
+
applyScope: "source-file" | "project";
|
|
246
|
+
applyBusy: boolean;
|
|
247
|
+
onSetApplyScope: (scope: "source-file" | "project") => void;
|
|
248
|
+
onApplyToScope: () => void;
|
|
249
|
+
onApplyScopeAvailable: boolean;
|
|
250
|
+
mediaMetadata: MediaMetadata | null;
|
|
251
|
+
}) {
|
|
252
|
+
const track = useTrackDesignInput();
|
|
253
|
+
const lutInputRef = useRef<HTMLInputElement>(null);
|
|
254
|
+
const [lutOpen, setLutOpen] = useState(false);
|
|
255
|
+
const [detailSettingsOpen, setDetailSettingsOpen] = useState<"vignette" | "grain" | null>(null);
|
|
256
|
+
const lutAssets = useMemo(
|
|
257
|
+
() => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
|
|
258
|
+
[assets],
|
|
259
|
+
);
|
|
260
|
+
const lut = grading.lut;
|
|
261
|
+
const selectedLutName = lut?.src ? (lut.src.split("/").pop() ?? lut.src) : null;
|
|
262
|
+
|
|
263
|
+
const applyPreset = (presetId: string) => {
|
|
264
|
+
const next = normalizeHfColorGrading({ preset: presetId, intensity: 1, lut: grading.lut });
|
|
265
|
+
if (next) onCommitColorGrading(next);
|
|
266
|
+
};
|
|
267
|
+
const updateIntensity = (value: number) => {
|
|
268
|
+
onCommitColorGrading({ ...grading, intensity: value / 100 });
|
|
269
|
+
};
|
|
270
|
+
const applyLut = (src: string | null, intensity = 1) => {
|
|
271
|
+
onCommitColorGrading({
|
|
272
|
+
...grading,
|
|
273
|
+
intensity: visibleIntensity(grading),
|
|
274
|
+
lut: src ? { src, intensity } : null,
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const importLuts = async (files: FileList | null) => {
|
|
278
|
+
if (!files?.length || !onImportAssets) return;
|
|
279
|
+
const uploaded = await onImportAssets(files, "assets/luts");
|
|
280
|
+
const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
|
|
281
|
+
if (firstLut) {
|
|
282
|
+
track("button", "Import LUT");
|
|
283
|
+
applyLut(firstLut, 1);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const renderDetailSlider = (key: HfColorGradingDetailKey) => {
|
|
288
|
+
const spec = detailByKey(key);
|
|
289
|
+
const value = grading.details[key];
|
|
290
|
+
const isSet = Math.abs(value - spec.defaultValue) > 1e-4;
|
|
291
|
+
return (
|
|
292
|
+
<FlatSlider
|
|
293
|
+
key={key}
|
|
294
|
+
label={spec.label}
|
|
295
|
+
value={Math.round(value * 100)}
|
|
296
|
+
min={key === "vignetteRoundness" ? -100 : 0}
|
|
297
|
+
max={100}
|
|
298
|
+
tier={isSet ? "explicitCustom" : "default"}
|
|
299
|
+
displayValue={`${Math.round(value * 100)}%`}
|
|
300
|
+
centerTick={key === "vignetteRoundness"}
|
|
301
|
+
onCommit={(next) =>
|
|
302
|
+
onCommitColorGrading({
|
|
303
|
+
...grading,
|
|
304
|
+
intensity: visibleIntensity(grading),
|
|
305
|
+
details: { ...grading.details, [key]: next / 100 },
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
onReset={() =>
|
|
309
|
+
onCommitColorGrading({
|
|
310
|
+
...grading,
|
|
311
|
+
intensity: visibleIntensity(grading),
|
|
312
|
+
details: { ...grading.details, [key]: spec.defaultValue },
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div className="space-y-1.5">
|
|
321
|
+
<HdrBanner metadata={mediaMetadata} />
|
|
322
|
+
<div data-flat-grade-preset="true" className="flex min-h-[30px] items-center justify-between">
|
|
323
|
+
<span className="text-[11px] text-panel-text-2">Preset</span>
|
|
324
|
+
<FlatSelectRow
|
|
325
|
+
label=""
|
|
326
|
+
ariaLabel="Preset"
|
|
327
|
+
value={grading.preset ?? "neutral"}
|
|
328
|
+
options={PRESET_OPTIONS}
|
|
329
|
+
tier={resolveValueTier(
|
|
330
|
+
grading.preset === "neutral" ? undefined : (grading.preset ?? undefined),
|
|
331
|
+
"neutral",
|
|
332
|
+
)}
|
|
333
|
+
onChange={applyPreset}
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
<FlatSlider
|
|
337
|
+
label="Strength"
|
|
338
|
+
value={Math.round(grading.intensity * 100)}
|
|
339
|
+
min={0}
|
|
340
|
+
max={100}
|
|
341
|
+
tier={grading.intensity === 1 ? "default" : "explicitCustom"}
|
|
342
|
+
displayValue={`${Math.round(grading.intensity * 100)}%`}
|
|
343
|
+
onCommit={updateIntensity}
|
|
344
|
+
onReset={() => updateIntensity(100)}
|
|
345
|
+
/>
|
|
346
|
+
|
|
347
|
+
<div className="border-t border-panel-hairline pt-1.5">
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
data-flat-grade-lut-toggle="true"
|
|
351
|
+
onClick={() => setLutOpen((v) => !v)}
|
|
352
|
+
className="flex min-h-[30px] w-full items-center justify-between text-left"
|
|
353
|
+
>
|
|
354
|
+
<span className="text-[11px] text-panel-text-2">Custom LUT</span>
|
|
355
|
+
<svg
|
|
356
|
+
width="10"
|
|
357
|
+
height="10"
|
|
358
|
+
viewBox="0 0 10 10"
|
|
359
|
+
fill="currentColor"
|
|
360
|
+
className={`flex-shrink-0 text-panel-text-5 transition-transform ${lutOpen ? "rotate-90" : ""}`}
|
|
361
|
+
>
|
|
362
|
+
<path d="M2 3l3 4 3-4z" />
|
|
363
|
+
</svg>
|
|
364
|
+
</button>
|
|
365
|
+
{lutOpen && (
|
|
366
|
+
<div className="space-y-1.5 pb-1">
|
|
367
|
+
<div className="flex items-center justify-between gap-2">
|
|
368
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10px] text-panel-text-3">
|
|
369
|
+
{selectedLutName ?? "None"}
|
|
370
|
+
</span>
|
|
371
|
+
<select
|
|
372
|
+
data-flat-grade-lut-select="true"
|
|
373
|
+
aria-label="Custom LUT"
|
|
374
|
+
value={lut?.src ?? ""}
|
|
375
|
+
onChange={(e) => {
|
|
376
|
+
const src = e.target.value;
|
|
377
|
+
track("select", "Custom LUT");
|
|
378
|
+
applyLut(src || null, src && lut?.src === src ? lut.intensity : 1);
|
|
379
|
+
}}
|
|
380
|
+
className="bg-transparent font-mono text-[10px] text-panel-text-3 outline-none"
|
|
381
|
+
>
|
|
382
|
+
<option value="">None</option>
|
|
383
|
+
{lutAssets.map((asset) => (
|
|
384
|
+
<option key={asset} value={asset}>
|
|
385
|
+
{asset.split("/").pop() ?? asset}
|
|
386
|
+
</option>
|
|
387
|
+
))}
|
|
388
|
+
</select>
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
disabled={!onImportAssets}
|
|
392
|
+
onClick={() => lutInputRef.current?.click()}
|
|
393
|
+
title="Import .cube LUT"
|
|
394
|
+
className="flex-shrink-0 text-panel-text-4 hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
395
|
+
>
|
|
396
|
+
<Plus size={12} />
|
|
397
|
+
</button>
|
|
398
|
+
<input
|
|
399
|
+
ref={lutInputRef}
|
|
400
|
+
type="file"
|
|
401
|
+
accept=".cube"
|
|
402
|
+
className="hidden"
|
|
403
|
+
onChange={(e) => {
|
|
404
|
+
void importLuts(e.currentTarget.files);
|
|
405
|
+
e.currentTarget.value = "";
|
|
406
|
+
}}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
{lut && (
|
|
410
|
+
<FlatSlider
|
|
411
|
+
label="LUT strength"
|
|
412
|
+
value={Math.round((lut.intensity ?? 1) * 100)}
|
|
413
|
+
min={0}
|
|
414
|
+
max={100}
|
|
415
|
+
tier={lut.intensity === 1 ? "default" : "explicitCustom"}
|
|
416
|
+
displayValue={`${Math.round((lut.intensity ?? 1) * 100)}%`}
|
|
417
|
+
onCommit={(v) => applyLut(lut.src, v / 100)}
|
|
418
|
+
onReset={() => applyLut(lut.src, 1)}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div className="space-y-0.5 border-t border-panel-hairline pt-1.5">
|
|
426
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
427
|
+
Adjust
|
|
428
|
+
</div>
|
|
429
|
+
{ADJUST_SLIDERS.map((slider) => {
|
|
430
|
+
const rawPercent = grading.adjust[slider.key] * 100;
|
|
431
|
+
const isSet = Math.abs(grading.adjust[slider.key]) > 1e-6;
|
|
432
|
+
return (
|
|
433
|
+
<div key={slider.key} data-flat-grade-adjust="true">
|
|
434
|
+
<FlatSlider
|
|
435
|
+
label={slider.label}
|
|
436
|
+
value={rawPercent}
|
|
437
|
+
min={slider.min}
|
|
438
|
+
max={slider.max}
|
|
439
|
+
step={slider.step}
|
|
440
|
+
tier={isSet ? "explicitCustom" : "default"}
|
|
441
|
+
displayValue={formatAdjustValue(slider.key, rawPercent)}
|
|
442
|
+
centerTick
|
|
443
|
+
onCommit={(next) =>
|
|
444
|
+
onCommitColorGrading({
|
|
445
|
+
...grading,
|
|
446
|
+
intensity: visibleIntensity(grading),
|
|
447
|
+
adjust: { ...grading.adjust, [slider.key]: next / 100 },
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
onReset={() =>
|
|
451
|
+
onCommitColorGrading({
|
|
452
|
+
...grading,
|
|
453
|
+
intensity: visibleIntensity(grading),
|
|
454
|
+
adjust: { ...grading.adjust, [slider.key]: 0 },
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
/>
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
})}
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div className="space-y-1.5 border-t border-panel-hairline pt-1.5">
|
|
464
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
465
|
+
Finishing
|
|
466
|
+
</div>
|
|
467
|
+
<div className="flex items-center gap-1.5">
|
|
468
|
+
<div className="flex-1">{renderDetailSlider("vignette")}</div>
|
|
469
|
+
<button
|
|
470
|
+
type="button"
|
|
471
|
+
data-flat-grade-settings="vignette"
|
|
472
|
+
title="Vignette settings"
|
|
473
|
+
onClick={() => setDetailSettingsOpen((c) => (c === "vignette" ? null : "vignette"))}
|
|
474
|
+
className="flex-shrink-0 text-panel-text-4 hover:text-panel-text-1"
|
|
475
|
+
>
|
|
476
|
+
<Settings size={12} />
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
<div className="flex items-center gap-1.5">
|
|
480
|
+
<div className="flex-1">{renderDetailSlider("grain")}</div>
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
data-flat-grade-settings="grain"
|
|
484
|
+
title="Grain settings"
|
|
485
|
+
onClick={() => setDetailSettingsOpen((c) => (c === "grain" ? null : "grain"))}
|
|
486
|
+
className="flex-shrink-0 text-panel-text-4 hover:text-panel-text-1"
|
|
487
|
+
>
|
|
488
|
+
<Settings size={12} />
|
|
489
|
+
</button>
|
|
490
|
+
</div>
|
|
491
|
+
{detailSettingsOpen && (
|
|
492
|
+
<div className="space-y-0.5 border-l-2 border-panel-border-input pl-2.5">
|
|
493
|
+
{(detailSettingsOpen === "vignette" ? VIGNETTE_TUNE_KEYS : GRAIN_TUNE_KEYS).map(
|
|
494
|
+
renderDetailSlider,
|
|
495
|
+
)}
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<div className="space-y-0.5 border-t border-panel-hairline pt-1.5">
|
|
501
|
+
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
502
|
+
Effects
|
|
503
|
+
</div>
|
|
504
|
+
{EFFECT_SLIDERS.map((slider) => {
|
|
505
|
+
const value = grading.effects[slider.key];
|
|
506
|
+
const isSet = value > 1e-6;
|
|
507
|
+
return (
|
|
508
|
+
<div key={slider.key} data-flat-grade-effect="true">
|
|
509
|
+
<FlatSlider
|
|
510
|
+
label={slider.label}
|
|
511
|
+
value={Math.round(value * 100)}
|
|
512
|
+
min={0}
|
|
513
|
+
max={100}
|
|
514
|
+
tier={isSet ? "explicitCustom" : "default"}
|
|
515
|
+
displayValue={`${Math.round(value * 100)}%`}
|
|
516
|
+
onCommit={(next) =>
|
|
517
|
+
onCommitColorGrading({
|
|
518
|
+
...grading,
|
|
519
|
+
intensity: visibleIntensity(grading),
|
|
520
|
+
effects: { ...grading.effects, [slider.key]: next / 100 },
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
onReset={() =>
|
|
524
|
+
onCommitColorGrading({
|
|
525
|
+
...grading,
|
|
526
|
+
intensity: visibleIntensity(grading),
|
|
527
|
+
effects: { ...grading.effects, [slider.key]: 0 },
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
/>
|
|
531
|
+
</div>
|
|
532
|
+
);
|
|
533
|
+
})}
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
{onApplyScopeAvailable && (
|
|
537
|
+
<div className="flex items-center justify-between gap-2 border-t border-panel-hairline pt-1.5">
|
|
538
|
+
<span className="flex items-center gap-1.5 text-[11px] text-panel-text-2">
|
|
539
|
+
Copy grade to
|
|
540
|
+
<select
|
|
541
|
+
aria-label="Copy grade to"
|
|
542
|
+
value={applyScope}
|
|
543
|
+
onChange={(e) => {
|
|
544
|
+
track("select", "Copy grade scope");
|
|
545
|
+
onSetApplyScope(e.target.value as "source-file" | "project");
|
|
546
|
+
}}
|
|
547
|
+
disabled={applyBusy}
|
|
548
|
+
className="bg-transparent font-mono text-[11px] text-panel-text-0 outline-none disabled:opacity-50"
|
|
549
|
+
>
|
|
550
|
+
<option value="source-file">Current file media</option>
|
|
551
|
+
<option value="project">All project media</option>
|
|
552
|
+
</select>
|
|
553
|
+
</span>
|
|
554
|
+
<button
|
|
555
|
+
type="button"
|
|
556
|
+
data-flat-grade-apply="true"
|
|
557
|
+
disabled={applyBusy}
|
|
558
|
+
onClick={() => {
|
|
559
|
+
track("button", "Apply grade to scope");
|
|
560
|
+
onApplyToScope();
|
|
561
|
+
}}
|
|
562
|
+
className="text-[11px] font-medium text-panel-accent hover:text-panel-accent/80 disabled:cursor-not-allowed disabled:opacity-50"
|
|
563
|
+
>
|
|
564
|
+
{applyBusy ? "Applying" : "Apply"}
|
|
565
|
+
</button>
|
|
566
|
+
</div>
|
|
567
|
+
)}
|
|
568
|
+
</div>
|
|
569
|
+
);
|
|
570
|
+
}
|