js抽奖算法
核心思路:
-
设定概率:
- 奖项1概率:5% 即 [0 - 0.05) 的范围
- 奖项2率:10%. 即 [0.05 - 0.1) 的范围。注:为什么是从0.05开始?因为<0.05的话就是中了上一个奖
- 不中奖. 即 [0.05 - 1) 的范围
- 要提高中奖难度,可以通过 缩小中奖概率区间 来减少中奖的机会
-
生成随机数:使用
Math.random()
生成一个 [0, 1) 区间的随机数。 -
根据随机数确定奖项:根据随机数值判断它落在哪个概率区间,决定是否中奖(积累概率来减少if语句的使用)。
const items = [ { value: "iPhone 15", chance: 0.05 }, { value: "AirPods", chance: 0.10 }, { value: "50元红包", chance: 0.20 }, { value: "谢谢参与", chance: 0.65 } ]; const lottery = (arr) => { const randomValue = Math.random(); // 随机数 [0, 1) let cumulativeChance = 0; // 累加的概率值 for (let i = 0; i < arr.length; i++) { cumulativeChance += arr[i].chance; // 更新累加概率 if (randomValue < cumulativeChance) { return arr[i]; // 如果随机数小于当前的累加概率,返回奖品 } } return arr[0]; // 默认返回第一个奖品(防止极端情况) }; console.log(lottery(items));
扩展:大转盘抽奖
1.核心逻辑
将一个圆分成n份,每份指定一个索引号;
计算每个索引号的起、止角度;
angle_per_section = 360 / n start_angle[i] = i * angle_per_section end_angle[i] = start_angle[i] + angle_per_section
抽奖:根据概率随机返回一个序号,停止角度为中间位置:
startAngle + (endAngle - startAngle) /2
抽奖代码实现
function getWinningIndex(items) { // 计算累计概率区间 let cumulative = 0; const angle_per_section = 360 / items.length; let cumulativeProbabilities = items.map((item, index) => { cumulative += item.chance; const angleStart = index * angle_per_section; const angleEnd = angleStart + angle_per_section; return { ...item, index, cumulative, angleStart, angleEnd }; }); console.log(cumulativeProbabilities); // 生成 [0,1) 之间的随机数 let randomNum = Math.random(); // 通过累积概率区间选择中奖项 let result = items[0]; for (let item of cumulativeProbabilities) { if (randomNum < item.cumulative) { result = item; break; } } result.stopAngle = result.angleStart + (result.angleEnd - result.angleStart) / 2; return result; }
2.转盘绘制
在 JavaScript Canvas API 中,所有角度计算都是基于弧度(radian),而不是度数(degree)。
所以每个扇区的弧度为
const anglePerSector = (2 * Math.PI) / labels.length;
按数组顺序顺时针绘制的问题
-
转盘的 0 度:在大转盘抽奖中,我们通常期望转盘的起始位置是 从顶部开始(即 12 点钟方向,类似于时钟的零度位置),但是 Canvas 默认的角度起始点是 从正右方开始(3点钟方向),这是为什么顺时针绘制时,扇形的位置会有偏差的原因。
-
调整起始角度:为了让大转盘从 顶部(12点位置) 开始,我们需要调整起始角度,将其偏移 -90°,使得扇形从顶部开始,而不是从默认的右侧开始。
实现代码
function drawWheel(labels) { const canvas = document.getElementById("wheelCanvas"); const ctx = canvas.getContext("2d"); const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = 150; const totalSectors = labels.length; const anglePerSector = (2 * Math.PI) / totalSectors; ctx.clearRect(0, 0, canvas.width, canvas.height); // 顺时针绘制: 将起始角度设置为负90度,使得第一个扇区从顶部开始 let startAngle = -Math.PI / 2; labels.forEach((text, index) => { const endAngle = startAngle + anglePerSector; // 🎨 设置不同颜色 ctx.fillStyle = index % 2 === 0 ? "#FFDD57" : "#FF5733"; // 🎯 绘制扇形 ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.closePath(); ctx.fill(); ctx.strokeStyle = "#fff"; ctx.stroke(); // ✍️ 绘制文字 ctx.fillStyle = "#000"; ctx.font = "16px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; // 计算文字的位置(扇形中间) const textAngle = startAngle + anglePerSector / 2; const textX = centerX + Math.cos(textAngle) * (radius * 0.7); const textY = centerY + Math.sin(textAngle) * (radius * 0.7); // 旋转绘制文字 ctx.save(); ctx.translate(textX, textY); ctx.rotate(textAngle + Math.PI / 2); // 旋转文字方向 ctx.fillText(text, 0, 0); ctx.restore(); // 更新起始角度 startAngle = endAngle; }); }
转动指定的圈数后停止到目标角度
因为扇区是顺时针绘制的,所以转动固定圈数时,也要顺时针转到,css的rotate需要设置为负角度
转动的速度动画用css的transform:transition 贝塞尔曲线控制
function start() { const canvas = document.getElementById("wheelCanvas"); //归位 canvas.style.transition = "transform 0s"; canvas.style.transform = "rotate(0deg)"; //抽取一个扇区 const finalTarget = getWinningIndex(prizes); // 等待短暂的时间后开始旋转 setTimeout(() => { //先顺时针旋转两圈,然后再顺针转到目标的角度 const randomDegree = -720 + (-finalTarget.stopAngle); canvas.style.transition = "transform 3s cubic-bezier(0.25, 0.8, 0.25, 1)"; canvas.style.transform = `rotate(${randomDegree}deg)`; // 控制整个转盘的旋转 }, 50); }