The issue is that ctx.scale() changes the canvas coordinate system, but getImageData() works with the canvas’s actual pixel coordinates. With a device pixel ratio of 2, for example, the rectangle drawn at logical (20, 20) ends up around device pixel (40, 40).
So reading (20, 20) samples a different pixel than the one you expect.
One approach is to keep drawing in CSS/logical coordinates but convert the sampling coordinates to device pixels:
I’d also make sure the canvas backing dimensions are scaled appropriately for the DPR. Otherwise you can still run into blurry rendering or coordinate mismatches.
Ah, you’ve spotted an interesting detail there. The scaling does indeed shift things in a way that can be deceptive. We’ll post the full solution later today.