How to use clearRect to not draw an moving object on canvas

Viewed 142

I have a blue circle which is rotating around the red circle and moves on canvas continuously in one direction as long as the button is pressed.

Now I want to draw with the red circle while it is moving when the button is pressed (trace of its path).

  • Problems:

I have tried to make changes to clearRect() but I didn't succeed. the blue circle starts to draw on the canvas while moving which I don't need.

If its not possible to do with clearRect() function, Is it possible to do this by stacking canvas layers. Please help with example

const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let positionX = 100;
let positionY = 100;
let X = 50;
let Y = 50;
let angle = 0;
let mouseButtonDown = false;

document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);

function circle(){
    ctx.fillStyle = 'red';
    ctx.beginPath();
    ctx.arc(X, Y, 20, 0, Math.PI*2);
    ctx.closePath();
    ctx.fill();
}
function direction(){
    ctx.fillStyle = 'blue';
    ctx.beginPath();
    ctx.arc(positionX + X, positionY + Y, 10, 0, Math.PI*2);
    ctx.closePath();
    positionX = 35 * Math.sin(angle);
    positionY = 35 * Math.cos(angle);
    ctx.fill();   
}
function animate(){
    if (mouseButtonDown) {
        X += positionX / 10;
        Y += positionY / 10;
    } else {
        angle += 0.1;
    }
    ctx.clearRect(X-positionX,Y-positionY, 20, 20);
    circle();
    direction();
    requestAnimationFrame(animate);   
}
animate();
#canvas1{
    position: absolute;
    top:0;
    left: 0;
    width: 100%;
    height: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <canvas id="canvas1"></canvas>
    
    <script src="script.js"></script>
</body>
</html>

3 Answers

Don`t stack canvas on the page

Each canvas you add to the page increases the amount of work the GPU and page compositor needs to do to render the page.

Use a second canvas that is not on the page and do the compositing by rendering the canvas to the onpage canvas using ctx.drawImage(secondCanvas, 0, 0).

This reduces the workload for the compositor, and in many cases avoid the need to do an addition image render (composite) for the second canvas I.E. onpage can require 3 drawImages (one for each canvas and once for the result) rather than 2 (once in your code and once as the result) if you use only one onpage canvas.

Using second canvas

Create a second canvas to store the drawn red lines.

You can create a copy of a canvas using

    function copyCanvas(canvas, copyContent = false) {
        const can = Object.assign(document.createElement("canvas"), {
            width: canvas.width, height: canvas.height
        });
        can.ctx = can.getContext("2d");
        copyContent && can.ctx.drawImage(canvas, 0, 0);
        return can;
    }

When you create render functions like circle, and direction pass as an argument the 2D context eg circle(ctx) so that it is easy to direct the rendering to any canvas.

     function circle(ctx){
        ctx.fillStyle = 'red';
        ctx.beginPath();
        ctx.arc(X, Y, redSize, 0, Math.PI*2);
        ctx.fill();
     }
     // the background canvas
     const bgCan = copyCanvas(canvas);
     circle(bgCan.ctx);  // will draw to the background canvas

Updating animation

When animating is is easiest to clear the whole canvas rather than mess about clearing only rendered pixels. Clearing rendered pixels gets complicated very quickly and will end up being many times slower than clearing the whole canvas.

After you clear the canvas draw the background canvas to the main canvas

    ctx.clearRect(0, 0, ctx.canvas.width,  ctx.canvas.height);
    ctx.drawImage(bgCan, 0, 0);

When the mouse button is down draw the circle to the background canvas and while it is up draw to the main canvas.

Example

  • Adds a function to copy a canvas. copyCanvas
  • Clears the main canvas, and draws the background canvas onto the main canvas.
  • Render functions circle and direction have argument ctx to direct rendering to any context.
  • When mouse is down circle is drawn to background canvas bgCan else to the main canvas.

requestAnimationFrame(animate);
const ctx = canvas1.getContext('2d');
canvas1.width = innerWidth;
canvas1.height = innerHeight;
const bgCan = copyCanvas(canvas1);
const redSize = 10, blueSize = 5; // circle sizes on pixels
const drawSpeed = 2; // when button down draw speed in pixels per frame
var X = 50, Y = 50;
var angle = 0;
var mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function copyCanvas(canvas) {
    const can = Object.assign(document.createElement("canvas"), {
        width: canvas.width, height: canvas.height
    });
    can.ctx = can.getContext("2d");
    return can;
}
function circle(ctx){
    ctx.fillStyle = 'red';
    ctx.beginPath();
    ctx.arc(X, Y, redSize, 0, Math.PI*2);
    ctx.fill();
}
function direction(ctx){
    const d = blueSize + redSize + 5;
    ctx.fillStyle = 'blue';
    ctx.beginPath();
    ctx.arc(d * Math.sin(angle) + X, d * Math.cos(angle) + Y, blueSize, 0, Math.PI*2);
    ctx.fill(); 
}
function animate(){
    ctx.clearRect(0, 0, ctx.canvas.width,  ctx.canvas.height);
    ctx.drawImage(bgCan, 0, 0);
    if (mouseButtonDown) {
        circle(bgCan.ctx);
        X += Math.sin(angle) * drawSpeed;
        Y += Math.cos(angle) * drawSpeed;
    } else {
        angle += 0.1;
        circle(ctx);
    }
    direction(ctx);
    requestAnimationFrame(animate);   
}
#canvas1{
    position: absolute;
    top:0;
    left: 0;
    width: 100%;
    height: 100%;
}
<canvas id="canvas1"></canvas>

  • BTW ctx.closePath() is like ctx.lineTo it is not the opposite to ctx.beginPath. A full arc or if you are just filling a shape you don't need to use ctx.closePath

  • BTW window is the default this, you don't need to include it, you dont use it to get at window.documentso why use it forwindow.innerWidth(same asinnerWidth` )

You could alter your code to keep track of the path of the red circle, with an array property, like this:

const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let mouseButtonDown = false;

document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function drawCircle({x, y, radius, color}) {  
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI*2);
    ctx.fill();
}
const red = { x: 50, y: 50, radius: 20, color: "red", path: [] };
const blue = { x: 100, y: 100, radius: 10, color: "blue", angle: 0 };
function animate(){
    if (mouseButtonDown) {
        red.path.push({x: red.x, y: red.y}); // store the old value
        red.x += (blue.x - red.x) / 10;
        red.y += (blue.y - red.y) / 10;
    } else {
        blue.angle += 0.1;
    }
    blue.x = red.x + 35 * Math.sin(blue.angle);
    blue.y = red.y + 35 * Math.cos(blue.angle);
    ctx.clearRect(0, 0, canvas.width, canvas.height);  // clear the whole canvas
    for (const {x, y} of red.path) {  // draw circle at all the previous positions
        drawCircle({...red, x, y});
    }
    drawCircle(red);
    drawCircle(blue);
    requestAnimationFrame(animate);   
}
animate();

Using 2 canvases also works and may perform better especially when the path of the red circle has gotten long, because the background canvas doesn't need to be cleared and redrawn. Add a 2nd canvas in your html page with the same positioning, and give them ids 'background' and 'foreground'. You can then adjust the code to draw the blue circle to the foreground and red circles to the background (or vice versa).

// Create 2 canvases, set them to full size and get the contexts
const backgroundCanvas = document.getElementById('background');
const foregroundCanvas = document.getElementById('foreground');
const background = backgroundCanvas.getContext("2d");
const foreground = foregroundCanvas.getContext("2d");
backgroundCanvas.width = innerWidth;
backgroundCanvas.height = innerHeight;
foregroundCanvas.width = innerWidth;
foregroundCanvas.height = innerHeight;

let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);

// Create objects to represent the current properties of the red and blue circle
const red = { x: 50, y: 50, radius: 20, color: "red" };
const blue = { x: 100, y: 100, radius: 10, color: "blue", angle: 0};

function drawCircle(ctx, {x, y, radius, color}) { 
    //--- Draw a circle to the specified canvas context, ctx = foreground or background
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI*2);
    ctx.closePath();
    ctx.fill();
}

function animate(){
    if (mouseButtonDown) {
        red.x += (blue.x - red.x) / 10;
        red.y += (blue.y - red.y) / 10;
    } else {
        blue.angle += 0.1;
    }
    blue.x = red.x + 35 * Math.sin(blue.angle);
    blue.y = red.y + 35 * Math.cos(blue.angle);

    drawCircle(background, red);  // Draw the red circle in the background (without clearing the existing circles)
    foreground.clearRect(0, 0, foregroundCanvas.width, foregroundCanvas.height);    // Clear the foreground
    drawCircle(foreground, blue); // Draw the blue circle on the foreground
    requestAnimationFrame(animate);  
}
animate();

Either way, it's convenient to abstract out the circle drawing code into a function or method, and to store the properties of the two circles in objects.

As @Blindman67's answer notes, there may be a performance cost of stacking 2 canvases, and if that is an issue you may want to try drawing the background offscreen then copying it to the on-screen canvas.

If you're not opposed to just building a particle class you can do it using them. In the snippet below I have a Circle class and a Particles class to creat what you are trying to achieve. I currently have the particles max at 500 but you can change it or delete that line all together if you ne er want them gone.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let mouseButtonDown = false;
//the array holding particles
let particles = [];
//the counter is only needed it you want to slow down how fast particles are being pushed and dispolayed
let counter = 0;

document.addEventListener("mousedown", () => (mouseButtonDown = true));
document.addEventListener("mouseup", () => (mouseButtonDown = false));

//ES6 constructor class
class Circle {
  //sets the basic structor of the object
  constructor(r, c) {
    this.x = 100; 
    this.y = 100;
    this.x2 = 50;
    this.y2 = 50;
    this.r = r; //will be assigned the argument passed in through the constructor by each instance created later
    this.color = c; //same as above. This allows each instance to have different parameters.
    this.angle = 0;
  }
  //this function creates the red circle
  drawRed() {
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.closePath();
  }
  //this function creates the blue circle
  drawBlue() {
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x + this.x2, this.y + this.y2, this.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.closePath();
  }
  //this function is where we'll place parameter that change our object
  update() {
    //makes the blue circle rotate
    this.x2 = 35 * Math.sin(this.angle);
    this.y2 = 35 * Math.cos(this.angle);
    //mouse action is same as your code
    if (mouseButtonDown) {
      this.x += this.x2 / 20;
      this.y += this.y2 / 20;
    } else {
      this.angle += 0.1;
    }
  }
}
//When using this type of constructor class you have to create an instance of it by calling new Object. You can create as money as you want.
let blueCircle = new Circle(10, "blue"); //passing in the radius and color in to the constructor
let redCircle = new Circle(20, "red"); 

//another class for the particles
class Particles {
  constructor() {
    this.x = redCircle.x;
    this.y = redCircle.y;
    this.r = redCircle.r;
    this.color = redCircle.color;
  }
  draw() {
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.closePath();
  }
}
//just wrapping all of the particle stuff into one function
function handleParticles() {
  //while the mouse is held it will push particles
  if (mouseButtonDown) {
    particles.push(new Particles());
  }
  //this loops through the array and calls the draw() function for each particle
  for (let i = 0; i < particles.length; i++) {
    particles[i].draw();
  }
  //this keeps the array from getting too big.
  if (particles.length > 500) {
    particles.shift();
  }
}

//wrap all functions into this one animate one and call requeatAnimationFrame
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  handleParticles();
  //These must be called for each instance created of the object
  blueCircle.drawBlue(); 
  blueCircle.update(); 
  redCircle.drawRed(); 
  redCircle.update(); 
  requestAnimationFrame(animate);
}
animate();
#canvas1{
    position: absolute;
    top:0;
    left: 0;
    width: 100%;
    height: 100%;
}
<canvas id="canvas"></canvas>

I'd also like to add you can change the rate that the particles are drawn by adding a counter variable and then limiting the draw like counter % 10 == 0

EXAMPLE

add global variable let counter = 0;

then in the handleParticles function add this

function handleParticles() {
  counter++
  if (mouseButtonDown && counter % 10 == 0) {
    particles.push(new Particles());
  }
  for (let i = 0; i < particles.length; i++) {
    particles[i].draw();
  }
  if (particles.length > 500) {
    particles.shift();
  }
}
Related