preface

Last Christmas product raised an activity demand, which had a background animation of snow. In the process of doing this animation, I have deepened my understanding of Canvas animation. Here I just share with you. Welcome your criticism.

The code has been uploaded to Github. Interested parties can clone the code to run locally. Please give a star support.

Into the topic

The UI style given in the requirements is as follows:

The UI needs snowflakes to fall at a slightly sloping Angle, each snowflake falling at a different speed but within a range.

The requirements are pretty much what you need to start implementing this effect (you need to know some basic canvas apis before reading this article).

drawImage

DrawImage takes nine parameters, five of which are commonly used and the others are used to cut images.

Directly using drawImage to cut images will not have good performance. It is suggested to save the part needed to be used in an off-screen canvas first, and use it directly when needed.

requestAnimationFrame

RequestAnimationFrame has several advantages over SetInterval animation handling:

  1. Optimized by browser for smoother animation
  2. The animation stops when the window is not active, saving computing resources
  3. It saves power, especially on mobile devices

The API does not need to pass in an animation interval, which tells the browser the best way to redraw the animation.

Due to compatibility issues, requestAnimationFrame can be rewritten using the following methods:

window.requestAnimationFrame = (function() {return  window.requestAnimationFrame       || 
                window.webkitRequestAnimationFrame || 
                window.mozRequestAnimationFrame    || 
                window.oRequestAnimationFrame      || 
                window.msRequestAnimationFrame     || 
                function(callback) { window.setTimeout(callback, 1000 / 60); }; }) ();Copy the code

Please consult the documentation for other apis.

First try

Once I have a general idea, I’m happy to start writing code. The basic idea is to use requestAnimationFrame to refresh the Canvas canvas.

Since snowflakes are irregular, snowflakes are images provided by THE UI. Since they are images, we need to preload the images well, otherwise the performance may be affected when converting images.

The preloading method used is as follows:

function preloadImg(srcArr){
    if(srcArr instanceof Array){
        for(let i = 0; i < srcArr.length; i++){
            letoImg = new Image(); oImg.src = srcArr[i]; }}}Copy the code

Front and back to write an afternoon, it is written, on the phone to view the effect found is very caton. 100 snowflakes for an FPS in the 40s. And there will be jitter in some models.

If the product sees this effect, I am afraid it is necessary to call relevant personnel to hold relevant meetings. So Caton must have written some expensive code that required a second try.

You still need to leave on time at night. However, after coming home from work, we should not be idle and start looking for relevant information so that we can finish it quickly the next day.

Preparation for the second attempt

After a night of searching and learning, I know the following methods to optimize canvas performance:

1. Use multiple layers of canvas to draw complex scenes

The goal of layering is to reduce completely unnecessary rendering performance overhead.

That is, divide the part that changes with high frequency and large amplitude and the part that changes with low frequency and small amplitude into two or more canvas objects. That is to say, generate multiple Canvas instances and place them on top of each other. Each Canvas uses a different Z-index to define the stacking order.

<canvas style="position: absolute; z-index: 0"></canvas>
<canvas style="position: absolute; z-index: 1"></canvas> // js codeCopy the code

2. Use requestAnimationFrame to animate

It’s mentioned above.

3. Use clearRect to clear the canvas

General performance: clearRect > fillRect > canvas.width = canvas.width;

4. Pre-render using off-screen rendering

Draw the same area with drawImage:

  1. If the data source (image, canvas) andcanvasThe size of the drawing board is similar, so the performance will be better;
  2. If the data source is just part of the big picture, performance will be poor; Because each drawing also involves clipping.

In the second case, we can first cut the region to be drawn and save it in an off-screen canvas object. As each frame is drawn, the object is drawn to the Canvas palette.

The first argument to the drawImage method can receive not only an Image object but also another Canvas object. Also, the overhead of drawing with a Canvas object is almost identical to the overhead of drawing with an Image object.

We can also use off-screen rendering to pre-render the object that needs to be called multiple times per frame to improve performance.

That is:

let cacheCanvas = document.createElement("canvas");
let cacheCtx = this.cacheCanvas.getContext("2d");

cacheCtx.save();
cacheCtx.lineWidth = 1;
for(leti = 1; i < 40; i++){ cacheCtx.beginPath(); cacheCtx.strokeStyle = this.color[i]; cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI); cacheCtx.stroke(); } this.cacheCtx.restore(); Context. DrawImage (cacheCtx, x, y);Copy the code

Set the width of cacheCtx to the actual width; otherwise, too much blank space reduces performance.

The following figure shows the performance improvement of pre-rendering using off-screen rendering:

5. Use as little as possiblecanvasAPIDraw as much as possible

The following code:

for (var i = 0; i < points.length - 1; i++) {
    var p1 = points[i];
    var p2 = points[i + 1];
    context.beginPath();
    context.moveTo(p1.x, p1.y);
    context.lineTo(p2.x, p2.y);
    context.stroke();
} 
Copy the code

Can be changed to:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
    var p1 = points[i];
    var p2 = points[i + 1];
    context.moveTo(p1.x, p1.y);
    context.lineTo(p2.x, p2.y);
}
context.stroke();
Copy the code

Tips: When writing particle effects, you can use squares instead of circles, because the particles are small, so squares and circles look similar. Some people say why? It is easy to understand that there are three steps to draw a circle: beginPath, then use arc to draw the arc, then fill. And the drawing side only needs a fillRect. When the number of particle objects reaches a certain level, the performance gap becomes apparent.

6. Pixel-level operations try to avoid floating point operations

When drawing canvas animation, CSS sub-pixel may occur if the coordinates are floating point numbers. This means that floating-point values are automatically rounded to integers, which can cause jitter during animation and anti-aliasing of elements’ edges.

Javascript provides some rounding methods like math.floor, math.ceil, and parseInt, but parseInt does some extra work (checking if the data is a valid value, converting arguments to strings first, etc.), so, Using parseInt directly is relatively performance-intensive. Rounding can be done directly in the following clever way:

function getInt(num){
    var rounded;
    rounded = (0.5 + num) | 0;
    return rounded;
}
Copy the code

In addition, the efficiency of the for loop is the highest, and those who are interested can experiment by themselves.

Second attempt

Through last night’s review, this animation has done the following optimization:

  1. Prerender using off-screen rendering
  2. Reduce the use of some apis
  3. A floating point number is rounded
  4. Cache variable
  5. Use a for loop instead of forEach
  6. The whole code was rewritten using a prototype chain

Once the scheme has been written, start writing code happily.

With 200 snowflakes, the FPS is basically stable at 60 and the jitter is no longer there. At 1000 FPS, it stayed at 60. [Fixed] Slightly sporadic card frames at 1500 tiles When it reached 2000, it began to stall.

This shows that this animation is still not optimized, there is room for optimization, please don’t hesitate to advise.

It is recommended to use the stats.js plug-in, which displays the FPS of the animation at run time.

The main code

let snowBox = function () {
    let canvasEl = document.getElementById("snowFall");
    let ctx = canvasEl.getContext('2d');
    canvasEl.width = window.innerWidth;
    canvasEl.height = window.innerHeight;
    letlineList = []; // Snow containerslet snow = function () {
        let _this = this;
        _this.cacheCanvas = document.createElement("canvas");
        _this.cacheCtx = _this.cacheCanvas.getContext("2d"); _this.cacheCanvas.width = 10; _this.cacheCanvas.height = 10; _this. Speed = [1, 1.5, 2], [math.h floor (Math) random () * 3)]; _this.posx = math.round (math.random () * canvasel.width); _this.posy = math.round (math.random () * canvasel.height); Img = './img/snow_(${Math.ceil(Math.random() * 9)}).png`;        // img
        _this.w = _this.getInt(5 + Math.random() * 6);
        _this.h = _this.getInt(5 + Math.random() * 6);
        _this.cacheSnow();
    };

    snow.prototype = {
        cacheSnow: function () {
            let _this = this;
            // _this.cacheCtx.save();
            letimg = new Image(); // Create an img element img. SRC = _this.img; _this.cacheCtx.drawImage(img, 0, 0, _this.w, _this.h); // _this.cacheCtx.restore(); }, fall:function () {
            let _this = this;
            if (_this.posy > canvasEl.height + 5) {
                _this.posy = _this.getInt(0 - _this.h);
                _this.posx = _this.getInt(canvasEl.width * Math.random());
            }
            if(_this.posx > canvasEl.width + 5) { _this.posx = _this.getInt(0 - _this.w); _this.posy = _this.getInt(canvasEl.height * Math.random()); } // If snowflakes are visibleif (_this.posy <= canvasEl.height || _this.posx <= canvasEl.width) {
                _this.posy = _this.posy + _this.speed;
                _this.posx = _this.posx + _this.speed * .5;
            }
            _this.paint();
        },
        paint: function () {
            ctx.drawImage(this.cacheCanvas, this.posx, this.posy)
        },
        getInt: function(num){
            let rounded;
            rounded = (0.5 + num) | 0;
            returnrounded; }};let control;
    control = {
        start: function (num) {
            for (let i = 0; i < num; i++) {
                let s = new snow();
                lineList.push(s);
            }
            (function loop() {
                ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
                for (leti = 0; i < num; i++) { lineList[i].fall(); } requestAnimationFrame(loop) })(); }};returncontrol; } (); window.onload =function(){
    snowBox.start(2000)
};
Copy the code

It is recommended to clone the code from Github and run it locally.

The latter

This article is about performance optimization of Canvas animation. Some big names have also seen that other aspects of performance optimization and this is roughly the same, nothing more than:

  1. Reduce API usage
  2. Use caching (important)
  3. Merge frequently used apis
  4. Avoid high-power apis
  5. Use webWorker to handle some time-consuming computations

Hope through reading this article, can give you a reference in terms of performance optimization, thank you for reading.

Front-end dictionary series

The Front End Dictionary series will continue to be updated, and in each issue I will cover a topic that comes up frequently. I would appreciate it if you could correct any inpreciseness or mistakes in the reading process. I will be delighted if I learn something from this series.

If you think my article is good, you can follow my wechat public account, which will reveal the plot in advance.

You can also add my wechat WQHHSD, welcome to exchange.

Next up

[Front-end dictionary] what caching steps are involved from entering the URL to rendering

portal

  1. Talk to your daughter-in-law about the unexpected harvest of acting
  2. Solution to the rolling penetration problem
  3. Inheritance (1) – Prototype chain Do you really understand?
  4. Inheritance (2) — Return (8
  5. Advanced Network Basics (PART 1)
  6. Advanced Network Basics (part 2)
  7. You can see the difference between F5 and Ctrl+F5