From 56dacaa970d7ff3c41a36dbf55a682fd071951f5 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Fri, 30 Oct 2026 20:41:46 +1100 Subject: [PATCH] fix: draw html5 frames on a software canvas so Firefox keeps their gradients --- src/components/canvas/players/html5-player.ts | 5 +++-- tests/html5-player.test.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/canvas/players/html5-player.ts b/src/components/canvas/players/html5-player.ts index dd2b8b0e..2b3bb7a5 100644 --- a/src/components/canvas/players/html5-player.ts +++ b/src/components/canvas/players/html5-player.ts @@ -41,7 +41,7 @@ const BAKE_SCALES = [0.25, 0.5, 0.75, 1]; // A remembered frame holds its body markup, so a window bounds the memory. 300 frames covers a 10 s loop at 30 fps. const REPEAT_WINDOW_FRAMES = 300; // Part of every bake's cache key, so raising it redraws every cached bake. -const FRAME_DRAWING_VERSION = 1; +const FRAME_DRAWING_VERSION = 2; const ANIMATION_OVERRIDE = ``; // A base64 bitmap in a frame's markup: a CSS url(), an source, or a canvas painted as a background. @@ -109,7 +109,8 @@ async function foreignObjectSvgToWebp(svg: string, width: number, height: number const canvas = document.createElement("canvas"); canvas.width = width * scale; canvas.height = height * scale; - const ctx = canvas.getContext("2d"); + // Firefox's GPU canvas draws some of a frame's gradients as a solid fill. A canvas set for reading back draws in software. + const ctx = canvas.getContext("2d", { willReadFrequently: true }); if (!ctx) throw new Error("2D context unavailable for foreignObject rasterise"); ctx.drawImage(img, 0, 0, width * scale, height * scale); const blob = await new Promise(resolve => { diff --git a/tests/html5-player.test.ts b/tests/html5-player.test.ts index 6516f45a..924fc8b4 100644 --- a/tests/html5-player.test.ts +++ b/tests/html5-player.test.ts @@ -1050,6 +1050,15 @@ it("draws frames at the bake scale and sizes their texture back to the clip", as expect((harness.contentContainer.children.at(-1) as PlaybackSprite).texture.resolution).toBe(0.5); }); +it("draws frames on a software canvas, since Firefox's GPU canvas fills some gradients solid", async () => { + const { harness } = createStillPlayer(); + const getContext = jest.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({ drawImage: jest.fn() } as never); + + await harness.runCapture(); + + expect(getContext).toHaveBeenCalledWith("2d", { willReadFrequently: true }); +}); + it("draws a full-size frame of a fractional clip size as an unscaled draw does", async () => { const { harness } = createStillPlayer(); Object.assign(harness, { renderedWidth: 1080.6, renderedHeight: 1920.4 });