如何实现转盘动画并精准停在指定目标位置
转盘精准停指定位置的实现方案
问题背景
我用HTML+JavaScript实现了转盘功能,现在需要让转盘完成转动动画后精准停在指定选项位置,但遇到两个问题:
- 设置
radiansUntilStop = 41(对应“neko”选项)时,转盘无转动动画直接停止 - 移除停止判断代码后,转盘能正常播放转动动画,但会随机停止,无法精准定位
问题原因
- 初始
currentDeg为0,而radiansUntilStop设为41,动画启动瞬间就满足currentDeg >= radiansUntilStop的条件,直接触发停止逻辑,跳过了转动动画 - 原有减速逻辑基于随机的
maxRotation(3-6整圈),没有将目标停止角度与多圈旋转结合,导致移除判断后只能随机停止
解决方案
核心思路是让转盘先转够若干整圈,再减速精准停在目标区间的中间位置,具体调整如下:
- 计算总停止角度:将随机多圈角度(保持3-6圈的随机性)与目标选项区间的中间角度相加,确保转盘转够多圈后落在指定区间
- 调整减速逻辑:以总停止角度为基准计算减速过程,让速度从快到慢自然过渡
- 优化停止判断:当
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
相关产品推荐
相关产品推荐

