mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): restore zero-strength revive, keyboard hold, and HDR detail in flat Grade
This commit is contained in:
@@ -103,6 +103,110 @@ describe("FlatColorGradingAccessory", () => {
|
||||
expect(resetGrading).toHaveBeenCalledTimes(1);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("shows the runtime status message as visible text next to the dot, not only as a title", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingAccessory
|
||||
state={{
|
||||
grading: neutralGrading(),
|
||||
compareEnabled: false,
|
||||
runtimeStatus: { state: "pending", message: "Waiting for shader" },
|
||||
commitCompare: vi.fn(),
|
||||
resetGrading: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const messageEl = host.querySelector('[data-flat-grade-status-message="true"]');
|
||||
expect(messageEl).not.toBeNull();
|
||||
expect(messageEl?.textContent).toBe("Waiting for shader");
|
||||
expect(host.textContent).toContain("Waiting for shader");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
function activeGrading() {
|
||||
const grading = neutralGrading();
|
||||
return { ...grading, adjust: { ...grading.adjust, contrast: 0.2 } };
|
||||
}
|
||||
|
||||
it("activates hold-to-compare on pointerdown and releases on window pointerup", () => {
|
||||
const commitCompare = vi.fn();
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingAccessory
|
||||
state={{
|
||||
grading: activeGrading(),
|
||||
compareEnabled: false,
|
||||
runtimeStatus: { state: "active", message: "Shader active" },
|
||||
commitCompare,
|
||||
resetGrading: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const compareButton = host.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Hold to show original"]',
|
||||
);
|
||||
if (!compareButton) throw new Error("expected a compare button");
|
||||
act(() => compareButton.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true })));
|
||||
expect(commitCompare).toHaveBeenNthCalledWith(1, true);
|
||||
act(() => window.dispatchEvent(new MouseEvent("pointerup", { bubbles: true })));
|
||||
expect(commitCompare).toHaveBeenNthCalledWith(2, false);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("activates hold-to-compare via keyboard Space and releases on keyup", () => {
|
||||
const commitCompare = vi.fn();
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingAccessory
|
||||
state={{
|
||||
grading: activeGrading(),
|
||||
compareEnabled: false,
|
||||
runtimeStatus: { state: "active", message: "Shader active" },
|
||||
commitCompare,
|
||||
resetGrading: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const compareButton = host.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Hold to show original"]',
|
||||
);
|
||||
if (!compareButton) throw new Error("expected a compare button");
|
||||
act(() =>
|
||||
compareButton.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: " ", bubbles: true, cancelable: true }),
|
||||
),
|
||||
);
|
||||
expect(commitCompare).toHaveBeenNthCalledWith(1, true);
|
||||
act(() =>
|
||||
compareButton.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { key: " ", bubbles: true, cancelable: true }),
|
||||
),
|
||||
);
|
||||
expect(commitCompare).toHaveBeenNthCalledWith(2, false);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("releases an active hold when the window loses focus mid-hold", () => {
|
||||
const commitCompare = vi.fn();
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingAccessory
|
||||
state={{
|
||||
grading: activeGrading(),
|
||||
compareEnabled: false,
|
||||
runtimeStatus: { state: "active", message: "Shader active" },
|
||||
commitCompare,
|
||||
resetGrading: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const compareButton = host.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Hold to show original"]',
|
||||
);
|
||||
if (!compareButton) throw new Error("expected a compare button");
|
||||
act(() => compareButton.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true })));
|
||||
expect(commitCompare).toHaveBeenNthCalledWith(1, true);
|
||||
act(() => window.dispatchEvent(new Event("blur")));
|
||||
expect(commitCompare).toHaveBeenNthCalledWith(2, false);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
function neutralPropsBase() {
|
||||
@@ -280,6 +384,43 @@ describe("FlatColorGradingSection — Adjust sliders", () => {
|
||||
expect(onCommitColorGrading.mock.calls[0][0].adjust.saturation).toBe(0.2);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("revives a grade parked at 0% strength back to 100% when an Adjust slider is committed", () => {
|
||||
const onCommitColorGrading = vi.fn();
|
||||
const grading = { ...neutralGrading(), intensity: 0 };
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingSection
|
||||
{...neutralPropsBase()}
|
||||
grading={grading}
|
||||
onCommitColorGrading={onCommitColorGrading}
|
||||
/>,
|
||||
);
|
||||
const contrastRow = findRowByText(host, '[data-flat-grade-adjust="true"]', "Contrast");
|
||||
// min=-100, max=100, step=1, ratio=0.75 -> raw=50 -> commit(50) -> adjust.contrast = 0.5
|
||||
dragSliderTrack(contrastRow, 75, 100);
|
||||
expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
|
||||
expect(onCommitColorGrading.mock.calls[0][0].intensity).toBe(1);
|
||||
expect(onCommitColorGrading.mock.calls[0][0].adjust.contrast).toBe(0.5);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("does NOT force intensity to revive when the Strength slider itself is dragged — it writes the value directly", () => {
|
||||
const onCommitColorGrading = vi.fn();
|
||||
const grading = { ...neutralGrading(), intensity: 0 };
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingSection
|
||||
{...neutralPropsBase()}
|
||||
grading={grading}
|
||||
onCommitColorGrading={onCommitColorGrading}
|
||||
/>,
|
||||
);
|
||||
const strengthRow = findRowByText(host, "div", "Strength", "startsWith");
|
||||
// min=0, max=100, step=1, ratio=0.4 -> raw=40 -> commit(40) -> intensity = 40/100 = 0.4
|
||||
dragSliderTrack(strengthRow, 40, 100);
|
||||
expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
|
||||
expect(onCommitColorGrading.mock.calls[0][0].intensity).toBe(0.4);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
describe("FlatColorGradingSection — Vignette and Grain", () => {
|
||||
@@ -386,6 +527,46 @@ describe("FlatColorGradingSection — HDR banner and Apply scope", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("shows a codec/profile/pixel-format/color detail line in the HDR banner when metadata provides it", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingSection
|
||||
{...neutralPropsBase()}
|
||||
mediaMetadata={{
|
||||
kind: "video",
|
||||
color: {
|
||||
dynamicRange: "hdr",
|
||||
hdrTransfer: "pq",
|
||||
label: "HDR10",
|
||||
isHdr: true,
|
||||
codecName: "hevc",
|
||||
profile: "Main10",
|
||||
pixelFormat: "yuv420p10le",
|
||||
colorPrimaries: "bt2020",
|
||||
colorTransfer: "smpte2084",
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const detail = host.querySelector('[data-flat-grade-hdr-detail="true"]');
|
||||
expect(detail).not.toBeNull();
|
||||
expect(detail?.textContent).toBe("hevc · Main10 · yuv420p10le · bt2020 · smpte2084");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("omits the HDR detail line entirely when no detail fields are populated", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingSection
|
||||
{...neutralPropsBase()}
|
||||
mediaMetadata={{
|
||||
kind: "video",
|
||||
color: { dynamicRange: "hdr", hdrTransfer: "pq", label: "HDR10", isHdr: true },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(host.querySelector('[data-flat-grade-hdr-detail="true"]')).toBeNull();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("omits the HDR banner for SDR media", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatColorGradingSection
|
||||
|
||||
@@ -49,20 +49,43 @@ export function FlatColorGradingAccessory({
|
||||
commitCompare(false);
|
||||
window.removeEventListener("pointerup", release);
|
||||
window.removeEventListener("pointercancel", release);
|
||||
window.removeEventListener("blur", release);
|
||||
};
|
||||
window.addEventListener("pointerup", release);
|
||||
window.addEventListener("pointercancel", release);
|
||||
window.addEventListener("blur", release);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (compareEnabled) commitCompare(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return;
|
||||
e.preventDefault();
|
||||
if (!compareEnabled) commitCompare(true);
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return;
|
||||
e.preventDefault();
|
||||
commitCompare(false);
|
||||
}}
|
||||
title="Hold to show original"
|
||||
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
<Compare size={12} />
|
||||
</button>
|
||||
<span
|
||||
data-flat-grade-status-dot="true"
|
||||
title={runtimeStatus.message}
|
||||
className={`h-[5px] w-[5px] flex-shrink-0 rounded-full ${STATUS_DOT_CLASS[runtimeStatus.state]}`}
|
||||
/>
|
||||
<span className="flex min-w-0 items-center gap-1" title={runtimeStatus.message}>
|
||||
<span
|
||||
data-flat-grade-status-dot="true"
|
||||
title={runtimeStatus.message}
|
||||
className={`h-[5px] w-[5px] flex-shrink-0 rounded-full ${STATUS_DOT_CLASS[runtimeStatus.state]}`}
|
||||
/>
|
||||
<span
|
||||
data-flat-grade-status-message="true"
|
||||
className="max-w-[84px] truncate text-[9px] text-panel-text-4"
|
||||
>
|
||||
{runtimeStatus.message}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
data-flat-grade-reset="true"
|
||||
@@ -100,6 +123,11 @@ const ADJUST_SLIDERS: Array<{
|
||||
{ key: "saturation", label: "Saturation", min: -100, max: 100, step: 1 },
|
||||
];
|
||||
|
||||
function visibleIntensity(grading: NormalizedHfColorGrading): number {
|
||||
// Earlier drafts could persist 0% strength; the next manual edit should revive visible grading.
|
||||
return grading.intensity === 0 ? 1 : grading.intensity;
|
||||
}
|
||||
|
||||
function formatAdjustValue(key: HfColorGradingAdjustKey, rawPercent: number): string {
|
||||
if (key === "exposure") {
|
||||
const stops = rawPercent / 100;
|
||||
@@ -140,6 +168,15 @@ const EFFECT_SLIDERS: Array<{ key: HfColorGradingEffectKey; label: string }> = [
|
||||
|
||||
function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
|
||||
if (metadata?.color.dynamicRange !== "hdr") return null;
|
||||
const details = [
|
||||
metadata.color.codecName,
|
||||
metadata.color.profile,
|
||||
metadata.color.pixelFormat,
|
||||
metadata.color.colorPrimaries,
|
||||
metadata.color.colorTransfer,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
return (
|
||||
<div
|
||||
data-flat-grade-hdr-banner="true"
|
||||
@@ -155,6 +192,14 @@ function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
|
||||
These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
|
||||
is not true HDR color grading yet.
|
||||
</p>
|
||||
{details && (
|
||||
<p
|
||||
data-flat-grade-hdr-detail="true"
|
||||
className="mt-0.5 truncate text-[9px] text-amber-100/55"
|
||||
>
|
||||
{details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -201,7 +246,11 @@ export function FlatColorGradingSection({
|
||||
onCommitColorGrading({ ...grading, intensity: value / 100 });
|
||||
};
|
||||
const applyLut = (src: string | null, intensity = 1) => {
|
||||
onCommitColorGrading({ ...grading, lut: src ? { src, intensity } : null });
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
lut: src ? { src, intensity } : null,
|
||||
});
|
||||
};
|
||||
const importLuts = async (files: FileList | null) => {
|
||||
if (!files?.length || !onImportAssets) return;
|
||||
@@ -225,11 +274,16 @@ export function FlatColorGradingSection({
|
||||
displayValue={`${Math.round(value * 100)}%`}
|
||||
centerTick={key === "vignetteRoundness"}
|
||||
onCommit={(next) =>
|
||||
onCommitColorGrading({ ...grading, details: { ...grading.details, [key]: next / 100 } })
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
details: { ...grading.details, [key]: next / 100 },
|
||||
})
|
||||
}
|
||||
onReset={() =>
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
details: { ...grading.details, [key]: spec.defaultValue },
|
||||
})
|
||||
}
|
||||
@@ -361,12 +415,14 @@ export function FlatColorGradingSection({
|
||||
onCommit={(next) =>
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
adjust: { ...grading.adjust, [slider.key]: next / 100 },
|
||||
})
|
||||
}
|
||||
onReset={() =>
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
adjust: { ...grading.adjust, [slider.key]: 0 },
|
||||
})
|
||||
}
|
||||
@@ -432,12 +488,14 @@ export function FlatColorGradingSection({
|
||||
onCommit={(next) =>
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
effects: { ...grading.effects, [slider.key]: next / 100 },
|
||||
})
|
||||
}
|
||||
onReset={() =>
|
||||
onCommitColorGrading({
|
||||
...grading,
|
||||
intensity: visibleIntensity(grading),
|
||||
effects: { ...grading.effects, [slider.key]: 0 },
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user