outlandish
outlandish

Reputation: 92

rendering huge number of elements in html5 canvas

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!

Upvotes: 2

Views: 1575

Answers (1)

sarkiroka
sarkiroka

Reputation: 1542

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

Upvotes: 3

Related Questions