feat(studio): add the 10 Adjust sliders to FlatColorGradingSection

This commit is contained in:
Vance Ingalls
2026-07-14 15:50:51 -07:00
parent 9b6bed2824
commit 96d90feb50
2 changed files with 160 additions and 0 deletions
@@ -183,3 +183,98 @@ describe("FlatColorGradingSection — Preset + LUT", () => {
act(() => root.unmount()); 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(<FlatColorGradingSection {...neutralPropsBase()} />);
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(
<FlatColorGradingSection
{...neutralPropsBase()}
grading={grading}
onCommitColorGrading={onCommitColorGrading}
/>,
);
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<HTMLButtonElement>(
'[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(
<FlatColorGradingSection
{...neutralPropsBase()}
onCommitColorGrading={onCommitColorGrading}
/>,
);
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<HTMLElement>('[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(
<FlatColorGradingSection
{...neutralPropsBase()}
grading={grading}
onCommitColorGrading={onCommitColorGrading}
/>,
);
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<HTMLElement>('[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());
});
});
@@ -3,6 +3,7 @@ import {
HF_COLOR_GRADING_PRESETS, HF_COLOR_GRADING_PRESETS,
isHfColorGradingActive, isHfColorGradingActive,
normalizeHfColorGrading, normalizeHfColorGrading,
type HfColorGradingAdjustKey,
type NormalizedHfColorGrading, type NormalizedHfColorGrading,
} from "@hyperframes/core/color-grading"; } from "@hyperframes/core/color-grading";
import { Compare, Plus, RotateCcw } from "../../icons/SystemIcons"; 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 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({ export function FlatColorGradingSection({
grading, grading,
assets, assets,
@@ -228,6 +256,43 @@ export function FlatColorGradingSection({
</div> </div>
)} )}
</div> </div>
<div className="space-y-0.5 border-t border-panel-hairline pt-1.5">
<div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
Adjust
</div>
{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 (
<div key={slider.key} data-flat-grade-adjust="true">
<FlatSlider
label={slider.label}
value={rawPercent}
min={slider.min}
max={slider.max}
step={slider.step}
tier={isSet ? "explicitCustom" : "default"}
displayValue={formatAdjustValue(slider.key, rawPercent)}
centerTick
onCommit={(next) =>
onCommitColorGrading({
...grading,
adjust: { ...grading.adjust, [slider.key]: next / scale },
})
}
onReset={() =>
onCommitColorGrading({
...grading,
adjust: { ...grading.adjust, [slider.key]: 0 },
})
}
/>
</div>
);
})}
</div>
</div> </div>
); );
} }