使用方法
- 「选项列表」默认已经填好 6 行:
披萨|3,加上汉堡|1、寿司|1、沙拉|1、面条|1、咖喱|1。一行就是一个扇区,竖线后面是权重,省略竖线的行按 1 算。 - 「转速档」默认 5,只接受 1~10 的整数,对应动画时长 4000 ms;它只决定转多快,不改变任何一项的命中概率。
- 「转动圈数」默认 5,只接受 3~12 的整数。圈数越多,最后那段慢动作越像真的在减速。
- 点「开始转动」。也可以把光标放在选项框里按 Ctrl/Cmd+Enter(单独的 Enter 留给换行),或者在转速、圈数输入框里按 Enter。
- 结果区给出选中的那一项、权重、命中概率与圆心角;「复制结果」只带走这一行文字,「重置为默认示例」恢复上面那 6 行。想让某项更容易被抽中就把权重写大,不想让某项上盘就把它的权重写成
0。
这个工具能解决什么问题
一是把口头定不下来的事交给一次抽签。午饭吃什么、谁先发言、周末去哪,候选项一行一条写进去,点一次就有结论。本页的随机转盘把过程摊开:每个扇区的面积就是它的命中概率,指针停在哪个扇区可以当场核对。
二是概率不必均等也照样能表达。外卖群里多数人想要披萨,那就写 披萨|3,它的扇区占 135.00°、命中概率 37.50%,其余五项各占 45.00°、各 12.50%。面积和概率永远一致,不需要在背后偷偷调概率。
三是给线下活动当一个可核对的道具。班会点名、团建选人、直播抽题,把屏幕投出来让所有人看着指针停下,规则简单、当场可验,也不用为这件事装一个 App。
四是不留下任何数据。这个随机转盘的选项文本只在当前标签页里解析与绘制,不发往服务器,刷新页面就清空。本页不做奖池库存管理、不保存历史名单、不批量分发一次性凭证——那些是活动系统的活,不是转盘该干的。
原理与变量:先抽签定扇区,再反算停靠角度
第一件事是把权重换成角度。第 i 个扇区的圆心角是 θ_i = 2π × w_i / Σw,w_i 是这一项的权重,Σw 是所有权重之和。全是等权时它就退化成 2π/n。默认盘 Σw = 8,披萨那一格的圆心角 = 2π × 3 / 8 = 2.3561944902 弧度 = 135.00°,其余五格各是 2π × 1 / 8 = 0.7853981634 弧度 = 45.00°。角度只由权重决定,与转速、圈数、画布大小都无关。
第二件事是抽签,抽签用累计权重而不是随机角度。先取一个 u = Math.random() × Σw,然后从第一项开始往后累加权重,u 落在哪一段累计区间里就选中谁。这样选中的概率严格等于 w_i / Σw,和大扇区的面积是同一个比值——所以不存在"面积大但概率没变"这种自相矛盾的实现。
第三件事才是算角度,方向是"从结果推角度",不是"从角度读结果"。指针位置是写死的:永远在正上方,也就是 −90°。若第 i 个扇区的中心角是 c_i,要让这个扇区转到指针下面,旋转量 rot 必须满足 c_i + rot ≡ −90°(mod 360°),也就是 rot ≡ −90° − c_i,再叠上整圈数,目标就是 目标 = 圈数 × 360° + (−90° − c_i) + 抖动。举例看 6 个等权扇区,各扇区中心角依次是 30°、90°、150°、210°、270°、330°:抽中 30° 那一格、转 5 圈,目标是 5 × 360 + (−90 − 30) = 1680.00°;抽中 210° 那一格,目标是 5 × 360 + (−90 − 210) = 1500.00°。
为什么绝不能反过来做。如果先跑动画、等它停下来、再读指针落在哪个扇区当结果,这个结果就同时受浮点误差和缓动收尾精度支配:末帧按 1 − (1 − t)³ 收敛,t = 0.9 时还剩 0.10% 的行程没走完,t 到 0.999 时仍剩约 3 × 10⁻⁹ 的行程。指针正好压在两格分界线上时,读数可能落到相邻那一格——结果就从"抽出来的"变成了"量出来的"。更麻烦的是,一旦有人知道你用最终角度反推结果,就会质疑这盘可以被参数调成偏向某一格。本页把顺序钉死:扇区先用随机数抽出,角度只负责把它转到指针下面,动画无论快慢、无论有没有被跳过,都不会改变已经定下的结果。
抖动是为了不每次都停在正中央。抽中之后会在该扇区内随机偏移一点,幅度取 0.7 × 半宽;默认盘披萨那一格半宽是 67.50°,所以抖动上限是 47.25°。注意顺序:抖动是抽签之后才加的,只改观感、不改结果,而且它在数学上被限制在扇区内部,指针不可能因为抖动而落到隔壁格。
缓动用 easeOutCubic,位移比例 = 1 − (1 − t)³,t 是归一化时间。它在 t = 0 处的斜率是 3,意思是起步速度是全程平均速度的 3 倍;t = 1 处斜率为 0,末帧无限放慢。取样一看就明白:t = 0.5 时位移已经走完 87.5%,t = 0.75 时 98.44%,t = 0.9 时 99.90%。所以转盘的"慢下来"其实发生在后半程,而真正让结果可信的不是这条曲线,是前面那一步抽签。曲线换成任何一条,结果都不变。
扇区边界要用累计权重算,不要逐个角度相加。每写一次 2π × w_i / Σw 都会四舍五入一次,把 24 个角度逐个累加,最后一条边界与 2π 之间会差 1.78 × 10⁻¹⁵ 弧度。换成 140 px 半径上的弧长只有 2.49 × 10⁻¹³ px,肉眼绝对看不见——真正出问题的是 canvas 给相邻扇形描边时,抗锯齿会沿着这条没对齐的缝烙出一道比周围偏亮的白线。本页改成先累加整数权重,再拿累计权重乘一次 2π/Σw 得到边界角,最后一条边界恒等于 2π,缝自然归零。源码里画的是 arc(0, 0, 140, 2π × 累计起点权重 / Σw, 2π × 累计终点权重 / Σw):同一个分母、同一次乘法,没有中间的四舍五入。
最后是随机数的均匀性。Math.random() 返回 [0, 1) 内的双精度浮点数,可取值一共 253 = 9,007,199,254,740,992 个,相邻两数的间隔是 2-53 ≈ 1.11 × 10⁻¹⁶。用 Math.random() × Σw 再落段,只有在 253 不能被 Σw 整除时才会有极小偏差:默认盘 Σw = 8,253 正好被 8 整除,偏差为 0;Σw = 6 时有 2 个取值被多算一次,偏差 2.22 × 10⁻¹⁶。就算把 24 个扇区的权重全填到 99(Σw = 2376),253 的每个网格点也只代表 9.50 × 10⁻¹¹ 度。需要说明的是,Math.random() 不是密码学安全的随机源,它的内部状态可被观测推导——做游戏、抽签、点名完全够用,但不要拿它生成密码或一次性凭证。
完整示例(数值仅作演示)
打开这个随机转盘时的默认条件与下面完全一致:6 个选项、权重 3 + 1 + 1 + 1 + 1 + 1、转速 5 档、5 圈。
- 总权重 Σw = 3 + 1 + 1 + 1 + 1 + 1 = 8;
- 披萨的圆心角 = 2π × 3 / 8 = 2.3561944902 弧度 = 135.00°,命中概率 37.50%;
- 其余五项的圆心角 = 2π × 1 / 8 = 0.7853981634 弧度 = 45.00°,各 12.50%;
- 六格角度相加 135.00 + 45.00 × 5 = 360.00°;概率相加 37.50% + 12.50% × 5 = 100%;
- 按累计权重算出的六格中心角依次是 67.50°、157.50°、202.50°、247.50°、292.50°、337.50°;
- 抽中披萨时目标旋转 = 5 × 360 + (−90 − 67.50) = 1642.50°,再叠加最多 ±47.25° 的抖动;
- 转速 5 档 → 动画时长 5600 − 400 × 4 = 4000 ms,easeOutCubic 在第 2000 ms 时已经走完 87.5% 的行程;
- 画布 300 × 300,RGBA 缓冲 = 300 × 300 × 4 = 360,000 字节;扇区外接半径 140 px 时,45.00° 那一格的弧长 = 140 × 0.7853981634 ≈ 109.96 px。
控件默认值、这段示例与下面的对照表用的是同一批数字:Σw = 8、披萨 135.00° / 37.50%、其余各 45.00° / 12.50%。
扇区数与权重对照表
下表第一列是扇区数量,后面三列说明等权时每一格有多大。半径固定取 140 px(与画布上扇区的外接半径一致),弧长 = 半径 × 圆心角。
| 扇区数 n | 等权圆心角 | 每扇区概率 | 半径 140 px 弧长 |
|---|---|---|---|
| 2 | 180.00° | 50.00% | 439.82 px |
| 3 | 120.00° | 33.33% | 293.22 px |
| 4 | 90.00° | 25.00% | 219.91 px |
| 5 | 72.00° | 20.00% | 175.93 px |
| 6(默认) | 60.00° | 16.67% | 146.61 px |
| 8 | 45.00° | 12.50% | 109.96 px |
| 9 | 40.00° | 11.11% | 97.74 px |
| 10 | 36.00° | 10.00% | 87.96 px |
| 12 | 30.00° | 8.33% | 73.30 px |
| 15 | 24.00° | 6.67% | 58.64 px |
| 18 | 20.00° | 5.56% | 48.87 px |
| 20 | 18.00° | 5.00% | 43.98 px |
| 24(上限) | 15.00° | 4.17% | 36.65 px |
| 30 | 12.00° | 3.33% | 29.32 px |
| 36 | 10.00° | 2.78% | 24.43 px |
| 48 | 7.50° | 2.08% | 18.33 px |
「加载更多扇区数」会补上 24、30、36、48 四档。注意 30 以上已经超出本页 24 个扇区的上限,列出来只是为了说明角度随 n 继续变小的趋势:n = 48 时单格只剩 7.50°、弧长 18.33 px,文字根本放不下,所以把上限压在 24。
第二张表把扇区数固定在 6,只改权重,看面积是怎么被分掉的。「最大」指权重最大的那一格,「最小」指权重最小的那一格;权重写 0 的行会被跳过,不计入扇区数也不计入 Σw。
| 选项写法 | Σw | 最大扇区圆心角 | 对应概率 | 最小扇区圆心角 | 对应概率 |
|---|---|---|---|---|---|
| 披萨|3 + 五个 |1(默认) | 8 | 135.00° | 37.50% | 45.00° | 12.50% |
| 六个都写 |1(等权) | 6 | 60.00° | 16.67% | 60.00° | 16.67% |
| |1 |2 |3 |4 | 10 | 144.00° | 40.00% | 36.00° | 10.00% |
| |2 |2 |1 |1 | 6 | 120.00° | 33.33% | 60.00° | 16.67% |
| |5 |1 |1 |1 | 8 | 225.00° | 62.50% | 45.00° | 12.50% |
| 十二个都写 |1 | 12 | 30.00° | 8.33% | 30.00° | 8.33% |
| 一项 |0 + 五个 |1 | 5 | 72.00° | 20.00% | 72.00° | 20.00% |
两张表横着看就清楚了:扇区数决定"每一格有多大",权重决定"格与格之间怎么分"。要改概率就改权重,要改一格能放多少文字就改扇区数,两者互不干扰。也正因为面积等于概率,把某一项权重写成 5 之后它占 225.00°(62.50%),这就是它真实的胜率,不会出现"看起来很大其实抽不到"的情况。
输入范围、边界与精度说明
下面这些是刻意设死的边界,知道它们能避免把这页当万能工具用:
- 选项 2~24 行。少于 2 行构不成有效抽签;超过 24 行时,140 px 半径上的单格弧长会掉到 36.65 px 以下,文字挤在一起没法看。
- 名称最多 20 个字符。盘面上超过 8 个字符会用省略号收尾,但校验按完整名称算——超长会直接报错并指出是第几行,而不是悄悄截断后继续转。
- 权重取 0~99 的整数。0 不是"很小",而是跳过:这一行不上盘,扇区数与 Σw 都不计入。权重省略时按 1 算,写了竖线但后面留空同样按 1 算。
- 名称里的竖线用最后一个切分。
A|B|2会被解析成名称A|B、权重 2,所以名称本身可以带竖线。 - Σw 最大 2376(24 行全填 99)。这个量级下 253 的网格细到每格 9.50 × 10⁻¹¹ 度,抽样偏差与均匀随机没有可测差别。
- 转速 1~10 档,对应 5600 ms 到 2000 ms,每档差 400 ms。档位只改动画时长,不改结果——同一组选项连点十次,抽中的分布与转速档无关。
- 圈数 3~12。低于 3 圈时指针几乎立刻到位,看不出减速;高于 12 圈因为总时长不变,会让中段显得更快、末段拖得更久。
- 盘面是 300 × 300 的 canvas,扇区外接半径 140 px。整块 RGBA 缓冲只有 360,000 字节,绘制与旋转都在内存里完成,不生成图片文件,也不会因为扇区多而变卡。
- 选项只在当前标签页里,刷新即清空。「复制结果」复制的是那一行结果文本,不包含选项列表全文,避免把整份名单误粘到聊天窗口里。
常见问题
结果到底是谁决定的,是随机数还是指针?
是随机数。Math.random() × Σw 先落在某一段累计权重上,选中哪一格在动画开始前就已经定死;指针停在哪儿只是把这一格转到正上方,是"结果的表现"而不是"结果的来源"。所以动画被跳过、被缩放到很小、甚至完全不看屏幕,结果都一样。
为什么扇区面积和命中概率一定一致?
因为它们是同一个比值。圆心角是 2π × w_i / Σw,选中概率是 w_i / Σw,两者只差一个常数 2π。想把某一格的概率做成 37.50%,就让它的权重占 Σw 的 37.50%——默认盘里 披萨|3 配 Σw = 8 正好是这个数。
权重填 0 会怎样,和填 1 差很多吗?
填 0 是整行跳过,那一项根本不上盘,扇区数和 Σw 都不算它;填 1 是正常参与,占 1/Σw 的概率。所以 0 与 1 之间不是"差一点点",是"在盘上"和"不在盘上"的区别。跳过后如果剩下的扇区少于 2 个,页面会直接报错并要求至少保留 2 个。
我把某一项权重写成 99,是不是就一定抽中它?
不是。权重只决定占比,不决定必然性。24 行里只有一项写 99、其余写 1 时 Σw = 122,这一项概率约 81.15%,仍有约 18.85% 会抽到别的格。想要"必然"只能把其余项删掉或权重写 0,但那样就没有抽签的意义了。
用 Math.random() 公平吗?会不会有取模偏差?
在本题的取值范围内偏差可以忽略。可取值总数是 253,Σw = 8 时能被整除,偏差为 0;Σw = 6 时只有 2 个取值被多算一次,偏差 2.22 × 10⁻¹⁶;即使 Σw 拉满到 2376,每个网格点也只代表 9.50 × 10⁻¹¹ 度。但要说清楚:Math.random() 不是密码学安全的随机源,涉及真金白银或凭证签发时不能靠它。
每次停下来的位置为什么都不太一样?
因为抽中之后会在扇区内加一点随机偏移,幅度是 0.7 × 半宽。默认盘里 45.00° 的格子半宽是 22.50°,所以指针可能落在正中央附近,也可能偏向边缘。这纯粹是观感处理——偏移在抽签之后才加,并且被限制在扇区内部,不会改变抽中的那一项。
窗口缩放或换到手机上,角度会不会错位?
不会。角度是在 300 × 300 的画布坐标系里算好的,缩放只是把这张画布按比例放大显示,扇区边界、指针位置和选中结果都不随之变化。反过来说,如果你把结果建立在"停下的角度"上,缩放和抗锯齿反而会放大误差——这也是本页坚持先抽签的原因之一。
最多能放几个选项?能拿它做发券或排班吗?
最多 24 个扇区,这是为了让每一格还放得下文字。它适合做一次性随机决定,不适合发券与排班:本页不保存历史、不去重、也不校验"同一人是否已经中过",刷新页面连选项都会清空。需要唯一凭证请用带发放记录的系统,需要排班请用带约束校验的排班工具。