👻Uniapp-使用canvas实现"刮刮乐"组件👻
哈喽,各位好呀,今天小编来分享一篇关于 Uniapp
内容的实战类文章,如果你觉得还不错,希望给小编点个赞呗(✪ω✪)。
写在开头
然后,在项目中新建 ScrapingHappy.vue
组件,在首页 index.vue
文件中引入:
<template>
<view class="content">
刮刮乐-中大奖
<view class="box">
<scraping-happy ref="scrapingHappy" />
</view>
<button @tap="again">再来一次</button>
</view>
</template>
<script>
import ScrapingHappy from './ScrapingHappy.vue';
export default {
components: { ScrapingHappy },
methods: {
again() {
this.$refs.scrapingHappy.reset();
}
}
};
</script>
<style>
.content {
box-sizing: border-box;
padding: 24rpx;
}
.box {
width: 100%;
height: 200rpx;
margin: 20rpx auto;
}
</style>
接下来,我们就专心编码 ScrapingHappy.vue
组件就行啦。
组件结构与样式
"刮刮乐" 组件的实现主要核心在于对逻辑的编写,其 HTML
结构与 CSS
样式相对比较简单,我们直接来看看就行。
<template>
<view class="scraping-happy" id="container">
<canvas
canvas-id="scraping-happy"
class="scraping__canvas"
:disable-scroll="true"
/>
<cover-view class="scraping__view">
恭喜你,中了40万
</cover-view>
</view>
</template>
<style>
.scraping-happy {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.scraping__canvas {
position: absolute;
z-index: 10;
width: 100%;
height: 100%;
}
.scraping__view {
position: absolute;
z-index: 1;
color: #f29100;
font-size: 20px;
font-weight: bold;
}
</style>
该功能原理大概就是将 canvas
定位在上方,用户刮开 canvas
即可看到底下的中奖信息,这个信息可以是文案、图片、GIF
等等,这就看你实际的业务需求了💀。
绘制涂层
接下来第一步,我们来实现刮刮乐的涂层,其实也就是给 canvas
上个色🎨。
<script>
let ctx = null;
/** @name 容错值,解决部分机型涂层没有覆盖满,出现边框的情况,主要原因是由于像素尺寸不同导致的,应尽可能让width与height保持整数 **/
const TOLERANT = 3;
/**
* @name 涂层配置默认值
* @property { string } color 涂层颜色
* @property { number } drawSize 清除涂层的画笔大小
*/
const MASK = {
color: '#DDDDDD',
drawSize: 20,
}
export default {
props: {
/** @name 涂层设置 **/
mask: {
type: [String, Object],
},
},
data() {
return {
width: 0,
height: 0,
}
},
computed: {
maskSetting() {
return {
...MASK,
...(typeof this.mask === 'object' ? this.mask : { color: this.mask }),
}
},
},
mounted() {
// 获取canvas画布实例
ctx = uni.createCanvasContext('scraping-happy', this);
this.init();
},
methods: {
/** @name 初始化 **/
init() {
const query = uni.createSelectorQuery().in(this);
query
.select('#container')
.boundingClientRect(({ width, height }) => {
this.width = width;
this.height = height;
this.initCanvas();
})
.exec();
},
/** @name 初始化canvas状态 **/
initCanvas() {
const { width, height } = this;
// 清空矩形内容
ctx.clearRect(0, 0, width, height);
// 设置画笔颜色
ctx.setFillStyle(this.maskSetting.color);
// 绘制矩形
ctx.fillRect(0, 0, width + TOLERANT, height + TOLERANT);
// 绘制到canvas身上
ctx.draw();
},
}
}
</script>
由于可能需要对涂层进行一些自定义的配置,所以把涂层的颜色与消除涂层"画笔"大小两个属性定义成 props
,提供给外部自定义配置😯。
其他逻辑主要是对 canvas API
的使用,对其还不熟悉的小伙伴们快去加强加强。传送门
绘制水印
绘制完涂层后,Em......有点单调😕,正常我们看到的刮刮乐涂层上应该还有一些水印啥的,这样看起来会好看一点,就像这样:

Em...好,那我们也给它整上,这都不是事😊。
<script>
...
/** @name 水印配置默认值 **/
const WATERMARK = {
text: '刮一刮',
fontSize: 14,
color: '#C5C5C5',
}
export default {
props: {
...,
/** @name 水印设置 **/
watermark: {
type: [String, Object],
},
},
...,
computed: {
...,
watermarkSetting() {
return {
...WATERMARK,
...(typeof this.watermark === 'object' ? this.watermark : { text: this.watermark }),
};
},
},
...,
methods: {
...,
/** @name 初始化canvas状态 **/
initCanvas() {
const { width, height } = this;
ctx.clearRect(0, 0, width, height);
ctx.setFillStyle(this.maskSetting.color);
ctx.fillRect(0, 0, width + TOLERANT, height + TOLERANT);
// 绘制水印
this.drawWatermark();
ctx.draw();
},
/** @name 绘制水印 **/
drawWatermark() {
if (!this.watermarkSetting.text) return;
// 保存当前的绘图上下文
ctx.save();
// 旋转
ctx.rotate((-10 * Math.PI) / 180);
// 水印具体绘制过程
const { width, height } = this;
const watermarkWidth = this.watermarkSetting.text.length * this.watermarkSetting.fontSize;
let x = 0;
let y = 0;
let i = 0;
while ((x <= width * 5 || y <= height * 5) && i < 300) {
ctx.setFillStyle(this.watermarkSetting.color);
ctx.setFontSize(this.watermarkSetting.fontSize);
ctx.fillText(this.watermarkSetting.text, x, y);
x += watermarkWidth + watermarkWidth * 1.6;
if (x > width && y <= height) {
x = -Math.random() * 100;
y += this.watermarkSetting.fontSize * 3;
}
i++;
}
ctx.restore();
},
}
}
</script>
同样,水印的一些属性我们也定义成 props
提供给外部自定义。
绘制水印主要使用 CanvasContext.fillText 方法,它接收四个参数。
而 x
与 y
参数是我们比较要关注的,如何在 canvas
画布上正确计算出每个水印文案的位置是关键,不同的计算逻辑绘制出来的水印位置也不同,网上有很多现成的方法,你可以看看哪种更加合适你👻。
绘制标题
现在整体好看一些了,咱们顺手再来把中间的标题给绘制上,同样的配方,同样的操作。
<script>
...
/** @name 标题配置默认值 **/
const TITLE = {
text: '刮一刮',
fontSize: 26,
color: '#888888',
}
export default {
props: {
...,
/** @name 提示文字 **/
title: {
type: [String, Object],
},
},
...,
computed: {
...,
titleSetting() {
return {
...TITLE,
...(typeof this.title === 'object' ? this.title : { text: this.title }),
};
}
},
...,
methods: {
...,
/** @name 初始化canvas状态 **/
initCanvas() {
const { width, height } = this;
ctx.clearRect(0, 0, width, height);
ctx.setFillStyle(this.maskSetting.color);
ctx.fillRect(0, 0, width + TOLERANT, height + TOLERANT);
this.drawWatermark();
// 绘制提示文字
this.drawTitle();
ctx.draw();
},
/** @name 绘制提示文字 **/
drawTitle() {
if (!this.titleSetting.text) return;
ctx.setTextAlign('center');
ctx.setTextBaseline('middle');
ctx.setFillStyle(this.titleSetting.color);
ctx.setFontSize(this.titleSetting.fontSize);
ctx.fillText(this.titleSetting.text, this.width / 2, this.height / 2);
},
}
}
</script>
这都不是事😁。
实现刮一刮交互
接下来,要实现刮刮乐组件的核心功能 "刮" 的交互逻辑,咱们先看代码再做解释:
<template>
<view class="scraping-happy" id="container">
<canvas
...
@touchstart="touchstart"
@touchmove="touchmove"
@touchend="touchend"
/>
...
</view>
</template>
<script>
...
export default {
...,
data() {
return {
...,
touchX: 0,
touchY: 0
}
},
...,
methods: {
...,
/** @name 触摸事件 **/
touchstart(e) {
this.touchX = e.touches[0].x;
this.touchY = e.touches[0].y;
},
touchmove(e) {
// 把画笔到画布中的指定点
ctx.moveTo(this.touchX, this.touchY);
// 清除涂层
ctx.clearRect(this.touchX, this.touchY, this.maskSetting.drawSize, this.maskSetting.drawSize);
ctx.draw(true);
// 记录移动点位
this.touchX = e.touches[0].x;
this.touchY = e.touches[0].y;
},
touchend() {
},
}
}
</script>
上面我们绑定了 touchstart
、 touchmove
、 touchend
三个事件,而其中实现涂层的清除是利用了 CanvasContext.clearRect 方法,该方法能清除画布上在该矩形区域内的内容。
实现绘制过半自动刮完
到这里其实刮刮乐的功能基本已经实现完了,但是我们再来实现一个功能,就是当用户刮了一半,或者刮了百分之多少的时候,我们就自动将整个涂层全部刮完展开,这样能提升一下用户的体验。
那么,具体应该怎么做呢?怎么来判断用户当前刮了多少了呢?
同样,我们先看代码:
<script>
...
export default {
props: {
...,
/** @name 刮开百分之多少直接消除图层,为0的时候不消除 **/
percentage: {
type: Number,
default: 50,
},
},
...,
methods: {
...,
async touchend() {
if (this.percentage > 0) {
const clearPercent = await this.getClearMaskPercent();
// 清除的大小 大于 限制的大小 即可全部自动涂完
if (clearPercent >= this.percentage) {
ctx.moveTo(0, 0);
ctx.clearRect(0, 0, this.width, this.height);
ctx.stroke();
ctx.draw(true);
}
}
},
/** @name 计算被清除的涂层百分比 **/
getClearMaskPercent() {
return new Promise(resolve => {
uni.canvasGetImageData({
canvasId: 'scraping-happy',
x: 0,
y: 0,
width: this.width,
height: this.height,
success: res => {
// 区域内所有点的像素信息,它是一个数组,数组中每 "4" 项表示一个点的 rgba 值
const allPointPixels = res.data;
// 储存被清除的点-点的透明度
const clearPoint = [];
// 取透明度来判断,如果透明度值小于一半,则判断为该点已经被清除
for (let i = 0; i < allPointPixels.length; i += 4) {
if (allPointPixels[i + 3] < 128) {
clearPoint.push(allPointPixels[i + 3]);
}
}
// 已被清除的百分比 = 清除的点 / 全部的点
const percent = (
(clearPoint.length / (allPointPixels.length / 4)) *
100
).toFixed(2);
resolve(percent);
},
fail: e => {
console.log('canvasGetImageData', e);
},
}, this);
});
},
}
}
</script>
效果如下:
要实现这个功能,核心点就是要知道用户当前刮了多少,知道了这个"值",我们就能一往无前,实现该功能,那具体要怎么计算得到呢?
这里其实是利用了 uni.canvasGetImageData 这个API
,以下是官方对它的描述:
初看有点懵😣,具体使用了一下(涂层铺满),它返回来的结果是这样子的:
仔细分析后发现,返回来的 data
数组中,每 "4" 项,则表示一个像素点的 rgba
值,整个数组就是整个 canvas(涂层)
所有点的 rgab
值。
当小编刮除了左上角的一些涂层,它返回的是这样子的:
噢,原来如此😲,好像懂了懂了。
只要我们计算一下有多少个点被刮除,再除以数组中所有点,不就能得到我们想要的了😀。
当前刮除的值 = 被刮除的点 / 所有点 * 100
那我们是不是判断一个点的 "4" 项都等于 0
就算这个点刮除了?
当然不是,当涂层是黑色的时候,则它的 rgba
值为 rgba(0, 0, 0, 255)
,这....😕。
所以代表颜色的前三项我们是用不上了,我们只能从透明度入手,一般来说透明度小于 0.5
的时候,我们基本就能看到背后的东西了。
用一个定位蒙层遮住底下的文案,蒙层设置不同的透明度:
所以,知道上面代码中为什么 if (allPointPixels[i + 3] < 128) {
要取 128
了没? 取的就是透明度 0.5
🤠。
当然,你也可以根据需求自己适当调整。
剩下的就不多说啦,自己看看就行,完结,溜了溜了。
完整源码
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。 老样子,点赞+评论=你会了,收藏=你精通了。
转载自:https://juejin.cn/post/7269296778226352167