From 96d90feb50e10bd12a22fd668b856678f803fd9d Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 9 Jul 2026 15:18:02 -0700 Subject: [PATCH] feat(studio): add the 10 Adjust sliders to FlatColorGradingSection --- ...pertyPanelFlatColorGradingSection.test.tsx | 95 +++++++++++++++++++ .../propertyPanelFlatColorGradingSection.tsx | 65 +++++++++++++ 2 files changed, 160 insertions(+) diff --git a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx index c9f79171f..5601b6300 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx @@ -183,3 +183,98 @@ describe("FlatColorGradingSection — Preset + LUT", () => { act(() => root.unmount()); }); }); + +describe("FlatColorGradingSection — Adjust sliders", () => { + it("renders all 10 adjust rows with a center tick, formatting exposure distinctly from percentage sliders", () => { + const { host, root } = renderInto(); + const adjustRows = host.querySelectorAll('[data-flat-grade-adjust="true"]'); + expect(adjustRows).toHaveLength(10); + for (const row of Array.from(adjustRows)) { + expect(row.querySelector('[data-flat-slider-center-tick="true"]')).not.toBeNull(); + } + expect(host.textContent).toContain("+0.00"); + act(() => root.unmount()); + }); + + it("commits an adjust change scaled correctly and shows a reset when non-neutral", () => { + const onCommitColorGrading = vi.fn(); + const grading = { ...neutralGrading(), adjust: { ...neutralGrading().adjust, contrast: 0.12 } }; + const { host, root } = renderInto( + , + ); + const contrastRow = Array.from(host.querySelectorAll('[data-flat-grade-adjust="true"]')).find( + (row) => row.textContent?.includes("Contrast"), + ); + if (!contrastRow) throw new Error("expected a Contrast row"); + const resetButton = contrastRow.querySelector( + '[data-flat-slider-reset="true"]', + ); + expect(resetButton).not.toBeNull(); + act(() => resetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onCommitColorGrading).toHaveBeenCalledTimes(1); + expect(onCommitColorGrading.mock.calls[0][0].adjust.contrast).toBe(0); + act(() => root.unmount()); + }); + + it("commits a dragged contrast value on slider track pointerdown, scaled from percent back to the internal -1..1 range", () => { + const onCommitColorGrading = vi.fn(); + const { host, root } = renderInto( + , + ); + const contrastRow = Array.from(host.querySelectorAll('[data-flat-grade-adjust="true"]')).find( + (row) => row.textContent?.includes("Contrast"), + ); + if (!contrastRow) throw new Error("expected a Contrast row"); + const track = contrastRow.querySelector('[data-flat-slider-track="true"]'); + if (!track) throw new Error("expected a slider track"); + Object.defineProperty(track, "getBoundingClientRect", { + value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }), + }); + act(() => { + // min=-100, max=100, step=1, ratio=0.75 -> raw=50 -> commit(50) -> adjust.contrast = 50/100 = 0.5 + track.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 75 })); + }); + expect(onCommitColorGrading).toHaveBeenCalledTimes(1); + expect(onCommitColorGrading.mock.calls[0][0].adjust.contrast).toBe(0.5); + act(() => root.unmount()); + }); + + it("commits a dragged exposure value scaled into stops, keeping other adjust keys untouched", () => { + const onCommitColorGrading = vi.fn(); + const grading = { + ...neutralGrading(), + adjust: { ...neutralGrading().adjust, saturation: 0.2 }, + }; + const { host, root } = renderInto( + , + ); + const exposureRow = Array.from(host.querySelectorAll('[data-flat-grade-adjust="true"]')).find( + (row) => row.textContent?.includes("Exposure"), + ); + if (!exposureRow) throw new Error("expected an Exposure row"); + const track = exposureRow.querySelector('[data-flat-slider-track="true"]'); + if (!track) throw new Error("expected a slider track"); + Object.defineProperty(track, "getBoundingClientRect", { + value: () => ({ left: 0, width: 200, top: 0, height: 2, right: 200, bottom: 2 }), + }); + act(() => { + // min=-200, max=200, step=5, ratio=1.0 -> raw=200 -> commit(200) -> adjust.exposure = 200/100 = 2 + track.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 200 })); + }); + expect(onCommitColorGrading).toHaveBeenCalledTimes(1); + expect(onCommitColorGrading.mock.calls[0][0].adjust.exposure).toBe(2); + expect(onCommitColorGrading.mock.calls[0][0].adjust.saturation).toBe(0.2); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx index dd161efba..e36e8aa61 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx @@ -3,6 +3,7 @@ import { HF_COLOR_GRADING_PRESETS, isHfColorGradingActive, normalizeHfColorGrading, + type HfColorGradingAdjustKey, type NormalizedHfColorGrading, } from "@hyperframes/core/color-grading"; import { Compare, Plus, RotateCcw } from "../../icons/SystemIcons"; @@ -78,6 +79,33 @@ export function FlatColorGradingAccessory({ const PRESET_OPTIONS = HF_COLOR_GRADING_PRESETS.map((p) => ({ value: p.id, label: p.label })); +const ADJUST_SLIDERS: Array<{ + key: HfColorGradingAdjustKey; + label: string; + min: number; + max: number; + step: number; +}> = [ + { key: "exposure", label: "Exposure", min: -200, max: 200, step: 5 }, + { key: "contrast", label: "Contrast", min: -100, max: 100, step: 1 }, + { key: "highlights", label: "Highlights", min: -100, max: 100, step: 1 }, + { key: "shadows", label: "Shadows", min: -100, max: 100, step: 1 }, + { key: "whites", label: "White Point", min: -100, max: 100, step: 1 }, + { key: "blacks", label: "Black Point", min: -100, max: 100, step: 1 }, + { key: "temperature", label: "Warmth", min: -100, max: 100, step: 1 }, + { key: "tint", label: "Tint", min: -100, max: 100, step: 1 }, + { key: "vibrance", label: "Vibrance", min: -100, max: 100, step: 1 }, + { key: "saturation", label: "Saturation", min: -100, max: 100, step: 1 }, +]; + +function formatAdjustValue(key: HfColorGradingAdjustKey, rawPercent: number): string { + if (key === "exposure") { + const stops = rawPercent / 100; + return `${stops >= 0 ? "+" : ""}${stops.toFixed(2)}`; + } + return `${Math.round(rawPercent)}%`; +} + export function FlatColorGradingSection({ grading, assets, @@ -228,6 +256,43 @@ export function FlatColorGradingSection({ )} + +
+
+ Adjust +
+ {ADJUST_SLIDERS.map((slider) => { + const scale = slider.key === "exposure" ? 100 : 100; + const rawPercent = grading.adjust[slider.key] * scale; + const isSet = Math.abs(grading.adjust[slider.key]) > 1e-6; + return ( +
+ + onCommitColorGrading({ + ...grading, + adjust: { ...grading.adjust, [slider.key]: next / scale }, + }) + } + onReset={() => + onCommitColorGrading({ + ...grading, + adjust: { ...grading.adjust, [slider.key]: 0 }, + }) + } + /> +
+ ); + })} +
); }