溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JS如何實現canvas仿ps橡皮擦刮卡效果

發布時間:2021-11-22 12:27:18 來源:億速云 閱讀:312 作者:小新 欄目:開發技術
# 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>

六、性能優化與兼容

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女