Web 刮刮乐抽奖游戏实现:HTML + Canvas + JavaScript 全解析(附完整源码)
本项目是一款基于 HTML、CSS 和 JavaScript 开发的 网页刮刮乐抽奖互动程序,灵感源自现实中的课堂提问、活动抽奖等场景。通过上传一个奖项(或题目)清单,用户可以模拟刮刮卡的方式,随机抽取一个奖项,并通过“刮开”操作来揭晓内容,增强参与者的趣味与互动感。
·
本文带你从零搭建一个网页抽奖小游戏,用户通过鼠标或手指“刮奖”揭晓抽中内容。项目使用原生 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 擦除技术
-
奖项逻辑管理
-
交互事件处理
-
抽奖动画与进度判断
如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注 💬 评论支持!
更多推荐
所有评论(0)