You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现转盘动画并精准停在指定目标位置

转盘精准停指定位置的实现方案

问题背景

我用HTML+JavaScript实现了转盘功能,现在需要让转盘完成转动动画后精准停在指定选项位置,但遇到两个问题:

  • 设置radiansUntilStop = 41(对应“neko”选项)时,转盘无转动动画直接停止
  • 移除停止判断代码后,转盘能正常播放转动动画,但会随机停止,无法精准定位

问题原因

  1. 初始currentDeg为0,而radiansUntilStop设为41,动画启动瞬间就满足currentDeg >= radiansUntilStop的条件,直接触发停止逻辑,跳过了转动动画
  2. 原有减速逻辑基于随机的maxRotation(3-6整圈),没有将目标停止角度与多圈旋转结合,导致移除判断后只能随机停止

解决方案

核心思路是让转盘先转够若干整圈,再减速精准停在目标区间的中间位置,具体调整如下:

  1. 计算总停止角度:将随机多圈角度(保持3-6圈的随机性)与目标选项区间的中间角度相加,确保转盘转够多圈后落在指定区间
  2. 调整减速逻辑:以总停止角度为基准计算减速过程,让速度从快到慢自然过渡
  3. 优化停止判断:当currentDeg接近总停止角度时,直接将角度修正到目标值并停止,避免因速度精度问题导致偏差

修改后的关键代码片段

1. 调整spin函数,计算总停止角度

function spin(){
    if(speed != 0){
        return
    }
    currentDeg = 0
    // 随机3-6整圈
    const randomFullRotations = randomRange(360*3,360*6)
    // 选择目标选项(这里以neko为例)
    const targetItem = items.find(item => item.name === "neko")
    // 计算目标区间的中间角度,避免停在边缘
    const targetMiddleDeg = (targetItem.minDegree + targetItem.maxDegree) / 2
    // 总停止角度 = 随机整圈 + 目标中间角度
    radiansUntilStop = randomFullRotations + targetMiddleDeg
    pause = false
    window.requestAnimationFrame(animate);
}

2. 修改animate函数的减速与停止逻辑

function animate(){
    if(pause){
        return;
    }
    // 计算当前已转动角度占总停止角度的比例,用于减速
    const progress = getPercent(currentDeg, 0, radiansUntilStop)
    // 用缓动函数实现自然减速,速度随进度增加而降低
    speed = easeOutSine(1 - progress) * 20

    // 当速度足够小,或者当前角度接近总停止角度时,停止转动
    if(speed < 0.01 || currentDeg >= radiansUntilStop - speed){
        currentDeg = radiansUntilStop
        speed = 0;
        pause = true;
    }
    
    currentDeg += speed;
    draw()
    window.requestAnimationFrame(animate);
}

完整实现代码

function randomColor(){
    r = Math.floor(Math.random() * 255);
    g = Math.floor(Math.random() * 255);
    b = Math.floor(Math.random() * 255);
    return {r,g,b}
}
function toRad(deg){
    return deg * (Math.PI / 180.0);
}
function randomRange(min,max){
    return Math.floor(Math.random() * (max - min + 1)) + min;
}
function easeOutSine(x) {
    return Math.sin((x * Math.PI) / 2);
}
// get percent between 2 number
function getPercent(input,min,max){
    return (((input - min) * 100) / (max - min))/100
}


const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
const width = document.getElementById("canvas").width
const height = document.getElementById("canvas").height

const centerX = width/2
const centerY = height/2
const radius = width/2


const items = [
      { minDegree: 0, maxDegree: 40, value: 1, name: "chicken" },
      { minDegree: 41, maxDegree: 80, value: 2, name: "neko" },
      { minDegree: 81, maxDegree: 120, value: 3, name: "bird" },
      { minDegree: 121, maxDegree: 160, value: 4, name: "goat" },
      { minDegree: 161, maxDegree: 200, value: 5, name: "sheep" },
      { minDegree: 201, maxDegree: 240, value: 6, name: "duck" },
      { minDegree: 241, maxDegree: 280, value: 7, name: "cow" },
      { minDegree: 281, maxDegree: 320, value: 8, name: "dog" },
      { minDegree: 321, maxDegree: 360, value: 9, name: "cat" },
    ];

console.log(items);

let currentDeg = 0
let step = 360/items.length
let colors = []
for(let i = 0 ; i < items.length + 1;i++){
    colors.push(randomColor())
}

function createWheel(){
    step = 360/items.length
    colors = []
    for(let i = 0 ; i < items.length + 1;i++){
        colors.push(randomColor())
    }
    draw()
}
draw()

function draw(){
    ctx.beginPath();
    ctx.arc(centerX, centerY, radius, toRad(0), toRad(360))
    ctx.fillStyle = `rgb(${33},${33},${33})`
    ctx.lineTo(centerX, centerY);
    ctx.fill()

    let startDeg = currentDeg;
    for(let i = 0 ; i < items.length; i++, startDeg += step){
        let rewardName = items[i].name;
        let endDeg = startDeg + step;

        color = colors[i]
        let colorStyle = `rgb(${color.r},${color.g},${color.b})`;

        ctx.beginPath();
        ctx.arc(centerX, centerY, radius - 2, toRad(startDeg), toRad(endDeg));
        let colorStyle2 = `rgb(${color.r - 30},${color.g - 30},${color.b - 30})`;
        ctx.fillStyle = colorStyle2;
        ctx.lineTo(centerX, centerY);
        ctx.fill();

        ctx.beginPath();
        ctx.arc(centerX, centerY, radius - 30, toRad(startDeg), toRad(endDeg));
        ctx.fillStyle = colorStyle;
        ctx.lineTo(centerX, centerY);
        ctx.fill();

        // draw text
        ctx.save();
        ctx.translate(centerX, centerY);
        ctx.rotate(toRad((startDeg + endDeg)/2));
        ctx.textAlign = "center";
        if(color.r > 150 || color.g > 150 || color.b > 150){
            ctx.fillStyle = "#000";
        }
        else{
            ctx.fillStyle = "#fff";
        }
        ctx.font = 'bold 24px serif';
        ctx.fillText(rewardName, 150, 10);
        ctx.restore();

        // check winner:修正判断逻辑,以0度指针为准
        const normalizedStart = startDeg % 360;
        const normalizedEnd = endDeg % 360;
        if ((normalizedStart <= 0 && normalizedEnd >= 0) || (normalizedStart > normalizedEnd && (normalizedStart >= 0 || normalizedEnd <= 0))) {
            document.getElementById("winner").innerHTML = rewardName;
        }
    }
}


let speed = radiansUntilStop = 0;
let pause = false;
function animate(){
    if(pause){
        return;
    }

    const progress = getPercent(currentDeg, 0, radiansUntilStop)
    speed = easeOutSine(1 - progress) * 20

    if(speed < 0.01 || currentDeg >= radiansUntilStop - speed){
        currentDeg = radiansUntilStop
        speed = 0;
        pause = true;
    }
    
    currentDeg += speed;
    draw()
    window.requestAnimationFrame(animate);
}

function spin(){
    if(speed != 0){
        return
    }
    currentDeg = 0
    const randomFullRotations = randomRange(360*3,360*6)
    // 可替换为其他目标name,比如"cat"
    const targetItem = items.find(item => item.name === "neko")
    const targetMiddleDeg = (targetItem.minDegree + targetItem.maxDegree) / 2
    radiansUntilStop = randomFullRotations + targetMiddleDeg
    pause = false
    window.requestAnimationFrame(animate);
}

内容的提问来源于stack exchange,提问作者Investor Support

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 03:40:02