这是我参与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)
接受的是一个方法作为参数,不支持传递参数,这里使用了传入一个匿名函数,匿名函数内调用其他函数传参解决。
结尾
- 实现效果
- 完整代码 查看github