Description of the issue:
When calling the method getImageData from the CanvasRenderingContext2D api, the rgb values of the data property, don’t match the rgb values used in the fillStyle property.
Steps to Reproduce (add as many as necessary): 1. 2. 3.
-
Draw any number of shapes on the canvas with different colors
-
Add pointer event or click event on the canvas
-
Get a 1x1 pixel at the offsetX and offsetY location on the canvas
-
Compare the rgb values from the
getImageDatawith the rgb used when filling the shape.
Actual Result (gifs and screenshots are welcome!):
Expected result
Reproduces how often:
It happens every time although some colors might work just fine
Operating System and Brave Version(See the About Brave page in the main menu):
1.93.137 Chromium: 151.0.7922.169 (Official Build) (arm64)
OS: macOS Version 26.5.2 (Build 25F84)
Additional Information:
I stumbled on this issue when implementing hit testing for shapes on the canvas.
On chrome, firefox and safari it works just fine.
Here the typescript code i used for testing. You can strip the types, or test using vite.
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
const hitCanvas = new OffscreenCanvas(canvas.width, canvas.height);
const hitCtx = hitCanvas.getContext("2d", { willReadFrequently: true })!;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
hitCanvas.width = window.innerWidth;
hitCanvas.height = window.innerHeight;
}
window.addEventListener("resize", resize);
resize();
let nextColorId = 1;
function getColorId() {
return nextColorId++;
}
function idToColor(id: number): string {
const r = id & 0xff;
const g = (id >> 8) & 0xff;
const b = (id >> 16) & 0xff;
return `rgb(${r},${g},${b})`;
}
function colorToId(r: number, g: number, b: number) {
return r | (g << 8) | (b << 16);
}
abstract class Interactive {
readonly colorID = getColorId();
abstract drawHitbox(hitCtx: OffscreenCanvasRenderingContext2D): void;
}
class Point extends Interactive {
constructor(
public x: number,
public y: number,
) {
super();
}
draw(ctx: CanvasRenderingContext2D, style: Partial<{ fill: string; size: number }> = {}) {
const { fill = "rgb(0,0,0)", size = 10 } = style;
ctx.beginPath();
ctx.arc(this.x, this.y, size, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
}
drawHitbox(hitCtx: OffscreenCanvasRenderingContext2D, style: Partial<{ size: number }> = {}) {
const { size = 10 } = style;
hitCtx.beginPath();
hitCtx.arc(this.x, this.y, size * 2, 0, Math.PI * 2);
hitCtx.closePath();
hitCtx.fillStyle = idToColor(this.colorID);
hitCtx.fill();
}
}
class Text {
constructor(
public x: number,
public y: number,
public value: string,
) {}
draw(ctx: CanvasRenderingContext2D) {
ctx.font = "16px monospace";
ctx.fillStyle = "black";
ctx.fillText(this.value, this.x, this.y);
}
}
class Shape extends Interactive {
constructor(
public x: number,
public y: number,
) {
super();
}
draw(ctx: CanvasRenderingContext2D, style: Partial<{ fill: string }> = {}) {
const { fill = "rgb(0,0,0)" } = style;
ctx.beginPath();
ctx.moveTo(100, 100);
ctx.bezierCurveTo(-100, 200, 100, 300, 200, 300);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
}
drawHitbox(hitCtx: OffscreenCanvasRenderingContext2D) {
hitCtx.beginPath();
hitCtx.moveTo(100, 100);
hitCtx.bezierCurveTo(-100, 200, 100, 300, 200, 300);
hitCtx.closePath();
hitCtx.fillStyle = idToColor(this.colorID);
hitCtx.fill();
}
}
const hitMap = new Map<number, Interactive>();
const points = [
new Point(100, 100),
new Point(200, 300),
new Point(400, 200),
new Point(500, 500),
new Shape(150, 200),
];
let selected: Interactive | null = null;
let hovered: Interactive | null = null;
const rgbAtPointer = new Text(0, 0, "");
for (const point of points) hitMap.set(point.colorID, point);
const pointsTextLabels = points.map(
(point) => new Text(point.x + 10, point.y + 10, "Point: " + idToColor(point.colorID)),
);
function clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
hitCtx.clearRect(0, 0, hitCanvas.width, hitCanvas.height);
}
function render() {
clear();
if (hovered) canvas.style.cursor = "pointer";
else canvas.style.cursor = "unset";
for (const point of points) {
point.draw(ctx, {
fill:
selected === point ? "red"
: hovered === point ? "orange"
: "black",
});
point.drawHitbox(hitCtx);
}
for (const label of pointsTextLabels) {
label.draw(ctx);
}
rgbAtPointer.draw(ctx);
requestAnimationFrame(render);
}
render();
canvas.addEventListener("click", (e) => {
const x = e.offsetX;
const y = e.offsetY;
const pixel = hitCtx.getImageData(x, y, 1, 1).data;
const [r, g, b, a] = pixel;
if (a !== 0 && a !== 255) return;
const id = colorToId(r!, g!, b!);
const elm = hitMap.get(id);
selected = selected === elm || !elm ? null : elm;
});
canvas.addEventListener("pointermove", (e) => {
const x = e.offsetX;
const y = e.offsetY;
const pixel = hitCtx.getImageData(x, y, 1, 1).data;
const [r, g, b, a] = pixel;
rgbAtPointer.x = x - 10;
rgbAtPointer.y = y - 10;
rgbAtPointer.value = `Mouse: rgb(${r}, ${g}, ${b})`;
if (a !== 0 && a !== 255) return;
const id = colorToId(r!, g!, b!);
const elm = hitMap.get(id);
hovered = elm ?? null;
});



