初始提交:三玩法 Web 原型与设计文档

- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
cabinw
2026-09-16 14:30:59 +08:00
commit 8026e4628d
169 changed files with 22162 additions and 0 deletions
+543
View File
@@ -0,0 +1,543 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>数字一笔 · UI 设计规范 v1.5</title>
<style>
/* ====== 令牌:与 modes/01-number-stroke/web/style.css :root 一致 ====== */
:root {
--bg: #ffffff; --bg-app: #f2f2f6; --surface-2: #f3f3f7; --cell: #e6e7ec; --line: #e1e2ea;
--ink: #23243a; --ink-2: #6b6d80; --ink-3: #a3a5b5;
--primary: #6558f5; --primary-bright: #7d72ff; --primary-soft: #eceafe; --on-primary: #ffffff;
--danger: #e5484d; --danger-soft: #fdecec; --hint: #f5a524; --hint-soft: #fef3dd;
--clue-ring: #23243a; --clue-ring-active: #dfe1ec; --overlay: rgba(35, 36, 58, 0.92);
--shadow-primary: 0 6px 18px rgba(101, 88, 245, 0.28); --shadow-frame: 0 20px 60px rgba(20, 20, 40, 0.14);
--font: ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif;
--fs-display: 34px; --fs-title: 22px; --fs-heading: 17px; --fs-body: 15px; --fs-label: 14px; --fs-caption: 13px; --fs-micro: 12px;
--r-sm: 10px; --r-md: 14px; --r-lg: 18px; --r-xl: 24px; --r-pill: 999px;
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-7: 28px;
--t-fast: 120ms; --t-base: 200ms; --t-slow: 320ms; --t-hold: 500ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--u: 64px; /* 样张里的单位格 */
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg-app); color: var(--ink); font-family: var(--font); font-size: 15px; line-height: 1.5; font-variant-numeric: tabular-nums; -webkit-font-smoothing: antialiased; }
a { color: var(--primary); text-decoration: none; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; background: var(--surface-2); padding: 1px 5px; border-radius: 5px; }
/* ====== 文档骨架 ====== */
.doc { display: grid; grid-template-columns: 220px 1fr; min-height: 100vh; }
nav { position: sticky; top: 0; height: 100vh; padding: 28px 20px; border-right: 1px solid var(--line); background: var(--bg); overflow: auto; }
nav h1 { font-size: 18px; font-weight: 800; margin: 0 0 4px; }
nav .ver { color: var(--ink-3); font-size: 12px; font-weight: 600; margin-bottom: 20px; }
nav a { display: block; padding: 6px 8px; border-radius: 8px; color: var(--ink-2); font-size: 14px; font-weight: 600; }
nav a:hover { background: var(--surface-2); color: var(--ink); }
main { padding: 36px 48px 96px; max-width: 1180px; }
section { margin-bottom: 64px; }
h2 { font-size: 26px; font-weight: 800; margin: 0 0 6px; letter-spacing: 0.01em; }
h3 { font-size: 17px; font-weight: 700; margin: 28px 0 10px; }
.lead { color: var(--ink-2); margin: 0 0 20px; max-width: 720px; }
.card { background: var(--bg); border: 1px solid var(--line); border-radius: 16px; padding: 20px; }
.grid { display: grid; gap: 14px; }
.g2 { grid-template-columns: repeat(2, 1fr); } .g3 { grid-template-columns: repeat(3, 1fr); } .g4 { grid-template-columns: repeat(4, 1fr); }
table { width: 100%; border-collapse: collapse; background: var(--bg); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; font-size: 13.5px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; }
th { background: var(--surface-2); color: var(--ink-2); font-weight: 700; font-size: 12.5px; }
tr:last-child td { border-bottom: 0; }
.note { font-size: 13px; color: var(--ink-2); }
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font-size: 12px; font-weight: 700; }
.sync { background: var(--primary-soft); color: var(--primary); border-radius: 12px; padding: 12px 16px; font-size: 13.5px; font-weight: 600; margin-bottom: 28px; }
/* ====== 色板 ====== */
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 12px; }
.sw { background: var(--bg); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.sw .chip { height: 64px; border-bottom: 1px solid var(--line); }
.sw .meta { padding: 10px 12px; }
.sw b { display: block; font-size: 13.5px; }
.sw code { display: inline-block; margin-top: 3px; }
.sw small { display: block; color: var(--ink-2); font-size: 12px; margin-top: 4px; }
/* ====== 字阶 ====== */
.type-row { display: grid; grid-template-columns: 150px 1fr 220px; align-items: baseline; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--line); }
.type-row:last-child { border-bottom: 0; }
.type-row .lab { color: var(--ink-3); font-size: 12px; font-weight: 700; }
.type-row .use { color: var(--ink-2); font-size: 13px; }
/* ====== 盘面样张 ====== */
.board { position: relative; display: inline-grid; grid-auto-rows: var(--u); grid-auto-columns: var(--u); background: var(--bg); padding: 14px; border-radius: 16px; border: 1px solid var(--line); }
.bc { position: relative; width: var(--u); height: var(--u); padding: calc(var(--u) * 0.05); }
.bc > i { position: absolute; inset: calc(var(--u) * 0.05); border-radius: calc(var(--u) * 0.18); background: var(--cell); display: flex; align-items: center; justify-content: center; font-style: normal; font-weight: 800; font-size: calc(var(--u) * 0.52); color: var(--ink); }
.bc.two > i { font-size: calc(var(--u) * 0.46); }
.bc.clue > i { box-shadow: inset 0 0 0 calc(var(--u) * 0.05) var(--clue-ring); }
.bc.start > i, .bc.visited > i { background: var(--primary); color: var(--on-primary); }
.bc.start > i { font-size: calc(var(--u) * 0.6); }
.bc.start > i, .bc.visited.clue > i { box-shadow: inset 0 0 0 calc(var(--u) * 0.05) var(--clue-ring-active); }
.bc.bright > i { background: var(--primary-bright); }
.bc.conflict > i { background: var(--danger); color: var(--on-primary); }
.bc.parity > i { font-size: calc(var(--u) * 0.44); font-weight: 700; }
.bc.visited.parity > i { flex-direction: column; font-size: calc(var(--u) * 0.52); font-weight: 800; line-height: 1; }
.bc.visited.parity > i small { font-size: calc(var(--u) * 0.19); font-weight: 700; margin-top: calc(var(--u) * 0.04); }
.bc .badge { position: absolute; right: calc(var(--u) * 0.08); top: calc(var(--u) * 0.08); width: calc(var(--u) * 0.3); height: calc(var(--u) * 0.3); border-radius: 50%; background: var(--ink); color: #fff; font-size: calc(var(--u) * 0.19); font-weight: 800; display: flex; align-items: center; justify-content: center; }
.bc.hint > i { box-shadow: 0 0 0 calc(var(--u) * 0.08) var(--hint); }
.bc.dim > i::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: rgba(255,255,255,.45); }
.bc.big { width: calc(var(--u) * 2); height: calc(var(--u) * 2); grid-column: span 2; grid-row: span 2; }
.bridge { position: absolute; background: var(--primary); z-index: 1; }
.bridge.h { width: calc(var(--u) * 0.1 + 2px); height: calc(var(--u) * 0.46); }
.bridge.v { height: calc(var(--u) * 0.1 + 2px); width: calc(var(--u) * 0.46); }
.bridge.red { background: var(--danger); }
.legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
.legend .bc { --u: 72px; }
.legend figure { margin: 0; text-align: center; }
.legend figcaption { font-size: 12.5px; color: var(--ink-2); margin-top: 6px; font-weight: 600; }
/* ====== 组件(与 style.css 同规格) ====== */
.topbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 52px; }
.icon-btn { justify-self: start; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; color: var(--ink-2); border-radius: 999px; margin-left: -8px; background: none; border: 0; }
.level-info { text-align: center; line-height: 1.1; }
.level-title { font-size: var(--fs-title); font-weight: 800; letter-spacing: .02em; }
.level-sub { margin-top: 3px; color: var(--ink-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .04em; white-space: nowrap; }
.remaining { justify-self: end; display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 12px 0 10px; border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font-size: 14px; font-weight: 700; min-width: 52px; justify-content: center; }
.remaining svg { color: var(--ink-3); }
.remaining.done { background: var(--primary-soft); color: var(--primary); } .remaining.done svg { color: var(--primary); }
.remaining.bump { animation: bump var(--t-base) var(--ease-spring); }
.level-progress { display: flex; gap: 3px; height: 4px; margin: 8px 4px 0; }
.level-progress i { flex: 1; border-radius: 2px; background: var(--cell); }
.level-progress i.cleared { background: var(--primary); opacity: .4; } .level-progress i.current { background: var(--primary); }
.caption { height: 36px; margin-top: 8px; display: flex; align-items: center; justify-content: center; color: var(--ink-2); font-size: 15px; font-weight: 500; }
.gesture-help { margin: 0; padding-top: 8px; color: var(--ink-2); font-size: 13px; text-align: center; }
.bottombar { display: flex; justify-content: center; gap: 12px; padding-top: 12px; }
.action { flex: 1; max-width: 170px; height: 56px; border-radius: 18px; background: var(--surface-2); color: var(--ink); font: inherit; font-size: 15px; font-weight: 700; display: flex; align-items: center; justify-content: center; gap: 4px; border: 0; cursor: pointer; transition: transform var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out); }
.action svg { color: var(--ink-2); } .action:active { transform: scale(.97); background: var(--cell); }
.action.pulse { animation: pulse .9s var(--ease-in-out) infinite; } .action:disabled { opacity: .4; }
.toast { display: inline-block; padding: 10px 16px; border-radius: 14px; background: var(--overlay); color: #fff; font-size: 14px; font-weight: 600; max-width: 100%; text-align: center; }
.toast.anim { animation: toast-in var(--t-base) var(--ease-out) both; }
.primary-btn { height: 54px; border-radius: 18px; background: var(--primary); color: #fff; font: inherit; font-size: 17px; font-weight: 700; letter-spacing: .02em; box-shadow: var(--shadow-primary); border: 0; width: 100%; cursor: pointer; transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); }
.primary-btn:active { transform: scale(.98); box-shadow: 0 3px 10px rgba(101,88,245,.22); }
.chip-btn { height: 34px; padding: 0 16px; border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font: inherit; font-size: 13px; font-weight: 600; border: 0; cursor: pointer; }
.chip-btn.on { background: var(--primary-soft); color: var(--primary); } .chip-btn.danger { color: var(--danger); }
.level-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
.tile { position: relative; aspect-ratio: 1; border-radius: 14px; background: var(--cell); color: var(--ink); font: inherit; font-size: 20px; font-weight: 700; display: flex; align-items: center; justify-content: center; border: 0; cursor: pointer; transition: transform var(--t-fast) var(--ease-out); }
.tile:active { transform: scale(.94); }
.tile.cleared { background: var(--primary); color: #fff; }
.tile.cleared::after { content: ""; position: absolute; right: 8px; top: 6px; width: 4px; height: 8px; border: solid rgba(255,255,255,.9); border-width: 0 2px 2px 0; transform: rotate(45deg); }
.tile.current { box-shadow: inset 0 0 0 3px var(--primary); background: var(--primary-soft); color: var(--primary); animation: breathe 2.4s var(--ease-in-out) infinite; }
.tile.locked { background: var(--surface-2); color: var(--ink-3); } .tile.locked::before { content: ""; width: 10px; height: 12px; border: 2px solid var(--ink-3); border-radius: 3px; border-top-width: 3px; } .tile.locked span { display: none; }
.tile.shake { animation: shake var(--t-slow) var(--ease-out); }
.overall { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.overall-bar { flex: 1; height: 6px; border-radius: 999px; background: var(--cell); overflow: hidden; } .overall-bar i { display: block; height: 100%; border-radius: 999px; background: var(--primary); width: 62%; }
.overall-text { color: var(--ink-3); font-size: 13px; font-weight: 600; }
/* ====== 手机样机 ====== */
.phones { display: flex; gap: 32px; flex-wrap: wrap; }
.phone { width: 390px; height: 760px; background: var(--bg); border-radius: 28px; box-shadow: var(--shadow-frame); padding: 12px 20px 16px; display: flex; flex-direction: column; overflow: hidden; }
.phone h1 { margin: 0; font-size: 34px; font-weight: 800; letter-spacing: .02em; line-height: 1.15; }
.phone .sub { margin: 8px 0 0; color: var(--ink-2); }
.pack { margin-top: 28px; } .pack-title { display: flex; justify-content: space-between; align-items: baseline; padding: 0 4px 12px; font-size: 17px; font-weight: 700; } .pack-progress { color: var(--ink-3); font-size: 14px; font-weight: 600; } .pack.done .pack-progress { color: var(--primary); }
.phone .list { flex: 1; overflow: hidden; margin-top: 12px; -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 20px), transparent); mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 20px), transparent); }
.phone .footer { display: flex; justify-content: center; gap: 8px; padding-top: 16px; }
.phone .board-area { flex: 1; display: flex; align-items: center; justify-content: center; padding-bottom: 6%; }
.phone .board { --u: 78px; border: 0; padding: 0; }
/* ====== 动效样张 ====== */
.demo { background: var(--bg); border: 1px solid var(--line); border-radius: 16px; padding: 18px; display: flex; flex-direction: column; gap: 12px; }
.demo header { display: flex; justify-content: space-between; align-items: center; }
.demo header b { font-size: 14px; }
.demo header span { color: var(--ink-3); font-size: 12px; font-weight: 700; }
.demo .stage { height: 120px; display: flex; align-items: center; justify-content: center; gap: 8px; background: var(--surface-2); border-radius: 12px; position: relative; overflow: hidden; }
.demo .stage .bc { --u: 64px; }
.demo p { margin: 0; font-size: 12.5px; color: var(--ink-2); }
.replay { height: 30px; padding: 0 12px; border-radius: 999px; background: var(--primary-soft); color: var(--primary); font: inherit; font-size: 12.5px; font-weight: 700; border: 0; cursor: pointer; }
.a-pop > i { animation: pop 220ms var(--ease-out) both; }
.a-fade > i span { animation: fade var(--t-fast) var(--ease-out) both; }
.a-flash > i::before { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 0 calc(var(--u) * 0.05) #fff; animation: flash var(--t-slow) linear both; }
.a-shake > i { animation: cell-shake 280ms linear both; }
.a-bridge .bridge { transform-origin: center; animation: bridge-grow var(--t-fast) var(--ease-out) both; }
.a-sweep > i::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: #fff; opacity: 0; animation: sweep 250ms linear both; animation-delay: calc(var(--i) * 64ms); }
.a-enter > i { animation: enter var(--t-base) var(--ease-out) both; animation-delay: calc(var(--i) * 14ms); }
.a-screen { animation: screen-in var(--t-base) var(--ease-out) both; }
.a-hint > i { animation: hint-pulse 2.4s linear both; }
.timeline { display: grid; grid-template-columns: 320fr 500fr 160fr 400fr; gap: 3px; margin-top: 10px; }
.timeline div { padding: 10px 10px; border-radius: 8px; font-size: 12.5px; font-weight: 700; color: var(--ink-2); background: var(--surface-2); }
.timeline div small { display: block; font-weight: 600; color: var(--ink-3); }
.timeline .p1 { background: var(--primary-soft); color: var(--primary); } .timeline .p3 { background: var(--cell); }
@keyframes pop { 0% { transform: scale(1); } 35% { transform: scale(1.12); } 70% { transform: scale(.985); } 100% { transform: scale(1); } }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes flash { from { opacity: 1; } to { opacity: 0; } }
@keyframes cell-shake { 0%,100% { transform: translateX(0); } 8% { transform: translateX(6%); } 25% { transform: translateX(-5%); } 42% { transform: translateX(4%); } 58% { transform: translateX(-3%); } 75% { transform: translateX(2%); } 92% { transform: translateX(-1%); } }
@keyframes bridge-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes sweep { 0% { opacity: 0; } 50% { opacity: .55; } 100% { opacity: 0; } }
@keyframes enter { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: scale(1); } }
@keyframes screen-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes bump { 0% { transform: scale(1); } 40% { transform: scale(1.14); } 100% { transform: scale(1); } }
@keyframes breathe { 0%,100% { box-shadow: inset 0 0 0 3px var(--primary), 0 0 0 0 rgba(101,88,245,0); } 50% { box-shadow: inset 0 0 0 3px var(--primary), 0 0 0 6px rgba(101,88,245,.16); } }
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(245,165,36,0); } 50% { box-shadow: 0 0 0 5px rgba(245,165,36,.35); } }
@keyframes shake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 50% { transform: translateX(5px); } 75% { transform: translateX(-3px); } }
@keyframes hint-pulse { 0% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,.55); } 15% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,1); } 30% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,.2); } 45% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,1); } 60% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,.2); } 75% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,1); } 100% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,0); } }
@media (max-width: 900px) { .doc { grid-template-columns: 1fr; } nav { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line); } main { padding: 24px 20px 64px; } .g2, .g3, .g4 { grid-template-columns: 1fr; } .type-row { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="doc">
<nav>
<h1>数字一笔 · UI 规范</h1>
<div class="ver">v1.5 · 2026-09-16</div>
<a href="#principles">1 设计原则</a>
<a href="#color">2 色彩</a>
<a href="#type">3 字体与字阶</a>
<a href="#space">4 间距 · 圆角 · 尺寸</a>
<a href="#board">5 盘面</a>
<a href="#components">6 组件</a>
<a href="#screens">7 页面</a>
<a href="#motion">8 动效</a>
<a href="#sound">9 声音与振动</a>
<a href="#sync">10 同步规则</a>
</nav>
<main>
<div class="sync">本页与 <code>docs/design.md</code> 同步维护。令牌真值在 <code>web/style.css :root</code> 与 <code>src/theme.ts</code>,由 <code>tests/theme.test.ts</code> 守门。改设计:先改 design.md → 改代码令牌 → 更新本页样张。</div>
<section id="principles">
<h2>1 · 设计原则</h2>
<p class="lead">逻辑 puzzle 的盘面只回答三个问题:我走到哪了、哪错了、过没过。</p>
<div class="grid g3">
<div class="card"><b>板要干净</b><p class="note">盘面无阴影、无纹理、无装饰。灰阶承担一切非路径信息。</p></div>
<div class="card"><b>一个主色</b><p class="note">蓝紫只表示「已走 / 当前 / 可继续」。红只表示冲突,琥珀只表示提示。不每关换色。</p></div>
<div class="card"><b>数字优先</b><p class="note">系统圆体、800 字重、等宽数字。其余文字都比数字轻。</p></div>
<div class="card"><b>动效四档</b><p class="note">只用 120 / 200 / 320 / 500 ms 与四条缓动。新动效先归档再写。</p></div>
<div class="card"><b>反馈贴手指</b><p class="note">落子、踩对、冲突、死路都发生在手指所在格子上。不弹窗、不撒纸片。</p></div>
<div class="card"><b>信息随手可见</b><p class="note">关号、包名、本包进度、剩余格数常驻顶栏。</p></div>
</div>
</section>
<section id="color">
<h2>2 · 色彩</h2>
<p class="lead">浅色单主题。DOM 用 CSS 变量,Canvas 用 <code>THEME</code>,同名同值。</p>
<div class="swatches" id="swatches"></div>
<h3>语义</h3>
<table>
<tr><th style="width:120px">颜色</th><th>只能用于</th><th>不能用于</th></tr>
<tr><td><span class="tag" style="background:var(--primary);color:#fff">主色</span></td><td>已走格、桥、起点 1、当前关、主按钮、进度</td><td>装饰、背景、标题文字</td></tr>
<tr><td><span class="tag" style="background:var(--danger);color:#fff">冲突</span></td><td>冲突格、「重置进度」</td><td>任何常驻元素、警示以外的强调</td></tr>
<tr><td><span class="tag" style="background:var(--hint);color:#fff">提示</span></td><td>提示描边、提示按钮脉冲</td><td>任何常驻元素</td></tr>
<tr><td><span class="tag" style="background:var(--primary-bright);color:#fff">主色·亮</span></td><td>过关后的全盘提亮(唯一允许的换色)</td><td>过关以外</td></tr>
</table>
<h3>对比度</h3>
<table>
<tr><th>组合</th><th>对比度</th><th>用途</th></tr>
<tr><td>白 on 主色</td><td>5.2 : 1</td><td>已走格数字、主按钮</td></tr>
<tr><td>墨 on 未走格</td><td>12.6 : 1</td><td>未走线索数字</td></tr>
<tr><td>白 on 冲突红</td><td>4.5 : 1</td><td>冲突格数字(角标另有墨底白字)</td></tr>
<tr><td>墨 2 on 白</td><td>5.1 : 1</td><td>副文案</td></tr>
<tr><td>墨 on 未走格</td><td>12.6 : 1</td><td>未走线索描边(与数字同色)</td></tr>
<tr><td>墨 3 on 白</td><td>2.5 : 1</td><td>仅非关键弱文案</td></tr>
</table>
</section>
<section id="type">
<h2>3 · 字体与字阶</h2>
<p class="lead">字体栈:<code>ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif</code>。全局等宽数字。</p>
<div class="card">
<div class="type-row"><div class="lab">display · 34 / 800</div><div style="font-size:34px;font-weight:800;line-height:1.15">数字一笔 0123</div><div class="use">选关页标题</div></div>
<div class="type-row"><div class="lab">title · 22 / 800</div><div style="font-size:22px;font-weight:800;letter-spacing:.02em">1-7 · 2-10 · 3-6</div><div class="use">顶栏关号</div></div>
<div class="type-row"><div class="lab">heading · 17 / 700</div><div style="font-size:17px;font-weight:700">入门 · 进阶 · 奇与偶 · 继续 · 2-1</div><div class="use">包标题、主按钮</div></div>
<div class="type-row"><div class="lab">body · 15 / 500</div><div style="font-size:15px;font-weight:500;color:var(--ink-2)">从 1 开始,滑到旁边的格</div><div class="use">副标题、字幕</div></div>
<div class="type-row"><div class="lab">label · 14 / 700</div><div style="font-size:14px;font-weight:700;color:var(--ink-2)">6 · 完成 · 10 / 10 · 下一步在这里</div><div class="use">胶囊、包进度、toast</div></div>
<div class="type-row"><div class="lab">caption · 13 / 600</div><div style="font-size:13px;font-weight:600;color:var(--ink-2)">音效 · 振动 · 16 / 26</div><div class="use">chip、总进度数字</div></div>
<div class="type-row"><div class="lab">micro · 12 / 600</div><div style="font-size:12px;font-weight:600;color:var(--ink-3);letter-spacing:.04em">入门 · 第 7 关</div><div class="use">顶栏副标题</div></div>
</div>
<h3>盘面字阶(相对单位格 u)</h3>
<div class="card" style="display:flex;gap:28px;align-items:flex-end;flex-wrap:wrap">
<figure style="margin:0;text-align:center"><div class="bc start" style="--u:96px"><i>1</i></div><figcaption class="note">起点 0.60 u · 800</figcaption></figure>
<figure style="margin:0;text-align:center"><div class="bc visited" style="--u:96px"><i>7</i></div><figcaption class="note">单位数 0.52 u · 800</figcaption></figure>
<figure style="margin:0;text-align:center"><div class="bc visited two" style="--u:96px"><i>14</i></div><figcaption class="note">两位数 0.46 u · 800</figcaption></figure>
<figure style="margin:0;text-align:center"><div class="bc clue parity" style="--u:96px"><i>奇</i></div><figcaption class="note">奇偶 0.44 u · 700</figcaption></figure>
<figure style="margin:0;text-align:center"><div class="bc visited clue parity" style="--u:96px"><i>6<small>偶</small></i></div><figcaption class="note">小字 0.19 u · 700</figcaption></figure>
<figure style="margin:0;text-align:center"><div class="bc visited conflict" style="--u:96px"><i>6</i><b class="badge">8</b></div><figcaption class="note">角标 r=0.15 u · 字 1.25 r</figcaption></figure>
</div>
</section>
<section id="space">
<h2>4 · 间距 · 圆角 · 尺寸</h2>
<div class="grid g3">
<div class="card"><b>间距 · 4pt 网格</b><p class="note">4 / 8 / 12 / 16 / 20 / 24 / 28。页面左右 20,顶 12 + 安全区,底 16 + 安全区。</p></div>
<div class="card"><b>圆角</b><p class="note">sm 10 · md 14(关卡格、toast)· lg 18(主按钮、底栏按钮)· xl 24 · 胶囊 999 · 桌面框 28。盘面格 0.18 u。</p></div>
<div class="card"><b>尺寸</b><p class="note">点击区 ≥ 44。顶栏 52,胶囊 32,底栏按钮 56,主按钮 54,chip 34,关卡格 5 列间距 12。</p></div>
</div>
<div class="card" style="margin-top:14px;display:flex;gap:20px;align-items:center;flex-wrap:wrap">
<div style="width:10px;height:10px;border-radius:10px;background:var(--cell)"></div><span class="note">10</span>
<div style="width:14px;height:14px;border-radius:14px;background:var(--cell)"></div><span class="note">14</span>
<div style="width:18px;height:18px;border-radius:18px;background:var(--cell)"></div><span class="note">18</span>
<div style="width:24px;height:24px;border-radius:24px;background:var(--cell)"></div><span class="note">24</span>
<div style="width:60px;height:60px;border-radius:14px;background:var(--cell)"></div><span class="note">关卡格 14</span>
<div style="width:120px;height:56px;border-radius:18px;background:var(--surface-2)"></div><span class="note">底栏按钮 18</span>
<div style="width:64px;height:64px;border-radius:11.5px;background:var(--cell)"></div><span class="note">盘面格 0.18 u</span>
</div>
</section>
<section id="board">
<h2>5 · 盘面</h2>
<p class="lead">格圆角 0.18 u、缝 0.10 u、桥厚 0.46 u。整盘垂直居中后上提 3%。四周死区:触屏 8%,鼠标 14%。点任意已走格直接回退,可接着滑动。</p>
<h3>格的状态</h3>
<div class="legend card">
<figure><div class="bc"><i></i></div><figcaption>未走</figcaption></figure>
<figure><div class="bc clue"><i>8</i></div><figcaption>未走线索</figcaption></figure>
<figure><div class="bc clue parity"><i>偶</i></div><figcaption>未走奇偶</figcaption></figure>
<figure><div class="bc start"><i>1</i></div><figcaption>起点 1(未按下)</figcaption></figure>
<figure><div class="bc visited"><i>5</i></div><figcaption>已走</figcaption></figure>
<figure><div class="bc visited clue"><i>8</i></div><figcaption>已走线索</figcaption></figure>
<figure><div class="bc visited clue parity"><i>6<small>偶</small></i></div><figcaption>已走奇偶</figcaption></figure>
<figure><div class="bc visited conflict"><i>6</i><b class="badge">8</b></div><figcaption>冲突 + 角标</figcaption></figure>
<figure><div class="bc visited conflict"><i>5</i></div><figcaption>错位:红格 + 轻晃</figcaption></figure>
<figure><div class="bc clue hint"><i>5</i></div><figcaption>补数字:1~9 补 5</figcaption></figure>
<figure><div class="bc clue hint"><i>4</i></div><figcaption>提示目标</figcaption></figure>
<figure><div class="bc visited dim"><i>9</i></div><figcaption>退回压暗</figcaption></figure>
<figure><div class="bc visited bright"><i>12</i></div><figcaption>过关提亮</figcaption></figure>
</div>
<h3>样张:1-7 半途(含大格、紫色连线、红色错格)</h3>
<div class="card" style="display:flex;gap:32px;flex-wrap:wrap;align-items:flex-start">
<div class="board" id="board-sample"></div>
<div class="board" id="board-sample-2"></div>
<p class="note" style="max-width:280px">左:路径 1→2→3→4→5,6 踩到线索 8 冲突,只让冲突格变红,连接线始终紫色。右:2×2 大格只计一步,异形格由单位矩形 + 接缝同色拼成。桥保持路径色,与出错格的颜色分开。</p>
</div>
</section>
<section id="components">
<h2>6 · 组件</h2>
<h3>顶栏 · 本包进度条 · 字幕</h3>
<div class="card" style="max-width:440px">
<div class="topbar">
<button class="icon-btn" aria-label="返回"><svg viewBox="0 0 24 24" width="26" height="26"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<div class="level-info"><div class="level-title">1-7</div><div class="level-sub">入门 · 第 7 关</div></div>
<div class="remaining" id="pill-demo"><svg viewBox="0 0 24 24" width="16" height="16"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><span>6</span></div>
</div>
<div class="level-progress"><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="current"></i><i></i><i></i><i></i></div>
<div class="caption">走错了,沿原路滑回去</div>
<div style="display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap">
<div class="remaining"><svg viewBox="0 0 24 24" width="16" height="16"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><span>12</span></div>
<div class="remaining done"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span>完成</span></div>
<button class="replay" onclick="replay('#pill-demo','bump')">回放 bump</button>
<span class="note">胶囊:默认 / 完成态</span>
</div>
</div>
<h3>底栏按钮 · Toast</h3>
<p class="note">三个等宽按钮,高 56px,15px 字号;补数字保留在本局盘面,指路沿用下一步 / 回退高亮。Toast 距页面底边 128px + 安全区,长句换行,避开手势说明。</p>
<div class="card" style="max-width:440px">
<p class="gesture-help">按住滑动连线 · 点已走格回退</p>
<div class="bottombar">
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 12a8 8 0 1 0 2.5-5.8" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M4 4v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>重开</span></button>
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.6.5 1 1.2 1.1 2.2h5c.1-1 .5-1.7 1.1-2.2A6 6 0 0 0 12 3z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>补数字</span></button>
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M5 19v-7a4 4 0 0 1 4-4h10m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>指路</span></button>
</div>
<div class="bottombar">
<button class="action pulse"><span>重开(脉冲)</span></button>
<button class="action" disabled><span>禁用</span></button>
</div>
<div style="margin-top:14px;display:flex;gap:10px;align-items:center"><span class="toast" id="toast-demo">下一步在这里</span><button class="replay" onclick="replay('#toast-demo','anim')">回放</button></div>
</div>
<h3>选关:总进度 · 关卡格四态 · 主按钮 · chip</h3>
<div class="card" style="max-width:440px">
<div class="overall"><div class="overall-bar"><i></i></div><span class="overall-text">16 / 26</span></div>
<div class="pack"><div class="pack-title"><span>进阶</span><span class="pack-progress">2 / 10</span></div>
<div class="level-grid">
<button class="tile cleared"><span>1</span></button><button class="tile cleared"><span>2</span></button><button class="tile current"><span>3</span></button><button class="tile"><span>4</span></button><button class="tile locked" id="tile-locked" onclick="replay('#tile-locked','shake')"><span>5</span></button>
</div>
<p class="note" style="margin:8px 0 0">已过关 / 已过关 / 当前(呼吸)/ 未过关 / 锁定(点一下抖动)</p>
</div>
<div style="margin-top:18px"><button class="primary-btn">继续 · 2-3</button></div>
<div class="phone footer" style="display:flex;gap:8px;justify-content:center;padding-top:14px"><button class="chip-btn on">音效</button><button class="chip-btn">振动</button><button class="chip-btn danger">重置进度</button></div>
</div>
</section>
<section id="screens">
<h2>7 · 页面</h2>
<p class="lead">390 × 760 样机,桌面浏览器上以 480px 手机框居中,圆角 28 + 框阴影。</p>
<div class="phones">
<div class="phone">
<div style="padding:28px 4px 8px"><h1>数字一笔</h1><p class="sub">按数字顺序,一笔走遍每一格</p>
<div class="overall"><div class="overall-bar"><i></i></div><span class="overall-text">16 / 26</span></div></div>
<div class="list" id="phone-packs"></div>
<button class="primary-btn" style="margin-top:auto">继续 · 2-1</button>
<div class="footer"><button class="chip-btn on">音效</button><button class="chip-btn on">振动</button><button class="chip-btn danger">重置进度</button></div>
</div>
<div class="phone">
<div class="topbar">
<button class="icon-btn"><svg viewBox="0 0 24 24" width="26" height="26"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<div class="level-info"><div class="level-title">1-7</div><div class="level-sub">入门 · 第 7 关</div></div>
<div class="remaining"><svg viewBox="0 0 24 24" width="16" height="16"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><span>6</span></div>
</div>
<div class="level-progress"><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="current"></i><i></i><i></i><i></i></div>
<div class="caption"></div>
<div class="board-area"><div class="board" id="phone-board"></div></div>
<p class="gesture-help">按住滑动连线 · 点已走格回退</p>
<div class="bottombar">
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 12a8 8 0 1 0 2.5-5.8" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M4 4v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>重开</span></button>
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.6.5 1 1.2 1.1 2.2h5c.1-1 .5-1.7 1.1-2.2A6 6 0 0 0 12 3z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>补数字</span></button>
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M5 19v-7a4 4 0 0 1 4-4h10m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>指路</span></button>
</div>
</div>
</div>
</section>
<section id="motion">
<h2>8 · 动效</h2>
<p class="lead">四档时长、四条缓动。Canvas 侧的弹簧与扫光在这里用 CSS 近似回放,参数以 <code>src/theme.ts MOTION</code> 为准。</p>
<table>
<tr><th>档</th><th>时长</th><th>用途</th><th>缓动</th></tr>
<tr><td>fast</td><td>120ms</td><td>按下反馈、步号淡入、桥生长</td><td>ease-out <code>cubic-bezier(.22,1,.36,1)</code></td></tr>
<tr><td>base</td><td>200ms</td><td>整屏切换、toast、胶囊 bump、进场单格</td><td>ease-out / spring <code>cubic-bezier(.34,1.56,.64,1)</code></td></tr>
<tr><td>slow</td><td>320ms</td><td>闪白衰减、过关扫光、抖动、进度条</td><td>linear(衰减)/ ease-out</td></tr>
<tr><td>hold</td><td>500ms</td><td>过关停留</td><td>–</td></tr>
<tr><td>循环</td><td>0.9s / 2.4s</td><td>提示按钮脉冲 / 当前关呼吸、提示描边</td><td>ease-in-out <code>cubic-bezier(.65,0,.35,1)</code></td></tr>
</table>
<h3>逐个回放</h3>
<div class="grid g3" id="demos">
<div class="demo"><header><b>开局进场</b><span>base · 每格 +14ms</span></header><div class="stage" id="d-enter"></div><p>缩放 0.92 → 1 + 淡入,按阅读顺序错落,延迟封顶 200ms。</p><button class="replay" onclick="replayStage('#d-enter','a-enter')">回放</button></div>
<div class="demo"><header><b>落子弹簧</b><span>≈200ms · k=260 c=18</span></header><div class="stage" id="d-pop"></div><p>当前格放大回 1,步号同时 120ms 淡入。</p><button class="replay" onclick="replayStage('#d-pop','a-pop a-fade')">回放</button></div>
<div class="demo"><header><b>桥生长</b><span>fast · ease-out</span></header><div class="stage" id="d-bridge"></div><p>只长厚度,跨缝方向第一帧就盖满,不露白缝。</p><button class="replay" onclick="replayStage('#d-bridge','a-bridge')">回放</button></div>
<div class="demo"><header><b>踩对线索闪白</b><span>slow · 线性衰减</span></header><div class="stage" id="d-flash"></div><p>描边闪白 1 → 0。这是「推对了」的主奖励。</p><button class="replay" onclick="replayStage('#d-flash','a-flash')">回放</button></div>
<div class="demo"><header><b>死路 / 非法起手抖动</b><span>280ms · 6 个来回</span></header><div class="stage" id="d-shake"></div><p>幅度 0.06 u 递减,不弹「失败」。</p><button class="replay" onclick="replayStage('#d-shake','a-shake')">回放</button></div>
<div class="demo"><header><b>过关扫光</b><span>slow · 每格 250ms</span></header><div class="stage" id="d-sweep"></div><p>全盘切亮色,按步序扫白光,320ms 内起完。</p><button class="replay" onclick="replayStage('#d-sweep','a-sweep')">回放</button></div>
<div class="demo"><header><b>提示描边</b><span>2.4s · ≈1.1Hz</span></header><div class="stage" id="d-hint"></div><p>琥珀外描边正弦脉冲,最后 0.4s 淡出。</p><button class="replay" onclick="replayStage('#d-hint','a-hint')">回放</button></div>
<div class="demo"><header><b>整屏切换</b><span>base · ease-out</span></header><div class="stage" id="d-screen"><div style="width:180px;height:80px;border-radius:14px;background:var(--bg);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-weight:700">选关 → 对局</div></div><p>淡入 + 上移 8px。关与关之间不切屏,只有盘面进退场。</p><button class="replay" onclick="replayStage('#d-screen','a-screen', true)">回放</button></div>
<div class="demo"><header><b>冲突</b><span>280ms · 单格轻晃</span></header><div class="stage" id="d-conflict"><div class="bc visited conflict a-shake"><i>5</i></div></div><p>立刻红底 + 单格轻晃 + 闷音 + 40ms 振动。连接线始终紫色;步号错位不加叉号。撤销立即褪回并停止晃动。</p><button class="replay" onclick="replayStage('#d-conflict','a-shake')">回放</button></div>
</div>
<h3>过关时间线</h3>
<div class="card">
<div class="timeline">
<div class="p1">扫光<small>0 – 320ms · 和弦起、胶囊「完成」、按钮禁用</small></div>
<div>停留<small>320 – 820ms</small></div>
<div class="p3">淡出<small>820 – 980ms</small></div>
<div class="p1">下一关进场<small>980ms 起 · 顶栏就地更新,盘面错落进场 ≤ 400ms</small></div>
</div>
</div>
<h3>减弱动态</h3>
<p class="note">系统开启「减弱动态效果」时:CSS 动画与过渡全部关闭;Canvas 去掉进场错落、弹簧、扫光与淡出,只保留状态切换与提示描边。</p>
</section>
<section id="sound">
<h2>9 · 声音与振动</h2>
<table>
<tr><th>事件</th><th>音效</th><th>振动</th></tr>
<tr><td>落子</td><td>90ms 正弦短音,C5 起每步升半音,12 步循环</td><td>8ms(踩对线索 18ms)</td></tr>
<tr><td>冲突</td><td>160ms 方波 130Hz 闷音</td><td>40ms</td></tr>
<tr><td>非法起手</td><td>60ms 三角波 220Hz</td><td>15ms</td></tr>
<tr><td>死路</td><td>–</td><td>10 · 30 · 10</td></tr>
<tr><td>过关</td><td>C-E-G-C 三角波和弦,每音 220ms,间隔 70ms</td><td>20 · 40 · 30</td></tr>
</table>
</section>
<section id="sync">
<h2>10 · 同步规则</h2>
<div class="card">
<ol class="note" style="margin:0;padding-left:18px;line-height:1.9">
<li>先改 <code>docs/design.md</code> 的对应表格,并在「变更记录」加一行。</li>
<li>改 <code>modes/01-number-stroke/web/style.css</code> 的 <code>:root</code> 与 <code>src/theme.ts</code>。</li>
<li>更新本页顶部 <code>:root</code> 与相应样张。</li>
<li><code>npm run check</code>:<code>tests/theme.test.ts</code> 会比对 CSS 与 theme.ts 的颜色、动效时长与字体栈。</li>
<li><code>npm run build:standalone</code> 重新生成试玩版。</li>
</ol>
</div>
</section>
</main>
</div>
<script>
// ---- 色板 ----
const TOKENS = [
['背景', '--bg', '#FFFFFF', '页面与盘面背景 · THEME.bg'],
['桌面底', '--bg-app', '#F2F2F6', '手机框外的底色'],
['二级表面', '--surface-2', '#F3F3F7', '按钮、胶囊、chip、锁定格'],
['未走格', '--cell', '#E6E7EC', '盘面未走格、未过关格、进度条底 · THEME.cell'],
['分隔线', '--line', '#E1E2EA', '细线(预留)'],
['墨', '--ink', '#23243A', '标题、正文、未走线索数字、角标底 · THEME.clueInk'],
['墨 2', '--ink-2', '#6B6D80', '副文案、字幕、胶囊、图标'],
['墨 3', '--ink-3', '#A3A5B5', '弱文案、进度数字、锁图标'],
['主色', '--primary', '#6558F5', '已走格、桥、当前关、主按钮 · THEME.visited'],
['主色·亮', '--primary-bright', '#7D72FF', '过关提亮 · THEME.visitedBright'],
['主色·淡', '--primary-soft', '#ECEAFE', '当前关格底、开启态 chip、完成态胶囊'],
['主色上的字', '--on-primary', '#FFFFFF', '已走格数字、主按钮文字 · THEME.visitedInk'],
['冲突', '--danger', '#E5484D', '冲突格、重置进度 · THEME.conflict'],
['冲突·淡', '--danger-soft', '#FDECEC', '预留(高对比模式)'],
['提示', '--hint', '#F5A524', '提示描边、提示按钮脉冲 · THEME.hint'],
['提示·淡', '--hint-soft', '#FEF3DD', '预留'],
['线索描边', '--clue-ring', '#23243A', '未走线索 / 奇偶格描边(= 墨,与数字同色)· THEME.clueRing'],
['已走线索描边', '(canvas)', '#DFE1EC', '起点与已走线索外圈 · THEME.activeClueRing'],
['遮罩', '--overlay', 'rgba(35,36,58,.92)', 'toast 底'],
];
document.getElementById('swatches').innerHTML = TOKENS.map(([n, v, hex, use]) =>
`<div class="sw"><div class="chip" style="background:${hex}"></div><div class="meta"><b>${n}</b><code>${hex}</code> <code>${v}</code><small>${use}</small></div></div>`).join('');
// ---- 盘面样张 ----
function cell(cls, text, opts = {}) {
const badge = opts.badge ? `<b class="badge">${opts.badge}</b>` : '';
const sub = opts.sub ? `<small>${opts.sub}</small>` : '';
const style = (opts.col ? `grid-column:${opts.col};` : '') + (opts.row ? `grid-row:${opts.row};` : '') + (opts.i !== undefined ? `--i:${opts.i};` : '');
return `<div class="bc ${cls}" style="${style}"><i>${text}${sub}</i>${badge}</div>`;
}
// 桥:放在两格之间。unit 用 CSS 变量,位置按格坐标算
function bridge(dir, x, y, red = false) {
// dir h:竖向共享边(左右相邻),x 为边的列号,y 为格行号;dir v:横向共享边(上下相邻)
const u = 'var(--u)';
const pad = '14px';
if (dir === 'h') return `<div class="bridge h ${red ? 'red' : ''}" style="left:calc(${pad} + ${u} * ${x} - (${u} * 0.05 + 1px)); top:calc(${pad} + ${u} * ${y} + ${u} * 0.27)"></div>`;
return `<div class="bridge v ${red ? 'red' : ''}" style="top:calc(${pad} + ${u} * ${y} - (${u} * 0.05 + 1px)); left:calc(${pad} + ${u} * ${x} + ${u} * 0.27)"></div>`;
}
const b1 = document.getElementById('board-sample');
b1.style.gridTemplateColumns = 'repeat(4, var(--u))';
b1.innerHTML =
cell('start visited clue', '1') + cell('visited', '2') + cell('visited', '3') + cell('visited', '4') +
cell('clue', '4') + cell('', '') + cell('visited conflict', '6', { badge: '8' }) + cell('visited', '5') +
cell('', '') + cell('', '') + cell('', '') + cell('', '') +
bridge('h', 1, 0) + bridge('h', 2, 0) + bridge('h', 3, 0) + bridge('v', 3, 1) + bridge('h', 3, 1);
const b2 = document.getElementById('board-sample-2');
b2.style.gridTemplateColumns = 'repeat(4, var(--u))';
b2.innerHTML =
cell('start visited clue', '1') + cell('visited', '2') + cell('visited big clue', '3', { col: '3 / span 2', row: '1 / span 2' }) +
cell('', '') + cell('clue', '7') +
cell('', '') + cell('', '') + cell('clue two', '12') + cell('', '') +
bridge('h', 1, 0) + bridge('h', 2, 0);
const pb = document.getElementById('phone-board');
pb.style.gridTemplateColumns = 'repeat(4, var(--u))';
pb.innerHTML =
cell('start visited clue', '1') + cell('visited', '2') + cell('', '') + cell('', '') +
cell('visited clue', '4') + cell('visited', '3') + cell('clue', '8') + cell('', '') +
cell('visited', '5') + cell('visited', '6') + cell('', '') + cell('', '') +
bridge('h', 1, 0).replace('14px', '0px').replace('14px', '0px') + bridge('v', 1, 1).replace('14px', '0px').replace('14px', '0px') +
bridge('h', 1, 1).replace('14px', '0px').replace('14px', '0px') + bridge('v', 0, 2).replace('14px', '0px').replace('14px', '0px') +
bridge('h', 1, 2).replace('14px', '0px').replace('14px', '0px');
// ---- 选关样机 ----
function packHtml(title, n, cleared, current, locked) {
let tiles = '';
for (let i = 1; i <= n; i++) {
const cls = i <= cleared ? 'cleared' : i === current ? 'current' : locked && i > current ? 'locked' : '';
tiles += `<button class="tile ${cls}"><span>${i}</span></button>`;
}
return `<div class="pack ${cleared === n ? 'done' : ''}"><div class="pack-title"><span>${title}</span><span class="pack-progress">${cleared} / ${n}</span></div><div class="level-grid">${tiles}</div></div>`;
}
document.getElementById('phone-packs').innerHTML = packHtml('入门', 10, 10, 0, false) + packHtml('进阶', 10, 0, 1, true) + packHtml('奇与偶', 6, 6, 0, false);
// ---- 动效样张 ----
const stages = {
'd-enter': () => [0, 1, 2, 3].map((i) => cell(i === 0 ? 'start visited clue' : i === 2 ? 'clue' : '', i === 0 ? '1' : i === 2 ? '8' : '', { i })).join(''),
'd-pop': () => cell('visited', '4') + cell('visited', '<span>5</span>'),
'd-bridge': () => `<div style="position:relative;display:flex">${cell('visited', '4')}${cell('visited', '5')}<div class="bridge h" style="left:calc(64px - (64px * 0.05 + 1px));top:calc(64px * 0.27)"></div></div>`,
'd-flash': () => cell('visited clue', '8'),
'd-shake': () => cell('visited', '7'),
'd-sweep': () => [1, 2, 3, 4, 5].map((n, i) => cell('visited bright', String(n), { i })).join(''),
'd-hint': () => cell('clue', '4'),
};
for (const [id, html] of Object.entries(stages)) document.getElementById(id).innerHTML = html();
function replay(sel, cls) {
const el = document.querySelector(sel);
cls.split(' ').forEach((c) => el.classList.remove(c));
void el.offsetWidth;
cls.split(' ').forEach((c) => el.classList.add(c));
}
function replayStage(sel, cls, self = false) {
const stage = document.querySelector(sel);
const targets = self ? [stage.firstElementChild] : [...stage.querySelectorAll('.bc, .bridge')].concat(stage.querySelector('.bridge') ? [] : []);
const list = self ? targets : (cls.includes('a-bridge') ? [stage] : targets);
list.forEach((el) => { cls.split(' ').forEach((c) => el.classList.remove(c)); });
void stage.offsetWidth;
list.forEach((el) => { cls.split(' ').forEach((c) => el.classList.add(c)); });
}
// 首屏自动播一遍进场
setTimeout(() => replayStage('#d-enter', 'a-enter'), 300);
</script>
</body>
</html>