rendering huge number of elements in html5 canvas

Viewed 460

Assume you have a 500x500 2D canvas and you want to animate 100000 of elements in it, for example you want to create noise effects. consider code bellow :

    const canvas = document.getElementById("plane");
    let animatelist = [];
    animate = function() {
        animatelist.forEach((e) => {
            e.render();
        });
        setTimeout(animate, 1000 / 30);
    } 
    animate();
    let point  = function(plane, x, y, size) {
        animatelist.push(this);
        this.plane = plane;
        this.x = x;
        this.y = y;
        this.size = size;
        this.render = () => {
            const context = this.plane.getContext("2d");
            this.x = Math.random() * 500;
            this.y = Math.random() * 500;
            context.fillStyle = "#000";
            context.fillRect(this.x, this.y, this.size, this.size);
        }
    }
    for (let i = 0;i < 100000;i++) {
        new point(canvas, Math.random() * 500, Math.random() * 500, 0.3);
    }

it barely gives you 2 or 3 fps and it is just unacceptable, i was wondering if there is a trick a about these kinda of animations or something to render massive amounts of elements smoothly!

1 Answers

You can play in memory and after that draw on an invisuble canvas. And when you are ready, copy all of bytes into visible canvas. And i see, you use a lot of random. This is slow instruction. Try to make a random table and implement your own random function

Here is an 12-15 fps version but I think you can reach better performance by pixel manipulating. So this code based on your solution, but I cannot increase fps because too many function calls, object manipulating and similar baklava. (a code below reach over 100 fps)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>sarkiroka</title>
    </head>
    <body>
        <canvas id="plane" width="500" height="500"></canvas>
        <script>
            // variable and function for speedup
            const randomTable = [];
            const randomTableLength = 1000007;
            const fpsMinimum = 1000 / 30;
            for (let i = 0; i < randomTableLength; i++) {
                randomTable.push(Math.random() * 500);
            }
            let randomSeed = 0;

            function getNextRandom() {
                if (++randomSeed >= randomTableLength) {
                    randomSeed = 0;
                }
                return randomTable[randomSeed];
            }

            // html, dom speedup
            const canvas = document.getElementById("plane");
            const context = canvas.getContext("2d");
            const drawCanvas = document.createElement('canvas');
            drawCanvas.setAttribute('width', canvas.getAttribute('width'));
            drawCanvas.setAttribute('height', canvas.getAttribute('height'));
            const drawContext = drawCanvas.getContext('2d');
            drawContext.fillStyle = "#000";

            let animatelist = [];
            let point = function (x, y, size) {
                animatelist.push(this);
                this.x = x;
                this.y = y;
                this.size = size;
                this.render = () => {
                    this.x = getNextRandom();
                    this.y = getNextRandom();
                    drawContext.fillRect(this.x, this.y, this.size, this.size);
                }
            }
            for (let i = 0; i < 100000; i++) {
                new point(getNextRandom(), getNextRandom(), 0.3);
            }

            //the animation
            let lastBreath = Date.now();
            const animateListLength = animatelist.length;
            let framesDrawed = 0;
            let copied = false;

            const maximumCallstackSize = 100;

            function continouslyAnimation(deep) {
                if (copied) {
                    drawContext.clearRect(0, 0, 500, 500);
                    for (let i = 0; i < animateListLength; i++) {
                        animatelist[i].render();
                    }
                    copied = false;
                }
                framesDrawed++;
                let now = Date.now();
                if (lastBreath + 15 > now && deep < maximumCallstackSize) {
                    continouslyAnimation(deep + 1);
                } else { // to no hangs browser
                    lastBreath = now;
                    setTimeout(continouslyAnimation, 1, 1);
                }
            }

            setInterval(() => {
                console.log(framesDrawed);
                framesDrawed = 0;
            }, 1000);

            continouslyAnimation(0);

            function copyDrawToVisible() {
                context.putImageData(drawContext.getImageData(0, 0, 499, 499), 0, 0);
                copied = true;
            }

            setInterval(copyDrawToVisible, fpsMinimum);
        </script>
    </body>
</html>

And here is a pixel manipulation solution, with much better performance (over 100 fps, 220-245 fps in my computer):

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <title>sarkiroka</title>
 </head>
 <body>
  <canvas id="plane" width="500" height="500"></canvas>
  <script>
   // variable and function for speedup
   const randomTable = [];
   const randomTableLength = 1000007;
   for (let i = 0; i < randomTableLength; i++) {
    randomTable.push(Math.random());
   }
   let randomSeed = 0;

   function getNextRandom() {
    if (++randomSeed >= randomTableLength) {
     randomSeed = Math.round(Math.random() * 1000);
    }
    return randomTable[randomSeed];
   }

   // html, dom speedup
   const canvas = document.getElementById("plane");
   const context = canvas.getContext("2d");

   let framesDrawed = 0;

   function drawNoise() {
    context.clearRect(0, 0, 500, 500);
    let imageData = context.createImageData(499, 499);
    let data = imageData.data;
    for (let i = 0, length = data.length; i < length; i += 4) {
     if (0.1 > getNextRandom()) {
      data[i] = 0;
      data[i + 1] = 0;
      data[i + 2] = 0;
      data[i + 3] = 255;
     }
    }
    context.putImageData(imageData, 0, 0);
    framesDrawed++;
   }

   setInterval(drawNoise, 0);
   setInterval(() => {
    console.log('fps', framesDrawed);
    framesDrawed = 0;
   }, 1000)
  </script>
 </body>
</html>

Explanataion: for noise, you don't need a function / object for every colored pixel. Trust to statistics and the random. In my example 10% of pixels are colored but we don't know how many pixel before render. But this is not important. From afar it is just like that perfect. And the most important thing: it can reach more fps.

General advice:

  • Which code is repeated many times, organize out of it whatever you can
  • Draw only on the canvas when you are done drawing in memory
  • Measure what is slow and optimize it, and only it
Related