随机转盘

把候选项一行一条写进选项框,可以给每条加权,扇区圆心角按 θ = 2π × w / Σw 精确分配;点开始后先用 Math.random 按权重抽出扇区,再把指针转到它上面,动画只是表现。默认 6 个扇区、总权重 8,最多 24 个扇区。

默认盘已就绪:6 个扇区、总权重 8。在选项框里按 Ctrl/Cmd+Enter 也能直接开转。
扇区数6
总权重 Σw8
最大圆心角135.00°
最小圆心角45.00°

使用方法

  1. 「选项列表」默认已经填好 6 行:披萨|3,加上 汉堡|1、寿司|1、沙拉|1、面条|1、咖喱|1。一行就是一个扇区,竖线后面是权重,省略竖线的行按 1 算。
  2. 「转速档」默认 5,只接受 1~10 的整数,对应动画时长 4000 ms;它只决定转多快,不改变任何一项的命中概率。
  3. 「转动圈数」默认 5,只接受 3~12 的整数。圈数越多,最后那段慢动作越像真的在减速。
  4. 点「开始转动」。也可以把光标放在选项框里按 Ctrl/Cmd+Enter(单独的 Enter 留给换行),或者在转速、圈数输入框里按 Enter。
  5. 结果区给出选中的那一项、权重、命中概率与圆心角;「复制结果」只带走这一行文字,「重置为默认示例」恢复上面那 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 = 8、披萨 135.00° / 37.50%、其余各 45.00° / 12.50%。

扇区数与权重对照表

下表第一列是扇区数量,后面三列说明等权时每一格有多大。半径固定取 140 px(与画布上扇区的外接半径一致),弧长 = 半径 × 圆心角。

扇区数 n等权圆心角每扇区概率半径 140 px 弧长
2180.00°50.00%439.82 px
3120.00°33.33%293.22 px
490.00°25.00%219.91 px
572.00°20.00%175.93 px
6(默认)60.00°16.67%146.61 px
845.00°12.50%109.96 px
940.00°11.11%97.74 px
1036.00°10.00%87.96 px
1230.00°8.33%73.30 px
1524.00°6.67%58.64 px
1820.00°5.56%48.87 px
2018.00°5.00%43.98 px
24(上限)15.00°4.17%36.65 px
3012.00°3.33%29.32 px
3610.00°2.78%24.43 px
487.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(默认)8135.00°37.50%45.00°12.50%
六个都写 |1(等权)660.00°16.67%60.00°16.67%
|1 |2 |3 |410144.00°40.00%36.00°10.00%
|2 |2 |1 |16120.00°33.33%60.00°16.67%
|5 |1 |1 |18225.00°62.50%45.00°12.50%
十二个都写 |11230.00°8.33%30.00°8.33%
一项 |0 + 五个 |1572.00°20.00%72.00°20.00%

两张表横着看就清楚了:扇区数决定"每一格有多大",权重决定"格与格之间怎么分"。要改概率就改权重,要改一格能放多少文字就改扇区数,两者互不干扰。也正因为面积等于概率,把某一项权重写成 5 之后它占 225.00°(62.50%),这就是它真实的胜率,不会出现"看起来很大其实抽不到"的情况。

输入范围、边界与精度说明

下面这些是刻意设死的边界,知道它们能避免把这页当万能工具用:

常见问题

结果到底是谁决定的,是随机数还是指针?

是随机数。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 个扇区,这是为了让每一格还放得下文字。它适合做一次性随机决定,不适合发券与排班:本页不保存历史、不去重、也不校验"同一人是否已经中过",刷新页面连选项都会清空。需要唯一凭证请用带发放记录的系统,需要排班请用带约束校验的排班工具。

相关工具