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); 
        }

 

posted @ 2024-12-04 20:46  我是格鲁特  阅读(261)  评论(0)    收藏  举报