# JS如何實現canvas仿PS橡皮擦刮卡效果
## 一、效果概述與實現原理
### 1.1 什么是刮卡效果
刮卡效果是一種模擬現實世界中刮獎卡的交互體驗,用戶通過鼠標或觸摸操作"刮開"表層涂層,露出下方隱藏內容。在Web開發中,這種效果常見于營銷活動、游戲驗證等場景。
### 1.2 核心實現原理
Canvas實現刮卡效果主要依賴以下技術點:
- 使用`globalCompositeOperation`設置混合模式
- 通過鼠標/觸摸事件獲取繪制路徑
- 利用`clip`或`clearRect`實現擦除效果
- 性能優化處理大面積擦除情況
### 1.3 與傳統PS橡皮擦的異同
| 特性 | PS橡皮擦 | Canvas橡皮擦 |
|------------|-----------------------|-----------------------|
| 實現方式 | 像素級修改 | 路徑繪制+混合模式 |
| 精度控制 | 可精細調節 | 依賴繪制路徑密度 |
| 撤銷功能 | 完整歷史記錄 | 需手動實現狀態管理 |
| 性能影響 | 局部重繪 | 全圖層重繪 |
## 二、基礎實現步驟
### 2.1 初始化Canvas環境
```html
<canvas id="scratchCanvas" width="500" height="300"></canvas>
const canvas = document.getElementById('scratchCanvas');
const ctx = canvas.getContext('2d');
// 設置涂層和底圖
function initCanvas() {
// 繪制底層內容(獎品信息)
drawPrize();
// 繪制覆蓋層
drawCover();
}
function drawPrize() {
ctx.fillStyle = '#f5f5f5';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '24px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText('恭喜獲得一等獎!', canvas.width/2, canvas.height/2);
}
function drawCover() {
ctx.fillStyle = '#999';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '16px Arial';
ctx.fillStyle = '#fff';
ctx.fillText('刮開涂層查看獎品', canvas.width/2, canvas.height/2 + 30);
}
let isDrawing = false;
// 鼠標事件監聽
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
function startDrawing(e) {
isDrawing = true;
draw(e);
}
function draw(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 設置混合模式為destination-out
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.arc(x, y, 15, 0, Math.PI * 2);
ctx.fill();
}
function stopDrawing() {
isDrawing = false;
}
// 觸摸事件支持
canvas.addEventListener('touchstart', handleTouch);
canvas.addEventListener('touchmove', handleTouch);
function handleTouch(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent(
e.type === 'touchstart' ? 'mousedown' : 'mousemove',
{
clientX: touch.clientX,
clientY: touch.clientY
}
);
canvas.dispatchEvent(mouseEvent);
}
let lastX = 0;
let lastY = 0;
function draw(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 30;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
lastX = x;
lastY = y;
}
function calculateScratchedPercentage() {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
let transparentPixels = 0;
for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] === 0) {
transparentPixels++;
}
}
return (transparentPixels / (canvas.width * canvas.height)) * 100;
}
// 在draw函數中調用
if (calculateScratchedPercentage() > 60) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPrize();
}
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
const offscreenCtx = offscreenCanvas.getContext('2d');
// 初始化時繪制到離屏Canvas
function initCanvas() {
drawPrize();
offscreenCtx.fillStyle = '#999';
offscreenCtx.fillRect(0, 0, canvas.width, canvas.height);
}
// 修改draw函數
function draw(e) {
// ...獲取坐標邏輯不變
// 在離屏Canvas上繪制
offscreenCtx.globalCompositeOperation = 'destination-out';
offscreenCtx.beginPath();
offscreenCtx.arc(x, y, 15, 0, Math.PI * 2);
offscreenCtx.fill();
// 將離屏內容繪制到主Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offscreenCanvas, 0, 0);
}
function drawCover() {
// 創建紋理
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 20;
patternCanvas.height = 20;
const patternCtx = patternCanvas.getContext('2d');
patternCtx.fillStyle = '#888';
patternCtx.fillRect(0, 0, 20, 20);
patternCtx.fillStyle = '#aaa';
for (let i = 0; i < 20; i += 4) {
patternCtx.fillRect(i, 0, 2, 20);
}
const pattern = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 3 + 2;
this.speedX = Math.random() * 4 - 2;
this.speedY = Math.random() * 4 - 2;
this.alpha = 1;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.alpha -= 0.03;
}
draw() {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = '#999';
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
let particles = [];
function createParticles(x, y, count) {
for (let i = 0; i < count; i++) {
particles.push(new Particle(x, y));
}
}
function animateParticles() {
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw();
if (particles[i].alpha <= 0) {
particles.splice(i, 1);
i--;
}
}
if (particles.length > 0) {
requestAnimationFrame(animateParticles);
}
}
// 修改draw函數
function draw(e) {
// ...原有邏輯
// 添加粒子效果
createParticles(x, y, 5);
if (particles.length === 5) {
animateParticles();
}
}
<!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>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
#scratchCanvas {
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
border-radius: 8px;
cursor: crosshair;
}
.container {
text-align: center;
}
.info {
margin-top: 20px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<canvas id="scratchCanvas" width="400" height="200"></canvas>
<p class="info">按住鼠標拖動刮開涂層</p>
</div>
<script>
const canvas = document.getElementById('scratchCanvas');
const ctx = canvas.getContext('2d');
let isDrawing = false;
let lastX = 0;
let lastY = 0;
const particles = [];
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 3 + 2;
this.speedX = Math.random() * 4 - 2;
this.speedY = Math.random() * 4 - 2;
this.alpha = 1;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.alpha -= 0.03;
}
draw() {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = '#999';
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function initCanvas() {
// 繪制底層內容
ctx.fillStyle = '#f5f5f5';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '24px Arial';
ctx.fillStyle = '#e74c3c';
ctx.textAlign = 'center';
ctx.fillText('恭喜獲得50元優惠券!', canvas.width/2, canvas.height/2 - 10);
ctx.font = '16px Arial';
ctx.fillStyle = '#7f8c8d';
ctx.fillText('有效期至2023-12-31', canvas.width/2, canvas.height/2 + 20);
// 繪制覆蓋層
drawCover();
}
function drawCover() {
// 創建紋理
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 20;
patternCanvas.height = 20;
const patternCtx = patternCanvas.getContext('2d');
patternCtx.fillStyle = '#95a5a6';
patternCtx.fillRect(0, 0, 20, 20);
patternCtx.fillStyle = '#bdc3c7';
for (let i = 0; i < 20; i += 4) {
patternCtx.fillRect(i, 0, 2, 20);
}
const pattern = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '18px Arial';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText('刮開此處查看獎品', canvas.width/2, canvas.height/2);
}
function createParticles(x, y, count) {
for (let i = 0; i < count; i++) {
particles.push(new Particle(x, y));
}
}
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offscreenCanvas, 0, 0);
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw();
if (particles[i].alpha <= 0) {
particles.splice(i, 1);
i--;
}
}
if (particles.length > 0) {
requestAnimationFrame(animateParticles);
}
}
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
const offscreenCtx = offscreenCanvas.getContext('2d');
// 初始化離屏Canvas
function initOffscreenCanvas() {
offscreenCtx.fillStyle = '#f5f5f5';
offscreenCtx.fillRect(0, 0, canvas.width, canvas.height);
offscreenCtx.font = '24px Arial';
offscreenCtx.fillStyle = '#e74c3c';
offscreenCtx.textAlign = 'center';
offscreenCtx.fillText('恭喜獲得50元優惠券!', canvas.width/2, canvas.height/2 - 10);
offscreenCtx.font = '16px Arial';
offscreenCtx.fillStyle = '#7f8c8d';
offscreenCtx.fillText('有效期至2023-12-31', canvas.width/2, canvas.height/2 + 20);
drawCover();
}
function startDrawing(e) {
isDrawing = true;
const rect = canvas.getBoundingClientRect();
lastX = e.clientX - rect.left;
lastY = e.clientY - rect.top;
}
function draw(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
offscreenCtx.globalCompositeOperation = 'destination-out';
offscreenCtx.lineWidth = 20;
offscreenCtx.lineCap = 'round';
offscreenCtx.lineJoin = 'round';
offscreenCtx.beginPath();
offscreenCtx.moveTo(lastX, lastY);
offscreenCtx.lineTo(x, y);
offscreenCtx.stroke();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offscreenCanvas, 0, 0);
// 添加粒子效果
createParticles(x, y, 3);
if (particles.length === 3) {
animateParticles();
}
lastX = x;
lastY = y;
// 檢查刮開比例
if (calculateScratchedPercentage() > 60) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
offscreenCtx.fillStyle = '#f5f5f5';
offscreenCtx.fillRect(0, 0, canvas.width, canvas.height);
offscreenCtx.font = '24px Arial';
offscreenCtx.fillStyle = '#e74c3c';
offscreenCtx.textAlign = 'center';
offscreenCtx.fillText('恭喜獲得50元優惠券!', canvas.width/2, canvas.height/2 - 10);
offscreenCtx.font = '16px Arial';
offscreenCtx.fillStyle = '#7f8c8d';
offscreenCtx.fillText('有效期至2023-12-31', canvas.width/2, canvas.height/2 + 20);
ctx.drawImage(offscreenCanvas, 0, 0);
}
}
function calculateScratchedPercentage() {
const imageData = offscreenCtx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
let transparentPixels = 0;
for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] === 0) {
transparentPixels++;
}
}
return (transparentPixels / (canvas.width * canvas.height)) * 100;
}
function stopDrawing() {
isDrawing = false;
}
// 觸摸事件支持
canvas.addEventListener('touchstart', function(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
});
canvas.addEventListener('touchmove', function(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
});
// 鼠標事件監聽
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
// 初始化
initOffscreenCanvas();
initCanvas();
</script>
</body>
</html>
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。