This is the third day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021


preface

Recently, we plan to launch an activity related to wechat movement. We have made a general demand description for the product and asked the R&D to do a technical survey. So I reference the ant forest patrol function to write a case, record the realization of the process.

Train of thought

I wrote once before that the realization of electronic signature on Canvas provides me with ideas: dynamic effect implementation adopts the idea of points forming lines. Two points are connected in sequence, and the line segment keeps getting longer to realize the effect of longer distance.

Code implementation

1. Initialize the Canvas

Initialize a class by passing in any items that can change as arguments. Initialize the canvas in the class. Use window.devicepixelratio devicePixelRatio and context.scale(scalewidth,scaleheight) to solve the burr problem of canvas drawing line segment.

class Game { constructor(options) { this.options = options this.ctx = null this.timer = null this.points = [] this.animateNum = 0 this.dpr = window.devicePixelRatio || 1 this.routes = options.routes this.passRoutes = options.passRoutes this.initCanvas() } initCanvas() { let canvas = document.getElementById(this.options.id) canvas.width  = this.options.width * this.dpr; canvas.height = this.options.height * this.dpr; this.ctx = canvas.getContext('2d') this.ctx.scale(this.dpr, this.dpr) this.drawInitialPath() } drawInitialPath() { //... } } let routes = [ { x: 100, y: 100 }, { x: 80, y: 190 }, ] let game = new Game({ id: "canvas", width: 750, height: 750, routes: routes, passRoutes:[] })Copy the code

2. Draw the initial route

To prevent style clashes and confusing patterns, try to start a new path with context.beginPath() every time you draw.

DrawInitialPath () {this.ctx.strokestyle = "# BBB "this.ctx.shadowblur = 0.5 this.ctx.shadowcolor = '#333' this.ctx.lineWidth = 5 this.ctx.lineJoin = "bevel" this.ctx.beginPath() for (let i = 0; i < this.routes.length; i++) { let point = this.routes[i] if (i == 0) { this.ctx.moveTo(point.x, point.y) } else { this.ctx.lineTo(point.x, point.y) } } this.ctx.stroke() this.ctx.closePath() for (let i = 0; i < this.routes.length; i++) { let point = this.routes[i] if (i <= this.passRoutes.length) { this.drawPoint(point.x, point.y, "#1DEFFF") if (i > 0) { this.drawLine(this.routes[i - 1], point, "#1DEFFF") } continue } this.drawPoint(point.x, point.y, "#bbb") } } drawPoint(x, y, color) { this.ctx.beginPath() this.ctx.fillStyle = color this.ctx.strokeStyle = color this.ctx.shadowColor = color this.ctx.arc(x, y, 5, Math.PI * 2, 0, true) this.ctx.stroke() this.ctx.fill() this.ctx.closePath() } drawLine(start, end, Color) {this.ctx.strokestyle = color this.ctx.shadowblur = color this.ctx.beginPath() this.ctx.moveTo(start.x, start.y) this.ctx.lineTo(end.x, end.y) this.ctx.stroke() this.ctx.closePath() }Copy the code

3. Draw dynamic effects

To achieve the effect of a line between two points, you can figure out the points that will pass between them in a certain proportion, and draw the connecting points in turn. For example, from (0,0) to (3,4), according to the Pythagorean theorem, it can be calculated that the displacement between the two points is 5 pixels. Then, only (3-0)\5 pixels need to be moved on the X axis and (4-0)\5 pixels need to be moved on the Y axis to achieve uniform motion between the two points.

    animate(start, end) {
        return new Promise((resolve, reject) => {
            let speed = 1
            let rate = Math.sqrt(
                Math.pow(end.x - start.x, 2) +
                Math.pow(end.y - start.y, 2)) / speed
            for (let i = 0; i < rate; i++) {
                this.points.push({
                    x: (start.x + ((end.x - start.x) / rate * i)).toFixed(1),
                    y: (start.y + ((end.y - start.y) / rate * i)).toFixed(1)
                })
            }
            this.points.push(end)
            this.startAnimate(resolve, reject)
        })
    }
    startAnimate(resolve, reject) {
        let nowPoint = this.points[this.animateNum]
        this.animateNum++
        let nextPoint = this.points[this.animateNum]
        this.ctx.beginPath()
        this.ctx.strokeStyle = "#1DEFFF"
        this.ctx.shadowColor = '#1DEFFF'
        this.ctx.lineWidth = 7
        this.ctx.moveTo(nowPoint.x, nowPoint.y)
        this.ctx.lineTo(nextPoint.x, nextPoint.y)
        this.ctx.stroke()
        this.ctx.closePath()
        this.timer = window.requestAnimationFrame(() => { this.startAnimate(resolve, reject) })
        if (this.animateNum >= this.points.length - 1) {
            this.points = []
            this.animateNum = 0
            window.cancelAnimationFrame(this.timer)
            this.drawPoint(nowPoint.x, nowPoint.y, "#1DEFFF")
            resolve()
        }
    }
Copy the code

Because animation execution is asynchronous, many times you need to know the end result of the animation, and use the result to process some other business logic and start the next animation. RequestAnimationFrame (callback) accepts a method as a parameter and does not support passing parameters. Instead, it uses an anonymous function that calls other functions.

At the end

  • Implementation effect
  • See Github for the full code