您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# 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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。