使用Canvas开发抽奖转盘时如何为选项设置自定义占比百分比?
自定义权重转盘的实现修改
原代码的核心缺陷是扇区渲染完全采用等分逻辑,没有读取配置中的百分比权重,且中奖判定逻辑和等分规则强绑定,无法匹配不同扇区大小的场景,可按以下步骤调整:
1. 预处理扇区权重配置
- 移除原代码中全局固定的等分弧长变量
arc - 遍历
options数组,把第二项带%的字符串解析为数值型权重(例如把'10%'转为10、'90%'转为90) - 计算所有选项的总权重值,按照单个权重占总权重的比例,换算为该扇区实际占有的弧度(整个转盘总弧度为2π),同时累计每个扇区的起始角度,保证所有扇区拼合后刚好填满整圆
- 以现有配置为例:@david占比10%,对应扇区弧度为
0.2π;@burn占比90%,对应扇区弧度为1.8π,两者相加刚好为整圆弧度
- 以现有配置为例:@david占比10%,对应扇区弧度为
预处理逻辑核心代码
const wheelSectors = []; let totalWeight = 0; // 解析权重 options.forEach(item => { const weight = parseFloat(item[1]); totalWeight += weight; wheelSectors.push({ label: item[0], weight: weight }) }) // 计算每个扇区的起始角度、实际扫过的弧长 let angleCursor = 0; wheelSectors.forEach(sector => { sector.sweepAngle = (sector.weight / totalWeight) * Math.PI * 2; sector.startAngle = angleCursor; angleCursor += sector.sweepAngle; })
2. 调整扇区绘制逻辑
- 绘制每个扇区时,不再使用
i * 固定弧长计算起始角度,改为读取预处理阶段生成的wheelSectors[i].startAngle作为扇区本身的固定偏移量,叠加全局旋转的startAngle作为实际绘制的起始角度 - 扇区的结束绘制角度,读取对应扇区的
sweepAngle值,不再使用固定的等分弧长 - 文字定位时,取扇区实际起始角度加自身弧长的一半作为中心位置,保证文字始终在扇区正中间
绘制循环核心替换代码
for (let i = 0; i < options.length; i++) { // 替换原固定等分的角度计算逻辑 const sector = wheelSectors[i]; const realStartAngle = startAngle + sector.startAngle; ctx.fillStyle = getColor(i, options.length); ctx.beginPath(); ctx.arc(250, 250, outsideRadius, realStartAngle, realStartAngle + sector.sweepAngle, false); ctx.arc(250, 250, insideRadius, realStartAngle + sector.sweepAngle, realStartAngle, true); ctx.stroke(); ctx.fill(); ctx.save(); // 替换原固定弧长计算的文字旋转角度 const textCenterAngle = realStartAngle + sector.sweepAngle / 2; ctx.translate( 250 + Math.cos(textCenterAngle) * textRadius, 250 + Math.sin(textCenterAngle) * textRadius ); ctx.rotate(textCenterAngle + Math.PI / 2); ctx.fillText(sector.label, -ctx.measureText(sector.label).width / 2, 0); ctx.restore(); }
3. 修正中奖判定逻辑
- 原代码基于固定等分弧长
arc计算中奖索引,在非等分场景下会出现匹配错位,需要替换为基于区间匹配的判定逻辑 - 先把停止时的全局角度归一化到0-2π的范围(适配转盘旋转多圈后的角度溢出问题)
- 从第一个扇区开始累加弧长,找到当前停止角度落在哪个扇区的累计角度区间,对应的索引即为中奖结果
中奖判定核心替换代码
clearTimeout(spinTimeout); // 角度归一化到0-2π范围 let normalizedAngle = (startAngle * 180 / Math.PI + 90) % 360; normalizedAngle = normalizedAngle * Math.PI / 180; if (normalizedAngle < 0) normalizedAngle += Math.PI * 2; // 区间匹配找中奖扇区 let winIndex = 0; let angleAccum = 0; for (let i = 0; i < wheelSectors.length; i++) { angleAccum += wheelSectors[i].sweepAngle; if (normalizedAngle <= angleAccum) { winIndex = i; break; } } ctx.save(); ctx.font = 'bold 30px Helvetica, Arial'; const winText = wheelSectors[winIndex].label; ctx.fillText(winText, 250 - ctx.measureText(winText).width / 2, 250 + 10); ctx.restore();
额外说明
- 权重配置不需要强制凑成100%,代码自动按照总权重比例换算扇区大小,例如你设置@david权重1、@burn权重9,效果和10%、90%的配置完全一致
- 角度计算时注意处理负角度的边界情况,避免因为旋转圈数过多导致的索引匹配错误
- 原代码存在文字绘制的bug:直接把数组形式的
options[i]传给fillText,会把权重值和用户名一起绘制到扇区上,上述修改已修复该问题
内容的提问来源于stack exchange,提问作者learning-canva
相关产品推荐
相关产品推荐

