Why does drawImage() change the canvas outside the destination box?

32 views Asked by At

Based on the documentation of drawImage(), I'm trying to draw to a specific part of a canvas. However, areas outside of the destination box are being affected, namly having their alpha values zeroed.

I've created a JSFiddle demonstrating the issue.

canvas composition results

I copied the canvas on the left (full yellow) to the canvas on the right and the drew the middle canvas to the center of that same canvas using "destination-atop". You can see that it applied properly but at the same time cleared everything outside the draw area. Same happens with "copy". See the Fiddle for details.

Why does drawImage() affect pixels outside its destination?

2

There are 2 answers

0
Brian White On BEST ANSWER

For reasons unknown to me, the "destination box" (dx, dy, dWidth, dHeight) isn't actually a destination box. It's more like a "center" and "scale" because drawing extends out of this box as though there were transparent source pixels being applied to that outside area.

The fix is to add a clipping rectangle before doing the drawing, as such:

ctx.beginPath()
ctx.rect(dx, dy, dWidth, dHeight)
ctx.clip()
ctx.drawImage(img, dx, dy, dWidth, dHeight)

The resulting Fiddle now does what I expected, applying the mask in the middle to the center of the solid-color image on the left to get the image on the right:

enter image description here

3
0stone0 On

The definition of destination-atop is:

The existing canvas is only kept where it overlaps the new shape. The new shape is drawn behind the canvas content.

You want to keep everything, use source-over:

This is the default setting and draws new shapes on top of the existing canvas content.

const SIZE = 150
const G = 63

let c1 = document.getElementById("c1")
c1.height = c1.width = SIZE
let ctx1 = c1.getContext("2d")
ctx1.fillStyle = "yellow"
ctx1.fillRect(0, 0, SIZE, SIZE)

let c2 = document.getElementById("c2")
c2.height = c2.width = SIZE
let ctx2 = c2.getContext("2d")
let g2= ctx2.createRadialGradient(
    SIZE / 2, SIZE / 2,
    SIZE / 10,
    SIZE / 2, SIZE / 2,
    SIZE / 2 - 1)
g2.addColorStop(0.00, `rgba(${G},${G},${G},0.000)`)
g2.addColorStop(0.25, `rgba(${G},${G},${G},0.500)`)
g2.addColorStop(0.50, `rgba(${G},${G},${G},0.750)`)
g2.addColorStop(0.75, `rgba(${G},${G},${G},0.875)`)
g2.addColorStop(1.00, `rgba(${G},${G},${G},1.000)`)
ctx2.fillStyle = g2
ctx2.globalCompositeOperation = "copy"
ctx2.fillRect(0, 0, SIZE, SIZE)

let c3 = document.getElementById("c3")
c3.height = c3.width = SIZE
let ctx3 = c3.getContext("2d")

ctx3.globalCompositeOperation = "source-over";
ctx3.drawImage(c1, 0, 0, SIZE, SIZE)
ctx3.drawImage(c2, SIZE/4, SIZE/4, SIZE/2, SIZE/2)
canvas {
  border: 2px solid red;
}
<canvas id="c1"></canvas>
<canvas id="c2"></canvas>
<canvas id="c3"></canvas>