本文带你从零搭建一个网页抽奖小游戏,用户通过鼠标或手指“刮奖”揭晓抽中内容。项目使用原生 HTML、CSS 和 JavaScript 编写,并结合 <canvas> 实现真实的刮刮卡交互体验。

女朋友是一名初中老师,平时上课喜欢通过“抽学生回答问题”来提高课堂互动。但每次她上课前,总要花时间写小纸条、剪成小块、再一张张揉成团,投进盒子里……搞得比出卷子还认真。

后来,她突然盯着我:“你不是前端的吗?给我搞个‘抽奖系统’,还能刮奖的那种,互动性强一点。”

就这样,作为一个本来只想躺平的程序员男友,我开始了“最强助教”的旅程。

于是有了这个项目 —— 一款可以上传题目或奖项,通过刮刮乐方式抽人答题的小程序,也许你用在班级抽学生、公司抽奖、聚会抽任务……都能派上用场!

🧩 项目介绍

  • ✅ 上传奖项清单(文本文件)

  • ✅ 点击“开始抽奖”生成随机奖项

  • ✅ 鼠标/手指滑动刮开 canvas 遮罩层

  • ✅ 刮开超过 10% 自动显示奖项内容

  • ✅ 支持查看历史奖项和一键重置

适合用于:抽奖活动页 / 前端学习 / 交互项目练手


🛠 技术栈

技术 用途
HTML5 页面结构
CSS3 + Tailwind 页面样式、美化
JavaScript 核心逻辑、交互绑定
Canvas API 刮刮乐涂层交互实现

📁 文件结构

scratch-lottery/
├── index.html   # 页面结构
├── style.css    # 页面样式
├── script.js    # 交互逻辑
└── 奖项清单.txt  # 示例奖项数据文件

📄 一、index.html(完整结构代码)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>刮刮乐抽奖游戏</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="stylesheet" href="style.css" />
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
  <div class="w-full max-w-2xl">
    <h1 class="text-4xl font-bold text-center mb-8 scratch-title">幸运刮刮乐抽奖</h1>

    <!-- 刮奖容器 -->
    <div class="scratch-container bg-white">
      <div id="scratchText" class="scratch-text">
        <p class="text-gray-500">请上传奖项清单文件并点击"开始抽奖"</p>
        <p class="text-sm text-gray-400 mt-2">(每行一个奖项)</p>
      </div>
      <canvas id="scratchCanvas" class="scratch-canvas"></canvas>
      <div id="scratchPercentage" class="scratch-percentage">已刮开: 0%</div>
    </div>

    <!-- 控制面板 -->
    <div class="scratch-controls flex flex-col gap-4 mt-4">
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1">上传奖项清单文件</label>
        <input type="file" id="fileInput" accept=".txt"
          class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" />
      </div>

      <div class="flex gap-4">
        <button id="drawButton" class="draw-button flex-1 px-4 py-3 text-white font-bold rounded-md shadow-md">
          开始抽奖
        </button>
        <button id="clearButton" class="clear-button flex-1 px-4 py-3 text-white font-bold rounded-md shadow-md">
          重置全部
        </button>
      </div>
    </div>

    <!-- 历史记录 -->
    <div id="prizeHistory" class="prize-history mt-6 hidden">
      <div class="prize-history-title">已抽奖项</div>
      <div id="prizeHistoryList" class="prize-history-list"></div>
    </div>
  </div>

  <script src="script.js"></script>
</body>
</html>

🎨 二、style.css(完整样式代码)

.scratch-container {
  position: relative;
  max-width: 600px;
  margin: 0 auto;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}

.scratch-text {
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  padding: 2rem;
  font-size: 1.2rem;
  line-height: 1.6;
  min-height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  flex-direction: column;
}

.scratch-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
  touch-action: none;
}

.scratch-percentage {
  position: absolute;
  bottom: 10px;
  right: 10px;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  padding: 5px 10px;
  border-radius: 20px;
  font-size: 0.8rem;
}

.draw-button,
.clear-button {
  transition: all 0.3s ease;
  font-weight: bold;
}

.draw-button {
  background: linear-gradient(to right, #4facfe, #00f2fe);
}

.clear-button {
  background: linear-gradient(to right, #ff416c, #ff4b2b);
}

.draw-button:hover,
.clear-button:hover {
  transform: scale(1.05);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.scratch-title {
  background: linear-gradient(to right, #ff8a00, #da1b60);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.prize-item {
  margin: 0.5rem 0;
  padding: 1rem;
  background-color: rgba(255, 255, 255, 0.8);
  border-radius: 8px;
  font-size: 1.8rem;
  font-weight: bold;
  animation: fadeIn 0.5s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.hidden-prize {
  color: transparent;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  background-color: rgba(0, 0, 0, 0.1);
  padding: 1rem;
  font-size: 1.8rem;
  font-weight: bold;
  border-radius: 8px;
}

.prize-history {
  background-color: white;
  padding: 1rem;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.prize-history-title {
  font-weight: bold;
  font-size: 1.3rem;
  color: #4a5568;
  margin-bottom: 0.5rem;
}

.prize-history-item {
  padding: 0.8rem;
  border-bottom: 1px solid #eee;
  font-size: 1.2rem;
}

.new-prize {
  background-color: #f0f9ff;
  color: #0369a1;
  font-weight: bold;
}

🧠 三、script.js(完整 JavaScript 功能)

document.addEventListener('DOMContentLoaded', () => {
  const canvas = document.getElementById('scratchCanvas');
  const ctx = canvas.getContext('2d');
  const scratchText = document.getElementById('scratchText');
  const fileInput = document.getElementById('fileInput');
  const drawButton = document.getElementById('drawButton');
  const clearButton = document.getElementById('clearButton');
  const scratchPercentage = document.getElementById('scratchPercentage');
  const prizeHistory = document.getElementById('prizeHistory');
  const prizeHistoryList = document.getElementById('prizeHistoryList');

  let allPrizes = [], availablePrizes = [];
  let currentPrize = '', previousPrize = '';
  let prizeHistoryItems = [];
  let isFirstDraw = true, hasShownCurrentPrize = false;
  let isDrawing = false, scratchedPixels = 0, totalPixels = 0, checkInterval;

  function initScratchCard() {
    const width = canvas.parentElement.offsetWidth;
    const height = canvas.parentElement.offsetHeight;
    canvas.width = width;
    canvas.height = height;
    ctx.fillStyle = '#888';
    ctx.fillRect(0, 0, width, height);
    ctx.globalCompositeOperation = 'destination-out';
    ctx.lineWidth = 30;
    ctx.lineCap = 'round';
    scratchedPixels = 0;
    totalPixels = width * height;
    hasShownCurrentPrize = false;
    if (checkInterval) clearInterval(checkInterval);
    checkInterval = setInterval(checkScratchedArea, 500);
  }

  function checkScratchedArea() {
    const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
    let transparentPixels = 0;
    for (let i = 3; i < pixels.length; i += 4) {
      if (pixels[i] === 0) transparentPixels++;
    }
    scratchedPixels = transparentPixels;
    updatePercentage();
  }

  function updatePercentage() {
    const percentage = Math.round((scratchedPixels / totalPixels) * 100);
    scratchPercentage.textContent = `已刮开: ${percentage}%`;
    if (percentage > 10 && currentPrize && !hasShownCurrentPrize) {
      showCurrentPrize();
      hasShownCurrentPrize = true;
    }
  }

  function showCurrentPrize() {
    scratchText.innerHTML = `<div class="prize-item">${currentPrize}</div>`;
  }

  function drawRandomPrize() {
    if (availablePrizes.length === 0) {
      scratchText.innerHTML = `<p class="text-gray-500">没有更多可抽奖项了</p>`;
      return false;
    }
    const index = Math.floor(Math.random() * availablePrizes.length);
    previousPrize = currentPrize;
    currentPrize = availablePrizes.splice(index, 1)[0];
    if (previousPrize) {
      prizeHistoryItems.push(previousPrize);
      showPrizeHistory();
    }
    scratchText.innerHTML = `<div class="hidden-prize">刮开查看结果</div>`;
    return true;
  }

  function showPrizeHistory() {
    prizeHistory.classList.remove('hidden');
    prizeHistoryList.innerHTML = '';
    prizeHistoryItems.forEach((item, idx) => {
      const div = document.createElement('div');
      div.className = 'prize-history-item' + (idx === prizeHistoryItems.length - 1 ? ' new-prize' : '');
      div.textContent = idx === prizeHistoryItems.length - 1 ? `最新: ${item}` : `${idx + 1}. ${item}`;
      prizeHistoryList.appendChild(div);
    });
  }

  function clearAll() {
    initScratchCard();
    availablePrizes = [...allPrizes];
    currentPrize = '';
    previousPrize = '';
    prizeHistoryItems = [];
    prizeHistory.classList.add('hidden');
    isFirstDraw = true;
    hasShownCurrentPrize = false;
    scratchText.innerHTML = `<p class="text-gray-500">已加载 ${allPrizes.length} 个奖项</p>`;
  }

  function startDraw() {
    if (availablePrizes.length === 0) return;
    initScratchCard();
    drawRandomPrize();
  }

  function getPosition(e) {
    const rect = canvas.getBoundingClientRect();
    return {
      x: (e.touches ? e.touches[0].clientX : e.clientX) - rect.left,
      y: (e.touches ? e.touches[0].clientY : e.clientY) - rect.top
    };
  }

  function startDrawing(e) {
    isDrawing = true;
    const pos = getPosition(e);
    ctx.beginPath();
    ctx.moveTo(pos.x, pos.y);
  }

  function draw(e) {
    if (!isDrawing) return;
    e.preventDefault();
    const pos = getPosition(e);
    ctx.lineTo(pos.x, pos.y);
    ctx.stroke();
  }

  function stopDrawing() {
    isDrawing = false;
  }

  fileInput.addEventListener('change', e => {
    const file = e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = function (e) {
      allPrizes = e.target.result.split('\n').map(x => x.trim()).filter(Boolean);
      clearAll();
    };
    reader.readAsText(file);
  });

  drawButton.addEventListener('click', startDraw);
  clearButton.addEventListener('click', clearAll);

  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', draw);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('mouseout', stopDrawing);
  canvas.addEventListener('touchstart', e => { e.preventDefault(); startDrawing(e); });
  canvas.addEventListener('touchmove', e => { e.preventDefault(); draw(e); });
  canvas.addEventListener('touchend', e => { e.preventDefault(); stopDrawing(); });

  initScratchCard();
});

❤️ 总结

你学会了如何用纯前端实现一个趣味互动的刮刮卡抽奖系统,涵盖:

  • Canvas 擦除技术

  • 奖项逻辑管理

  • 交互事件处理

  • 抽奖动画与进度判断

如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注 💬 评论支持!

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐