Files
NumberSwipe_New/docs/design.html
T
cabinw 8026e4628d 初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
2026-09-16 14:30:59 +08:00

544 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>