JS如何实现canvas仿ps橡皮擦刮卡效果

发布时间:2021-11-22 12:27:18 作者:小新
来源:亿速云 阅读:295
# 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);
}

2.2 实现擦除功能

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;
}

2.3 触摸屏适配

// 触摸事件支持
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);
}

三、高级优化技巧

3.1 使用路径绘制提高性能

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;
}

3.2 添加刮卡百分比计算

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();
}

3.3 使用离屏Canvas优化

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);
}

四、视觉效果增强

4.1 添加纹理效果

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);
}

4.2 实现粒子飞溅效果

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>

六、性能优化与兼容

推荐阅读:
  1. js实现选项卡效果
  2. js仿360开机效果

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

js canvas

上一篇:C语言如何实现财务管理系统

下一篇:c语言怎么实现含递归清场版扫雷游戏

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》