美术风格
深蓝的夜,暖黄的灯,圆脑袋的小人。 所有素材都从这份取;色板之外的颜色要先加进第二节再用。
两个口径的验收判据在第四节,改完素材必须跑一遍:
python3 tools/art/night_check.py <文件...>

小农场 是日本清新风(暖白、清新绿),野花笔记 是灰蓝低饱和水彩。
夜市比这两个都更有颜色——它本来就是灯笼照着的一条夜街。
实测对照:野花笔记基准图的色度 P50 是 11,夜市概念稿是 46,差 4 倍。
跨项目复用 CSS / 调色板时,必须逐色重跑一次判据,
否则就会像野花笔记那次一样,把 小农场 的 --gold:#C9A45C(饱和 58.6%)带过去,在灰蓝页面上扎眼。
一、画风基准#
| 维度 | 定调 | 禁止 |
|---|---|---|
| 光 | 一个光源:灯笼。 所有暖色都从灯笼的高度往下走 | 多个方向不明的光;全局均匀打光(会变成白天) |
| 色 | 深蓝夜色打底,暖黄灯芯点睛 | 荧光色;纯黑;纯白 |
| 形 | 圆、胖、短。小人两头身到三头身 | 尖角、硬折线、写实比例 |
| 边 | 细描边或不描边,靠色块和明暗分界 | 粗黑描边 |
| 质感 | 哑光平涂 + 一点柔光晕 | 写实材质、金属高光、3D 渲染感 |
| 空 | 夜要留得下来。街上要能看见空地 | 画面塞满 |
二、色板(22 色,全部实测)#
色号全部用 tools/art/night_palette.py --check-hex '...' 验过,
色度、饱和度、明度三列都是脚本读数,不是估计值。
2.1 夜色(背景)#
| 用途 | 色号 | 色度 | 明度 |
|---|---|---|---|
| 夜空最上 | #131C33 |
32 | 0.20 |
| 夜空主色 | #1E2A44 |
38 | 0.27 |
| 远处街道 | #2B3A5C |
49 | 0.36 |
| 街面石板 | #3A4358 |
30 | 0.35 |
2.2 暖光(灯笼到摊台)#
| 用途 | 色号 | 色度 | 明度 |
|---|---|---|---|
| 灯笼纸 | #F2C879 |
121 | 0.95 |
| 灯笼芯(最亮) | #F0D5A0 |
80 | 0.94 |
| 摊灯暖橙 | #E2A968 |
122 | 0.89 |
| 木台(亮面) | #8A6244 |
70 | 0.54 |
| 木台(暗面) | #5E4433 |
43 | 0.37 |
2.3 界面#
| 用途 | 色号 | 色度 | 明度 |
|---|---|---|---|
| 面板底(纸米) | #F4E9D6 |
30 | 0.96 |
| 面板描边 | #C9A87C |
77 | 0.79 |
| 主文字 | #2A2F3E |
20 | 0.24 |
| 次文字 | #6B6F80 |
21 | 0.50 |
| 金币 | #E8C36E |
122 | 0.91 |
2.4 小人#
| 用途 | 色号 | 色度 | 明度 |
|---|---|---|---|
| 圆脑袋(无脸) | #F2DCC6 |
44 | 0.95 |
| 头发 / 配饰 | #4A4258 |
22 | 0.35 |
| 衣 ① 珊瑚 | #D98F7A |
95 | 0.85 |
| 衣 ② 芥黄 | #D9B267 |
114 | 0.85 |
| 衣 ③ 雾青 | #7FAAA5 |
43 | 0.67 |
| 衣 ④ 藕紫 | #A98CB5 |
41 | 0.71 |
| 衣 ⑤ 米灰 | #C8C2A8 |
32 | 0.78 |
| 衣 ⑥ 淡蓝 | #8FA7C9 |
58 | 0.79 |
灯笼和金币已经占了画面里全部的高饱和配额。 小人再艳,画面就没有主次了——光是主角,人是群众。 六色色相拉开(16° / 40° / 174° / 285° / 60° / 213°), 排成一队时一眼能数出"来了 6 个人",这是排队游戏最重要的可读性。
三、小人:无脸的圆脑袋#

3.1 为什么不做五官#
没有五官,情绪就只能靠动作表达。这看着像妥协,实际是三条好处:
- 省素材:一张头、一张身体、两手两脚,六个零件演完所有情绪。
- 好动:没有眼睛要跟着转,木偶怎么摆都不"死"。
- 可爱:观众会自动脑补表情——这是无脸角色比有脸角色更好用的地方。
代价是:动作必须做到位。 一个姿势僵硬的无脸小人,就是一根会走的柱子。
3.2 情绪只能靠这四件事#
| 情绪 | 怎么做 | 出现在 |
|---|---|---|
| 等待 | 身体左右轻晃(±3°),重心轻微上下 | 队伍里所有人,永远在动 |
| 开心 | 蹦一下(离地 6px)+ 头顶冒爱心 | 拿到吃的之后 |
| 好奇 | 歪头(头旋转 ±12°),身体不动 | 新菜品第一次出现时 |
| 累了 | 原地坐下(腿收起来,整体矮 1/4) | 等太久的客人。不是生气,更不会走 |
坐下可以,不能出现皱眉、跺脚、转身走。见核心玩法铁律一: 零失败,不存在"客人被气跑"。孩子看到一个小人生气走掉,第一反应是"我做错了"。
3.3 拆件规范(六片)#
零件必须头身分离、四肢分开出图,关节处留 5% 重叠边,并标出轴心。
| 零件 | 说明 | 轴心标记 |
|---|---|---|
head |
圆脑袋 + 头饰(头饰单独一层,方便换) | 颈点 |
body |
胶囊身体 | 颈点、双髋 |
arm-l / arm-r |
短圆手臂 | 肩点 |
leg-l / leg-r |
短圆腿 | 髋点 |
小农场 那一轮踩过:重出零件看着省事,实际是偷偷换了角色——
把正面小鸡(两只眼睛、喙在正中)换成了侧面(一只眼睛、喙偏一侧)。
无脸小人更危险,因为没有五官当参照物,换了配色或比例根本看不出来。
切完必须能原样拼回去,和原图做像素差,差 0 才算过。
3.4 衣服颜色用染色,不用六套图#
零件出带明暗的灰度图,颜色在代码里乘算染上去。 这样 6 种衣服只要 6 张灰度零件,之后加配色不用重出图。 唯一的例外是头饰——它是形状差异,必须单独出图。
3.5 尺寸#
| 角色 | 屏上高度(750×1334 设计稿) | 站位 |
|---|---|---|
| 街上路人 | 40~52 px | 沿街走,越远越小(纵深 1.6~3.0) |
| 排队的客人 | 56~68 px | 沿摊位前沿排成一列 |
| 贵客 | 72~84 px | 摊位侧面,比常人高一档 |
| 摊主(你) | 76 px | 摊位内侧,不动 |
同屏小人的上限按 40 个做——超出的人数用幂曲线压成"街上更多的路人", 不做成真的 40 个物体(参考旧夜市那次的教训:人流量不能 1:1 换算成场上人数)。
四、验收判据(两个口径,缺一个都会漏)#
python3 tools/art/night_check.py <文件...> # 成品图(像素口径)
python3 tools/art/night_palette.py --check-hex '#...' # 色板(色号口径)
python3 tools/art/night_check.py --self-test # 验判据本身有没有区分度
4.1 色号口径(管色板)#
| # | 判据 | 拦什么 |
|---|---|---|
| ① | 色度 ≥ 18 | 太灰 |
| ② | 色度 ≤ 130 | 太艳 |
| ③ | 明度 ≥ 0.10 | 纯黑(夜市允许深,不许死黑) |
| ④ | 明度 ≤ 0.96 | 纯白(夜里的最亮处也只是暖白) |
| ⑤ | 亮色(明度 ≥ 0.5)饱和 ≤ 55% | 亮色刺眼 |
HSV 饱和 = (max−min)/max,max 越小分母越小,暗色会自动虚高。
实测 #131C33 明度只有 0.20,HSV 饱和 62.7% 看着像超标一倍,
而色度只有 32,落在色板正中间。
差点为了迁就一条错判据,把本来就对的深蓝夜色改坏。
(同样的坑野花笔记那一轮记过一次,这是第二次。)
4.2 像素口径(管成品图,阈值拿概念稿反推)#
| # | 判据 | 实测(概念稿) |
|---|---|---|
| ⑥ | 色度 P50 ≥ 25 | 46.0 ✓ |
| ⑦ | 饱和均值 ≤ 45% | 42.1% ✓ |
| ⑧ | 饱和 P99.9 ≤ 82% | 80.0% ✓ |
| ⑨ | 明度均值 0.38~0.60 | 0.488 ✓ |
| ⑩ | 暗部(V<0.25)2%~18% | 5.5% ✓ |
| ⑪ | 亮部(V>0.85)5%~25% | 9.0% ✓ |
阈值必须低于基准图的实测值,否则会出现"基准图自己不合格"。
4.3 判据有效性:拿已知样本跑,必须有一个被拦下#
--self-test 会跑三个样本,实跑结果:
| 样本 | 结果 | 被哪条拦下 |
|---|---|---|
| 夜市概念稿 | 通过 | — |
| 野花笔记基准图 | 拦下 | ⑥ 色度 P50 = 11(太灰)、⑨ 明度 0.626(太亮) |
| 概念稿加饱和 200% | 拦下 | ⑦ 饱和均值 66%、⑧ 饱和 P99.9 100% |
只能"全过"的判据等于没有判据。 三个样本一眼可分,判据才算立住。
五、动画:一律用木偶,不用生图序列帧#
四条硬规矩,缺一条就不像在动:
- 腿拆成两根骨头(大腿 + 脚掌),否则一转身脚就翘。
- 地面锁定:髋要下沉
L − L·cos(a)抵消"腿变短",让踩地的脚钉住不动。 - 必须有地面投影:没有影子就看不出脚有没有离地。这是可读性,不是装饰。
- 层次 + 相位滞后:根 / 腿 / 身体 / 头 / 手各一层, 头比身体晚动一点(follow-through)。只做"整体上下晃"就是僵硬感的来源。
5.1 侧面走路是"迈步",正面是"摇摆"#
| 什么时候 | 做法 |
|---|---|
| 街上路人(横向移动) | 侧面迈步:两条腿几乎同 x(差 ~16px),前后交替 |
| 排队客人(面向摊位) | 正面原地轻晃,步频降到 0.22 倍,只做重心左右移 |
① 是 waddle 不是 walk:身体左右倾 + 交替抬脚,重心和抬脚必须反相 (身体往右倾 = 左脚抬起)。反了就是在原地抽搐。 ② 脚的旋转要小:转多了会把抬起量吃掉(实测设 26px 抬起只剩 8px)。
5.2 站定之后要降到 0.22 倍速#
排队的小人永远不能完全静止,但也不能像走路那样摆。 实测旧夜市的做法:站定后步频降到 0.22 倍原地踏步, 看起来是"在等",而不是"卡住了"。
新客人按 i % 6 错开出发相位,否则一整批同时迈步,像一群机器人。
六、场景分层#
竖屏 750×1334 设计稿,五层,从后往前:
| 层 | 内容 | 实现 |
|---|---|---|
| L0 远景 | 夜空、月亮、星、屋顶剪影 | 静态图,可做极慢的星闪 |
| L1 中景 | 街道、两侧建筑、灯笼串 | 静态图;灯笼可单独做轻微摇曳 |
| L2 近景 | 你的摊位(棚、灶台、招牌) | 静态图 + 代码驱动的热气 / 火苗 |
| L3 角色 | 排队客人、街上路人、贵客、摊主 | Canvas 木偶,唯一每帧重画的一层 |
| L4 前景 | 金币飘字、爱心、热气、烟火 | 纯代码,零素材 |
背景是一张图,UI 是 DOM。不要每秒重建整棵树——
旧夜市那一版就是每秒 innerHTML = ...,直接后果是 8%~15% 的按压会丢
(手指按下的节点被换掉了,click 落到没有 data-act 的祖先上,静默失效)。
骨架只在切换页签时重建,数字和进度条原地改。
七、素材规范#
| 类别 | 尺寸 | 单张上限 | 命名 |
|---|---|---|---|
| 背景(L0+L1 合成) | 750×1334 | 180 KB | bg/street.webp |
| 摊位 | 宽 520 | 60 KB | stall/cart.webp |
| 小人零件(灰度) | 头 96 / 身 96 / 四肢 64 见方 | 各 8 KB | char/head.webp char/arm-l.webp |
| 头饰 | 64 见方 | 各 4 KB | char/hat-*.webp |
| 菜品图标 | 96 见方 | 各 6 KB | food/skewer.webp |
| 灯笼 / 招牌布 | 128 / 256 宽 | 各 12 KB | prop/lantern.webp |
| 金币 / 爱心 | 48 见方 | 各 2 KB | fx/coin.webp |
首包预算 ≤ 2 MB(微信小游戏 4 MB 上限留一半给代码和音频)。
八、音频(暂时只列,不做)#
| 时机 | 声音 | 说明 |
|---|---|---|
| 递出一份餐 | "叮" + 金币声 | 全局最重要的一个音,前 60 秒全指望它 |
| 一声吆喝 | 短促人声 | 队伍空的时候点屏幕 |
| 招牌凑齐 | 一小段上行音阶 | 一天听不到几次,可以做得夸张 |
| 贵客出现 | 铃声 | 要有辨识度,玩家不看屏幕也知道他来了 |
不需要背景音乐循环也可以发——但上面四个音效一个都不能省。 旧版本诊断里那句"完全没有声音"就是一条硬伤。