刮刮乐这玩意儿到底咋实现的?
其实这玩意儿原理简单到离谱 —— 就像你在奶茶杯上贴了层便利贴,底层是 “再来一杯”(中奖内容),上层是便利贴(遮挡层);你用指甲刮便利贴(鼠标 / 触摸),刮掉的地方就露出底下的字,Canvas 只是把 “便利贴” 换成了像素,把 “指甲” 换成了代码而已~
实现原理:
直接抄作业!完整可运行代码(复制 = 拥有)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 刮刮乐</title>
<style>
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #f5f5f5;
font-family: 'Arial', sans-serif;
}
h1 {
color: #333;
margin-bottom: 20px;
font-size: 24px;
}
.scratch-container {
position: relative;
width: 300px;
height: 150px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
border-radius: 8px;
overflow: hidden;
}
/* 底层:中奖内容画布 */
#prizeCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
}
/* 上层:遮挡层画布(用户刮的层) */
#scratchCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
/* 禁止选择,避免刮动时选中画布 */
user-select: none;
-webkit-user-select: none;
}
.tip {
margin-top: 15px;
color: #666;
font-size: 14px;
}
button {
margin-top: 20px;
padding: 8px 20px;
border: none;
border-radius: 4px;
background: #42b983;
color: #fff;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #359469;
}
</style>
</head>
<body>
<h1>Canvas 刮刮乐</h1>
<div class="scratch-container">
<!– 底层:中奖内容 –>
<canvas id="prizeCanvas" width="300" height="150"></canvas>
<!– 上层:遮挡层 –>
<canvas id="scratchCanvas" width="300" height="150"></canvas>
</div>
<p class="tip">按住鼠标/手指滑动刮开</p>
<button id="resetBtn">重置刮刮乐</button>
<script>
// 1. 获取画布和上下文
const prizeCanvas = document.getElementById('prizeCanvas');
const scratchCanvas = document.getElementById('scratchCanvas');
const prizeCtx = prizeCanvas.getContext('2d');
const scratchCtx = scratchCanvas.getContext('2d');
const resetBtn = document.getElementById('resetBtn');
// 2. 初始化底层中奖内容
function initPrize() {
// 清空底层画布
prizeCtx.clearRect(0, 0, prizeCanvas.width, prizeCanvas.height);
// 设置中奖文字样式
prizeCtx.font = 'bold 28px Arial';
prizeCtx.fillStyle = '#e74c3c';
prizeCtx.textAlign = 'center';
prizeCtx.textBaseline = 'middle';
// 绘制中奖文字(可替换为图片:prizeCtx.drawImage(img, 0, 0))
prizeCtx.fillText('恭喜中奖 100元', prizeCanvas.width / 2, prizeCanvas.height / 2);
}
// 3. 初始化上层遮挡层
function initScratch() {
// 重置合成模式为默认值,确保背景填充正常
scratchCtx.globalCompositeOperation = 'source-over';
// 清空遮挡层
scratchCtx.clearRect(0, 0, scratchCanvas.width, scratchCanvas.height);
// 设置遮挡层背景(灰色,可替换为图片)
scratchCtx.fillStyle = '#95a5a6';
scratchCtx.fillRect(0, 0, scratchCanvas.width, scratchCanvas.height);
// 关键:设置合成模式,后续绘制会清除原有像素(实现“擦除”效果)
scratchCtx.globalCompositeOperation = 'destination-out';
// 设置擦除笔触大小(刮开的痕迹宽度)
scratchCtx.lineWidth = 20;
scratchCtx.lineCap = 'round'; // 笔触圆润
scratchCtx.lineJoin = 'round';
}
// 4. 交互逻辑:监听鼠标/触摸事件
let isScratching = false; // 是否正在刮动
// 鼠标事件
scratchCanvas.addEventListener('mousedown', startScratch);
scratchCanvas.addEventListener('mousemove', scratch);
scratchCanvas.addEventListener('mouseup', endScratch);
scratchCanvas.addEventListener('mouseleave', endScratch);
// 触摸事件(适配移动端)
scratchCanvas.addEventListener('touchstart', (e) => {
e.preventDefault(); // 阻止默认滚动行为
startScratch(e.touches[0]);
});
scratchCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
scratch(e.touches[0]);
});
scratchCanvas.addEventListener('touchend', endScratch);
scratchCanvas.addEventListener('touchcancel', endScratch);
// 开始刮动:记录起始位置
function startScratch(e) {
isScratching = true;
const { x, y } = getCanvasPosition(e);
scratchCtx.beginPath();
scratchCtx.moveTo(x, y);
}
// 刮动中:绘制轨迹(清除像素)
function scratch(e) {
if (!isScratching) return;
const { x, y } = getCanvasPosition(e);
scratchCtx.lineTo(x, y);
scratchCtx.stroke(); // 绘制线条,由于合成模式为 destination-out,会清除对应像素
}
// 结束刮动
function endScratch() {
isScratching = false;
// 可选:检测刮开面积,达到阈值自动显示全部内容
checkScratchProgress();
}
// 计算鼠标/触摸点在画布上的相对位置(解决 canvas 宽高缩放问题)
function getCanvasPosition(e) {
const rect = scratchCanvas.getBoundingClientRect();
return {
x: (e.clientX – rect.left) * (scratchCanvas.width / rect.width),
y: (e.clientY – rect.top) * (scratchCanvas.height / rect.height)
};
}
// 检测刮开进度(可选功能)
function checkScratchProgress() {
const imageData = scratchCtx.getImageData(0, 0, scratchCanvas.width, scratchCanvas.height);
const pixels = imageData.data;
let transparentPixels = 0; // 透明像素数(已刮开的)
const totalPixels = pixels.length / 4; // 总像素数(RGBA 四个通道为一个像素)
// 遍历所有像素,统计 alpha 通道为 0 的像素(透明)
for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] === 0) {
transparentPixels++;
}
}
// 刮开面积超过 70% 自动显示全部
if (transparentPixels / totalPixels > 0.7) {
scratchCtx.clearRect(0, 0, scratchCanvas.width, scratchCanvas.height);
}
}
// 重置刮刮乐
resetBtn.addEventListener('click', () => {
initScratch();
});
// 页面加载时初始化
window.addEventListener('load', () => {
initPrize();
initScratch();
});
</script>
</body>
</html>
为啥这代码这么牛?(亮点速看)
由于 canvas 的 width/height 属性(绘图分辨率)和 CSS 宽高(显示尺寸)可能不一致,必须通过 getBoundingClientRect() 计算鼠标 / 触摸点的相对位置,否则刮动轨迹会偏移。
这是实现 “擦除” 的核心!默认合成模式是 source-over(新内容覆盖旧内容),而 destination-out 会让新绘制的内容 “清除” 底层内容(像素变透明),比 clearRect 更灵活(支持任意形状的擦除轨迹)。
必须监听 touchstart/touchmove/touchend 事件,并通过 e.touches[0] 获取触摸点坐标,同时阻止默认滚动行为(e.preventDefault()),否则移动端会出现滚动冲突。
1)笔触大小 lineWidth 不宜过大(建议 15-30),避免绘制压力;
2)检测刮开进度时,遍历像素是耗时操作,可降低检测频率(如每 500ms 检测一次)。
1)底层内容替换:将文字改为图片(prizeCtx.drawImage(img, 0, 0));
2)遮挡层替换:用图片作为遮挡层(scratchCtx.drawImage(coverImg, 0, 0));
3)随机中奖内容:通过数组随机选择中奖文案 / 图片;
4)动画效果:刮开进度达标时添加淡入动画。


