Writing in the front

After a long time, I finally decided to pick up a pen to record how I overcame my fear and fell in love with writing animation. This article is based on a good JS digging friends, if your JS master is not good enough, remember must first lay a solid foundation oh.

Animation really isn’t as complicated as you think, that’s what I’ve come to conclude from my own experience recently. There was a time when I thought animation required a very solid foundation in math and physics (good foundation is good, don’t be afraid to have no foundation), but that’s not the case. It needs to be based more on JS, object oriented thinking, is your programming skills. There are mathematical and physical basics, but the main thing that matters is how complex and sophisticated your animations are. So don’t be afraid, all things are difficult at the beginning, and once you find the pattern and the trick, you will love it.

This article uses a snowflake flying animation example, take you into the JS animation door. Click preview

What is the animation

In fact, animation is the effect produced by the continuous play of countless pictures, each picture is called a frame, animation is actually to draw each frame, and then play continuously. Our main concern is how to draw each frame and how to play it.

The instance

instructions

This example of the project moved to GitHub repository, the project under snow. Js is the core code, feel good animation digg friends can clone down to study, also can directly introduce snow. Js to use, through some configuration quickly achieve special effects oh. See the GitHub repository for details.

technology

Mainly using canvas, drawing through the window. The refresh requestAnimationFrame implementation. Through the browser window. The animation frames requestAnimationFrame method by request, and pass in a callback function, the browser will at the appropriate time to carry out the callback function (that is, our redraw function).

Train of thought

snow

The snowflakes flying all over the sky, how can we show them in the way of program? Using object-oriented programming, it is easy to analyze that every Snowflake is an object, and this object has size, shape, speed, etc. Naturally, we can abstract a Snowflake class to create Snowflake objects, describe their properties, and use them in animation. Snowflakes can never be exactly the same, so you need to use random numbers when creating them. Here we create two functions to get random elements for development purposes:

// Generate a random number const random =function (min, max, floor = false) {// Whether to round?if (floor) {
        return Math.floor(min + Math.random() * (max - min))
    } else {
        returnmin + Math.random() * (max - min) } }; Const randomIn = arr => arr[random(0, arr. Length, 0, arr.true)];
Copy the code

movement

Snowflake is actually an entity class. Although the created individual contains various attributes of the Snowflake, it cannot move by itself. We need a controller to operate the Snowflake and make it move, so we have a control class — Snow. Snow is mainly responsible for drawing each frame on the canvas and playing the frames continuously to create animation.

Code organization

Snowflake class

First release the overall code, specifically added a detailed comment:

class Snowflake {
    constructor(config, image = null) {
        this.config = config;
        this.image = image;
        this.load()
    }
    center () {
        let x = this.x + this.radius / 2;
        let y = this.y + this.radius / 2;
        return{x: x, y: y}}load() {// Initialize the drawing property this.x = random(0, window.innerWidth); This. y = random(-window.innerheight, 0); // This. Alpha = random(... this.config.alpha); // Transparency this.radius = random(... this.config.radius); // size this.color = randomIn(this.config.color); // this. Angle = 0; // This. Flip = 0; This. Va = math. PI/random(... this.config.va); This.va = math.random () >= 0.5? this.va : -this.va; // This. Vx = random(... this.config.vx); // this. Vy = random(... this.config.vy); // y axis moving speed // flip speed, default not to flip, here to make a decision!! Vf represents the flip speed, that is, the zoom speed!! This.config. vFlip && (this.vf = random(0, this.config.vflip))} update(range) {this.config.vFlip = this.vf = random(0, this.config.vflip))} update(range) { This. X += this. Vx; this.y += this.vy; this.angle += this.va; this.flip += this.vf; // Prevent infinite zoom and keep the zoom ratio between 0 and 1if(this. Flip > 1 | | this. Flip < 0) {this. Vf = - this. Vf} / / fly out of the range is reset when the element properties, reuse elementsif (this.y >= range + this.radius ) {
            this.load()
        }
    }
}
Copy the code
  1. Constructor: Pass in a snowflake configuration and an optional image. If there is no image, draw a circle by default.
  2. Load: Used to initialize and reset all properties.
  3. Center: Returns the current midpoint of the element.
  4. Update: This is called every time a frame is drawn, updating the property so that it is the equivalent of a motion.

Snow class

Again, let’s put out the code. The code is longer, mainly because of the added comments, so patience is sure to pay off:

class Snow {
    constructor(container, config = {}) {
        let{num} = config; / / number of snowflakes, generally do not need to change this. Num = num | | window. The innerWidth / 2; delete config['num']; This.config = object.assign ({image: [], // Optional image (network or local) vx: VFlip: 0, // Flip speed, recommended: slow 0.05/ normal 0.1/ fast 0.2 radius: [5, 15], // The radius of the incoming image needs to adjust the color: ['white'Alpha: [0.1, 0.9] // Transparency range}, config); This.init (container)} init(container) {// Initialize the basic configuration this.container = document.querySelector(container); // Get dom this.canvas = document.createElement('canvas'); // Get the actual width and height of the DOM element and fill the canvas with the DOM. This is necessary to add CSS code. Canvas. Width = this. The container. The offsetWidth; this.canvas.height = this.container.offsetHeight; This.ctx = this.canvas. GetContext ('2d'); / / insert document to show this. The container. The appendChild (enclosing canvas); this.snowflakes = new Set(); // It is used to store the created snowflakes. // Depending on the configuration passed in, you can draw a picture or draw a circle. // The default is to use white circles instead of snowflakesif(!!!!! This.config.image.length) {// Load the address of the passed image, This.loadimage (this.config.image). Then (images => {this.createsnowFlakes (images); RequestAnimationFrame (this.drawpicture ())}). Catch (e => console.error(e))}else{ this.createSnowflakes(); RequestAnimationFrame (this.drawCircle())}} loadImage (images) {// Define a function to load images, wrapped with Promiselet load = (src) => new Promise(resolve => {
            let image = new Image();
            image.src = src;
            image.onload = () => resolve(image);
            image.onerror = e => console.error('Image load failed:'+ e.path[0].src) }); // Use promise.all () to wait for all asynchronous operations to completereturn Promise.all(images.map(src => load(src)))
    }
    createSnowflakes (image) {
        if (image) {
            for (let i = 0; i < this.num; i++) {
                let img = randomIn(image);
                let flake = new Snowflake(this.config, img);
                this.snowflakes.add(flake)
            }
        } else {
            for (let i = 0; i < this.num; i++) {
                letflake = new Snowflake(this.config); This.snowflakes. Add (flake)}}} // this.snowflakes. Add (flake)}} // this.snowflakes. Add (flake)}} // this.snowflakes. Flake. Update (this.canvas. Height);let{x, y} = flake.center(); This.ctx.translate (x, y); this.ctx.translate(x, y); this.ctx.translate(x, y); this.ctx.translate(x, y); this.ctx.rotate(flake.angle); // Determine if you need to flip (zoom)!! flake.vFlip && this.ctx.scale(1, flake.flip); This.ctx.translate (-x, -y)} // Returns a frame animation functiondrawCircle() {/ / because Windows. RequestAnimationFrame executes this point to the window / / the function and use a lot of this, so you need to make sure that this point cannot change / / use the arrow function here, this will point to the Snow, This can now be used normallylet frame = () => {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            for (letFlake of this.snowflakes) {// Each change must start with a save() call and end with a restore() call. this.transform(flake); Canvas API this.ctx.beginPath(); This. CTX. Arc (flake. X, flake. J y, flake. The radius, 0, 2 * Math. PI); this.ctx.closePath(); this.ctx.globalAlpha = flake.alpha; this.ctx.fillStyle = flake.color; this.ctx.fill(); This.ctx.restore () // Restore canvas context property} // requestAnimationFrame(frame)};return frame
    }
    drawPicture() {// same as abovelet frame = () => {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            for (let flake of this.snowflakes) {
                this.ctx.save();
                this.transform(flake);
                this.ctx.globalAlpha = flake.alpha;
                this.ctx.drawImage(flake.image, flake.x, flake.y, flake.radius, flake.radius);
                this.ctx.restore()
            }
            requestAnimationFrame(frame)
        };
        return frame
    }
}
Copy the code

For arrow functions and more new ES6 features, I recommend Yifeng Ruan’s Introduction to ES6 Standards.

  1. Constructor: The constructor primarily handles the problem of default arguments
  2. Init: initializes control class-related attributes
  3. LoadImage: Use the Promise to load the incoming image
  4. CreateSnowflakes: Determines what kind of flakes to create based on whether an image is uploaded or not
  5. Transform: Extract the common transformation code of the two drawing methods and reuse it
  6. DrawCircle: Draw a round snowflake.
  7. DrawPicture: custom snowflake

About requestAnimationFrame

As mentioned earlier, if you pass in a callback function, the browser will execute it at the appropriate time, but only once. So we need to call ourselves again with requestAnimationFrame inside the callback function, so that the animation loop can be formed.

It’s kind of like setInterval, or even more like chained setTimeout, which loops over itself. The browser determines the execution time interval is about ten milliseconds, such a refresh rate, the naked eye is impossible to distinguish, so the animation effect is achieved.

So why do we use requestAnimationFrame? Because what the browser means by timing is, if you switch to the background, the animation will stop rendering until you switch back, and so on. The former, on the other hand, runs all the time, which is undoubtedly a drain on performance. If older browsers don’t support it, you’ll still use the same old methods, but know how they work.

Refer more MDN — window. RequestAnimationFrame

conclusion

The general steps for drawing animation using Canvas are as follows:

  1. Analysis of animation elements, the use of object-oriented thought to animation elements into objects, abstract out the corresponding class.
  2. Write a frame animation function to draw a picture in frame units.
  3. Call API refresh.

Note:

When drawing Canvas, it is necessary to save the state first, and restore the state after each object is drawn to avoid Canvas configuration disorder.

reference

  1. Ruan Yifeng, Introduction to ES6 Standards
  2. MDN — window. RequestAnimationFrame

Introduction to use

Example code has been perfected, can be put into use, simple configuration can achieve flying effect, can be used as a background screen or glass window effect. For details, see GitHub.

Quick to use

// Pass in the id. By default, snowflakes are white dots of different sizes and transparency.'#snow')
Copy the code

Built-in Default configuration

// Configure the corresponding item, do not configure the default new Snow('#snow', {image: [], // Optional image (network or local) vx: [-3, 3], // horizontal speed vy: [2, 5], // vertical speed va: [45, 180], 0, // Flip speed, recommended: slow 0.05/ normal 0.1/ fast 0.2 radius: [5, 15], // Radius range, incoming pictures need to adjust this color: ['white'Alpha: [0.1, 0.9] // Transparency range num: window.innerwidth / 2, // Number of snowflakes, normally unchanged})Copy the code

GIF sample

Sample code:

new Snow('#snow', {
    image: ['./snow.png'],
    radius: [10, 80]
})
Copy the code

The effect is as follows: