Wordpressor
Wordpressor

Reputation: 7553

Clearing canvas "layers" separately?

I've been battling with <canvas> for a while. I want to create an animation/game with lots of different units on different layers.

Due to <canvas> limitation to just one context my approach is as follows:

But this approach does not seem to work properly due to one quirk - in order to stack "layers" on my main canvas I'm doing realCanvas.drawImage(layerCanvas, 0, 0);. Otherwise the layers are not being rendered.

The issue here is ultimately it does not change a thing as everything is in being drawn on my main <canvas> and when I do clearRect on one of my layers it does nothing as the pixels are also drawn on the main canvas in addition to given layer. If I run clearRect on main canvas then the layers are useless as every layer is on main canvas so I'm back to starting point as I'm clearing the whole canvas and layers are not separated at all.

Is there a way to fix it easily? I feel like I'm missing something obvious.

Here's an example, how do I clear blue ball trail without touching background rectangles here? There should be only one blue ball under your cursor. Note it's a very simplified example, I'll have multiple blue balls and multiple other layers. I just want to know how the heck do I clear only one layer in canvas. Note I don't want to use multiple <canvas> elements and don't want to use any libs/engines as I'm trying to learn canvas by this. I know many apps use just one canvas html element, many layers and somehow animate them separately.

Source: https://jsfiddle.net/rpmf4tsb/

Try adding canvas2ctx.clearRect(0,0, canvas.width, canvas.height); under ctx.clearRect(0,0, canvas.width, canvas.height); and it works as supposed but all the layers are being cleared, not only the one with the ball...

Upvotes: 1

Views: 1082

Answers (2)

obscure
obscure

Reputation: 12891

If you look at things from a performance point-of-view, things are better if you use a single visible <canvas> element for your visual output. Nothing is stopping you from doing things on seperate canvases you stack on top of each other though. Maybe there's just a basic misunderstanding here.

You say:

and when I do clearRect on one of my layers it does nothing as the pixels are also drawn on the main canvas in addition to given layer

Well that's not true. If you draw the contents of a freshly cleared canvas onto another canvas it won't overwrite the target canvas with 'nothing'.

Take a look at this example:

let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();

let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>

Our main canvas contains a green background with a black circle and we're utilizing the drawImage() method to draw a dynamically created, freshly cleared canvas onto, which results in a green background with a black circle as the new canvas element did not contain any data to draw. It did not erase the main canvas.

If we change the example a bit, so the second canvas contains a rectangle things will work as expected:

let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();

let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempContext.strokeRect(tempCanvas.width / 2 - 60, tempCanvas.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>

Now if we assume the green background with the circle (tempCanvasA) and the rectangle (tempCanvasB) are two separate canvases we ultimately want to draw to a main canvas it will bring up an important point: the order of drawing.

So this will work:

let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");

let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();

let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);


ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>

while this fails:

let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");

let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();

let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);

ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>

The rectangle is missing! Why does it fail? Because we changed the order we draw the canvases onto the main canvas. In the latter example:

ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);

We first draw tempCanvasB which contains a transparent background & the rectangle and afterwards tempCanvasA with the solid green background - which covers the entire canvas - and the circle. As there are no transparent pixels it will overwrite the rectangle which we've drawn first.

To get to your example with the ball. The problem is that you're drawing the ball to the wrong canvas. Inside your draw function you're doing this:

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ball.draw();
  ball.x = e.clientX;
  ball.y = e.clientY;

  ctx.drawImage(canvas2, 0, 0);

So first you clear ctx, afterwards call ball's draw method which draws onto canvas2ctx and finally drawImage onto ctx with the contents of canvas2ctx.

Instead draw the ball onto the main ctx after using drawImage()

e.g.

// helper functions

function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min)
}

// canvas
let firstRender = true;
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth - 50;
canvas.height = window.innerHeight - 50;
let ctx = canvas.getContext('2d');

// virtual canvas for rectangles layer
let canvas2 = document.createElement("canvas");
canvas2.width = window.innerWidth - 50;
canvas2.height = window.innerHeight - 5;
let canvas2ctx = canvas2.getContext("2d");


let ball = {
    x: 100,
    y: 100,
    vx: 5,
    vy: 2,
    radius: 25,
    color: 'blue',
    draw: function() {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
        ctx.closePath();
        ctx.fillStyle = this.color;
        ctx.fill();
    }
};

function draw(e) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(canvas2, 0, 0);
    ball.draw();
    ball.x = e.clientX;
    ball.y = e.clientY;
    if (firstRender) {
        drawRandomRectangles()
        firstRender = false;
    }
}

function drawRandomRectangles() {
    for (i = 0; i < 50; i++) {
        canvas2ctx.beginPath();
        canvas2ctx.rect(randomInt(0, window.innerWidth - 50), randomInt(0, window.innerWidth - 50), randomInt(5, 20), randomInt(5, 20));
        canvas2ctx.stroke();
    }
}

canvas.addEventListener('mousemove', function(e) {
    draw(e);
});


ball.draw();
<canvas id="canvas"></canvas>

Upvotes: 3

Stefan Walter
Stefan Walter

Reputation: 126

Thinking about your approach of multiple canvas stacking above each other sounds like an interesting approach to get things done. I would not recommend doing this in that way and therefore handle multiple layers through JavaScript and then still render every time everything new. Especially if you will use animations, then I believe that multiple not synchronized canvases will give you another sort of headache.

Then you would do the following:

  1. Clear your canvas with clearRect.
  2. Draw in an iteration each layer above each other

I hope this theoretical explanation helps.

Now to your code: At the end of the day your ctx and canvas2ctx are in the very same context, because they are from the same canvas. That makes anyway not much sense.

Upvotes: 2

Related Questions