# HEX TRAILS 美术方向与制作简报 > 历史简报,保留 2026-09-11 的核查背景。当前五套主游戏设计的 Agent 入口已整理到 [design/README.md](../design/README.md),规则见 [shared.md](../design/shared.md) 与各套 design.md,数值见对应 tokens.json。下文“唯一数值来源”、数量、时序和设计债描述可能属于旧快照,不应覆盖当前五套方案的独立规则。 版本:2026-09-11。本文面向承担本项目**美术设计**工作的会话或成员,说明角色边界、设计系统事实、改色与素材流程、已知设计债,以及三条工作线的入手点。 本文是**工作简报**,不是设计规范本身。数值与规则的唯一来源仍是 `src/design-tokens.js` 与 [Design.md](../Design.md);本文负责把"怎么改、别踩什么、先做什么"讲清楚。 --- ## 1. 角色边界 **职责** - 配色、主题、图标、动效时长、布局与响应式表现 - 素材生成与裁切:图集、图标、背景,以及 `src/sprites.js` / `src/ui-skin.js` 中的裁切框 - 设计规范与生成产物的一致性:`Design.md` / `Design.html` / `design/*` / `assets/ui-surfaces.png` - 视觉验收:改动后提供实机证据,而不只是"测试通过" - 原生端视觉对齐:`cocos/`、`wechat/HexTrail/` **不做** - 不修改玩法规则、求解器、碰撞数学(`src/core.js`、`src/path-math.js`) - 不修改关卡布局与生成参数(`levels/*-seeds.json`、生成脚本) - 不静默替换已接受的素材,不另写一套 HEX,不用滤镜临时拼配色(见 [Design.md](../Design.md) §3.5) **前置阅读**:[Design.md](../Design.md)(设计规范全文)· [cocos-port.md](cocos-port.md)(移植约束)· [level-design-principles-v2.md](level-design-principles-v2.md)(了解产品方向,避免设计脱离玩法) --- ## 2. 设计系统事实(当前状态) ### 2.1 单一来源 `src/design-tokens.js`(80 行)是颜色、字体、动效、圆角的**唯一数值来源**。 | 导出 | 内容 | 规模 | | --- | --- | --- | | `THEME` | 语义角色 | 30 个 | | `SURFACES` | 面板角色(对应 `ui-surfaces.png` 的色块) | 13 个 | | `PIGMENTS` | 棋子色库,箭头色按对比度自动选白/深蓝灰 | 12 色 | | `LEVEL_THEMES` | 场景主题,按难度分数选取 | 5 套 | | `TYPOGRAPHY` | level 28 / moves 32 / dialogTitle 24 / body 14 / label 12 / caption 11 | 6 档 | | `MOTION` | 动效时长,见 §6 债务 D3 | 17 项 | | `RADII` | control 14 / dialog 24 / small 8 | 3 档 | ### 2.2 主题按难度分数选取 `themeForScore(score)` 的分档与 `analyze().label` **同构**(已穷举 0–120 分及 140 关实测,零不一致): | 分数 | 标签 | 主题 | 背景 | 棋子搭配 | | --- | --- | --- | --- | --- | | 0–29 | 轻松 | 薄荷浅湾 lagoon | `#C5E4DF` | 晴空蓝、湖水青、日光黄 | | 30–49 | 进阶 | 杏色午后 apricot | `#F1D7B8` | 珊瑚橙、鸢尾蓝、湖水青、蜜桃杏 | | 50–69 | 烧脑 | 鸢尾花园 lavender | `#D8D0EB` | 葡萄紫、嫩芽绿、玫瑰红、冰川青 | | 70–100 | 挑战 | 玫瑰暮色 rose | `#E7C3CE` | 玫瑰红、鸢尾蓝、日光黄、兰花紫 | | 特别主题 | 每周挑战 | 午夜极光 midnight | `#293956` | 冰川青、兰花紫、日光黄、莓果粉 | **`midnight` 不被 `themeForScore` 返回**,只能通过 `?theme=midnight` 或 showcase 进入。周赛功能尚未接入([Design.md](../Design.md) 已声明)。 ### 2.3 取色机制(容易搞错) `src/renderer.js:23` 取的是**棋子序号**,不是棋子自带的 `color` 字段: ```js pigmentIndex(state.theme ? Math.max(0, level.tiles.findIndex(t => t.id === tile.id)) // 棋子序号 : tile.color, state.theme) // 仅 edit/trial 用 color ``` 因此主线是**调色板循环分配**:20 枚棋子、7 色主题 → 只有 7 种颜色,多枚同色是设计如此。换来的是 `Design.md` 承诺的稳定性:**移走棋子、旋转、撤销都不会让剩余棋子变色**(已端到端验证,逐棋子变化数 0)。 **图集只有 4 帧**(`TILE_FRAMES`),palette 索引 ≥4 时会降级为纯色多边形绘制。这是 `CanvasPainter.sprite()` 返回 `false` 触发的正常降级路径,不是错误。 --- ## 3. 改色工作流 **只有这一条路**:改 `src/design-tokens.js` → 重新生成 → 检查产物 → 跑测试。 ```bash # 1. 改 src/design-tokens.js # 2. 重新生成设计产物 npm run design # = build-design.mjs + build-design-guide.mjs # 3. 检查产物与对比度报告 npm test # 107 项,含 design.test.js / themes.test.js npm run build # 全量构建;对比度有失败会 exitCode=1 ``` `npm run design` 会重写这些文件: | 产物 | 内容 | | --- | --- | | `design/tokens.css` | kebab 命名 CSS 变量 | | `design/tokens.json` | theme / pigments / levelThemes / surfaces / typography / motion / radii | | `assets/ui-surfaces.png` | **由脚本手写 PNG 编码器**烘出的 13 个 32×32 纯色块 | | `design/contrast-report.json` | 33 项对比度校验结果 | | `Design.html` | 2.6MB 单文件离线手册,内嵌图集 base64 与 SVG 复刻的棋盘示例 | **对比度有三道校验**:构建脚本(失败 `exitCode=1`)、`tests/design.test.js`、`tests/themes.test.js`。 **注意 `npm run build` 不生成设计产物**,它只调用 `levels:versions → levels:v04 → design → build.mjs`,其中 `build.mjs` 仅把文件复制进 `dist/`。所以改色必须**单独跑 `npm run design`**。 --- ## 4. 素材管线 ### 4.1 当前使用中的素材 | 用途 | 文件 | 规格 | | --- | --- | --- | | 棋子图集 | `assets/tiles-matte-clean-v8.png` | 1293×1216,RGB 无 alpha | | 图标图集 | `assets/ui-icons-generated.png` | 1254×1254,16 个图标 | | 面板色块 | `assets/ui-surfaces.png` | 416×32,**生成物**,勿手改 | ### 4.2 裁切框是代码,不是图片 - 棋子:`src/sprites.js` 的 `TILE_FRAMES`(4 个 `540×465` 框)+ `src/tile-shape.js` 的 `tileSample()`(取每框**中央 20%**)与 `roundedHex()`(顶点内缩 7.5%) - 图标:`src/ui-skin.js` 的 `ICONS`(16 个**手写光学中心** bounds,扩成正方形 +12px 留白) **图片只能在原图不透明、无杂点的区域采样。** `tiles-matte-clean-v8.png` 之所以叫 "clean",就是因为 `assets/GENERATION.md` 记录**两次边缘杂点清理尝试均失败**(其中一次产出不透明棋盘格),最终改为只采样中央干净色块。改动裁切框前先读那份记录。 ### 4.3 生成流程与提示词 `assets/GENERATION.md` 保存完整生成史;提示词档案为 `assets/prompt-v4/v6/v7/v8.txt`、`assets/prompts-v3.json`。历史素材(`tiles-candy-v2/v3/v4`、`tiles-matte-v5/v6/v7`、`ui-icons.png`、`ui-panels.png`、`board-tray.png`、`background-candy-v2.png`、`logo-candy-v2.png`)保留但不用于当前界面;`ui-icons-flat.png` 是代码重绘版,**明确不再引用**。 **产出新素材的规则**:原始 PNG 不做脚本像素修改;替换已接受的素材要明确告知用户来源与变化,并同步更新 `Design.md` §5 的资产表。 --- ## 5. 三条工作线与入手点 ### 5.1 打磨现有界面(优先级最高的具体项见 §6) 改视觉前先确认改的是**语义角色**而不是某个具体位置。`Design.md` §8 的流程要求:明确角色 → 改 token → `npm run design` → 检查产物 → `npm test` / `npm run build` → 五套背景逐一检查。 响应式要点:竖屏 390×844 为主线,较矮屏幕需纵向滚动;点击区至少 44×44;棋盘支持 1–4 倍缩放与双指手势。 ### 5.2 产出新素材 先读 `assets/GENERATION.md` 了解既有提示词的措辞风格与失败教训,再决定是否需要新素材。**优先复用现有图集**——`ui-surfaces.png` 是生成的,改 token 即可变色,不需要新图片。 ### 5.3 为微信 / Cocos 移植准备视觉资产 **现状**:`wechat/HexTrail/` 是 Cocos Creator 3.8.8 **空壳工程**(`assets/` 只有 `.gitkeep`,无场景无脚本);`cocos/HexTrailsBoard.ts` 是适配示例,**未运行场景、未做 Android/iOS 构建**。 移植的视觉契约是:**Cocos 读取同一份 JSON/JS token 和 PNG,不得在原生侧另外猜颜色**([Design.md](../Design.md) §8)。`CandyUiSkin` 用纯色 Sprite + 圆角 Mask + 居中图标实现。 **已知会阻塞移植的两项**(详见 §6 D1、D2):复制清单漏文件会导致 import 失败;原生端硬编码的动效时长与网页现行模型不一致。 --- ## 6. 已知设计债(按入手价值排序) > 以下均为**实测复核过**的结论,不是推测。修复它们不需要改动玩法或关卡数据。 ### D1 · 移植复制清单漏 `src/exit-animation.js`(会直接报错) `docs/cocos-port.md` 的复制清单列了 `collision-animation.js`,但 `src/renderer.js:1` 直接 `import { exitTiming, smoothMotion, travelProgress } from './exit-animation.js'`。**照清单复制 → import 失败。** ### D2 · 原生端硬编码动效时长,与网页不一致 | 项 | 网页(`MOTION`) | Cocos(`HexTrailsBoard.ts`) | | --- | --- | --- | | 离场 | `120ms/单位 + ramp≤80 + 180ms 收束` | `Math.min(1.7, 0.55 + steps*0.085)`(:117) | | 碰撞 | `800ms` | `1.25`(:120) | | 旋转 | `240ms` | `0.42`(:138) | `HexTrailsBoard.ts` **完全没有 import `MOTION`**。另外它的 `redraw()` 只传 `{path, hint, animation, boardAngle}`,缺 `theme / reduced / queued / shake / selected` —— 原生端因此**没有主题配色、没有 `prefers-reduced-motion` 分支、没有连点紫环反馈**。 ### D3 · `MOTION` 有 7 个死 token `exitMinTravelMs`、`exitMaxTravelMs`、`exitLongBaseMs`、`exitLongCellMs`、`exitLongMaxMs`、`exitLongStartCells`、`exitLongFullCells` —— 在 `design-tokens.js` 之外**引用数均为 0**,但仍在导出给原生端。 **根因**:提交 `e92cccb` 把 `exitTiming` 从"clamp 140–800ms + 长路混合曲线"换成了纯路径长度模型,没有清理 token、没有同步文档与适配器。 ### D4 · `Design.md` 两处数字错误 - **§7 离场(`Design.md:166`)**:写的是**已废弃的模型**("每单位 60ms、滑行段限 140–800ms、5 格及以上 550ms+85ms/格、最长 1700ms")。实际是 **120ms/单位、无任何 clamp**。`tests/themes.test.js` 的断言(120ms、"without a duration cap")是对的,文档是错的。 - **§8 对比度(`Design.md:175`)**:写"轨道/底板约 **3.55:1**",实测 `contrast(track, board) = **5.7636**`。源码里没有任何一对等于 3.55。 **连带问题**:`Design.html` **自相矛盾** —— 生成表格(:43)写 5.76,正文(:105)与内嵌 JSON(:114)写 3.55。改 `Design.md` 后重跑 `npm run design` 即可一并消除。 ### D5 · 棋子面 vs 棋盘:12 色中 7 色低于 3:1(**未声明的覆盖缺口**) `CONTRAST_PAIRS` 的 33 项**没有校验棋子面与它实际相邻的颜色**。而 `renderer.js:44` 把所有格子统一画成 `THEME.board`(`#F2EEE5`),`drawTile()` 只填纯色、**无描边** —— 棋子的真实相邻面是**棋盘**,不是主题背景。 | 棋子色 | vs 棋盘 `#F2EEE5` | | --- | --- | | 日光黄 `#FDBC28` | 1.4626 | | 蜜桃杏 `#F5AA83` | 1.6548 | | 冰川青 `#58C9D5` | 1.6902 | | 嫩芽绿 `#91C95B` | 1.6935 | | 珊瑚橙 `#EF704F` | 2.5542 | | 晴空蓝 `#2392FD` | 2.7474 | | 莓果粉 `#FC3E80` | 2.9539 | **诚实定性**:项目从未声明过这条要求,所以这是**未声明的缺口**而非违约。缓解因素是每枚棋子上都有 ≥3:1 的箭头(已证实),且颜色不参与玩法。**最坏后果是"棋子面与棋盘融在一起、只剩箭头悬空"。** 是否补描边、调棋盘底色、还是明确声明"不承诺此项",属设计决策,需与用户确认后再动。 ### D6 · `style.css` 旧色板与 token 脱钩 `style.css` 仍是旧的薰衣草紫体系(`--purple:#8772ed`、`--line:#d4d9f0`、`--muted:#8c94b3`),**`var(--color-*)` 引用数为 0**。当前靠 `design/components.css` **后加载**(元素选择器同特异性下后者胜出)+ 7 处 `!important` 覆盖。 仍未被覆盖的硬编码包括 `.rotate-button.hinted{outline:4px solid #ffd667}`、dialog/metric 的旧灰紫(`#8d9abb`、`#95a1c0`)。 ### D7 · `components.css` 星光辉光违反本项目自己的禁令 ```css .result-emblem .ui-icon[data-icon="star"]{ filter:brightness(0) saturate(100%) invert(79%) sepia(85%) saturate(1450%) ...} ``` 硬编码了 `#c88b2380`、`#ffd45c99`、`#ffe8a366` 并用 `brightness/invert/saturate` 滤镜链调色 —— 直接违反 `Design.md` §3.5「**禁止**用 `brightness / invert / saturate` 临时调面板色」与「`src/design-tokens.js` 是颜色数值的唯一来源」。同一规则复制在 `design/guide-template.html:19`。 ### D8 · 两处冗余配对与小余量 - `CONTRAST_PAIRS` 里"轨道/棋盘"与"轨道/浅色外缘"**实为同一项**(`trackHalo === board === #F2EEE5`),是冗余校验 - 最紧的两项:分格/棋盘 **+0.1434**、箭头/晴空蓝 **+0.1811** - **脆弱点**:珊瑚橙的白箭头比值 **2.9574**,只比阈值低 **0.0426** —— 阈值若降到 2.95 就会选白箭头并跌破 3:1(当前判定正确) ### D9 · lagoon 裸主题色板碰撞(当前发行路径不可达) `LEVEL_THEMES[0]` 的 `palette` 只有 3 项 `[0,4,2]`,`pigmentIndex` 取模后 `[0,1,2,3] → [0,4,2,0]`,**colour 0 与 3 都是晴空蓝**。`tests/themes.test.js` 只断言 palette 长度 3–4,测不出来。 **当前不可达**:`app.js` 播放态总是先过 `themeWithColors()`(补足到 4/7/9/12)。属潜在缺陷,改动主题时注意。 --- ## 7. 视觉验收 **自动测试不能替代视觉验收。** 现有测试是"测试全绿但覆盖不到视觉"的典型 —— §6 的 D5、D7、D8、D9 **都没有测试保护**,所以"107 项通过"不构成对它们的反驳。 ### 可用的预览入口 | 入口 | 用途 | | --- | --- | | `/design-showcase.html` | 五套主题切换 + 真实棋盘 + 离场动效;`src/showcase.js` 支持主题标签与配色数切换 | | `/?theme=` | 主游戏强制指定主题,例如 `?theme=midnight`(唯一能看午夜极光的方式) | | `/?colors=<4\|7\|9\|12>` | 覆盖单关配色数 | | `/Design.html` | 单文件离线设计手册,内嵌图集与组件示例 | | `/?editor=1` | 关卡工坊,含完整 12 色库 | 需要 `npm run dev` 起服务(默认 4173),**必须通过 HTTP 访问**(ES modules)。 ### 验收要求 1. **五套背景逐一检查**场景文字与箭头 2. 检查**离场、旋转、撤销后颜色稳定** 3. 390×844 竖屏 + 较矮屏幕 4. 主界面、选关、设置、通关/失败、碰撞状态 5. 弹窗不穿透操作、图标居中、轨道接缝不断裂 **本项目目前没有任何视觉回归基线。** 建立截图基线是值得做的一件事,但目前没有;改动视觉时应提供实机证据(截图),而不是只报"测试通过"。 --- ## 8. 参考 | 路径 | 内容 | | --- | --- | | `Design.md` / `Design.html` | 设计规范(前者手写,后者生成) | | `docs/cocos-port.md` | 移植接入步骤与验证限制 | | `docs/level-design-principles-v2.md` | 产品方向与关卡准则 | | `assets/GENERATION.md` | 素材生成史与提示词 | | `design/tokens.json` | 给原生端读的 token 导出 | | `design/contrast-report.json` | 33 项对比度实测结果 | **本文的核查范围**:§2、§3、§4、§6 的全部数字来自对当前工作区的实测复核(构建、测试、内联 node 计算),不是转述。**未验证**:真人观感、实机触感、原生端渲染效果 —— 这三项都还没有可引用的证据。