Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/core/src/runtime/colorGrading.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,36 @@ describe("createColorGradingRuntime", () => {
expect(canvas.style.opacity).toBe("1");
});

it("updates canvas opacity through grading's own hide when CSS animation progresses (#3329)", () => {
const video = makeDrawableVideo();
// Simulate a CSS entrance animation: the authored opacity is empty (no
// inline opacity), but at parse time the element has opacity 0 from the
// animation's initial keyframe.
video.setAttribute("data-hf-authored-opacity", "");
document.body.appendChild(video);

runtime = createColorGradingRuntime();
const canvas = document.querySelector<HTMLCanvasElement>("[data-hf-color-grading-canvas]");
if (!canvas) throw new Error("Expected color grading canvas");

// Source is hidden by color grading.
expect(video.style.getPropertyValue("opacity")).toBe("0");
expect(video.style.getPropertyPriority("opacity")).toBe("important");

// The canvas opacity should NOT be frozen at "0" — after the hide is
// temporarily lifted, jsdom's getComputedStyle returns "" (no inline
// opacity set), which the code normalizes to "1".
expect(canvas.style.opacity).toBe("1");

// Simulate the CSS animation progressing by re-reading after a redraw.
runtime.redraw();
expect(canvas.style.opacity).toBe("1");

// Source remains hidden throughout.
expect(video.style.getPropertyValue("opacity")).toBe("0");
expect(video.style.getPropertyPriority("opacity")).toBe("important");
});

it("allows a drawable producer render frame to initialize hidden source grading", () => {
const video = makeDrawableVideo();
video.style.display = "none";
Expand Down
32 changes: 23 additions & 9 deletions packages/core/src/runtime/colorGrading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3065,16 +3065,30 @@ function drawEntry(entry: ColorGradingEntry): boolean {
const sourceVisibility = entry.element.style.getPropertyValue("visibility");
const injectedFrameSource = isRenderFrameImage(source);
if (injectedFrameSource) keepCanvasAboveSource(entry, source);
// When color grading hid the source (opacity:0 !important), temporarily
// restore the authored inline opacity so getComputedStyle reflects the CSS
// animation's current value instead of our own hide. Without this, a CSS
// entrance animation (opacity: 0→1) freezes at its first-frame value for
// the entire render because the !important blocks all animation effects on
// the property (#3329). The restore–read–rehide is synchronous, so no
// repaint occurs between the style writes.
if (hiddenByColorGrading && !injectedFrameSource) {
if (entry.sourceInlineOpacity !== null) {
entry.element.style.setProperty(
"opacity",
entry.sourceInlineOpacity,
entry.sourceInlineOpacityPriority || "",
);
} else {
entry.element.style.removeProperty("opacity");
}
}
const computed = window.getComputedStyle(injectedFrameSource ? source : entry.element);
// `hideSourceElement` owns the source's inline opacity while grading is active
// (opacity:0 !important), so reading it back would mirror grading's own hide
// onto the canvas and blank it. That gate is about opacity ONLY: grading never
// writes `visibility`, so the source's computed visibility always tracks the
// clip window and has to be re-read every frame. Leaving it stale let the
// canvas keep an explicit `visibility: visible` and paint straight through an
// inactive ancestor clip's inherited `visibility: hidden`.
if (injectedFrameSource || !hiddenByColorGrading) {
entry.sourceOpacityForCanvas = computed.opacity || "1";
// Grading never writes `visibility`, so the source's computed visibility
// always tracks the clip window and has to be re-read every frame.
entry.sourceOpacityForCanvas = computed.opacity || "1";
if (hiddenByColorGrading && !injectedFrameSource) {
entry.element.style.setProperty("opacity", "0", "important");
}
entry.sourceVisibleForCanvas =
(injectedFrameSource || sourceVisibility !== "hidden") && computed.visibility !== "hidden";
Expand Down
Loading