Search code examples
javascripthtmlcanvas

Increase performance for 10,000 particles in HTML5 Canvas


I have two JS Fiddles, both with 10,000 snow flakes moving around but with two different approaches.

The first fiddle: http://jsfiddle.net/6eypdhjp/

Uses fillRect with a 4 by 4 white square, providing roughly 60 frames per second @ 10,000 snow flakes.

So I wondered if I could improve this and found a bit of information on HTML5Rocks' website regarding canvas performance. One such suggestion was to pre-render the snow flakes to canvases and then draw the canvases using drawImage.

The suggestion is here http://www.html5rocks.com/en/tutorials/canvas/performance/, namely under the title Pre-render to an off-screen canvas. Use Ctrl + f to find that section.

So I tried their suggestion with this fiddle: http://jsfiddle.net/r973sr7c/

How ever, I get about 3 frames per second @ 10,000 snow flakes. Which is very odd given jsPerf even shows a performance boost here using the same method http://jsperf.com/render-vs-prerender

The code I used for pre-rendering is here:

//snowflake particles
var mp = 10000; //max particles
var particles = [];
for(var i = 0; i < mp; i++) {
    var m_canvas        = document.createElement('canvas');
        m_canvas.width  = 4;
        m_canvas.height = 4;
    var tmp             = m_canvas.getContext("2d");
        tmp.fillStyle   = "rgba(255,255,255,0.8)";
        tmp.fillRect(0,0,4,4);
    
    particles.push({
        x  : Math.random()*canvas.width, //x-coordinate
        y  : Math.random()*canvas.height, //y-coordinate
        r  : Math.random()*4+1, //radius
        d  : Math.random()*mp, //density
        img: m_canvas //tiny canvas
    })
}   
//Lets draw the flakes
function draw()    {   
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for(var i = 0; i < particles.length; i++)       {
        var flake = particles[i];
        ctx.drawImage(flake.img, flake.x,flake.y);    
    }
}

So I wondered why am I getting such horrendous frame rate? And is there any better way to get higher particle counts moving on screen whilst maintaining 60 frames per second?


Solution

  • Best frame rates are achieved by drawing pre-rendered images (or pre-rendered canvases).

    You could refactor your code to:

    • Create about 2-3 offscreen (in-memory) canvases each with 1/3 of your particles drawn on them
    • Assign each canvas a fallrate and a driftrate.
    • In each animation frame, draw each offscreen canvas (with an offset according to its own fallrate & driftrate) onto the on-screen canvas.

    The result should be about 60 frames-per-second.

    This technique trades increased memory usage to achieve maximum frame rates.

    Here's example code and a Demo:

            var canvas=document.getElementById("canvas");
            var ctx=canvas.getContext("2d");
            var cw=canvas.width;
            var ch=canvas.height;
    
    var mp=10000;
    var particles=[];
    var panels=[];
    var panelCount=2;
    var pp=panelCount-.01;
    var maxFallrate=2;
    var minOffsetX=-parseInt(cw*.25);
    var maxOffsetX=0;
    
    // create all particles
    for(var i=0;i<mp;i++){
    		particles.push({
    			x: Math.random()*cw*1.5,  //x-coordinate
    			y: Math.random()*ch, //y-coordinate
    			r: 1, //radius
    			panel: parseInt(Math.random()*pp) // panel==0 thru panelCount
    		})
    }
    
    // create a canvas for each panel
    var drift=.25;
    for(var p=0;p<panelCount;p++){
        var c=document.createElement('canvas');
        c.width=cw*1.5;
        c.height=ch*2;
        var offX=(drift<0)?minOffsetX:maxOffsetX;
        panels.push({
            canvas:c,
            ctx:c.getContext('2d'),
            offsetX:offX,
            offsetY:-ch,
            fallrate:2+Math.random()*(maxFallrate-1),
            driftrate:drift
        });
        // change to opposite drift direction for next panel
        drift=-drift;
    }
    
    // pre-render all particles
    // on the specified panel canvases
    for(var i=0;i<particles.length;i++){
        var p=particles[i];
        var cctx=panels[p.panel].ctx;
        cctx.fillStyle='white';
        cctx.fillRect(p.x,p.y,1,1);
    }
    
    // duplicate the top half of each canvas
    // onto the bottom half of the same canvas
    for(var p=0;p<panelCount;p++){
        panels[p].ctx.drawImage(panels[p].canvas,0,ch);
    }
    
    // begin animating
    drawStartTime=performance.now();
    requestAnimationFrame(animate);
    
    
    function draw(time){
        ctx.clearRect(0,0,cw,ch);
        for(var i=0;i<panels.length;i++){
            var panel=panels[i];
            ctx.drawImage(panel.canvas,panel.offsetX,panel.offsetY);
        }
    }
    
    function animate(time){
        for(var i=0;i<panels.length;i++){
    
            var p=panels[i];
    
            p.offsetX+=p.driftrate;
            if(p.offsetX<minOffsetX || p.offsetX>maxOffsetX){
                p.driftrate*=-1;
                p.offsetX+=p.driftrate;
            }
    
            p.offsetY+=p.fallrate;
            if(p.offsetY>=0){p.offsetY=-ch;}
    
            draw(time);
    
        }
        requestAnimationFrame(animate);
    }
    body{ background-color:#6b92b9; padding:10px; }
    #canvas{border:1px solid red;}
    <canvas id="canvas" width=300 height=300></canvas>