本篇文章介绍如何使用 Canvas 技术,在微信小程序中实现流畅的刮刮卡效果,并且你将学到如何优化性能,提升用户体验。
技术需求:为什么选择Canvas实现刮刮卡效果?
Canvas 是微信小程序中用于绘制图形、图像、动画等的强大工具,尤其适合用来创建动态的交互效果。通过 Canvas,我们可以创建出涂层效果,并响应用户的触摸事件来模拟刮开涂层的过程,从而展示底层的答案或隐藏信息。
本功能需求:
- 涂层展示:在 Canvas 上绘制一个涂层,用户可以通过触摸或滑动去刮开涂层。
- 答案显示:当涂层被刮开时,底部的内容将逐渐显现,达到一定清除比例(如 70%)时,完整答案会展示出来。
- 流畅的交互体验:需要确保刮刮卡效果在不同设备上都能平滑运行,避免卡顿。
实现步骤:从设计到交互
1. 初始化Canvas
首先,我们需要在小程序页面中添加一个 Canvas 元素。这个 Canvas 将作为刮刮卡的画布,用户的触摸事件将在这个区域内进行。
.wxml
<view class="area P1_4">
<view class="bd">
<view class="d_ct" wx:for="{{questions}}" wx:key="index">
<view class="cap3">{{item.question}}
<view wx:if="{{index==1}}" class="d s_tip">(至少列举三项)</view>
</view>
<view class="txt">
<!-- 添加canvas用于刮刮卡答案区域,显示答案 -->
<canvas id="scratchCanvas-{{index}}" type="2d" class="scratch-canvas" catchtouchstart="onTouchStart" catchtouchmove="onTouchMove" catchtouchend="onTouchEnd" data-index="{{index}}"></canvas>
<!-- 显示逐步揭开的答案 -->
<view wx:if="{{item.answerVisible}}" class="answer">
<text>{{item.answerVisible}}</text>
</view>
</view>
</view>
</view>
</view>
在这段代码中,使用了 catchtouchstart、catchtouchmove 和 catchtouchend 来绑定触摸事件处理方法。接下来我们将详细解释为什么选择 catch 事件绑定方式,而不是使用 bind。
catch事件绑定 vsbind事件绑定在微信小程序中,
catch和bind都是用于事件绑定的关键字,它们的主要区别在于事件的传播机制。
bind绑定事件:事件会触发冒泡,即触发元素的事件会向上传播,可能会影响到父级元素的触摸事件响应。catch绑定事件:事件不会冒泡,而是阻止事件的传播,确保事件只在当前元素上处理,而不会影响到父级元素。在实现刮刮卡效果时,我们希望触摸事件只在当前的 Canvas 元素上处理,并避免事件冒泡到其他父级元素,以免造成其他页面元素不必要的响应或样式问题。因此,使用
catch绑定事件是最佳选择。
2. 绘制涂层
我们将在 Canvas 上绘制一个矩形涂层,模拟刮刮卡的覆盖层。用户通过滑动触摸屏,涂层逐渐被清除,露出下方的内容。
.js
Page({
data: {
questions: [{
question: '1、元宵节是中国的哪个传统节日?',
answer: '答:元宵节是中国的农历正月十五的节日,又称上元节或灯节。',
clearedArea: 0,
isDrawing: false,
answerVisible: ''
},
{
question: '2、元宵节的主要习俗有哪些?',
answer: '答:元宵节的主要习俗包括赏花灯、吃元宵(或汤圆)、猜灯谜等。',
clearedArea: 0,
isDrawing: false,
answerVisible: ''
},
{
question: '3、“元宵”和“汤圆”有什么区别?',
answer: '答:元宵是“滚”出来的,以馅为基础,在糯米粉中滚成球形;而汤圆则是“包”出来的,类似于包饺子,将糯米粉团皮包上馅料后捏合成型。',
clearedArea: 0,
isDrawing: false,
answerVisible: '' // 用于逐步显示答案
},
],
imgSrc: 'https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_wx_logo.png'
},
onReady() {
this.data.questions.forEach((item, index) => {
this.initCanvas(index); // 初始化每个 canvas
});
},
initCanvas(index) {
const query = wx.createSelectorQuery().in(this);
query.select('#scratchCanvas-' + index)
.fields({
node: true,
size: true
})
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
// 设置canvas的宽高
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
// 填充背景色(遮罩)
ctx.fillStyle = "#f90";
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 在图片后面绘制文字
ctx.font = '20rpx Arial'; // 设置字体大小和字体
ctx.fillStyle = 'black'; // 设置文字颜色
ctx.textAlign = 'center'; // 设置文字居中
ctx.textBaseline = 'middle'; // 设置文字基线为中间
// 计算文字的位置
const textX = res[0].width / 2;
const textY = 10; // 文字位置在图片上方
// 绘制文字
ctx.fillText('呱呱查看答案', textX, textY);
// 图片对象
const image = canvas.createImage()
// 设置图片src
image.src = this.data.imgSrc
// 图片加载完成回调
image.onload = () => {
// 获取图片的原始宽高
const imgWidth = 64;
const imgHeight = 64;
const x = (res[0].width - imgWidth) / 2;
const Y = (res[0].height - imgHeight) / 2;
// 将图片绘制到 canvas 上
ctx.drawImage(image, x, Y)
}
// 初始化状态
this.setData({
[`questions[${index}].clearedArea`]: 0, // 清除的面积
[`questions[${index}].isDrawing`]: false, // 是否在绘制
[`questions[${index}].answerVisible`]: '' // 初始化答案可见部分
});
})
},
onTouchStart(e) {
const index = e.currentTarget.dataset.index;
this.setData({
[`questions[${index}].isDrawing`]: true,
});
this.clearCanvas(e, index);
},
onTouchMove(e) {
const index = e.currentTarget.dataset.index;
if (this.data.questions[index].isDrawing) {
this.clearCanvas(e, index);
}
},
onTouchEnd(e) {
const index = e.currentTarget.dataset.index;
this.setData({
[`questions[${index}].isDrawing`]: false,
});
},
// 清除遮罩并计算清除区域
clearCanvas(e, index) {
const query = wx.createSelectorQuery().in(this);
query.select(`#scratchCanvas-${index}`).fields({
node: true,
size: true
}).exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// 获取触摸点的坐标
const touch = e.touches[0];
const x = touch.x;
const y = touch.y;
const radius = 30;
// 清除圆形区域
ctx.globalCompositeOperation = 'destination-out'; // 设置清除模式为擦除
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI); // 画圆
ctx.fill();
// 更新清除的区域
this.updateClearedArea(index, x, y, canvas.width, canvas.height);
});
},
// 更新清除的区域,并计算清除比例
updateClearedArea(index, x, y, canvasWidth, canvasHeight) {
const clearedArea = this.data.questions[index].clearedArea || 0; // 获取当前清除区域的面积
const radius = 15 * wx.getSystemInfoSync().pixelRatio; // 清除区域的半径
const clearSize = Math.PI * Math.pow(radius, 2); // 计算每次清除的圆形区域面积
// 计算新的清除面积
const newClearedArea = clearedArea + clearSize;
// 计算清除比例
const totalArea = canvasWidth * canvasHeight;
const clearedPercentage = (newClearedArea / totalArea) * 100;
// 更新数据,保存新的清除区域面积
this.setData({
[`questions[${index}].clearedArea`]: newClearedArea,
});
// 逐步显示答案
this.updateAnswerVisible(index, clearedPercentage);
// 如果清除的区域超过70%,显示答案
if (clearedPercentage >= 70) {
this.showFullAnswer(index);
}
},
// 逐步显示答案
updateAnswerVisible(index, clearedPercentage) {
const question = this.data.questions[index];
const answer = question.answer;
// 根据清除比例更新显示的答案
const visibleLength = Math.floor((clearedPercentage / 100) * answer.length);
this.setData({
[`questions[${index}].answerVisible`]: answer.substring(0, visibleLength)
});
},
// 显示完整答案
showFullAnswer(index) {
this.setData({
[`questions[${index}].answerVisible`]: this.data.questions[index].answer // 完整显示答案
});
},
})
3. 响应用户触摸事件
通过监听用户的触摸事件,我们能够获取滑动的轨迹,实时更新涂层的透明度,模拟刮刮卡效果。
touchstart:开始触摸,启用涂层刮开模式。touchmove:触摸滑动,清除相应位置的涂层。touchend:触摸结束,停止清除涂层。
4. 展示效果:刮开涂层,揭示答案
通过上述代码,用户将能够在小程序中实现刮刮卡效果,并通过滑动手指刮开涂层,看到隐藏的答案。
关键词
- 微信小程序
- Canvas
- 刮刮卡
- 互动体验
- 动态涂层