From 724790545434a671bee80e900b3186e7f0dcc8ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 18:07:45 +0000 Subject: [PATCH 1/2] Initial plan From efa4a5ee355c549199d0333f8bb5e52af28c7ada Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 18:21:20 +0000 Subject: [PATCH 2/2] fix: color picker now correctly composites layer blend modes when sampling Previously, getColorAtCoordinate used stage.toCanvas() which does not apply CSS mix-blend-mode set on layer canvas elements. This caused the color picker to return raw layer colors instead of the visually composited result. The fix manually composites each visible Konva stage layer in order, reading the CSS mix-blend-mode from the underlying HTML canvas element and mapping it to the equivalent Canvas 2D globalCompositeOperation (only 'normal' -> 'source-over' needs remapping; all others match). Device pixel ratio is accounted for when converting CSS coordinates to physical canvas pixel coordinates. Bounds checking is also added. Agent-Logs-Url: https://github.com/dunkeroni/InvokeAI/sessions/5c74db51-8269-4ad8-80d5-c40b0affeb70 Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com> --- .../src/features/controlLayers/konva/util.ts | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 4b8e2ad97f7..d2d8f3d5ec1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -748,21 +748,52 @@ export const getPointerType = (e: KonvaEventObject): 'mouse' | 'pe }; /** - * Gets the color at the given coordinate on the stage. + * Gets the color at the given coordinate on the stage, correctly compositing all visible layers with their CSS + * blend modes (mix-blend-mode). Using stage.toCanvas() is not sufficient because it renders each layer via the + * Canvas 2D API without applying CSS mix-blend-mode, which is a browser compositor-level operation. Instead, we + * manually composite each visible layer's canvas using the corresponding Canvas 2D globalCompositeOperation. * @param stage The konva stage. * @param coord The coordinate to get the color at. This must be the _absolute_ coordinate on the stage. * @returns The color under the coordinate, or null if there was a problem getting the color. */ export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): RgbaColor | null => { - const ctx = stage - .toCanvas({ x: coord.x, y: coord.y, width: 1, height: 1, imageSmoothingEnabled: false }) - .getContext('2d'); + const offscreenCanvas = document.createElement('canvas'); + offscreenCanvas.width = 1; + offscreenCanvas.height = 1; + const offscreenCtx = offscreenCanvas.getContext('2d'); - if (!ctx) { + if (!offscreenCtx) { return null; } - const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; + // coord is in CSS pixel coordinates (from stage.getPointerPosition()). The underlying layer canvas elements + // are scaled by devicePixelRatio for HiDPI displays, so we must convert to physical pixel coordinates. + const pixelRatio = window.devicePixelRatio || 1; + const sourceX = Math.floor(coord.x * pixelRatio); + const sourceY = Math.floor(coord.y * pixelRatio); + + for (const layer of stage.getLayers()) { + if (!layer.visible()) { + continue; + } + + // Access the underlying HTML canvas element for this Konva layer. This uses the internal `_canvas` property, + // consistent with the pattern used elsewhere in this codebase (e.g. CanvasEntityAdapterRasterLayer). + const htmlCanvas = (layer.getCanvas() as { _canvas?: HTMLCanvasElement })._canvas; + if (!htmlCanvas || sourceX < 0 || sourceY < 0 || sourceX >= htmlCanvas.width || sourceY >= htmlCanvas.height) { + continue; + } + + // Map the CSS mix-blend-mode to a Canvas 2D globalCompositeOperation. CSS 'normal' corresponds to Canvas 2D + // 'source-over'; all other blend mode names are identical between CSS and Canvas 2D. + const mixBlendMode = htmlCanvas.style.mixBlendMode; + const compositeOp = !mixBlendMode || mixBlendMode === 'normal' ? 'source-over' : mixBlendMode; + + offscreenCtx.globalCompositeOperation = compositeOp as GlobalCompositeOperation; + offscreenCtx.drawImage(htmlCanvas, sourceX, sourceY, 1, 1, 0, 0, 1, 1); + } + + const [r, g, b, a] = offscreenCtx.getImageData(0, 0, 1, 1).data; if (r === undefined || g === undefined || b === undefined || a === undefined) { return null;