canvas实现一个仿蚂蚁森林巡护

这是我参与11月更文挑战的第3天,活动详情查看: 2021最后一次更文挑战


前言

        最近打算上线一个跟微信运动相关的活动,产品做了大概的需求描述,让研发做个技术调研。于是我就参考蚂蚁森林的巡护功能写个了案例,记录一下实现的过程。

思路

        之前写过一次canvas实现电子签名给我提供了思路:动效的实现采用点构成线的思路,两个点依次连线,线段不断变长,实现路程不断变长的效果。

代码实现

1.初始化Canvas

将可能变化的项都当作参数传入,初始化一个类。在类中初始化canvas。使用window.devicePixelRatio设备像素比和context.scale(scalewidth,scaleheight)缩放解决canvas绘制线段会出现毛边问题

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:[]
})
复制代码

2.绘制初始路线

为了防止出现样式冲突和绘制出莫名其妙的图案,尽量每一次绘制都使用context.beginPath()开启一个新的路线。

    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.shadowColor = color
        this.ctx.shadowBlur = 0.5
        this.ctx.beginPath()
        this.ctx.moveTo(start.x, start.y)
        this.ctx.lineTo(end.x, end.y)
        this.ctx.stroke()
        this.ctx.closePath()
    }
复制代码

3.绘制动效

要在两个点之间实现一个连线的效果,可以按照一定的比例求出两个点之间会经过的点,依次绘制连接这些点。比如(0,0)到(3,4)这两个点,根据勾股定理可以算出两个点之间的位移是5个像素,那每次x轴只需要移动(3-0)\5个像素,y轴移动(4-0)\5个像素就可以实现两个点之间的匀速运动。

    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()
        }
    }
复制代码

这里对animate()返回值做了一下处理,返回了一个promise对象,是因为动画的执行是异步的,很多时候需要知道动画执行结束的结果,在结果里处理一些其他的业务逻辑并开启下一次动画。 requestAnimationFrame(callback)接受的是一个方法作为参数,不支持传递参数,这里使用了传入一个匿名函数,匿名函数内调用其他函数传参解决。

结尾

  • 实现效果

猜你喜欢

转载自juejin.im/post/7034525228646531108