Files
HexTrails/docs/art-direction.md
T

266 lines
15 KiB
Markdown
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.
# HEX TRAILS 美术方向与制作简报
版本: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=<id>` | 主游戏强制指定主题,例如 `?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 计算),不是转述。**未验证**:真人观感、实机触感、原生端渲染效果 —— 这三项都还没有可引用的证据。