如何修复 canvas html5 中的性能滞后问题?

How to fix performance lag in canvas html5?

我正在构建一个项目,用户可以在其中输入文字并使用输入值,canvas 将粒子绘制到文字上。当鼠标悬停在被推回去的粒子上(核心动画)

然而,性能太糟糕了,太慢了,我一直在网上查找并找到帧速率、显示比率、getImageData、putImageData、new Uint32Array()、按位运算符等内容,但经过数小时尝试不同的事情我发现我没有取得任何进展,而是陷得更深了

我的代码在下面,如果有人能告诉我应该在哪里修复它就太好了。

在 index.html

<canvas> </canvas>

<form>  
  <input class="text" type="text" value="touch me!" placeholder="type your message.."/>
  <div class="input-bottom"></div>
</form> 

在 app.js - 我没有在表单提交中包含任何代码,因为它工作正常

let canvas = document.querySelector(".canvas")
let canvasContext2d = canvas.getContext("2d") 
let canvasWidth = canvas.width = window.innerWidth
let canvasHeight = canvas.height = window.innerHeight

let form = document.querySelector('form')
let text = form.querySelector(".text")
let textMessage = text.value 

let mouse = {x: undefined, y: undefined}

function Particle(x, y, r, accX, accY){
    this.x = randomIntFromRange(r, canvasWidth-r)
    this.y = randomIntFromRange(r, canvasHeight-r)
    this.r = r
    this.color = "black"
    this.velocity = {
      x: randomIntFromRange(-10, 10), 
      y: randomIntFromRange(-10, 10)
     }
    this.dest = {x : x, y : y}
    this.accX = 5;
    this.accY = 5;
    this.accX = accX;
    this.accY = accY;
    this.friction = randomNumDecimal(0.94, 0.98)


    this.draw = function(){    
     canvasContext2d.beginPath()
     canvasContext2d.arc(this.x, this.y, this.r, 0, Math.PI * 2)
     canvasContext2d.fillStyle = "rgb(250, 250, 247)"
     canvasContext2d.fill()
     canvasContext2d.closePath() 

     // mouse ball
     canvasContext2d.beginPath()
     canvasContext2d.arc(mouse.x, mouse.y, 50, 0, Math.PI * 2)
     canvasContext2d.fill()
     canvasContext2d.closePath()
   }

   this.update = function(){
     this.draw()

     if(this.x + this.r > canvasWidth || this.x - this.r < 0){
          this.velocity.x = -this.velocity.x
      }

    if(this.y + this.r > canvasHeight || this.y - this.r < 0){
         this.velocity.y = -this.velocity.y
      }

    this.accX = (this.dest.x - this.x) / 300;
    this.accY = (this.dest.y - this.y) / 300;

    this.velocity.x += this.accX;
    this.velocity.y += this.accY;

    this.velocity.x *= this.friction;
    this.velocity.y *= this.friction;

    this.x += this.velocity.x;
    this.y += this.velocity.y;

   if(dist(this.x, this.y, mouse.x, mouse.y) < 70){
     this.accX = (this.x - mouse.x) / 30;
     this.accY = (this.y - mouse.y) / 30;
     this.velocity.x += this.accX;
     this.velocity.y += this.accY;
    }
  }
}

let particles;
function init(){
  particles = []

  canvasContext2d.font = `bold ${canvasWidth/10}px sans-serif`;
  canvasContext2d.textAlign = "center"
  canvasContext2d.fillText(textMessage, canvasWidth/2, canvasHeight/2)

  let imgData = canvasContext2d.getImageData(0, 0, canvasWidth, canvasHeight)
  let data = imgData.data

  for(let i = 0; i < canvasWidth; i += 4){
    for(let j = 0; j < canvasHeight; j += 4){
      if(data[((canvasWidth * j + i) * 4) + 3]){
        let x = i + randomNumDecimal(0, 3)
        let y = j + randomNumDecimal(0, 3)
        let r = randomNumDecimal(1, 1.5)
        let accX = randomNumDecimal(-3, 0.2)
        let accY = randomNumDecimal(-3, 0.2)

        particles.push(new Particle(x, y, r, accX, accY))
      } 
    }
  }
} 

function animate(){
  canvasContext2d.clearRect(0, 0, canvasWidth, canvasHeight)
  for(let i = 0; i < particles.length; i++){
    particles[i].update()
  } 
  requestAnimationFrame(animate)
}

init()
animate()

首先,您可以考虑减少全屏像素数的总工作量,例如:

  • 减小 canvas 大小(如有必要,您可以考虑使用 CSS transform: scale 将其放大),
  • 减少粒子数量,
  • 使用较少的 expensive/less 精确距离操作,例如检查两个对象之间的水平距离和垂直距离,
  • 使用整数值而不是浮点数(使用浮点数绘制到 canvas 更昂贵)
  • 考虑使用 fillRect 而不是绘制圆弧。 (在这么小的尺寸下,它在视觉上不会产生太大的差异,但通常绘制它们的成本较低 - 你可能想测试它是否会产生很大的差异),
  • 甚至考虑减少重绘 canvas 的频率(添加 setTimeout 来包裹 requestAnimationFrame 并增加帧之间的延迟(requestAnimationFrame 通常约为 17 毫秒))

以及代码中的一些小优化:

  • 在创建变量后将 particles.length 存储在变量中,这样您就不会在 animate 中的每个 for 循环迭代中计算 particles.length ] 功能。但是数百万次计算减去这个 2048 不会有太大的不同。

  • 只设置上下文fillStyle一次。您永远不会更改此颜色,所以为什么要在每次抽奖时都设置它?

  • 删除 closePath() 行。他们在这里什么都不做。

  • 将粒子绘制到屏幕外的“缓冲区”canvas,只有在所有粒子都被绘制到它之后才将 canvas 绘制到屏幕上的缓冲区。这可以使用普通的 <canvas> 对象来完成,但根据您使用的浏览器,您也可以查看 OffscreenCanvas。基本示例如下所示:

var numParticles;

// Inside init(), after creating all the Particle instances
numParticles = particles.length;


function animate(){
  // Note: if offscreen canvas has background color drawn, this line is unnecessary
  canvasContext2d.clearRect(0, 0, canvasWidth, canvasHeight)
  
  for(let i = 0; i < numParticles; i++){
    particles[i].update() // remove .draw() from .update() method
    particles[i].drawToBuffer(); // some new method drawing to buffer canvas
  }

  drawBufferToScreen(); // some new method drawing image from buffer to onscreen canvas
  requestAnimationFrame(animate)
}