mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-02 20:18:35 +00:00
refactor(studio): simplify color grading inspector
This commit is contained in:
@@ -366,6 +366,12 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
|
||||
{STUDIO_COLOR_GRADING_ENABLED && isColorGradingCapableElement(element) && (
|
||||
<ColorGradingSection
|
||||
key={[
|
||||
element.id ?? "",
|
||||
element.hfId ?? "",
|
||||
element.selector ?? "",
|
||||
String(element.selectorIndex ?? ""),
|
||||
].join("|")}
|
||||
element={element}
|
||||
assets={assets}
|
||||
previewIframeRef={previewIframeRef}
|
||||
|
||||
@@ -41,19 +41,6 @@ const SLIDERS: Array<{
|
||||
{ key: "saturation", label: "Saturation", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
|
||||
];
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
return `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
function formatExposure(value: number): string {
|
||||
const stops = value / 100;
|
||||
return `${stops > 0 ? "+" : ""}${stops.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function fileLabel(path: string): string {
|
||||
return path.split("/").pop() ?? path;
|
||||
}
|
||||
|
||||
function clampNumber(value: number, min: number, max: number): number {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.min(max, Math.max(min, value));
|
||||
@@ -70,17 +57,6 @@ function parseNumericInput(value: string, scale: number): number | null {
|
||||
return parsed * scale;
|
||||
}
|
||||
|
||||
function buildSliderTicks(min: number, max: number, neutral: number): number[] {
|
||||
const span = max - min;
|
||||
if (span <= 0) return [];
|
||||
const step = span <= 200 ? 50 : span / 4;
|
||||
const ticks = new Set<number>([min, max, neutral]);
|
||||
for (let value = min; value <= max + step / 2; value += step) {
|
||||
ticks.add(Math.round(value));
|
||||
}
|
||||
return Array.from(ticks).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function tickPercent(value: number, min: number, max: number): number {
|
||||
if (max <= min) return 0;
|
||||
return ((value - min) / (max - min)) * 100;
|
||||
@@ -186,7 +162,7 @@ function ColorGradingSliderControl({
|
||||
const neutralPercent = range === 0 ? 0 : ((neutral - min) / range) * 100;
|
||||
const fillLeft = Math.min(valuePercent, neutralPercent);
|
||||
const fillWidth = Math.abs(valuePercent - neutralPercent);
|
||||
const ticks = buildSliderTicks(min, max, neutral);
|
||||
const ticks = Array.from(new Set([min, neutral, max])).sort((a, b) => a - b);
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 gap-1.5 rounded-md bg-panel-input/30 p-2">
|
||||
@@ -237,6 +213,7 @@ function ColorGradingSliderControl({
|
||||
step={step}
|
||||
value={draft}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
onChange={(event) => scheduleCommit(Number(event.currentTarget.value))}
|
||||
onMouseUp={() => commitDraft(draft)}
|
||||
onTouchEnd={() => commitDraft(draft)}
|
||||
@@ -323,7 +300,7 @@ export function ColorGradingControls({
|
||||
[assets],
|
||||
);
|
||||
const selectedLut = grading.lut?.src ?? "";
|
||||
const selectedProjectLut = selectedLut ? fileLabel(selectedLut) : null;
|
||||
const selectedProjectLut = selectedLut ? (selectedLut.split("/").pop() ?? selectedLut) : null;
|
||||
|
||||
const applyPreset = (preset: string) => {
|
||||
const next = normalizeHfColorGrading({ preset, intensity: 1 }) ?? defaultColorGrading;
|
||||
@@ -384,7 +361,7 @@ export function ColorGradingControls({
|
||||
<optgroup label="Uploaded LUTs">
|
||||
{lutAssets.map((asset) => (
|
||||
<option key={asset} value={asset}>
|
||||
{fileLabel(asset)}
|
||||
{asset.split("/").pop() ?? asset}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
@@ -434,7 +411,7 @@ export function ColorGradingControls({
|
||||
step={1}
|
||||
neutral={0}
|
||||
suffix="%"
|
||||
displayValue={formatPercent((grading.lut.intensity ?? 1) * 100)}
|
||||
displayValue={`${Math.round((grading.lut.intensity ?? 1) * 100)}%`}
|
||||
onCommit={updateLutIntensity}
|
||||
onReset={() => updateLutIntensity(100)}
|
||||
/>
|
||||
@@ -443,14 +420,14 @@ export function ColorGradingControls({
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-2 gap-3">
|
||||
{SLIDERS.map((slider) => {
|
||||
{SLIDERS.map((slider, index) => {
|
||||
const value = grading.adjust[slider.key] * slider.scale;
|
||||
const isExposure = slider.key === "exposure";
|
||||
return (
|
||||
<div
|
||||
key={slider.key}
|
||||
className={
|
||||
SLIDERS.length % 2 === 1 && slider.key === "saturation" ? "col-span-2" : ""
|
||||
SLIDERS.length % 2 === 1 && index === SLIDERS.length - 1 ? "col-span-2" : ""
|
||||
}
|
||||
>
|
||||
<ColorGradingSliderControl
|
||||
@@ -462,7 +439,11 @@ export function ColorGradingControls({
|
||||
neutral={0}
|
||||
scale={isExposure ? 100 : 1}
|
||||
suffix={isExposure ? "" : slider.suffix}
|
||||
displayValue={isExposure ? formatExposure(value) : formatPercent(value)}
|
||||
displayValue={
|
||||
isExposure
|
||||
? `${value > 0 ? "+" : ""}${(value / 100).toFixed(2)}`
|
||||
: `${Math.round(value)}%`
|
||||
}
|
||||
onCommit={(next) => {
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
|
||||
@@ -43,14 +43,6 @@ const DEFAULT_COLOR_GRADING: NormalizedHfColorGrading = {
|
||||
colorSpace: HF_COLOR_GRADING_COLOR_SPACE,
|
||||
};
|
||||
|
||||
interface ColorGradingCompareState {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_COMPARE: ColorGradingCompareState = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
const COLOR_GRADING_DATA_KEY = HF_COLOR_GRADING_ATTR.replace(/^data-/, "");
|
||||
|
||||
type RuntimeColorGradingStatusState = "missing" | "inactive" | "pending" | "active" | "unavailable";
|
||||
@@ -60,16 +52,6 @@ interface RuntimeColorGradingStatus {
|
||||
message: string;
|
||||
}
|
||||
|
||||
type RuntimeColorGradingWindow = Window & {
|
||||
__hf?: {
|
||||
colorGrading?: {
|
||||
getStatus?: (
|
||||
target: HfColorGradingTarget | string | null | undefined,
|
||||
) => RuntimeColorGradingStatus;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function isColorGradingCapableElement(element: DomEditSelection): boolean {
|
||||
return element.tagName === "video" || element.tagName === "img";
|
||||
}
|
||||
@@ -83,13 +65,8 @@ function readColorGradingFromElement(element: DomEditSelection): NormalizedHfCol
|
||||
|
||||
function toBridgeColorGrading(grading: NormalizedHfColorGrading): unknown {
|
||||
if (!isHfColorGradingActive(grading)) return null;
|
||||
return {
|
||||
preset: grading.preset,
|
||||
intensity: grading.intensity,
|
||||
adjust: grading.adjust,
|
||||
lut: grading.lut,
|
||||
colorSpace: grading.colorSpace,
|
||||
};
|
||||
const { enabled: _enabled, ...bridgeGrading } = grading;
|
||||
return bridgeGrading;
|
||||
}
|
||||
|
||||
function readRuntimeColorGradingStatus(
|
||||
@@ -97,7 +74,18 @@ function readRuntimeColorGradingStatus(
|
||||
target: HfColorGradingTarget,
|
||||
): RuntimeColorGradingStatus {
|
||||
try {
|
||||
const win = iframe?.contentWindow as RuntimeColorGradingWindow | null | undefined;
|
||||
const win = iframe?.contentWindow as
|
||||
| (Window & {
|
||||
__hf?: {
|
||||
colorGrading?: {
|
||||
getStatus?: (
|
||||
target: HfColorGradingTarget | string | null | undefined,
|
||||
) => RuntimeColorGradingStatus;
|
||||
};
|
||||
};
|
||||
})
|
||||
| null
|
||||
| undefined;
|
||||
const status = win?.__hf?.colorGrading?.getStatus?.(target);
|
||||
return status ?? { state: "pending", message: "Waiting for runtime" };
|
||||
} catch {
|
||||
@@ -140,12 +128,10 @@ function HoldBeforeButton({
|
||||
onHoldChange(false);
|
||||
window.removeEventListener("pointerup", release);
|
||||
window.removeEventListener("pointercancel", release);
|
||||
window.removeEventListener("mouseup", release);
|
||||
window.removeEventListener("blur", release);
|
||||
};
|
||||
window.addEventListener("pointerup", release);
|
||||
window.addEventListener("pointercancel", release);
|
||||
window.addEventListener("mouseup", release);
|
||||
window.addEventListener("blur", release);
|
||||
};
|
||||
const stopHold = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
@@ -203,7 +189,7 @@ export function ColorGradingSection({
|
||||
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
|
||||
}) {
|
||||
const [grading, setGrading] = useState(() => readColorGradingFromElement(element));
|
||||
const [compare, setCompare] = useState<ColorGradingCompareState>(DEFAULT_COMPARE);
|
||||
const [compareEnabled, setCompareEnabled] = useState(false);
|
||||
const [runtimeStatus, setRuntimeStatus] = useState<RuntimeColorGradingStatus>(() => ({
|
||||
state: "pending",
|
||||
message: "Waiting for runtime",
|
||||
@@ -211,9 +197,9 @@ export function ColorGradingSection({
|
||||
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingPersistValueRef = useRef<string | null | undefined>(undefined);
|
||||
const onSetAttributeLiveRef = useRef(onSetAttributeLive);
|
||||
const compareRef = useRef(compare);
|
||||
const compareEnabledRef = useRef(compareEnabled);
|
||||
onSetAttributeLiveRef.current = onSetAttributeLive;
|
||||
compareRef.current = compare;
|
||||
compareEnabledRef.current = compareEnabled;
|
||||
const target = useMemo(
|
||||
(): HfColorGradingTarget => ({
|
||||
id: element.id ?? null,
|
||||
@@ -223,30 +209,14 @@ export function ColorGradingSection({
|
||||
}),
|
||||
[element.hfId, element.id, element.selector, element.selectorIndex],
|
||||
);
|
||||
const targetKey = useMemo(
|
||||
() =>
|
||||
[
|
||||
target.id ?? "",
|
||||
target.hfId ?? "",
|
||||
target.selector ?? "",
|
||||
String(target.selectorIndex ?? ""),
|
||||
].join("|"),
|
||||
[target],
|
||||
);
|
||||
const colorGradingAttribute = element.dataAttributes[COLOR_GRADING_DATA_KEY] ?? "";
|
||||
|
||||
const refreshRuntimeStatus = useCallback(() => {
|
||||
setRuntimeStatus(readRuntimeColorGradingStatus(previewIframeRef?.current, target));
|
||||
}, [previewIframeRef, target]);
|
||||
|
||||
useEffect(() => {
|
||||
setGrading(normalizeHfColorGrading(colorGradingAttribute) ?? DEFAULT_COLOR_GRADING);
|
||||
refreshRuntimeStatus();
|
||||
}, [element, colorGradingAttribute, refreshRuntimeStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
setCompare(DEFAULT_COMPARE);
|
||||
}, [targetKey]);
|
||||
}, [refreshRuntimeStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = previewIframeRef?.current;
|
||||
@@ -289,7 +259,7 @@ export function ColorGradingSection({
|
||||
);
|
||||
|
||||
const postCompare = useCallback(
|
||||
(nextCompare: ColorGradingCompareState) => {
|
||||
(enabled: boolean) => {
|
||||
previewIframeRef?.current?.contentWindow?.postMessage(
|
||||
{
|
||||
source: "hf-parent",
|
||||
@@ -297,7 +267,7 @@ export function ColorGradingSection({
|
||||
action: "set-color-grading-compare",
|
||||
target,
|
||||
compare: {
|
||||
enabled: nextCompare.enabled,
|
||||
enabled,
|
||||
position: 1,
|
||||
lineWidth: 0,
|
||||
},
|
||||
@@ -310,48 +280,45 @@ export function ColorGradingSection({
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
postCompare({ ...DEFAULT_COMPARE, enabled: false });
|
||||
postCompare(false);
|
||||
},
|
||||
[postCompare],
|
||||
);
|
||||
|
||||
const commitColorGrading = (nextGrading: NormalizedHfColorGrading) => {
|
||||
setGrading(nextGrading);
|
||||
setRuntimeStatus({ state: "pending", message: "Updating shader" });
|
||||
postColorGrading(nextGrading);
|
||||
const active = isHfColorGradingActive(nextGrading);
|
||||
if (compareRef.current.enabled) {
|
||||
postCompare({
|
||||
...compareRef.current,
|
||||
enabled: active,
|
||||
});
|
||||
if (!active) setCompare(DEFAULT_COMPARE);
|
||||
}
|
||||
window.setTimeout(refreshRuntimeStatus, 50);
|
||||
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
||||
pendingPersistValueRef.current = isHfColorGradingActive(nextGrading)
|
||||
? serializeHfColorGrading(nextGrading)
|
||||
: null;
|
||||
persistTimerRef.current = setTimeout(() => {
|
||||
const value = pendingPersistValueRef.current;
|
||||
pendingPersistValueRef.current = undefined;
|
||||
void onSetAttributeLive(COLOR_GRADING_DATA_KEY, value ?? null);
|
||||
}, 350);
|
||||
};
|
||||
const commitColorGrading = useCallback(
|
||||
(nextGrading: NormalizedHfColorGrading) => {
|
||||
setGrading(nextGrading);
|
||||
setRuntimeStatus({ state: "pending", message: "Updating shader" });
|
||||
postColorGrading(nextGrading);
|
||||
const active = isHfColorGradingActive(nextGrading);
|
||||
if (compareEnabledRef.current) {
|
||||
postCompare(active);
|
||||
if (!active) setCompareEnabled(false);
|
||||
}
|
||||
window.setTimeout(refreshRuntimeStatus, 50);
|
||||
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
||||
pendingPersistValueRef.current = isHfColorGradingActive(nextGrading)
|
||||
? serializeHfColorGrading(nextGrading)
|
||||
: null;
|
||||
persistTimerRef.current = setTimeout(() => {
|
||||
const value = pendingPersistValueRef.current;
|
||||
pendingPersistValueRef.current = undefined;
|
||||
void onSetAttributeLive(COLOR_GRADING_DATA_KEY, value ?? null);
|
||||
}, 350);
|
||||
},
|
||||
[onSetAttributeLive, postColorGrading, postCompare, refreshRuntimeStatus],
|
||||
);
|
||||
|
||||
const resetColorGrading = () => {
|
||||
const resetColorGrading = useCallback(() => {
|
||||
commitColorGrading(DEFAULT_COLOR_GRADING);
|
||||
};
|
||||
}, [commitColorGrading]);
|
||||
|
||||
const commitCompare = useCallback(
|
||||
(nextCompare: ColorGradingCompareState) => {
|
||||
const active = isHfColorGradingActive(grading);
|
||||
const normalized = {
|
||||
enabled: nextCompare.enabled && active,
|
||||
};
|
||||
setCompare(normalized);
|
||||
if (normalized.enabled) postColorGrading(grading);
|
||||
postCompare(normalized);
|
||||
(enabled: boolean) => {
|
||||
const nextEnabled = enabled && isHfColorGradingActive(grading);
|
||||
setCompareEnabled(nextEnabled);
|
||||
if (nextEnabled) postColorGrading(grading);
|
||||
postCompare(nextEnabled);
|
||||
window.setTimeout(refreshRuntimeStatus, 50);
|
||||
},
|
||||
[grading, postColorGrading, postCompare, refreshRuntimeStatus],
|
||||
@@ -364,9 +331,9 @@ export function ColorGradingSection({
|
||||
accessory={
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<HoldBeforeButton
|
||||
active={compare.enabled}
|
||||
active={compareEnabled}
|
||||
disabled={!isHfColorGradingActive(grading)}
|
||||
onHoldChange={(holding) => commitCompare({ enabled: holding })}
|
||||
onHoldChange={commitCompare}
|
||||
/>
|
||||
<StatusPill status={runtimeStatus} />
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user