Author: @channinghyl 🙌. This article has been authorized to be exclusively used by the public account of the Nuggets Developer community, including but not limited to editing and marking original rights.

preface

Long time no see. I’m Channing

I’m sorry that I haven’t written a new article for two months due to a lot of things recently

These days I finally have the opportunity to share this article with you because of some inspiration from my work

Hope can have certain inspiration or help to you 🙏

With a powerful Canvas, we can use JavaScript to control it and easily draw all kinds of graphics we want. We can also use JavaScript to closely connect user interaction with Canvas’s drawing, and we can even use our imagination to do all kinds of things.

Although canvas helps us draw various graphics easily, canvas itself has no transition effect and will not change every time it is drawn. Then how to make the “static” canvas move?

First, let’s look at the meaning of animation itself, refer to Wikipedia:

Animation is a kind of work and video technology that mistakenly thinks that pictures or objects (pictures) are active by taking a series of stationary solid-state images (frames) at a certain frequency and the speed of motion (playing) (such as 16 frames per second), resulting in the illusion of visual residual images of the naked eye.

As mentioned above, animation is a set of static images switching at a certain speed, each frame is a frame, as long as the frame speed (switching speed) is fast enough, these static images appear to the naked eye as if they are moving.

So how fast is this fast enough?

Visual residual characteristics of the human eye: it is the phenomenon that the vision produced by light to the retina remains for a period of time after the light stops acting, which is caused by the reaction speed of the optic nerve element. Its time value is 1/24 of a second.

So, to fool our eyes, it’s theoretically possible to switch images at a speed of 24 frames per twenty-fourth of a second, and of course, the faster this speed, the smoother the animation.

After mastering this basic knowledge, the idea of implementing Canvas animation becomes very clear: the result of each canvas drawing is regarded as a frame, and the effect of animation can be achieved by repeatedly drawing canvas.

This article takes path animation as an example to see how to draw some Canvas graphics path animation.

How — How to control animation frames

In order to achieve the animation effect of Canvas, we first need a timed method to periodically perform Canvas drawing, that is, to periodically draw each frame of our animation.

When it comes to timing, two methods come to mind: window.settimeout and window.setinterval.

, of course, the two methods can achieve animation effects we need, but I’m not going to use them, also because there is a more appropriate method: window. RequestAnimationFrame.

If you know JavaScript well enough, you know that due to its single-threaded nature, the timer is implemented to execute the timer callback after the current task queue completes, meaning that if the current queue is longer than the timer time, the timer time is not very reliable.

Here’s an example:

The task queue execution time is short:

Queue execution time is long:

Since the timer time is just a desired render time set by ourselves, but it is not a redraw time for the browser, when the two time points deviate, such as frame loss can occur.

CSS3 animation is so strong, requestAnimationFrame is still used?

RequestAnimationFrame provides a smoother and more efficient way to execute the animation, calling our animation frame drawing method when the system is ready to redraw conditions.

In other words, call the requestAnimationFrame method and send a request to the browser that I want to do the drawing of the animation frame when the browser wants to redraw it

The callback we passed in requestAnimationFrame will be called, and in our callback we will call requestAnimationFrame again as long as we need to continue drawing animation frames, passing in the callback itself until the animation is finished.

The use of requestAnimationFrame may sound a bit convolutional, but it should make sense after watching the following example.

With the timing method chosen to control the redraw, it’s time to animate our paths.

Let’s start with a straight line

The first is the basic drawing method of a line:

<body>
<div id="executeButton" onclick="handleExecute()">perform</div>
<canvas id="myCanvas" width="800" height="800"></canvas>
<script>
    function handleExecute() {
        // Get the canvas element
        const canvas = document.querySelector('#myCanvas')
        // Get canvas rendering context
        const ctx = canvas.getContext('2d')

        // Set the line style
        ctx.strokeStyle = 'rgba (81, 160, 255, 1)'
        ctx.lineWidth = 3
        // Create a path
        ctx.beginPath()
        // Move the stroke to the (100,100) coordinates
        ctx.moveTo(100.100)
        // Connect the wire to (700,700)
        ctx.lineTo(700.700)
        // Draw the path you just created
        ctx.stroke()
    }
</script>
</body>
Copy the code

Codepen Portal: Codepen. IO /channinghan…

This is a complete line, so how do you animate a path?

There are two ideas:

  1. Using multiple paths, this line is thought to be composed of several line segments, each time one of them is drawn in order, each time the drawing is an animation frame.
  2. Using the same path, the length of the line drawn by each animation frame increases gradually until we reach the desired length.

The two approaches are almost the same, but there are some differences that can not be ignored: The second type of painting may require that the canvas area of the previous frame be cleared with each redraw, otherwise continuous overwriting can have some undesirable results, such as when you have a line color with transparency, because overwriting will cause the line color to deepen and not achieve the desired transparency.

In addition, if you empty the canvas, it will affect other graphics when you draw multiple graphics closely, for example, some or all of the other graphics will be erased.

In order to make the scene more complex without having to worry about the impact of clearing the canvas, I chose to use the same idea for the path animation in the following examples.

But in order to see the realization or difference of the two ideas more intuitively, IN this example, I will first implement both ideas once:

Idea 1: Use multiple paths

In each frame, the beginPath method is used to create a new path (it will empty the memory of the previous path, but rest assured that it will not empty the drawing result). The calculation of the starting and ending points on the path requires some simple mathematical and geometric knowledge, and the stroke method is executed at the end of each frame to draw the path.

The progress of animation is denoted by Progress, which is obtained by dividing the time from the start of drawing execution of each frame by the animation duration we set.

If progress is 1, the progress is complete. When progress is less than 1 we continue to call requestAnimationFrame to request execution of our draw animationFrame method to the browser.

<script>
    function handleExecute() {
        // Get the canvas element
        const canvas = document.querySelector('#myCanvas')
        // Get canvas rendering context
        const ctx = canvas.getContext('2d')

        // Set the line style
        ctx.strokeStyle = 'rgba (81, 160, 255, 1)'
        ctx.lineWidth = 4
        ctx.lineJoin = 'round'

        // Define the coordinates of the starting and ending points
        const startX = 100
        const startY = 100
        const endX = 700
        const endY = 700
        let prevX = startX
        let prevY = startY
        let nextX
        let nextY
        // The time of the first frame execution
        let startTime;
        // Expected animation duration
        const duration = 1000

        * currentTime is a run-time time passed in when requestAnimation executes the step callback (as obtained by performing.now ()). * */
        const step = (currentTime) = > {
            // Record the start time when the first frame is drawn! startTime && (startTime = currentTime)// The elapsed time (ms)
            const timeElapsed = currentTime - startTime
            // animation execution progress {0,1}
            const progress = Math.min(timeElapsed / duration, 1)

            // Draw method
            const draw = () = > {
                // Create a new path
                ctx.beginPath()
                // Create a subpath and move the starting point to the coordinates reached in the previous frame
                ctx.moveTo(prevX, prevY)
                // Calculate the coordinates that the line segment should reach in this frame, and update the prevX/Y value to use in the next frame.
                prevX = nextX = startX + (endX - startX) * progress
                prevY = nextY = startY + (endY - startY) * progress
                // Connect the points in moveTo to (nextX,nextY) with a straight line
                ctx.lineTo(nextX, nextY)
                ctx.strokeStyle = `rgba(The ${81}.The ${160}.The ${255}.The ${0.25}) `
                // Draw the path of this frame
                ctx.stroke()
            }
            draw()

            if (progress < 1) {
                requestAnimationFrame(step)
            } else {
                console.log('Animation completed')
            }
        }

        requestAnimationFrame(step)
    }
</script>
Copy the code

CodePen Portal: Codepen. IO /channinghan…

Effect:

Second: Use the same path

<script>
    function handleExecute() {
        // Get the canvas element
        const canvas = document.querySelector('#myCanvas')
        // Get canvas rendering context
        const ctx = canvas.getContext('2d')

        // Define the coordinates of the starting and ending points
        const startX = 100
        const startY = 100
        const endX = 700
        const endY = 700
        let nextX
        let nextY

        // The time of the first frame execution
        let startTime;
        // Expected animation duration
        const duration = 1000

        // Create a path
        ctx.beginPath()
        // Create a sliver and move the start point of the new subpath to the (prevX,prevY) coordinates.
        ctx.moveTo(startX, startY)
        // Set the line style
        ctx.strokeStyle = `rgba(The ${81}.The ${160}.The ${255}.The ${0.25}) `
        ctx.lineWidth = 4

        * currentTime is a run-time time passed in when requestAnimation executes the step callback (as obtained by performing.now ()). * */
        const step = (currentTime) = > {
            // ctx.clearRect(startX - 4, startY - 4, Math.abs(endX - startY) + 8, Math.abs(endY - startY) + 8)
            // Record the start time when the first frame is drawn! startTime && (startTime = currentTime)// The elapsed time (ms)
            const timeElapsed = currentTime - startTime
            // animation execution progress {0,1}
            const progress = Math.min(timeElapsed / duration, 1)

            // Draw method
            const draw = () = > {
                // Calculate the coordinate points that the line segment should reach in this frame
                nextX = startX + (endX - startX) * progress
                nextY = startY + (endY - startY) * progress
                // Connect the last point of the child path to the (nextX,nextY) coordinate with a straight line
                ctx.lineTo(nextX, nextY)
                // Draw paths (all subpaths are drawn once)
                ctx.stroke()
            }
            draw()

            if (progress < 1) {
                requestAnimationFrame(step)
            } else {
                console.log('Animation completed')
            }
        }

        requestAnimationFrame(step)
    }
</script>
Copy the code

Codepen Portal: Codepen. IO /channinghan…

Effect:

Add easing function

To make our animations more realistic and rich, we need to use easing function, which is very common in CSS (also called timing function in CSS), for example

transition:  all 600ms ease-in-out;
Copy the code

Ease-in-out specifies the use of ease-in-out easing functions for animation, but there are limitations in CSS that do not support all easing functions, or specify cubic bezier to implement other easing functions.

We can also add easing functions to our linear path animation, and these easing functions can be found in various places, here I use the tween.js easing function: github.com/tweenjs/twe…

The amount function corresponds to our animation progress. For example, Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic Quadratic 1] The image looks like this:

As you can imagine, the animation starts slowly, then gets faster and faster.

Quadratic.Out is almost the opposite, going fast first and then slow.

Quadratic.InOut is slow, then fast, then slow.

Ease (ease) ease (ease) in (ease) out (ease) in and out (ease)

Progress is the amount parameter in the easing function. The result of the easing function can be interpreted as a calculated property instead of progress:

let progress = Math.min(timeElapsed / duration, 1)
progress = Easing.Quadratic.In(progress)
Copy the code

Codepen Portal: Codepen. IO /channinghan…

Effect:

Observe the frame animation in the dotted line

In general, on the browser will execute 60 times a second callback function, which is 60 frames (60 FPS), but the browser will maintain the stability of frame rate as far as possible, which is can reduce to other frame rate, such as poor performance when the browser page may choose to 30 FPS, when the browser’s context is not visible to around 4 FPS is even lower.

To visualize requestAniamtionFrame calls more clearly, based on the first method, we use a count variable to count the number of calls and do not draw if count is odd to form a dashed line:

<script>
    function handleExecute() {
        / /...
      
        // Expected animation duration
        const duration = 1000
        let count = 0

        const step = (currentTime) = > {
            !startTime && (startTime = currentTime)
            const timeElapsed = currentTime - startTime
            const progress = Math.min(timeElapsed / duration, 1)

            // Draw method
            const draw = () = > {
                ctx.beginPath()
                ctx.moveTo(prevX, prevY)

                // Calculate the end of the subline draw and update the prevX/Y value for the next draw
                prevX = nextX = startX + (endX - startX) * progress
                prevY = nextY = startY + (endY - startY) * progress

                if (count % 2= = =0) {
                    // Set the line style
                    ctx.lineWidth = 20 * progress
                    ctx.strokeStyle = `rgba(The ${171 * (1 - progress) + 81}.The ${160 * progress}.The ${255}`, 1)
                    ctx.lineTo(nextX, nextY)
                    ctx.stroke()
                }
            }
            draw()

            if (progress < 1) {
                count++
                requestAnimationFrame(step)
            } else {
                console.log('Animation completed')
                console.log(`${duration}Number of callback executions within MS:${count}Time `)}}// Send an animation execution request to the browser. When the browser wants to redraw, it calls the step method we passed in
        requestAnimationFrame(step)
    }
</script>
Copy the code

Codepen Portal: Codepen. IO /channinghan…

Effect:

As you can see, when I set the animation duration to 1 second, the step frame animation callback in the requestAnimationFrame was executed 60 times, which is up to 60 frames. Even if you can count 30 colored lines on the graph, the corresponding dotted line is also 30, which is exactly 60.

Broken line

If you master the path animation of a straight line, the path animation of a broken line will be easily solved. It is nothing more than to disassemble the broken line into a straight line according to the key turning coordinate points.

There are many ways to split it, but here’s one of my implementations.

Codepen Portal: Codepen. IO /channinghan…

Effect:

 function handleExecute() {
        const canvas = document.querySelector('#myCanvas')
        const ctx = canvas.getContext('2d')

        // Set the line style
        ctx.lineWidth = 7
        ctx.lineJoin = 'round'
        ctx.lineCap = 'round'

        // Sequentially define the coordinates of each turning point on a broken line
        const keyPoints = [
            {x: 250.y: 200},
            {x: 550.y: 200},
            {x: 250.y: 500},
            {x: 550.y: 500},
            {x: 250.y: 200}]let prevX = keyPoints[0].x
        let prevY = keyPoints[0].y
        let nextX
        let nextY
        // The time of the first frame execution
        let startTime;
        // Expected animation duration
        const duration = 900


        // The animation is divided into several segments, each of which is proportional to the total progress
        const partProportion = 1 / (keyPoints.length - 1)
        // Cache draws the n value of the NTH segment in order to complete the end of the segment before drawing the next segment
        let lineIndexCache = 1

        * currentTime is a run-time time passed in when requestAnimation executes the step callback (as obtained by performing.now ()). * */
        const step = (currentTime) = > {
            // Record the start time when the first frame is drawn! startTime && (startTime = currentTime)// The elapsed time (ms)
            const timeElapsed = currentTime - startTime
            // animation execution progress {0,1}
            let progress = Math.min(timeElapsed / duration, 1)
            // Add the quadratic slow function
            progress = Easing.Quadratic.In(progress)

            // Describe what line segment is being drawn
            const lineIndex = Math.min(Math.floor(progress / partProportion) + 1, keyPoints.length - 1)

            {0,1}
            const partProgress = (progress - (lineIndex - 1) * partProportion) / partProportion

            // Draw method
            const draw = () = > {
                ctx.strokeStyle = `rgba(The ${81 + 175 * Math.abs(1 - progress * 2)}.The ${160 - 160 * Math.abs(progress * 2 - 1)}.The ${255}.The ${1}) `
                ctx.beginPath()
                ctx.moveTo(prevX, prevY)
                // Before drawing the next line segment, fill in the missing part at the end of the previous line segment
                if(lineIndex ! == lineIndexCache) { ctx.lineTo(keyPoints[lineIndex -1].x, keyPoints[lineIndex - 1].y)
                    lineIndexCache = lineIndex
                }
                prevX = nextX = ~~(keyPoints[lineIndex - 1].x + ((keyPoints[lineIndex]).x - keyPoints[lineIndex - 1].x) * partProgress)
                prevY = nextY = ~~(keyPoints[lineIndex - 1].y + ((keyPoints[lineIndex]).y - keyPoints[lineIndex - 1].y) * partProgress)
                ctx.lineTo(nextX, nextY)
                ctx.stroke()
            }
            draw()

            if (progress < 1) {
                requestAnimationFrame(step)
            } else {
                console.log('Animation completed')
            }
        }

        requestAnimationFrame(step)
    }
Copy the code

It is mainly divided into several line segments according to the turning point, and then divide the total progress into several line segments, the progress proportion of each line segment partProportion (for example, divided into four segments, then each segment is 0.25), and then calculate the progress partProgress of the current line segment. The rest is pretty much like a straight line.

It is worth mentioning that the progress calculation method of the current line segment is as follows:

const partProgress = (progress - (lineIndex - 1) * partProportion) / partProportion
Copy the code

Since the total progress does not increase linearly and continuously, the partProgress of line segment will not be exactly equal to 1, which results in that the end of each line segment will not be drawn, or the starting point of the next line segment is not the turning point defined by us. This can happen when the animation is short or the total path is long (imagine playing a racing game where there is a shortcut entrance directly to the other side of a hairpin turn).

In order to solve this problem, I used a lineIndexCache variable to cache lineIndex. When the lineIndex drawn is not equal to lineIndexCache, that is, when preparing the next line segment to draw, draw the short segment from the end of the last drawing point to the corresponding turning point. Update lineIndexCache before drawing the next line segment.

In this way, any/any number of transition coordinate points can be drawn into a continuous path animation.

Of course, with this idea, even discontinuous path animation is easy to do, you can try it yourself.

round

Of course, you can use enough triangle segments to stack a circle and enough line segments to draw a “circle”, just like drawing a circle in WebGL, but it is a bit troublesome. Canvas has enclosed special API for drawing circle: Arc (x, Y, RADIUS, startAngle, endAngle, anticlockwise), (there is also the arcTo method, but it is not officially recommended because the implementation of this method is a bit “unreliable”).

The idea of circular path animation is that each animation frame draws an Angle range of the arc.

Codepen Portal: Codepen. IO /channinghan…

Effect:

Core code:

 function handleExecute() {
        // Get the canvas element
        const canvas = document.querySelector('#myCanvas')
        // Get canvas rendering context
        const ctx = canvas.getContext('2d')

        // Set the line style
        ctx.lineWidth = 7
        ctx.lineJoin = 'round'
        ctx.lineCap = 'round'

        // Define the coordinates of the center of the circle
        // const center = {x: ctx.canvas.width / 2, y: ctx.canvas.height / 2}
        const center = {x: 400.y: 400}
        // Define the radius of the circle
        const radius = 200
        // Define the starting and ending angles
        const startAngle = 0
        const endAngle = 2 * Math.PI
        let prevAngle = startAngle
        let nextAngle
        // The time of the first frame execution
        let startTime;
        // Expected animation duration
        const duration = 900

        * currentTime is a run-time time passed in when requestAnimation executes the step callback (as obtained by performing.now ()). * */
        const step = (currentTime) = > {
            // Record the start time when the first frame is drawn! startTime && (startTime = currentTime)// The elapsed time (ms)
            const timeElapsed = currentTime - startTime
            // animation execution progress {0,1}
            let progress = Math.min(timeElapsed / duration, 1)
            progress = Easing.Cubic.In(progress)
            // Draw method
            const draw = () = > {
                // Create a new path
                ctx.beginPath()
                // Calculate the Angle that the arc should reach in this frame
                nextAngle = startAngle + (endAngle - startAngle) * progress
                // Create an arc
                ctx.arc(center.x, center.y, radius, prevAngle, nextAngle, false)
                // Set the gradient color
                ctx.strokeStyle = `rgba(The ${81 + 171 * Math.abs(1 - progress * 2)}.The ${160 - 160 * Math.abs(1 - progress * 2)}.The ${255}`, 1)
                // Draw the arc of this frame
                ctx.stroke()
                // Update prevAngle to nextAngle in this frame for use in the next frame
                prevAngle = nextAngle
            }
            draw()

            if (progress < 1) {
                requestAnimationFrame(step)
            } else {
                console.log('Animation completed')
            }
        }

        requestAnimationFrame(step)
    }
Copy the code

The only line that matters is this:

// Calculate the Angle that the arc should reach in this frame
                nextAngle = startAngle + (endAngle - startAngle) * progress
Copy the code

And you’ll see that it’s the same thing that we did when we animated straight paths.

It’s hard to think of circles without thinking of arcs, and then you might think of a famous person….

The GIF has dropped a few frames

Codepen Portal: Codepen. IO /channinghan…

Bessel curve

Canvas has quadratic and cubic **** Bezier curves, which are inherently difficult to use, but with enough patience you will be able to draw complex and regular shapes.

Let’s first look at the Canvas API that draws them:

Draw quadratic bezier curves: quadraticCurveTo(cp1x, cp1y, x, y)

Cp1x,cp1y is a control point, x,y is the end point

Draw bezier curves three times: bezierCurveTo(cp1x, CP1y, CP2x, cp2y, x, y)

Cp1x and CP1y are control points 1, cp2x and cp2y are control points 2, x and Y are end points.

You can think of it as the lineTo method, but it needs to pass in one or two more coordinate parameters of control points, and the underlying operations are more complicated.

If you don’t know about bezier curves and are interested in them, there’s a great article on bezier curves:

Draw a curve animation on canvas – in-depth understanding of Bezier curves

Wouldn’t it be even harder to animate the paths of Bezier curves? Canvas is to draw a complete Bezier curve. It seems that we cannot draw part of it directly.

So they had to find another way:

  1. Method one: Calculate the points on the Bezier curve and connect enough of them with straight lines.
  2. Method two: by observing and analyzing the characteristics of Bessel curves, the control points in some Bessel curves are found out, and these partial Bessel curves are drawn accordingly.

First of all, either method one or method two, we need to figure out the coordinates of any point on the Bezier curve.

It’s very difficult to bring yourself to derive the coordinates of a particular point on a Bezier curve, but we can easily find the equation of the curve, just Google it.

P0 is the starting point (default is the starting point of the current path on canvas), P2(quadratic Bessel) or P3 (cubic Bessel) are the ending points, and the rest are the control points that control the Bessel curve.

According to the formula of point B is that we have requested a point on the bezier curve, it is enough to let us know with a draw their path animation method, but this way is more ugly, because we already know how to get the coordinates of point under any progress, will be enough connect with straight line at a point on the path.

Since the first method is not “elegant” to implement, I’m going to fiddle with it and try the second method.

The second method requires one more condition: the control points on the partial Bezier curve, which I will name SC (sub Control point) for the moment.

So how do we get this subcontrol point?

Quadratic Bessel curve

First, let’s look at a GIF of a quadratic Bezier curve:

And then there’s the physical knowledge, and when you look at this GIF a few times, you get this intuition:

The sub-control point is on the p0-P1 line segment, and its position is relevant to our progress. Now we boldly assume:

sc.x = p0.x + p1.x - p0.x
sc.y = p0.y + p1.y - p0.y
Copy the code

Then check carefully and implement it in code:

function handleExecute() {

        // Calculate the coordinates of the child control points
        function calSC(t) {
            SC.x = p0.x + (p1.x - p0.x) * t
            SC.y = p0.y + (p1.y - p0.y) * t
        }

        // Calculate the end of the subBezier curve
        function calB(t) {
            B.x = Math.pow(1 - t, 2) * p0.x + 2 * t * (1 - t) * p1.x + Math.pow(t, 2) * p2.x
            B.y = Math.pow(1 - t, 2) * p0.y + 2 * t * (1 - t) * p1.y + Math.pow(t, 2) * p2.y
        }


        // Get the canvas element
        const canvas = document.querySelector('#myCanvas')
        // Get canvas rendering context
        const ctx = canvas.getContext('2d')


        // Set the line style
        ctx.strokeStyle = 'rgba (81, 160, 255, 1)'
        ctx.lineWidth = 4
        ctx.lineJoin = 'round'

        // The time of the first frame execution
        let startTime;
        // Expected animation duration
        const duration = 1000

        / / starting point
        const p0 = {x: 100.y: 500}
        / / control points
        const p1 = {x: 200.y: 100}
        / / the end
        const p2 = {x: 700.y: 500}
        // Set the initial coordinates to p0.
        constSC = {... p0}// The end point of the subbezier curve (this is not important, set to p0)
        letB = {... p0}// First draw a complete Bezier curve to verify the accuracy of bezier curve animation
        ctx.beginPath()
        ctx.moveTo(p0.x, p0.y)
        ctx.strokeStyle = '#e3e3e3'
        ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y)
        ctx.stroke()

        // Draw an eye (not important)
        function drawEye(color) {
            ctx.beginPath()
            ctx.strokeStyle = color
            ctx.arc(p0.x + 100, p0.y - 50.50.0.2 * Math.PI, false)
            ctx.stroke()
            ctx.moveTo(p0.x + 300, p0.y - 50)
            ctx.arc(p0.x + 250, p0.y - 50.50.0.2 * Math.PI, false)
            ctx.stroke()
        }

        drawEye('rgb(227, 227, 227)')



        * currentTime is a run-time time passed in when requestAnimation executes the step callback (as obtained by performing.now ()). * */
        const step = (currentTime) = > {
            // Record the start time when the first frame is drawn! startTime && (startTime = currentTime)// The elapsed time (ms)
            const timeElapsed = currentTime - startTime
            // animation execution progress {0,1}
            let progress = Math.min(timeElapsed / duration, 1)
            progress = Easing.Quadratic.In(progress)


            // Draw method
            const draw = () = > {
                ctx.beginPath()
                ctx.moveTo(p0.x, p0.y)
                // Calculate and update the coordinates of B and SC
                calB(progress)
                calSC(progress)
                // Connect the points in moveTo to (nextX,nextY) with a straight line
                ctx.quadraticCurveTo(SC.x, SC.y, B.x, B.y)
                ctx.strokeStyle = `rgba(The ${171 * (1 - progress) + 81}.The ${160 * progress}.The ${255}`, 1)
                ctx.stroke()
                // Eyes fade
                drawEye(`rgba(The ${227 - (227 - 81) * progress}.The ${227 - (227 - 160) * progress}.The ${255}`, 1))
            }
            draw()

            if (progress < 1) {
                requestAnimationFrame(step)
            } else {
                console.log('Animation completed')}}setTimeout(() = > {
            requestAnimationFrame(step)
        }, 1000)}Copy the code

The core is to extract two calculation methods calcB and calSC to calculate and update the end point and control point of the sub-Bezier curve, and then use the Drawing API of bezier curve:

ctx.quadraticCurveTo(SC.x, SC.y, B.x, B.y)
Copy the code

Codepen Portal: Codepen. IO /channinghan…

Before performing the animation, I first draw the complete Bezier curve with gray lines, and then perform the animation to see if it can be accurately covered to check whether our animation method is correct:

God bless 🙏

Cubic Bezier curves

For cubic Bezier curves, we’ll start with the usual gifs:

After finding a sense in the quadratic Bezier curve, it is not difficult to sense where the two sub-control points are, and make a bold guess:

Then we only need to add SC1, SC2, SC3 and modify the calculation method of B coordinates, where SC1 and SC2 are sub-control points and SC3 is used to calculate the coordinates of SC2.

The calculation method of these points is:

// Calculate the coordinates of child control point 1
        function calSC1(t) {
            SC1.x = p0.x + (p1.x - p0.x) * t
            SC1.y = p0.y + (p1.y - p0.y) * t
        }

        // Calculate the point coordinates used to calculate the coordinates of child control point 2
        function calSC3(t) {
            SC3.x = p1.x + (p2.x - p1.x) * t
            SC3.y = p1.y + (p2.y - p1.y) * t
        }

        // Calculate the coordinates of child control point 2
        function calSC2(t) {
            SC2.x = SC1.x + (SC3.x - SC1.x) * t
            SC2.y = SC1.y + (SC3.y - SC1.y) * t
        }

        // Calculate the end of the subBezier curve
        function calB(t) {
            B.x = Math.pow(1 - t, 3) * p0.x + 3 * t * Math.pow(1 - t, 2) * p1.x + 3 * p2.x * Math.pow(t, 2) * (1 - t) + Math.pow(t, 3) * p3.x
            B.y = Math.pow(1 - t, 3) * p0.y + 3 * t * Math.pow(1 - t, 2) * p1.y + 3 * p2.y * Math.pow(t, 2) * (1 - t) + Math.pow(t, 3) * p3.y
        }
Copy the code

Core methods in frame animation:

ctx.beginPath()
ctx.moveTo(p0.x, p0.y)
// Calculate and update the coordinates of B and SC1, SC2, SC3
calB(progress)
calSC1(progress)
calSC3(progress)
calSC2(progress)
// Connect the point in moveTo to B with three Bezier curves
ctx.bezierCurveTo(SC1.x, SC1.y, SC2.x, SC2.y, B.x, B.y)
ctx.strokeStyle = `rgba(The ${171 * (1 - progress) + 81}.The ${160 * progress}.The ${255}`, 1)
ctx.stroke()
Copy the code

The rest is pretty much the path animation of a quadratic Bezier curve.

Codepen Portal: Codepen. IO /channinghan…

Let’s verify the path animation of the Bessel curve three times again:

God bless again 🙏

In fact, I did not succeed at the first time, when the child control points are not chosen correctly, there will be some interesting phenomena:

The last

The path animation of various graphics on canvas here has been basically shared. In fact, it is not necessarily used in daily work. At the same time, there are other ways to complete path animation, such as CSS and SVG.

The reason why I spend so much time writing such a story is firstly because I find it interesting, and secondly, I understand more about canvas and some principles behind animation in this process.

Thank you for your patience.

Of course, there may be deficiencies and mistakes in the article, and you are welcome to communicate with me in the comments.

All the demos used in this article are already in the GitHub portal.

Finally, I hope friends can like, comment, pay attention to three, these are all the power source of my share 🙏