Follow me on my blog shymean.com

Developing H5 active page is a common requirement in front-end development. It is a very interesting requirement with light business logic and emphasis on interaction and presentation. This article will organize the basic principles and simple implementations of Web animations commonly used in H5 development.

Frame by frame animation

reference

  • CSS3 animation frame by frame
  • Twitter Thumbs-up animation, a thumbs-up effect achieved using frame-by-frame animation

Frame by frame animation is also called stop-motion animation, its principle is to play each frame of different images continuously, so as to produce animation effect

The most common frame-by-frame animation is probably a GIF. On a Mac, use the Preview tool to open a GIF and see multiple still images on the left.

In addition to using GIF images directly, the front end can also use JS or CSS to achieve frame-by-frame animation, the general steps are as follows

  • Make a still image for each frame ahead of time. To reduce network requests, Sprite images are usually used and passedbackground-postionTo control the display of animation
  • Play each frame in order to create an animation effect

The disadvantage of frame-by-frame animation is that it requires the UI to provide a picture of each frame, which is inefficient, scalable and has poor network performance.

CSS can define keyframe animations with @keyframes. Each keyframe is named by a percentage value that represents the stage in the animation at which the style contained in the frame is triggered.

CSS also provides animation-timing-function to control the speed at which the style changes two frames before

  • Cubic – Bezier () timing function
  • Steps () function

The following example shows a flappy Bird bird flight animation whose core implementation relies on @keyframes and step()

  1. Get ready for the blurry Sprite below

  1. Define CSS properties for three frames, mainly to find the background offset of each frame
.bird {
    position: relative;
    width: 46px;
    height: 31px;
    background-image: url("./bird.png");
    background-repeat: no-repeat;
}
.bird1 {
    background-position: 0 center;
}
.bird2 {
    background-position: -47px center;
}
.bird3 {
    background-position: -94px center;
}
Copy the code
  1. Put the above three frames in the animation
@keyframes fly-bird {
    0% {
        background-position: 0;
    }

    33.3333% {
        background-position: -47px;
    }

    66.6666% {
        background-position: -94px; }}Copy the code
  1. Run the animation
.bird-fly {
    animation: fly-bird 0.4 s steps(1) infinite;
}
Copy the code

The effect achieved is

It should be understood that animation-timing-function is applied between two keyframes, not the entire animation. Therefore, steps(1) means that only one jump is executed between 0 and 33.33%, not between 0 and 100% of the animation. So I could have written it a simpler way

@keyframes fly-bird2 {
    100% {
        background-position: -141px; }}.bird-fly {
    // Specify 3 frames. Step calculates the offset of each frame
    animation: fly-bird2 0.4 s steps(3, end) infinite; 
}
Copy the code

SVG stroke animation

SVG stroke animation is the most commonly used animation form in SVG. Its core principle is related to two attributes

  • stroke-dasharray, is mainly used to draw dashed lines, and its value isx,yWhere X represents the length of the solid line dash and Y represents the gap between two solid lines
  • Stroke-dashoffset, used to define the start of the Dash line

The idea behind stroke animation is to set a large dash and gap, and then hide the entire solid line in the initial state by setting a large stroke-dashoffset, and then gradually show the implementation by shrinking the stroke-dashoffset. This creates a “Stroke” animation.

Note that the dotted lines drawn follow the path path, so in theory they can be any shape!

So what should be the initial values of stroke-Dashoffset and stroke-Dasharray? Just set it to the length of path.

The path of a path is not necessarily a regular path, but fortunately JavaScript provides an interface to get the length of the path

let path = document.querySelector('path');
let length = path.getTotalLength(); // use this value as the dashoffset and dasharray values
Copy the code

A dialog box using stroke animation is shown below

@keyframes stroke {
    100% {
        stroke-dashoffset: 0; }}.chat_corner {
    animation: stroke 0.6 s linear 0.3 s forwards;
}

.chat-path {
    width: 110px;
    margin: -5px auto 0;
}

.chat-path .chat_corner {
    transform-origin: 50% 50%;
    stroke-dasharray: 641;
    stroke-dashoffset: 641;
}
Copy the code

The results are as follows:

Complete source code

Canvas animation

Canvas animation can be understood as frame-by-frame animation of JS version. The main principle is to manually calculate the state of elements in each frame and draw them on the canvas.

Basic animation

The following code shows drawing a moving ball using canvas

let canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");

let ball = {
    x: 0.y: 100.r: 10.dx: 2.rgbaColorArr: [111.123.222.1].draw() {
        const {x, y, r, rgbaColorArr} = this
        ctx.beginPath()
        ctx.arc(x, y, r, 0.Math.PI * 2);
        ctx.closePath()
        ctx.fillStyle = `rgba(${rgbaColorArr.join(', ')}) `
        ctx.fill();
    },
    move() {
        this.x += this.dx
        this.draw()
    },
    animate() {
        const d = 1000 // Animation length 1000ms
        const update = (t) = > {
            // Clear the canvas
            ctx.clearRect(0.0, canvas.width, canvas.height);
            // Move the ball and redraw
            this.x += this.dx
            this.draw()

            if (t < d) {
                requestAnimationFrame(update);
            }
        }
        requestAnimationFrame(update);
    }
}

ball.animate()
Copy the code

The code is relatively simple, and the concrete implementation is

  • throughrequestAnimationFrameClear the canvas and draw the contents of each frame
  • In each frame, modifythis.xX-coordinate position, and then callball.brawMethod to redraw the ball

In this way, the naked eye can see the motion of the ball.

As you can see from the code above, the key to ball motion is

this.x += this.dx
Copy the code

Assuming that the interval between frames is the same, the ball we see is moving at a constant speed,

  • The x-coordinate of the first ball is zerox + dx
  • The abscissa of the second ball is zerox + 2*dx
  • .

Another way to think about it is that we can achieve non-uniform animation effects, such as the famous Tween.js, as long as we control the change of x variable

Most easing methods take four parameters

// t: current time
// b: start state
// c: the changed state
// d: the time required to change from b to B + C
function linear(t, b, c, d) {
    return c * t / d + b
}
function easeIn(t, b, c, d) {
    return c * (t /= d) * t + b;
}
Copy the code

For example linear(1000, 10, 100, 2000) is a linear animation that starts at state 10 and ends at state 10+100, which takes 2000ms and corresponds to the state at 1000ms. Note that t and D need to be in the same unit, and you don’t need to force them to be converted to seconds or milliseconds

So change the way this. X is evaluated in the ball-.animate method

let ball = {
    / /... Other attributes
    animate() {
        const d = 1000 // Exercise time 1000ms
        const target = canvas.width - this.r * 2 // Target position

        const update = (t) = > {
            ctx.clearRect(0.0, canvas.width, canvas.height);
            // this.x += this.dx
            // this.x = linear(t, 0, target, d) = this.x += this.dx
            this.x = easeIn(t, 0, target, d) // easeIn
            this.draw()

            if(t < d) { requestAnimationFrame(update); } } requestAnimationFrame(update); }}Copy the code

You can see the different effects of the exercise. Keep in mind that we are not changing the time of the animation, but calculating the state of the ball at one point in time as a function of time and then updating it to the canvas, which is the basis of many animations.

In addition to the easing function, bezier curves can be used to calculate the state of the elements

reference

  • Practical CSS – Cubic bezier curve
  • Bezier curve principle
  • Cubic – Bezier generation online

Particle animations

Particles can be understood as pixels, the smallest unit of composition in a canvas, or as points of relatively small area. It is difficult to see the effect of a single particle, and a large number of particles are used to describe irregular objects, often used in game systems such as flames, fireworks, etc.

reference

  • Canvas animation particle effects
  • Create a lofty Canvas particle animation

Particle animation can be roughly divided into the following steps

  • Define a particle. The basic properties of a particle include position, size, color, speed, residence duration, etc
  • Generate particles, can be directly traversed dynamically generated, or can be prepared ahead of time particles (such as usinggetImageDataGets the pixels of an area on the canvas.
  • Firing the particles and rendering the particles on the canvas for each frame is similar to the animation of the individual balls above, except now we need to deal with thousands of particles

The basic properties of a particle include: initial position, end position, color, and shape. In addition, particles often include duration, delay, and other properties

Effect of basis

Below is a sample that will take pixels from the canvas and show the basic particle effect

Its core code is

const pos = {x: 50.y: 50.w: 100.h: 100}
const originX = pos.x + pos.w / 2
const originY = pos.y + pos.h + 200

// Use an array to store the generated particles
let particles = []
const reductionFactor = 5
for (let x = 0; x < pos.w; x += reductionFactor) {
    for (let y = 0; y < pos.h; y += reductionFactor) {
        let particle = {
            // The initial position
            x0: originX,
            y0: originY,
            // End position
            x1: x + pos.x,
            y1: y + pos.y,
            // Style attributes
            rgbaColorArr: randomColor(),
            currTime: 0.// The time the particle has been running
            duration: 3000}; particles.push(particle); }}Copy the code

I then iterate through the list, animating each particle in turn,

function easeInOutExpo(e, a, g, f) {
    return g * (-Math.pow(2, -10 * e / f) + 1) + a
}

let requestId

function renderParticles(t) {
    // The last particle
    const last = particles[particles.length - 1]
    if (last.duration < last.currTime) {
        cancelAnimationFrame(requestId)
        return
    }

    ctx.clearRect(0.0, canvas.width, canvas.height);

    for (let p of particles) {
        const {duration, currTime} = p
        ctx.fillStyle = `rgba(${p.rgbaColorArr.join(', ')}) `
        if (currTime < duration) {
            let x = easeInOutExpo(currTime, p.x0, p.x1 - p.x0, duration);
            let y = easeInOutExpo(currTime, p.y0, p.y1 - p.y0, duration);
            ctx.fillRect(x, y, 1.1)}else {
            ctx.fillRect(p.x1, p.y1, 1.1)
        }
        p.currTime = t
    }

    requestId = requestAnimationFrame(renderParticles)
}

const animate = () = > {
    requestId = requestAnimationFrame(renderParticles)
}

animate()
Copy the code

Complete source code

As you can see from the animation, for a single particle, the animation implementation is no different from the “larger” ball above. But as a whole, the animation is very stiff.

Make particle animation more realistic

If you want particle animations to be realistic, you don’t want them to be too monolithic. Instead, you want each particle to start at intervals and alternate animations. There are two ways to do this

  • Stagger the start times of each row of particles regularly
  • Random staggered start times between each row of particles

The independent sense of particle and the overall sense of hierarchy are the main reasons to ensure the authenticity of particle animation.

So I changed the code a bit and added some random parameter control

const frameTime = 16.66 // Assume a frame of 16.66ms

for (let x = 0; x < pos.w; x += reductionFactor) {
    for (let y = 0; y < pos.h; y += reductionFactor) {
        let particle = {
            / /... The other parameters
            delay: y / 20 * frameTime, // Delay the startup time by line, highlighting the sense of hierarchy
            interval: parseInt(Math.random() * 10 * frameTime), // Each particle startup interval, random 1 to 10 frames, highlighting the particle sense of granularity} particles.push(particle); }}Copy the code

Then add delay and interval processing when drawing particles

for (let p of particles) {
    // The startup time is not reached
    if (p.currTime < p.delay) {
        p.currTime = t
        continue
    }

    const {duration, currTime, interval} = p
    ctx.fillStyle = `rgba(${p.rgbaColorArr.join(', ')}) `
    if (currTime < duration + interval) {
        // Introduce interval to control the startup interval for each particle
        if (currTime >= interval) {
            let x = easeInOutExpo(currTime - interval, p.x0, p.x1 - p.x0, duration)
            let y = easeInOutExpo(currTime - interval, p.y0, p.y1 - p.y0, duration)
            ctx.fillRect(x, y, 1.1)}}else {
        ctx.fillRect(p.x1, p.y1, 1.1)
    }
    p.currTime = t
}
Copy the code

Complete source code

physics

In addition to using slow functions and Bezier curves to control the state of elements in each frame, we can also simulate more realistic physical effects such as parabolic balls, free-falling bodies, object collisions, etc

More physics and mathematics are required here, such as vectors, mathematical vectors, etc., which are not expanded here

FLIP implements DOM switch animation

FLIP stands for First, Last, Invert, Play.

reference

  • React and Vue are using the FLIP idea, write very detailed, recommended reading
  • FLIP Your Animations

FLIP is an implementation of DOM state – switching animation scheme

  • First: The initial state of the element
  • Last: Terminates the element
  • INvert: this is the core idea of the entire animation implementation. For example, if an element starts at x position 0 and moves to the right 90px, to animate it, we can set the initial state of the element totransform: translate
  • Play: the animation returns to its original position

Invert is the result of a period in which the element’s DOM information has changed but the browser has not rendered, since changes in the DOM element’s attributes are collected by the browser and deferred to the next frame

The following is in Vue using FLIP array chaos animation effect, the core code is

function getPositionList(domList) {
  return domList.map((dom) = > {
    const {left, top} = dom.getBoundingClientRect()
    return {left, top}
  })
}

this.list = shuffle(this.list)

const cellList = this.$refs.cell.slice() // Save the snapshot

// Get the initial state
const originPositions = getPositionList(cellList)

this.$nextTick(() = > {
    // Get the new node status
    // It is necessary to ensure that the DOM nodes are the same before and after the change, so v-for key cannot use index
    const currentPositions = getPositionList(cellList)

    cellList.forEach((cell, index) = > {
        let cur = currentPositions[index]
        let origin = originPositions[index]

        const invert = {
            left: origin.left - cur.left,
            top: origin.top - cur.top,
        }

        const keyframes = [
            {transform: `translate(${invert.left}px, ${invert.top}px)`},
            {transform: "translate(0)"},]const options = {
            duration: 300.easing: "linear",
        }

        cell.animate(keyframes, options)
    })
})
Copy the code

React uses useLayoutEffect to perform logic after DOM updates. It should be noted that the core of FLIP is to obtain the state before and after the element is changed, and the premise is that it is for the same element. Therefore, for VNode, its key value should be unique, so as to ensure the comparison of the same DOM node instead of “local reuse”.

Complete source code

Lottie: Hold the design leg

reference

  • Lottie’s official website
  • Lottie Files, which provides a large number of Lottie animations, can be downloaded from the JSON file above
  • Lottie Editor, which supports simple editing of Lottie animations

Lottie is a Calayer-based animation, all paths are pre-calculated in AE, converted to Json files, and then automatically converted to Layer animation,

So we just have to wrap our arms around the design, which is a very small amount of work for development

Development and access steps:

  • exportlottie-webLibrary, import animation JSON file
  • lottie.loadAnimationRun the file

Lottie runs on multiple platforms, including iOS, Android and even Flutter. When you need some complicated animations, ask the design masters

summary

This paper sorted out several common ideas of web animation development

  • Frame by frame animation, the most common form of animation
  • Stroke animation, path animation using SVG
  • Particle animation, using Canvas to achieve cool particle effects
  • FLIP animation, mainly used for DOM switching and other effects

Animation implementation, in addition to understanding its principle, but also need to debug the animation running time, switching speed and other parameters, which requires a lot of experience and experience, probably this is also calculated as a front-end fun bar.