【问题标题】:How to use clearRect to not draw an moving object on canvas如何使用 clearRect 不在画布上绘制移动对象
【发布时间】:2021-08-11 04:42:53
【问题描述】:

我有一个蓝色圆圈,它围绕红色圆圈旋转,只要按下按钮,就会在画布上沿一个方向连续移动。

现在我想在按下按钮时用红色圆圈绘制(它的路径轨迹)。

  • 问题:

我尝试对clearRect() 进行更改,但没有成功。蓝色圆圈在我不需要的移动时开始在画布上绘制。

如果不能使用clearRect() 函数,是否可以通过堆叠画布层来做到这一点。请帮忙举个例子

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>

【问题讨论】:

    标签: javascript html css canvas html5-canvas


    【解决方案1】:

    不要在页面上堆叠画布

    您添加到页面的每个画布都会增加 GPU 和页面合成器渲染页面所需的工作量。

    使用不在页面上的第二个画布并通过使用ctx.drawImage(secondCanvas, 0, 0) 将画布渲染到页面上的画布来进行合成。

    这减少了合成器的工作量,并且在许多情况下避免了为第二个画布 I.E 执行附加图像渲染(合成)的需要。如果您仅使用一个 onpage 画布,onpage 可能需要 3 个 drawImage(每个画布一个,一个用于结果)而不是 2 个(一个在您的代码中,一个作为结果)。

    使用第二个画布

    创建第二个画布来存储绘制的红线。

    您可以使用创建画布的副本

        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;
        }
    

    当您创建像 circledirection 这样的渲染函数时,将 2D 上下文(例如 circle(ctx))作为参数传递,以便将渲染定向到任何画布。

         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
    
    

    更新动画

    当动画最容易清除整个画布而不是只清除渲染像素时。清除渲染像素会很快变得复杂,最终会比清除整个画布慢很多倍。

    清除画布后,将背景画布绘制到主画布

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

    当鼠标按钮按下时,将圆圈绘制到背景画布上,当它向上时,画到主画布上。

    示例

    • 添加了复制画布的功能。 copyCanvas
    • 清除主画布,并将背景画布绘制到主画布上。
    • 渲染函数 circledirection 具有参数 ctx 以将渲染定向到任何上下文。
    • 当鼠标按下时,圆圈被绘制到背景画布bgCan 否则到主画布。

    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%;
    }
    &lt;canvas id="canvas1"&gt;&lt;/canvas&gt;
    • 顺便说一句 ctx.closePath() 就像 ctx.lineTo 它与 ctx.beginPath 不是相反的。一个完整的弧线或者如果你只是填充一个你不需要使用的形状ctx.closePath

    • 顺便说一句window是默认的this,你不需要包含它,你不用t use it to get at window.documentso why use it forwindow.innerWidth(same asinnerWidth`)

    【讨论】:

    • 谢谢,你能在画布上添加一个按钮来切换drawnot draw。并且请添加 cmets 我发现更新后代码中的画布部分的复制很难理解。
    【解决方案2】:

    您可以使用数组属性更改代码以跟踪红色圆圈的路径,如下所示:

    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();
    

    使用 2 个画布也可以工作,并且可能会表现得更好,尤其是当红色圆圈的路径变长时,因为不需要清除和重绘背景画布。在您的 html 页面中添加具有相同位置的第二个画布,并为它们提供 id 'background' 和 'foreground'。然后,您可以调整代码以将蓝色圆圈绘制到前景,将红色圆圈绘制到背景(反之亦然)。

    // 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();
    

    无论哪种方式,都可以方便地将画圆代码抽象成一个函数或方法,并将两个圆的属性存储在对象中。

    正如@Blindman67 的回答所述,堆叠 2 个画布可能会降低性能,如果这是一个问题,您可能想尝试在屏幕外绘制背景,然后将其复制到屏幕上的画布。

    【讨论】:

    • 谢谢,当我运行 2 画布代码时,它没有显示任何错误,但旋转的蓝色圆圈不存在。
    • 您的 html 中是否有 2 个画布并且在 css 中应用了相同的样式?
    • 可惜我没有更新 CSS。它工作得很好谢谢。
    • 您能否为 2 个画布添加更详细的 cmets,我正在尝试使用按钮切换绘图,但我发现代码很难理解。
    • 添加了一些 cmets 并稍微简化了代码以便于理解
    【解决方案3】:

    如果您不反对仅仅构建一个粒子类,您可以使用它们来实现。在下面的 sn-p 中,我有一个 Circle 类和一个 Particles 类来创建您想要实现的目标。我目前的粒子最大值为 500,但如果你不想让它们消失,你可以更改它或删除那条线。

    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%;
    }
    &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

    我还想补充一点,您可以通过添加一个计数器变量来更改粒子的绘制速率,然后像counter % 10 == 0那样限制绘制

    示例

    添加全局变量let counter = 0;

    然后在handleParticles函数中添加这个

    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();
      }
    }
    

    【讨论】:

    • 谢谢,我只是想知道如果我删除if (particles.length &gt; 500) { particles.shift(); } 并运行代码一段时间会不会有任何滞后,因为你在particle 列表中附加路径的历史可能会爆炸由于数量大?
    • 你能在你的代码中添加 cmets,我很难理解发生了什么
    • 我假设如果你开始进入数千个它最终会滞后。我会尽快更新 cmets。
    猜你喜欢
    • 2021-08-10
    • 2021-08-11
    • 1970-01-01
    • 2015-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多