擲筊
點擊擲出兩枚筊杯的 3D 翻轉動畫,落地判定聖杯/笑杯/陰杯並可綁 onResult 做趣味決策
廟宇文化的趣味互動:一張漸層供桌上放著兩枚紅色新月形筊杯(純 CSS,一面平、一面凸),閒置時微幅呼吸;點供桌或按 Enter/空白鍵,兩枚各自沿隨機軌跡拋起、多圈翻滾、落地小彈跳並揚起塵埃,落定後依「一平一凸=聖杯、兩平=笑杯、兩凸=陰杯」判定,以大字加短語顯示結果。可開啟「連續三聖杯才算」模式、注入固定種子的亂數、調整三種結果的權重,並透過 onResult 取得含歷史的完整結果——適合抽獎、決策小工具與節慶活動頁。
載入預覽⋯
npx shadcn@latest add https://webberui.com/r/moon-blocks-toss.jsonPlayground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
72
<MoonBlocksToss />
安裝
npx shadcn@latest add https://webberui.com/r/moon-blocks-toss.json或在 components.json 設定 registries 後,改用 @webberui/moon-blocks-toss 安裝。
使用
import { MoonBlocksToss } from "@/components/ui/moon-blocks-toss";
<MoonBlocksToss
questionText="今天要不要吃鹹酥雞?"
buttonText="擲筊問問看"
requireThree
onResult={(result, history) => {
console.log(`第 ${result.index} 擲:${result.outcome}`, result.faces);
if (result.confirmed) console.log("三聖杯定案!", history);
}}
/>想讓結果可重現(例如寫測試或做每日固定籤),把 rng 換成有種子的產生器;weights 則可以調整三種結果的相對機率(娛樂用途,預設即兩枚公平筊杯的理論比例 2:1:1):
import {
MoonBlocksToss,
pickMoonBlocksOutcome,
resolveMoonBlocks,
} from "@/components/ui/moon-blocks-toss";
// 簡單的線性同餘產生器:同一個種子每次都得到同樣的序列
function seeded(seed: number) {
let s = seed >>> 0;
return () => {
s = (s * 1664525 + 1013904223) >>> 0;
return s / 4294967296;
};
}
<MoonBlocksToss rng={seeded(20260818)} weights={{ sheng: 3, xiao: 1, yin: 1 }} />;
// 判定與抽樣函式可單獨使用
resolveMoonBlocks("flat", "round"); // "sheng"
pickMoonBlocksOutcome(Math.random); // "sheng" | "xiao" | "yin"Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
value | MoonBlocksResult | null | — | 受控:目前顯示的擲筊結果,null 為尚未擲出;不提供時為非受控模式 |
defaultValue | MoonBlocksResult | null | null | 非受控模式的初始結果 |
onChange | (result: MoonBlocksResult | null) => void | — | 結果變動時回呼:落定為新結果,按「重來」時為 null |
onResult | (result: MoonBlocksResult, history: MoonBlocksResult[]) => void | — | 每次落定時回呼,附上含本次在內的完整歷史(由舊到新) |
labels | Partial<MoonBlocksLabels> | 見下表 | 自訂結果名稱與短語(未給的欄位沿用預設) |
requireThree | boolean | false | 連續三聖杯才算數:顯示 3 個進度圓點,第三個聖杯落定時 result.confirmed 為 true |
questionText | string | — | 顯示在供桌上方的問題文字 |
buttonText | string | "擲筊" | 供桌下方擲筊按鈕的文字 |
size | number | 72 | 單枚筊杯的寬度(px),供桌高度與拋高依比例縮放 |
weights | MoonBlocksWeights | { sheng: 2, xiao: 1, yin: 1 } | 三種結果的相對權重(娛樂用途;預設為兩枚公平筊杯的理論比例 2:1:1) |
rng | () => number | Math.random | 亂數來源,需回傳 [0, 1) 區間的數字;可注入固定種子的產生器讓結果可重現 |
disabled | boolean | false | 停用擲筊 |
className | string | — | 透傳到最外層容器 |
MoonBlocksResult
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
outcome | "sheng" | "xiao" | "yin" | — | 判定結果:聖杯/笑杯/陰杯 |
faces | ["flat" | "round", "flat" | "round"] | — | 兩枚筊杯各自朝上的面(左、右);flat 平面、round 凸面 |
index | number | — | 本次是第幾擲(自 1 起算,按「重來」後歸零) |
streak | number | — | requireThree 模式下截至本次的連續聖杯數(0–3;非該模式恆為 0) |
confirmed | boolean | — | requireThree 模式下本次是否湊滿三連聖杯 |
MoonBlocksLabels
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
sheng | string | "聖杯" | 聖杯名稱 |
xiao | string | "笑杯" | 笑杯名稱 |
yin | string | "陰杯" | 陰杯名稱 |
shengHint | string | "允杯——同意,可以放心去做" | 聖杯短語 |
xiaoHint | string | "笑而不答——問法再想想,再擲一次" | 笑杯短語 |
yinHint | string | "不允——這次先別,換個方式問" | 陰杯短語 |
idle | string | "點桌面或按下方按鈕擲出" | 尚未擲出時的提示 |
tossing | string | "筊杯落地中……" | 筊杯落地前的提示 |
streak | string | "連續聖杯" | requireThree 模式下進度圓點旁的說明 |
confirmed | string | "三聖杯!定案" | requireThree 模式下湊滿三聖杯的短語 |
reset | string | "重來" | 重來按鈕文字 |
MoonBlocksWeights
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
sheng | number | 2 | 聖杯的相對權重 |
xiao | number | 1 | 笑杯的相對權重 |
yin | number | 1 | 陰杯的相對權重 |
權重只看相對比例,全為 0 或負數時會退回預設比例。另外具名匯出 resolveMoonBlocks(left, right)(由兩面判定結果)、pickMoonBlocksOutcome(rng, weights?)(依權重抽樣)、DEFAULT_MOON_BLOCKS_LABELS,以及 MoonBlocksOutcome、MoonBlockFace 型別,方便在外層重用同一套規則。
細節
- 判定規則:每枚筊杯一面平(陽面,較亮)、一面凸(陰面,較暗有隆起陰影)。一平一凸為聖杯(允杯)、兩平為笑杯(笑而不答)、兩凸為陰杯(不允);兩枚公平投擲的理論比例是 2:1:1,也就是預設
weights - 先抽結果再排面:亂數只在點擊事件裡取用(render 期間不碰
Math.random,避免水合不一致),先依權重抽出結果,再反推兩枚的面——聖杯時隨機決定哪一枚是平面,畫面不會每次都一樣 - 翻滾角度只增不減:每次拋擲在上一次落定的
rotateX/rotateY上再累加 2–4 圈與 1–2 圈(方向隨機),朝上的面靠 0°/180° 的半圈偏移決定,因此 motion 會從當前角度接著轉,不會先倒轉回零;兩面各自backface-visibility: hidden背對背貼合,露出哪一面完全由角度決定 - 拋物線與落地:
y以五個關鍵影格走「起拋 → 最高點 → 落地 → 小彈跳 → 靜止」,翻滾在落地時刻停止;桌面投影同步縮小變淡再恢復,塵埃小點延遲到落地瞬間才向兩側散開。兩枚各自的動畫結束後才寫入結果並觸發onResult,不用計時器,卸載也不需清理 - 供桌即按鈕:整塊供桌就是原生
button,點桌面、按鈕視覺或用鍵盤都能擲,拋擲中重複觸發會被忽略 - 連續三聖杯:
requireThree開啟時,每個聖杯讓streak加一、其他結果歸零,第三個聖杯把confirmed設為true並換成定案短語;下一擲會重新從零累計 - 受控模式:傳入
value後畫面跟隨父層給的結果(含兩枚的面),按「重來」時onChange收到null
可及性
- 整張供桌是單一原生
button(type="button"),Enter/空白鍵即可擲出;aria-label帶入按鈕文字與上一擲的結果,aria-busy標示拋擲中,aria-describedby連到結果區;筊杯、投影與塵埃皆為裝飾並以aria-hidden隱藏 - 結果區為
role="status"加aria-live="polite",落定後螢幕閱讀器會朗讀結果名稱與短語;requireThree的進度圓點以role="img"與aria-label朗讀「連續聖杯 n / 3」 - 供桌與重來按鈕皆有
focus-visible焦點環,disabled時降低不透明度並停用互動 - 使用者系統開啟「減少動態效果」時,停用翻滾、拋物線、塵埃與閒置呼吸,按下即直接落定並顯示結果