cdelambert
cdelambert

Reputation: 31

HTML canvas drawing and erasing with transparent png base

Here is a jsfiddle https://jsfiddle.net/g10qgefy/46/

if (drawingMode === 'brush') {
      ctx.globalCompositeOperation = "source-atop";

    } else { //erase
      ctx.globalCompositeOperation = "destination-out";
      ctx.strokeStyle = 'rgba(1,0,0,0)';
    }

I am trying to create this simple drawing feature. I have a canvas element, I draw an image to it using drawImage(). Then, you can brush on the non-transparent pixels using canvas method globalCompositeOperation = "source-atop".

That all does what I want. But I also need to be able to erase some of the drawn lines without affecting the transparent image. So I just want to select the erase button and start erasing away the black lines I have drawn.

I have been playing with globalCompositionOperation and changing the values from Canvas Rendering | MDN but these mostly just delete everything.

I'm sure there is a solution - would love to hear your thoughts!

Upvotes: 2

Views: 2711

Answers (2)

user1693593
user1693593

Reputation:

When erasing simply set your current brush as a pattern (with no repeat) using the original image, or to work around most caveats (scale, translate etc.): define the initial canvas (with the image drawn in) as the pattern and store the pattern globally (shown in the modified code below).

Update Although this is correct code-wise, it turns out some browsers currently has issues with using the canvas element as pattern source (a browser bug, in this case FF v59 on osX, ref. Kaiido's comment).

No worries though, there is a simple workaround for this though: just use the original image instead of canvas for the pattern (as shown below - code example updated). ***

This will allow you to draw back portions of the image using the same compositing mode as before.

// define and set brush
let pattern = ctx.createPattern(img, "no-repeat");
ctx.strokeStyle = pattern;

let canvasElement = document.getElementById("myCanvas");
let img = new Image();
let brushSize = 25;
let brushColor = "#000000"
let drawingMode = 'brush';
let ctx = canvasElement.getContext('2d');
let lastX;
let lastY;
let moving = false;
let pattern;

img.src = 'https://i.pinimg.com/originals/49/af/b1/49afb1d21ae594cb7ac3534a15383711.png';
img.onload = () => {
  ctx.drawImage(img, 0, 0);
  ctx.globalCompositeOperation = "source-atop";
  pattern = ctx.createPattern(img, "no-repeat");
}

let eraseButton = document.getElementById('erase');
let brushButton = document.getElementById('brush');

eraseButton.addEventListener('click', () => {
  drawingMode = 'erase';
  ctx.strokeStyle = pattern; // use pattern for style here
})

brushButton.addEventListener('click', () => {
  drawingMode = 'brush';
  ctx.strokeStyle = "#000";                       // restore current color here
})

canvasElement.addEventListener('mousedown', (ev) => {
  moving = true;
  lastX = ev.pageX;
  lastY = ev.pageY;
})

canvasElement.addEventListener('mouseup', (ev) => {
  moving = false;
  lastX = ev.pageX;
  lastY = ev.pageY;
})

canvasElement.addEventListener('mousemove', (ev) => {
  if (moving) {
    let currentX = ev.pageX;
    let currentY = ev.pageY;

    ctx.beginPath();
    ctx.lineJoin = "round";
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(currentX, currentY);
    ctx.closePath();
    //ctx.strokeStyle = brushColor; // can be set during color selecting (if mode=draw)
    ctx.lineWidth = brushSize;
    ctx.stroke();

    lastX = currentX;
    lastY = currentY;
  }

})
canvas {
  position: absolute;
  left: 0;
  border: 5px solid red;
}

button {
  position: absolute;
  top: 550px;
}

#erase {
  left: 60px;
}
<canvas id="myCanvas" width="500" height="500"></canvas>

<button id="brush">
brush
</button>

<button id="erase">
erase
</button>

Upvotes: 1

Kaiido
Kaiido

Reputation: 136608

Keep two layers: the one the user is drawing to, and the rendering one.

Draw the user's drawings on the first layer (offscreen canvas) and then do your compositing with the png image on top of this layer on a second canvas, the rendering one.

This way you can perform your erase calls safely, without having to care about the png layer.

let renderingElement = document.getElementById("myCanvas");
// create an offscreen canvas only for the drawings
let drawingElement = renderingElement.cloneNode();
let drawingCtx = drawingElement.getContext('2d');
let renderingCtx = renderingElement.getContext('2d');


let img = new Image();
let brushSize = 25;
let brushColor = "#000000"
let drawingMode = 'brush';

let lastX;
let lastY;
let moving = false;

img.src = 'https://i.pinimg.com/originals/49/af/b1/49afb1d21ae594cb7ac3534a15383711.png';
img.onload = () => {
  renderingCtx.drawImage(img, 0, 0);
}

let eraseButton = document.getElementById('erase');
let brushButton = document.getElementById('brush');
let exportButton = document.getElementById('export');

eraseButton.addEventListener('click', () => {
  drawingMode = 'erase';
})

brushButton.addEventListener('click', () => {
  drawingMode = 'brush';
})

renderingElement.addEventListener('mousedown', (ev) => {
  moving = true;
  lastX = ev.pageX - renderingElement.offsetLeft;
  lastY = ev.pageY - renderingElement.offsetTop;
})

renderingElement.addEventListener('mouseup', (ev) => {
  moving = false;
  lastX = ev.pageX - renderingElement.offsetLeft;
  lastY = ev.pageY - renderingElement.offsetTop;
})

renderingElement.addEventListener('mousemove', (ev) => {
  if (moving) {
    if (drawingMode === 'brush') {
      drawingCtx.globalCompositeOperation = "source-over";
    } else {
      drawingCtx.globalCompositeOperation = "destination-out";
    }
    let currentX = ev.pageX - renderingElement.offsetLeft;
    let currentY = ev.pageY - renderingElement.offsetTop;

    drawingCtx.beginPath();
    drawingCtx.lineJoin = "round";
    drawingCtx.moveTo(lastX, lastY);
    drawingCtx.lineTo(currentX, currentY);
    drawingCtx.closePath();
    drawingCtx.strokeStyle = brushColor;
    drawingCtx.lineWidth = brushSize;
    drawingCtx.stroke();

    lastX = currentX;
    lastY = currentY;

    // draw to visible canvas
    renderingCtx.clearRect(0, 0, renderingElement.width, renderingElement.height);
    renderingCtx.drawImage(img, 0, 0);
    renderingCtx.globalCompositeOperation = 'source-atop';
    renderingCtx.drawImage(drawingElement, 0, 0);
    
    // reset
    renderingCtx.globalCompositeOperation = 'source-over';
  }

});
canvas {
  border: 5px solid red;
}
<button id="brush">brush</button>
<button id="erase">erase</button>
<canvas id="myCanvas" width="500" height="500"></canvas>

Upvotes: 4

Related Questions