实现的方案
- CSS3 动画 +JavaScript
- Canvas++JavaScript
考虑到性能问题,决定采用 Canvas 方案进行实现。
关键实现类
class LuckyWheel {
constructor(config) {
this.canvas = config.canvas;
this.ctx = this.canvas.getContext('2d');
this.resultDisplay = config.resultDisplay;
this.historyList = config.historyList;
this.spinBtn = config.spinBtn;
this.resetConfigBtn = config.resetConfigBtn;
this.defaultPrizes = config.defaultPrizes;
this.prizes = JSON.parse(JSON.stringify(this.defaultPrizes));
this.isSpinning = false;
this.rotation = 0;
this.currentPrizeIndex = -1;
this.centerX = this.canvas.width / 2;
this.centerY = this.canvas.height / 2;
this.radius = Math.min(this.centerX, this.centerY) - 10;
this.init();
}
init() {
this.drawWheel();
this.bindEvents();
}
drawWheel() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const numPrizes = this.prizes.length;
const angleStep = (2 * Math.PI) / numPrizes;
for (let i = 0; i < numPrizes; i++) {
const startAngle = i * angleStep + this.rotation - Math.PI/2 - angleStep/2;
const endAngle = (i + 1) * angleStep + this.rotation - Math.PI/2 - angleStep/2;
this.ctx.beginPath();
this.ctx.moveTo(this.centerX, this.centerY);
this.ctx.arc(this.centerX, this.centerY, this.radius, startAngle, endAngle);
this.ctx.closePath();
this.ctx.fillStyle = this.prizes[i].color;
this.ctx.fill();
this.ctx.strokeStyle = '#fff';
this.ctx.lineWidth = 2;
this.ctx.stroke();
this.ctx.save();
this.ctx.translate(this.centerX, this.centerY);
this.ctx.rotate(startAngle + angleStep / 2);
this.ctx.textAlign = 'right';
this.ctx.fillStyle = '#fff';
this.ctx.font = 'bold 18px "Microsoft YaHei"';
this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
this.ctx.shadowBlur = 3;
this.ctx.fillText(this.prizes[i].name, this.radius - 20, 8);
this.ctx.restore();
}
this.ctx.beginPath();
this.ctx.arc(this.centerX, this.centerY, 20, 0, 2 * Math.PI);
this.ctx.fillStyle = '#2c3e50';
this.ctx.fill();
this.ctx.strokeStyle = '#fff';
this.ctx.lineWidth = 4;
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(this.centerX, this.centerY, 10, 0, 2 * Math.PI);
this.ctx.fillStyle = '#FFD700';
this.ctx.fill();
}
spin() {
if (this.isSpinning) return;
this.isSpinning = true;
this.spinBtn.disabled = true;
this.resultDisplay.textContent = "旋转中...";
this.resultDisplay.classList.remove('winner');
const spins = 5 + Math.random() * 3;
const extraDeg = Math.random() * 360;
const totalRotationDeg = spins * 360 + extraDeg;
const duration = 4000 + Math.random() * 2000;
const normalizedDeg = (360 - (totalRotationDeg % 360)) % 360;
const prizeAngleDeg = 360 / this.prizes.length;
const adjustedDeg = (normalizedDeg + prizeAngleDeg/2) % 360;
this.currentPrizeIndex = Math.floor(adjustedDeg / prizeAngleDeg);
if (this.currentPrizeIndex >= this.prizes.length) this.currentPrizeIndex = 0;
const startTime = Date.now();
const animate = () => {
const now = Date.now();
const elapsed = now - startTime;
let progress = elapsed / duration;
progress = Math.min(progress, 1);
const easeOut = 1 - Math.pow(1 - progress, 3);
this.rotation = (easeOut * totalRotationDeg * Math.PI) / 180;
this.drawWheel();
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.finishSpin();
}
};
requestAnimationFrame(animate);
}
finishSpin() {
this.isSpinning = false;
this.spinBtn.disabled = false;
const prizeName = this.prizes[this.currentPrizeIndex].name;
this.resultDisplay.textContent = `恭喜获得:${prizeName}!`;
this.resultDisplay.classList.add('winner');
const historyItem = document.createElement('li');
const now = new Date();
const timeStr = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
historyItem.textContent = `[${timeStr}] ${prizeName}`;
if (this.historyList.firstChild) {
this.historyList.insertBefore(historyItem, this.historyList.firstChild);
} else {
this.historyList.appendChild(historyItem);
}
while (this.historyList.children.length > 10) {
this.historyList.removeChild(this.historyList.lastChild);
}
}
resetConfig() {
if (this.isSpinning) return;
if (confirm('确定要重置为默认奖品配置吗?当前配置将会丢失。')) {
this.prizes = JSON.parse(JSON.stringify(this.defaultPrizes));
this.rotation = 0;
this.initPrizeConfigUI();
this.drawWheel();
this.resultDisplay.textContent = "配置已重置,等待旋转...";
this.resultDisplay.classList.remove('winner');
this.historyList.innerHTML = '';
}
}
bindEvents() {
this.spinBtn.addEventListener('click', () => this.spin());
this.resetConfigBtn.addEventListener('click', () => this.resetConfig());
this.canvas.addEventListener('click', () => this.spin());
}
}
初始化
需要注意的是,初始化必须在 dom 树加载完毕后执行,否则获取元素可能为空,可以在 script 标签加 defer 标记,或者放在 body 下方,以保证 dom 加载完成。
// 初始化应用
const wheel = new LuckyWheel({
defaultPrizes: [
{ name: "一等奖", color: "#FF6B6B" },
{ name: "谢谢参与", color: "#4ECDC4" },
{ name: "二等奖", color: "#FFD166" },
{ name: "优惠券", color: "#06D6A0" },
{ name: "三等奖", color: "#118AB2" },
{ name: "小礼品", color: "#EF476F" },
{ name: "幸运奖", color: "#9D4EDD" },
{ name: "纪念品", color: "#FF9E6D" }
],
canvas: document.getElementById('wheelCanvas'),
resultDisplay: document.getElementById('resultDisplay'),
historyList: document.getElementById('historyList'),
spinBtn: document.getElementById('spinBtn'),
resetConfigBtn: document.getElementById('resetConfigBtn')
});
// 将实例挂载到window以便调试
window.luckyWheel = wheel;
后续优化
后续还可以对 this.currentPrizeIndex = Math.floor(adjustedDeg / prizeAngleDeg);进行修改,加入接口调用获取抽奖结果。
评论区