本文详解如何使用JavaScript和Canvas技术实现支付宝画年兔小游戏。核心内容包括利用getImageData限制鼠标绘制区域、通过requestAnimationFrame实现画布旋转动画、使用栈结构处理撤销操作,以及结合贝塞尔曲线绘制兔子轮廓。文章提供了关键代码逻辑与实现思路,帮助开发者理解前端图形交互与动画控制的实际应用。
春节临近,支付宝的集五福活动如期而至,大家是否已经集齐了五福?每年的集福活动都会推出互动小游戏,今年的“画年画”便是其中之一。本文将介绍如何利用Canvas技术开发一款画年兔的互动游戏。

开发前的思路规划
画年画游戏的核心规则是:用户需沿着特定的轮廓线条进行描画。
关键问题一:如何限制鼠标仅在特定区域内绘制?
首先需要获取轮廓区域在Canvas画布上的具体坐标位置,并判断鼠标绘制点是否落在指定范围内。利用Canvas的getImageData函数可以获取画布上有颜色的像素数据,进而根据像素点的分布情况计算出其对应的坐标位置。
关于getImageData函数的具体用法,此前在“Canvas文字粒子特效”一文中已有详细介绍,如有不清楚之处可参考该文。
getImageData(canvas, ctx) {
const data = ctx.getImageData(
0,
0,
canvas.width,
canvas.height,
).data;
const gap = 4;
const points = [];
const length = data.length;
for (let i = 0, wl = canvas.width * gap; i < length; i += gap) {
if (data[i + gap - 1] == 255) {
// 根据透明度判断
const x = (i % wl) / gap;
const y = Math.ceil(i / wl);
points.push([x, y]);
}
}
return points;
}关键问题二:如何让绘制的图案产生动态效果?
可以通过定时旋转画布来实现动态效果。这里推荐使用requestAnimationFrame函数进行帧动画处理,相比setInterval函数,其性能表现更优。
关键问题三:如何实现上一步操作的撤销?
将每一步绘制的关键点记录到创建的栈结构中,每次执行撤销操作时,从栈中移除上一步的绘制点数据即可。
关键问题四:如何判断线条绘制是否完成?
经过多次尝试,目前采用以下逻辑:当触发mouseup事件时,判断当前步骤下绘制的点数是否大于等于30。若满足条件,则延迟0.5秒执行下一步绘制;若在0.5秒内再次触发mousedown事件,则取消之前的延迟函数,等待下一个mouseup事件。
// mouseup事件
const length = _this.execStack.reduce((prev, next) => {
if (next.step == _this.curStep) {
prev += next.points.length;
}
return prev;
}, 0)
if (length >= 30) {
_this.timer = setTimeout(() => {
if (_this.curStep == 2) {
_this.curStep = 2.5
_this.canEdit = false;
_this.animate('ears_1', 3);
} else if (_this.curStep == 4) {
_this.curStep = 4.5
_this.canEdit = false;
_this.animate('shake_head', 5);
} else if (_this.curStep == 6) {
_this.curStep = 6.5
_this.canEdit = false;
_this.animate('shake_body', 7);
} else if (_this.curStep <= 6) {
_this.canEdit = true;
_this.curStep += 1;
}
_this.execCanvas();
}, 500);
}核心实现步骤
1. 创建RabbitPainting类,在初始化阶段监听Canvas的鼠标点击事件。需注意移动端与PC端监听的事件类型存在差异:
this.pcEvents = ['mousedown', 'mousemove', 'mouseup'];
this.mobileEvents = ['touchstart', 'touchmove', 'touchend'];
代码中的兼容性处理较为简略,主要目的是展示核心功能思路,建议读者重点关注实现逻辑。
鼠标移开后需解除事件监听,当鼠标再次按下时重新绑定事件。mousedown事件的监听流程如下:

鼠标移动时,系统会获取两个关键点:鼠标按下时的位置与鼠标移动时的位置。如果绘制的线条直接连接这两个点,会出现如图所示的效果:

图中红色线条代表鼠标移动的路径,黑色线条为Canvas实际绘制的线条。因此,在mousemove函数执行完毕后,需要更新初始按压点,确保前后两个点能够无缝衔接。

线条绘制的具体函数实现如下:
drawLine(point) {
const { ctx } = this
ctx.beginPath()
ctx.moveTo(point.startX, point.startY);
ctx.lineTo(point.endX, point.endY);
if (point.style) {
for (let key in point.style) {
ctx[key] = point.style[key]
}
}
ctx.stroke();
ctx.closePath()
}2. 兔子轮廓的绘制,采用二阶贝塞尔曲线来构建图形。

// 外轮廓样式
const wrapperStyle = {
lineWidth: "30",
strokeStyle: this.tipPathColor[0]
}
// 内虚线样式
const innerStyle = {
lineWidth: "3",
strokeStyle: this.tipPathColor[1],
lineDash: [15, 12]
}drawCurve({ list, wrapperStyle, innerStyle }) {
const { tempCtx: ctx } = this
list.forEach(point => {
const { x, y, list } = point;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.bezierCurveTo(...list);
for (let key in wrapperStyle) {
ctx[key] = wrapperStyle[key]
}
ctx.stroke();
ctx.save()
ctx.beginPath();
ctx.moveTo(x, y);
ctx.bezierCurveTo(...list);
for (let key in innerStyle) {
if (key == 'lineDash') {
ctx.setLineDash(innerStyle[key]);
}
ctx[key] = innerStyle[key]
}
ctx.stroke();
ctx.restore();
})
}贝塞尔曲线的关键在于合理设置p1和p2两个控制点,具体参数需根据实际需求自行调整。

整体兔子轮廓的大致结构如下:

3. 画布旋转功能的实现
使用Canvas的rotate函数进行旋转。由于画布的默认中心点为(0,0),旋转前需使用translate(x,y)函数将中心点移动至特定位置。需要注意的是,旋转结束后必须将画布中心点还原至(0,0)。
const rotateCanvas = (centerPoints, item) => {
ctx.save()
ctx.translate(...centerPoints)
ctx.rotate(Math.PI / 180 * item.curDeg)
ctx.translate(-centerPoints[0], -centerPoints[1])
}务必使用ctx.save()保存画布旋转前的状态,绘制完成后需通过ctx.restore()还原画布状态,否则定时函数执行角度旋转时,角度值会不断累加。
4. 眨眼动画的实现
眨眼动画使用一张精灵图,由于图片为自行绘制且仅包含六帧,动画效果可能略显生硬,仅供参考。
以上即为使用JavaScript和Canvas模拟实现支付宝画年兔游戏的详细内容,更多相关技术资料请关注本站其他文章。