Search code examples
javascriptanimationhtml5-canvasrendering

How to fix performance lag in canvas html5?


I am building a project where a user can type word in the input text and with the input value, canvas draws the particles onto the text. When the mouse hovers over the particles pushed back and comes back(core animation)

However, the performance is just terrible, it's way too slow and I have been looking up online and found stuff like frame rate, display ratio, getImageData, putImageData, new Uint32Array(), bitwise operator etc but after hours of trying different things I noticed I wasn't making any progress rather got stuck deeper

My codes are below, and if anyone can tell me where I should go about fixing it would be great.

in index.html

<canvas> </canvas>

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

in app.js - I didn't include any code on form submit since it works fine

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

Solution

  • First you can consider reducing how much total work is going on for a full screen's number of pixels, for instance:

    • reduce the canvas size (you can consider using CSS transform: scale to scale it back up if you must),
    • reduce the number of particles,
    • use a less expensive/less accurate distance operation like just checking horizontal distance and vertical distance between two objects,
    • use integer values instead of floats (drawing to canvas with floats is more expensive)
    • consider using fillRect instead of drawing arcs. (At such a small size it won't make much of a difference visually, but these are generally less expensive to draw- you may want to test if it makes much of a difference),
    • even consider reducing how often you redraw the canvas (adding a setTimeout to wrap your requestAnimationFrame and increasing the delay between frames (requestAnimationFrame is generally about 17ms))

    And some more small optimizations in the code:

    • store particles.length in a variable after they are created so that you don't calculation particles.length on every for loop iteration in your animate function. But millions of calculations minus this 2048 isn't going to make much of a difference.

    • only set the context fillStyle once. You never change this color, so why set it on every draw?

    • remove the closePath() lines. They do nothing here.

    • draw the particles onto an offscreen "buffer" canvas, and draw that canvas to the onscreen one only after all the particles have been drawn to it. This can be done with a normal <canvas> object, but depending on what browser you are working with, you can also look into OffscreenCanvas. Basic example would look something like this:

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