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 passed
background-postion
To 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()
- Get ready for the blurry Sprite below
- 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
- 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
- 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 is
x,y
Where 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
- through
requestAnimationFrame
Clear the canvas and draw the contents of each frame - In each frame, modify
this.x
X-coordinate position, and then callball.braw
Method 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 zero
x + dx
- The abscissa of the second ball is zero
x + 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 using
getImageData
Gets 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 to
transform: 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:
- export
lottie-web
Library, import animation JSON file lottie.loadAnimation
Run 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.