用Vue实现根据奖品自适应宽高的刮刮乐、刮奖组件
简介
一个使用Vue实现的刮刮乐,刮奖组件,canvas宽高根据用户设置的奖品样式自适应宽高
效果
相关配置
props
组件调用时, 支持传入以下 props
:
参数 | 说明 | 类型 | 默认值 | 备注 |
---|---|---|---|---|
maskColor | 刮奖图层背景颜色 | String | '#cccccc' | 当imageUrl 非空时会被覆盖 |
text | 刮奖图层文字 | String | '刮一刮' | 当imageUrl 非空时会被覆盖 |
fillStyle | 刮奖图层文字颜色 | String | '#000000' | - |
font | 刮奖图层字体 | String | '18px Arial' | |
imageUrl | 刮奖图层使用图片 | String | - | 会覆盖掉刮奖图层文字 |
radius | 刮奖图层圆角 | Number | 0 | - |
scratchRadius | 刮奖半径 | Number | 10 | 刮奖半径 |
scratchPercent | 刮开占比 | Number | 80 | 当刮奖图层挂到scratchPercent 百分比时,移除刮奖图层 |
events
组件调用时, 会触发以下事件,可供监听回调:
事件 | 说明 | 备注 |
---|---|---|
scratchStart | 开始刮卡时 | 手指触控或鼠标按下 |
scratchEnd | 刮卡结束时 | 手指离开或鼠标点击抬起时 |
scratchAll | 刮光全部时 | 刮刮卡被刮完时触发 |
使用方式
<template>
<StratchCard maskColor="skyblue" fillStyle="red" font="30px 微软雅黑" text="刮一刮文字" :imageUrl="imageUrl" :radius="5"
:scratchRadius="20" :scratchPercent="80" @scratchStart="scratchStart" @scratchEnd="scratchEnd"
@scratchAll="scratchAll">
<!-- 自定义奖品内容插槽 -->
<div class="prize">我的奖品</div>
</StratchCard>
</template>
核心代码
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, defineEmits } from "vue";
/*
省略掉了 defineProps
*/
const emit = defineEmits(['scratchStart', 'scratchEnd', 'scratchAll'])
const ctx = ref<CanvasRenderingContext2D | null | undefined>(null);
const slot = ref<HTMLInputElement | null>(null);
const canvas = ref<HTMLCanvasElement | null>(null);
const width = ref(0);
const height = ref(0);
const isScratching = ref(false);
const init = () => {
initCanvas();
nextTick(() => {
initDraw();
bindEvents();
})
};
const initCanvas = () => {
width.value = slot.value?.offsetWidth || 0;
height.value = slot.value?.offsetHeight || 0;
};
//初始化绘制
const initDraw = () => {
ctx.value = canvas.value?.getContext("2d");
if (ctx.value) {
ctx.value.globalAlpha = 1;
ctx.value.fillStyle = props.maskColor;
//绘制圆角
if (props.radius) {
const radius = props.radius;
//使用clip剪切出圆角
ctx.value.beginPath();
//左上角圆角
ctx.value.moveTo(radius, 0);
ctx.value.arcTo(0, 0, 0, radius, radius);
//左下角圆角
ctx.value.lineTo(0, height.value - radius);
ctx.value.arcTo(0, height.value, radius, height.value, radius);
//右下角圆角
ctx.value.lineTo(width.value - radius, height.value);
ctx.value.arcTo(
width.value,
height.value,
width.value,
height.value - radius,
radius
);
//右上角圆角
ctx.value.lineTo(width.value, radius);
ctx.value.arcTo(width.value, 0, width.value - radius, 0, radius);
ctx.value.closePath();
ctx.value.clip();
}
ctx.value.fillRect(0, 0, width.value, height.value);
//若没有图片绘制文本
if (!props.imageUrl) {
// 文本
ctx.value.fillStyle = props.fillStyle;
ctx.value.font = props.font;
// ctx.textAlign = "center";
// ctx.textBaseline = "middle";
// 绘制文字
let text = props.text;
let textWidth = ctx.value.measureText(text).width;
let x = width.value / 2 - textWidth / 2;
let y = height.value / 2 + 6;
ctx.value.fillText(text, x, y);
}
//绘制图片
if (props.imageUrl) {
// 创建 Image 对象
const img = new Image();
// 设置图片URL
img.src = props.imageUrl;
// 监听图片加载完成事件
img.onload = () => {
// 在 Canvas 上绘制图片
ctx.value?.drawImage(img, 0, 0, width.value, height.value);
};
}
}
}
//绑定事件
const bindEvents = () => {
if (!canvas.value) {
return;
}
// pc
canvas.value.addEventListener("mousedown", (e) => {
isScratching.value = true;
drawArc(e);
});
canvas.value.addEventListener("mousemove", (e) => {
if (isScratching.value == true) {
drawArc(e);
}
});
canvas.value.addEventListener("mouseup", () => {
isScratching.value = false;
calcArea();
});
// wap
canvas.value.addEventListener("touchstart", (e) => {
emit("scratchStart");
isScratching.value = true;
drawArc(e);
});
canvas.value.addEventListener("touchmove", (e) => {
if (isScratching.value == true) {
drawArc(e);
}
});
canvas.value.addEventListener("touchend", () => {
isScratching.value = false;
emit("scratchEnd");
calcArea();
});
}
// 刮开区域
const drawArc = (e: MouseEvent | TouchEvent) => {
if (!canvas.value || !ctx.value) {
return;
}
const canvasPos = canvas.value.getBoundingClientRect();
const pageScrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
const pageScrollLeft =
document.documentElement.scrollLeft || document.body.scrollLeft;
let mouseX = 0;
let mouseY = 0;
if (e instanceof MouseEvent) {
mouseX = e.pageX - canvasPos.left - pageScrollLeft;
mouseY = e.pageY - canvasPos.top - pageScrollTop;
} else if (e instanceof TouchEvent) {
mouseX = e.targetTouches[0].pageX - canvasPos.left - pageScrollLeft;
mouseY = e.targetTouches[0].pageY - canvasPos.top - pageScrollTop;
} else {
return;
}
ctx.value.globalCompositeOperation = "destination-out";
ctx.value.beginPath();
ctx.value.fillStyle = "white";
ctx.value.moveTo(mouseX, mouseY);
ctx.value.arc(mouseX, mouseY, props.scratchRadius, 0, 3 * Math.PI);
ctx.value.fill();
}
// 计算区域
const calcArea = () => {
if (!canvas.value || !ctx.value) {
return;
}
let myImg = ctx.value.getImageData(0, 0, width.value, height.value);
let num = 0;
let max = myImg.data.length / 4;
for (let i = 0; i < myImg.data.length; i += 4) {
if (myImg.data[i + 3] == 0) {
num++;
}
}
if (num >= max * (props.scratchPercent / 100)) {
canvas.value.remove();
emit("scratchAll");
}
}
//重绘
const onResize = () => {
ctx.value = null;
init();
}
onMounted(() => {
init();
window.addEventListener("resize", onResize);
})
onUnmounted(() => {
window.removeEventListener("resize", onResize);
})
<template>
<div class="box">
<div ref="slot" class="slot" :style="{ borderRadius: radius + 'px' }">
<slot></slot>
</div>
<canvas v-if="width && height" class="canvas" ref="canvas" :width="width" :height="height"></canvas>
</div>
</template>
该组件的模板包括一个 div
元素,包含两个子元素:一个用于显示遮罩层和文字(如果没有图片),另一个用于绘制刮开的区域。在 mounted
钩子函数中调用了 init
方法,绑定了窗口大小改变时重新绘制的事件。在 beforeDestroy
钩子函数中解绑了事件。
在 init
方法中,首先调用了 initCanvas
方法获取 canvas 的宽度和高度,然后在 $nextTick
回调函数中调用了 initDraw
方法初始化绘制,最后调用了 bindEvents
方法绑定事件。
在 initDraw
方法中,首先获取了 canvas 的 2D 上下文,并设置了全局透明度为 1。如果有圆角半径,则使用 clip
方法剪切出圆角,并用遮罩层填充整个 canvas。如果没有图片,则使用 fillText
方法绘制文字。如果有图片,则创建一个 Image
对象,设置它的 src
属性,监听 onload
事件,在图片加载完成后调用 drawImage
方法绘制图片。最后调用 bindEvents
方法绑定事件。
在 bindEvents
方法中,分别绑定了鼠标和触摸事件,其中包括 mousedown
、mousemove
、mouseup
以及 touchstart
、touchmove
、touchend
事件。在 drawArc
方法中,根据鼠标或触摸事件的坐标,以及刮刮卡刮开的半径,使用 arc
方法绘制一个圆形路径,并使用 stroke 方法将路径描边。在 calcArea
方法中,通过遍历像素点的数据,计算刮开的面积,并触发scratchEnd
事件。
总体而言,这个组件实现了一个简单的刮刮卡游戏,支持自定义刮开面积、显示的图片、遮罩层颜色、刮开后的颜色、文字内容和样式等属性,并且支持在窗口大小改变时重新绘制 canvas。组件的核心是使用 canvas 绘制刮刮卡的遮罩层和刮开的区域,并且绑定鼠标和触摸事件来实现用户刮开的效果。
完整代码&仓库地址
转载自:https://juejin.cn/post/7215106271145672761