From 8d998926f9b49698302d0a565da4f261b8f28e8a Mon Sep 17 00:00:00 2001 From: by-ae <25704492+by-ae@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:21:23 +1000 Subject: [PATCH] fix: blend the canvas background over black The opacity slider is documented as a blend over black, and the preview image does that. The editor composited the image over #1a1a1a instead. --- tests/caption_layout.test.js | 11 +++++++++++ web/js/caption_layout.js | 9 +++++++++ web/js/ideogram4_prompt_builder_enhanced.js | 13 ++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/caption_layout.test.js b/tests/caption_layout.test.js index a1bccbd..1c8b492 100644 --- a/tests/caption_layout.test.js +++ b/tests/caption_layout.test.js @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { + backgroundPlateShown, captionText, cleanPalette, elementToRegion, @@ -236,6 +237,16 @@ describe("overlapping regions", () => { }); }); +describe("background image plate", () => { + it("is black only while a reference image is actually drawn", () => { + assert.equal(backgroundPlateShown("/view?filename=bg.png", 0.5), true); + assert.equal(backgroundPlateShown("/view?filename=bg.png", 1), true); + assert.equal(backgroundPlateShown("/view?filename=bg.png", 0), false); + assert.equal(backgroundPlateShown("", 0.5), false); + assert.equal(backgroundPlateShown(null, 0.5), false); + }); +}); + describe("color palettes", () => { it("uppercases #RRGGBB, expands #RGB, and drops values Ideogram rejects", () => { assert.deepEqual( diff --git a/web/js/caption_layout.js b/web/js/caption_layout.js index 9a0f8c0..9e41a64 100644 --- a/web/js/caption_layout.js +++ b/web/js/caption_layout.js @@ -228,6 +228,15 @@ export function cleanPalette(colors, limit) { return out; } +/** + * The reference image is drawn with CSS opacity. The black plate is what + * that opacity blends over, matching the preview. Hidden when there is no + * image, so the empty canvas stays the editor gray. + */ +export function backgroundPlateShown(url, opacity) { + return Boolean(url) && Number(opacity) > 0; +} + /** Lowercase #rrggbb for input[type=color]. Anything else is unusable there. */ export function swatchHex(color) { const hex = normalizeHex(color); diff --git a/web/js/ideogram4_prompt_builder_enhanced.js b/web/js/ideogram4_prompt_builder_enhanced.js index 4140455..40e9466 100644 --- a/web/js/ideogram4_prompt_builder_enhanced.js +++ b/web/js/ideogram4_prompt_builder_enhanced.js @@ -8,6 +8,7 @@ import { normalizeStoreName, savedPresetCollision, } from "./ideogram4_shared.js"; import { + backgroundPlateShown, captionText, cleanPalette, dimsFromAspect, elementToRegion, ideogramResolutionWarning, parseCaptionText, snapLayoutDim, @@ -129,6 +130,7 @@ function injectStyle() { s.textContent = ` .byae-ideo-wrap { display:flex; flex-direction:column; overflow:hidden; position:relative; pointer-events:auto; gap:4px; width:100%; box-sizing:border-box; } .byae-ideo-canvas-wrap { position:relative; flex:0 0 auto; width:100%; border-radius:4px; overflow:hidden; background:#1a1a1a; } + .byae-ideo-bg-plate { position:absolute; inset:0; background:#000; pointer-events:none; display:none; } .byae-ideo-bg { position:absolute; inset:0; width:100%; height:100%; object-fit:fill; pointer-events:none; display:none; } .byae-ideo-canvas { cursor:crosshair; display:block; width:100%; height:auto; position:relative; z-index:1; background:transparent; border-radius:4px; outline:none; } .byae-ideo-bar { display:flex; align-items:center; gap:6px; font:11px sans-serif; color:#aaa; user-select:none; padding:0 2px; flex:0 0 auto; flex-wrap:wrap; width:100%; box-sizing:border-box; } @@ -753,6 +755,8 @@ app.registerExtension({ const canvasWrap = document.createElement("div"); canvasWrap.className = "byae-ideo-canvas-wrap"; + const bgPlate = document.createElement("div"); + bgPlate.className = "byae-ideo-bg-plate"; const bgImgEl = document.createElement("img"); bgImgEl.className = "byae-ideo-bg"; bgImgEl.alt = ""; @@ -766,6 +770,7 @@ app.registerExtension({ const canvasEl = document.createElement("canvas"); canvasEl.className = "byae-ideo-canvas"; canvasEl.tabIndex = 0; + canvasWrap.appendChild(bgPlate); canvasWrap.appendChild(bgImgEl); canvasWrap.appendChild(canvasEl); let ctx = canvasEl.getContext("2d"); @@ -2233,11 +2238,16 @@ app.registerExtension({ } function syncBgDisplay() { + const host = getEditorHost(); const { bgImgEl: bg } = getLiveCanvas(); + const plate = host?.querySelector?.(".byae-ideo-bg-plate"); const url = node._bgDisplayUrl || ""; const op = getBgOpacity(); if (!bg) return; - if (url && op > 0) { + // Opacity blends the image with whatever is painted behind it. The + // plate is black so that matches the preview, which composites onto black. + const show = backgroundPlateShown(url, op); + if (show) { if (bg.dataset.byaeSrc !== url) { bg.dataset.byaeSrc = url; bg.src = url; @@ -2249,6 +2259,7 @@ app.registerExtension({ bg.removeAttribute("src"); bg.style.display = "none"; } + if (plate) plate.style.display = show ? "block" : "none"; } function applyBgDisplay(displayUrl) {