更新技术美术明亮教具室版:字体、指路、教学与庆祝效果
@@ -2,7 +2,13 @@
|
||||
|
||||
本仓库管理 NumberSwipe-Variants 下的衍生玩法、关卡、生成工具和设计资料。
|
||||
|
||||
## 当前最新版:campaign-200-r2
|
||||
## 最新微信美术版(2026-10-22)
|
||||
|
||||
- 源码及微信开发者工具导入目录: [wechat-campaign-200-r2](wechat-campaign-200-r2/)。
|
||||
- 最新独立包:[明亮教具室·过关重音修复版](releases/NumberSwipe-campaign-200-r2-明亮教具室-过关重音修复版-20261022.zip)。
|
||||
- 包含明亮教具室美术、字体资源、指路表现、循环教学及庆祝效果;网页版本入口见下方。
|
||||
|
||||
## 网页版:campaign-200-r2
|
||||
|
||||
- 试玩入口:[200 关评分修订版](campaign-200-r2/index.html),下载后可直接打开。
|
||||
- 版本说明与校验方式:[最新版 README](campaign-200-r2/README.md)。
|
||||
@@ -95,4 +101,4 @@ node tools/isolation.mjs
|
||||
|
||||
建议先玩方向通道、双色交替,再玩永久墙与步进门,最后玩六边融合。各玩法按四档顺序推进;观察是否能只看盘面说明就复述规则。
|
||||
|
||||
在“记录本关体验”填口述推理,导出本关JSON。观察者用 `reports/playtest-template.csv` 补充第一次错误、回退、提示、思考时间和边界可读性。尤其比较墙与门是否产生不同决策,以及六边融合是否仍主要靠顺手试画。
|
||||
在“记录本关体验”填口述推理,导出本关JSON。观察者用 `reports/playtest-template.csv` 补充第一次错误、回退、提示、思考时间和边界可读性。尤其比较墙与门是否产生不同决策,以及六边融合是否仍主要靠顺手试画。
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# 明亮教具室 · 参考图圆润版修订
|
||||
|
||||
日期:2026-10-21。版本:reference-cushion-v2。
|
||||
|
||||
本次以用户补充的效果图为视觉依据,覆盖此前报告中“薄描边、轻倒角、较深紫色”的生产建议。保持真实200关几何和玩法,重做棋块、底座、按钮及其色彩、厚度、受光和投影。
|
||||
|
||||
## 视觉修正
|
||||
|
||||
| 项目 | 此版实现 |
|
||||
|---|---|
|
||||
| 配色 | 从参考图局部采样:紫面约RGB(129,120,244),奶油块面约RGB(249,237,213),舞台约RGB(250,238,214);黄底改为金黄渐变 |
|
||||
| 圆润材质 | 按轮廓距离生成软倒角,叠加上缘亮部、下缘暗部、实体底沿与柔和投影;几何外形决定材质,不加载图片 |
|
||||
| 多余线框 | 移除固定条件方框、当前位置四角框、双色已走内框、常驻块体描边和完成时轮廓扫线 |
|
||||
| 桥接 | 由原来宽条改为细短圆润连接件,具有自身倒角和深度,仍连接真实共边 |
|
||||
| 比例 | 简单盘面的棋块按可用舞台放大,减少先前大量空白;复杂盘面仍受真实包围盒和文字空间约束 |
|
||||
| 按钮 | 增大圆角和厚度;重开用珊瑚实体回转符,撤销用暖黄实体箭头,指路用黄色灯泡;操作保持重开/撤销/指路 |
|
||||
| 底座与状态 | 奶油软边底座、珊瑚关号标签、奶油计数胶囊;继续显示实际访问步数和剩余块数 |
|
||||
|
||||
必要的数字、奇偶、通道、墙门和类别形状保留。双色关保留蓝橙身份;小节距普通紫块采用较深紫面,保持小号数字清楚。此版没有替换或伪造真实关卡构图。
|
||||
|
||||
## 实现
|
||||
|
||||
新增 `wechat-campaign-200-r2/src/bright-classroom/cushion.mjs`:圆角矩形使用解析距离,异形和内孔使用原轮廓的贝塞尔采样距离;由距离和方向场计算亮暗、透明度及极轻静态颗粒。生成的Canvas只作为运行时缓存,字段缓存与材质缓存分别设10MiB、20MiB预算,按最近使用顺序淘汰。
|
||||
|
||||
参数集中于 `docs/design/bright-classroom/tokens.json` 的 `referenceMatch`。旧 `color` 与原报告保留为历史规范;原色对审计不作为新浅紫材质的验证结论。主要代码在 `theme.mjs`、`ui.mjs`、`geometry.mjs` 和 `board-renderer.mjs`。
|
||||
|
||||
仍未修改上游四份来源快照、关卡顺序、规则、存档key及音频;仅更新构建适配版本说明。Canvas的像素生成能力不可用时保留程序矢量回退。
|
||||
|
||||
## 验证与交付
|
||||
|
||||
| 证据 | 结果 | 对应结论与路径 |
|
||||
|---|---|---|
|
||||
| `node --test bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs` | 30/30通过;新增软倒角像素的内孔透明、上亮下暗、不修改几何检查 | 材质生成与逻辑几何分离 |
|
||||
| `node smoke.mjs` | 200/200唯一解与模拟微信触摸通关,四种屏幕、教学隔离、提示、撤销、存档及减少动态通过 | 全关卡行为保持 |
|
||||
| 浏览器 `qa/hints.html` | 200/200已走格采样像素在开启提示及撤销重画前后相同 | 新缓存材质仍被提示层正确避让 |
|
||||
| 实际浏览器观察 | 基础大块、双色奇偶、320×568的36块密集盘 | 取消线框后继续检查读盘与布局 |
|
||||
|
||||
试玩:[明亮教具室](http://127.0.0.1:17859/qa/bright-classroom.html)。
|
||||
|
||||
导入包:[参考图圆润版](../releases/NumberSwipe-campaign-200-r2-明亮教具室-参考图圆润版-20261021.zip)。SHA-256:`80EE3B573414A4A295497014916E401D3F27973B88815D2ABB62C904E1B6E628`。解压后导入微信开发者工具,重新编译并生成新预览;旧包保留。
|
||||
|
||||
实际运行截图:[基础大块](../wechat-campaign-200-r2/qa/bright-classroom-reference-v2.png)、[320宽密集盘](../wechat-campaign-200-r2/qa/bright-classroom-reference-v2-dense.png)、[200关像素检查](../wechat-campaign-200-r2/qa/bright-classroom-reference-v2-pixels.png)。截图不进入游戏包。
|
||||
|
||||
本次完成程序材质与浏览器视觉核对,未执行微信手机的帧耗时、内存和像素一致实测,也没有宣称整张概念图逐像素一致。手机上应使用新包检查初次生成材质、连续滑动、字体和前后台恢复。
|
||||
@@ -0,0 +1,49 @@
|
||||
# 明亮教具室 · 投影、数字和按钮精修
|
||||
|
||||
日期:2026-10-21。修订:v3。基于用户提供的941×1672参考图和v2实际运行画面进行逐项核对。
|
||||
|
||||
## 参考图分析与修正
|
||||
|
||||
| 参考图的特征 | v2差异 | 本次修正 |
|
||||
|---|---|---|
|
||||
| 棋块外沿有近距离黄棕晕染,下方另有短投影 | 只有普通投影,受光与暗部偏灰 | 暖色近边晕染与落地投影分开绘制;紫面暗部用蓝紫混色,奶油面用暖棕混色,不再统一压黑 |
|
||||
| 底盘下方是向外衰减的阴影 | 向下复制底盘轮廓并填充黄色,出现额外实体下沿 | 底盘设为零挤出厚度,只用真实面alpha投影;底盘内部暗部减弱为淡暖色 |
|
||||
| 小块数字按可见字形居中,大块数字位于左上单元区 | 使用字体行框中心,大块统一在整体中心 | 使用实际字形包围盒;大块与异形使用左上占用单元锚点,逻辑命中中心保持不变 |
|
||||
| 白字有柔光和非常短的暗边 | 白字基本平涂,只有普通下投影 | 独立生成字形缓存,叠短紫暗边、白色柔光和清楚的浅色字面 |
|
||||
| 深色数字有淡材质,并非纯黑 | 深色数字纯色 | 使用暖深墨渐变与稳定微颗粒,保持轻微、静止、不影响阅读 |
|
||||
| 按钮的图标较大、短标签较小,二者有明确间隔 | 图标视觉尺寸不一致,文字偏规整且与图标争抢 | 重画回转、撤销、灯泡图标;按视觉质量分别校正尺寸;使用圆润短标签字体,重新设定上下留白 |
|
||||
|
||||
大块数字移动只是美术排版调整,仍保留一个逻辑块只走一步。通道格继续使用专用中心通道与数字角标,六边格保留中心布局;墙、门、类别和奇偶语义不变。
|
||||
|
||||
## 实现位置
|
||||
|
||||
- `src/bright-classroom/cushion.mjs`:从真实面alpha生成暖晕与投影,底盘不再复制实体底层;材质暗部支持着色混合。
|
||||
- `src/bright-classroom/theme.mjs`:按字形墨迹边界定位文字;原生Canvas仅返回文字宽度时,通过一次离屏alpha扫描测量字形边界。
|
||||
- `src/bright-classroom/number-stamp.mjs`:数字柔光、短暗边、淡渐变、固定颗粒与4MiB缓存预算。字形与材质像素均运行时生成。
|
||||
- `src/bright-classroom/geometry.mjs`、`board-renderer.mjs`:区分数字锚点与逻辑中心,组合规则继续独立排版。
|
||||
- `src/bright-classroom/ui.mjs`:按钮图文比例、图标轮廓和短标签字体。
|
||||
- `src/hints.mjs`:已走块遮罩扩展到外沿晕染区域,避免提示压住新的材质效果。
|
||||
|
||||
按钮短标签使用站酷快乐体的改名子集 `NumberSwipePlayful.ttf`;数字继续使用Nunito800,正文继续使用Noto Sans SC。字体源、字集与哈希记录于 `fonts/manifest.json`;相关OFL一并交付。来源:[站酷快乐体](https://github.com/google/fonts/tree/main/ofl/zcoolkuaile)。
|
||||
|
||||
## 验证与证据
|
||||
|
||||
| Evidence | Finding | Path |
|
||||
|---|---|---|
|
||||
| 用户参考图与v2画面对照 | 底盘实体复制层与真实投影不同,字形行框不等于视觉中心 | 分离投影与挤出厚度,按字形边界定位 |
|
||||
| `node --test bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs`:33/33通过 | 几何、内孔、类别、字形居中、原生宽度测量回退和零厚度底盘检查通过 | 新渲染模块→模块验证 |
|
||||
| `node smoke.mjs`:200/200通过 | 关卡唯一解、触摸通关、撤销、重开、提示、教学隔离、存档和前后台保持原行为 | 实际game.js→模拟微信验收 |
|
||||
| 浏览器 `qa/hints.html`:200/200采样像素一致 | 新字效与外沿晕染不被提示改变 | 实际Canvas→提示与撤销重画对照 |
|
||||
| 浏览器实际检查基础大块与320×568的36块关卡 | 普通数字、大块锚点、按钮图文与底盘投影可检查 | 样张观察→保存运行截图 |
|
||||
|
||||
没有新增图片游戏资源。截图仅用于QA,不进入运行包。微信手机的最终显示和性能仍需设备预览,浏览器与模拟微信结果不代替真机记录。
|
||||
|
||||
## 交付
|
||||
|
||||
- [当前试玩](http://127.0.0.1:17859/qa/bright-classroom.html)
|
||||
- [投影字效精修版导入包](../releases/NumberSwipe-campaign-200-r2-明亮教具室-投影字效精修版-20261021.zip)
|
||||
- [基础大块运行截图](../wechat-campaign-200-r2/qa/bright-classroom-polish-v3.png)
|
||||
- [320宽密集盘运行截图](../wechat-campaign-200-r2/qa/bright-classroom-polish-v3-dense.png)
|
||||
- [200关像素验证截图](../wechat-campaign-200-r2/qa/bright-classroom-polish-v3-pixels.png)
|
||||
|
||||
导入包SHA-256:`7A490BD9352563455F65501AF0EA0078839ECF22BE16DEB6712FF4DBA07D7ADD`。保留此前发布包;解压新包导入微信开发者工具,重新编译并生成预览。
|
||||
@@ -0,0 +1,344 @@
|
||||
# 数字一笔画 · 明亮教具室程序化美术实现方案
|
||||
|
||||
日期:2026-10-21|状态:技术方案,尚未接入运行时|首个落地版本:`wechat-campaign-200-r2`
|
||||
|
||||
以 **Canvas 2D 路径几何+参数化二维材质+状态驱动动效** 实现明亮教具室。背景、积木、桥接、图标、按钮和特效全部由程序生成,形成暖黄纸板、奶油教具垫、轻厚度彩色积木的视觉层次。
|
||||
|
||||
本方案承接[完整美术与 UI 设计报告](2026-10-21_美术设计-明亮教具室-report.md)及其[设计令牌](design/bright-classroom/tokens.json)。本次交付实现设计,不修改规则、正式渲染、关卡和发布包。
|
||||
|
||||
## 1. 制作原则与资源边界
|
||||
|
||||
### 1.1 “不用图片”的执行定义
|
||||
|
||||
| 内容 | 生产方式 | 交付形式 |
|
||||
|---|---|---|
|
||||
| 背景、舞台、积木、按钮 | 圆角路径、多边形、实色、有限渐变 | JavaScript 绘制模块 |
|
||||
| 功能图标、类别、墙门、通道 | 归一化坐标、线段、圆弧、贝塞尔曲线 | 路径命令与参数 |
|
||||
| 数字、中文 | 字体绘制;数字可选专用轮廓 | 字体文件或矢量路径数据 |
|
||||
| 首页标题 | 第一版用字体叠绘;精修版用原创字形轮廓 | 文字排版或路径数据 |
|
||||
| 动效、扫光、少量纸片 | 时间插值、裁切、几何粒子 | 动画代码 |
|
||||
| 绘制缓存 | 运行时生成的离屏 Canvas | 临时内存,可随时重建 |
|
||||
|
||||
游戏不加载 PNG、JPG、WebP、预烘焙阴影图、图标贴图、粒子贴图或图片图集。参考图只用于美术比对,不进入运行包。不用 SVG 图片加载器绕过限制;如需要矢量形状,直接执行路径命令。
|
||||
|
||||
离屏缓存由代码生成,不属于外部图片美术资源;现有 `drawImage(boardCanvas, …)` 这种 Canvas 合成可以保留。字体是文字资源,音频沿用已有方案,二者与图片资源分别管理。
|
||||
|
||||
### 1.2 风格还原的着力点
|
||||
|
||||
优先还原“大色面、圆角、短底沿、左上亮边、干净数字、短促按压”。参考图中的颗粒、软糯高光和明显浮雕数字不作为生产目标,避免密集关变糊。二维分层足以建立所需触感,首版继续使用当前 Canvas 渲染链路。
|
||||
|
||||
首页可承载更多品牌厚度;正式对局缩小标题,让棋盘成为主物体。材质强弱按单位格节距决定,2×2 大块与普通块使用同一厚度、圆角基准和数字字号。
|
||||
|
||||
## 2. 当前工程与接入路线
|
||||
|
||||
### 2.1 已有能力和实际缺口
|
||||
|
||||
| 已核对来源 | 当前行为 | 对实施的影响 |
|
||||
|---|---|---|
|
||||
| `src/main.mjs` | Canvas 绘制导航、按钮、选关、规则面板;合成棋盘和提示 Canvas | UI 和棋盘必须一起升级 |
|
||||
| `src/upstream/ui.mjs` | `localGeometry`、`paintCell`、`drawBridge`、`drawBarrier` 已程序绘制 | 可沿用几何来源,重做材质和信息布局 |
|
||||
| `src/upstream/rules.mjs` | 提供 `units`、`outlineUnits`、共边、墙门状态和判定 | 逻辑结果由此消费,不在美术层重算规则 |
|
||||
| `src/hints.mjs` | 独立透明层,擦除已走单位多边形,按中心点连线 | 应升级为新几何遮罩及语义避让 |
|
||||
| `build.mjs` | 构建时变换上游 UI,注入微信宿主、分段提示和教学入口 | 皮肤接入需纳入受检查的构建变换 |
|
||||
|
||||
当前 `draw()` 同时负责几何、表现和按钮状态回写;不能整段替换而漏掉剩余块数、教学反馈、撤销禁用与下一关显隐。当前异形轮廓工具只追踪一个闭环,不能直接声称支持带内孔的单块。当前完成态亮紫、类别冲突整面染红,也需改成报告规定的状态合成。
|
||||
|
||||
### 2.2 建议的模块拆分
|
||||
|
||||
以下是拟新增模块,当前尚不存在;统一位于 `wechat-campaign-200-r2/src/bright-classroom/`。
|
||||
|
||||
| 模块 | 责任 | 不承担的工作 |
|
||||
|---|---|---|
|
||||
| `theme.mjs` | 消费原设计令牌、派生随 u 变化的材质参数 | 不另建一套手写颜色 |
|
||||
| `geometry.mjs` | 多环轮廓、偏移、圆角、安全区、共边入口 | 不决定合法邻接 |
|
||||
| `scene.mjs` | 把现有规则结果转换为块、边、标注、遮罩的表现数据 | 不调用求解器决定玩家行为 |
|
||||
| `material.mjs` | 底沿、块面、边界、亮边和投影 | 不画数字或规则 |
|
||||
| `board-renderer.mjs` | 组织棋盘各绘制通道 | 不写存档和播放声音 |
|
||||
| `symbols.mjs` | 数字辅助框、奇偶、通道、类别、墙门 | 不以图形碰撞替代判定 |
|
||||
| `icons.mjs`、`ui.mjs` | 统一图标与按钮、面板、标签 | 不显示未实现的设置能力 |
|
||||
| `layout.mjs` | 舞台、UI、安全区、文字保留区和输入坐标变换 | 不改变关卡格点 |
|
||||
| `motion.mjs` | 时间轴、按压、门切换、扫光、减少动态 | 不阻塞输入 |
|
||||
| `cache.mjs` | 轮廓、材质和静态 UI 缓存及失效 | 不持有第二份玩法状态 |
|
||||
|
||||
数据流为:**关卡和路径 → 现有规则结果 → 表现场景 → 几何与布局 → 分层绘制 → 主 Canvas 合成**。触摸经同一布局逆变换回到原规则坐标。
|
||||
|
||||
### 2.3 保留快照来源的接入方法
|
||||
|
||||
1. 保持 `src/upstream` 快照及其来源哈希不变。在本地新增构建适配模块,扩展现有 `build.mjs` 的 UI 变换。
|
||||
2. 首先建立明确的“几何准备/绘制/UI 回写”接缝,再委托新 renderer 绘制。保留原输入、求解、存档、提示控制器、通关及教学业务。
|
||||
3. 构建变换以明确入口与预期匹配次数检查,入口不符直接构建失败。禁止对全部 `fillStyle` 或颜色字符串做无范围替换,也不运行时拦截 Canvas API 猜语义。
|
||||
4. 主题开关放在构建配置,支持旧皮肤与新皮肤对照;第一阶段不增加玩家设置项。
|
||||
5. `renderLesson` 必须使用同一 renderer;预览调用的临时状态要完整保存和恢复,新增场景及缓存状态也必须隔离,避免教学修改玩家棋盘。
|
||||
6. 构建变换模块与快照哈希一起纳入回归。未来如接入新上游,先检查接缝,再更新变换。
|
||||
|
||||
不直接依靠只读 `variantSnapshot()` 重建整个正式渲染:它当前不包含完整轮廓、共边入口和文字保留区。应由同一几何准备过程向棋盘、提示和教学输出共同的 Scene 数据。
|
||||
|
||||
## 3. 几何:先保证“一整块只算一步”
|
||||
|
||||
### 3.1 分开管理三种几何
|
||||
|
||||
| 几何 | 用途 | 约束 |
|
||||
|---|---|---|
|
||||
| 逻辑几何 | 原有命中、相邻、阻挡和手势插值 | 按原规则;皮肤不扩大相邻重叠触点 |
|
||||
| 显示轮廓 | 缩缝、圆角、底沿、材质 | 不反馈给规则判定 |
|
||||
| 信息安全区 | 数字、奇偶、类别、通道、提示避让 | 基于实际文字边界和轮廓内距生成 |
|
||||
|
||||
单元边、块 ID、共边入口全部保留稳定标识。显示轮廓按关卡和布局生成一次,按压动画只改变表现变换。
|
||||
|
||||
### 3.2 矩形与异形轮廓算法
|
||||
|
||||
1. 从 `units(c)` 获取占用单位格;每格生成定向四条边,共享内部边抵消。
|
||||
2. 连接剩余有向边,遍历所有闭环;按包含关系识别外环与内孔,统一绕向,删除共线点。
|
||||
3. 同一块的单位格先形成完整轮廓,再缩进、倒角、投影。任何中间单位格线都不进入材质绘制。
|
||||
4. 面轮廓向块内缩进 `inset = 0.045u`,凸角半径 `clamp(6, 0.18u, 12)`;凹角最多 `0.06u`,窄凹口可保持直角。
|
||||
5. 正交轮廓按边法线偏移并求相邻偏移边交点,限制尖角与短边;检查偏移后自交、环消失和细颈收缩。失败时先减小局部圆角和缩进,仍失败则使用保拓扑直角轮廓。
|
||||
6. 外环与内孔反向绕制,用基础非零绕数填充和裁切;目标宿主需做内孔自检。若目标不满足裁切行为,独立离屏遮罩擦出内孔后合成,不能擦除主画布已有内容。
|
||||
|
||||
矩形走专用圆角矩形快路径。禁止对 L、T、S 等异形逐单位格画圆角矩形,那会产生内部缝和多份阴影。棋盘块之间围成的空洞与“单块自身内孔”分别测试,后者只有经内容审计确认存在时才作为当前内容覆盖项。
|
||||
|
||||
### 3.3 六边形
|
||||
|
||||
沿用当前轴向坐标到竖屏顶点的转换。当前 renderer 的 `unit` 是外接半径缩放量,视觉用的六向中心距为 `uHex = √3 × unit`;材质和文字比例统一使用 `uHex`。
|
||||
|
||||
从实际顶点包围盒求适配比例;圆角上限 `0.11uHex`。墙门和桥仍取未圆滑的真实共边,不能把 q/r 范围当矩形宽高,不能用缩圆后的外轮廓推导邻接。
|
||||
|
||||
### 3.4 数字锚点
|
||||
|
||||
普通矩形保留整体中心;异形以现有占用单位格安全锚点为起点,再检查整组文字矩形是否留在块内。候选点按离中心距离、到外环/内孔距离、与墙门及通道的间隔排序;候选选择与 ID 稳定绑定。
|
||||
|
||||
同一关锚点固定,访问前后数字不跳位置。若完整信息组放不下,切换预定义密集排版;必要说明移至固定反馈栏。不得仅检查中心点在块内,就认定两位数和角标一定可放下。
|
||||
|
||||
## 4. 材质:用二维叠层表现轻厚度
|
||||
|
||||
### 4.1 每块的绘制配方
|
||||
|
||||
| 通道 | 实现 | 起始参数 |
|
||||
|---|---|---|
|
||||
| 接触投影 | 整块轮廓的短投影,静态或材质缓存时生成 | x0、y3、blur5、墨色10% |
|
||||
| 底沿 | 完整轮廓向下平移后实色填充,再由面覆盖 | `h=clamp(2,0.055u,4)` |
|
||||
| 正面 | 同一轮廓填充,中心文字区域尽量纯色 | 奶油/紫/类别面 |
|
||||
| 有效边界 | 清楚的连续描边 | 1.25,密集态1.5 |
|
||||
| 左上倒角 | 只强调朝光侧的内侧轮廓段,裁切于面内 | 0.75–1;紫块用 `primaryEdge` |
|
||||
| 右下收边 | 低强度暗边,必要时省略 | 不能叠成第二圈厚黑边 |
|
||||
|
||||
未走面 `#FFF8EA`、底沿 `#D8C4A0`、边界 `#8A7660`;标准已走面 `#6552D9`、底沿 `#49379E`。蓝橙类别块保持原类别面,访问仅增加紫内框和桥。
|
||||
|
||||
先按轮廓绕向确定外法线,再与左上光方向做点积选择受光边。圆角亮边用路径分段近似,端部淡化;不把整个块复制一层白描边。块面可选线性微渐变,各 RGB 通道变化不超过6,不用逐像素噪声、法线贴图或实时模糊制造质感。
|
||||
|
||||
投影用于接触感,边界负责可读性。全板先画底沿和投影,再画所有块面,避免后一块阴影压在前一块数字上。内孔处的底沿也按相同上下关系裁切,不填平孔洞。
|
||||
|
||||
### 4.2 桥接应成为积木的一部分
|
||||
|
||||
桥只出现在玩家实际连续访问、规则允许跨越的共边上,宽度取 `0.42u`,六边使用 `0.42uHex`,并受有效共边长度限制。
|
||||
|
||||
- 合并连续共线的单位共边,去掉端点安全距;多段共边按实际入口选择,选定后保持稳定。不能无条件取 `sharedSegments()[0]`。
|
||||
- 取共边中点和法向构造短胶囊,沿法向长度覆盖两侧缩进及少量重叠;不拿两块数字中心直接相连充当实体桥。
|
||||
- 绘制桥底沿和面,与块面小幅重叠以消除抗锯齿裂缝;隐藏被桥穿过的局部边界和亮边,保留其他外边界。
|
||||
- 桥厚度动效仅在沿共边方向展开,跨缝方向始终连通;墙保持可见,锁定开启门保留框与扣件。
|
||||
- 双色关桥可用紫色,但只覆盖桥本身,不能涂满两端类别面。
|
||||
|
||||
桥的连接关系来自逻辑,材质动画不会让一条非法边看起来可通过。
|
||||
|
||||
## 5. 状态、规则符号与提示合成
|
||||
|
||||
### 5.1 用组合状态替代单一颜色
|
||||
|
||||
每块至少分开持有:类别、已访问步号、固定要求、通道状态、冲突、当前末端、提示保留区。场景引用规则给出的墙门状态;这些字段共同决定绘制通道。
|
||||
|
||||
| 状态组合 | 块面和数字 | 附加表现 |
|
||||
|---|---|---|
|
||||
| 普通未走 | 奶油面,固定条件深墨字 | 固定条件内框 |
|
||||
| 普通已走 | 紫面白步号 | 深紫底沿、桥 |
|
||||
| 类别已走 | 蓝/橙面深墨步号 | 圆/三角、2单位紫内框 |
|
||||
| 固定要求已满足 | 沿所属访问态 | 条件框保持,以适配底色的浅/深色呈现 |
|
||||
| 普通冲突 | 深红面,实际步号 | 叹号和具体要求;反馈栏说明 |
|
||||
| 类别冲突 | 保留类别面与形状 | 红色局部角标和边;不整面染红 |
|
||||
| 当前末端 | 沿所属访问态 | 一组短焦点角标 |
|
||||
| 提示 | 保留全部底层状态 | 独立白芯深边轨迹 |
|
||||
| 完成 | 保留最终基础颜色和数字 | 外围短扫光、对勾与完成文案 |
|
||||
|
||||
空路径的固定1仍要清楚标识起点;表现模型将“起点条件”和“已访问”分开,计数、桥和实际步号完全依据玩家路径。
|
||||
|
||||
### 5.2 规则符号的程序实现
|
||||
|
||||
| 元素 | 几何做法 | 排版与语义 |
|
||||
|---|---|---|
|
||||
| 奇偶 | 字体绘制“奇/偶”+条件框 | 已走后数字在上,奇偶在下,副标目标11–12 |
|
||||
| 通道 | 端口到中心的折线路径,浅衬线+深槽线 | 六种方形直/弯通道,可反向,无箭头;数字右上 |
|
||||
| 类别 | 实心/描边圆与三角 | 持续可见;与通道组合时类别左下 |
|
||||
| 永久墙 | 真实共边上的深墨连续条带,裁切黄色短斜线 | 宽6–8;不使用虚线代替实体阻挡 |
|
||||
| 关闭门 | 两侧端框+中间连续棕色门片 | 附“关”,挡条连续 |
|
||||
| 开启门 | 保留端框,门片收向两侧 | 中间明确留空,附“开” |
|
||||
| 已穿门 | 开口+固定扣几何 | 可读“已过”;回退由规则恢复 |
|
||||
|
||||
通道位置由真实端口方向决定,中心数字与通道分区。非常密集时优先保留数字、类别和开口形态,将冗余解释移到反馈栏,不把全部信息缩成难辨的小点。
|
||||
|
||||
六边版只渲染实际数据支持的机制;若后续增加六向通道,应补六向端口合同,不能直接套用当前四向 `l/r/u/d` 绘制函数。实验转墙只接六边原型,不加入当前200关皮肤范围。
|
||||
|
||||
### 5.3 正式绘制顺序
|
||||
|
||||
| 顺序 | 内容 |
|
||||
|---|---|
|
||||
| 1 | 暖黄背景、外围静态纹样、舞台 |
|
||||
| 2 | 全板块体与桥的投影、底沿 |
|
||||
| 3 | 块面、桥面、有效轮廓及局部倒角 |
|
||||
| 4 | 类别、条件框、通道、数字、当前位置、局部错误 |
|
||||
| 5 | 独立提示层,先做遮罩和语义避让后合成 |
|
||||
| 6 | 最终墙门及必要语义补绘 |
|
||||
| 7 | 导航、反馈、操作按钮、弹窗 |
|
||||
|
||||
提示遮罩是必要条件;只把底层数字画在上面没有用,因为独立提示 Canvas 会更晚合成。
|
||||
|
||||
### 5.4 提示升级
|
||||
|
||||
保留当前分段提示的请求、段末消失、偏离、撤销恢复行为;只升级显示路线和遮罩。
|
||||
|
||||
1. 路线按“块内引导点 → 合法共边入口 → 下一块内引导点”构造。异形内部折线限制在所属块的占用区域内,必要时在占用单位格图上寻路;这是提示画线,不改变玩家规则。
|
||||
2. 已走块遮罩取完整显示轮廓,并包含底沿和抗锯齿安全边;已走桥也加入遮罩。覆盖错误路径的已走块同样擦除,不能只擦正确前缀。
|
||||
3. 数字、条件副标、类别、通道核心和墙门各输出文字/图形保留区。提示路径绕行或切段;光点与端点环采用相同裁切。
|
||||
4. 提示全程只改透明提示层;`destination-out` 只能在此层或专用遮罩层执行,不用于擦除主画面。
|
||||
5. 白芯4–7,深色外线总宽再增3–4;透明度与底色合成后复核。减少动态采用持续静态虚线,保留整段信息。
|
||||
|
||||
优先从真实共边生成引导路线,避免中心连线跨出 L 形块、穿过空洞。数字处允许线段中断,不为追求连续发光而遮挡关键要求。
|
||||
|
||||
## 6. UI、文字和屏幕适配
|
||||
|
||||
### 6.1 通用组件
|
||||
|
||||
背景默认 `#FFE69A`,可选底端 `#FFDE79`;舞台 `#FFF6DF`。背景仅生成3–5个静态圆、圆角方框、三角轮廓和弧线,使用关卡/页面稳定种子,避免每帧随机闪变;棋盘与操作区周围保留净区。
|
||||
|
||||
按钮由圆角路径、底沿、图标路径和文字构成。主按钮紫面白字,次按钮奶油面深墨字;常规高64、圆角18,紧凑高52、圆角14。按下表面下移1单位,底沿变浅,触点和布局不动。重开/撤销/指路按当前微信能力显示,完成后同一区域替换下一关。
|
||||
|
||||
功能图标统一24单位母版、2–2.5笔画:重开用回转弧+箭头,撤销用左回箭头,指路用圆弧灯泡+短光线,设置用齿轮路径,选关用圆角网格,对勾用两段折线。绘制前统一缩放与设置圆端点,颜色从语义令牌取。禁止使用 Emoji 代替正式图标,以免平台外观不同。
|
||||
|
||||
选关卡片、弹窗、规则教学共用组件。新增首页属于全页面阶段;现有微信包起始选关页并不等于已经有独立首页。新开关必须接线、持久化并通过生命周期检查后才显示。
|
||||
|
||||
### 6.2 字体选择
|
||||
|
||||
正式目标采用 Nunito 800 数字和 Noto Sans SC 中文。先清点实际使用汉字与字号,按许可交付所需字集及声明;不能根据字体名称就假设宿主已经可用。
|
||||
|
||||
字体加载属于平台适配工作,实施时核对目标基础库支持的加载方式和路径约束。进入对局前完成关键字形准备;若失败,使用明确回退并一次性完成测量,避免解题中途换字体。数字0–9也可制作原创贝塞尔轮廓,支持1–36拼排,维持一致字形;中文仍保留文字排版能力,不逐句做成轮廓。
|
||||
|
||||
路径命令、文字测量缓存和离屏绘制均不需要生产图片资源。首页品牌首版可用标题文字叠绘米白外缘、短底沿与珊瑚/紫主字;原创轮廓精修列为P2,避免把标题制作变成首个技术阻塞点。
|
||||
|
||||
### 6.3 自适应和统一坐标
|
||||
|
||||
矩形按占地包围盒计算 `u = min(stageInnerW/C, stageInnerH/R, 64)`,优选56;根据内容和屏幕进入密集布局。当前代码允许96/110的放大上限,要在新皮肤布局中改成设计报告的限制。
|
||||
|
||||
7列36块盘面:375宽采用外边距12、内边距8时净宽335,`u≈47.9`;320宽同策略净宽280,`u≈40`。后者需要真机检查,不能承诺全部达到44触点。
|
||||
|
||||
固定顶栏、反馈和底部操作区,舞台使用剩余高度。常规单数字约 `0.50u`、两位数约 `0.44u`;低于报告目标字号时输出布局诊断,不默默缩到不可读。
|
||||
|
||||
`Layout` 同时输出舞台原点、缩放、UI矩形和逆变换。同步修改当前 `event()` 中硬编码的 `top+82`,以及棋盘合成位置和尺寸;这三处必须取同一份布局。DPR只决定 backing store 清晰度,不能重复进入触摸坐标和逻辑单位。
|
||||
|
||||
## 7. 动效与运行调度
|
||||
|
||||
| 事件 | 时间与表现 | 规则/输入约束 |
|
||||
|---|---|---|
|
||||
| 按钮 | 80ms按下、120ms恢复,最多0.98缩放 | 点击即时生效 |
|
||||
| 落格 | 140ms,0.985压低后最高1.015回稳 | 固定命中;窄异形/墙门附近只变亮边 |
|
||||
| 桥接 | 120ms扩展宽度 | 不出现暂时断路 |
|
||||
| 数字 | 100–120ms淡入 | 固定条件持续可读 |
|
||||
| 撤销 | 100ms残影淡出 | 路径立即回退,批量撤销不串行等待 |
|
||||
| 错误 | 200ms红角/边强调 | 墙挡后末端不动,不全屏摇晃 |
|
||||
| 门 | 160ms门片收拢/展开 | 连续输入合并到最新目标状态 |
|
||||
| 完成 | 总计320ms外缘扫光,完成反馈500ms | 下一关可手动点击,不按块逐个等待 |
|
||||
|
||||
统一使用当前时刻求进度,采用设计令牌中的缓出曲线;新事件从当前视觉值过渡到新目标,旧门动画不排队。形变围绕稳定锚点,底沿与亮边一起响应。
|
||||
|
||||
减少动态时取消缩放、抖动、光点和粒子,门直接显示最终形态。退后台取消 RAF 和临时动效,回前台按最新玩法状态重建,清除过期动画;沿用当前音频中断管理,不额外合成声音。
|
||||
|
||||
当前主循环每帧全屏绘制,棋盘自身还会重设 Canvas 尺寸。新方案改为脏状态触发:输入、布局、页面、提示时间或动画进度变化才更新。无活动动效时停止 RAF;反馈自动消失、字体完成等异步变化显式标脏,不依赖常驻循环才看得到变化。
|
||||
|
||||
## 8. 缓存、兼容与性能预算
|
||||
|
||||
### 8.1 分层缓存
|
||||
|
||||
| 缓存 | 键/失效条件 | 策略 |
|
||||
|---|---|---|
|
||||
| 本地轮廓与真实共边 | 内容修订、几何ID | 与颜色和访问态无关 |
|
||||
| 屏幕几何和信息区 | 舞台尺寸、缩放、字体、密度档 | resize与字体准备后重建 |
|
||||
| 材质轮廓或小块缓存 | 轮廓、u、DPR、面类别、质量档、主题版本 | 惰性生成,有总量上限 |
|
||||
| 静态背景和UI | 页面、尺寸、主题、文字 | 状态变化时更新 |
|
||||
| 教学预览 | 教学关、尺寸、主题 | 不复用玩家可变Scene |
|
||||
|
||||
首版无需为每块每种状态建缓存,也不必每层创建全屏 Canvas。数字和墙门动态绘制,只有测量数据支持时才进一步缓存。Canvas宽高只在尺寸/DPR真正变化时赋值,避免每次落格重新分配并清空状态。
|
||||
|
||||
375×812、DPR3的全屏RGBA表面原始像素约10.45MiB,三张全屏表面约31.36MiB,尚未计宿主副本和其他内存。棋盘/提示按实际区域分配,缓存计数加上字节估算;质量降级先关模糊与粒子,再降低非关键层缓存分辨率或DPR,避免常驻大量全屏离屏层。
|
||||
|
||||
### 8.2 平台实现约束
|
||||
|
||||
基线采用项目已经使用的 `beginPath`、`lineTo`、`quadraticCurveTo`、`arc`、`fill`、`stroke`、Canvas合成方式。轮廓缓存为普通路径命令数组,重放到 context;不把 `Path2D`、原生 `roundRect`、滤镜或某种额外离屏接口作为必需前提。
|
||||
|
||||
新增裁切、渐变、投影和字体加载在微信目标环境逐项自检。原生投影若成本高或显示不一致,可用缓存时生成的2–3层低透明度短偏移轮廓近似;降级仍保留实色底沿与有效边界。每次绘制通道用 `save/restore` 隔离裁切、透明度、混合方式和虚线状态。
|
||||
|
||||
### 8.3 度量目标
|
||||
|
||||
目标是常规设备稳定60fps;低性能档可评估稳定30fps。绘制CPU耗时建议以P95低于8ms为首轮目标,并同时记录整帧耗时、输入到首次可见反馈、缓存字节数与重建次数。这些是待测目标,不能由桌面Canvas或模拟微信测试推导真机达标。
|
||||
|
||||
密集盘快速连画、分段提示、教学动画分别记录;纯思考阶段应无持续绘制。降级永远不删除墙门、数字、条件框、类别和提示信息。
|
||||
|
||||
## 9. 制作顺序与交付门槛
|
||||
|
||||
| 阶段 | 实际工作 | 交付门槛 |
|
||||
|---|---|---|
|
||||
| A 技术骨架 | 主题读取、布局统一、渲染接缝、几何与Scene | 原有行为保持,快照不变,教学状态隔离 |
|
||||
| B 黄金样张 | 用真实关卡生成基础、大块、异形、通道、双色、门及密集盘 | 面、底沿、桥和文字在复杂盘面清楚 |
|
||||
| C 交互单关 | 落格、连画、逐格回退、撤销、提示、门动画 | 输入即时,提示不改玩家填色 |
|
||||
| D 全页面 | 选关、对局、规则、完成;按范围新增首页与设置 | 窄屏、胶囊、安全区、按钮布局通过 |
|
||||
| E 全内容与平台 | 200关自动回归、视觉抽样、真机性能与生命周期 | 新皮肤独立产出验证记录,才进入发布 |
|
||||
|
||||
P0:真实轮廓、规则信息、类别保留、桥接、提示遮罩、输入坐标。
|
||||
|
||||
P1:完整轻厚度材质、UI字阶、字体、局部动效、缓存调度。
|
||||
|
||||
P2:首页原创字形精修、少量通关纸片。首版只做暖黄主题,不为200关逐关做美术资源。
|
||||
|
||||
建议先交付“6类真实关卡的可交互样张页”,共用正式 renderer,以配置切换主题、尺寸、减少动态与质量档。该页是开发验收工具,不进入玩家流程。预览和QA截图可以导出用于检查,但不可反向成为运行时游戏资源。
|
||||
|
||||
### 9.1 验收矩阵
|
||||
|
||||
| 验收项 | 方法与通过条件 |
|
||||
|---|---|
|
||||
| 无图片美术依赖 | 审查运行包和加载调用;没有作为游戏美术使用的位图、图片图集或外部SVG |
|
||||
| 几何与大块 | L/T/S、凹口、空洞样张;一块一次,内部无格线,轮廓无自交;桥不穿墙 |
|
||||
| 数字与组合 | 10/11/17/20/28/36、奇偶、通道、类别组合;保留区互不覆盖 |
|
||||
| 提示 | 比较开提示、撤销重画前后的已走面采样;基础填色相同,提示避开数字与通道 |
|
||||
| 门 | 空盘、落格、穿过、回退、重开;形态与 `doorOpen/doorLatched` 一致 |
|
||||
| 适配 | 320×568、375×667、390×844、414×896;至少含 r2-163/r2-187 |
|
||||
| 手势 | 快画、慢画、抬手续画、逐格退回、撤销、重开;屏幕位置与命中一致 |
|
||||
| 教学和弹窗 | 正式renderer复用,关闭后玩家路径/提示/坐标/存档不变 |
|
||||
| 动效生命周期 | 后台、返回、旋转/尺寸变化、减少动态;没有积压动画和状态漂移 |
|
||||
| 内容和构建 | 全关唯一解和原有输入回归通过;快照哈希与适配入口检查通过 |
|
||||
| 真机表现 | 同时记录视觉、帧耗时、内存估算、音频与触摸;浏览器截图不当作微信实测 |
|
||||
|
||||
### 9.2 后续实施时复用的检查命令
|
||||
|
||||
以下在仓库根目录执行,命令沿用现有工程;本次文档编写未执行新皮肤测试。
|
||||
|
||||
```powershell
|
||||
node docs/design/bright-classroom/audit-design.mjs
|
||||
Push-Location wechat-campaign-200-r2
|
||||
node build.mjs
|
||||
node --test hints.test.mjs lessons.test.mjs audio-copy.test.mjs
|
||||
node smoke.mjs
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
`build.mjs` 当前依赖本仓库原型内的 esbuild 安装;`smoke.mjs` 会读取manifest指定的上游来源路径。实施前检查这些依赖,缺失时修正可复现环境,不能把路径错误当成皮肤问题。原有颜色与采样断言需要主题参数化,同时保持“提示前后不改变玩家填色”的不变量,不能简单删除失败断言。
|
||||
|
||||
新增有实际价值的验证集中在多环轮廓、窄凹口退化、真实共边入口、信息区碰撞、教学Scene隔离、坐标正逆变换及提示像素不变量;颜色常量和简单绘制包装无需逐项写镜像测试。
|
||||
|
||||
## 10. 依据、结论和执行路径
|
||||
|
||||
下表是本方案的 Evidence → Finding → Path,对应本地文件,可从仓库根目录用 `Get-Content -LiteralPath <路径>` 复核;未以未运行的原型或真机截图作为证据。
|
||||
|
||||
| 证据 | 已观察事实 | 技术结论 | 执行路径 |
|
||||
|---|---|---|---|
|
||||
| E01:[完整美术报告](2026-10-21_美术设计-明亮教具室-report.md)及[令牌](design/bright-classroom/tokens.json) | 暖黄、奶油、轻厚度、明确规则和版本边界 | F01:程序化材质与语义分层,材质服从读盘 | P01:主题→材质→复杂关样张 |
|
||||
| E02:`wechat-campaign-200-r2/src/upstream/ui.mjs` | 现有Canvas几何、块、桥、墙门和UI回写 | F02:可复用Canvas,但不能只改颜色或删除draw业务 | P02:渲染接缝→Scene→新绘制通道 |
|
||||
| E03:`wechat-campaign-200-r2/src/upstream/rules.mjs` | 占用格、单环轮廓、共边、门和判定 | F03:显示需补多环与安全偏移,规则仍沿用原结果 | P03:边抵消→闭环→缩进倒角→共边入口 |
|
||||
| E04:`wechat-campaign-200-r2/src/hints.mjs`、`src/main.mjs` | 透明提示层、中心连线、玩家单位格擦除、延后合成 | F04:升级引导路线、完整遮罩和信息避让 | P04:统一几何→提示切段→像素不变量 |
|
||||
| E05:`wechat-campaign-200-r2/build.mjs`与README | 上游快照有哈希;构建注入教学与宿主 | F05:皮肤采用可检查的构建适配,教学复用同一renderer | P05:入口断言→构建→教学隔离→整包回归 |
|
||||
|
||||
第一项可验收工作应是:用真实大块/异形关和一个密集组合关,在当前微信宿主中跑通“完整轮廓+轻厚度+桥接+数字+提示遮罩”,并保持原有操作。通过后再铺开全页面,最终以200关回归与微信真机记录判定是否可以交付。
|
||||
@@ -0,0 +1,83 @@
|
||||
# 数字一笔200关 · 明亮教具室实现与验证
|
||||
|
||||
日期:2026-10-21。状态:已接入、已构建、自动回归与浏览器验证通过;微信手机验收待进行。
|
||||
|
||||
## 直接体验
|
||||
|
||||
- 源码与微信工程:[wechat-campaign-200-r2](../wechat-campaign-200-r2/README.md)。在微信开发者工具导入该目录,重新编译并生成新的手机预览。
|
||||
- 独立导入包:[NumberSwipe-campaign-200-r2-明亮教具室-20261021.zip](../releases/NumberSwipe-campaign-200-r2-明亮教具室-20261021.zip)。解压后导入;不需要构建npm。
|
||||
- 浏览器试玩:在工程目录执行 `node qa/serve.mjs`,打开 [明亮教具室试玩](http://127.0.0.1:17859/qa/bright-classroom.html)。提供200关、五种屏幕尺寸和真实关卡样张选择;页面上的自动画路按钮属于验收工具,不进入游戏包。
|
||||
|
||||
导入包 SHA-256:`53945F4558F7DD3EA4904B9AE2373E88288B728D16FC359504631AF550D1F251`。
|
||||
|
||||
## 已实现的美术
|
||||
|
||||
| 范围 | 当前表现 |
|
||||
|---|---|
|
||||
| 整体与UI | 暖黄背景、低对比静态几何纹样、奶油舞台、圆角面板、按钮下沿、紫色指路主按钮、程序路径图标 |
|
||||
| 棋盘 | 矩形/异形完整轮廓、多环与内孔支持、缩缝圆角、短底沿、接触投影、左上亮边与微渐变 |
|
||||
| 路径 | 普通块整面紫色;真实共边桥接;数字单位不随大块面积放大;当前位置短角标 |
|
||||
| 规则 | 固定条件框、奇偶副标、可反向通道、类别形状、连续黑黄墙带、开/关/已过门形态 |
|
||||
| 组合状态 | 蓝橙类别面全程保留;错误采用局部标记与反馈;六边条件框和角标单独适配 |
|
||||
| 指路 | 独立透明层;按真实入口和块内可通行区域引导;避让数字与规则;擦除已走块的完整面和底沿 |
|
||||
| 教学与完成 | 教学复用新renderer,隔离玩家状态;门160ms短切换、通关320ms短扫光;完成后原位显示下一关 |
|
||||
| 字体与偏好 | Nunito800数字、Noto Sans SC500/600/700中文子集;本地加载、许可及哈希记录;减少动态持久化 |
|
||||
| 调度 | 静态选关、无活动动效的思考状态与减少动态下静态提示停止RAF;前后台正确停止和恢复 |
|
||||
|
||||
关卡、顺序、规则、存档key、半音音效、抬手续画、逐格回退、撤销、重开、分段提示与手动下一关保持原有行为。当前起始页继续使用原有选关流程,没有增加独立首页或扩展商业系统。未制作额外主题、原创标题轮廓和通关粒子。
|
||||
|
||||
运行包中没有PNG、JPG、WebP、SVG图片美术;全部形状由Canvas绘制。`qa/`下截图仅是验收证据,打包时排除。字体共369700字节,附两份OFL与字集/SHA-256 manifest;构建工具、原始完整字体不进入运行包。
|
||||
|
||||
## 源码接入
|
||||
|
||||
| 文件 | 责任 |
|
||||
|---|---|
|
||||
| `src/bright-classroom/theme.mjs` | 直接导入设计令牌、字体加载、文字与面板基础绘制 |
|
||||
| `src/bright-classroom/geometry.mjs` | 全闭环轮廓、内缩、圆角、布局、稳定锚点、合并共边与块内引导 |
|
||||
| `src/bright-classroom/board-renderer.mjs` | 材质、桥、标注、类别、墙门、动效、表现缓存与提示几何 |
|
||||
| `src/bright-classroom/ui.mjs` | 纹样、图标、按钮与舞台 |
|
||||
| `bright-classroom-build.mjs` | 检查过的上游绘制接缝替换,保留原业务回写与教学状态恢复 |
|
||||
| `src/main.mjs`、`src/platform.mjs` | 宿主UI、输入、设置、脏状态调度、Canvas合成与诊断 |
|
||||
| `src/hints.mjs` | 保留分段控制器,新增几何引导与安全遮罩绘制 |
|
||||
| `fonts/`、`build-fonts.py` | 改名的静态字体子集、许可、manifest及离线重建脚本 |
|
||||
|
||||
上游四份来源快照的内容和哈希不变,`source-manifest.json`只更新适配版本说明。`build.mjs`已构建新 `game.js`;独立包只含三个入口/配置文件、41个音频和7个字体及许可/manifest文件。
|
||||
|
||||
## 验证结果
|
||||
|
||||
以下命令在 `wechat-campaign-200-r2` 执行,均已通过:
|
||||
|
||||
```powershell
|
||||
node build.mjs
|
||||
node --test bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs
|
||||
node smoke.mjs
|
||||
```
|
||||
|
||||
| 检查 | 结果与证据 |
|
||||
|---|---|
|
||||
| 模块测试 | 29/29通过:既有23项+6项新美术检查,包括内孔、200关四种尺寸几何、异形引导不穿洞、类别身份、字体哈希与构建接缝 |
|
||||
| 微信模拟整包 | 200/200唯一解与触摸通关;撤销/重开/提示;教学状态隔离;四种手机尺寸;前后台;存档/音效恢复。记录见 `validation.json` |
|
||||
| 新调度与偏好 | 字体本地装载调用、减少动态存档恢复、静态页与静态提示无RAF积压,加入 `smoke.mjs` |
|
||||
| 浏览器实际像素 | 200/200关的已走格采样像素在提示前后及撤销重画后相同。使用随包字体,入口 `qa/hints.html` |
|
||||
| 人工浏览器检查 | 基础大块、双色奇偶、六边墙、320×568密集36块;据此修正六边类别标记和焦点角标位置 |
|
||||
| 色对与内容审计 | 仓库根目录 `node docs/design/bright-classroom/audit-design.mjs` 通过规定色对检查,关卡统计仍为20章200关 |
|
||||
| 运行包资源 | ZIP条目检查无图片美术,无源码、QA截图、字体构建工具和完整源字体 |
|
||||
|
||||
浏览器截图:[大块关](../wechat-campaign-200-r2/qa/bright-classroom-basic.png)、[六边关](../wechat-campaign-200-r2/qa/bright-classroom-hex.png)。截图来自实际游戏包的浏览器宿主,不是概念效果图或微信真机截图。
|
||||
|
||||
## 微信手机验收
|
||||
|
||||
在开发者工具导入新目录或ZIP解压目录,重新编译并生成预览;至少检查基础大块、双色通道、门、六边和r2-163密集盘。关注字体是否实际加载、滑动与回退命中、墙门和两位数、提示遮罩、后台恢复、音效及持续操作耗时。
|
||||
|
||||
浏览器验证与模拟触摸不能证明手机的帧率、内存、音色和字体API兼容性。当前没有60fps或微信手机显示已验收的结论;字体失败时有稳定系统回退,并可在 `GameGlobal.numberSwipeR2.snapshot().fonts` 查看加载诊断。
|
||||
|
||||
## Evidence → Finding → Path
|
||||
|
||||
| 证据 | 结论 | 执行路径 |
|
||||
|---|---|---|
|
||||
| 原美术报告、tokens.json、真实200关及来源快照 | 风格应服从真实几何与规则,不能只替换颜色 | 轮廓与共边→材质→规则标注→UI与教学 |
|
||||
| 新专项测试、`validation.json`、浏览器200关像素检查 | 新renderer保持既有玩法、类别身份和提示填色不变量 | 实际game.js→模拟微信与浏览器→独立运行包 |
|
||||
| 真实浏览器样张与320宽密集关 | 六边角落与密集数字需独立排版 | 六边标记修正→复查样张→保留截图 |
|
||||
| 未执行微信手机预览 | 平台显示与性能仍需设备证据 | 新包导入→手机预览→记录设备与实测结果 |
|
||||
|
||||
字体上游:[Nunito](https://github.com/google/fonts/tree/main/ofl/nunito)、[Noto Sans SC](https://github.com/google/fonts/tree/main/ofl/notosanssc)。实际使用的源文件哈希、输出字重与子集哈希记录于 `fonts/manifest.json`,相应许可随包交付。
|
||||
@@ -0,0 +1,752 @@
|
||||
# 数字一笔画 · 明亮教具室完整美术与 UI 设计报告
|
||||
|
||||
**版本:1.0 设计提案|日期:2026-10-21|设计基准:375 × 812 逻辑单位|平台:手机竖屏小游戏、Android、iOS**
|
||||
|
||||
**核心定义:把路径推理表现为一套明亮、精确、可触摸的彩色教具。暖黄背景负责亲和,奶油舞台负责聚焦,紫色积木负责连贯,短反馈负责愉悦。**
|
||||
|
||||
本报告以用户选定的“明亮教具室”概念图为方向,结合当前仓库规则与 200 关微信包制作。文中参数是采用新风格时的设计规范,不是现有程序已实现的描述;本次交付报告、设计令牌与审计数据,不修改游戏规则、正式渲染、关卡或发布包。
|
||||
|
||||
“必须”表示本方案的验收要求;“建议”表示首轮实现起点;“待验证”表示需要真机或用户测试。尺寸都是逻辑单位,不能直接当作设备物理像素、Android dp 或 iOS pt。
|
||||
|
||||
## 阅读导航与配套文件
|
||||
|
||||
| 读者 | 优先阅读 | 需要作出的决定 |
|
||||
|---|---|---|
|
||||
| 制作人与主美 | 第 1–4、21、25 节 | 风格、产品气质、资源范围、实施顺序 |
|
||||
| UI 设计师 | 第 5–9、14–19 节 | 颜色、文字、组件、页面、适配 |
|
||||
| 棋盘与特效美术 | 第 10–13、20 节 | 拼块、规则符号、状态、动效、音效 |
|
||||
| 开发与测试 | 第 2、9–13、22–26 节 | 分支差异、状态优先级、绘制顺序、验收 |
|
||||
|
||||
配套:
|
||||
|
||||
- [选定概念图](design/brain-academy-concepts/bright-classroom.png):风格参考;不是生产资源切图。
|
||||
- [结构化设计令牌](design/bright-classroom/tokens.json):颜色、字体、尺寸、圆角、阴影、动效。
|
||||
- [静态色对审计](design/bright-classroom/contrast-audit.json):文字与关键图形的对比度。
|
||||
- [项目内容审计](design/bright-classroom/project-audit.json):200 关几何、机制、规模与来源文件哈希。
|
||||
- [复核脚本](design/bright-classroom/audit-design.mjs):在仓库根目录运行 `node docs/design/bright-classroom/audit-design.mjs`。
|
||||
|
||||

|
||||
|
||||
## 1. 风格定位:亲和的逻辑玩具
|
||||
|
||||
### 1.1 设计关键词
|
||||
|
||||
明亮、圆润、整洁、轻厚度、明确回应、全年龄。它比原“柔和积木”增加背景色、舞台层次与少量图形活力,同时保持思考阶段安静。
|
||||
|
||||
玩家感受到的是“拿起一套有品质的逻辑玩具”。教具是材质与组织方式,不意味着学科课程、考试、儿童专属产品或智商评估。
|
||||
|
||||
### 1.2 从参考作品借鉴什么
|
||||
|
||||
《灵活脑学校 一起伸展大脑》的官方画面把明亮大底、浅色答题区、简化实体物件、清楚数字与状态反馈组合在一起。本方案借鉴这种信息分层与触感;原创制作标题、图标、几何纹样、块体与音效,不使用其角色和界面资产。[任天堂官方画面与介绍](https://www.nintendo.com/hk/switch/azlsa/)
|
||||
|
||||
| 参考中的设计手法 | 本项目的转译 | 执行边界 |
|
||||
|---|---|---|
|
||||
| 彩色背景与浅色题目区 | 暖黄底与奶油棋盘舞台 | 舞台内不铺纹样 |
|
||||
| 简化的实体教具 | 正俯视、轻厚度的积木 | 不用透视改变邻接 |
|
||||
| 鲜明的数字与选中状态 | 深色线索、白色路径数字、独立当前位置 | 规则信息不能只靠色相 |
|
||||
| 声音和动作回应答题 | 落格、撤销、条件命中与通关的短反馈 | 不增加分数、倒计时或强制节拍 |
|
||||
| 成体系的亲和感 | 统一标题、按钮、图标、章节小标 | 首版不增加角色养成系统 |
|
||||
|
||||
### 1.3 概念图转生产时的四个修正
|
||||
|
||||
1. 大标题主要用于首页;正式对局缩为紧凑关号与章节信息,增加棋盘空间。
|
||||
2. 图中的珊瑚色白字标签改为深棕字;当前珊瑚色上白字对比度不足。
|
||||
3. 块体减少表面噪声与大面积渐变,保留倒角、底沿和短阴影;数字必须像清楚的印字。
|
||||
4. 概念图的“重开/补数字/指路”不能直接替换当前微信包的“重开/撤销/指路”。按钮由实际版本提供。
|
||||
|
||||
## 2. 项目玩法与版本边界
|
||||
|
||||
### 2.1 统一产品前提
|
||||
|
||||
手机竖屏,单机路径推理;每块走一次,数字对应精确的访问步号。核心体验是观察、推理、绘制与修正。页面不引入倒计时、体力、答题分数或 IQ 判断。松手后可继续,视觉不能暗示必须一次不抬手完成。
|
||||
|
||||
### 2.2 当前仓库存在的三类实现
|
||||
|
||||
| 实现 | 核对来源 | 本方案适配范围 | 不可混写的行为 |
|
||||
|---|---|---|---|
|
||||
| 数字原型 | `modes/01-number-stroke` | 矩形、大块、异形、数字、奇偶、补数 | 可点已走格截断;补数与原始线索分开 |
|
||||
| 六边原型 | `modes/03-hex-walls` | 六邻接、静态边墙、实验转墙 | 转墙按步号转 120°,不等于节奏门 |
|
||||
| 200 关微信包 | `wechat-campaign-200-r2` | 方形、大块异形、六边、墙、通道、双色交替、奇偶、节奏门 | 逐格回退、撤销、分段指路、手动下一关 |
|
||||
|
||||
当前 200 关包共有 20 章、200 关,方形 49 关、大块/异形几何 113 关、六边 38 关;规模为 3–36 个逻辑块。机制出现次数可重叠:墙 133、通道 97、双色交替 74、奇偶 68、节奏门 69。上述为本次读取数据后的统计,不把旧 README 的原型关数当作当前交付量。
|
||||
|
||||
当前关卡没有使用 `relations`、`orderChains`、`windows`、`gates`。规则模块虽有对应能力,本报告不为这些未使用字段增加首版图标、教学和页面。实验转墙只在六边原型单独适配。
|
||||
|
||||
### 2.3 规则与表现的职责
|
||||
|
||||
合法邻接、通道成立、门是否开启、线索冲突、提示段落、通关都由规则状态决定。美术只消费结果,不用材质、动画进度或屏幕碰撞代替逻辑。
|
||||
|
||||
例如门正在播放开启动画,玩家能否经过仍以逻辑状态为准;快速画两步也必须按两次状态变化绘制最终结果。
|
||||
|
||||
## 3. 注意力层级与丰富度控制
|
||||
|
||||
从高到低为:**规则几何与线索 → 当前路径与操作状态 → 必要导航与提示 → 材质 → 装饰。**
|
||||
|
||||
| 元素 | 思考阶段 | 操作阶段 | 完成阶段 |
|
||||
|---|---|---|---|
|
||||
| 棋盘 | 全部清晰、无循环浮动 | 局部回应 | 路径短扫光后稳定 |
|
||||
| 背景纹样 | 静止、低对比 | 不响应手指 | 可有一次极轻的强调 |
|
||||
| 标题与标签 | 稳定 | 不缩放、不跳动 | 简短祝贺替换状态文字 |
|
||||
| 功能按钮 | 位置固定 | 局部按压 | 原位替换下一关按钮 |
|
||||
| 粒子 | 无 | 无 | 最多少量,出现在棋盘外 |
|
||||
|
||||
装饰不以全屏固定面积百分比验收。首版建议全屏仅保留 3–5 个大号、低对比度、裁切在边缘的轮廓图形;棋盘、信息栏与底部按钮周围至少留出 12 单位净区。
|
||||
|
||||
复杂关自动降低装饰:7 列或包含三种以上机制时,背景纹样隐藏,舞台阴影减半,标题使用紧凑模式。装饰变化不参与规则。
|
||||
|
||||
## 4. 视觉构成与品牌元素
|
||||
|
||||
### 4.1 三层结构
|
||||
|
||||
外层是平静的暖黄纸板背景,中层是奶油色教具垫,内层是清楚的拼块。整张棋盘是主要物体,不再给每条提示、每段文案套一张卡片。
|
||||
|
||||
背景可以有极浅的上下色差,但不使用真实桌面、教室透视、黑板墙、文具堆或复杂场景。无需角色也能成立。
|
||||
|
||||
### 4.2 标题与标识
|
||||
|
||||
“数字一笔”为工作显示名。首页使用原创圆润字形标识,可有米白描边与 2–3 单位下沿,珊瑚与紫色分配到字组;不逐字随机换色。正式对局不重复摆放大 Logo。
|
||||
|
||||
首版不增加吉祥物。未来需要角色时,先放首页、教学末尾和章节结束,避开棋盘。不要让角色表情承担错误与通关的唯一信息。
|
||||
|
||||
### 4.3 背景图形家族
|
||||
|
||||
只使用圆、圆角方框、三角外轮廓和短弧线。线宽 2–3,尺寸 36–88,统一 `pattern` 色的 20%–35% 不透明度;此参数是装饰起点,需按实际合成画面复核。禁止使用箭头、数字、“奇/偶”、墙条纹、门缝作为背景装饰。
|
||||
|
||||
## 5. 完整配色系统
|
||||
|
||||
颜色以不透明 sRGB 值为准;透明图层、渐变与实际贴图另作合成校验。配色语义优先于章节美化。
|
||||
|
||||
### 5.1 背景、表面与积木
|
||||
|
||||
| 令牌 | 色值 | 用途 | 约束 |
|
||||
|---|---|---|---|
|
||||
| `background` | `#FFE69A` | 默认暖黄背景 | 首版全章节稳定使用 |
|
||||
| `backgroundBottom` | `#FFDE79` | 背景底部可选色 | 不用于文字底板和棋盘块面 |
|
||||
| `pattern` | `#E8BE54` | 外围浅纹样 | 使用低不透明度;不承担信息 |
|
||||
| `stage` | `#FFF6DF` | 棋盘舞台 | 不加纹样和噪声 |
|
||||
| `surface` | `#FFFBF2` | 次按钮、面板、弹窗 | 小面积高层表面 |
|
||||
| `surfaceMuted` | `#F1E6D0` | 分组与次级底板 | 不用作禁用的唯一表现 |
|
||||
| `cellFace` | `#FFF8EA` | 未走积木正面 | 依靠清楚轮廓识别 |
|
||||
| `cellDepth` | `#D8C4A0` | 未走块底沿 | 不能替代关键边界 |
|
||||
| `cellBoundary` | `#8A7660` | 积木轮廓 | 默认 1.25,密集态可到 1.5 |
|
||||
|
||||
块面与舞台颜色接近是有意保留的柔和关系,二者本身约 1.02:1;必须使用有效轮廓,不能只用阴影区分拼块。
|
||||
|
||||
### 5.2 主色、文字与品牌色
|
||||
|
||||
| 令牌 | 色值 | 用途 |
|
||||
|---|---|---|
|
||||
| `ink` | `#43382F` | 标题、线索、主文案、规则图标 |
|
||||
| `inkSecondary` | `#71604F` | 说明、章节、计数标签 |
|
||||
| `primary` | `#6552D9` | 标准模式已走路径、主按钮 |
|
||||
| `primaryPressed` | `#5140B7` | 主按钮按压、淡紫面文字、焦点 |
|
||||
| `primaryDepth` | `#49379E` | 紫色积木底沿 |
|
||||
| `primarySoft` | `#ECE7FF` | 选关选中底、补数小底板 |
|
||||
| `primaryEdge` | `#ADA0F1` | 局部倒角提亮 |
|
||||
| `onPrimary` | `#FFFFFF` | 深紫与错误深红上的数字和文字 |
|
||||
| `coral` | `#F57950` | 首页品牌、章节小标签、重开图标 |
|
||||
| `coralInk` | `#4F2418` | 珊瑚底小字 |
|
||||
| `coralSoft` | `#FFE0CF` | 轻量章节底与装饰 |
|
||||
|
||||
珊瑚不是错误色,黄色不是提示的唯一依据,紫色不是所有模式里所有块的统一填色。禁止把双色关画过的蓝橙格全部变紫。
|
||||
|
||||
### 5.3 语义色与特殊机制
|
||||
|
||||
| 语义 | 色值 | 必须同时具备的非颜色线索 |
|
||||
|---|---|---|
|
||||
| 错误面 | `#B93B46` | 叹号、所需数字/具体原因 |
|
||||
| 错误浅底/文字 | `#FBE4E5` / `#862735` | 固定反馈栏文案 |
|
||||
| 提示强调/浅底 | `#8B5E00` / `#FFF0B8` | 角标、双层线或段落文案 |
|
||||
| 提示白芯/深外线 | `#FFFFFF` / `#43382F` | 独立路径线型;不替玩家填色 |
|
||||
| 成功色/浅底 | `#2E6B52` / `#DEEFDC` | 对勾与“完成” |
|
||||
| 蓝类别块/标记 | `#CEE4FF` / `#245A92` | 圆形标记 |
|
||||
| 橙类别块/标记 | `#FFE0B2` / `#8A470E` | 三角标记 |
|
||||
| 永久墙 | `#43382F` + `#F7C74D` | 实体连续条带+短条纹 |
|
||||
| 门关闭/开启 | `#8B4B29` / `#2E6B52` | 中央封闭/明确开口;关/开 |
|
||||
| 禁用面/文字 | `#E9E1D3` / `#7A7064` | 状态不可操作、无按压下沿 |
|
||||
|
||||
禁用控制不降低到近乎看不见;其解释仍使用可读文字。禁用项不纳入本次必须通过的色对门槛,不等于已验证其最终呈现。
|
||||
|
||||
### 5.4 色彩变体
|
||||
|
||||
首版只制作暖黄主主题。章节用图形小标、标签字和题卡组织区分,路径色与错误、墙、门语义保持不变。未来薄荷/珊瑚背景主题必须重跑对比度,并重新检查双色块、错误面和提示轨迹,不能全局自动换色。
|
||||
|
||||
## 6. 字体与文字表现
|
||||
|
||||
### 6.1 字体分工
|
||||
|
||||
| 用途 | 首选 | 字重 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 路径数字与关卡编号 | Nunito | 800 | 圆润但字腔清楚,重点检查 1/7、3/8、6/9 |
|
||||
| 中文标题 | Noto Sans SC | 700 | 首页 Logo 另制作原创字形 |
|
||||
| 按钮与短标签 | Noto Sans SC | 600–700 | 不能用 Logo 字体替代小字 |
|
||||
| 正文、规则 | Noto Sans SC | 500 | 长文读起来稳定 |
|
||||
| 英文 | Nunito | 600–800 | 不强制全大写 |
|
||||
|
||||
Nunito 与 Noto CJK 对应仓库提供 SIL Open Font License 文件;实际分发须随所使用的字体文件保留相应许可证与版权说明,子集、改名等处理按实际许可证要求检查。[Nunito 许可](https://github.com/googlefonts/nunito/blob/main/OFL.txt)、[Noto CJK 许可](https://github.com/notofonts/noto-cjk/blob/main/Sans/LICENSE)
|
||||
|
||||
本次没有下载、打包或子集化字体。正式资源交付需记录字体文件版本及哈希。系统回退只能用于开发预览,不作为跨平台字形一致的保证。
|
||||
|
||||
### 6.2 字阶:375 宽设计基准
|
||||
|
||||
| 层级 | 字号/行高 | 字重 | 具体用途 |
|
||||
|---|---|---|---|
|
||||
| 首页展示标题 | 32 / 40 | 800 或原创 Logo | 首页,不放进常规对局 |
|
||||
| 页面标题 | 24 / 32 | 700 | 选关、设置、规则标题 |
|
||||
| 分组标题 | 18 / 26 | 700 | 章节、教学页标题 |
|
||||
| 正文 | 16 / 24 | 500 | 规则说明、设置正文 |
|
||||
| 按钮文字 | 16 / 22 | 700 | 下一关、三项操作 |
|
||||
| 关键标签 | 14 / 20 | 600 | 关号、剩余、状态 |
|
||||
| 辅助说明 | 13 / 18 | 500 | 短教学、章节补充 |
|
||||
| 最小辅助字 | 12 / 16 | 500 | 非必要编号、小型角标 |
|
||||
|
||||
中文常规字距 0;单行展示标题最多 0.02em。正文不做粗描边、凸起或长投影。中文与数字按视觉基线微调,不能逐平台随机下沉 4% 而不检查实际字体。
|
||||
|
||||
### 6.3 棋盘字号
|
||||
|
||||
设 `u` 为矩形单位格间距,六边模式则采用相邻共边两格中心的距离作为视觉单位 `uHex`;不能直接用六边外接圆半径套矩形比例。
|
||||
|
||||
| 信息 | 标准比例 | 典型 u=56 | 密集态建议 |
|
||||
|---|---|---|---|
|
||||
| 起点 1 | 0.56u | 31 | 不靠巨型字号区分起点 |
|
||||
| 单位数步号/线索 | 0.50u | 28 | 下限目标 18,实际服从可用空间 |
|
||||
| 两位数 | 0.44u | 25 | 下限目标 16,超过空间则列为适配问题 |
|
||||
| 未走奇/偶 | 0.42u | 24 | 与数字相近的阅读权重 |
|
||||
| 已走奇偶副标 | 0.20u | 12 | 需要 11–12;不挤进两位数 |
|
||||
| 通道角标数字 | 0.24u | 13 | 不低于 11,必要时换排版 |
|
||||
|
||||
数字字号由单位格决定,不随 2×2、3×3 的面积放大。两位数整体居中;测试 10、11、17、20、28、36 的实际宽度。静态线索不能发生字形跳动。
|
||||
|
||||
奇偶+数字的密集块采用“数字在上、奇偶在下”;通道+数字采用“中心通道、角落数字底板”。两种布局不叠加使用,当前数据的实际组合由审计决定;未来组合先出样张再放进关卡。
|
||||
|
||||
## 7. 圆角与形状语言
|
||||
|
||||
| 元素 | 标准圆角 | 使用说明 |
|
||||
|---|---:|---|
|
||||
| 小型图标底 | 8 | 不为每个图标都加底 |
|
||||
| 标签 | 12 | 随高度限制,不能超过半高 |
|
||||
| 三项操作按钮 | 18 | 高 64;紧凑态高 52 时用 14 |
|
||||
| 内容卡片 | 24 | 章节、设置分组 |
|
||||
| 棋盘舞台 | 28 | 密集/窄屏可降为 20–24 |
|
||||
| 对话框 | 24 | 支持正文换行 |
|
||||
| 底部面板 | 顶角 28 | 不挤压原有棋盘布局 |
|
||||
| 拼块外角 | `clamp(6,0.18u,12)` | 大块仍用同一 u |
|
||||
|
||||
异形块先合并单位格形成单一轮廓,再做圆角与描边;内部线、内部阴影、内部倒角全部消失。凹角处理上限为 0.06u;狭窄凹口优先保留结构,必要时不圆。
|
||||
|
||||
六边外角圆滑量建议不超过 0.11uHex,并保留足够直线共边。墙和门要贴在逻辑共边上,不能跟随柔化轮廓漂移到格内。
|
||||
|
||||
## 8. 材质、阴影与光照
|
||||
|
||||
### 8.1 二维制作模型
|
||||
|
||||
本风格可以用二维程序几何+少量贴图实现。光源统一在左上,正俯视,不引入可旋转相机。视觉顺序为短投影、底沿、块面、轻倒角、规则标识。
|
||||
|
||||
| 参数 | 建议值 | 限制 |
|
||||
|---|---|---|
|
||||
| 厚度 | `clamp(2,0.055u,4)` | 大块不因面积增加厚度 |
|
||||
| 按下后可见底沿 | 1 | 只压低表现层,不挪命中区域 |
|
||||
| 外轮廓 | 1.25 | 密集态 1.5;不得虚化 |
|
||||
| 倒角亮边 | 0.75–1 | 不围一圈高亮白环 |
|
||||
| 块面渐变 | RGB 各通道变化不超过 6 | 中央字区优先纯色 |
|
||||
| 接触投影 | x0 / y3 / blur5 / 10% | 色源使用主墨,须检查实际合成 |
|
||||
| 舞台投影 | x0 / y4 / blur10 / 8% | 首版一层即可 |
|
||||
| 按钮投影 | x0 / y3 / blur5 / 12% | 按压时减弱 |
|
||||
| 弹窗投影 | x0 / y12 / blur28 / 16% | 仅高层面板使用 |
|
||||
|
||||
“厚度”用向下偏移的实色轮廓表达;阴影只提供接触感,不能把块形泡软。密集关先降投影,再降厚度,不删除有效边界。
|
||||
|
||||
### 8.2 数字像印字,积木像教具
|
||||
|
||||
生产版数字不用概念图那种明显浮雕与长投影。允许 0–0.5 单位极轻底缘,但优先纯色字形。材质纹理不覆盖数字、通道和奇偶区。背景和块面不叠随机颗粒动画。
|
||||
|
||||
### 8.3 大块与异形的阴影
|
||||
|
||||
阴影依据整块轮廓生成一次,不能对每个单位格分别投影。大块底沿与邻块缝隙至少留出 1 单位清晰分界;最大厚度不侵入下一行文字区。桥接区域统一处理底沿,避免看起来像额外小块。
|
||||
|
||||
## 9. 布局、尺寸与适配
|
||||
|
||||
### 9.1 375 × 812 对局基准
|
||||
|
||||
下表为示意坐标,假设顶部与底部安全区各 24。正式布局读取平台实际安全区与小游戏胶囊区域;不能硬编码为所有设备 24。
|
||||
|
||||
| 区域 | 参考位置 | 作用 |
|
||||
|---|---|---|
|
||||
| 顶部导航 | x20,y24,高48 | 选关、紧凑关号、音效/设置 |
|
||||
| 关卡标题 | x20,y80,高40 | 章节与关号,不重复大 Logo |
|
||||
| 计数栏 | x20,y128,高28 | 已走与剩余;保持单行 |
|
||||
| 棋盘舞台 | x20,y168,335×464 | 内边距 16;中央放几何 |
|
||||
| 反馈区 | x20,y640,高40 | 最多两行;不因错误出现而推棋盘 |
|
||||
| 操作区 | x20,y692,335×64 | 三按钮,间隔 10 |
|
||||
| 底部留白 | y756–788 | 隔开手势区域 |
|
||||
|
||||
三按钮等宽约 105。图标 22–24、文字 16,两者间隔 4;整按钮是触点,不能只点击图标。首页大标题与功能图标的 3D 感强一些,对局导航保持简洁。
|
||||
|
||||
### 9.2 以可用空间求棋盘尺度
|
||||
|
||||
在矩形/异形盘面中,先按单位格占地求包围盒列数 C、行数 R,再按舞台净宽 W 与净高 H 计算 `u=min(W/C,H/R,uMax)`。逻辑拼块数不能代替 C、R;36 块不等于 6×6。
|
||||
|
||||
默认最大 u 建议 64,优选约 56;很小的教学关居中并保留空间,不把 3 块放大到满屏。通常节距不低于 44 是目标,不是所有盘面的数学保证。
|
||||
|
||||
当前 200 关中存在 **7 列 × 6 行、36 块**的关卡,例如 `r2-163`、`r2-181`。标准舞台净宽 303 时 u≈43.3,应进入密集布局:外边距 12、舞台内边距 8,净宽 335,u≈47.9。
|
||||
|
||||
在 320 宽屏幕,同样密集布局净宽 280,u≈40,必须单独试玩,不能宣称所有格都达到 44 或 48。处理优先级为:减少装饰与内边距 → 压缩标题与反馈占高 → 稳定数字与符号排版 → 实机检查手势;不得随意扩大相邻重叠命中区。
|
||||
|
||||
六边盘面用实际顶点集合求包围盒,再统一缩放。六向中心距、外接半径和符号安全区分别计算;不能把六边的 q/r 范围直接当矩形列数。
|
||||
|
||||
### 9.3 设备档位
|
||||
|
||||
| 档位 | 典型画布 | 布局处理 |
|
||||
|---|---|---|
|
||||
| 紧凑 | 320×568 | 取消独立大标题,顶栏40、信息24、反馈32、操作52;舞台用剩余高度 |
|
||||
| 常规短屏 | 375×667 | 标题一行;按钮56;必要时降低舞台上下空白 |
|
||||
| 基准 | 375×812 / 390×844 | 使用标准排布并按真实安全区调整 |
|
||||
| 长屏 | 414×896 | 增加舞台留白,不无限放大数字与按钮 |
|
||||
| 宽屏竖持 | 平板 | 内容建议最大宽520–560;棋盘单独合理放大,文案保持可读 |
|
||||
|
||||
画面高度不足时用分区预算重新求舞台,不整体缩放一张 375×812 截图。不在对局中加入滚动;规则面板可滚动或分页,但绘制输入必须与面板隔离。
|
||||
|
||||
## 10. 基础棋盘与路径设计
|
||||
|
||||
### 10.1 单块、大块、异形、空洞
|
||||
|
||||
| 元素 | 表现规则 | 玩家应理解 |
|
||||
|---|---|---|
|
||||
| 单位块 | 统一正面、轻底沿、清楚轮廓 | 一个节点、一步 |
|
||||
| 1×2/2×1 | 整块连续面,数字不拉伸 | 长边提供邻接,仍只一步 |
|
||||
| 2×2/3×3 | 没有内部小格线 | 面积与步数分离 |
|
||||
| I/L/S/T 等异形 | 联合轮廓、保留凹口 | 凹口与出口属于推理信息 |
|
||||
| 空洞 | 直接露舞台,不画浅色假格 | 没有节点、不能斜穿 |
|
||||
| 六边 | 六向共边清楚 | 碰角不构成邻接 |
|
||||
|
||||
块面建议每边内缩约 0.045u,形成约 0.09u 的视觉缝隙。实际共边仍以逻辑占地计算;缝隙只是渲染表现。手势以真实逻辑区域判断,阴影、底沿和提示外线不参与命中。
|
||||
|
||||
### 10.2 路径桥
|
||||
|
||||
桥宽建议 0.42u,长度只覆盖两块实际共边的视觉缝隙与必要搭接。已走块用整块填色,桥让路径连续;不再在块中央加一条抢眼的常驻粗线。
|
||||
|
||||
长共边上的桥应来自规则与布局求得的共享边线段,放在该段安全位置。异形的形心可能在空洞中,不可直接以两块中心连线当作正确桥。多段共边时采用稳定选择,避免重绘后跳位置。
|
||||
|
||||
### 10.3 数字锚点与既有实现差异
|
||||
|
||||
概念图把大块数字放在左上单位区;这不能当作当前代码的统一事实。原型 `anchorCenter` 对矩形使用包围盒中心,对异形选择形心附近的实际占用单位。
|
||||
|
||||
生产默认:**矩形数字居中,异形数字放在实际占用的安全单位中心,位置在整局固定。** 不随进出方向移动。优先沿用各分支稳定布局,避免只改皮肤导致线索跳位。
|
||||
|
||||
若主美希望保留概念图的左上锚点,需要单独做“中心/左上”可读性评审并列为排版变更,不能无说明改变。通道格采用专用角标数字,不套普通居中规则。
|
||||
|
||||
## 11. 规则元素的专用表现
|
||||
|
||||
### 11.1 固定数字与动态步号
|
||||
|
||||
固定线索使用相同数字字形,额外配 1.5–2 单位深墨内框或短角标,作为“这是条件”的持久区别;动态步号不加此框。走上固定线索后,框切为浅色高对比形态,数字仍保持阅读。
|
||||
|
||||
起点 1 可在空路径时做一次轻强调,不持续跳动;没有固定 1 的关卡不画虚构起点。早期数字原型允许从任意合法起点尝试时,说明使用“选一块开始”,不能继续写“从 1 开始”。
|
||||
|
||||
走错时保留实际步号并显示“需 8”一类所需值。若 36 块密集盘无法同时容纳两者,块内保留实际步号+叹号,固定反馈栏显示“这块应在第 8 步经过”;不要用错误填色把原始要求永久抹掉。
|
||||
|
||||
### 11.2 奇偶
|
||||
|
||||
未走格中央显示“奇/偶”,配固定条件内框。已走后显示实际步号,下方保留奇偶字。数字原型补数落在奇偶格时同时保留两项条件,不能删除奇偶。
|
||||
|
||||
错误示例文案:“这块需要偶数步,现在是第 5 步。”用词解释访问步数,不谈积木面积。海外版本需使用清楚可教学的 ODD/EVEN 标签;不能直接假设单字母 O/E 对所有玩家自明。
|
||||
|
||||
### 11.3 通道与方向
|
||||
|
||||
当前 `direction` 是连接两条边的通道,可反向经过;不是单向箭头。以两条平行边或深墨槽线画直通/弯通,连接实际边方向,中心不画箭头。不能作为起点或终点。
|
||||
|
||||
中心通道线建议宽 4–5,外侧浅衬线总宽 8–10,随密度降低但保留两层区别。角落数字放 14–18 的小底板;通道成立后改变衬线与端点标记,错误时标注冲突出口,不整块用厚高光盖住槽线。
|
||||
|
||||
### 11.4 双色交替
|
||||
|
||||
当前数据类别是圆形与三角形,现有画面采用蓝/橙。新皮肤采用浅蓝面+圆标、浅橙面+三角标;两类颜色持续保留,类别形状不变。
|
||||
|
||||
已走态增加紫色内轮廓、实际步号和桥接,不把类别面覆盖为普通紫块。建议内轮廓 2,向内放置,避免扩大块外轮廓。错误用局部红角标与边,不把整块永久染红遮住类别。
|
||||
|
||||
底部一次性规则示例“蓝色与橙色交替走”。颜色之外必须能靠圆/三角识别;类别标放在安全角落,避免与通道数字角标重叠。复杂格优先通道居中、数字右上、类别左下,相关小标不低于 11–12。
|
||||
|
||||
### 11.5 永久墙
|
||||
|
||||
墙位于两格的逻辑共边上。深墨实体带建议宽 6–8,中心配黄色短斜条,端部不越过相邻顶点。细缝、阴影或单色黄线都不能单独代表墙。
|
||||
|
||||
撞墙是“不允许追加”,不是走入后算错。墙做一次局部 160–200ms 强调,路径末端保持原位,反馈“这条边有墙,换个方向”。墙不能藏在块的下沿或桥后。
|
||||
|
||||
### 11.6 节奏门:按步数切换
|
||||
|
||||
“节奏”指每落一块切换开关状态,不是按音乐节拍操作。当前微信包的教学约定为空盘关闭,起点也触发切换;通过时必须已开,通过后锁定开启;退回到穿门之前或重开后恢复相应状态。门状态以 `doorOpen` 与 `doorLatched` 为准。
|
||||
|
||||
关闭:双端框+连续棕色挡条+“关”;开启:框架保留、中央留明显通道+“开”;锁定开启:保持开口,附小型固定扣/“已过”。开关状态不能仅靠棕变绿。
|
||||
|
||||
开闭动画建议 160ms,采用门片收拢/展开,不旋转整张棋盘。连续输入时可以合并表现至最新状态,但不能排队播放旧状态阻断操作。没有落格时门不自行切换。
|
||||
|
||||
### 11.7 实验转墙:只适配六边原型
|
||||
|
||||
墙绕固定顶点每走一格顺时针转 120°,起点计入;撤销反向还原。用明确轴心圆点与单根墙臂表现,必要时仅在教学显示下一朝向短弧标。
|
||||
|
||||
转墙与节奏门使用不同外形;不得用门的开口图标代替旋转墙。动画只表现 `spinnerPos` 的结果,碰撞用步号 t 判断,不依据动画中途角度。实验转墙不默认加入 200 关包。
|
||||
|
||||
## 12. 状态系统与覆盖优先级
|
||||
|
||||
### 12.1 状态矩阵
|
||||
|
||||
| 状态 | 块面 | 数字与标记 | 边缘/反馈 |
|
||||
|---|---|---|---|
|
||||
| 未走普通块 | 奶油色 | 无步号 | 深暖灰边、轻底沿 |
|
||||
| 未走条件块 | 奶油或类别面 | 固定数字/奇偶/通道 | 条件框保持 |
|
||||
| 标准已走 | 紫色 | 白色实际步号 | 桥接、紫色下沿 |
|
||||
| 双色已走 | 原蓝/橙类别面 | 深墨步号+类别 | 紫色内边与桥 |
|
||||
| 当前末端 | 沿已走态 | 步号保持 | 一组短焦点角标 |
|
||||
| 已走冲突 | 普通模式深红;双色保留类别 | 实际步号、叹号、必要条件 | 局部冲突标记+原因 |
|
||||
| 原型单步提示 | 保留原面 | 不覆盖原条件 | 双层角标/外框 |
|
||||
| 微信分段提示 | 保留玩家与类别面 | 数字和规则保持清楚 | 未完成段上白芯深边线 |
|
||||
| 通关 | 保持最终颜色 | 数字稳定 | 一次扫光+完成文案 |
|
||||
|
||||
### 12.2 多状态同时存在
|
||||
|
||||
语义判断先于绘制。错误说明优先于普通说明,门墙与通道优先于材质,数字优先于扫光。冲突+提示时保留冲突原因,只在未完成区域显示指路,不用黄色把错误擦掉。
|
||||
|
||||
当前末端、固定条件与提示使用不同线型:末端为短角标,固定条件为内框,提示为带深衬的线/外侧标记。不能让所有状态都成为三层发光圆环。
|
||||
|
||||
## 13. 提示、撤销、重开与通关
|
||||
|
||||
### 13.1 按版本提供操作
|
||||
|
||||
| 版本 | 常规三按钮 | 撤销方式 | 通关后 |
|
||||
|---|---|---|---|
|
||||
| 200 关微信包 | 重开/撤销/指路 | 按钮与逐格回退 | 原操作区替换为“下一关”;末关返回选关 |
|
||||
| 数字原型 | 重开/补数字/指路 | 既有手势/点已走格截断 | 保留该分支当前自动进关行为,另案才修改 |
|
||||
| 六边原型 | 根据现有能力映射重开/撤销/提示 | 逐格撤销 | 按分支现有行为 |
|
||||
|
||||
未实现的补数字不能出现在当前微信包的常驻操作栏。若之后移植,须独立做玩法与存档设计,不借换皮顺手加入。
|
||||
|
||||
### 13.2 当前微信包的分段指路
|
||||
|
||||
保持现有机制:点击显示最早尚未正确完成的一段;段落按起点、数字点、通道方向点、奇偶点、终点划分;正确到达段末后消失,再次点击才展示下一段。偏离正确路线时,不沿错误路径继续延展。
|
||||
|
||||
表现为白色内芯+深墨外线,有限运动的白光点用于方向说明。提示层独立于玩家棋盘,并从提示图层中擦除玩家已经走过的实际拼块区域;已画连接不重复叠提示。底层填色、数字、通道和蓝橙类别不得因指路改变。
|
||||
|
||||
标准建议:内芯 4–7,外线总宽增加 3–4;提示在奶油上也必须能辨认。端点环避开中心数字,光点经过时不覆盖关键标记。用遮罩与切段避让数字、通道、墙门的核心信息。
|
||||
|
||||
当前提示会保持到段末完成,不将其改成 2.4 秒自动消失。`hintAttention=2400` 只表示刚请求时的注意力强调窗口;减少动态模式下静态虚线路径与端点仍持续存在。
|
||||
|
||||
### 13.3 数字原型的补数与提示
|
||||
|
||||
补数只添加一个真实确定步号,不自动画路;新增线索用小型紫色短角标标识“辅助补出”,不能和玩家步号混淆。重开保留本局补数,退出重进按该原型现有规则恢复原始题目。
|
||||
|
||||
单步指路为空路径指起点、正确前缀指下一块、走错时指最后正确位置。需要回退就直接说明“退回标记位置”,不能显示会被误认为正确下一步的箭头。
|
||||
|
||||
### 13.4 重开与撤销
|
||||
|
||||
重开是普通操作,不使用破坏性红按钮;首版不增加每次都弹的确认框。保留本版本的关卡、偏好与提示恢复规则。撤销无可退步骤时禁用,位置与宽度保持不变。
|
||||
|
||||
路径回退按逻辑立即生效,动画只淡出被删除的步号和桥。批量回退不得逐步串行播放几秒。当前微信包不新增“点任意已走格跳回”的行为。
|
||||
|
||||
### 13.5 通关
|
||||
|
||||
微信包保持手动下一关:完成后棋盘固定,反馈栏换为“完成!准备好就进入下一关。”,三按钮区原位替换成单个主按钮。按钮不能从屏幕底外飞入推挤棋盘,最后一关显示“返回选关”。
|
||||
|
||||
没有分数、星级、连胜奖励的规则就不新增成绩星星。可以用没有评分含义的完成章与对勾。章末用短页面总结“本章完成”,不展示推断的智力年龄。
|
||||
|
||||
## 14. 首页与继续游戏
|
||||
|
||||
首页承担较多品牌表达:标题 Logo 约占首屏可用高度的 12%–16%,一组真实拼块小构图占 20%–26%,继续游戏是第一主操作。比例是构图建议,不作为适配硬编码。
|
||||
|
||||
信息顺序:品牌 → “每块一次,数字对步数”的简短玩法句 → 继续第 X 关 → 选关 → 设置。初次启动主按钮为“开始”;已有进度为“继续第 X 关”。
|
||||
|
||||
不新增购买皮肤、每日签到、排行、宝箱等未定义系统。入口如果只提供数字游戏,不展示空的玩法选择;未来共壳接入六边原型时才添加真实玩法卡。
|
||||
|
||||
首页背景可以比对局多 1–2 个装饰,但不循环飘动。开始按钮高56–60、圆角18、紫面白字,触点至少48。
|
||||
|
||||
## 15. 选关与章节页面
|
||||
|
||||
200 关按现有 `playOrder`、20 章与解锁状态组织,不按关号字符串重新排序。每章使用一张奶油题卡,关卡入口为简洁积木按钮;维持现有分页能力,不把 200 个按钮同时塞一页。
|
||||
|
||||
| 关卡状态 | 表现 | 不允许 |
|
||||
|---|---|---|
|
||||
| 可玩未完成 | 奶油底、深数字 | 用很浅灰字隐藏入口 |
|
||||
| 当前/继续 | 紫色或淡紫选中面、明确标记 | 与通关对勾长得一样 |
|
||||
| 已完成 | 小对勾+普通可点状态 | 以三颗星虚构评分 |
|
||||
| 真实锁定 | 锁标+明确不可点 | 对当前全开放试玩版强加锁定 |
|
||||
|
||||
默认 4 列,按钮高48–52,间距8。每章题量以数据为准,不假设永远10关。题卡可附一个代表机制的小图形,不展示每个复杂关的缩小棋盘。
|
||||
|
||||
关卡名太长时两行,并保留“第 X 关”可读;分页与继续按钮在固定底部区域,不压在最后一排题目上。反馈与导出记录入口属于现有微信工具能力,可在设置/更多中延续,避免抢首屏。
|
||||
|
||||
## 16. 正式对局 UI
|
||||
|
||||
顶栏左侧选关,中央关号,右侧音效或设置;微信胶囊占用区必须避让。规则入口置于第二信息行或设置中可见位置,不能只靠第一次弹出。
|
||||
|
||||
状态默认“已走 3 步 · 还剩 8 块”,两者按逻辑块计算;2×2走一次只增加1步。无需同时显示多个重复进度条。若计数空间不足,只保留“剩余 8 块”,避免挤小关键文本。
|
||||
|
||||
反馈栏高度固定,普通“每块只走一次”和错误原因在同一位置切换。允许最多两行;不足时短文案优先,完整说明交给规则页,不能把“需第8步”截掉。
|
||||
|
||||
屏幕绝大多数交互发生在棋盘,不要求手指每走一步都去点按钮。布局变化只在关卡加载或屏幕尺寸变化时发生;显示提示、错误、通关按钮都不重新计算棋盘位置。
|
||||
|
||||
## 17. 教学与规则面板
|
||||
|
||||
当前微信包已有真实教学关卡与逐步演示,继续复用正式绘制与判定,演示使用独立状态,不改玩家路径、存档、提示次数或音效偏好。
|
||||
|
||||
教学页结构:机制名称 → 一句具体动作 → 小型真实盘面演示 → 最多两句解释 → 重播/上一段/下一段 → 返回游戏。复杂组合按实际出现的机制拆页,不提前画出正式关的答案。
|
||||
|
||||
| 机制 | 首句文案建议 | 演示必须表达 |
|
||||
|---|---|---|
|
||||
| 基础 | 从 1 开始,走过每一块 | 边挨边,不能斜走 |
|
||||
| 大块 | 这是一整块,只算一步 | 整块一次变色,数字只加1 |
|
||||
| 异形 | 拐着的这一整块也只算一步 | 先强调完整轮廓 |
|
||||
| 通道 | 从连着的两条边进出 | 可反向,错误拐出后退回 |
|
||||
| 双色 | 每一步换一种颜色 | 蓝→橙→蓝,形状标记同时可见 |
|
||||
| 奇偶 | 这块要在偶数步经过 | 2、4、6与当前步号的关系 |
|
||||
| 节奏门 | 每走一块,门就切换一次 | 起点也切换,通过后保持开 |
|
||||
| 六边 | 六条边都可能通往邻块 | 共边与碰角的区别 |
|
||||
|
||||
当前约1秒连贯演示可以保留为默认重播速度,但增加“逐段读懂”的排版空间;若需要更慢演示,作为教学设置提案,不在本报告中声称已实现。关闭面板回到原关原位置。
|
||||
|
||||
规则页面板内文字16/24,正文宽度不超过可用宽度;窄屏用分页或自然滚动。遮罩下棋盘完全停止接收绘制输入。返回游戏按钮始终可达,不藏在超长正文之后。
|
||||
|
||||
## 18. 按钮、图标、标签与面板组件
|
||||
|
||||
### 18.1 按钮状态
|
||||
|
||||
| 状态 | 主按钮 | 次按钮 | 几何变化 |
|
||||
|---|---|---|---|
|
||||
| 常态 | 紫面白字、深紫下沿 | 奶油面深墨字、暖灰下沿 | 稳定 |
|
||||
| 按下 | 深紫面、投影减弱 | 下压、轻加深 | y+1,最多缩0.98 |
|
||||
| 松开 | 120ms回原位 | 同左 | 不反弹超过原尺寸 |
|
||||
| 禁用 | 中性面与文字、无活跃下沿 | 同左 | 不接受动作 |
|
||||
| 键盘焦点 | 外置2px深紫线、间隔2 | 同左 | 不移动布局 |
|
||||
|
||||
图标建议重开为珊瑚色回转符、撤销为深墨回退、指路为紫/暖黄组合灯泡;固定按钮文字不随图标颜色改变。图标24,笔画2–2.5,端点圆润,但避免全部做厚塑料玩具占满按钮。
|
||||
|
||||
### 18.2 标签与数字胶囊
|
||||
|
||||
标签高28–32,左右内边距12,圆角12。珊瑚标签配深棕字。不要为了复制概念图使用浅珊瑚白字;若需要白字珊瑚标签,必须另选更深背景并重新审计。
|
||||
|
||||
计数标签只突出变化的数字,不每次弹跳整个胶囊。计数使用稳定宽度或经过测试的等宽数字特性,不能未经核对声称 Nunito 的所有数字必定等宽。
|
||||
|
||||
### 18.3 设置与对话框
|
||||
|
||||
设置建议包含音效、振动、减少动态、规则帮助,以及现有数据操作。新开关必须实际接线后才显示;不把未实现设置做成可点击假按钮。
|
||||
|
||||
确认删除进度属于已有数据管理时使用清楚的二次确认;普通撤销和重开不复用删除进度的红色样式。弹窗遮罩建议主墨32%不透明度,实际可读性按合成结果检查。
|
||||
|
||||
## 19. 文案、国际化与无障碍
|
||||
|
||||
用“试着退回一块”“这块需要第8步”“门现在关闭”解释状态。避免“笨蛋”“智商不足”“天才年龄”等人格或能力评判。错误反馈提供修正方向,不威胁扣分。
|
||||
|
||||
英文、本地化后的长字串预留约30%宽度余量只是布局起点,仍要测真实译文。主按钮允许合理两行,但底部不能变成四行小字。中日韩文字分别使用对应地区字体,标点和数字基线需复核。
|
||||
|
||||
重要状态同时使用颜色、形状和文字;双色模式使用圆/三角,门使用开口,固定条件用内框,提示用线型,错误用叹号与原因。减少动态关闭缩放、抖动、循环光点和粒子,仍完整保留门状态与提示路径。
|
||||
|
||||
对比度内部采用普通文字4.5:1、关键非文字信息3:1作为目标,参考 W3C 文本与非文本对比度说明;本报告不是平台合规认证,静态色对通过也不代表整个游戏符合 WCAG。[文字对比度](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum)、[非文字对比度](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)
|
||||
|
||||
标准按钮触点目标48×48。盘面按真实密度适配,不能因相邻格过小就让它们的触点重叠并随机吞输入;密集关必须实机检查。Canvas 版若没有语义辅助层,不能宣称已有屏幕阅读器完整支持,后续另列实现工作。
|
||||
|
||||
## 20. 动效、特效、声音与触觉
|
||||
|
||||
### 20.1 四档基础节奏
|
||||
|
||||
快速120ms、基础200ms、较慢320ms、停留500ms。动效是操作结果,不是操作前提;规则与输入不等待动画完成。
|
||||
|
||||
| 事件 | 推荐时长 | 表现与幅度 | 输入要求 |
|
||||
|---|---:|---|---|
|
||||
| 按钮按下/松开 | 80 / 120ms | 下压1,缩至0.98后恢复 | 点击即时响应 |
|
||||
| 落格 | 140ms | 块面短压0.985,回弹最大1.015 | 几何与命中不缩放 |
|
||||
| 桥接 | 120ms | 只长桥厚度,跨缝方向始终覆盖 | 不出现临时断路 |
|
||||
| 步号出现 | 100–120ms | 轻淡入,无飞入旋转 | 关键固定线索始终可读 |
|
||||
| 条件命中 | 180–200ms | 边框短强调 | 不用整块闪白盖数字 |
|
||||
| 撤销 | 100ms | 被删数字与桥淡出 | 状态立即撤回 |
|
||||
| 冲突 | 200ms | 红边、叹号;可选≤1.5位移 | 不全屏抖动,不打断手势 |
|
||||
| 门切换 | 160ms | 门片收拢/展开 | 逻辑状态独立生效 |
|
||||
| 转墙 | 160ms | 绕真实轴心120° | 不以中间角度判碰撞 |
|
||||
| 进场 | 单块200ms | 小幅出现,间隔12ms,总延迟封顶120ms | 输入开启时几何已固定 |
|
||||
| 通关扫光 | 总计320ms | 沿整条路径压缩播放一次 | 不按36块各播320ms |
|
||||
| 通关停留 | 500ms | 显示完成反馈 | 微信包下一关仍由玩家点击 |
|
||||
|
||||
推荐缓出曲线 `cubic-bezier(0.22,1,0.36,1)`。柔和回弹只在极小幅度内,狭窄异形与墙门附近可取消形变只做色边变化。
|
||||
|
||||
### 20.2 特效边界
|
||||
|
||||
思考阶段无常驻发光、漂浮、呼吸缩放。扫光用低透明浅色边,不把紫面提亮到白字不足对比的颜色。原代码的 `visitedBright` 等旧亮面不直接沿用;新皮肤优先维持基础紫面,只增外围光。
|
||||
|
||||
首次机制出现可强调一遍。提示流动是用户主动请求后的局部动画,退后台停止;回前台按当前提示状态恢复,不一次播放积压帧。
|
||||
|
||||
粒子首版可不制作。若保留通关小纸片,建议8–12片、总时长600–900ms,出现在棋盘边缘外,不遮数字,不常驻,不把按钮延迟到粒子结束才出现。减少动态下关闭。
|
||||
|
||||
### 20.3 声音
|
||||
|
||||
延续当前包已有的半音阶落格声音身份,不因换皮重写成强制节奏游戏。音色方向为短木琴/柔和玩具键音,低频不过重,高频不刺耳;参考时长落格60–100ms、撤销50–80ms、冲突100–140ms、通关400–650ms。
|
||||
|
||||
快速连画限流与声部数需要实机调试,不能每帧创建新音频对象。建议同类短音最多3–4声部并发,旧短音按策略释放。关闭音效立即停止,退后台与音频中断按现有生命周期停止;静音不影响判定与提示。
|
||||
|
||||
规则上正确落下一步不等于已证明整条路径正确。音效可表达“已落格”“线索吻合”,不能每步都播绝对正确的奖励旋律误导玩家。
|
||||
|
||||
### 20.4 触觉
|
||||
|
||||
默认克制:普通每步可不震;错误或墙挡轻短反馈一次,通关一次较柔和反馈。振动使用平台支持能力与用户开关,后台不振动。具体强度按真机 API 能力设定,不用不存在的跨平台精确毫秒幅度承诺。
|
||||
|
||||
## 21. 资源清单与美术交付
|
||||
|
||||
### 21.1 首版资源范围
|
||||
|
||||
| 资源组 | 必需内容 | 推荐形式 |
|
||||
|---|---|---|
|
||||
| 品牌 | 原创中文标题,必要英文版本 | 矢量母版+透明位图导出 |
|
||||
| 背景 | 1套暖黄渐变/纯色、4种几何纹样 | 小型矢量或透明贴图 |
|
||||
| 舞台与面板 | 舞台、卡片、弹窗、按钮 | 程序圆角/九宫格 |
|
||||
| 块体材质 | 面、底沿、边、倒角 | 由实际轮廓程序生成 |
|
||||
| 功能图标 | 选关、设置、音效开关、重开、撤销、指路、规则、关闭、下一关、对勾 | 同一图标规范,建议24逻辑单位母版 |
|
||||
| 规则符号 | 奇偶、通道六种、圆/三角、墙、门状态、实验转轴 | 可读的矢量/程序几何 |
|
||||
| 字体 | 数字与UI实际使用字重/字集 | 字体文件+许可+版本记录 |
|
||||
| 反馈 | 短落格、撤销、冲突、线索命中、通关音 | 复用现有链路,按资源预算调整 |
|
||||
| 展示素材 | 真实对局、复杂关、规则与选关截图 | 基于实际运行,非概念图替代 |
|
||||
|
||||
不需要为200关分别制作背景和积木贴图。矩形、异形、六边共用材质语言,几何由关卡生成。首版不制作复杂角色、真实教室和多套天气。
|
||||
|
||||
### 21.2 导出与命名
|
||||
|
||||
命名建议 `bc_ui_button_primary`、`bc_icon_hint`、`bc_pattern_triangle`,颜色与逻辑状态通过令牌或明确定义的变体管理。可缩放组件标出九宫格安全区、内边距、透明边与原点。
|
||||
|
||||
位图提供至少对应目标显示密度的清晰版本,但不盲目全量4倍导出。带透明的边缘检查白边;图集间隔与挤出按引擎采样策略配置。字体与贴图内存预算以打包和真机数据决定。
|
||||
|
||||
概念图只作为方向参考,不能整张切成按钮和棋盘。按钮、文字、数字、规则符号必须可独立排版和本地化。
|
||||
|
||||
## 22. 渲染、性能与程序接入
|
||||
|
||||
### 22.1 图层顺序
|
||||
|
||||
背景 → 装饰 → 舞台 → 块体投影与底沿 → 块面与桥 → 类别底与规则符号 → 数字与条件框 → 局部状态/提示 → 保证清楚的墙门及关键标记 → UI → 弹窗。
|
||||
|
||||
该顺序不是简单全局重画一次的保证。提示经过数字或通道时使用遮罩/裁切避让;关键符号可在最终语义层补绘,不能只声明“数字在上”就忽视独立提示 Canvas 的实际覆盖。
|
||||
|
||||
对于双色块,规则类别是底层身份;已走是叠加状态,错误是局部覆盖,提示是独立效果。不同语义不能压缩成一个 `fillColor` 变量。
|
||||
|
||||
### 22.2 实现建议
|
||||
|
||||
保留当前规则层,抽出视觉令牌与纯表现状态:普通/类别面、访问、冲突、焦点、条件、提示、门状态。二维 Canvas 或 Cocos Graphics 可复用几何轮廓;九宫格只用于UI,不能拉伸一张L形贴图模拟任意拼块。
|
||||
|
||||
静态背景、舞台与不变轮廓可缓存;不在每帧对36个块重复执行大模糊。半透明提示 Canvas 维持独立,不清空底层玩家棋盘。设备像素比只用于清晰度,不参与逻辑坐标和命中计算。
|
||||
|
||||
字体加载后重新计算文字边界;启动前等待关键字形准备完成或提供明确fallback,避免解题中途数字换宽。前后台切换取消旧动画计时,减少动态模式停掉无必要的持续帧循环。
|
||||
|
||||
性能目标建议60fps,低性能设备稳定30fps作为降级候选;这不是本次已完成的实测。单次更新需记录输入到反馈延迟、绘制耗时、提示层重绘与音频并发。先关阴影和粒子,不删墙、门、条件框、类别标记。
|
||||
|
||||
### 22.3 接入边界
|
||||
|
||||
旧原型若改配色,需要同步各自 `src/theme.ts`、Web样式与现有设计文档,并通过主题一致性检查;本报告不直接覆盖旧令牌。微信包另有宿主UI、上游快照、提示层与构建,不能只改旧Web CSS就声称小游戏换皮完成。
|
||||
|
||||
上游快照及其哈希有来源追踪要求,后续应明确在适配层引入主题,或以新的版本快照重建并更新manifest。不能暗改来源文件而保留旧校验记录。
|
||||
|
||||
## 23. 商店、小游戏与商业展示
|
||||
|
||||
首版宣传视觉沿用暖黄+奶油+紫三色识别。图标优先“带数字的两三块积木与一笔桥接”,小尺寸下清楚;首页Logo不是直接压缩成图标。
|
||||
|
||||
展示顺序建议:基础玩法一张 → 大块一步一张 → 通道/双色的真实复杂关一张 → 辅助与撤销一张。每图只讲一个重点,玩家应先看见棋盘。
|
||||
|
||||
选定效果图是方向资产,不应作为已上线实际画面或实机截图。正式截图来自对应版本运行;不把通关奖励、分数或联机能力画进没有实现的产品。
|
||||
|
||||
当前指路未接广告,报告不新增商业化流程。如后续决定激励广告,提示的免费/广告属性必须在点击前明确,观看成功后保持玩家路线,失败也不清空;这是后续产品工作,不作为本次必做功能。
|
||||
|
||||
评价“商业更好”需要对照测试,不能从任天堂参考或用户选图直接推导收入提升。可对比旧柔和积木与新风格的素材点击、进入后理解、完成和继续意愿,玩法和题目保持一致。
|
||||
|
||||
## 24. 样张与验收矩阵
|
||||
|
||||
### 24.1 必须制作的黄金样张
|
||||
|
||||
| 样张 | 核心检查 |
|
||||
|---|---|
|
||||
| 1-8标准大块对照 | 一整块、清晰锚点、紫色路径与奶油边界 |
|
||||
| L/T/S 异形+空洞 | 凹口、内部无缝、真邻接 |
|
||||
| 两位数与奇偶 | 10/11/28/36、条件副标、错误要求不丢 |
|
||||
| 通道+数字 | 中心槽线与角标互不覆盖 |
|
||||
| 双色已走+提示 | 蓝橙身份、圆三角、玩家填色始终保留 |
|
||||
| 节奏门开/关/已过/撤销 | 形态明确、无背景自动切换 |
|
||||
| 六边墙+提示 | 六邻接、共边墙、桥不穿墙 |
|
||||
| 密集 r2-163/r2-187 | 7列36块与机制组合,320宽真机 |
|
||||
| 实验转墙 | 单独原型,轴心、120°、回退恢复 |
|
||||
| 通关与下一关 | 微信包棋盘不移、手动前进 |
|
||||
|
||||
### 24.2 验收标准
|
||||
|
||||
- 读盘:所有轮廓、空洞、实际共边清楚;大格和异形没有内部格线。
|
||||
- 数字:不被扫光、桥、指路点、门标签遮挡;固定条件和实际步号可区分。
|
||||
- 状态:错误、当前位置、提示、条件框使用不同形态;蓝橙关全程保留类别。
|
||||
- 操作:快速滑动、慢滑、松手续画、逐格退回、撤销、重开与提示行为不变。
|
||||
- 提示:开启、撤销再画、偏离、段末完成前后,玩家已画块的基础填色像素不因提示改变。
|
||||
- 布局:320×568、375×667、390×844、414×896均检查;弹窗与通关按钮不遮盘面。
|
||||
- 生命周期:退后台、音频中断、返回前台、方向/尺寸变化,状态与位置正确。
|
||||
- 文字:中文、英文长文、系统字号变化与fallback检查,无省略关键规则。
|
||||
- 表现:减少动态与静音下所有玩法和提示仍可理解。
|
||||
- 内容:采用同一发布快照,仍跑全关卡唯一解、原有输入和回归检查;换皮不能替代规则校验。
|
||||
|
||||
新风格试玩目标建议:首次看到2×2后,多数新玩家能解释“一整块只一步”;玩家能在不依赖颜色时区分类别、墙和门。样本量、成功标准与通过比例在测试计划中预先确定,本报告不把这些目标写成已有结论。
|
||||
|
||||
## 25. 分阶段制作与决策
|
||||
|
||||
| 阶段 | 交付物 | 进入下一阶段的条件 |
|
||||
|---|---|---|
|
||||
| A 规范冻结 | 本报告、令牌、选定参考、分支映射 | 颜色、尺寸、按钮能力一致 |
|
||||
| B 黄金样张 | 基础、大块、通道、双色、门、密集六类 | 复杂关也清楚,非只看首页 |
|
||||
| C 可交互单关 | 渲染、输入、声音、提示、撤销 | 连画与状态不被动效阻塞 |
|
||||
| D 全页面 | 首页、选关、对局、规则、设置、完成 | 窄屏与真实安全区通过 |
|
||||
| E 全内容与平台 | 200关抽样/全量自动回归、真机验证 | 原有测试通过,显示和性能达标 |
|
||||
| F 展示与验证 | 真实截图、素材对照、试玩记录 | 结论基于数据而不是概念偏好 |
|
||||
|
||||
优先级:P0为逻辑轮廓、条件、状态、输入与提示;P1为字阶、材质、页面、教学;P2为首页Logo精修、少量通关粒子、附加主题。先完成复杂关可读性,再增加装饰。
|
||||
|
||||
本报告没有虚构工期、成本和转化提升。实际排期需结合现有引擎、字体包、两套原型与微信宿主的接入量评估。
|
||||
|
||||
## 26. 已验证事项、证据与剩余工作
|
||||
|
||||
### 26.1 本次验证边界
|
||||
|
||||
已做:读取当前关卡与代码;统计200关机制与几何;记录关键源文件SHA-256;以不透明sRGB计算指定色对;对照选定概念图制定参数。
|
||||
|
||||
未做:应用新主题到运行时、导入字体、生成生产切图、跑换皮后的整包回归、测真机帧率、验证商店表现或宣称完整无障碍合规。原包既有验证记录是参考证据,不等于新主题已经通过测试。
|
||||
|
||||
### 26.2 Evidence → Finding → Path
|
||||
|
||||
| 证据 | 来源与复核 | 观察 | 对应设计结论 |
|
||||
|---|---|---|---|
|
||||
| E-01 | `src/upstream/levels.json`;运行配套审计脚本 | 200关、20章、3–36块;多机制组合 | F-01:必须适配密集关及组合符号,不能照11块样图直接缩放 |
|
||||
| E-02 | `wechat-campaign-200-r2/src/main.mjs` | 重开/撤销/指路;通关手动下一关 | F-02:按钮按版本映射,完成时原位替换 |
|
||||
| E-03 | `wechat-campaign-200-r2/src/hints.mjs` | 分段提示、独立透明层、已走区域擦除 | F-03:提示不覆盖玩家填色,不改为短时单步闪烁 |
|
||||
| E-04 | `src/upstream/rules.mjs` | 可反向通道、双色、奇偶、门按路径步号 | F-04:规则使用专用符号,动画不替代判定 |
|
||||
| E-05 | `modes/01-number-stroke/src/board.ts` 与 `game.ts` | 矩形中心锚点、异形安全锚点、补数与截断 | F-05:概念图锚点和按钮不直接冒充当前行为 |
|
||||
| E-06 | `modes/03-hex-walls/src/board.ts` | 转墙按离散步号定位 | F-06:转墙与节奏门分离适配 |
|
||||
| E-07 | `contrast-audit.json` | 规定色对计算与诊断 | F-07:珊瑚配深字,奶油块依靠有效边界 |
|
||||
| E-08 | 选定概念图与用户选择 | 暖黄+奶油+紫色方向已选定 | F-08:视觉以此冻结,生产布局仍按玩法修正 |
|
||||
|
||||
复核路径 P-01:读取 E-01 与 E-04确定规则 → 按 F-01/F-04制作基础和组合样张 → 按 E-02/E-03/F-02/F-03接入操作与提示 → 按 E-07验证色对 → 按第24节完成新主题回归与真机测试。
|
||||
|
||||
文件内容哈希与统计见配套 `project-audit.json`。该表用于设计追溯,不是安全审计报告。若源文件以后更新,重新运行脚本并审核差异,不能把旧记录作为新版本证明。
|
||||
|
||||
### 26.3 参考与来源
|
||||
|
||||
- 项目:[当前200关微信包说明](../wechat-campaign-200-r2/README.md)、[早期关卡设计规范](关卡设计规范.md)、[既有UI规范](design.md)。旧文档的关数、提示或通关行为需按各自版本解释。
|
||||
- 任天堂:[官方页面与玩法画面](https://www.nintendo.com/hk/switch/azlsa/)、[开发者访谈](https://www.nintendo.com/en-gb/News/2021/December/Ask-the-Developer-Vol-3-Big-Brain-Academy-Brain-vs-Brain-2087189.html)。仅用于参考设计语言与研究来源。
|
||||
- 字体:[Nunito](https://github.com/googlefonts/nunito/blob/main/OFL.txt)、[Noto CJK](https://github.com/notofonts/noto-cjk/blob/main/Sans/LICENSE)。
|
||||
- 可读性:[W3C文字对比](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum)、[W3C非文字对比](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)。
|
||||
|
||||
## 附录:一页执行参数
|
||||
|
||||
| 项目 | 首版默认 |
|
||||
|---|---|
|
||||
| 气质 | 明亮、整洁、可触摸的逻辑教具 |
|
||||
| 主背景/舞台 | `#FFE69A` / `#FFF6DF` |
|
||||
| 未走面/有效边界 | `#FFF8EA` / `#8A7660` |
|
||||
| 标准路径/路径字 | `#6552D9` / `#FFFFFF` |
|
||||
| 主墨/辅助墨 | `#43382F` / `#71604F` |
|
||||
| 珊瑚/标签字 | `#F57950` / `#4F2418` |
|
||||
| 数字/中文 | Nunito 800 / Noto Sans SC 500–700 |
|
||||
| 正文/按钮/辅助 | 16/24;16/22;13/18 |
|
||||
| 对局大标题 | 不重复首页Logo,优先关号与章节 |
|
||||
| 按钮/舞台圆角 | 18 / 28 |
|
||||
| 棋块外角 | `clamp(6,0.18u,12)`,大块不加倍 |
|
||||
| 厚度/投影 | 2–4;0/3/5/10% |
|
||||
| 桥宽 | 0.42u,严格落在共边 |
|
||||
| 特效 | 局部、短促;思考时无常驻动画 |
|
||||
| 微信三按钮 | 重开/撤销/指路 |
|
||||
| 微信指路 | 独立分段叠层,保留已画填色,到段末消失 |
|
||||
| 微信通关 | 棋盘固定,原操作区显示下一关 |
|
||||
| 特殊颜色 | 双色面永久保留,错误与提示靠形状补充 |
|
||||
| 制作首要检查 | 7列36块、通道数字、双色提示、门开闭、六边共边 |
|
||||
@@ -0,0 +1,711 @@
|
||||
# 数字一笔 · 柔和积木风格完整美术与 UI 设计报告
|
||||
|
||||
**版本:1.0 设计提案|日期:2026-10-21|适用:小游戏、Google Play、App Store 手机竖屏**
|
||||
|
||||
本报告将用户选定的“柔和积木”方向转成可执行规范。它是**新视觉方案**,不是对现有程序的实现描述;本次不改规则、关卡、正式 UI 或打包文件。报告内“必须”表示采用此方案时的设计验收要求,不表示引擎已强制执行。
|
||||
|
||||
配套文件:
|
||||
|
||||
- [可离线查看的视觉样张](design/soft-blocks/style-guide.html):色板、字阶、状态、页面、动效演示。
|
||||
- [结构化设计令牌](design/soft-blocks/tokens.json):颜色、尺寸、字体、材质和动效参数。
|
||||
- [对比度计算结果](design/soft-blocks/contrast-audit.json)与[复核脚本](design/soft-blocks/check-contrast.mjs)。
|
||||
- 现有规范:[design.md](design.md)。本报告与之冲突的项目列在第 22 节;采用后应通过一次完整视觉版本更新同步,而不是部分覆盖。
|
||||
|
||||
## 1. 风格定义与体验目标
|
||||
|
||||
### 1.1 一句话定义
|
||||
|
||||
**像一套摆在温暖桌面上的精密哑光拼块:形体柔和、数字清楚,手指划过时路径连成完整的一体。**
|
||||
|
||||
关键词:温和、清楚、可触摸、克制、可靠。玩家首先理解“能走哪、走到第几步、哪里要改”,然后感受到材质和完成反馈。
|
||||
|
||||
这是一套二维俯视视觉系统。轻厚度、圆角、颜色和短反馈共同构成实体感,不需要透视相机、真实灯光或 3D 资产。
|
||||
|
||||
### 1.2 视觉优先级
|
||||
|
||||
1. **规则信息:** 格形与共边、数字/奇偶、墙、转轴。
|
||||
2. **操作状态:** 当前末端、已走路径、冲突、提示。
|
||||
3. **任务与导航:** 关号、剩余格数、重开、补数字、指路。
|
||||
4. **材质与品牌:** 暖白、蓝紫、边缘厚度、短音。
|
||||
5. **装饰:** 少量章节点缀,仅在不影响前四项时出现。
|
||||
|
||||
不能用提高装饰层级弥补玩法层级不清。商业展示中的“有质感”也不等于游戏内每个元素都加阴影。
|
||||
|
||||
### 1.3 应有与不应有
|
||||
|
||||
| 应有 | 不应有 | 原因 |
|
||||
|---|---|---|
|
||||
| 哑光块面与清楚外轮廓 | 强镜面、高光塑料球 | 反光会盖住数字、分散注意 |
|
||||
| 稳定 2–3 单位厚度 | 大幅挤压、果冻拉伸 | 异形凹口与邻接不能随表现改变 |
|
||||
| 正俯视、统一光向 | 等距视角、倾斜棋盘 | 手指命中和边墙需要准确对位 |
|
||||
| 单主色连贯路径 | 每格随机糖果色 | 当前不是颜色配对玩法 |
|
||||
| 局部确认、清晰收束 | 常驻粒子、整屏闪白、长撒花 | 思考阶段需要安静 |
|
||||
| 简短友好文案 | 儿童口吻、失败羞辱、虚假 IQ 判断 | 不误导成人休闲逻辑产品定位 |
|
||||
|
||||
### 1.4 适用范围
|
||||
|
||||
同一套材质和 UI 语言服务数字一笔、六边一笔。矩形大格/奇偶属于数字玩法;墙/转墙属于六边玩法,美术统一不意味着规则合并。
|
||||
|
||||
本版主皮肤为浅色。暗色、木质、节日主题不纳入首版制作量;后续主题仍需保留语义、可读性与几何轮廓,不能直接做全局颜色反转。
|
||||
|
||||
## 2. 画面构成与品牌一致性
|
||||
|
||||
### 2.1 颜色面积关系
|
||||
|
||||
背景与留白占视觉大部;未走块形成中性结构;已走路径是最醒目的面积色。不要强制按全屏固定百分比配色:关卡规模与解题进度天然改变占比。控制的是**注意力次序**,不是每张截图相同的色块面积。
|
||||
|
||||
章包可以使用小图形或几何排布区别,不能在每关切换不同路径色。红与琥珀不承担装饰性品牌色。
|
||||
|
||||
### 2.2 几何家族
|
||||
|
||||
矩形、圆角矩形、圆角六边形、圆点和圆头线段是基础形。图标、按钮和卡片采用相近的圆角节奏,但不要全部变成圆球。
|
||||
|
||||
矩形大格的圆角按**单位格尺度**计算,不随自身 2×2、3×3 面积成倍放大。异形拼块只在整体外轮廓处理边缘,内部拼接线必须消失。
|
||||
|
||||
### 2.3 排版气质
|
||||
|
||||
大标题有分量,正文短而平静;数字比装饰更有存在感。UI 不是杂志封面,不使用极细字体、大段全大写或过度字距。中文不用故意拉宽字间距营造“高级感”。
|
||||
|
||||
## 3. 完整配色系统
|
||||
|
||||
颜色为不透明 sRGB 十六进制;透明蒙层必须另算合成后的效果。下表是设计令牌的主要用途,JSON 为机器可读镜像。
|
||||
|
||||
### 3.1 背景与表面
|
||||
|
||||
| 令牌 | 色值 | 用途 | 使用约束 |
|
||||
|---|---|---|---|
|
||||
| `background` | `#F7F5F1` | 游戏背景、主页面背景 | 统一暖白,不叠纸纹/噪声 |
|
||||
| `surface` | `#FFFEFC` | 卡片、对话框、设置面板 | 与背景保持轻分层 |
|
||||
| `surfaceRaised` | `#FFFFFF` | 必要的高层面 | 不用于满屏纯白高光 |
|
||||
| `surfaceMuted` | `#EEEBE6` | 次按钮、分组、胶囊 | 文字用主墨或辅助墨 |
|
||||
| `divider` | `#D5D0C9` | 非关键分隔线 | 不能单独承担格边/重要状态 |
|
||||
| `cellFace` | `#E5E3E1` | 未走拼块正面 | 配合有足够对比的外轮廓 |
|
||||
| `cellDepth` | `#C5C0CB` | 拼块下沿与厚度 | 只表现材质,不独自承担边界信息 |
|
||||
| `cellBoundary` | `#817B88` | 未走块有效外边界 | 1.25 单位起,实机必要时加粗 |
|
||||
|
||||
### 3.2 字体颜色与主色
|
||||
|
||||
| 令牌 | 色值 | 用途 |
|
||||
|---|---|---|
|
||||
| `ink` | `#292638` | 线索、标题、关键正文 |
|
||||
| `inkSecondary` | `#66616F` | 说明、图标、剩余格数 |
|
||||
| `inkTertiary` | `#716A78` | 非主要信息;仅放在通过对比校验的底上 |
|
||||
| `primary` | `#6558F5` | 已走路径、当前进度、主要按钮 |
|
||||
| `primaryPressed` | `#5245D6` | 主按钮按压面、深色选中标签文字 |
|
||||
| `primaryDepth` | `#4B3AC8` | 已走块下沿 |
|
||||
| `primarySoft` | `#EDEAFF` | 当前关/已选设置的淡紫背景;已走线索环 |
|
||||
| `selectedInk` | `#5245D6` | 淡紫面上的小字号文字,避免直接用较浅主紫 |
|
||||
| `primaryEdgeLight` | `#A49BFF` | 装饰性局部边缘光,不覆盖数字区域 |
|
||||
| `onPrimary` | `#FFFFFF` | 紫面或错误面上的数字、主按钮文字 |
|
||||
|
||||
### 3.3 状态与墙
|
||||
|
||||
| 令牌 | 色值 | 用途 | 非颜色提示 |
|
||||
|---|---|---|---|
|
||||
| `danger` | `#C74350` | 当前冲突格正面、危险操作主按钮 | 原条件角标、短晃动或静态冲突描边 |
|
||||
| `dangerDepth` | `#9E2F3A` | 错误块下沿 | 与正面同一整体 |
|
||||
| `dangerSoft` | `#FBEAED` | 解释性错误提示底 | 与 `dangerInk` 配套 |
|
||||
| `dangerInk` | `#8B2632` | 淡红底错误文字 | 写清发生什么,而非仅“错误” |
|
||||
| `hint` | `#966000` | 外圈提示、目标标记 | 外圈/角括号和辅助说明 |
|
||||
| `hintSoft` | `#FFF0D2` | 提示说明背景 | 不铺满棋盘 |
|
||||
| `hintInk` | `#664400` | 提示文字 | 与底色配对 |
|
||||
| `wallInk` | `#3B3547` | 共边墙身、转轴轮廓 | 斜纹/实体杆身 |
|
||||
| `wallStripe` | `#D7B35B` | 墙内条纹 | 固定在杆身内部,不是提示环 |
|
||||
| `focus` | `#6558F5` | 键盘/焦点外圈 | 外圈偏移与明确焦点语义 |
|
||||
| `overlay` | `#292638` | 遮罩源色 | 弹窗背景遮罩建议 32% 起,需合成验收 |
|
||||
|
||||
墙和提示都含暖色,但形状不同:**墙是常驻共边杆+内部斜纹,提示是目标外圈+说明**。不能只依赖黄/橙色相区别。
|
||||
|
||||
### 3.4 颜色纪律
|
||||
|
||||
- 紫色只在路径和可操作的产品强调中使用。无第二条竞争性常驻彩色路径。
|
||||
- 红色不用于“未解锁”、高难度装饰、普通未完成进度。
|
||||
- 琥珀不表示可购买商品;奖励和促销不占用谜题提示语义。
|
||||
- “已通关”不再引入大面积绿色;用勾、文字、淡紫背景表达。
|
||||
- 禁用态不对整个组件统一降低至 40% 透明;分别处理底、字、图标,让解锁条件仍可读。
|
||||
- 高对比辅助模式加粗关键边、强化标记,不修改题目难度或邻接。
|
||||
|
||||
## 4. 对比度与柔和材质的平衡
|
||||
|
||||
文字以 **4.5:1** 为本方案的默认最低目标,关键图形/状态以 **3:1** 为目标。即使某些大字可适用较低标准,本项目仍优先让盘面数字达到 4.5。该目标参考 WCAG,不等于声明整个 Canvas 游戏已满足全部可访问性要求。[文字参考](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum)、[非文字参考](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)
|
||||
|
||||
本次用相对亮度公式计算,判断阈值时使用未四舍五入值,表中仅作显示:
|
||||
|
||||
| 前景 / 背景 | 比值约 | 设计结论 |
|
||||
|---|---:|---|
|
||||
| 主墨 / 暖白 | 13.50:1 | 标题与关键说明充分清楚 |
|
||||
| 辅助墨 / 暖白 | 5.50:1 | 可用于常规辅助文字 |
|
||||
| 三级墨 / 暖白 | 4.78:1 | 可用,不再任意减透明度 |
|
||||
| 线索墨 / 未走面 | 11.48:1 | 数字清楚 |
|
||||
| 白字 / 主紫 | 4.92:1 | 路径数字与按钮可用 |
|
||||
| 白字 / 按压紫 | 6.60:1 | 按压不损失可读性 |
|
||||
| 白字 / 新错误红 | 4.83:1 | 错误仍能读到步号 |
|
||||
| 深选中文字 / 淡紫 | 5.60:1 | 用于当前关与开关标签 |
|
||||
| 格边 / 未走面 | 3.20:1 | 格边承担结构识别 |
|
||||
| 格边 / 暖白 | 3.77:1 | 轮廓在背景上可辨 |
|
||||
| 提示环 / 未走面 | 4.13:1 | 不依赖发光或淡色提示 |
|
||||
| 深墙 / 墙条纹 | 5.87:1 | 条纹与墙身清楚 |
|
||||
| 未走面 / 暖白 | 1.18:1 | **不能只靠填充/阴影作为格边** |
|
||||
|
||||
复算还发现,现有 `#E5484D` 上白字约 3.91:1、现有过关亮紫 `#7D72FF` 上白字约 3.67:1;不能沿用旧表数值宣称普通小字达到 4.5。本方案调整错误红,并让过关面仍保持主紫,亮色仅用于不遮数字的边缘/光带。
|
||||
|
||||
这次静态检查共验证 19 组必要颜色配对,均达各自目标;另含 3 组诊断。没有验证全部透明叠层、真实字体抗锯齿、屏幕阳光可读性和动画帧,正式资产仍需实机检查。
|
||||
|
||||
## 5. 字体方案与字形要求
|
||||
|
||||
### 5.1 推荐组合
|
||||
|
||||
| 内容 | 主选 | 字重 | 原因 |
|
||||
|---|---|---|---|
|
||||
| 盘面阿拉伯数字 | **Nunito** | 800 | 圆润、饱满,与积木形体统一 |
|
||||
| 英文标题/品牌短字 | Nunito | 700/800 | 与数字建立同族感 |
|
||||
| 简体中文 UI | **Noto Sans SC / 思源黑体系对应版本** | 500/600/700 | 笔画稳定,小字号易读 |
|
||||
| 长说明/帮助 | Noto Sans SC,拉丁同用清楚的无衬线 | 400/500 | 长段落不过度加粗 |
|
||||
| 临时原型/字体加载前 | 系统字体栈 | 实际可用字重 | 保证启动和离线可用,不假定所有平台字形相同 |
|
||||
|
||||
中文正文选清楚无衬线,圆润感主要交给数字、圆角和材质;不强求所有汉字都做成幼态圆体。Noto Sans SC 与 Source Han Sans 是相关但不同发行包,正式工程必须选定一个具体来源和文件,不以名称互相替代。
|
||||
|
||||
Nunito 和 Noto CJK 对应官方仓库提供开放字体许可;交付字体时保留各自实际版本的许可证和来源记录。本轮只核对来源,没有下载或把字体打进游戏。[Nunito](https://github.com/google/fonts/blob/main/ofl/nunito/OFL.txt)、[Noto CJK](https://github.com/notofonts/noto-cjk/blob/main/Sans/LICENSE)
|
||||
|
||||
### 5.2 回退与跨平台
|
||||
|
||||
建议以分角色字体来实现:盘面数字先尝试已打包 `Nunito`,中文先尝试已打包 `Noto Sans SC`,再回退系统。原型可继续采用 `ui-rounded, system-ui, -apple-system, "Segoe UI", sans-serif` 作为后备,但必须检查中文实际回退。
|
||||
|
||||
- 不把苹果系统圆体当作 Android/微信必然存在的字体。
|
||||
- Web 的 CSS `font-variant-numeric` 不会自动修复 Canvas/Cocos 文本布局;必须检查实际渲染器。
|
||||
- 字体未就绪时使用稳定回退,字体替换后重测文字和重新绘制;不让按钮宽度和数字锚点跳动。
|
||||
- 正式数字字形需检查 `1/7`、`3/8`、`6/9`、`0/8`、`11/17`、`18/19`、`20/21`、`100`,不能仅看单个 `8`。
|
||||
- 检查数字等宽能力。若渲染路径不支持相关特性,则使用固定推进宽度与居中测量;不要对单个数字随意横向拉伸。
|
||||
- 本版推荐 1–2 个必要中文字重与一个数字字重;复杂多语种需求再扩大字库。不为仅有几页 UI 打包重复完整 CJK 字族。
|
||||
|
||||
### 5.3 数字的排版规则
|
||||
|
||||
盘面数字始终水平,不随转墙、块按压或过关光带旋转。整数字串一起居中,不逐位居中。两位数只调字阶,不压窄单个字符。
|
||||
|
||||
静态数字可以按字面视觉中心轻微校正,但 `1` 不能每次跳到不同坐标。是否下沉 2%–4% 字号由实际字形决定;不要沿用旧字体偏移后不验新字体。
|
||||
|
||||
## 6. UI 字阶、行高与实际文字大小
|
||||
|
||||
### 6.1 计量约定
|
||||
|
||||
以下尺寸单位是**逻辑设计单位 D**,以 375×812 竖屏为参考。Web 1D 在基准下映射 1 CSS px;原生应按平台布局坐标映射,Android 字体使用 sp,iOS 使用 pt;Cocos 以统一设计坐标缩放。它们不是物理像素的等号关系。
|
||||
|
||||
750×1624 可作为 2 倍设计图,图中的 32px 对应基准 16D,而不是游戏文字要写 32。屏幕 DPR 不改变正文应有的视觉大小。
|
||||
|
||||
### 6.2 常规文字
|
||||
|
||||
| 级别 | 字号 D | 行高 D | 字重 | 适用 |
|
||||
|---|---:|---:|---:|---|
|
||||
| 页面展示标题 | 30 | 38 | 800 拉丁 / 700 中文 | 首页或选关主标题,一屏不超过一个 |
|
||||
| 页面标题 | 24 | 32 | 700 | 关卡页标题、结果标题、弹窗标题 |
|
||||
| 分组标题 | 18 | 26 | 700 | 包名、设置分组、帮助小节 |
|
||||
| 主正文 | 16 | 24 | 500 | 规则教学、主要说明 |
|
||||
| 按钮 | 16 | 24 | 700 | 继续、下一关、底栏动作 |
|
||||
| 信息标签 | 14 | 20 | 600 | 剩余格数、关卡包进度、开关标签 |
|
||||
| 辅助说明 | 13 | 18 | 500 | 简短手势说明、状态解释 |
|
||||
| 微型元信息 | 12 | 16 | 500 | 非必要元信息;不放核心规则 |
|
||||
| 选关数字 | 20 | 24 | 800 数字 | 关卡格 |
|
||||
| 顶栏关号 | 22 | 28 | 800 数字 | `1–8`,长关号需留宽 |
|
||||
|
||||
正文默认 16D,相比旧版 15D 更舒展。12D 是极少量元信息的底限,不可把操作说明缩到 10/11D 来塞版。关键规则要用 16D,不能藏在标题下的细灰字里。
|
||||
|
||||
中文字距默认 0;英语正文默认 0,短标签最多 `0.02em`;数字字距默认 0,计数与关号启用稳定数字宽度。不要普遍加粗到 800,粗细差是信息层级的一部分。
|
||||
|
||||
### 6.3 盘面字阶
|
||||
|
||||
`u` 表示逻辑单位网格的节距,不是大格总宽。六边版本取单格几何布局中可稳定容纳文字的参考宽度,不能把外接圆直径直接当成矩形单位格宽。
|
||||
|
||||
| 内容 | 字号建议 | 辅助限制 |
|
||||
|---|---|---|
|
||||
| 起点 `1` | `clamp(26, 0.60u, 46)` D | 数字更醒目,不让起点占满整块 |
|
||||
| 一位步号/线索 | `clamp(24, 0.52u, 44)` D | 800 字重 |
|
||||
| 两位步号/线索 | `clamp(22, 0.46u, 40)` D | 800 字重,整串测宽 |
|
||||
| 原始奇/偶 | `clamp(22, 0.44u, 36)` D | 中文 700,不当小装饰 |
|
||||
| 已走奇偶的保留标签 | 至少 12D,常用 12–14D | 主步号上移,二者不能相撞 |
|
||||
| 冲突条件角标 | 目标至少 12D | 放不下则调整盘面/说明布局,不能无限缩小 |
|
||||
|
||||
示例:u=60 时,起点 36、一位 31.2、两位 27.6、奇偶 26.4D。u=48 时,一位约 25、两位约 22D。大格数字仍按 u 算,不随 3×3 面积变成三倍字号。
|
||||
|
||||
三位数字要按实际发行内容单独验证;不能把参考表的“两位数”策略直接套用到三位或更多数字。盘面最多出现 N 步号,N 是拼块数量,不是关卡编号。
|
||||
|
||||
### 6.4 文本溢出
|
||||
|
||||
关号与剩余格数不省略;长包名可在非关键副标题中省略,选关入口要能看到完整名称。按钮优先简短翻译,其次增宽或两行,不先缩字体。中文与英文分别验版,德语等较长语言预留约三成文本宽度只是制作预留,不保证适用所有翻译。
|
||||
|
||||
系统大字开启时,菜单、帮助与设置允许换行/滚动;盘面不能只放大字而不检查格内碰撞。先压缩非关键顶栏/留白、保持关键数字,再提供适合屏幕的盘面尺寸。不能宣称 Canvas 会自动继承系统动态字阶。
|
||||
|
||||
## 7. 积木造型、光照与材质
|
||||
|
||||
### 7.1 基础尺寸
|
||||
|
||||
| 属性 | 默认 | 上限/原则 |
|
||||
|---|---|---|
|
||||
| 单位格节距 u | 舒适 56–72D,推荐不低于 48D | 不能为了塞题自动缩至不可读 |
|
||||
| 单侧内缩 | 0.05u | 两相邻块面之间合计约 0.10u 缝 |
|
||||
| 外圆角 | `clamp(7, 0.18u, 14)` D | 大格仍用同一个圆角尺度 |
|
||||
| 厚度下沿 | 2D | 教学大块最多 3D;小盘优先 2D |
|
||||
| 按压厚度 | 1D | 不改变逻辑外轮廓 |
|
||||
| 未走结构轮廓 | 1.25D | 实机抗锯齿后不足时 1.5–2D |
|
||||
| 线索环 | `max(2, 0.04u)` D | 位于块面内,避免把缝变成墙 |
|
||||
| 提示外圈 | 3D 起 | 与轮廓之间留 2D;不盖数字 |
|
||||
| 接触阴影 | 黑/深墨,透明度不超过 8%,模糊最多 6D | 只为接触感,不承担状态 |
|
||||
|
||||
推荐使用**纯色面+2D 偏移下沿+轻量轮廓**作为可跨 Canvas/Cocos 的基础版。真实模糊阴影是可选增强,低端机可去掉而不丢信息。
|
||||
|
||||
### 7.2 光照
|
||||
|
||||
全局光从左上方来;顶部/左侧微亮,底部稍深。不允许各块光向随机。没有必要做实时光源和反射,深浅可用固定颜色层表达。
|
||||
|
||||
若使用正面渐变,表面相对亮度变化控制在约 0.02 内,且最终逐状态重新算对比。基准令牌用纯色面,渐变是可选样板,不能默认继承纯色配对的验收结果。
|
||||
|
||||
不要做突出角上“糖果白点”,也不要给数字做压印/浮雕。数字是印在表面的清楚符号。
|
||||
|
||||
### 7.3 层级顺序
|
||||
|
||||
从下到上:背景 → 接触阴影/下沿 → 未走/已走块面与整体轮廓 → 路径连接桥及接缝修复 → 必要光带/内环 → 数字/原条件角标 → 墙与转轴 → 提示外圈/指示 → UI 浮层。
|
||||
|
||||
墙应在所有相关格的路径填色上方保持清晰。提示外圈不盖墙身;两个相交时调整标记局部断开,不能让高亮暗示墙消失。
|
||||
|
||||
### 7.4 异形和大格
|
||||
|
||||
- `2×2/3×3` 是一整块,不画内部网格、单元凸点或四块各自阴影。
|
||||
- L/T/S 形将单位格合并成整体轮廓后画一次底沿;内部拼接缝和重复描边必须消除。
|
||||
- 外凸角柔和,凹口保留足以读懂形状的几何精度;凹角不靠大圆角抹掉。
|
||||
- 数字位置保持在几何层选定的有效锚点单位格内。不能简单用包围盒中心,使数字落到 L/S 的空洞。
|
||||
- 大格按压时用块面色/下沿高度反馈,不让整块缩小到露出假通道。
|
||||
|
||||
## 8. 路径与格子状态
|
||||
|
||||
### 8.1 状态矩阵
|
||||
|
||||
| 状态 | 正面/边缘 | 数字与标记 | 反馈 |
|
||||
|---|---|---|---|
|
||||
| 未走普通格 | `cellFace`+有效结构轮廓+浅下沿 | 无数字 | 静止 |
|
||||
| 未走数字线索 | 同上+深墨内环 | 固定数字深墨 | 数字先清楚,环不抢字 |
|
||||
| 未走奇偶 | 同上+深墨内环 | 大号“奇/偶” | 非装饰标签 |
|
||||
| 起点未按下 | 主紫/深紫下沿 | 白色 `1`,已知线索环 | 可以静态强调,不预先计步 |
|
||||
| 已走普通格 | 主紫/深紫下沿 | 当前真实步号白色 | 新进入时局部按压/回弹 |
|
||||
| 已走线索 | 同已走+淡紫内环 | 真实步号白色 | 轻微边缘确认 |
|
||||
| 已走奇偶 | 同已走+淡紫内环 | 主步号+保留“奇/偶”标签 | 标签不得缩得不可读 |
|
||||
| 当前末端 | 同已走,稳定的细内圈或局部边缘标记 | 步号照常 | 不持续跳动 |
|
||||
| 错误 | 新错误红/深红下沿 | 当前步号+已有原要求角标 | 短晃动;路径桥仍紫色 |
|
||||
| 死路 | 保留当前颜色,突出末端轮廓 | 保留数字 | 一次轻提示,无失败大弹窗 |
|
||||
| 提示目标 | 不改原状态,叠琥珀外圈 | 保留全部原信息 | 有限次数脉冲后静态/淡出 |
|
||||
| 补数字 | 成为普通固定线索 | 新数保留原奇偶标签 | 琥珀圈短时强调,不用永久异色 |
|
||||
| 待回退段 | 保留结构,适度降低路径饱和/亮度 | 数字仍可读 | 分叉格明确,不能整段低透明到不可辨 |
|
||||
| 通关 | 主紫面保持 | 数字保留,顶栏有“完成” | 路径次序收束,不整屏爆白 |
|
||||
|
||||
原关数字与补数字仍按真实步号约束,不能因填色状态遮住要求。当前“允许画错、可撤回”不变,美术不得替换成错误即失败。
|
||||
|
||||
### 8.2 连接桥
|
||||
|
||||
桥的可见宽度约 0.46u,跨缝方向从第一帧就覆盖完整间隙。增长只改变桥的厚度/局部亮度,不先画一条与指尖分离的飞线。
|
||||
|
||||
桥连接实际经过的相邻格;相邻且都已走,不代表都应该连桥。大格多出口处只连接路径真正的前后邻居。桥面与已走面同色,接缝不留黑线;需移除其覆盖区域内的块边描线,避免路径看起来被截断。
|
||||
|
||||
错误格进出桥仍为紫色,避免红色错误扩散成整条路线。撤销时被删除的桥立即消失,不滞留装饰残影。
|
||||
|
||||
## 9. 六边形、静态墙与转墙
|
||||
|
||||
### 9.1 六边积木
|
||||
|
||||
沿用暖白、主紫、数字字形和厚度;几何仍为正六边形共边关系。圆角只修外形边缘,不改变各格逻辑顶点/中心。多个相邻六边形的下沿不要叠成重黑蜂窝。
|
||||
|
||||
### 9.2 墙造型
|
||||
|
||||
墙是**位于共边中线的薄杆**,深墨身+低饱和金色短斜纹。宽度起点为参考单格宽度的 0.08,限制在约 4–6D;有效长度以真实共边长度为基础,两端留几何空间,避免侵入相邻转轴。具体最小可画长度需逐布局验收。
|
||||
|
||||
杆身是信息,阴影是辅助。墙不占格,不画成数字前的门牌,不做跨越盘面的浮空栏杆。色弱模式仍能通过杆形/斜纹看出被封边。
|
||||
|
||||
### 9.3 转轴与旋转
|
||||
|
||||
转墙用同款杆身,轴端增加清楚的圆轴与深色轮廓。圆轴外径建议 8–12D,密集处先避免重叠而非继续缩小。空盘相位由关卡给定,不能为构图擅自旋转到“好看位置”。
|
||||
|
||||
起手落格也转、每走一步转 120°,撤销逆向复原;按**出发前墙位**判断通行,落格后更新。动效只能展示真实状态,不能自行加停顿、等待或按现实时间转动。
|
||||
|
||||
旋转建议用 fast 120ms、单调 ease-out,不用超过目标角的弹簧。累计角度按步数连续展开,不在 0°/360°边界突然倒转。快速新输入到来时,应先对齐当前逻辑墙位再处理下一步,打断/合并表现,不能排长动画队列拖慢笔尖。
|
||||
|
||||
这是待实现的表现要求,不代表当前渲染已支持上述中断策略。要在真实快速滑动中确认玩家看到的阻挡与判定一致。
|
||||
|
||||
### 9.4 撞墙与教学
|
||||
|
||||
撞墙:墙轮廓短时加强+局部小幅反馈,末端不穿越,桥不生成。不能让整块被错误染红从而误导“数字踩错”。
|
||||
|
||||
教学文案:“条纹是墙,绕开它。”“每走一步,转墙就转一次。”显示实际旋转而非给玩家暴露 `mod 3`、状态机等技术术语。减弱动态下直接切到正确墙位,保留清楚静态轴心。
|
||||
|
||||
## 10. 布局网格、安全区与适配
|
||||
|
||||
### 10.1 基准与间距
|
||||
|
||||
以 4D 为基础节奏:4、8、12、16、20、24、32、40、48。正常左右边距 20D,紧凑模式 12D;独立小标签内部 8–12D,卡片内部 16–20D。
|
||||
|
||||
底部、刘海、摄像头、小游戏胶囊使用宿主返回的实际安全区。参考设计图不能把 44/34 等常见高度硬编码成所有设备值。
|
||||
|
||||
### 10.2 对局区域预算
|
||||
|
||||
| 区域 | 基准高度/间距 | 紧凑模式 |
|
||||
|---|---|---|
|
||||
| 顶安全区下留白 | 12D | 8D |
|
||||
| 顶栏 | 56D | 48D,保留点击区 |
|
||||
| 包进度条 | 高 4D,上下合计约 12D | 内容密集时可用文本进度替代 |
|
||||
| 教学说明 | 36D 起;两行时随行高扩展 | 不挤到盘面内部 |
|
||||
| 盘面容器 | 剩余空间,外保护区 12D | 可缩保护区到 4D,不能侵入 UI |
|
||||
| 手势说明 | 18–20D | 已学会后可省略冗余,不省规则 |
|
||||
| 动作栏 | 高 56D | 52D 起,不减有效触区 |
|
||||
| 底部留白 | 16D+实际安全区 | 12D+实际安全区 |
|
||||
|
||||
盘面以可用空间计算:`u = min((可用宽−左右保护区)/列数, (可用高−上下保护区)/行数)`。六边应使用自己的精确包围盒,不套矩形列数公式。
|
||||
|
||||
375 宽、边距 20、保护区各 12 时,5 列节距约 62.2D。320 宽紧凑模式、边距 12、保护区各 4 时,6 列可达 48D。7 列在同条件下不足 48D,不能只用缩字解决:需要重新评审版型或关卡投放;若改几何则必须重新做规则/唯一性检查。
|
||||
|
||||
### 10.3 触区与视觉边界
|
||||
|
||||
UI 按钮统一按至少 48×48D 设计,再核对平台映射。苹果通常建议至少 44×44pt;Android 建议至少 48×48dp,不能把它们当作物理像素。[Apple](https://developer.apple.com/design/human-interface-guidelines/buttons)、[Android](https://support.google.com/accessibility/android/answer/7101858?hl=en)
|
||||
|
||||
盘面连续滑动命中需要沿用真实几何与输入策略,不能为了凑 48 把相邻格触区无条件扩成重叠。u≥48 是设计目标,还要检查死区、窄边和异形凹口后的实际命中感。
|
||||
|
||||
## 11. UI 组件规范
|
||||
|
||||
### 11.1 按钮
|
||||
|
||||
| 组件 | 尺寸 | 造型 | 交互 |
|
||||
|---|---|---|---|
|
||||
| 主按钮 | 高 56D,横向内边距 24D,圆角 16D | 主紫面、白字 16/700、2D 深紫下沿 | 按压下沿 1D、面深紫、最多缩至 .98 |
|
||||
| 次按钮 | 高 52–56D,圆角 16D | 次级暖灰面、深墨字 | 下压 1D,不变主紫 |
|
||||
| 对局动作 | 三等宽,高 56D,间距 8D | 图标 20–22D+16D 标签 | 状态不依赖 hover |
|
||||
| 图标按钮 | 48×48D,图形 22–24D | 中性底或透明底 | 有辅助名称与清晰焦点 |
|
||||
| 文字按钮 | 有效高至少 48D | 14–16D 深选中文字 | 不只依赖文字下划线识别动作 |
|
||||
|
||||
主要页面最多一个视觉上最强的主操作。主按钮不常驻脉冲;正常状态不发光。禁用按钮不接受点击,但旁边必要的原因文字仍满足可读要求。
|
||||
|
||||
### 11.2 关卡块
|
||||
|
||||
正常模式 5 列,间距 10–12D。375 宽可用 335D 时,12D 间隔下单块约 57.4D;320 紧凑宽可用 296D 时约 49.6D。若要增列,先保留 48D 目标。
|
||||
|
||||
- 可挑战:暖灰积木、深墨数字、结构轮廓。
|
||||
- 已完成:淡紫底、深选中数字、清楚的小勾;避免整页满紫与当前关竞争。
|
||||
- 当前关:主紫底白字+明确“继续”入口;不靠不停呼吸标记。
|
||||
- 未解锁:中性面、锁标记,提供可读解锁条件;不使用红色。若业务允许展示编号,可保留编号,不改变进度规则。
|
||||
|
||||
上面是**选关 UI 的状态语义**,与盘面“紫色表示已走”处于不同页面语境,不应将它们混成同一张状态图。
|
||||
|
||||
### 11.3 进度、标签与开关
|
||||
|
||||
包进度高 4D;总进度高 6D;必要文本 `已完成 x / n` 使用 14D,不把文本当作装饰性弱灰。关数多时用连续进度,不画几百根难以分辨的短柱。
|
||||
|
||||
标签高 28–32D,字 13/600;可点击标签的触区补足 48D且不重叠。开关轨道视觉约 40×24D、有效行高 52D,必须同时显示设置名和开/关状态,不能只给一个彩色点。
|
||||
|
||||
### 11.4 对话框、底部面板与 Toast
|
||||
|
||||
弹窗宽 `min(视口−40D, 340D)`,圆角 24D,内边距 24D;标题 24/32、正文 16/24、按钮间隔 12D。弹窗遮罩起点为深墨 32%,不把背后棋盘模糊到造成性能浪费。
|
||||
|
||||
底部面板保留底安全区,长内容内部可滚;不要同时放多个堆叠弹窗。帮助关闭后返回原来的关卡状态,不自动重开。
|
||||
|
||||
Toast:深墨面白字 14/20,最大宽度视口减 40D;默认停留约 2 秒,重要说明可更久或使用面板;位于动作栏上方,不盖当前末端或教学区。只有信息性文字,无需要快速点击才能避免损失的按钮。
|
||||
|
||||
### 11.5 图标
|
||||
|
||||
统一圆头、圆角连接的线性图标,24D 网格,约 2D 笔画,正常 20–24D;不混用 emoji、彩色插画、细线和实心风格。
|
||||
|
||||
重开使用回转箭头,补数字使用数字/加号语义,指路使用路线/方向语义;两个提示按钮必须不同图标和文字。锁、勾、设置、关闭保持直观。图标字体缺字会丢功能,正式使用可控矢量/图集并保留清楚标签。
|
||||
|
||||
## 12. 页面设计:从进入到完成
|
||||
|
||||
### 12.1 玩法首页(共享壳,拟新增页面)
|
||||
|
||||
顶部品牌标题和设置入口;中间用两个玩法卡表现数字积木、六边与边墙。卡面是简短盘面切片,不是宣传插画。只用相同主色,通过轮廓区分玩法。
|
||||
|
||||
若只有一个已上线玩法,直接进入继续/选关,不为了共享壳多加一页。当前 `shared/` 是否具备完整壳能力需实施时确认,样张不代表已实现。
|
||||
|
||||
### 12.2 选关页
|
||||
|
||||
标题“数字一笔”,副说明“按步数,一笔走遍每一块”;下方总进度、分包标题和关卡网格。底部主要操作“继续 · {关号}”,设置入口降一级。
|
||||
|
||||
内容量大时使用章节折叠/分段加载等列表策略,但不在用户中途折叠当前章节。关卡总数从实际数据读取,不能在静态美术图里把某个早期关数写成固定总量。
|
||||
|
||||
### 12.3 数字对局页
|
||||
|
||||
顶栏:左返回,中关号/包名,右剩余格数。盘面是视觉中心;底栏固定保留**重开 / 补数字 / 指路**三个不同动作,而不是因概念图简化就合并成一个提示。
|
||||
|
||||
首次关键教学使用短句:“从 1 开始,滑向旁边的格。”“大格,也只算一步。”“数字要在对应步数走到。”后续减少文字,不持续显示多条规则抢盘面。
|
||||
|
||||
若实际版本有分段指路或动画教学,沿用该版本真实语义核对;不能用这份风格报告覆盖其他进行中的交互设计。
|
||||
|
||||
### 12.4 六边对局页
|
||||
|
||||
结构和排版一致,盘面使用独立六边模型。墙/转墙教学发生在盘面附近,避免 UI 卡片压住轴心。没有实现补数字的玩法不得放一个实际无效的“补数字”按钮;动作栏按真实功能注册,两按钮时调整间距而非硬塞第三个。
|
||||
|
||||
### 12.5 过关
|
||||
|
||||
普通主线继续使用短收束后进入下一关;先给玩家看清完成的路线。若产品仍采用自动下一关,不新增必须点按钮的庆祝页。
|
||||
|
||||
包尾或全部完成可以展示轻量结果面板:标题“这一包完成了”,几何小图、完成进度、下一包/返回关卡。没有计时/评分系统时,不新增三星、金币、假时长或“智商提升”。具体完成态取实际内容,不写“全部 26 关”一类固定旧值。
|
||||
|
||||
### 12.6 设置与帮助
|
||||
|
||||
分组:声音、触感、减弱动态、对比辅助、语言/其他真实支持项。尚未实现的选项只可标作设计稿,不应在正式产品露出空开关。
|
||||
|
||||
帮助使用小盘分步示例:普通共边 → 大格一步 → 数字时机 → 撤销。六边单独教墙和起手旋转。每张说明只讲一件事,示例不应与真实可走规则冲突。
|
||||
|
||||
### 12.7 重开与清进度
|
||||
|
||||
重开关卡是低成本常用动作,不默认弹确认框;若存在本局补数字保留规则,文案与状态保持一致。清除全部进度是另一动作,放设置下方,需解释并确认,视觉使用深红文字,不与“重开”相邻混排。
|
||||
|
||||
### 12.8 加载、离线、广告与购买(条件页面)
|
||||
|
||||
加载:品牌几何标记+真实阶段/进度,纯本地短载入不加假进度条。字体未就绪先稳定回退,不显示白屏等字。
|
||||
|
||||
离线:常规关照常玩;仅网络依赖功能说明不可用。广告不可用写“暂时无法加载,请稍后再试”,不写成玩家失败,也不重置路线。
|
||||
|
||||
若接激励提示,明确“看视频获得一次指路”,不要把广告按钮伪装成普通免费提示;奖励发放以实际成功回调为准。加载期间保持棋盘状态,关闭/失败后可返回。这里是将来商业 UI 条件规范,不是要求本轮接广告。
|
||||
|
||||
去广告购买页保持同一风格,用平台真实价格与支付方式;购买、恢复和网络错误用准确文案,不造倒计时。商店功能未实现时不出现在正式截图。
|
||||
|
||||
## 13. 动效系统与特效表现
|
||||
|
||||
### 13.1 基础时长与曲线
|
||||
|
||||
| 令牌 | 时长 | 适用 |
|
||||
|---|---:|---|
|
||||
| fast | 120ms | 按压、桥厚度增长、转墙表现 |
|
||||
| base | 200ms | 按压回弹、组件/面板入场 |
|
||||
| slow | 320ms | 冲突短反馈、过关收束、页面平移 |
|
||||
| hold | 500ms | 完成路线的停留 |
|
||||
|
||||
有意保留现有四档系统。提示显示总长 2.4 秒是序列总长,不是第五种基本过渡。现有 280/160ms 等特例若采用本方案再统一;本次不改代码。
|
||||
|
||||
入场/增长 `cubic-bezier(.22,1,.36,1)`;退场 `(.4,0,1,1)`;柔和往返 `(.65,0,.35,1)`。逻辑机构旋转只用单调曲线,不使用过冲。触觉“软”主要来自轻按压和短回弹,不来自持续晃动。
|
||||
|
||||
### 13.2 事件表现表
|
||||
|
||||
| 事件 | 几何/颜色表现 | 时长 | 声音/触感意图 |
|
||||
|---|---|---:|---|
|
||||
| 点按钮 | 下沿 2→1D,面向下 1D,缩放不小于 .98 | fast | 可选很轻点击;不每个 UI 都振 |
|
||||
| 开始画 | 起点进入真实已走状态 | 立即,局部 base | 轻落块声 |
|
||||
| 增加一步 | 逻辑立即追加,填色立即到位;局部压入→恢复,最大1.02缩放但不改变命中 | base | 轻短音;支持时一次轻触感 |
|
||||
| 连接桥出现 | 全跨缝已有,只增长可见厚度 | fast | 不单独重复播放一条音效 |
|
||||
| 踩对线索 | 内环/边缘光短扫,数字保持清楚 | fast/base | 较轻的确认音色 |
|
||||
| 步号冲突 | 立即红面,最多±2D一次衰减晃动 | slow | 低音量闷响;一次短提醒 |
|
||||
| 撤销一步 | 逻辑与填色/桥立即恢复,取消该格未完特效 | 立即 | 可选更轻回退音,不每步强振 |
|
||||
| 多步回退 | 后缀整体立即恢复,分叉目标轻确认 | base | 一次反馈,不按撤销格数叠加 |
|
||||
| 指路 | 目标外圈有限2次柔和变化,之后退到静态/淡出 | 总2.4s | 不持续播声 |
|
||||
| 补数字 | 新数与原条件同时可读,目标轻圈出 | base+2.4s强调 | 一次确认 |
|
||||
| 转墙更新 | 累计角度转120°,可被新逻辑状态打断对齐 | fast | 不为每个转轴独立叠音 |
|
||||
| 撞墙 | 杆轮廓加强、局部最多1D反馈;不生成桥 | fast/base | 单次柔和拒绝音 |
|
||||
| 死路 | 当前末端短标记/小幅反馈 | base/slow | 无长警报、无全屏失败 |
|
||||
| 页面入场 | 轻上移最多8D+淡入 | base | 不附加过场音乐 |
|
||||
| 关卡进场 | 格子小幅到位,延迟12ms/格,总延迟封顶120ms | base+有限错落 | 不按面积无限延长 |
|
||||
| 完成 | 保持主紫面,按路径次序扫外沿光带,顶栏切完成 | slow+hold | 一次短完成和弦 |
|
||||
|
||||
数字与实际路径不能等动画播完才出现。快速输入不能被某个按压特效或转轴动画锁住。
|
||||
|
||||
### 13.3 特效禁区
|
||||
|
||||
不在数字上叠白色全亮闪片;不让光带遮住原条件;不让阴影越过墙像一条连接;不在游戏思考时生成漂浮星星;不使用整屏 RGB 震动、镜头摇晃、彩虹路径或每步飞出的金币。
|
||||
|
||||
可以让**按钮和积木下沿**产生轻微接触反馈,但不能把规则对象当橡皮泥拉扯。环境光、背景和材质在静止时应当静止。
|
||||
|
||||
### 13.4 过关时间线
|
||||
|
||||
以最后一步确认时刻为 0:
|
||||
|
||||
| 时间 | 内容 |
|
||||
|---|---|
|
||||
| 0 | 逻辑通关,顶栏“完成”,停止冲突/提示残留 |
|
||||
| 0–320ms | 路径沿线/边缘收束,数字不被提亮覆盖 |
|
||||
| 320–820ms | 完整解停留500ms |
|
||||
| 820–940ms | 旧盘面退出120ms |
|
||||
| 940ms起 | 新盘面进入,顶栏就地更新;单格200ms,错落延迟总封顶120ms |
|
||||
|
||||
正常自动切关时,退出期间不再接受旧盘面命令;新关输入与显示必须在同一关号下重新绑定。若用户主动返回选关,取消等待序列,不让后台自动跳关。
|
||||
|
||||
### 13.5 减弱动态与高频事件
|
||||
|
||||
减弱动态:保留颜色、文本、位置和静态提示;取消缩放/晃动/扫光/错落;转墙直接切正确位置;不隐藏错误和提示。完成后仍留一段稳定可看的路线,不用突然闪白替代。
|
||||
|
||||
快速走格只保留最近的局部按压,音效限并发,不把十几个特效排队。撤销、重开、换关必须取消已有对象的表现,不能出现幽灵闪光和旧关转轴。
|
||||
|
||||
## 14. 声音与触感
|
||||
|
||||
质感方向是短、轻、清楚的哑光敲击;避免尖锐键盘音、玻璃碎裂和果冻咕噜。声音可帮助理解操作,但静音不损失信息。
|
||||
|
||||
| 类型 | 建议长度 | 音色/变化 | 限制 |
|
||||
|---|---:|---|---|
|
||||
| 落格 | 40–80ms | 柔和木槌/钝塑料短音,音高有限递进 | 不随无限步数一直升高,不模仿游戏警报 |
|
||||
| 线索正确 | 60–120ms | 比落格更明确的短确认 | 与普通落格合并,不叠响 |
|
||||
| 冲突/撞墙 | 80–140ms | 低沉软拒绝 | 相对落格不必更响,不做惩罚吓声 |
|
||||
| 撤销 | 30–60ms或静音 | 更轻的回收感 | 连续回退限频 |
|
||||
| 完成 | 300–600ms | 柔和2–4音上行/收束 | 不阻塞切关,不含语音夸张喊叫 |
|
||||
|
||||
首版默认不做常驻背景音乐;需要时后续独立开关,不让循环乐承担留存假设。音效文件留余量避免削波,最终音量用真机听测,不以某个混音软件响度数值代替听感。
|
||||
|
||||
触感用“轻落格/确认/错误”意图映射平台能力;Android 毫秒参数、iOS 触感类型、小游戏能力不同,不能承诺同样数值产生同样感觉。不支持时安静降级;“关闭触感”必须覆盖全部事件。
|
||||
|
||||
## 15. 文案、教学与国际化
|
||||
|
||||
文字语气:简洁、准确、温和。优先描述规则与可恢复操作,不评价玩家能力。
|
||||
|
||||
| 场景 | 推荐 | 避免 |
|
||||
|---|---|---|
|
||||
| 大格 | “大格,也只算一步” | “四个格子连成一个”,容易误认仍分四步 |
|
||||
| 中段数 | “走到这里时,应是第8步” | “按顺序连接8”,容易误认 Zip 检查点语义 |
|
||||
| 回退 | “沿原路退回,换个方向试试” | “挑战失败,请复活” |
|
||||
| 转墙 | “每走一步,转墙就转一次” | “等墙转开”,实际不是时间机关 |
|
||||
| 死路 | “这里走不通,退回试试” | “你走错了!”反复弹窗 |
|
||||
| 完成 | “这一关完成了” | “智商增加10点” |
|
||||
|
||||
英文样例可用 `One block. One step.`、`Reach this block on step 8.`、`Trace back to try another route.`;最终按母语文案校对,不把中文逐字硬译。
|
||||
|
||||
奇/偶是语言相关信息:英文建议 `Odd / Even`,与数字字体分开检查;已走后的长标签会比单汉字占空间,应通过相应样板重排,不缩到不可读。未来需其他文字系统时检查字库覆盖和镜像规则,不将数字路径/转墙方向随 RTL 自动反转。
|
||||
|
||||
## 16. 无障碍与特殊显示
|
||||
|
||||
- 不只靠红色/黄色:错误保留条件角标/明确提示;墙有轮廓与条纹;完成有勾与文字;当前末端有位置标记。
|
||||
- 必要边界默认已有足够对比,不把基本可读性只放进“高对比模式”。辅助模式再加粗轮廓、提供稳定图形提示。
|
||||
- 不将 Canvas 上的可见数字等同于屏幕阅读器已可读;如需语音访问,应另建格子/操作语义层并专门设计交互。
|
||||
- 重复 UI 提示适度通知,不在快速每步都用读屏打断;剩余格数可合并播报。
|
||||
- 焦点圈约3D,外偏2D;仅键盘焦点出现,不能把所有格都画成焦点。
|
||||
- 低对比屏、灰度、红绿色觉模拟、强环境光、减弱动态、静音分别检查。模拟结果不能代替真实用户测试。
|
||||
- 常规文字放大到150%与200%测试菜单重排;盘面需要单独检查,不以牺牲关键数字读取来维持原布局。
|
||||
|
||||
## 17. 图标、品牌与商店素材
|
||||
|
||||
### 17.1 应用图标
|
||||
|
||||
主图标以**少量数字+连贯路径+一块较大的拼块**构成;取其中两个主识别点,第三个不要塞得太满。保留主紫和浅背景,厚度方向与游戏一致,不加角色和长中文副标题。
|
||||
|
||||
制作1024×1024的可编辑母稿及矢量形状来源,用48/64/128等缩略预览检查辨识。母稿尺寸是制作建议,平台正式文件规格以上传时官方要求为准。iOS和Android的图标处理流程不同,导出不要把装饰性圆角、阴影和系统遮罩重复叠加。
|
||||
|
||||
### 17.2 商店截图
|
||||
|
||||
前3张依次表达:一笔走遍 → 大块只算一步 → 数字决定路线。后续表现真实已有六边/转墙、容错和内容等。大标题与真实盘面分层;放大盘面局部时保留规则上下文,不做不可能解或未实现特效。
|
||||
|
||||
游戏内 UI 用16D正文;商店宣传标题的大小按最终导出图片与缩略展示单独设计,不能拿游戏字号表直接乘DPR当广告标题。不同语言使用同一版式骨架,检查长标题占比。
|
||||
|
||||
### 17.3 视频/小游戏入口素材
|
||||
|
||||
优先展示真实手势、数字到达、一个大格只填一个步号,再给完整路线收束。柔和材质要在普通视频压缩后仍有轮廓;极淡阴影不是视频中的唯一信息。手指演示标记只用于教学/宣传,不能遮当前数字。
|
||||
|
||||
素材与实际皮肤保持一致。投放美术和游戏皮肤分别测试,不能只看点击判定风格成功。商业论证见 [前一份报告](一笔画美术风格与商业验证方案.md),本报告不新增收益承诺。
|
||||
|
||||
## 18. 性能、渲染与资源交付
|
||||
|
||||
### 18.1 渲染策略
|
||||
|
||||
现有 Painter 支持圆角矩形/多边形/圆/文本等基础原语,基础材质可通过分层绘制实现。无需先增加全屏模糊、PBR材质或实时阴影。
|
||||
|
||||
当前文本原语主要接收字号、颜色和 `bold` 布尔值,并没有完整的字体角色/字重接口。落地 Nunito 数字800与中文500/600/700时,应明确增加文字角色映射或扩展渲染层参数;不能仅改CSS后宣称Canvas/Cocos也已统一字体。
|
||||
|
||||
若要渐变/复合轮廓/精确剪裁而当前接口不支持,应由渲染实现明确扩展;不能在文档里假定原接口自动支持。数字与六边规则代码仍各自独立,壳只共用视觉规范和无规则含义的组件。
|
||||
|
||||
静止盘面不为装饰持续重绘;有真实动画时才更新。Canvas按DPR设置绘图分辨率,必要时根据设备设置合理上限并复核锐度;DPR倍率不改变逻辑命中坐标。
|
||||
|
||||
### 18.2 性能目标与降级
|
||||
|
||||
参考目标:主流设备交互动画60fps;低端机若需30fps降级仍保持逻辑状态正确。16.7/33.3ms是整帧预算,不是单个阴影可占用的时长。该目标尚未通过本轮实测。
|
||||
|
||||
降级顺序:去模糊阴影 → 去可选渐变/边缘光 → 取消非必要错落 → 减少并发反馈;最后仍保留数字、格边、路径、错误、墙与转轴。不能通过漏画墙或延迟逻辑来保帧。
|
||||
|
||||
### 18.3 资源清单与命名
|
||||
|
||||
| 类别 | 交付 | 检查 |
|
||||
|---|---|---|
|
||||
| 规范 | 本报告、令牌JSON、视觉样张 | 版本号、颜色、尺寸一致 |
|
||||
| 页面 | 玩法入口/选关、数字对局、六边对局、设置、帮助、包尾完成、确认面板 | 正常/长文本/窄屏/禁用态 |
|
||||
| 盘面 | 单格、大格、L/T/S、六边、墙、转轴、桥、条件角标 | 全状态与接缝 |
|
||||
| 图标 | 可编辑矢量母稿+目标平台导出 | 线宽、圆角、语义一致 |
|
||||
| 字体 | 指定来源/版本/字重/许可文件 | 字形、测宽、缺字、包体 |
|
||||
| 音效 | 落格、确认、拒绝、完成的无损母稿+平台编码版本 | 削波、延迟、静音/触感开关 |
|
||||
| 商店 | 图标、截图、短视频分层源文件 | 与真实游戏一致、本地化 |
|
||||
|
||||
命名示例:`sb-v1/button-primary-pressed`、`sb-v1/tile-idle`、`sb-v1/wall-spinner`。这些是资源命名规则,不是在当前项目中虚构已存在的资源路径。避免 `最终版2-真最终` 这类无法回溯的名称。
|
||||
|
||||
## 19. 美术制作与开发接入顺序
|
||||
|
||||
1. 锁定基准:暖白/主紫/数字字体意图、单位体系、状态语义。
|
||||
2. 先做单格、大格、异形、六边和墙的状态样板,检查读盘。
|
||||
3. 做数字对局与六边对局两张可操作样板,含快速输入、回退、提示、转墙。
|
||||
4. 完成选关、设置、帮助和包尾收束,保证完整流程。
|
||||
5. 对比现有扁平版,验证清楚程度、误触与偏好,而非只问“好不好看”。
|
||||
6. 确定正式候选后,同步 `design.md`、样张、CSS/theme、绘制层、平台宿主与相应检查。
|
||||
7. 通过实机矩阵后制作正式商店素材;商店图标/截图与产品皮肤分别验证。
|
||||
|
||||
当前只交付规范和样张,未执行第3–7步的游戏接入与用户验证。
|
||||
|
||||
## 20. 验收矩阵
|
||||
|
||||
### 20.1 设计检查
|
||||
|
||||
| 项 | 合格条件 |
|
||||
|---|---|
|
||||
| 风格 | 正俯视、同一哑光材质、统一光向与浅厚度 |
|
||||
| 颜色 | 关键文字和边界使用已定义颜色配对;无任意透明降对比 |
|
||||
| 字体 | 数字与中文的实际字形、字重和回退经过确认 |
|
||||
| 大格 | 2×2、3×3均被读为一块;无内部缝、无面积放大字号 |
|
||||
| 异形 | 凹口、共边、文字锚点清楚,无阴影假邻接 |
|
||||
| 六边 | 共边杆/轴心准确、密集位置可读 |
|
||||
| 当前状态 | 能区分已走、末端、冲突和提示,无需只看颜色 |
|
||||
| UI | 三种数字动作分明,真实功能与文案一致 |
|
||||
| 动效 | 不延迟逻辑、不遮数字、不依赖速度完成机关 |
|
||||
| 反馈 | 错误可恢复;撤销/重开清除旧特效 |
|
||||
|
||||
### 20.2 实机与内容覆盖
|
||||
|
||||
至少覆盖320/360/375/390/430逻辑宽度、短屏/长屏、实际刘海与小游戏胶囊;iOS/Android及目标小游戏宿主分别测。不是要求五种宽度刚好对应某五款机型。
|
||||
|
||||
关卡样本:最小教学盘、2×2枢纽、3×3大块、L/T/S凹口、两位数字密集盘、奇偶保留标签、静态墙、单转墙、多转墙、无起点1、已偏离后的指路、补数字落在奇偶格。采用当前真实内容,并记录具体关id,不能仅写“随便测一关”。
|
||||
|
||||
交互样本:慢滑、快速跨格、松手续画、点按追加、沿路回退、该玩法支持的多步回退、重开、切后台/恢复、提示取消、关卡切换中返回。数字与六边回退能力若不同,分别说明,不强行在美术层统一。
|
||||
|
||||
### 20.3 尚待完成的验证
|
||||
|
||||
| 验证 | 本轮状态 |
|
||||
|---|---|
|
||||
| 令牌静态配对对比度 | 已通过19组目标配对,3组诊断另列 |
|
||||
| 样张布局与局部交互 | 在本轮配套HTML中演示并检查;不等于游戏验收 |
|
||||
| 字体正式文件打包与全平台字形 | 未执行;样张使用系统可用回退 |
|
||||
| 真实游戏最小触点与所有布局 | 未执行 |
|
||||
| 连续转墙/输入/回退真实运行 | 未执行 |
|
||||
| 完整工程check与微信/原生构建 | 本轮不修改运行时代码,未执行 |
|
||||
| 真人读盘/误触/留存与商业A/B | 未执行 |
|
||||
|
||||
## 21. 证据 → 结论 → 执行路径
|
||||
|
||||
| 证据 | 结论 | 执行路径 |
|
||||
|---|---|---|
|
||||
| 用户选择柔和积木,已有几何路径与单主色 | 不需要重做题材,优先增量材质升级 | 在原语上补下沿/边界和按压样板 |
|
||||
| 现有Board/Painter、GDD定义大格一步与边墙 | 美术不能改变格数感和邻接 | 整体轮廓、共边中线、转轴精确对位 |
|
||||
| 静态对比脚本:面/背景约1.18 | 灰块不能只靠阴影识别 | 所有必要结构加有效边界 |
|
||||
| 旧错误红与亮紫白字配对低于本方案4.5目标 | 状态变亮不能牺牲数字可读性 | 新错误红;完成保留主紫面、只扫边缘 |
|
||||
| 平台触区建议与最小屏幕预算 | 逻辑单位、物理像素和可见图形需区分 | 按平台映射、紧凑模式、实机命中测试 |
|
||||
| Nunito/Noto官方字体来源 | 可建立可控跨平台字形方案 | 正式选版、保留许可、测缺字与测宽 |
|
||||
|
||||
复核静态颜色的命令(项目根目录,Node.js可用):
|
||||
|
||||
```powershell
|
||||
node docs/design/soft-blocks/check-contrast.mjs
|
||||
```
|
||||
|
||||
脚本读取同目录令牌、输出JSON和控制台结果;有目标配对不合格时返回非零退出码。它只证明所列不透明颜色配对,不代替整屏/字体/动态状态验收。
|
||||
|
||||
## 22. 与现有规范的差异与采用方式
|
||||
|
||||
| 项 | 现有规范/实现 | 本提案 |
|
||||
|---|---|---|
|
||||
| 主背景 | 纯白 | 暖白 `#F7F5F1` |
|
||||
| 盘面材质 | 无阴影平面 | 哑光面+2D下沿+结构边界;模糊可省 |
|
||||
| 主路径 | 蓝紫 `#6558F5` | 保留 |
|
||||
| 六边路径 | 独立靛蓝 | 建议统一品牌主紫,墙与状态重新验收 |
|
||||
| 错误底 | `#E5484D` | `#C74350`,普通白字配对更清楚 |
|
||||
| 过关底 | 切更亮紫 | 面保持主紫,只扫不遮字的边缘光 |
|
||||
| 辅助文字 | 含对比很弱的旧三级灰 | 改为可读的三级墨与明确用途 |
|
||||
| 字体 | 主要依赖系统栈 | 推荐受控数字/中文字体+稳健回退 |
|
||||
| UI正文 | 15D左右 | 基准16D,行高24D |
|
||||
| 盘面小标签 | 主要按u比例 | 增加实际最小字号与放不下的处理 |
|
||||
| 按钮触区 | 旧44基准 | 新48D设计目标+平台核对 |
|
||||
| 连续动效 | 含较强弹簧与若干特例 | 更小幅按压;四档时长;机构无过冲 |
|
||||
| 长期呼吸 | 当前关/部分提示可循环 | 常驻内容默认静止,提示有限强调 |
|
||||
|
||||
采用时先确认“视觉版本1.0”作为实施目标,再一次性同步旧权威规范、各玩法theme/CSS与样张,并跑相关检查。不直接将本报告的JSON复制为某个现有Theme对象:字段、渲染能力、单位和命名并不完全相同,需要映射。
|
||||
|
||||
本版的核心验收句是:**不看材质也能清楚解题;看见材质会愿意触摸;快速操作时规则、数字和手指始终一致。**
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tool": "built-in image_gen",
|
||||
"purpose": "原创竖屏美术概念效果图,非最终生产素材",
|
||||
"directions": [
|
||||
"Use case: ui-mockup. Create ONE polished portrait mobile puzzle game screenshot, 9:16 composition, edge-to-edge UI, no phone hardware, no surrounding presentation board. Original game inspired by the cheerful approachable educational toy design principles of Big Brain Academy Brain vs Brain, NOT copying its logo, mascot or exact UI. Simplified charming console-quality art direction, moderately richer than minimalist wireframes, not elaborate scenery. Perfectly top-down readable puzzle with shallow 3D bevels and short soft shadows, no perspective distortion. Exact central board geometry on a 4 column x 5 row invisible square grid: row1 only col2 single tile; row2 only col2 single tile; rows3 and4 col1 andcol4 are separate single tiles, while cols2+3 across BOTH rows are ONE seamless 2x2 large square tile; row5 four separate single tiles. Total 11 logical tiles. No internal seam in the large square. Top two tiles and large central tile are purple/blue visited, connected down their shared edges by small matching color bridges. White digits 1 in top tile, 2 in second tile, 3 anchored in TOP LEFT unit of large square. One dark fixed digit 8 in row5 col2. All other tiles blank. No other numeric labels inside board. Large board occupies main central area with spacious margins. Chinese text exact and legible: top title \"数字一笔\", small level label \"关卡 1-8\", compact status \"已走 3 步 · 还剩 8 块\"; below board small instruction \"每块只走一次\"; bottom exactly three generous buttons labeled \"重开\" \"补数字\" \"指路\" with simple appropriate icons above words. Consistent typographic hierarchy. Do not add timer, currency, ads, heart lives, sidebars or scores. No hands, no device, no watermarks. DIRECTION: 明亮教具室. Warm butter yellow full page background with extremely faint oversized outline doodles of simple geometry only outside board, cream softly rounded central teaching mat. Matte ivory resin unvisited blocks with substantial but subtle pale beige side faces, blueberry purple visited blocks and path, small coral accent on level tab and reset icon. Bold rounded friendly Chinese title, tidy readable functional type. Slightly handmade polished educational toy character; no mascot. Bottom buttons three chunky cream rounded keycaps, right hint key purple. One small scalloped coral chapter tag at top, small sparse geometric marks outside play area only. Warm cheerful daytime studio lighting. Restrained coherent palette, crisp high quality game art, not glossy candy. Clearly visible toy bevels and clean structural borders.",
|
||||
"Use case: ui-mockup. Create ONE polished portrait mobile puzzle game screenshot, 9:16 composition, edge-to-edge UI, no phone hardware, no surrounding presentation board. Original game inspired by the cheerful approachable educational toy design principles of Big Brain Academy Brain vs Brain, NOT copying its logo, mascot or exact UI. Simplified charming console-quality art direction, moderately richer than minimalist wireframes, not elaborate scenery. Perfectly top-down readable puzzle with shallow 3D bevels and short soft shadows, no perspective distortion. Exact central board geometry on a 4 column x 5 row invisible square grid: row1 only col2 single tile; row2 only col2 single tile; rows3 and4 col1 andcol4 are separate single tiles, while cols2+3 across BOTH rows are ONE seamless 2x2 large square tile; row5 four separate single tiles. Total 11 logical tiles. No internal seam in the large square. Top two tiles and large central tile are purple/blue visited, connected down their shared edges by small matching color bridges. White digits 1 in top tile, 2 in second tile, 3 anchored in TOP LEFT unit of large square. One dark fixed digit 8 in row5 col2. All other tiles blank. No other numeric labels inside board. Large board occupies main central area with spacious margins. Chinese text exact and legible: top title \"数字一笔\", small level label \"关卡 1-8\", compact status \"已走 3 步 · 还剩 8 块\"; below board small instruction \"每块只走一次\"; bottom exactly three generous buttons labeled \"重开\" \"补数字\" \"指路\" with simple appropriate icons above words. Consistent typographic hierarchy. Do not add timer, currency, ads, heart lives, sidebars or scores. No hands, no device, no watermarks. DIRECTION: 桌游题卡. Mint sage desktop-color backdrop, a tall large cream card with generous elegant margins centered nearly filling screen, rounded clipped card corners, subtle paper thickness and short shadow, navy ink typography. Header within card uses a clean orderly editorial title, small coral rectangular chapter tab and delicate dividing rule. Puzzle blocks are matte pastel ivory board game pieces, active path muted saturated cobalt blue; unvisited edges subtle blue grey. Background very sparse, only one or two simplified offset card corners visible behind main card near top; no desk objects. Bottom three orderly rectangular tactile buttons integrated into lower card, hint cobalt. Small printed circular completion-stamp motif empty near footer but not distracting. Family-friendly mature refined board game style, richer than flat UI, calm balanced composition.",
|
||||
"Use case: ui-mockup. Create ONE polished portrait mobile puzzle game screenshot, 9:16 composition, edge-to-edge UI, no phone hardware, no surrounding presentation board. Original game inspired by the cheerful approachable educational toy design principles of Big Brain Academy Brain vs Brain, NOT copying its logo, mascot or exact UI. Simplified charming console-quality art direction, moderately richer than minimalist wireframes, not elaborate scenery. Perfectly top-down readable puzzle with shallow 3D bevels and short soft shadows, no perspective distortion. Exact central board geometry on a 4 column x 5 row invisible square grid: row1 only col2 single tile; row2 only col2 single tile; rows3 and4 col1 andcol4 are separate single tiles, while cols2+3 across BOTH rows are ONE seamless 2x2 large square tile; row5 four separate single tiles. Total 11 logical tiles. No internal seam in the large square. Top two tiles and large central tile are purple/blue visited, connected down their shared edges by small matching color bridges. White digits 1 in top tile, 2 in second tile, 3 anchored in TOP LEFT unit of large square. One dark fixed digit 8 in row5 col2. All other tiles blank. No other numeric labels inside board. Large board occupies main central area with spacious margins. Chinese text exact and legible: top title \"数字一笔\", small level label \"关卡 1-8\", compact status \"已走 3 步 · 还剩 8 块\"; below board small instruction \"每块只走一次\"; bottom exactly three generous buttons labeled \"重开\" \"补数字\" \"指路\" with simple appropriate icons above words. Consistent typographic hierarchy. Do not add timer, currency, ads, heart lives, sidebars or scores. No hands, no device, no watermarks. DIRECTION: 几何游乐场. Soft coral pink background, pale apricot large rounded asymmetric stage behind board. Original cheerful geometric brand system: sparse large cropped circle, arch and triangle at outer margins, never touching or resembling board cells; no character. Active path deep teal blue, unvisited tiles warm cream with smooth small bevels and subtle warm side faces. Playful rounded Chinese heading slightly dynamic but all numeric clues upright and precise. Top level label small cobalt pill. Bottom three rounded physical buttons, hint teal, one small mustard accent on reset. Richer playful rhythm in spacing and composition, but restrained total decorative shapes and no scenery. Premium simplified game UI, strong coherent shapes, crisp readability, no excessive confetti."
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,34 @@
|
||||
import {readFileSync,writeFileSync} from 'node:fs';
|
||||
import {createHash} from 'node:crypto';
|
||||
const local=p=>new URL(p,import.meta.url);
|
||||
const tokens=JSON.parse(readFileSync(local('tokens.json'),'utf8'));
|
||||
function luminance(hex){
|
||||
if(!/^#[0-9a-f]{6}$/i.test(hex))throw Error('Expected opaque sRGB: '+hex);
|
||||
const c=[1,3,5].map(i=>parseInt(hex.slice(i,i+2),16)/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4);
|
||||
return c[0]*.2126+c[1]*.7152+c[2]*.0722;
|
||||
}
|
||||
const resolve=k=>tokens.color[k]??k;
|
||||
const rows=tokens.contrastPairs.map(p=>{const foreground=resolve(p.fg),background=resolve(p.bg),a=luminance(foreground),b=luminance(background),ratio=(Math.max(a,b)+.05)/(Math.min(a,b)+.05);return {...p,foreground,background,ratio,pass:p.minimum===null?null:ratio>=p.minimum};});
|
||||
writeFileSync(local('contrast-audit.json'),JSON.stringify({method:'WCAG relative luminance; opaque sRGB; unrounded thresholds; static token pairs only, not complete UI compliance',rows},null,2)+'\n');
|
||||
console.table(rows.map(p=>({pair:p.name,ratio:p.ratio.toFixed(3),minimum:p.minimum,result:p.pass===null?'diagnostic':p.pass?'pass':'FAIL'})));
|
||||
const tracked=[
|
||||
'../../../README.md',
|
||||
'../../../wechat-campaign-200-r2/README.md',
|
||||
'../../../wechat-campaign-200-r2/src/upstream/levels.json',
|
||||
'../../../wechat-campaign-200-r2/src/upstream/rules.mjs',
|
||||
'../../../wechat-campaign-200-r2/src/main.mjs',
|
||||
'../../../wechat-campaign-200-r2/src/hints.mjs',
|
||||
'../../../modes/01-number-stroke/src/board.ts',
|
||||
'../../../modes/01-number-stroke/src/game.ts',
|
||||
'../../../modes/03-hex-walls/src/board.ts'
|
||||
];
|
||||
// Paths above resolve from docs/design/bright-classroom/ to repository root.
|
||||
const sources=tracked.map(path=>{const bytes=readFileSync(local(path));return {path,sha256:createHash('sha256').update(bytes).digest('hex')};});
|
||||
const p=JSON.parse(readFileSync(local('../../../wechat-campaign-200-r2/src/upstream/levels.json'),'utf8'));
|
||||
const countBy=fn=>p.levels.reduce((out,l)=>{const k=fn(l);out[k]=(out[k]??0)+1;return out;},{});
|
||||
const extent=l=>{if(l.geometry==='hex')return null;const points=l.cells.flatMap(c=>c.shape?c.shape.map(([x,y])=>[c.x+x,c.y+y]):Array.from({length:(c.w??1)*(c.h??1)},(_,i)=>[c.x+i%(c.w??1),c.y+Math.floor(i/(c.w??1))]));return {cols:Math.max(...points.map(p=>p[0]))-Math.min(...points.map(p=>p[0]))+1,rows:Math.max(...points.map(p=>p[1]))-Math.min(...points.map(p=>p[1]))+1};};
|
||||
const examples=p.levels.map(l=>({id:l.id,chapter:l.chapter,geometry:l.geometry,features:l.features,cells:l.cells.length,...extent(l)}));
|
||||
const audit={date:'2026-10-21',scope:'Read-only design inventory; not solver validation',sources,campaign:{levels:p.levels.length,chapters:p.chapters.length,playOrder:p.playOrder.length,geometry:countBy(l=>l.geometry),featureCounts:Object.fromEntries([...new Set(p.levels.flatMap(l=>l.features))].map(f=>[f,p.levels.filter(l=>l.features.includes(f)).length])),minCells:Math.min(...p.levels.map(l=>l.cells.length)),maxCells:Math.max(...p.levels.map(l=>l.cells.length)),nonemptyConstraintCounts:Object.fromEntries(['relations','orderChains','windows','walls','doors','gates'].map(k=>[k,p.levels.filter(l=>l[k]?.length).length]))},examples};
|
||||
writeFileSync(local('project-audit.json'),JSON.stringify(audit,null,2)+'\n');
|
||||
console.log(JSON.stringify(audit.campaign,null,2));
|
||||
if(rows.some(p=>p.pass===false))process.exitCode=1;
|
||||
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"method": "WCAG relative luminance; opaque sRGB; unrounded thresholds; static token pairs only, not complete UI compliance",
|
||||
"rows": [
|
||||
{
|
||||
"name": "标题/黄底",
|
||||
"fg": "ink",
|
||||
"bg": "background",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#43382F",
|
||||
"background": "#FFE69A",
|
||||
"ratio": 9.232457114035482,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "辅助文字/黄底",
|
||||
"fg": "inkSecondary",
|
||||
"bg": "background",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#71604F",
|
||||
"background": "#FFE69A",
|
||||
"ratio": 4.884289301747754,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "正文/舞台",
|
||||
"fg": "ink",
|
||||
"bg": "stage",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#43382F",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 10.566877010581722,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "辅助文字/舞台",
|
||||
"fg": "inkSecondary",
|
||||
"bg": "stage",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#71604F",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 5.590243604512046,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "数字/未走块",
|
||||
"fg": "ink",
|
||||
"bg": "cellFace",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#43382F",
|
||||
"background": "#FFF8EA",
|
||||
"ratio": 10.765868439756863,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "白数字/路径",
|
||||
"fg": "onPrimary",
|
||||
"bg": "primary",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#6552D9",
|
||||
"ratio": 5.581827872446398,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "白字/按压紫",
|
||||
"fg": "onPrimary",
|
||||
"bg": "primaryPressed",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#5140B7",
|
||||
"ratio": 7.589436402107891,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "白数字/错误",
|
||||
"fg": "onPrimary",
|
||||
"bg": "danger",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#B93B46",
|
||||
"ratio": 5.56014089796343,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "错误说明/淡红",
|
||||
"fg": "dangerInk",
|
||||
"bg": "dangerSoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#862735",
|
||||
"background": "#FBE4E5",
|
||||
"ratio": 7.358198572071083,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "珊瑚标签文字",
|
||||
"fg": "coralInk",
|
||||
"bg": "coral",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#4F2418",
|
||||
"background": "#F57950",
|
||||
"ratio": 4.839371438999691,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "淡紫选中文字",
|
||||
"fg": "primaryPressed",
|
||||
"bg": "primarySoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#5140B7",
|
||||
"background": "#ECE7FF",
|
||||
"ratio": 6.303185573751691,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "提示文字/淡黄",
|
||||
"fg": "hint",
|
||||
"bg": "hintSoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#8B5E00",
|
||||
"background": "#FFF0B8",
|
||||
"ratio": 4.976695326004001,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "成功文字/淡绿",
|
||||
"fg": "success",
|
||||
"bg": "successSoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#2E6B52",
|
||||
"background": "#DEEFDC",
|
||||
"ratio": 5.233622489592483,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "蓝类别标记",
|
||||
"fg": "categoryBlueInk",
|
||||
"bg": "categoryBlueFace",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#245A92",
|
||||
"background": "#CEE4FF",
|
||||
"ratio": 5.4751847239513065,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "橙类别标记",
|
||||
"fg": "categoryOrangeInk",
|
||||
"bg": "categoryOrangeFace",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#8A470E",
|
||||
"background": "#FFE0B2",
|
||||
"ratio": 5.54066890035091,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "数字/蓝类别面",
|
||||
"fg": "ink",
|
||||
"bg": "categoryBlueFace",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#43382F",
|
||||
"background": "#CEE4FF",
|
||||
"ratio": 8.760129662894578,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "数字/橙类别面",
|
||||
"fg": "ink",
|
||||
"bg": "categoryOrangeFace",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#43382F",
|
||||
"background": "#FFE0B2",
|
||||
"ratio": 8.972232377643234,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "格边/格面",
|
||||
"fg": "cellBoundary",
|
||||
"bg": "cellFace",
|
||||
"minimum": 3,
|
||||
"foreground": "#8A7660",
|
||||
"background": "#FFF8EA",
|
||||
"ratio": 4.10397343242954,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "格边/舞台",
|
||||
"fg": "cellBoundary",
|
||||
"bg": "stage",
|
||||
"minimum": 3,
|
||||
"foreground": "#8A7660",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 4.0281174489400735,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "路径/未走格",
|
||||
"fg": "primary",
|
||||
"bg": "cellFace",
|
||||
"minimum": 3,
|
||||
"foreground": "#6552D9",
|
||||
"background": "#FFF8EA",
|
||||
"ratio": 5.280691791017535,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "提示描边/舞台",
|
||||
"fg": "hintCasing",
|
||||
"bg": "stage",
|
||||
"minimum": 3,
|
||||
"foreground": "#43382F",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 10.566877010581722,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "提示白芯/描边",
|
||||
"fg": "hintCore",
|
||||
"bg": "hintCasing",
|
||||
"minimum": 3,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#43382F",
|
||||
"ratio": 11.379801530993449,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "墙/黄色条纹",
|
||||
"fg": "wallInk",
|
||||
"bg": "wallStripe",
|
||||
"minimum": 3,
|
||||
"foreground": "#43382F",
|
||||
"background": "#F7C74D",
|
||||
"ratio": 7.1700148267761294,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "墙/舞台",
|
||||
"fg": "wallInk",
|
||||
"bg": "stage",
|
||||
"minimum": 3,
|
||||
"foreground": "#43382F",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 10.566877010581722,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "开门标记/舞台",
|
||||
"fg": "doorOpen",
|
||||
"bg": "stage",
|
||||
"minimum": 3,
|
||||
"foreground": "#2E6B52",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 5.836381119577005,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "关门标记/舞台",
|
||||
"fg": "doorClosed",
|
||||
"bg": "stage",
|
||||
"minimum": 3,
|
||||
"foreground": "#8B4B29",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 6.217581699869901,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "焦点/黄底",
|
||||
"fg": "focus",
|
||||
"bg": "background",
|
||||
"minimum": 3,
|
||||
"foreground": "#5140B7",
|
||||
"background": "#FFE69A",
|
||||
"ratio": 6.15732584714453,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "白字/珊瑚(仅诊断,不采用)",
|
||||
"fg": "onPrimary",
|
||||
"bg": "coral",
|
||||
"minimum": null,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#F57950",
|
||||
"ratio": 2.715535351933964,
|
||||
"pass": null
|
||||
},
|
||||
{
|
||||
"name": "格面/舞台(仅诊断,格边承担识别)",
|
||||
"fg": "cellFace",
|
||||
"bg": "stage",
|
||||
"minimum": null,
|
||||
"foreground": "#FFF8EA",
|
||||
"background": "#FFF6DF",
|
||||
"ratio": 1.0188316215827884,
|
||||
"pass": null
|
||||
},
|
||||
{
|
||||
"name": "蓝类别面/橙类别面(仅诊断,另配形状)",
|
||||
"fg": "categoryBlueFace",
|
||||
"bg": "categoryOrangeFace",
|
||||
"minimum": null,
|
||||
"foreground": "#CEE4FF",
|
||||
"background": "#FFE0B2",
|
||||
"ratio": 1.0242122802870215,
|
||||
"pass": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"meta":{"name":"NumberSwipe 明亮教具室","version":"1.0-proposal","date":"2026-10-21","status":"Design specification; not applied to runtime","unit":"logical layout unit, not physical pixel","reference":[375,812]},
|
||||
"color":{
|
||||
"background":"#FFE69A","backgroundBottom":"#FFDE79","pattern":"#E8BE54","stage":"#FFF6DF","surface":"#FFFBF2","surfaceMuted":"#F1E6D0","cellFace":"#FFF8EA","cellDepth":"#D8C4A0","cellBoundary":"#8A7660","cellBevel":"#FFFFFF",
|
||||
"ink":"#43382F","inkSecondary":"#71604F","primary":"#6552D9","primaryPressed":"#5140B7","primaryDepth":"#49379E","primarySoft":"#ECE7FF","primaryEdge":"#ADA0F1","onPrimary":"#FFFFFF",
|
||||
"coral":"#F57950","coralInk":"#4F2418","coralSoft":"#FFE0CF","danger":"#B93B46","dangerDepth":"#8D2934","dangerSoft":"#FBE4E5","dangerInk":"#862735",
|
||||
"hint":"#8B5E00","hintSoft":"#FFF0B8","hintCore":"#FFFFFF","hintCasing":"#43382F","hintRibbon":"#F49B75","hintRibbonDepth":"#C96E48","hintRibbonLight":"#FFF5DD","hintSpark":"#FFFDF0","success":"#2E6B52","successSoft":"#DEEFDC",
|
||||
"categoryBlueFace":"#CEE4FF","categoryBlueInk":"#245A92","categoryOrangeFace":"#FFE0B2","categoryOrangeInk":"#8A470E","wallInk":"#43382F","wallStripe":"#F7C74D","doorClosed":"#8B4B29","doorOpen":"#2E6B52","disabledFace":"#E9E1D3","disabledInk":"#7A7064","focus":"#5140B7","overlay":"#43382F"
|
||||
},
|
||||
"referenceMatch":{
|
||||
"version":"2-reference-cushion","authority":"User-supplied reference image; overrides earlier flat production proposal",
|
||||
"color":{"background":"#FFE383","backgroundBottom":"#FFDA68","pattern":"#EFBF4D","stage":"#FAEED6","surface":"#FBEFD9","cellFace":"#F9EDD5","cellDepth":"#CDB590","ink":"#51452E","inkSecondary":"#796644","primary":"#8178F4","primaryPressed":"#7369DF","primaryDepth":"#5750BA","primaryEdge":"#B7AAFF","coral":"#FF7946"},
|
||||
"pitchMax":104,"bridgeRatio":0.25,"cellDepthRatio":0.085,"cellDepthMax":7,"bevelRatio":0.085,"stageMargin":12,"buttonRadiusRatio":0.32,
|
||||
"conditionFrames":false,"endpointCorners":false,"categoryVisitedFrames":false
|
||||
},
|
||||
"font":{"numeric":"Nunito","cjk":"Noto Sans SC","fallback":"system-ui, -apple-system, Segoe UI, Microsoft YaHei, sans-serif","numericWeight":800,"bodyWeight":500,"labelWeight":600,"headingWeight":700},
|
||||
"type":{"display":{"size":32,"line":40,"weight":800},"pageTitle":{"size":24,"line":32,"weight":700},"section":{"size":18,"line":26,"weight":700},"body":{"size":16,"line":24,"weight":500},"button":{"size":16,"line":22,"weight":700},"label":{"size":14,"line":20,"weight":600},"caption":{"size":13,"line":18,"weight":500},"micro":{"size":12,"line":16,"weight":500}},
|
||||
"spacing":[4,8,12,16,20,24,32,40,48],
|
||||
"layout":{"referenceWidth":375,"referenceHeight":812,"margin":20,"compactMargin":16,"topbarHeight":48,"headerHeight":40,"statusHeight":28,"playfieldHeight":464,"feedbackHeight":40,"actionHeight":64,"actionGap":10,"tapTarget":48,"pitchPreferred":56,"pitchTargetMin":44,"pitchReviewBelow":44,"tileInsetRatio":0.045,"bridgeThicknessRatio":0.42,"stagePadding":16},
|
||||
"radius":{"small":8,"label":12,"button":18,"stage":28,"card":24,"sheet":28,"dialog":24,"cellRatio":0.18,"cellMin":6,"cellMax":12,"concaveMaxRatio":0.06,"hexMaxRatio":0.11},
|
||||
"material":{"light":"upper-left","cellDepthRatio":0.055,"cellDepthMin":2,"cellDepthMax":4,"pressedDepth":1,"cellBoundary":1.25,"bevelWidth":1,"gradientChannelDeltaMax":6,"cellShadow":{"x":0,"y":3,"blur":5,"alpha":0.10},"stageShadow":{"x":0,"y":4,"blur":10,"alpha":0.08},"buttonShadow":{"x":0,"y":3,"blur":5,"alpha":0.12},"dialogShadow":{"x":0,"y":12,"blur":28,"alpha":0.16}},
|
||||
"motionMs":{"fast":120,"base":200,"slow":320,"hold":500,"buttonPress":80,"buttonRelease":120,"step":140,"bridge":120,"undo":100,"conflict":200,"hintAttention":2400,"hintSegmentCycleMin":1200,"door":160,"spinner":160,"clearSweep":320,"clearHold":500,"entryStagger":12,"entryStaggerCap":120},
|
||||
"motion":{"easeOut":[0.22,1,0.36,1],"stepPeak":1.015,"stepPressed":0.985,"buttonPressed":0.98,"conflictTravelMax":1.5,"spinnerDegrees":120,"ambientAnimation":false,"reducedMotionHint":"static dashed path; keep all hint information"},
|
||||
"contrastPairs":[
|
||||
{"name":"标题/黄底","fg":"ink","bg":"background","minimum":4.5},
|
||||
{"name":"辅助文字/黄底","fg":"inkSecondary","bg":"background","minimum":4.5},
|
||||
{"name":"正文/舞台","fg":"ink","bg":"stage","minimum":4.5},
|
||||
{"name":"辅助文字/舞台","fg":"inkSecondary","bg":"stage","minimum":4.5},
|
||||
{"name":"数字/未走块","fg":"ink","bg":"cellFace","minimum":4.5},
|
||||
{"name":"白数字/路径","fg":"onPrimary","bg":"primary","minimum":4.5},
|
||||
{"name":"白字/按压紫","fg":"onPrimary","bg":"primaryPressed","minimum":4.5},
|
||||
{"name":"白数字/错误","fg":"onPrimary","bg":"danger","minimum":4.5},
|
||||
{"name":"错误说明/淡红","fg":"dangerInk","bg":"dangerSoft","minimum":4.5},
|
||||
{"name":"珊瑚标签文字","fg":"coralInk","bg":"coral","minimum":4.5},
|
||||
{"name":"淡紫选中文字","fg":"primaryPressed","bg":"primarySoft","minimum":4.5},
|
||||
{"name":"提示文字/淡黄","fg":"hint","bg":"hintSoft","minimum":4.5},
|
||||
{"name":"成功文字/淡绿","fg":"success","bg":"successSoft","minimum":4.5},
|
||||
{"name":"蓝类别标记","fg":"categoryBlueInk","bg":"categoryBlueFace","minimum":4.5},
|
||||
{"name":"橙类别标记","fg":"categoryOrangeInk","bg":"categoryOrangeFace","minimum":4.5},
|
||||
{"name":"数字/蓝类别面","fg":"ink","bg":"categoryBlueFace","minimum":4.5},
|
||||
{"name":"数字/橙类别面","fg":"ink","bg":"categoryOrangeFace","minimum":4.5},
|
||||
{"name":"格边/格面","fg":"cellBoundary","bg":"cellFace","minimum":3},
|
||||
{"name":"格边/舞台","fg":"cellBoundary","bg":"stage","minimum":3},
|
||||
{"name":"路径/未走格","fg":"primary","bg":"cellFace","minimum":3},
|
||||
{"name":"提示描边/舞台","fg":"hintCasing","bg":"stage","minimum":3},
|
||||
{"name":"提示白芯/描边","fg":"hintCore","bg":"hintCasing","minimum":3},
|
||||
{"name":"墙/黄色条纹","fg":"wallInk","bg":"wallStripe","minimum":3},
|
||||
{"name":"墙/舞台","fg":"wallInk","bg":"stage","minimum":3},
|
||||
{"name":"开门标记/舞台","fg":"doorOpen","bg":"stage","minimum":3},
|
||||
{"name":"关门标记/舞台","fg":"doorClosed","bg":"stage","minimum":3},
|
||||
{"name":"焦点/黄底","fg":"focus","bg":"background","minimum":3},
|
||||
{"name":"白字/珊瑚(仅诊断,不采用)","fg":"onPrimary","bg":"coral","minimum":null},
|
||||
{"name":"格面/舞台(仅诊断,格边承担识别)","fg":"cellFace","bg":"stage","minimum":null},
|
||||
{"name":"蓝类别面/橙类别面(仅诊断,另配形状)","fg":"categoryBlueFace","bg":"categoryOrangeFace","minimum":null}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {readFileSync,writeFileSync} from 'node:fs';
|
||||
const tokens=JSON.parse(readFileSync(new URL('tokens.json',import.meta.url),'utf8'));
|
||||
function luminance(hex){
|
||||
if(!/^#[0-9a-f]{6}$/i.test(hex))throw new Error(`Expected opaque sRGB hex: ${hex}`);
|
||||
const c=[1,3,5].map(i=>parseInt(hex.slice(i,i+2),16)/255).map(v=>v<=0.04045?v/12.92:((v+0.055)/1.055)**2.4);
|
||||
return .2126*c[0]+.7152*c[1]+.0722*c[2];
|
||||
}
|
||||
const resolve=k=>tokens.color[k]??k;
|
||||
const rows=tokens.contrastPairs.map(p=>{
|
||||
const foreground=resolve(p.fg),background=resolve(p.bg),a=luminance(foreground),b=luminance(background);
|
||||
const ratio=(Math.max(a,b)+.05)/(Math.min(a,b)+.05);
|
||||
return {...p,foreground,background,ratio,pass:p.minimum===null?null:ratio>=p.minimum};
|
||||
});
|
||||
writeFileSync(new URL('contrast-audit.json',import.meta.url),JSON.stringify({method:'WCAG relative luminance, opaque sRGB; threshold checked without rounding; does not validate all composed/animated UI states',rows},null,2)+'\n');
|
||||
console.table(rows.map(r=>({pair:r.name,foreground:r.foreground,background:r.background,ratio:r.ratio.toFixed(3),minimum:r.minimum,result:r.pass===null?'diagnostic':r.pass?'pass':'FAIL'})));
|
||||
if(rows.some(r=>r.pass===false))process.exitCode=1;
|
||||
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"method": "WCAG relative luminance, opaque sRGB; threshold checked without rounding; does not validate all composed/animated UI states",
|
||||
"rows": [
|
||||
{
|
||||
"name": "主文字/背景",
|
||||
"fg": "ink",
|
||||
"bg": "background",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#292638",
|
||||
"background": "#F7F5F1",
|
||||
"ratio": 13.499592723055425,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "辅助文字/背景",
|
||||
"fg": "inkSecondary",
|
||||
"bg": "background",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#66616F",
|
||||
"background": "#F7F5F1",
|
||||
"ratio": 5.50340046530515,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "三级文字/背景",
|
||||
"fg": "inkTertiary",
|
||||
"bg": "background",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#716A78",
|
||||
"background": "#F7F5F1",
|
||||
"ratio": 4.7797069926956475,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "线索数字/未走格",
|
||||
"fg": "ink",
|
||||
"bg": "cellFace",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#292638",
|
||||
"background": "#E5E3E1",
|
||||
"ratio": 11.484041400281836,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "路径数字/紫面",
|
||||
"fg": "onPrimary",
|
||||
"bg": "primary",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#6558F5",
|
||||
"ratio": 4.920632349356288,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "路径数字/按压紫面",
|
||||
"fg": "onPrimary",
|
||||
"bg": "primaryPressed",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#5245D6",
|
||||
"ratio": 6.601647105487607,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "错误数字/错误面",
|
||||
"fg": "onPrimary",
|
||||
"bg": "danger",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#C74350",
|
||||
"ratio": 4.830782166769728,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "错误说明/淡红底",
|
||||
"fg": "dangerInk",
|
||||
"bg": "dangerSoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#8B2632",
|
||||
"background": "#FBEAED",
|
||||
"ratio": 7.473414279853196,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "提示文字/淡琥珀底",
|
||||
"fg": "hintInk",
|
||||
"bg": "hintSoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#664400",
|
||||
"background": "#FFF0D2",
|
||||
"ratio": 7.79606369057448,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "格边/格面",
|
||||
"fg": "cellBoundary",
|
||||
"bg": "cellFace",
|
||||
"minimum": 3,
|
||||
"foreground": "#817B88",
|
||||
"background": "#E5E3E1",
|
||||
"ratio": 3.2030630920005154,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "格边/背景",
|
||||
"fg": "cellBoundary",
|
||||
"bg": "background",
|
||||
"minimum": 3,
|
||||
"foreground": "#817B88",
|
||||
"background": "#F7F5F1",
|
||||
"ratio": 3.765229129807595,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "提示环/背景",
|
||||
"fg": "hint",
|
||||
"bg": "background",
|
||||
"minimum": 3,
|
||||
"foreground": "#966000",
|
||||
"background": "#F7F5F1",
|
||||
"ratio": 4.857982445311365,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "提示环/未走格",
|
||||
"fg": "hint",
|
||||
"bg": "cellFace",
|
||||
"minimum": 3,
|
||||
"foreground": "#966000",
|
||||
"background": "#E5E3E1",
|
||||
"ratio": 4.132663308317279,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "墙身/墙条纹",
|
||||
"fg": "wallInk",
|
||||
"bg": "wallStripe",
|
||||
"minimum": 3,
|
||||
"foreground": "#3B3547",
|
||||
"background": "#D7B35B",
|
||||
"ratio": 5.872010474102764,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "墙身/未走格",
|
||||
"fg": "wallInk",
|
||||
"bg": "cellFace",
|
||||
"minimum": 3,
|
||||
"foreground": "#3B3547",
|
||||
"background": "#E5E3E1",
|
||||
"ratio": 9.1851898632995,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "焦点/表面",
|
||||
"fg": "focus",
|
||||
"bg": "surface",
|
||||
"minimum": 3,
|
||||
"foreground": "#6558F5",
|
||||
"background": "#FFFEFC",
|
||||
"ratio": 4.881824820806739,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "选中项文字/淡紫底",
|
||||
"fg": "selectedInk",
|
||||
"bg": "primarySoft",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#5245D6",
|
||||
"background": "#EDEAFF",
|
||||
"ratio": 5.600093119280275,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "辅助文字/二级面",
|
||||
"fg": "inkSecondary",
|
||||
"bg": "surfaceMuted",
|
||||
"minimum": 4.5,
|
||||
"foreground": "#66616F",
|
||||
"background": "#EEEBE6",
|
||||
"ratio": 5.039815678579112,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "已走线索环/路径面",
|
||||
"fg": "primarySoft",
|
||||
"bg": "primary",
|
||||
"minimum": 3,
|
||||
"foreground": "#EDEAFF",
|
||||
"background": "#6558F5",
|
||||
"ratio": 4.174109721683276,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "未走格面/背景(诊断,边界另保证)",
|
||||
"fg": "cellFace",
|
||||
"bg": "background",
|
||||
"minimum": null,
|
||||
"foreground": "#E5E3E1",
|
||||
"background": "#F7F5F1",
|
||||
"ratio": 1.1755088868561658,
|
||||
"pass": null
|
||||
},
|
||||
{
|
||||
"name": "旧错误色白字(诊断)",
|
||||
"fg": "#FFFFFF",
|
||||
"bg": "#E5484D",
|
||||
"minimum": null,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#E5484D",
|
||||
"ratio": 3.913738950339162,
|
||||
"pass": null
|
||||
},
|
||||
{
|
||||
"name": "旧过关亮面白字(诊断)",
|
||||
"fg": "#FFFFFF",
|
||||
"bg": "#7D72FF",
|
||||
"minimum": null,
|
||||
"foreground": "#FFFFFF",
|
||||
"background": "#7D72FF",
|
||||
"ratio": 3.6694552685051627,
|
||||
"pass": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"meta": {"name":"NumberSwipe Soft Blocks", "version":"1.0-proposal", "date":"2026-10-21", "status":"Design proposal; not applied to runtime", "unit":"logical design unit; map per platform, not physical pixel"},
|
||||
"color": {
|
||||
"background":"#F7F5F1", "surface":"#FFFEFC", "surfaceRaised":"#FFFFFF", "surfaceMuted":"#EEEBE6",
|
||||
"divider":"#D5D0C9", "cellFace":"#E5E3E1", "cellDepth":"#C5C0CB", "cellBoundary":"#817B88",
|
||||
"ink":"#292638", "inkSecondary":"#66616F", "inkTertiary":"#716A78", "onPrimary":"#FFFFFF",
|
||||
"primary":"#6558F5", "primaryPressed":"#5245D6", "primaryDepth":"#4B3AC8", "primarySoft":"#EDEAFF", "selectedInk":"#5245D6", "primaryEdgeLight":"#A49BFF",
|
||||
"danger":"#C74350", "dangerDepth":"#9E2F3A", "dangerSoft":"#FBEAED", "dangerInk":"#8B2632",
|
||||
"hint":"#966000", "hintSoft":"#FFF0D2", "hintInk":"#664400",
|
||||
"wallInk":"#3B3547", "wallStripe":"#D7B35B", "focus":"#6558F5", "overlay":"#292638"
|
||||
},
|
||||
"font": {"numericPreferred":"Nunito", "cjkPreferred":"Noto Sans SC", "fallback":"ui-rounded, system-ui, -apple-system, Segoe UI, sans-serif", "numericWeight":800, "bodyWeight":500, "labelWeight":600, "headingWeight":700},
|
||||
"type": {
|
||||
"display":{"size":30,"line":38,"weight":800}, "pageTitle":{"size":24,"line":32,"weight":700}, "section":{"size":18,"line":26,"weight":700},
|
||||
"body":{"size":16,"line":24,"weight":500}, "button":{"size":16,"line":24,"weight":700}, "label":{"size":14,"line":20,"weight":600},
|
||||
"caption":{"size":13,"line":18,"weight":500}, "micro":{"size":12,"line":16,"weight":500}
|
||||
},
|
||||
"boardType":{"startRatio":0.60,"singleRatio":0.52,"doubleRatio":0.46,"parityRatio":0.44,"paritySubMin":12,"doubleMin":22,"singleMin":24,"numberMax":46},
|
||||
"spacing":[4,8,12,16,20,24,32,40,48],
|
||||
"radius":{"small":8,"control":16,"card":20,"sheet":24,"cellRatio":0.18,"cellMin":7,"cellMax":14},
|
||||
"layout":{"referenceWidth":375,"referenceHeight":812,"contentPadding":20,"tapMin":48,"topbarHeight":56,"actionHeight":56,"actionGap":8,"boardOuterMargin":12,"boardPitchMin":48,"boardPitchPreferred":56,"tileInsetRatio":0.05,"bridgeThicknessRatio":0.46},
|
||||
"material":{"lightDirection":"upper-left","cellDepth":2,"cellDepthMax":3,"pressedDepth":1,"boundaryWidth":1.25,"shadowBlurMax":6,"shadowOpacityMax":0.08,"gradientLuminanceDeltaMax":0.02},
|
||||
"motionMs":{"fast":120,"base":200,"slow":320,"hold":500,"hintVisible":2400,"entryStagger":12,"entryStaggerCap":120},
|
||||
"motion":{"easeOut":[0.22,1,0.36,1],"easeIn":[0.4,0,1,1],"easeInOut":[0.65,0,0.35,1],"stepScalePeak":1.02,"buttonScaleMin":0.98,"shakeAmplitudeMax":2,"rotationDegrees":120},
|
||||
"contrastPairs":[
|
||||
{"name":"主文字/背景","fg":"ink","bg":"background","minimum":4.5},
|
||||
{"name":"辅助文字/背景","fg":"inkSecondary","bg":"background","minimum":4.5},
|
||||
{"name":"三级文字/背景","fg":"inkTertiary","bg":"background","minimum":4.5},
|
||||
{"name":"线索数字/未走格","fg":"ink","bg":"cellFace","minimum":4.5},
|
||||
{"name":"路径数字/紫面","fg":"onPrimary","bg":"primary","minimum":4.5},
|
||||
{"name":"路径数字/按压紫面","fg":"onPrimary","bg":"primaryPressed","minimum":4.5},
|
||||
{"name":"错误数字/错误面","fg":"onPrimary","bg":"danger","minimum":4.5},
|
||||
{"name":"错误说明/淡红底","fg":"dangerInk","bg":"dangerSoft","minimum":4.5},
|
||||
{"name":"提示文字/淡琥珀底","fg":"hintInk","bg":"hintSoft","minimum":4.5},
|
||||
{"name":"格边/格面","fg":"cellBoundary","bg":"cellFace","minimum":3},
|
||||
{"name":"格边/背景","fg":"cellBoundary","bg":"background","minimum":3},
|
||||
{"name":"提示环/背景","fg":"hint","bg":"background","minimum":3},
|
||||
{"name":"提示环/未走格","fg":"hint","bg":"cellFace","minimum":3},
|
||||
{"name":"墙身/墙条纹","fg":"wallInk","bg":"wallStripe","minimum":3},
|
||||
{"name":"墙身/未走格","fg":"wallInk","bg":"cellFace","minimum":3},
|
||||
{"name":"焦点/表面","fg":"focus","bg":"surface","minimum":3},
|
||||
{"name":"选中项文字/淡紫底","fg":"selectedInk","bg":"primarySoft","minimum":4.5},
|
||||
{"name":"辅助文字/二级面","fg":"inkSecondary","bg":"surfaceMuted","minimum":4.5},
|
||||
{"name":"已走线索环/路径面","fg":"primarySoft","bg":"primary","minimum":3},
|
||||
{"name":"未走格面/背景(诊断,边界另保证)","fg":"cellFace","bg":"background","minimum":null},
|
||||
{"name":"旧错误色白字(诊断)","fg":"#FFFFFF","bg":"#E5484D","minimum":null},
|
||||
{"name":"旧过关亮面白字(诊断)","fg":"#FFFFFF","bg":"#7D72FF","minimum":null}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
# 数字一笔:美术风格与商业验证方案
|
||||
|
||||
日期:2026-10-21。平台:小游戏、Google Play、App Store。目的:决定第一版值得投入的美术方向,不修改当前游戏皮肤。
|
||||
|
||||
## 1. 推荐
|
||||
|
||||
**首选:现代休闲益智的清晰几何风,加入轻微哑光积木质感。**
|
||||
|
||||
具体是暖白或浅灰背景、清楚的大数字、圆角拼块、单条鲜明路径、很浅的厚度和局部触摸反馈。保持正俯视二维布局,质感通过边缘/短阴影/按压实现,不需要建 3D 场景。
|
||||
|
||||
第二个测试方向是**当前清晰扁平版的精修**;浅木桌游作为备选。重卡通场景、角色化关卡、持续霓虹特效暂不优先。
|
||||
|
||||
这是基于当前玩法、已有工程、制作成本和素材测试便利度的投资排序,**不是已证明某风格收入最高**。没有取得竞品按美术方案拆分的收入、留存、买量成本或因果实验。
|
||||
|
||||
默认商业定位:免费体验、广泛成年休闲用户、短局逻辑解谜,后续考虑广告与去广告;没有假设特定性别、年龄段、国家或已有买量预算。若改成儿童产品、重 IP 或精品买断,应重新评估。
|
||||
|
||||
## 2. 证据如何获得
|
||||
|
||||
本轮读取项目 `docs/design.md`,并在浏览器实际查看官方页面的图标、宣传图和可见截图:1LINE/X-FLOW、Flow Free、Number Sums、Woodoku、LYNE;另核对 Flow Free 的 App Store 页面与苹果/Google 的测试工具说明。
|
||||
|
||||
App Store 的 Flow Free 截图在本轮浏览器中未加载成功,因此不把空白占位当作美术证据;其商店说明可核对“干净矢量图形与动画”。没有安装竞品或声称观察了其真实游戏运行帧率、手感和全部动效。
|
||||
|
||||
当前工程已有暖/冷灰、蓝紫路径、红色冲突、琥珀提示、粗数字和圆角块规范。本轮工作建议做**受控的增量试验**。仓库已有其他开发中的微信构建和关卡变更,本次不修改这些文件,也不继续沿用早期“45 关就是当前全部内容”的判断。
|
||||
|
||||
## 3. 竞品风格观察
|
||||
|
||||
下载档位是官方页面累计档位,不等于当前活跃或付费规模。评分随地区/设备/抓取可能不同,本次不据小数分差排序。
|
||||
|
||||
| 样本 | 实际可见美术语言 | 公开规模信号 | 对本项目的含义 |
|
||||
|---|---|---|---|
|
||||
| 1LINE / X-FLOW | 白底、点线图形、紫色几何图标;截图通过不同颜色图形和短标题说明学习/挑战/提示,部分线条有轻微体积感 | Google Play 5,000 万+下载 | 极简几何能清楚传递一笔画;轻质感可以存在,但不遮住任务。[官方页](https://play.google.com/store/apps/details?hl=en_US&id=com.one1line.onetouch.onestroke.dotgame) |
|
||||
| Flow Free | 黑底、明亮多色管线、细网格;图标直接表现同色路径 | Google Play 1 亿+下载;iOS 有大量评分,跨平台长期运营 | 高对比几何有成熟需求;它的多色承载配对规则,不意味着本项目也要多色路线。[Google Play](https://play.google.com/store/apps/details?hl=en&id=com.bigduckgames.flow)、[App Store](https://apps.apple.com/us/app/flow-free/id526641427?platform=ipad) |
|
||||
| Number Sums / Easybrain | 白底数字、低饱和区域色;商店前部截图讲玩法,后部展示蓝色活动/日历及更丰富奖励画面 | Google Play 500 万+下载 | 盘面可克制,外层活动可丰富;不是所有地方都必须“极简”或都必须“卡通”。[Google Play](https://play.google.com/store/apps/details?hl=en_US&id=com.easybrain.number.sums.puzzle)、[App Store](https://apps.apple.com/sg/app/number-sums-math-games/id6449427809) |
|
||||
| Woodoku / Tripledot | 暖棕木纹、木块厚度、凹槽盘面;宣传截图还有放大积木、路径箭头、分数/皇冠式包装 | Google Play 1 亿+下载 | 木质风是有规模的有效参照,不能简单说过时;但它是块消除,对本项目的数字和异形边界可能增加噪声。[官方页](https://play.google.com/store/apps/details?id=com.tripledot.woodoku) |
|
||||
| LYNE / Thomas Bowker | 柔和绿色/米色品牌图、细字和抽象几何;氛围安静,依赖图形组织 | 开发者售卖页面;本轮未取得当前移动收入 | 可借鉴放松气质,不以其审美直接推断小游戏广告变现表现。[作者页面](https://thomasbowker.itch.io/lyne) |
|
||||
|
||||
**能支持:** 几何极简、深色高对比、木质实体三种方向都有真实产品落地。**不能支持:** 浅色一定优于深色、木质一定 CPM 高、App Store 必须做艺术风、国内用户必须要角色。成功产品的发行、内容、历史积累、投放和广告策略都混在结果中。
|
||||
|
||||
## 4. 方向比较与投资排序
|
||||
|
||||
这是策划判断,不是实验评分;不编造 CTR、CPI 或收益提升比例。
|
||||
|
||||
| 方向 | 可用优势 | 本项目风险 | 制作/维护负担 | 建议 |
|
||||
|---|---|---|---|---|
|
||||
| 清晰扁平几何 | 数字与边界最清楚,资产少,接近现有工程 | 图标/截图若太普通,难形成记忆 | 最低 | A/B 基准,资源极少时也可直接首发 |
|
||||
| **柔和哑光积木** | 保留逻辑清晰,增加可触摸感;能用同套形状覆盖矩形、异形、六边 | 阴影过大就像分裂成小块;整体太淡会损失对比 | 低到中 | **最值得先做样板** |
|
||||
| 浅木桌游 | 亲切、实体玩具联想,已有大规模相邻品类 | 数字印在木纹上、转墙融入木色、异形接缝难读 | 中 | 第二轮备选或后续主题 |
|
||||
| 黑底高对比/霓虹 | 路径缩略图醒目,可做夜间主题 | 持续外发光会糊掉数字/共边,且视觉不必然代表反应速度玩法 | 低到中 | 可测深色主题,别把强光效当核心 |
|
||||
| 纸笔/手账 | 逻辑谜题认知清楚,有手工温度 | 线稿弱、数字细、纸纹/涂鸦增加噪声 | 中 | 精品主题储备 |
|
||||
| 重卡通/角色场景 | 有机会做情感记忆和活动内容 | 当前没有角色循环,投入增加却未必帮助理解一笔画 | 高 | 暂不做核心盘面路线 |
|
||||
|
||||
“柔和”指材质与背景克制,不是降低数字/墙/状态对比。“轻质感”也不是玻璃模糊、强镜面、软到看不清的黏土。
|
||||
|
||||
## 5. 给美术的可执行 Brief
|
||||
|
||||
### 5.1 盘面
|
||||
|
||||
- 正俯视,单位格与大格边界保持准确。一个 `2×2` 大格必须像一整块,不能画出四枚可单独点击的小格。
|
||||
- 未走块:浅灰哑光面,轻微底边厚度;路径:连续、饱满的蓝紫。大块与小块使用同一材质。
|
||||
- 起点、数字线索、已走步号维持不同状态层级;不要所有数字都加发光或立体描边。
|
||||
- 留出能看清共边和桥的缝隙。阴影只帮助确认块的边缘,不能制造“有桥/有路”的假象。
|
||||
- 六边版本保留同样材质、路径和字形;静态墙/转墙仍必须在共边上,转轴与墙身要能分辨。不能因为美化把墙变成格子里的装饰。
|
||||
- 相机不倾斜,手指与路径末端保持一致;不为“3D 感”改变输入和视觉坐标关系。
|
||||
|
||||
### 5.2 建议配色候选
|
||||
|
||||
以下仅用于视觉样板,不替换现行令牌:
|
||||
|
||||
| 用途 | 候选 | 原则 |
|
||||
|---|---|---|
|
||||
| 背景 | 暖白 `#F7F5F1` 或冷白 `#F7F8FC` | 两个候选只选一个,不每关变色 |
|
||||
| 未走格 | `#E5E3E1` 一类中性浅灰 | 不能与背景融成一片 |
|
||||
| 路径 | 先保留现有 `#6558F5` | 用统一主色建立品牌,颜色不是唯一胜负因素 |
|
||||
| 正文/线索 | 先保留现有深墨 `#23243A` | 数字清楚、等宽、粗细稳定 |
|
||||
| 冲突/提示 | 保留红/琥珀语义 | 用描边、标记或运动补充信息,不只靠色相 |
|
||||
|
||||
大字号、圆角和阴影都应按实际最小屏幕验证。候选色没有在此完成全部状态的对比度验收,不能仅凭色值表认定可发布。
|
||||
|
||||
### 5.3 动效与声音
|
||||
|
||||
优先投资手指附近的落块/填色反馈、连接生长、到达线索的轻确认、可恢复的冲突反馈、转墙方向与过关收束。过关应让玩家看清“整条路完成”,不让庆祝遮掉解。
|
||||
|
||||
短按压与很小位移可营造实体感;不要让已走块持续漂浮、弹跳,也不要在思考阶段持续播放粒子。遵守项目已有动效档位,若样板需要新时长,要先改规范而不是各处自行设置。
|
||||
|
||||
### 5.4 图标与商店包装
|
||||
|
||||
图标表达“数字+一条路+大块”中的两个识别点即可。建议先做两个方向:
|
||||
|
||||
- 几何玩法图标:少数粗数字与一条连贯路径,突出大块一步。
|
||||
- 哑光积木图标:相同内容,加入轻厚度与按压光感。
|
||||
|
||||
先用一个图标说明主玩法;不把矩形、六边、转墙、吉祥物、中文长名同时塞进小方块。蓝紫应与已上线游戏实际颜色匹配,轮廓在小尺寸下先清楚。
|
||||
|
||||
商店前三张素材分别讲:①一条路走遍;②大格也只一步;③数字决定路线。之后再展示六边/转墙、提示/内容等已实现功能。不要放未实现的活动地图或假计时器来提升画面丰富度。
|
||||
|
||||
宣传图可加品牌标题和放大的真实玩法局部,但正式上传应针对具体资产类型/平台查最新规格;不能把宣传海报、手机截图、视频封面当成同一种素材要求。
|
||||
|
||||
## 6. 三个平台如何使用同一风格
|
||||
|
||||
| 平台/入口 | 建议重点 | 保留的统一部分 |
|
||||
|---|---|---|
|
||||
| 小游戏 | 短视频里让手势和结果很快可读;首屏直接展示可理解盘面;在真实运行包验证加载与触摸 | 图形、路径色、数字、墙/错误语义一致 |
|
||||
| Google Play | 图标和截图缩略图有清楚轮廓;本地化短标题;用商店实验验证而非靠审美猜测 | 同一游戏皮肤,按市场制作文案与素材构图 |
|
||||
| App Store | 用清晰排版与真实盘面表达玩法,保持图标、预览和游戏内体验一致;测试不同展示重点 | 同一材质与玩法,不额外开发“iOS 专属高级风” |
|
||||
|
||||
这些是针对展示场景的制作建议,不是已经证明的平台审美差异。小游戏暂按微信/抖音等短入口讨论,未取得其同类游戏当前视觉转化数据。
|
||||
|
||||
两套/三套完全不同的核心皮肤会扩大 QA 和维护成本。本项目更适合一套可扩展系统加平台化商店素材。
|
||||
|
||||
## 7. 先验证什么,怎样决定投入
|
||||
|
||||
### 7.1 第一轮样板
|
||||
|
||||
做 A 清晰扁平精修、B 柔和积木两个真实游戏样板,使用同一关卡、相同数字/路径和相同文案;第三个浅木方向只做快速备选,不立即重绘全部 UI。
|
||||
|
||||
先看读盘:未走/已走是否清楚,大格是否被误认为多格,转墙是否像装饰,冲突和提示是否容易分辨。再看触摸:更换阴影和材质后,是否误以为邻接或可走区域发生变化。
|
||||
|
||||
样板至少覆盖:一个矩形大格关、一个异形关、一个奇偶关、一个六边墙关、一个转墙关,以及选关和过关。不能只在最简单的一关看起来好看。
|
||||
|
||||
### 7.2 分开测试获客包装与游戏皮肤
|
||||
|
||||
**素材测试:** 相同玩法视频节奏、关卡难度、文案、目标人群与投放设置下,只比较图标或一种包装变量。先看点击/安装转化,再追踪进入游戏和完成早期内容的用户质量。
|
||||
|
||||
**产品测试:** 同期同来源用户随机分配皮肤,保持关序、提示、广告频率相同。观察规则理解、早期完成、误触、回退原因、D1/D7 留存及每安装的收入/回收表现。有足够数据再判断商业优劣。
|
||||
|
||||
商店测试影响“看到素材后愿不愿意安装”;它不能单独证明“进入游戏后更喜欢该皮肤”。不同时改界面、教程、关卡和广告再归因到美术。
|
||||
|
||||
Google 官方提供 store listing experiments 测试图形/文案;Apple 提供 product page optimization 测试图标、截图与预览。不同平台、国家、语言与渠道的结果分别看,不把一种流量的获胜率直接推广到全部市场。[Google 官方](https://support.google.com/googleplay/android-developer/answer/12053285?hl=en)、[Apple 官方](https://developer.apple.com/app-store/product-page-optimization/)
|
||||
|
||||
不预设“跑三天/几百人就是赢家”。先确定希望检测的差异、当前基线、样本需求与结束规则;流量不够就保留“未分胜负”。尤其不要只因为点击更高,就忽视留存或误触变差。
|
||||
|
||||
### 7.3 资源投入顺序
|
||||
|
||||
1. 可读性与大格/墙的辨识。
|
||||
2. 路径完成、按压、冲突、转墙的手感与动效。
|
||||
3. 一个有辨识度的图标和首张商店素材。
|
||||
4. 多玩法一致的壳与选关界面。
|
||||
5. 有数据后再做木质/深色主题、装饰与收藏。
|
||||
6. 若有明确情感循环需求,再评估吉祥物和角色资产。
|
||||
|
||||
若现在只够做一套:选柔和积木;若连一次皮肤样板的资源也很紧,优先精修现有扁平版并做好商店素材。推荐不是要求先重构美术才能上架。
|
||||
|
||||
## 8. 本轮交付边界
|
||||
|
||||
已完成:官方视觉素材观察、五个代表样本比较、风格投资排序、跨平台制作 Brief、验证方案,以及同一真实关卡的三种可切换原创概念。
|
||||
|
||||
未完成:最终美术资产、游戏皮肤改造、广告投放、商店上传、真实用户 A/B、收入预测。概念视图中的按钮只演示本地视觉状态,不是完整游戏,也不等于已验证其商业转化。
|
||||
|
||||
本项目当前规范:[design.md](design.md)。若采纳候选后实施,按该文件约定同步样张、CSS/theme 及相关检查,不在本轮调研中直接覆盖。
|
||||
@@ -0,0 +1,2 @@
|
||||
.font-sources/
|
||||
.font-tools/
|
||||
@@ -1,5 +1,62 @@
|
||||
2026-10-22 数字和六边形优化:通道一位数按格宽36%、两位数按30%绘制,上限分别28/25逻辑像素;调整通道及数字牌间距,小屏按比例缩放以避免越界。双色玩法移除圆/三角角标,数字在方块中央,保留奇偶规则文字。六边形厚度1.2–2.2像素,投影透明度0.09,减小偏移、倒角暗部及暖色外晕。发布包:releases/NumberSwipe-campaign-200-r2-明亮教具室-大数字双色轻六边版-20261022.zip。
|
||||
|
||||
2026-10-22 指路重新设计:参考任天堂官方《灵活脑学校 一起伸展大脑》玩法画面中的奶油色承托、珊瑚橙强调和清楚的方向标识,结合本游戏圆润积木材质,自行程序化绘制奶油色圆边+浅珊瑚色教学引导带。三个圆角方向标沿圆弧移动,带短柔光尾;终点有贴纸式目标环;减少动态保持静态方向。已走方块、规则文字继续避让。参考图片仅用于分析,未作为游戏资源。连接桥没有改动。
|
||||
|
||||
参考:https://www.nintendo.com/hk/switch/azlsa/stretch/index.html
|
||||
官方示例:https://www.nintendo.com/hk/switch/azlsa/assets/img/stretch/genre5-img3.png
|
||||
当前包:releases/NumberSwipe-campaign-200-r2-明亮教具室-教具指路设计版-20261022.zip。
|
||||
|
||||
2026-10-22 更新:连接桥缩短到格子边缘,仅保留1.2逻辑像素衔接;指路合并连续单元路径,并用二次曲线采样圆滑转角。光点及渐隐拖尾沿同一条曲线按弧长向目标移动,过弯保持平稳速度。减少动态模式保持静态指引。新增圆弧拼接与断段隔离回归测试。当前包:releases/NumberSwipe-campaign-200-r2-明亮教具室-短桥圆弧流光版-20261022.zip。
|
||||
|
||||
本轮连接桥与指路精修(2026-10-21):连接桥去掉端部封口、端部倒角和实体厚边,只有侧面保留柔和倒角,两端延伸并渐隐到已走块体内。指路改为暖金色光带、浅金高光、移动光点和方向箭头;减少动态模式显示静态虚线及箭头。提示继续独立绘制并避让已走方块和规则文字。
|
||||
|
||||
验收:34项模块测试通过;200关模拟微信触摸回归通过;浏览器200关提示前后及撤销重画的玩家填色像素一致。新增截图:qa/bright-classroom-bridge-v4.png、qa/bright-classroom-hint-v4.png。发布包:releases/NumberSwipe-campaign-200-r2-明亮教具室-连接桥与指路精修版-20261021.zip(位于项目根目录)。
|
||||
# 数字一笔 · campaign-200-r2 微信小游戏
|
||||
|
||||
## 明亮教具室程序化美术(2026-10-21)
|
||||
|
||||
### 投影、数字与按钮精修(v3,当前版本)
|
||||
|
||||
根据参考图再次区分两类效果:棋块外沿使用黄棕色近边晕染+短投影,底盘则没有实体下沿,只由实际面轮廓alpha投出软阴影。底盘暗部改为淡暖色,避免灰黑底沿看起来像第二块垫子。
|
||||
|
||||
数字以真实字形边界定位;原生Canvas若只返回文字宽度,会通过一次离屏alpha测量获得字形边界。普通格数字在视觉中心,大块/异形数字位于左上占用单元区域,输入中心与规则不变。白字增加短暗边和柔光,深色数字使用淡渐变与固定微颗粒;生成缓存不作为外部图片资源。
|
||||
|
||||
按钮重新调整实体图标轮廓、视觉尺寸与文字留白,短标签使用随包的站酷快乐体改名子集 `NumberSwipePlayful.ttf`,字体许可见 `fonts/zcoolkuaile-OFL.txt`。规则和正文继续用Noto Sans SC。当前有5个字体子集、3份许可;字体源及输出哈希见manifest。
|
||||
|
||||
33项模块测试、200关模拟触摸与200关浏览器提示像素检查通过。新版包为 `../releases/NumberSwipe-campaign-200-r2-明亮教具室-投影字效精修版-20261021.zip`;微信手机的显示、字体和生成材质耗时仍需新包真机预览。
|
||||
|
||||
以下为历史修订记录。
|
||||
|
||||
### 参考图圆润材质修订(v2)
|
||||
|
||||
按用户随后提供的效果图重做主游戏区:浅紫 `#8178F4`、暖奶油 `#F9EDD5`、金黄背景;移除固定数字框、四角框、双色访问内框和常驻块轮廓线。现在使用 `src/bright-classroom/cushion.mjs` 从几何距离场生成柔光倒角、下缘暗部和软投影,所有缓存像素均在运行时生成,未引入图片资源。按钮使用同一圆润材质和实心玩具图标;桥接缩细,简单盘面按可用面积放大。
|
||||
|
||||
本次用户参考图优先于早期报告的薄描边与低渐变建议,对应参数集中在设计令牌 `referenceMatch`。密集盘的小号白数字仍采用较深紫面。规则所需的墙、通道、类别符号继续保留,操作和关卡不变。
|
||||
|
||||
新版导入包:`../releases/NumberSwipe-campaign-200-r2-明亮教具室-参考图圆润版-20261021.zip`。以下为第一版说明及历史记录;当前执行30项模块测试。
|
||||
|
||||
当前 `game.js` 已应用暖黄背景、奶油舞台、轻厚度块体、紫色路径、程序图标和圆角按钮。方形、大块异形、六边、通道、双色、奇偶与墙门共用新渲染器;规则教学同步换皮。所有游戏美术均由Canvas路径生成,不使用图片资源。字体为随包提供的Nunito数字800与Noto Sans SC中文500/600/700子集,许可证、字集和SHA-256见 `fonts/`。
|
||||
|
||||
200关内容、顺序、原规则、微信存档key、逐格回退、撤销、分段提示及手动下一关保持原行为。双色已走保留蓝橙面与圆/三角;提示使用真实共边和块内引导,遮罩避让玩家块面与关键标注。新增选关页“设置”中的减少动态开关,保存于本地;静态页面和无动画的思考阶段停止RAF。
|
||||
|
||||
直接导入本目录可运行,或解压 `../releases/NumberSwipe-campaign-200-r2-明亮教具室-20261021.zip` 后导入。包含 `game.js`、两个配置文件、`audio/`、`fonts/`,无需构建npm。不要使用旧二维码判断新皮肤是否生效,应重新编译并生成预览。
|
||||
|
||||
开发预览:在本目录执行 `node qa/serve.mjs`,打开 [明亮教具室试玩](http://127.0.0.1:17859/qa/bright-classroom.html),可切换全部200关和五种屏幕尺寸;画前几步/完成本关是验收页工具,正式游戏不提供自动解题按钮。
|
||||
|
||||
验证命令:
|
||||
|
||||
```powershell
|
||||
node build.mjs
|
||||
node --test bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs
|
||||
node smoke.mjs
|
||||
```
|
||||
|
||||
29项模块测试通过;模拟微信200关唯一解与触摸通关通过,覆盖教学隔离、四种手机尺寸、减少动态存档、静态停止绘制和字体装载调用。浏览器200关已走格采样像素在提示前后及撤销重画后保持一致。自动检查与浏览器预览不代表微信手机实测;仍需手机验收字体加载、密集关手势、音频与帧率。
|
||||
|
||||
美术接入在 `bright-classroom-build.mjs` 与 `src/bright-classroom/`,上游四份快照未修改。重建依赖本仓库的 `docs/design/bright-classroom/tokens.json` 与原型内esbuild安装。字体无需在普通构建时重做;补充字集时用 `build-fonts.py`,读取 `.font-sources/` 的上游字体(构建专用),生成改名子集及manifest。来源:[Nunito](https://github.com/google/fonts/tree/main/ofl/nunito)、[Noto Sans SC](https://github.com/google/fonts/tree/main/ofl/notosanssc),许可随资源保留。
|
||||
|
||||
以下为此前版本的变更记录。
|
||||
|
||||
本包源于 G:\项目\NumberSwipe-Variants\campaign-200-r2,版本 scored-2026-10-15-r2。完整保留原版 200 关及 playOrder,不混入原 NumberSwipe 项目关卡,也不重新生成关卡。
|
||||
|
||||
## 导入
|
||||
@@ -48,4 +105,4 @@ validation.json:200/200 唯一解及模拟微信触摸通关,撤销/重开/
|
||||
|
||||
提示使用独立透明 Canvas,每帧只清空提示层,并从中擦除玩家已经走过的格子区域;已画的连接段不再叠加提示。玩家棋盘及整格填色、数字、方向反馈继续由原绘制代码负责,双色关保留原有蓝/橙格色。撤销、重开会按实际路线恢复相应提示。
|
||||
|
||||
构建:node build.mjs。检查:node --test hints.test.mjs lessons.test.mjs audio-copy.test.mjs(23项),node smoke.mjs(200关,并比较点击指路前后玩家填色数据)。浏览器像素回归:node qa/serve.mjs 后打开 http://127.0.0.1:17859/qa/hints.html,检查200关前两步的已走格填色采样像素在开启提示、撤销再画前后相同;qa/hint-player-fill.png 为浏览器检查截图,不能作为微信真机截图。独立导入包通过 ./package.ps1 生成,只含运行必需文件。真机显示仍需手机预览确认。
|
||||
构建:node build.mjs。检查:node --test hints.test.mjs lessons.test.mjs audio-copy.test.mjs(23项),node smoke.mjs(200关,并比较点击指路前后玩家填色数据)。浏览器像素回归:node qa/serve.mjs 后打开 http://127.0.0.1:17859/qa/hints.html,检查200关前两步的已走格填色采样像素在开启提示、撤销再画前后相同;qa/hint-player-fill.png 为浏览器检查截图,不能作为微信真机截图。独立导入包通过 ./package.ps1 生成,只含运行必需文件。真机显示仍需手机预览确认。
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import fs from 'node:fs';import path from 'node:path';
|
||||
// 与 r2 的半音上行、其余五类提示音采用相同的频率和时序。
|
||||
export function buildAudio(root){const out=path.join(root,'audio');fs.mkdirSync(out,{recursive:true});const cues={correct:[[659,.09,0],[880,.14,.075]],error:[[330,.10,0],[247,.15,.09]],undo:[[587,.07,0],[440,.10,.055]],restart:[[523,.075,0],[392,.09,.065],[523,.11,.14]],complete:[[523,.12,0],[659,.12,.10],[784,.14,.20],[1046,.25,.32]]};for(let n=1;n<=36;n++)cues['move-'+n]=[[523*Math.pow(2,(n-1)/12),.07,0]];
|
||||
for(const [name,tones]of Object.entries(cues)){const rate=22050,length=Math.ceil((Math.max(...tones.map(([,d,t])=>t+d))+.03)*rate),b=Buffer.alloc(44+length*2);b.write('RIFF');b.writeUInt32LE(b.length-8,4);b.write('WAVEfmt ',8);b.writeUInt32LE(16,16);b.writeUInt16LE(1,20);b.writeUInt16LE(1,22);b.writeUInt32LE(rate,24);b.writeUInt32LE(rate*2,28);b.writeUInt16LE(2,32);b.writeUInt16LE(16,34);b.write('data',36);b.writeUInt32LE(length*2,40);for(let i=0;i<length;i++){const t=i/rate;let sample=0;for(const [f,d,offset]of tones){const u=t-offset;if(u>=0&&u<d){const gain=name==='error'?.24:.32;const env=u<.008?u/.008:Math.pow(.0001/gain,(u-.008)/(d-.008));sample+=Math.sin(2*Math.PI*f*u)*gain*env;}}b.writeInt16LE(Math.round(Math.max(-1,Math.min(1,sample))*32767),44+i*2);}fs.writeFileSync(path.join(out,name+'.wav'),b);}}
|
||||
// 原版音效保持不变;路径点新增同音色的短C大调庆祝音。
|
||||
export function buildAudio(root){const out=path.join(root,'audio');fs.mkdirSync(out,{recursive:true});const cues={checkpoint:[[523,.065,0],[784,.38,.05],[392,.25,.065]],correct:[[659,.09,0],[880,.14,.075]],error:[[330,.10,0],[247,.15,.09]],undo:[[587,.07,0],[440,.10,.055]],restart:[[523,.075,0],[392,.09,.065],[523,.11,.14]],complete:[[392,.10,0],[494,.10,.075],[587,.13,.15],[523,.65,.26],[659,.55,.28],[784,.48,.30],[196,.18,0,.28],[261.5,.22,.075,.25],[392,.24,.15,.25]]};for(let n=1;n<=36;n++)cues['move-'+n]=[[523*Math.pow(2,(n-1)/12),.07,0]];
|
||||
for(const [name,tones]of Object.entries(cues)){const rate=22050,length=Math.ceil((Math.max(...tones.map(([,d,t])=>t+d))+.03)*rate),b=Buffer.alloc(44+length*2);b.write('RIFF');b.writeUInt32LE(b.length-8,4);b.write('WAVEfmt ',8);b.writeUInt32LE(16,16);b.writeUInt16LE(1,20);b.writeUInt16LE(1,22);b.writeUInt32LE(rate,24);b.writeUInt32LE(rate*2,28);b.writeUInt16LE(2,32);b.writeUInt16LE(16,34);b.write('data',36);b.writeUInt32LE(length*2,40);for(let i=0;i<length;i++){const t=i/rate;let sample=0;for(const [f,d,offset,weight=1]of tones){const u=t-offset;if(u>=0&&u<d){const themed=name==='checkpoint'||name==='complete',gain=themed?.20:name==='error'?.24:.32;
|
||||
const env=u<.008?u/.008:Math.pow(.0001/gain,(u-.008)/(d-.008));
|
||||
sample+=(Math.sin(2*Math.PI*f*u)+(themed?.08*Math.sin(4*Math.PI*f*u)*Math.exp(-u/.05):0))*gain*env*weight;}}b.writeInt16LE(Math.round(Math.max(-1,Math.min(1,sample))*32767),44+i*2);}fs.writeFileSync(path.join(out,name+'.wav'),b);}}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';import {CasualAudio,stopAudio,audioDiagnostics} from './src/audio.mjs';import {tutorials,rulesFor,playerText} from './src/copy.mjs';
|
||||
const root=path.dirname(fileURLToPath(import.meta.url));
|
||||
function setup(){const played=[],contexts=[],storage=new Map(),options=[];globalThis.wx={setInnerAudioOption:o=>options.push(o),showToast:()=>{},createInnerAudioContext:()=>{const a={stops:0,stop(){this.stops++;},play(){assert.ok(fs.existsSync(path.join(root,this.src)));played.push(this.src);},onError(fn){this.error=fn;}};contexts.push(a);return a;}};const sound=new CasualAudio({}, {getItem:k=>storage.get(k),setItem:(k,v)=>storage.set(k,v)},'sound');return {sound,played,contexts,storage,options};}
|
||||
test('手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和五类反馈音',()=>{const h=setup();for(let step=1;step<=36;step++)h.sound.play('move',0,step);for(const kind of ['correct','error','undo','restart','complete'])h.sound.play(kind);assert.equal(h.played.length,41);assert.equal(h.contexts.length,4);assert.ok(h.options.every(o=>o.obeyMuteSwitch===false));assert.ok(h.contexts.every(a=>a.obeyMuteSwitch===false));});
|
||||
function setup(){const played=[],contexts=[],storage=new Map(),options=[];globalThis.wx={setInnerAudioOption:o=>options.push(o),showToast:()=>{},createInnerAudioContext:()=>{const a={stops:0,stop(){this.stops++;},play(){assert.ok(fs.existsSync(path.join(root,this.src)));played.push(this.src);},onError(fn){this.error=fn;},onPlay(fn){this.playing=fn;}};contexts.push(a);return a;}};const sound=new CasualAudio({}, {getItem:k=>storage.get(k),setItem:(k,v)=>storage.set(k,v)},'sound');return {sound,played,contexts,storage,options};}
|
||||
test('手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和六类反馈音',()=>{const h=setup();for(let step=1;step<=36;step++)h.sound.play('move',0,step);for(const kind of ['correct','error','undo','restart','complete','checkpoint'])h.sound.play(kind);assert.equal(h.played.length,42);assert.equal(h.contexts.length,4);assert.ok(h.options.every(o=>o.obeyMuteSwitch===false));assert.ok(h.contexts.every(a=>a.obeyMuteSwitch===false));});
|
||||
test('关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器',()=>{const h=setup();h.sound.play('move');h.sound.toggle();const count=h.played.length;h.sound.play('move');assert.equal(h.played.length,count);assert.equal(h.storage.get('sound'),'off');h.sound.toggle();assert.equal(h.played.at(-1),'audio/correct.wav');assert.equal(h.storage.get('sound'),'on');const stops=h.contexts.map(a=>a.stops);stopAudio();assert.ok(h.contexts.every((a,i)=>a.stops>stops[i]));});
|
||||
test('延迟通关音在静音或进入后台时取消,不在返回后突然播放',async()=>{const h=setup();h.sound.play('complete',.02);h.sound.toggle();await new Promise(resolve=>setTimeout(resolve,40));assert.equal(h.played.length,0);});
|
||||
test('音频错误可诊断,不再静默吞掉',()=>{const h=setup();h.sound.unlock();h.contexts[0].error({errCode:10003,errMsg:'test missing audio'});assert.equal(audioDiagnostics.lastError.errCode,10003);});
|
||||
test('所有音频均为非静音 PCM;第 13 步比第 1 步高一个八度',()=>{const crossings=[];for(const file of fs.readdirSync(path.join(root,'audio'))){const b=fs.readFileSync(path.join(root,'audio',file));assert.equal(b.toString('ascii',0,4),'RIFF');assert.equal(b.readUInt32LE(24),22050);assert.equal(b.readUInt16LE(22),1);let energy=0,z=0;for(let i=44;i<b.length;i+=2){const x=b.readInt16LE(i);energy+=x*x;if(i>44&&x>=0&&b.readInt16LE(i-2)<0)z++;}assert.ok(energy>100000,file+' must not be silent');if(file==='move-1.wav')crossings[0]=z;if(file==='move-13.wav')crossings[1]=z;}assert.ok(Math.abs(crossings[1]/crossings[0]-2)<.1);});
|
||||
test('所有音频均为非静音 PCM;原版第13步比第1步高一个八度',()=>{
|
||||
const crossings=[];for(const file of fs.readdirSync(path.join(root,'audio'))){const b=fs.readFileSync(path.join(root,'audio',file));assert.equal(b.toString('ascii',0,4),'RIFF');assert.equal(b.readUInt32LE(24),22050);assert.equal(b.readUInt16LE(22),1);let energy=0,z=0;for(let i=44;i<b.length;i+=2){const x=b.readInt16LE(i);energy+=x*x;if(i>44&&x>=0&&b.readInt16LE(i-2)<0)z++;}assert.ok(energy>100000,file+' must not be silent');if(file==='move-1.wav')crossings[0]=z;if(file==='move-13.wav')crossings[1]=z;}assert.ok(Math.abs(crossings[1]/crossings[0]-2)<.1);
|
||||
});
|
||||
test('教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语',()=>{const levels=JSON.parse(fs.readFileSync(path.join(root,'src/upstream/levels.json'))).levels;for(const l of levels){const text=rulesFor(l);assert.ok(!/共边|落格|访问步号/.test(text),l.id);assert.ok(text.includes('每个格子只能经过一次'));}assert.ok(tutorials.basic.includes('不能斜着走'));assert.ok(tutorials.parity.includes('1、3、5'));assert.ok(tutorials['rhythm-door'].includes('起点也算一步'));assert.ok(!playerText('认清异形共边').includes('共边'));});
|
||||
|
||||
test('过关提示同步使用实际播放起点,非请求时间',()=>{const h=setup();h.sound.play('complete');assert.equal(audioDiagnostics.completeStartedAt,null);const requested=audioDiagnostics.completeRequestedAt;h.contexts.find(c=>c.src==='audio/complete.wav').playing();assert.ok(audioDiagnostics.completeStartedAt>=requested);});
|
||||
|
||||
test('过关和弦收尾持续衰减,不在后半段重新起音',()=>{
|
||||
const b=fs.readFileSync(path.join(root,'audio/complete.wav')),rate=b.readUInt32LE(24),rms=[];
|
||||
for(let t=.4;t<.8;t+=.1){let energy=0,n=0;for(let i=Math.floor(t*rate);i<Math.min(Math.floor((t+.1)*rate),(b.length-44)/2);i++){const v=b.readInt16LE(44+i*2);energy+=v*v;n++;}rms.push(Math.sqrt(energy/n));}
|
||||
for(let i=1;i<rms.length;i++)assert.ok(rms[i]<rms[i-1],rms.join(','));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Checked adapter: upstream snapshots remain byte-for-byte unchanged.
|
||||
export function applyBrightClassroom(source) {
|
||||
const start=source.indexOf('function draw(){');
|
||||
const end=source.indexOf('function roundedRect(',start);
|
||||
if(start<0||end<0||source.indexOf('function draw(){',start+1)>=0)throw Error('Bright Classroom: upstream draw seam changed');
|
||||
const original=source.slice(start,end);
|
||||
const tail=original.slice(original.indexOf(' if(previewOnly)return;updateTutorial(bad);'));
|
||||
if(!tail.startsWith(' if(previewOnly)return;'))throw Error('Bright Classroom: UI writeback seam changed');
|
||||
const draw=`function draw(){
|
||||
if(!level)return;const width=canvas.clientWidth,height=canvas.clientHeight;if(!width||!height)return;
|
||||
const dpr=window.devicePixelRatio||1;
|
||||
if(canvas.width!==Math.round(width*dpr))canvas.width=Math.round(width*dpr);
|
||||
if(canvas.height!==Math.round(height*dpr))canvas.height=Math.round(height*dpr);
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
brightScene=renderBrightBoard(ctx,level,path,width,height,{preview:previewOnly});
|
||||
({polygons,centers,unit}=brightScene);channelViews=brightScene.channels;cellViews=brightScene.views;
|
||||
const bad=violations(level,path);
|
||||
${tail}`;
|
||||
source=source.slice(0,start)+draw+source.slice(end);
|
||||
const checkpointCue="sound.play(cell.clue!==undefined&&cell.clue===path.length?'correct':'move',0,path.length)";
|
||||
if(!source.includes(checkpointCue))throw Error('Checkpoint audio seam changed');
|
||||
source=source.replace(checkpointCue,"if(path.length<level.cells.length)sound.play(cell.clue!==undefined&&cell.clue===path.length?(cell.clue>1?'checkpoint':'correct'):'move',0,path.length)");
|
||||
if(!source.includes("sound.play('complete',.23)"))throw Error('Complete audio timing seam changed');
|
||||
source=source.replace("sound.play('complete',.23)","sound.play('complete',0)");
|
||||
const hint='visitedPolygons:polygons.filter(p=>path.includes(p.id)).map(p=>p.p.map(v=>[...v]))';
|
||||
if(!source.includes(hint))throw Error('Bright Classroom: hint seam changed');
|
||||
source=source.replace(hint,`${hint},...hintGeometry(brightScene,path,h.route)`);
|
||||
// Preview rendering must restore every reference used by the live hint layer.
|
||||
source=source.replace('const saved={canvas,ctx,level,solution,path,polygons,centers,unit,focus,cleared,channelViews,cellViews};','const saved={canvas,ctx,level,solution,path,polygons,centers,unit,focus,cleared,channelViews,cellViews,brightScene};');
|
||||
source=source.replace('({canvas,ctx,level,solution,path,polygons,centers,unit,focus,cleared,channelViews,cellViews}=saved)','({canvas,ctx,level,solution,path,polygons,centers,unit,focus,cleared,channelViews,cellViews,brightScene}=saved)');
|
||||
source=`import {renderBrightBoard,hintGeometry,drawBarriers} from '../bright-classroom/board-renderer.mjs';\nlet brightScene=null;\n`+source;
|
||||
source+=`
|
||||
export function refreshBrightBoard(now){
|
||||
if(!brightScene?.animating)return;
|
||||
const dpr=window.devicePixelRatio||1;ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
brightScene=renderBrightBoard(ctx,level,path,canvas.clientWidth,canvas.clientHeight,{now});
|
||||
}
|
||||
export function brightOverlay(target){if(brightScene)drawBarriers(target,brightScene,path);}
|
||||
export function brightAnimating(){return Boolean(brightScene?.animating);}
|
||||
`;
|
||||
return source;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
import {outlines,buildGeometry,interiorGuide,edgeFor,distance,inside} from './src/bright-classroom/geometry.mjs';
|
||||
import {renderBrightBoard,hintGeometry} from './src/bright-classroom/board-renderer.mjs';
|
||||
import {solve,units} from './src/upstream/rules.mjs';
|
||||
import {applyBrightClassroom} from './bright-classroom-build.mjs';
|
||||
import {C,label} from './src/bright-classroom/theme.mjs';
|
||||
import {cushion} from './src/bright-classroom/cushion.mjs';
|
||||
const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url)));
|
||||
const area=p=>p.reduce((n,a,i)=>{const b=p[(i+1)%p.length];return n+a[0]*b[1]-b[0]*a[1];},0)/2;
|
||||
function context(){const ctx=new Proxy({measureText:s=>({width:String(s).length*(Number(/([\d.]+)px/.exec(ctx.font)?.[1])||16)*.6})},{get:(o,k)=>o[k]??(()=>{}),set:(o,k,v)=>(o[k]=v,true)});return ctx;}
|
||||
|
||||
test('merged irregular outlines retain holes and never draw internal unit edges',()=>{
|
||||
const donut=[];for(let y=0;y<3;y++)for(let x=0;x<3;x++)if(x!==1||y!==1)donut.push([x,y]);
|
||||
const rings=outlines(donut);assert.equal(rings.length,2);assert.equal(rings.reduce((n,p)=>n+area(p),0),8);assert.ok(rings.some(p=>area(p)<0));
|
||||
for(const l of pack.levels.filter(l=>l.geometry!=='hex'))for(const c of l.cells){const u=units(c),r=outlines(u);assert.equal(r.reduce((n,p)=>n+area(p),0),u.length,l.id+':'+c.id);}
|
||||
});
|
||||
|
||||
test('all 200 boards fit four screen budgets with stable safe anchors and real shared entrances',()=>{
|
||||
for(const [w,h]of [[320,220],[375,313],[390,442],[414,494]])for(const level of pack.levels){
|
||||
const scene=buildGeometry(level,w,h);assert.ok(scene.pitch<=104*Math.min(1,w/375)+.001);
|
||||
for(const g of scene.cells){assert.ok(g.parts.some(p=>inside(g.center,p)),level.id+':'+g.cell.id);assert.ok(g.parts.some(p=>inside(g.labelAnchor,p)),'label must stay on its block '+level.id+':'+g.cell.id);for(const [x,y]of g.parts.flat()){assert.ok(x>=scene.margin-1&&x<=w-scene.margin+1,level.id);assert.ok(y>=0&&y<=h,level.id);}}
|
||||
for(const e of scene.edges.values()){const [a,b]=e.segments[0];assert.ok(distance(a,b)>0);assert.ok(Math.abs(distance(a,e.entry)+distance(e.entry,b)-distance(a,b))<.001);}
|
||||
}
|
||||
});
|
||||
|
||||
test('guides through every irregular block stay inside its occupied shape',()=>{
|
||||
for(const level of pack.levels.filter(l=>l.cells.some(c=>c.shape))){
|
||||
const scene=buildGeometry(level,375,400);
|
||||
for(const g of scene.cells.filter(g=>g.cell.shape))for(const edge of scene.edges.values()){
|
||||
if(edge.a!==g.cell.id&&edge.b!==g.cell.id)continue;
|
||||
const guide=interiorGuide(g,g.center,edge.entry,scene);assert.ok(guide.length>=2);
|
||||
for(let i=1;i<guide.length;i++)for(let t=0;t<=1;t+=.1){const a=guide[i-1],b=guide[i],p=[a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t];assert.ok(g.parts.some(poly=>p[0]>=poly[0][0]-.001&&p[0]<=poly[2][0]+.001&&p[1]>=poly[0][1]-.001&&p[1]<=poly[2][1]+.001),level.id+':'+g.cell.id);}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('visited, conflicting and hinted category cells retain their material identity',()=>{
|
||||
const level=pack.levels.find(l=>l.features.includes('alternation')&&l.features.includes('parity')),route=solve(level)[0],ctx=context();
|
||||
for(const path of [[],route.slice(0,4),route.slice(0,4).reverse(),route]){
|
||||
const scene=renderBrightBoard(ctx,level,path,375,400),before=JSON.stringify(scene.views);
|
||||
const hint=hintGeometry(scene,path,route.slice(0,4));
|
||||
assert.equal(JSON.stringify(scene.views),before);assert.equal(hint.visitedShapes.length,path.length);
|
||||
for(const view of scene.views){const c=level.cells.find(c=>c.id===view.cell);assert.equal(view.fill,c.category==='circle'?C.categoryBlueFace:C.categoryOrangeFace);}
|
||||
}
|
||||
});
|
||||
|
||||
test('five font subsets match their manifest and fit the package budget',()=>{
|
||||
const manifest=JSON.parse(fs.readFileSync(new URL('./fonts/manifest.json',import.meta.url)));
|
||||
assert.equal(manifest.outputs.length,5);
|
||||
for(const item of manifest.outputs){const bytes=fs.readFileSync(new URL('./fonts/'+item.file,import.meta.url));assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'),item.sha256);assert.equal(bytes.length,item.bytes);}
|
||||
assert.ok(manifest.outputs.reduce((n,o)=>n+o.bytes,0)<500000);
|
||||
});
|
||||
|
||||
test('upstream adapter fails closed if the inspected render seams change',()=>{
|
||||
assert.throws(()=>applyBrightClassroom('function draw(){}'),/seam changed/);
|
||||
assert.throws(()=>applyBrightClassroom('function draw(){foo()} function roundedRect(){}'),/writeback seam changed/);
|
||||
});
|
||||
|
||||
test('procedural cushion pixels preserve holes and light the upper bevel without a border',()=>{
|
||||
const before=globalThis.wx,images=[];
|
||||
globalThis.wx={createCanvas:()=>({width:0,height:0,getContext:()=>new Proxy({createImageData:(w,h)=>({width:w,height:h,data:new Uint8ClampedArray(w*h*4)}),putImageData:image=>images.push(image)},{get:(o,k)=>o[k]??(()=>{})})})};
|
||||
try{
|
||||
const outer=[[0,0],[61,0],[61,61],[0,61]],hole=[[20,20],[20,41],[41,41],[41,20]],original=JSON.stringify([outer,hole]);
|
||||
assert.ok(cushion(context(),[outer,hole],5,{face:'#8178F4',depthColor:'#5750BA',depth:5,bevel:6,shadow:3,resolution:1}));
|
||||
const image=images.at(-1),pixel=(x,y)=>Array.from(image.data.slice((y*image.width+x)*4,(y*image.width+x)*4+4));
|
||||
assert.equal(pixel(31,31)[3],0,'hole must remain transparent');
|
||||
assert.equal(pixel(10,10)[3],255,'occupied face must remain opaque');
|
||||
assert.ok(pixel(10,2)[0]>pixel(10,59)[0],'upper bevel must be lighter than lower bevel');
|
||||
assert.equal(JSON.stringify([outer,hole]),original,'material must not mutate logical rings');
|
||||
}finally{if(before===undefined)delete globalThis.wx;else globalThis.wx=before;}
|
||||
});
|
||||
|
||||
test('visible glyph bounds are centered independently of font ascent and side bearings',()=>{
|
||||
let draw;
|
||||
const ctx={measureText:()=>({width:28,actualBoundingBoxLeft:2,actualBoundingBoxRight:20,actualBoundingBoxAscent:30,actualBoundingBoxDescent:2}),fillText:(value,x,y)=>draw={value,x,y}};
|
||||
label(ctx,'1',100,200,48,C.onPrimary,800,true);
|
||||
assert.equal(draw.x+(20-2)/2,100);
|
||||
assert.equal(draw.y+(2-30)/2,200);
|
||||
const level=pack.levels.find(l=>l.id===pack.playOrder[3]),scene=buildGeometry(level,375,450),big=scene.cells.find(g=>g.cell.w===2&&g.cell.h===2);
|
||||
assert.notDeepEqual(big.labelAnchor,big.center,'reference large-block label is separate from logical input center');
|
||||
const first=big.parts[0];assert.ok(inside(big.labelAnchor,first));
|
||||
});
|
||||
|
||||
test('stage with zero depth casts alpha shadows without painting a solid backing',()=>{
|
||||
const before=globalThis.wx,events=[];
|
||||
globalThis.wx={createCanvas:()=>({width:0,height:0,getContext:()=>new Proxy({createImageData:(w,h)=>({width:w,height:h,data:new Uint8ClampedArray(w*h*4)}),fill:()=>events.push('solid'),drawImage:()=>events.push('image')},{get:(o,k)=>o[k]??(()=>{})})})};
|
||||
try{
|
||||
assert.ok(cushion(context(),[[[0,0],[62,0],[62,80],[0,80]]],8,{face:C.stage,depthColor:'#E9D5A9',depth:0,bevel:6,shadow:12,shadowY:4,resolution:1}));
|
||||
assert.equal(events.filter(e=>e==='solid').length,0,'no duplicated yellow slab below the stage');
|
||||
assert.ok(events.filter(e=>e==='image').length>=2,'face alpha is reused for a soft cast shadow');
|
||||
}finally{if(before===undefined)delete globalThis.wx;else globalThis.wx=before;}
|
||||
});
|
||||
|
||||
test('native width-only TextMetrics still centers the rasterized glyph ink',()=>{
|
||||
const before=globalThis.wx;let draw;
|
||||
globalThis.wx={createCanvas(){
|
||||
const canvas={width:0,height:0};let originX=0,baseline=0;
|
||||
canvas.getContext=()=>({setTransform(){},fillText(text,x,y){originX=x;baseline=y;},getImageData(){
|
||||
const data=new Uint8ClampedArray(canvas.width*canvas.height*4);
|
||||
for(let y=(baseline-18)*2;y<(baseline-2)*2;y++)for(let x=(originX+2)*2;x<(originX+10)*2;x++)data[(y*canvas.width+x)*4+3]=255;
|
||||
return {data};
|
||||
}});return canvas;
|
||||
}};
|
||||
try{
|
||||
const ctx={measureText:()=>({width:25}),fillText:(text,x,y)=>draw={x,y}};
|
||||
label(ctx,'8',100,200,29,C.ink,800,true);
|
||||
assert.equal(draw.x+6,100);assert.equal(draw.y-10,200);
|
||||
}finally{if(before===undefined)delete globalThis.wx;else globalThis.wx=before;}
|
||||
});
|
||||
|
||||
test('fixed channels remain anchored at their block centers after number enlargement',()=>{
|
||||
for(const level of pack.levels.filter(l=>l.cells.some(c=>c.direction))){
|
||||
const path=solve(level)[0],scene=renderBrightBoard(context(),level,path,320,300);
|
||||
for(const channel of scene.channels)assert.deepEqual(channel.center,scene.byId.get(channel.cell).center,level.id+':'+channel.cell);
|
||||
}
|
||||
});
|
||||
|
||||
test('checkpoint ribbons trigger on correct arrival only and never duplicate per frame',()=>{
|
||||
const level=pack.levels.find(l=>l.cells.some(c=>c.clue>1&&c.clue<l.cells.length)),route=solve(level)[0],n=level.cells.find(c=>c.clue>1&&c.clue<level.cells.length).clue,ctx=context();
|
||||
renderBrightBoard(ctx,level,route.slice(0,n-1),375,400,{now:1000});
|
||||
let scene=renderBrightBoard(ctx,level,route.slice(0,n),375,400,{now:1100});
|
||||
assert.equal(scene.bursts.length,1);assert.equal(scene.animating,true);
|
||||
scene=renderBrightBoard(ctx,level,route.slice(0,n),375,400,{now:1200});assert.equal(scene.bursts.length,1);
|
||||
scene=renderBrightBoard(ctx,level,route.slice(0,n),375,400,{now:2800});assert.equal(scene.bursts.length,0);
|
||||
const wrong=context();renderBrightBoard(wrong,level,[],375,400,{now:1000});
|
||||
scene=renderBrightBoard(wrong,level,[route[n-1]],375,400,{now:1100});assert.equal(scene.bursts.length,0);
|
||||
});
|
||||
|
||||
test('checkpoint pulse is small, centered in time and settles without looping',async()=>{
|
||||
const {checkpointScale}=await import('./src/bright-classroom/board-renderer.mjs');
|
||||
assert.equal(checkpointScale(0),1);assert.equal(checkpointScale(180),1.035);assert.equal(checkpointScale(360),1);assert.equal(checkpointScale(1000),1);assert.equal(checkpointScale(180,true),1);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Offline font subset builder. Sources and fonttools are build-only, not shipped.
|
||||
|
||||
Run with fonttools on PYTHONPATH (e.g. .font-tools), from this directory.
|
||||
Source downloads: https://github.com/google/fonts/tree/main/ofl/{nunito,notosanssc}
|
||||
"""
|
||||
from pathlib import Path
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools import subset
|
||||
from fontTools.varLib.instancer import instantiateVariableFont
|
||||
|
||||
root = Path(__file__).resolve().parent
|
||||
corpus = ''.join(p.read_text(encoding='utf-8') for p in (root / 'src').rglob('*')
|
||||
if p.suffix in ('.mjs', '.json'))
|
||||
chars = ''.join(sorted(set(corpus + ''.join(chr(i) for i in range(32, 127)) + '✓→…!:·')))
|
||||
manifest = {'generator': 'fonttools', 'sources': [], 'outputs': [], 'characters': chars}
|
||||
for family, weights in [('notosanssc', [500, 600, 700]), ('nunito', [800])]:
|
||||
source = root / '.font-sources' / (family + '.ttf')
|
||||
manifest['sources'].append({'family': family, 'sha256': sha256(source.read_bytes()).hexdigest(),
|
||||
'url': 'https://github.com/google/fonts/tree/main/ofl/' + family})
|
||||
for weight in weights:
|
||||
font = TTFont(source)
|
||||
options = subset.Options()
|
||||
options.name_IDs = ['*']
|
||||
sub = subset.Subsetter(options=options)
|
||||
text = chars if family == 'notosanssc' else '0123456789'
|
||||
sub.populate(text=text)
|
||||
sub.subset(font)
|
||||
font = instantiateVariableFont(font, {'wght': weight}, inplace=True)
|
||||
name = ('NumberSwipeSans' if family == 'notosanssc' else 'NumberSwipeDigits') + str(weight)
|
||||
for record in font['name'].names:
|
||||
if record.nameID in (1, 2, 3, 4, 6, 16, 17):
|
||||
value = 'Regular' if record.nameID in (2, 17) else name
|
||||
record.string = value.encode(record.getEncoding(), errors='replace')
|
||||
target = root / 'fonts' / (name + '.ttf')
|
||||
font.save(target)
|
||||
cmap = font.getBestCmap()
|
||||
missing = sorted({c for c in text if not c.isspace() and ord(c) not in cmap})
|
||||
# Source fonts don't contain code symbols like private-use/non-printing chars;
|
||||
# all player-visible Han characters and digits must be present.
|
||||
critical = [c for c in missing if '\u4e00' <= c <= '\u9fff' or c.isdigit()]
|
||||
if critical:
|
||||
raise ValueError('Missing glyphs: ' + ''.join(critical))
|
||||
manifest['outputs'].append({'file': target.name, 'weight': weight,
|
||||
'sha256': sha256(target.read_bytes()).hexdigest(),
|
||||
'bytes': target.stat().st_size, 'glyphs': len(cmap)})
|
||||
print(target.name, target.stat().st_size)
|
||||
source = root / '.font-sources' / 'zcoolkuaile.ttf'
|
||||
font = TTFont(source)
|
||||
options = subset.Options()
|
||||
options.name_IDs = ['*']
|
||||
sub = subset.Subsetter(options=options)
|
||||
playful_chars = '重开撤销指路已走步还剩块第关0123456789 ·'
|
||||
sub.populate(text=playful_chars)
|
||||
sub.subset(font)
|
||||
for record in font['name'].names:
|
||||
if record.nameID in (1, 2, 3, 4, 6, 16, 17):
|
||||
record.string = ('Regular' if record.nameID in (2, 17) else 'NumberSwipePlayful').encode(record.getEncoding(), errors='replace')
|
||||
target = root / 'fonts' / 'NumberSwipePlayful.ttf'
|
||||
font.save(target)
|
||||
manifest['sources'].append({'family': 'zcoolkuaile', 'sha256': sha256(source.read_bytes()).hexdigest(),
|
||||
'url': 'https://github.com/google/fonts/tree/main/ofl/zcoolkuaile'})
|
||||
manifest['outputs'].append({'file': target.name, 'weight': 400, 'sha256': sha256(target.read_bytes()).hexdigest(),
|
||||
'bytes': target.stat().st_size, 'glyphs': len(font.getBestCmap()), 'characters': playful_chars})
|
||||
print(target.name, target.stat().st_size)
|
||||
(root / 'fonts' / 'manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
@@ -1,4 +1,5 @@
|
||||
import {buildAudio} from './audio-assets.mjs';
|
||||
import {applyBrightClassroom} from './bright-classroom-build.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -16,7 +17,7 @@ await build({ absWorkingDir: root, entryPoints: ['src/main.mjs'], outfile: 'game
|
||||
contents=contents.replace(" updateTutorial(bad);", " if(previewOnly)return;updateTutorial(bad);");
|
||||
const hintHandler=contents.split('\n').find(line=>line.startsWith("$('hint').onclick="));
|
||||
if(!hintHandler)throw Error('上游指路入口已变化');
|
||||
contents=contents.replace(hintHandler, `$('hint').onclick=()=>{record.hints++;focus=[];const segment=segmentHints.request(level,solution,path);setStatus(segment?'沿半透明光路连接两端;走偏可撤销或重开,正确到达后提示消失。':'已经完成。');draw();};`);
|
||||
contents=contents.replace(hintHandler, `$('hint').onclick=()=>{record.hints++;focus=[];const segment=segmentHints.request(level,solution,path);setStatus(segment?'沿引导带连接两端;走偏可撤销或重开,正确到达后提示消失。':'已经完成。');draw();};`);
|
||||
contents=contents.replace("function start(i,open=true){", "function start(i,open=true){segmentHints.clear();");
|
||||
contents=contents.replace("if(level.tutorial&&!bad.length&&!cleared)focus=", "if(false)focus=");
|
||||
contents+=`\nexport function hintView(){const h=segmentHints.view(level,solution,path);return h?{...h,visitedPolygons:polygons.filter(p=>path.includes(p.id)).map(p=>p.p.map(v=>[...v]))}:null;}\n`;
|
||||
@@ -32,8 +33,9 @@ export function renderLesson(target,l,route,w,h){
|
||||
if (!old)
|
||||
throw Error('上游导出入口变化,需要重新检查微信适配');
|
||||
contents = contents.replace(old, `$('export').onclick=()=>{saveRecord();exportRecords({version:pack.version,exportedAt:new Date().toISOString(),clearedIds:completed,records});setStatus('已保存试玩记录。');};`);
|
||||
contents=applyBrightClassroom(contents);
|
||||
return { contents: "import {PACK,document,window,localStorage,exportRecords,performance} from '../platform.mjs';\n" + contents, loader: 'js' };
|
||||
});
|
||||
} }] });
|
||||
console.log('Built campaign-200-r2: 200 levels');
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"generator": "fonttools",
|
||||
"sources": [
|
||||
{
|
||||
"family": "notosanssc",
|
||||
"sha256": "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/notosanssc"
|
||||
},
|
||||
{
|
||||
"family": "nunito",
|
||||
"sha256": "bb55a5ca5c2042335b3991af27c4d0705d0ef41cac6164ac737fd8f2a1e85207",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/nunito"
|
||||
},
|
||||
{
|
||||
"family": "zcoolkuaile",
|
||||
"sha256": "812a6fc1fe54b6d73a419245c32dfeba8aa33104d5be90d1cf6af082007cb71d",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/zcoolkuaile"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "NumberSwipeSans500.ttf",
|
||||
"weight": 500,
|
||||
"sha256": "a02c66351825f2af3e15de47e332d0dff11c60854ce37b0c0c8e2ae3de52aca5",
|
||||
"bytes": 121952,
|
||||
"glyphs": 503
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeSans600.ttf",
|
||||
"weight": 600,
|
||||
"sha256": "6bd45b2532ee4afed2ff8c1bbb43cac6cc8490983df8c7312a3b57aa53241a9f",
|
||||
"bytes": 121940,
|
||||
"glyphs": 503
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeSans700.ttf",
|
||||
"weight": 700,
|
||||
"sha256": "cf4834799e8c9277902654f90ca9b86165eb8afcf62e8ce08a3b0c33f09eba35",
|
||||
"bytes": 121972,
|
||||
"glyphs": 503
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeDigits800.ttf",
|
||||
"weight": 800,
|
||||
"sha256": "7ee176cbb7311f4f8e794fe660a7f58148479eba0528b9ec97563354b0360b79",
|
||||
"bytes": 4652,
|
||||
"glyphs": 10
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipePlayful.ttf",
|
||||
"weight": 400,
|
||||
"sha256": "a376431996b69449de38604a73d4fc7253092316bea8e9f1774b9ef768388c7b",
|
||||
"bytes": 4832,
|
||||
"glyphs": 26,
|
||||
"characters": "重开撤销指路已走步还剩块第关0123456789 ·"
|
||||
}
|
||||
],
|
||||
"characters": "\n !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~·×–“”…→✓、。一三上下不与且两个中为久么之也习了交亮从他以们会位住余作使例供依侧保信修候值偶允先入全六共关其具再冲准减几出分切划则刚别到前剩力加动包区单卡即原去又双反发变叠口另只可右号合同后向否启和哪唯喜器回园围固图在坐块域堵墙处备复多大太失奇好如始子字存学它完定察对导封小少尚就尽尾局峰工左差己已布并序应座开异式引弯当录形往径循微必态怎思恢恰想戏成或战所才打找把拍拐择拼持指按挑挡挨振换接推提撤播支收改放效教数整斜新方旁时是显替最有未束条来果枢标核格框桥楚横橙次欢正此步段每永求没沿法浏深混清渐游源滑点版状玩环现理生用画留略的盘直相看着知矩短确示离种积称移空穿突窗竖章端笔符第算类索红约纹纽线练终经绕继续综缺置者耗能脚自至色节花范荡落蓝行表被要观规视览角解计订认让记许设访评试该误请读调败资赖走起越趣跨路转载边达过近返还这进连退选逐通道那邻部都配里重链销错门闭问间随难需面音页顺须预频题颜馈验黄黑!,/:;?"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2018 The ZCOOL KuaiLe Project Authors (https://github.com/googlefonts/zcool-kuaile)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -47,3 +47,27 @@ test('parity is a segment anchor; colour is not; flow moves continuously across
|
||||
drawHint(ctx,{route:['a','b','c']},{a:[0,0],b:[100,0],c:[100,100]},50,300);
|
||||
assert.deepEqual(arcs.at(-3).slice(0,2),[50,0]);
|
||||
});
|
||||
|
||||
test('coral teaching ribbon preserves visited masks and offers direction in reduced motion',()=>{
|
||||
for(const reducedMotion of [false,true]){
|
||||
const strokes=[],fills=[],stack=[];
|
||||
const ctx=new Proxy({globalCompositeOperation:'source-over',save(){stack.push(this.globalCompositeOperation);},restore(){this.globalCompositeOperation=stack.pop();},stroke(){strokes.push({color:this.strokeStyle,mode:this.globalCompositeOperation});},fill(){fills.push(this.globalCompositeOperation);}},{get:(o,k)=>o[k]??(()=>{}),set:(o,k,v)=>(o[k]=v,true)});
|
||||
const path=['a'];
|
||||
drawHint(ctx,{route:['a','b'],guides:[{a:'a',b:'b',points:[[0,0],[100,0]]}],visitedShapes:[{rings:[[[0,0],[20,0],[20,20],[0,20]]],radius:3,depth:3,maskPad:4}],keepouts:[{x:40,y:0,w:12,h:15}],reducedMotion},{},50,300,path);
|
||||
assert.ok(strokes.some(s=>s.color==='#F49B75'));
|
||||
assert.ok(strokes.filter(s=>s.color==='#C96E48'&&s.mode==='source-over').length>=1,'directional chevron');
|
||||
assert.ok(fills.includes('destination-out'));
|
||||
assert.equal(ctx.globalCompositeOperation,'source-over');
|
||||
assert.deepEqual(path,['a']);
|
||||
}
|
||||
});
|
||||
|
||||
test('hint rounds shared guide junctions and keeps separate runs disconnected',async()=>{
|
||||
const {smoothHintRuns}=await import('./src/hints.mjs');
|
||||
const guides=[{a:'a',b:'b',points:[[0,0],[50,0]]},{a:'b',b:'c',points:[[50,0],[50,50]]},{a:'e',b:'f',points:[[100,0],[150,0]]}];
|
||||
const runs=smoothHintRuns(guides,10);
|
||||
assert.equal(runs.length,2);assert.deepEqual(runs[0][0],[0,0]);assert.deepEqual(runs[0].at(-1),[50,50]);
|
||||
assert.ok(!runs[0].some(p=>p[0]===50&&p[1]===0),'sharp junction removed');
|
||||
assert.ok(runs[0].some(p=>p[0]>40&&p[0]<50&&p[1]>0&&p[1]<10),'rounded transition');
|
||||
assert.deepEqual(runs[1],[[100,0],[150,0]]);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {makeLesson,lessonFrame,lessonKeys} from './src/lessons.mjs';import {violations,doorLatched,doorOpen} from './src/upstream/rules.mjs';
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {makeLesson,lessonFrame,lessonKeys,loopLessonTime} from './src/lessons.mjs';import {violations,doorLatched,doorOpen} from './src/upstream/rules.mjs';
|
||||
const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url)));
|
||||
for(const l of pack.levels.filter(l=>l.tutorial))test('实际教学关动画:'+l.tutorial,()=>{const lesson=makeLesson(pack,l.tutorial);assert.equal(lesson.level.id,l.id);assert.equal(violations(l,lesson.solution).length,0);for(let i=0;i<lesson.beats.length-1;i++){const frame=lessonFrame(lesson,(i+.5)*lesson.duration/(lesson.beats.length-1));assert.ok(frame.message.length);assert.ok(frame.route.every(id=>l.cells.some(c=>c.id===id)));if(violations(l,frame.route).length)assert.match(frame.message,/错误示例/);}if(['straight','vertical','corner','parity'].includes(l.tutorial))assert.ok(lesson.beats.some(b=>b.message?.includes('错误示例')));if(l.tutorial==='rhythm-door'){const states=lesson.beats.map(b=>[doorOpen(l.doors[0],b.route),doorLatched(l.doors[0],b.route)]);assert.ok(states.some(([open])=>!open));assert.ok(states.some(([,latched])=>latched));assert.equal(states.at(-1)[1],false);}});
|
||||
test('200关的组合规则均能找到对应真实教学关',()=>{for(const l of pack.levels)for(const key of lessonKeys(l))assert.ok(makeLesson(pack,key));});
|
||||
test('200关的组合规则均能找到对应真实教学关',()=>{for(const l of pack.levels)for(const key of lessonKeys(l))assert.ok(makeLesson(pack,key));});
|
||||
|
||||
test('整段演示一秒内连续完成,结束后停留并支持从头重播',()=>{for(const level of pack.levels.filter(l=>l.tutorial)){const lesson=makeLesson(pack,level.tutorial);assert.equal(lesson.duration,1000);for(let i=0;i<lesson.beats.length-1;i++){const time=(i+.4)*1000/(lesson.beats.length-1);const f=lessonFrame(lesson,time);assert.ok(Math.abs(f.progress-.4)<1e-8);assert.equal(f.finished,false);}const end=lessonFrame(lesson,1000);assert.equal(end.finished,true);assert.equal(end.progress,1);assert.deepEqual(lessonFrame(lesson,20000).route,end.route);assert.equal(lessonFrame(lesson,0).finished,false);}});
|
||||
test('教学减速,结束停留1秒后自动循环',()=>{for(const level of pack.levels.filter(l=>l.tutorial)){const lesson=makeLesson(pack,level.tutorial);assert.ok(lesson.duration>=3600);for(let i=0;i<lesson.beats.length-1;i++){const time=(i+.4)*lesson.duration/(lesson.beats.length-1);const f=lessonFrame(lesson,time);assert.ok(Math.abs(f.progress-.4)<1e-8);assert.equal(f.finished,false);}const end=lessonFrame(lesson,lesson.duration);assert.equal(end.finished,true);assert.equal(end.progress,1);assert.deepEqual(lessonFrame(lesson,20000).route,end.route);assert.equal(lessonFrame(lesson,0).finished,false);assert.equal(loopLessonTime(lesson,lesson.duration+999),lesson.duration);assert.equal(loopLessonTime(lesson,lesson.duration+1000),0);}});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$releaseDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'releases'
|
||||
New-Item -ItemType Directory -Path $releaseDir -Force | Out-Null
|
||||
$archivePath = Join-Path $releaseDir 'NumberSwipe-campaign-200-r2-高亮指路-保留玩家填色-20261020.zip'
|
||||
$assets = @('game.js','game.json','project.config.json','audio') | ForEach-Object { Join-Path $PSScriptRoot $_ }
|
||||
$archivePath = Join-Path $releaseDir 'NumberSwipe-campaign-200-r2-明亮教具室-过关重音修复版-20261022.zip'
|
||||
$assets = @('game.js','game.json','project.config.json','audio','fonts') | ForEach-Object { Join-Path $PSScriptRoot $_ }
|
||||
Compress-Archive -LiteralPath $assets -DestinationPath $archivePath -Force
|
||||
Get-FileHash -LiteralPath $archivePath -Algorithm SHA256
|
||||
|
||||
@@ -1,71 +1,76 @@
|
||||
{
|
||||
"setting": {
|
||||
"es6": false,
|
||||
"minified": false,
|
||||
"urlCheck": false
|
||||
},
|
||||
"compileType": "game",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
"ignore": [
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "src"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "build.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "smoke.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "README.md"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "source-manifest.json"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "validation.json"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "audio-assets.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "audio-copy.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "HANDOFF.md"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "lessons.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "hints.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "package.ps1"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isGameTourist": false,
|
||||
"appid": "wx7a14d0d70b5dd13d",
|
||||
"editorSetting": {},
|
||||
"projectname": "NumberSwipe-campaign-200-r2",
|
||||
"description": "数字一笔200关评分修订版,源自campaign-200-r2",
|
||||
"miniprogramRoot": "./"
|
||||
}
|
||||
{
|
||||
"setting": {
|
||||
"es6": false,
|
||||
"minified": false,
|
||||
"urlCheck": false
|
||||
},
|
||||
"compileType": "game",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
"ignore": [
|
||||
{"type":"folder","value":".font-sources"},
|
||||
{"type":"folder","value":".font-tools"},
|
||||
{"type":"file","value":"bright-classroom-build.mjs"},
|
||||
{"type":"file","value":"bright-classroom.test.mjs"},
|
||||
{"type":"file","value":"build-fonts.py"},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "src"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "build.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "smoke.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "README.md"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "source-manifest.json"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "validation.json"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "audio-assets.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "audio-copy.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "HANDOFF.md"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "lessons.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "hints.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "package.ps1"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isGameTourist": false,
|
||||
"appid": "wx7a14d0d70b5dd13d",
|
||||
"editorSetting": {},
|
||||
"projectname": "NumberSwipe-campaign-200-r2",
|
||||
"description": "数字一笔200关评分修订版,源自campaign-200-r2",
|
||||
"miniprogramRoot": "./"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>数字一笔 · 明亮教具室试玩</title>
|
||||
<style>
|
||||
@font-face{font-family:NumberSwipeSans500;src:url(../fonts/NumberSwipeSans500.ttf)}
|
||||
@font-face{font-family:NumberSwipeSans600;src:url(../fonts/NumberSwipeSans600.ttf)}
|
||||
@font-face{font-family:NumberSwipeSans700;src:url(../fonts/NumberSwipeSans700.ttf)}
|
||||
@font-face{font-family:NumberSwipeDigits800;src:url(../fonts/NumberSwipeDigits800.ttf)}
|
||||
@font-face{font-family:NumberSwipePlayful;src:url(../fonts/NumberSwipePlayful.ttf)}
|
||||
*{box-sizing:border-box}body{margin:0;background:#fff6df;color:#43382f;font:15px NumberSwipeSans500,sans-serif}header{padding:28px 32px 20px;border-bottom:1px solid #e5d7ba}h1{margin:0 0 8px;font:25px NumberSwipeSans700}p{margin:8px 0;line-height:1.7;color:#71604f}main{display:flex;gap:32px;padding:28px 32px;align-items:flex-start}.tools{width:270px;position:sticky;top:24px}.tools h2{font-size:17px;margin:24px 0 10px}.tools button,select{font:14px NumberSwipeSans600;cursor:pointer;border:1px solid #d8c4a0;background:#fffbf2;color:#43382f;border-radius:12px;padding:12px;margin:0 6px 8px 0}.tools button:hover{border-color:#6552d9;color:#5140b7}.tools select{width:100%}.phone{border:1px solid #d8c4a0;border-radius:24px;overflow:hidden;box-shadow:0 12px 34px #43382f14;background:#ffe69a}canvas{display:block;touch-action:none}.note{font-size:13px}#status{background:#ece7ff;color:#5140b7;border-radius:12px;padding:12px;margin-top:16px}a{color:#5140b7}@media(max-width:750px){main{padding:16px;flex-direction:column}.tools{width:100%;position:static}.phone{align-self:center}header{padding:20px}}
|
||||
</style>
|
||||
<header><h1>数字一笔 · 明亮教具室</h1><p>200关实际游戏包的 Canvas 预览。全部画面由程序绘制,字体随包提供。</p></header>
|
||||
<main><aside class="tools"><label for="size">屏幕尺寸</label><select id="size"><option>375×812</option><option>320×568</option><option>375×667</option><option>390×844</option><option>414×896</option></select><h2>真实关卡样张</h2><div id="samples"></div><h2>指定关卡</h2><select id="level"></select><button id="partial">画前几步</button><button id="hint">显示指路</button><button id="clear">完成本关</button><button id="select">返回选关</button><p class="note">在右侧直接点击或滑动。减少动态与音效开关在游戏选关页的“设置”中。辅助演示按钮仅属于此验收页。</p><div id="status">加载字体与游戏包…</div><p class="note" id="audio-status" role="status">音效:点击游戏后播放</p><p class="note">这是浏览器预览,微信手机上的字体、触摸与性能仍需真机验收。</p><p><a href="hints.html" target="_blank">运行200关提示像素检查</a></p></aside><div class="phone" id="phone"></div></main>
|
||||
<script type="module">
|
||||
import {solve} from '../src/upstream/rules.mjs';
|
||||
import {browserAudio} from './browser-audio.mjs';
|
||||
await Promise.all(['NumberSwipeSans500','NumberSwipeSans600','NumberSwipeSans700','NumberSwipeDigits800','NumberSwipePlayful'].map(f=>document.fonts.load('16px '+f)));
|
||||
const source=await(await fetch('../game.js')).text(),pack=await(await fetch('../src/upstream/levels.json')).json();
|
||||
const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
const size=document.querySelector('#size'),levelSelect=document.querySelector('#level'),status=document.querySelector('#status'),phone=document.querySelector('#phone');
|
||||
let runtime;
|
||||
for(const [i,l] of levels.entries()){const o=document.createElement('option');o.value=i;o.textContent=`第 ${i+1} 关 · ${l.title}`;levelSelect.append(o);}
|
||||
const samples=[['基础大块',3],['异形拼块',levels.findIndex(l=>l.cells.some(c=>c.shape)&&l.cells.length>=12)],['通道与数字',levels.findIndex(l=>l.features.includes('direction')&&l.cells.some(c=>c.clue&&c.direction))],['双色与奇偶',levels.findIndex(l=>l.features.includes('alternation')&&l.features.includes('parity'))],['节奏门',levels.findIndex(l=>l.doors?.length)],['六边墙',levels.findIndex(l=>l.geometry==='hex'&&l.walls?.length)],['密集36块',levels.findIndex(l=>l.id==='r2-163')]];
|
||||
for(const [name,index] of samples){const b=document.createElement('button');b.textContent=name;b.onclick=()=>show(index);document.querySelector('#samples').append(b);}
|
||||
function boot(){
|
||||
const [width,height]=size.value.split('×').map(Number),handlers={},frames=new Map(),canvases=[],store=new Map();let frame=0,now=100000;
|
||||
const timers=new Set(),sound=browserAudio(message=>{document.querySelector('#audio-status').textContent=message;});
|
||||
const later=(callback,delay)=>{const timer=setTimeout(()=>{timers.delete(timer);callback();},delay);timers.add(timer);return timer;};
|
||||
const cancel=timer=>{clearTimeout(timer);timers.delete(timer);};
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},loadFont:p=>p.split('/').at(-1).replace('.ttf',''),getWindowInfo:()=>({windowWidth:width,windowHeight:height,pixelRatio:2,safeArea:{top:24,bottom:height-24}}),getMenuButtonBoundingClientRect:()=>({bottom:42}),getPerformance:()=>({now:()=>now}),getStorageSync:k=>store.get(k)||'',setStorageSync:(k,v)=>store.set(k,v),removeStorageSync:k=>store.delete(k),setInnerAudioOption:sound.setInnerAudioOption,createInnerAudioContext:sound.createInnerAudioContext,showModal(){},env:{USER_DATA_PATH:'/preview'}};
|
||||
for(const n of ['TouchStart','TouchMove','TouchEnd','TouchCancel','Hide','Show','WindowResize'])wx['on'+n]=cb=>handlers[n]=cb;
|
||||
const global={};new Function('wx','GameGlobal','requestAnimationFrame','cancelAnimationFrame','setTimeout','clearTimeout','Date',source)(wx,global,fn=>{frames.set(++frame,fn);return frame;},id=>frames.delete(id),later,cancel,Clock);
|
||||
const api=global.numberSwipeR2,canvas=canvases[0];canvas.style.width=width+'px';canvas.style.height=height+'px';phone.style.width=width+'px';phone.replaceChildren(canvas);
|
||||
function tick(ms=16){now+=ms;const entry=frames.entries().next().value;if(entry){frames.delete(entry[0]);entry[1]();}}
|
||||
function touch(kind,x,y){handlers[kind]({changedTouches:[{identifier:1,clientX:x,clientY:y}],touches:[{identifier:1,clientX:x,clientY:y}]});}
|
||||
function tap(x,y){touch('TouchStart',x,y);touch('TouchEnd',x,y);tick();}
|
||||
function button(name){const b=api.buttons().find(b=>b.label===name&&b.enabled);if(!b)throw Error('缺少按钮 '+name);tap(b.x+b.w/2,b.y+b.h/2);}
|
||||
function cell(id){const s=api.snapshot(),p=s.centers[id];tap(p[0],p[1]+s.boardRect.y);}
|
||||
function select(index){if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(index+1)||b.label===`${index+1} ✓`)){button('下一页');if(++n>30)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(index+1))?String(index+1):`${index+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');tick(300);}
|
||||
let down=false;
|
||||
const coords=e=>{const r=canvas.getBoundingClientRect();return [(e.clientX-r.left)*width/r.width,(e.clientY-r.top)*height/r.height];};
|
||||
canvas.onpointerdown=e=>{down=true;canvas.setPointerCapture(e.pointerId);touch('TouchStart',...coords(e));tick();};
|
||||
canvas.onpointermove=e=>{if(down){touch('TouchMove',...coords(e));tick();}};
|
||||
canvas.onpointerup=e=>{down=false;touch('TouchEnd',...coords(e));tick();};canvas.onpointercancel=e=>{down=false;touch('TouchCancel',...coords(e));tick();};
|
||||
tick();return {tick,api,button,cell,select,dispose(){sound.dispose();for(const timer of timers)clearTimeout(timer);timers.clear();}};
|
||||
}
|
||||
function show(index){runtime?.dispose();runtime=boot();runtime.select(index);levelSelect.value=index;update();}
|
||||
function update(){const s=runtime.api.snapshot();status.textContent=`第 ${s.displayOrder} 关 · ${s.path.length} / ${levels.find(l=>l.id===s.level).cells.length} 步 · ${s.theme}`;}
|
||||
levelSelect.onchange=()=>show(Number(levelSelect.value));size.onchange=()=>show(Number(levelSelect.value));
|
||||
document.querySelector('#partial').onclick=()=>{const l=levels.find(l=>l.id===runtime.api.snapshot().level);runtime.button('重开');for(const id of solve(l)[0].slice(0,Math.min(5,l.cells.length-1)))runtime.cell(id);runtime.tick(300);update();};
|
||||
document.querySelector('#hint').onclick=()=>{runtime.button('指路');update();};
|
||||
document.querySelector('#clear').onclick=()=>{const l=levels.find(l=>l.id===runtime.api.snapshot().level);runtime.button('重开');for(const id of solve(l)[0])runtime.cell(id);runtime.tick(600);update();};
|
||||
document.querySelector('#select').onclick=()=>{runtime.button('选关');update();};
|
||||
show(3);function pump(){runtime.tick();requestAnimationFrame(pump);}requestAnimationFrame(pump);
|
||||
</script></html>
|
||||
@@ -0,0 +1,31 @@
|
||||
// Browser-only adapter for the packaged WeChat WAV sound effects.
|
||||
export function browserAudio(report){
|
||||
const players=new Set();let disposed=false,plays=0;
|
||||
return {
|
||||
setInnerAudioOption(){},
|
||||
createInnerAudioContext(){
|
||||
const player=new Audio();player.preload='auto';players.add(player);
|
||||
let errorHandler=()=>{},playHandler=()=>{},generation=0;
|
||||
const error=reason=>{
|
||||
if(disposed||reason?.name==='AbortError')return;
|
||||
report('音效播放失败:'+(reason?.message||'请检查页面声音权限'));
|
||||
errorHandler({errMsg:reason?.message||'浏览器音频加载失败'});
|
||||
};
|
||||
player.addEventListener('error',()=>error(player.error));
|
||||
return {
|
||||
set src(path){player.src=new URL('../'+path,location.href).href;},
|
||||
set volume(value){player.volume=value;},
|
||||
set obeyMuteSwitch(value){},
|
||||
onError(callback){errorHandler=callback;},
|
||||
onPlay(callback){playHandler=callback;},
|
||||
stop(){generation++;player.pause();if(player.readyState)player.currentTime=0;},
|
||||
play(){
|
||||
const current=generation;
|
||||
player.play().then(()=>{if(!disposed&¤t===generation){playHandler();report('音效已播放 · '+(++plays)+' 次');}})
|
||||
.catch(reason=>{if(current===generation)error(reason);});
|
||||
}
|
||||
};
|
||||
},
|
||||
dispose(){disposed=true;for(const player of players){player.pause();player.removeAttribute('src');player.load();}players.clear();}
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
<!doctype html><meta charset="utf-8"><title>指路与玩家填色像素回归</title>
|
||||
<style>body{font:16px sans-serif;background:#eee}#views{display:flex;gap:12px}canvas{width:375px;height:812px;background:white}h1{font-size:20px}</style>
|
||||
<style>
|
||||
@font-face{font-family:NumberSwipeSans500;src:url(../fonts/NumberSwipeSans500.ttf)}
|
||||
@font-face{font-family:NumberSwipeSans600;src:url(../fonts/NumberSwipeSans600.ttf)}
|
||||
@font-face{font-family:NumberSwipeSans700;src:url(../fonts/NumberSwipeSans700.ttf)}
|
||||
@font-face{font-family:NumberSwipeDigits800;src:url(../fonts/NumberSwipeDigits800.ttf)}
|
||||
@font-face{font-family:NumberSwipePlayful;src:url(../fonts/NumberSwipePlayful.ttf)}
|
||||
</style>
|
||||
<h1>指路与玩家填色像素回归</h1><p id="result">检查中…</p><div id="views"></div>
|
||||
<script type="module">
|
||||
import {solve} from '../src/upstream/rules.mjs';
|
||||
await Promise.all(['NumberSwipeSans500','NumberSwipeSans600','NumberSwipeSans700','NumberSwipeDigits800','NumberSwipePlayful'].map(f=>document.fonts.load('16px '+f)));
|
||||
const pack=await(await fetch('../src/upstream/levels.json')).json();
|
||||
const source=await(await fetch('../game.js')).text();
|
||||
const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
const canvases=[],handlers={},frames=new Map(),storage=new Map();let frameId=0,now=100000;
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={
|
||||
createCanvas(){const c=document.createElement('canvas');canvases.push(c);return c;},
|
||||
loadFont:p=>p.split('/').at(-1).replace('.ttf',''),
|
||||
createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},
|
||||
getWindowInfo:()=>({windowWidth:375,windowHeight:812,pixelRatio:1,safeArea:{top:44,bottom:778}}),
|
||||
getMenuButtonBoundingClientRect:()=>({bottom:80}),getPerformance:()=>({now:()=>now}),
|
||||
getStorageSync:k=>storage.get(k)||'',setStorageSync:(k,v)=>storage.set(k,v),removeStorageSync:k=>storage.delete(k),
|
||||
@@ -19,13 +28,13 @@ for(const n of ['TouchStart','TouchMove','TouchEnd','TouchCancel','Hide','Show',
|
||||
const global={};
|
||||
new Function('wx','GameGlobal','requestAnimationFrame','cancelAnimationFrame','setTimeout','clearTimeout','Date',source)(wx,global,cb=>{frames.set(++frameId,cb);return frameId;},id=>frames.delete(id),()=>1,()=>{},Clock);
|
||||
const api=global.numberSwipeR2;
|
||||
const tick=(ms=16)=>{now+=ms;const [id,fn]=frames.entries().next().value;frames.delete(id);fn();};tick();
|
||||
const tick=(ms=16)=>{now+=ms;const entry=frames.entries().next().value;if(!entry)return;const [id,fn]=entry;frames.delete(id);fn();};tick();
|
||||
const tap=(x,y)=>{const e={changedTouches:[{identifier:1,clientX:x,clientY:y}],touches:[{identifier:1,clientX:x,clientY:y}]};handlers.TouchStart(e);handlers.TouchEnd(e);tick();};
|
||||
const button=label=>{const b=api.buttons().find(b=>b.label===label);if(!b||!b.enabled)throw Error('缺少按钮 '+label);tap(b.x+b.w/2,b.y+b.h/2);};
|
||||
const cell=id=>{const [x,y]=api.snapshot().centers[id];tap(x,y+174);};
|
||||
const cell=id=>{const s=api.snapshot(),[x,y]=s.centers[id];tap(x+s.boardRect.x,y+s.boardRect.y);};
|
||||
const select=i=>{if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(i+1)||b.label===`${i+1} ✓`)){button('下一页');if(++n>25)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(i+1))?String(i+1):`${i+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');};
|
||||
const capture=()=>{const c=document.createElement('canvas');c.width=375;c.height=812;c.getContext('2d').drawImage(canvases[0],0,0);return c;};
|
||||
const samples=(route)=>{const s=api.snapshot(),ctx=canvases[0].getContext('2d');return route.map(id=>{const p=s.centers[id];return Array.from(ctx.getImageData(Math.round(p[0]-s.unit*.08),Math.round(p[1]+174+s.unit*.18),5,5).data);});};
|
||||
const samples=(route)=>{const s=api.snapshot(),ctx=canvases[0].getContext('2d');return route.map(id=>{const p=s.centers[id];return Array.from(ctx.getImageData(Math.round(p[0]-s.unit*.08),Math.round(p[1]+s.boardRect.y+s.unit*.18),5,5).data);});};
|
||||
let checked=0;const result=document.querySelector('#result');
|
||||
try{
|
||||
for(let i=0;i<levels.length;i++){
|
||||
@@ -38,6 +47,7 @@ try{
|
||||
button('撤销');cell(route[n-1]);tick(300);
|
||||
if(JSON.stringify(before)!==JSON.stringify(samples(route.slice(0,n))))throw Error('撤销后填色变化:'+(i+1));
|
||||
checked++;
|
||||
if(i%4===0){result.textContent=`正在检查 ${checked}/200 关…`;await new Promise(resolve=>requestAnimationFrame(resolve));}
|
||||
if(i===3){document.querySelector('#views').replaceChildren(base,capture());}
|
||||
}
|
||||
result.textContent=`PASS:${checked}/200 关,点击指路前后和撤销重画后,玩家已走格子的实际 Canvas 填色像素一致。左图无提示,右图开启高对比提示。`;
|
||||
|
||||
@@ -19,19 +19,20 @@ function boot(storage = new Map(), width = 375, height = 812) {
|
||||
let frameId = 0,now=100000;
|
||||
class Clock extends Date {static now(){return now;}}
|
||||
const text = [],images=[];
|
||||
const ctx = new Proxy({ drawImage:(image,x,y,w,h)=>images.push({x,y,w,h}),measureText: s => ({ width: [...String(s)].reduce((n,c)=>n+(/[^\x00-\x7f]/.test(c)?1:.55),0)*(parseFloat(ctx.font)||16) }), fillText: (s, x, y) => text.push({ s, x, y }) }, { get: (o, k) => o[k] ?? (() => { }), set: (o, k, v) => (o[k] = v, true) });
|
||||
const ctx = new Proxy({ drawImage:(image,x,y,w,h)=>images.push({x,y,w,h}),measureText: s => ({ width: [...String(s)].reduce((n,c)=>n+(/[^\x00-\x7f]/.test(c)?1:.55),0)*(Number(/([\d.]+)px/.exec(ctx.font)?.[1])||16) }), fillText: (s, x, y) => text.push({ s, x, y }) }, { get: (o, k) => o[k] ?? (() => { }), set: (o, k, v) => (o[k] = v, true) });
|
||||
const wx = {setInnerAudioOption:()=>{},createInnerAudioContext:()=>({stop(){},onError(){},play(){assert.ok(fs.existsSync(path.join(root,this.src)),this.src);played.push(this.src);}}), createCanvas: () => ({ getContext: () => ctx }), getWindowInfo: () => ({ windowWidth: width, windowHeight: height, pixelRatio: 2, safeArea: { top: 44, bottom: height - 34 } }), getMenuButtonBoundingClientRect: () => ({ bottom: 80 }), getStorageSync: k => storage.get(k) || '', setStorageSync: (k, v) => storage.set(k, v), removeStorageSync: k => storage.delete(k), getPerformance: () => ({ now: () => Date.now() }), showModal: () => { }, env: { USER_DATA_PATH: '/user' } };
|
||||
wx.loadFont=p=>{assert.ok(fs.existsSync(path.join(root,p)),'字体必须随包提供 '+p);return path.basename(p,'.ttf');};
|
||||
for (const name of ['TouchStart', 'TouchMove', 'TouchEnd', 'TouchCancel', 'Hide', 'Show', 'WindowResize'])
|
||||
wx['on' + name] = cb => handlers[name] = cb;
|
||||
const global = {};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root, 'game.js'), 'utf8'), {Date:Clock, wx, GameGlobal: global, requestAnimationFrame: cb => { frames.set(++frameId, cb); return frameId; }, cancelAnimationFrame: id => frames.delete(id), setTimeout: () => 1, clearTimeout: () => { } }, { timeout: 10000 });
|
||||
const tick = () => { const [id, fn] = frames.entries().next().value; frames.delete(id);text.length=0;images.length=0; fn(); };
|
||||
const tick = () => { const entry=frames.entries().next().value;if(!entry)return;const [id,fn]=entry; frames.delete(id);text.length=0;images.length=0; fn(); };
|
||||
tick();
|
||||
const touch = (kind, x, y) => handlers[kind]({ changedTouches: [{ identifier: 1, clientX: x, clientY: y }], touches: [{ identifier: 1, clientX: x, clientY: y }] });
|
||||
const tap = (x, y) => { touch('TouchStart', x, y); touch('TouchEnd', x, y); tick(); };
|
||||
const api = global.numberSwipeR2;
|
||||
const button = label => { const b = api.buttons().find(b => b.label === label); assert.ok(b, '缺少按钮 ' + label); assert.ok(b.enabled, '按钮不可用 ' + label); tap(b.x + b.w / 2, b.y + b.h / 2); };
|
||||
const cell = id => { const [x, y] = api.snapshot().centers[id]; tap(x, y + 174); };
|
||||
const cell = id => { const s=api.snapshot(),[x,y]=s.centers[id];tap(x+s.boardRect.x,y+s.boardRect.y); };
|
||||
const select = i => { if (api.snapshot().screen !== 'select')
|
||||
button('选关'); const label = String(i + 1); let steps = 0; while (!api.buttons().some(b => b.label === label || b.label === label + ' ✓')) {
|
||||
button('下一页');
|
||||
@@ -40,6 +41,7 @@ function boot(storage = new Map(), width = 375, height = 812) {
|
||||
button('返回游戏'); assert.equal(api.snapshot().level, ordered[i].id); };
|
||||
return { advance(ms){now+=ms;tick();}, api, button, cell, select, tick, handlers, frames, storage, text, played,images };
|
||||
}
|
||||
const wallLesson=boot();wallLesson.select(4);wallLesson.button('规则');wallLesson.advance(1200);assert.equal(wallLesson.api.snapshot().lesson.finished,false);assert.ok(wallLesson.frames.size>0,'教学超过1.1秒仍需继续刷新');wallLesson.advance(wallLesson.api.snapshot().lesson.duration-1200+50);assert.equal(wallLesson.api.snapshot().lesson.finished,true,'墙壁教学必须播到完成');wallLesson.advance(900);assert.equal(wallLesson.api.snapshot().lesson.finished,true);wallLesson.advance(100);assert.equal(wallLesson.api.snapshot().lesson.finished,false,'停留1秒后自动重播');wallLesson.button('返回游戏');wallLesson.advance(6000);assert.equal(wallLesson.api.snapshot().lesson,null,'离开后停止教学');
|
||||
const ui = boot(), rows = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const l = ordered[i];
|
||||
@@ -69,7 +71,8 @@ for (let i = 0; i < 200; i++) {
|
||||
assert.ok(hinted?.route.length>=2);
|
||||
assert.equal(ui.api.snapshot().path.length,0);
|
||||
for (const [step,id] of solution.entries()) {
|
||||
ui.cell(id);
|
||||
const playedBefore=ui.played.length;ui.cell(id);
|
||||
if(step===solution.length-1)assert.deepEqual(ui.played.slice(playedBefore),['audio/complete.wav'],'最后一步只播放一次过关音,不叠加路径点音');
|
||||
if(step===1)assert.equal(JSON.stringify(ui.api.snapshot().cellViews),originalFills,'点击指路前后玩家填色必须一致 '+l.id);
|
||||
assert.equal(Boolean(ui.api.snapshot().hint),step<hinted.end,'到达段末后消失');
|
||||
}
|
||||
@@ -111,7 +114,15 @@ for(const [w,h]of [[320,568],[375,667],[390,844],[414,896]]){
|
||||
}
|
||||
const narrow=boot(new Map(),320,568);
|
||||
for(const l of ordered.filter(l=>l.tutorial)){narrow.select(ordered.indexOf(l));narrow.button('规则');let pages=0;do{assert.ok(narrow.api.buttons().every(b=>b.y>=0&&b.y+b.h<=568));const before=narrow.api.snapshot();narrow.advance(3900);const after=narrow.api.snapshot();for(const field of ['path','centers','doors','latchedDoors','records','lastSound'])assert.deepEqual(after[field],before[field],field);if(!narrow.api.buttons().some(b=>b.label==='下一段'&&b.enabled))break;narrow.button('下一段');assert.ok(++pages<30);}while(true);narrow.button('返回游戏');}
|
||||
const result = {segmentHints:true,playerFillUnchangedAfterHint:true,animatedLessons:10,previewStateIsolation:true,narrowTutorials:'320x568', source: manifest.source, version: pack.version, total: 200, passed: 200, sourceHashesMatch: true, uniqueSolution: true, simulatedWeChatTouch: true, undoRestartHint: true, storageRestore: true, foregroundLoop: true, nativeAudioPlaybackCalls: ui.played.length, plainLanguageRules: true, pagedRules: true, clearActionLayout: true, phoneSizesChecked: 4, rows };
|
||||
const idle=boot();idle.advance(1000);assert.equal(idle.frames.size,0,'静态选关页不持续绘制');
|
||||
assert.ok(Object.values(idle.api.snapshot().fonts).every(v=>v==='loaded'));
|
||||
idle.button('设置');idle.button('减少动态:关');idle.button('完成');
|
||||
assert.equal(idle.api.snapshot().reducedMotion,true);
|
||||
idle.select(162);idle.cell(solve(ordered[162])[0][0]);idle.advance(1000);
|
||||
assert.equal(idle.frames.size,0,'减少动态的思考阶段不持续绘制');
|
||||
idle.button('指路');idle.advance(1000);assert.ok(idle.api.snapshot().hint);assert.equal(idle.frames.size,0,'静态指路仍保留信息但不循环绘制');
|
||||
const reducedRestored=boot(idle.storage);assert.equal(reducedRestored.api.snapshot().reducedMotion,true);
|
||||
const result = {theme:'bright-classroom',bundledFonts:true,reducedMotionPersisted:true,idleRenderingStops:true,segmentHints:true,playerFillUnchangedAfterHint:true,animatedLessons:10,previewStateIsolation:true,narrowTutorials:'320x568', source: manifest.source, version: pack.version, total: 200, passed: 200, sourceHashesMatch: true, uniqueSolution: true, simulatedWeChatTouch: true, undoRestartHint: true, storageRestore: true, foregroundLoop: true, nativeAudioPlaybackCalls: ui.played.length, plainLanguageRules: true, pagedRules: true, clearActionLayout: true, phoneSizesChecked: 4, rows };
|
||||
fs.writeFileSync(path.join(root, 'validation.json'), JSON.stringify(result, null, 2));
|
||||
console.log('PASS 200/200: r2源文件哈希一致、唯一解、微信发布包触摸通关、撤销/重开/指路、存档/音效设置恢复、前后台切换。');
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"sha256": "7cecb4bae026919131325d86e970bb7bee55d4569d574af78032714e7bb1cf04"
|
||||
}
|
||||
],
|
||||
"adaptationRevision": "2026-10-19-animated-lessons-one-second",
|
||||
"notes": "上游四文件保持原样;微信宿主加载时替换音频实现,优化玩家可见文案,不改变200关布局、约束及顺序。"
|
||||
}
|
||||
"adaptationRevision": "2026-10-21-bright-classroom-shadow-glyph-polish-v3",
|
||||
"notes": "上游四文件保持原样;区分棋块暖晕与底盘alpha投影,按字形定位数字,新增白字柔光与深字纹理、按钮显示字体;关卡、输入几何、规则、顺序、存档key不变。"
|
||||
}
|
||||
|
||||