dsh-creation-mode 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ # 音画同步
2
+
3
+ 画面和声音各做各的,观众一眼就能看出「配乐是贴上去的」。同步是剪出来的,不是碰出来的。
4
+
5
+ 这一份讲:卡点用哪套时间、声音怎么分线、落点差几毫秒会被听出来、混音电平怎么定。
6
+
7
+ ---
8
+
9
+ ## 一、两套时间
10
+
11
+ | 时间 | 来源 | 精度 | 用途 |
12
+ |---|---|---|---|
13
+ | `cuts` | 分镜切点 | 秒(0.01) | 段落结构 |
14
+ | `cues` | 分镜里的镜头重点 | 秒(0.01) | 镜头内动作 |
15
+ | `beats` | 你从动画代码里导出 | 毫秒(0.001) | **卡点** |
16
+
17
+ `cuts` 只告诉你「第 3.6 秒换场」,`beats` 告诉你「第 3.617 秒标题砸下」。
18
+
19
+ **每写完一段动画,把里面真正「炸」的时刻抄进 `beats`。** 文字砸下、数字跳到、
20
+ 粒子聚拢、故障闪一下——这些才是观众会跟着抖的点。
21
+
22
+ ```json
23
+ { "time": 3.617, "type": "impact", "label": "中文标题砸下", "power": 0.95 }
24
+ ```
25
+
26
+ `power` 决定配乐里这一下的力度档位:0.8 以上当重击,0.4–0.8 当强调,0.4 以下当轻点。
27
+
28
+ ---
29
+
30
+ ## 二、两条总线
31
+
32
+ 配乐代码拿到两个出口:
33
+
34
+ | 形参 | 接什么 | 处理 |
35
+ |---|---|---|
36
+ | `dest` | 重音、打击乐、转场冲击 | 原样输出,不衰减 |
37
+ | `bed` | 铺底、旋律、和弦、律动 | 每个卡点前后被侧链下压 |
38
+
39
+ ```js
40
+ // 铺底接 bed
41
+ pad.connect(bed);
42
+ // 重音接 dest
43
+ kick.connect(dest);
44
+ ```
45
+
46
+ 分线错了,重音就淹在自己的伴奏里,怎么调音量都没用。
47
+
48
+ ### 侧链的时间常数
49
+
50
+ `bed` 的下压由 `beats` 驱动,形状是:
51
+
52
+ | 阶段 | 时长 | 增益 |
53
+ |---|---|---|
54
+ | 提前量 | 80–120ms | 1.0 → 0.25 |
55
+ | 重音瞬间 | 0 | 0.2 |
56
+ | 恢复 | 180–260ms | 0.2 → 1.0 |
57
+
58
+ 提前量太短(<50ms)听不出「让位」,太长(>200ms)会觉得音乐在喘。
59
+
60
+ ---
61
+
62
+ ## 三、落点
63
+
64
+ | 偏差 | 听感 |
65
+ |---|---|
66
+ | 声音早 1 帧(33ms) | 抢,像音效先到 |
67
+ | 完全同帧 | 准,有力量 |
68
+ | 声音晚 1 帧 | 稍微钝,但可接受 |
69
+ | 声音晚 2 帧以上 | 散,像两条轨没对上 |
70
+
71
+ 所以:**声音事件用 `start(b.time)` 精确落点,不要四舍五入到拍上。**
72
+
73
+ ```js
74
+ const t0 = ctx.currentTime;
75
+ for (const b of beats) {
76
+ if (b.type === 'impact') kick(t0 + b.time, b.power);
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 四、留白
83
+
84
+ 不是每一秒都要有声音。整条片子里留 2–3 处 0.5s 的静默,重音才有分量。
85
+
86
+ | 位置 | 效果 |
87
+ |---|---|
88
+ | 情绪转折前 | 屏息感 |
89
+ | 高潮前一拍 | 蓄力 |
90
+ | 结尾前 | 收束 |
91
+
92
+ 静默不要放在段落中间——那会像掉帧或音轨断了。
93
+
94
+ ---
95
+
96
+ ## 五、电平
97
+
98
+ | 元素 | 峰值 | 说明 |
99
+ |---|---|---|
100
+ | 铺底(bed) | −18 dBFS | 比人声/字幕低很多 |
101
+ | 重音(dest) | −6 ~ −3 dBFS | 要能「跳出来」 |
102
+ | 整体峰值 | ≤ −1 dBFS | 留 1dB 余量,不削波 |
103
+ | 结尾 | 0(静音) | 最后 0.3–0.5s 自然衰减 |
104
+
105
+ ```js
106
+ const master = ctx.createGain();
107
+ master.gain.value = 0.9; // 留余量
108
+ master.connect(dest);
109
+ ```
110
+
111
+ 硬切音频在播放器里会有「啪」的爆音。**所有声音都要在 `duration` 内自然收尾**:
112
+
113
+ ```js
114
+ const fade = ctx.createGain();
115
+ fade.gain.setValueAtTime(1, t0 + duration - 0.4);
116
+ fade.gain.linearRampToValueAtTime(0, t0 + duration);
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 六、音效设计
122
+
123
+ ### 6.1 转场音效
124
+
125
+ | 转场 | 音效 | 时长 |
126
+ |---|---|---|
127
+ | 硬切 | 短促 kick / hit | 0.15–0.25s |
128
+ | 擦除 / 推 | 上行扫频 whoosh | 0.25–0.4s |
129
+ | 叠化 | 不需要 | — |
130
+ | 闪白 | 白噪声瞬态 + 低频撞击 | 0.2s |
131
+
132
+ 扫频不要做低频(见 timbre 里的硬禁令),用中高频的噪声过带通。
133
+
134
+ ### 6.2 文字音效
135
+
136
+ 逐字出现时给每个字一个极轻的 tick(−30dBFS),整句一次性给一个 whoosh。
137
+ **不要每个字都给重音**,那会变成打字机噪音。
138
+
139
+ ### 6.3 数字滚动
140
+
141
+ 滚动过程中给一条持续的低频嗡鸣(随进度升调),落定时给一记 hit。
142
+
143
+ ---
144
+
145
+ ## 七、和画面的关系
146
+
147
+ | 画面 | 声音 |
148
+ |---|---|
149
+ | 静止 | 可以有声音(呼吸、底噪) |
150
+ | 快速运动 | 必须有声音(whoosh / 律动) |
151
+ | 切点 | 必须有重音,否则切点「软」 |
152
+ | 留白 | 最好静音,或只有极轻底噪 |
153
+
154
+ **画面在动、声音是空的**,观众会觉得「卡了」。
155
+
156
+ ---
157
+
158
+ ## 八、自检
159
+
160
+ - [ ] 每个 `beats` 里的 impact 都有对应声音?
161
+ - [ ] 重音接的是 `dest` 不是 `bed`?
162
+ - [ ] 铺底比重音低 10dB 以上?
163
+ - [ ] 有没有 2–3 处静默?
164
+ - [ ] 结尾 0.3–0.5s 自然衰减?
165
+ - [ ] 整体峰值不超过 −1 dBFS?
166
+ - [ ] 把音量调到很小再听,卡点还在不在?
167
+ - [ ] 配乐密度有没有起伏(不能全程一个密度)?
@@ -0,0 +1,168 @@
1
+ # 镜头与空间
2
+
3
+ 平面画布也能有空间感。靠的是透视、视差、景深和运动。
4
+
5
+ 这一份讲:2.5D 推拉摇移、视差分层、伪 3D 投影、镜头运动的速度曲线。
6
+
7
+ ---
8
+
9
+ ## 一、镜头运动
10
+
11
+ ### 1.1 推(zoom in)
12
+
13
+ 画面向中心放大。速度曲线决定情绪:
14
+
15
+ | 曲线 | 情绪 |
16
+ |---|---|
17
+ | ease-in-out | 平稳推进(叙事) |
18
+ | ease-out | 猛地推近然后稳住(强调) |
19
+ | 线性 | 机械、冷(科技感) |
20
+
21
+ ```js
22
+ const z = 1 + 0.35 * easeInOutCubic(tween(t, start, 1.2));
23
+ ctx.translate(cx, cy);
24
+ ctx.scale(z, z);
25
+ ctx.translate(-cx, -cy);
26
+ drawScene(t);
27
+ ```
28
+
29
+ 放大超过 2 倍会明显糊(画布分辨率限制)。要推很远就用「换场」而不是「推」。
30
+
31
+ ### 1.2 摇(pan)
32
+
33
+ 画布内容整体平移,模拟摄像机左右摇。
34
+
35
+ ```js
36
+ const dx = -W * 0.25 * easeInOutCubic(tween(t, start, 1.5));
37
+ ctx.translate(dx, 0);
38
+ drawScene(t);
39
+ ```
40
+
41
+ 平移时**背景要比前景慢**(视差),否则像整张图在滑动。
42
+
43
+ ### 1.3 手持感
44
+
45
+ 轻微的随机抖动,让画面不像 CG:
46
+
47
+ ```js
48
+ const rand = mulberry32(Math.floor(t * 30));
49
+ const shakeX = (rand() - 0.5) * 3;
50
+ const shakeY = (rand() - 0.5) * 2;
51
+ ctx.translate(shakeX, shakeY);
52
+ ```
53
+
54
+ 幅度 2–4px。超过 8px 会晕。
55
+
56
+ ---
57
+
58
+ ## 二、视差分层
59
+
60
+ 把画面拆成 3 层,按不同速度移动:
61
+
62
+ ```js
63
+ function drawParallax(t, offset) {
64
+ ctx.save(); ctx.translate(offset * 0.2, 0); drawFar(t); ctx.restore(); // 远景最慢
65
+ ctx.save(); ctx.translate(offset * 0.5, 0); drawMid(t); ctx.restore(); // 中景
66
+ ctx.save(); ctx.translate(offset * 1.0, 0); drawNear(t); ctx.restore(); // 近景最快
67
+ }
68
+ ```
69
+
70
+ 速度比 0.2 : 0.5 : 1.0 是经验值。远景再慢就变成静止了。
71
+
72
+ ---
73
+
74
+ ## 三、伪 3D
75
+
76
+ ### 3.1 透视投影
77
+
78
+ 把 3D 点投到 2D:
79
+
80
+ ```js
81
+ function project(x, y, z, fov = 600) {
82
+ const s = fov / (fov + z);
83
+ return { x: cx + x * s, y: cy + y * s, s };
84
+ }
85
+ ```
86
+
87
+ `s` 是缩放系数,可以拿来同时缩放尺寸和透明度:
88
+
89
+ ```js
90
+ const p = project(x, y, z);
91
+ ctx.globalAlpha = 0.3 + 0.7 * p.s;
92
+ ctx.drawImage(img, p.x - w * p.s / 2, p.y - h * p.s / 2, w * p.s, h * p.s);
93
+ ```
94
+
95
+ ### 3.2 绕 Y 轴旋转
96
+
97
+ ```js
98
+ const a = t * 0.6;
99
+ const rx = x * Math.cos(a) - z * Math.sin(a);
100
+ const rz = x * Math.sin(a) + z * Math.cos(a);
101
+ const p = project(rx, y, rz);
102
+ ```
103
+
104
+ ### 3.3 卡片翻转
105
+
106
+ 单个元素绕 Y 轴翻转,用横向缩放模拟:
107
+
108
+ ```js
109
+ const flip = Math.cos(tween(t, start, 0.5) * Math.PI); // 1 → -1
110
+ ctx.scale(Math.abs(flip) || 0.001, 1);
111
+ ```
112
+
113
+ `flip` 过 0 时要跳过绘制(避免除零或宽度为 0 的怪异渲染)。
114
+
115
+ ---
116
+
117
+ ## 四、景深
118
+
119
+ 近处清晰、远处模糊(或反过来):
120
+
121
+ ```js
122
+ // 远景
123
+ ctx.save();
124
+ ctx.filter = 'blur(6px)';
125
+ ctx.globalAlpha = 0.7;
126
+ drawFar(t);
127
+ ctx.restore();
128
+ // 近景
129
+ drawNear(t);
130
+ ```
131
+
132
+ `ctx.filter` 贵,远景用离屏预渲染的模糊图更省。
133
+
134
+ ---
135
+
136
+ ## 五、构图
137
+
138
+ | 法则 | 用法 |
139
+ |---|---|
140
+ | 三分法 | 主体放在 1/3 线上,不要正中 |
141
+ | 视线方向留白 | 人物/箭头朝向的一侧留更多空间 |
142
+ | 前景遮挡 | 加一个模糊的前景元素(柱子、叶片),立刻有纵深 |
143
+ | 大小对比 | 一个大元素 + 一堆小元素,比均匀分布有层次 |
144
+
145
+ ---
146
+
147
+ ## 六、速度与重量
148
+
149
+ 大物体移动要慢,小物体可以快。同样的位移,大元素用更长的时长:
150
+
151
+ ```js
152
+ const durBig = 0.9; // 大标题
153
+ const durSmall = 0.4; // 小图标
154
+ ```
155
+
156
+ 违反这条,画面会像「轻飘飘的塑料」。
157
+
158
+ ---
159
+
160
+ ## 七、自检清单
161
+
162
+ - [ ] 推镜头的放大倍数 ≤ 2?
163
+ - [ ] 平移时背景比前景慢?
164
+ - [ ] 手持抖动 ≤ 4px?
165
+ - [ ] 3D 投影的缩放同时作用在尺寸上?
166
+ - [ ] 翻转经过 0 时跳过了绘制?
167
+ - [ ] 大元素比小元素动得慢?
168
+ - [ ] 主体没有压在正中(除非刻意对称)?
@@ -0,0 +1,272 @@
1
+ # 调色与质感
2
+
3
+ 纯代码画出来的画面很容易「塑料」:太干净、太平、太像 PPT。
4
+
5
+ 这一份讲:调色板怎么定、层次怎么拉、质感怎么加、什么时候该收手。
6
+
7
+ ---
8
+
9
+ ## 一、先限制调色板
10
+
11
+ 一条片子 3–5 个主色就够。超过 5 个,画面会散。
12
+
13
+ 分配比例(60/30/10 法则):
14
+
15
+ | 角色 | 占比 | 用途 |
16
+ |---|---|---|
17
+ | 底色 | 60% | 背景、大面积色块 |
18
+ | 辅色 | 30% | 次级元素、分隔线、装饰 |
19
+ | 强调色 | 10% | 只用在「要观众看的地方」:数字、关键词、按钮 |
20
+
21
+ **强调色不要平均分布**。整屏都是强调色,等于没有强调。
22
+
23
+ ### 深色片子的起手式
24
+
25
+ ```js
26
+ const PALETTE = {
27
+ bg: '#0b0d12', // 带一点蓝的黑
28
+ surface: '#161a23', // 卡片/面板
29
+ line: '#242a36', // 分隔线
30
+ text: '#e8e6e1', // 带一点暖的白
31
+ muted: '#8b8f9a', // 次要文字
32
+ accent: '#ff8a3d', // 橙
33
+ accent2: '#3fd7c0', // 青
34
+ };
35
+ ```
36
+
37
+ ### 浅色片子的起手式
38
+
39
+ ```js
40
+ const PALETTE = {
41
+ bg: '#f5f2ea', // 米白,不是纯白
42
+ surface: '#ffffff',
43
+ line: '#ddd8cc',
44
+ text: '#1a1815',
45
+ muted: '#6b675f',
46
+ accent: '#d97757', // 陶土橙
47
+ };
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 二、层次靠明度,不靠饱和度
53
+
54
+ 新手喜欢用鲜艳的颜色区分层次,结果画面吵。正确做法是拉开明度:
55
+
56
+ | 层 | 明度(HSL 的 L) | 例子 |
57
+ |---|---|---|
58
+ | 背景 | 8–18% | `#0b0d12` |
59
+ | 次级背景 | 20–30% | `#161a23` |
60
+ | 中间层 | 45–60% | `#4a5060` |
61
+ | 主体 | 85–100% | `#e8e6e1` |
62
+
63
+ 相邻层至少差 20 个百分点,否则在暗环境下会糊在一起。
64
+
65
+ ---
66
+
67
+ ## 三、暗部不纯黑,亮部不纯白
68
+
69
+ `#000` 和 `#fff` 在大屏上生硬,而且会让渐变出现断层。
70
+
71
+ | 用途 | 不要用 | 改用 |
72
+ |---|---|---|
73
+ | 黑底 | `#000000` | `#0b0d12`(冷)或 `#14100c`(暖) |
74
+ | 白字 | `#ffffff` | `#e8e6e1`(暖)或 `#e6eaf0`(冷) |
75
+
76
+ 这一条改完,画面立刻有「片子感」。
77
+
78
+ ---
79
+
80
+ ## 四、渐变
81
+
82
+ ### 4.1 避免色带
83
+
84
+ 两个相近色之间的长距离线性渐变会出现明显条纹(banding),尤其是 8bit 输出。
85
+
86
+ 解法一:加噪点(见第五节)。
87
+ 解法二:用多个中间色标,让过渡更平滑。
88
+
89
+ ```js
90
+ const g = ctx.createLinearGradient(0, 0, 0, H);
91
+ g.addColorStop(0.00, '#0b0d12');
92
+ g.addColorStop(0.35, '#12161f');
93
+ g.addColorStop(0.70, '#1a2030');
94
+ g.addColorStop(1.00, '#0b0d12');
95
+ ```
96
+
97
+ ### 4.2 径向渐变做光
98
+
99
+ ```js
100
+ const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
101
+ g.addColorStop(0, 'rgba(255,138,61,0.55)');
102
+ g.addColorStop(0.4, 'rgba(255,138,61,0.18)');
103
+ g.addColorStop(1, 'rgba(255,138,61,0)');
104
+ ctx.fillStyle = g;
105
+ ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
106
+ ```
107
+
108
+ 半径至少是光源尺寸的 3 倍,否则会看到明显的圆边。
109
+
110
+ ---
111
+
112
+ ## 五、颗粒
113
+
114
+ 加一层极淡的噪点,能消掉渐变色带,也让画面不那么塑料。
115
+
116
+ ### 5.1 预生成噪声贴图
117
+
118
+ ```js
119
+ function makeNoise(size = 256, seed = 1) {
120
+ const c = document.createElement('canvas');
121
+ c.width = c.height = size;
122
+ const x = c.getContext('2d');
123
+ const img = x.createImageData(size, size);
124
+ const rand = mulberry32(seed);
125
+ for (let i = 0; i < img.data.length; i += 4) {
126
+ const v = 128 + (rand() - 0.5) * 255;
127
+ img.data[i] = img.data[i + 1] = img.data[i + 2] = v;
128
+ img.data[i + 3] = 255;
129
+ }
130
+ x.putImageData(img, 0, 0);
131
+ return c;
132
+ }
133
+ ```
134
+
135
+ ### 5.2 平铺
136
+
137
+ ```js
138
+ const noise = ctx.createPattern(makeNoise(256, 7), 'repeat');
139
+ ctx.globalAlpha = 0.03;
140
+ ctx.globalCompositeOperation = 'overlay';
141
+ ctx.fillStyle = noise;
142
+ ctx.fillRect(0, 0, W, H);
143
+ ctx.globalAlpha = 1;
144
+ ctx.globalCompositeOperation = 'source-over';
145
+ ```
146
+
147
+ 透明度:
148
+
149
+ | 值 | 观感 |
150
+ |---|---|
151
+ | 1–2% | 几乎看不见,但能消 banding |
152
+ | 3–4% | 有胶片颗粒感(推荐) |
153
+ | 6% 以上 | 脏 |
154
+
155
+ 噪声贴图要固定种子。每帧都重新生成会抖成雪花屏。
156
+
157
+ ---
158
+
159
+ ## 六、光晕
160
+
161
+ ### 6.1 shadowBlur
162
+
163
+ 最简单,但贵(每帧重算模糊):
164
+
165
+ ```js
166
+ ctx.shadowColor = 'rgba(255,138,61,0.8)';
167
+ ctx.shadowBlur = 24;
168
+ ctx.fillRect(x, y, w, h);
169
+ ctx.shadowBlur = 0; // 复位,否则后续所有绘制都带阴影
170
+ ```
171
+
172
+ `shadowBlur` 超过 40 在 1080p 上明显掉帧。
173
+
174
+ ### 6.2 预渲染光斑
175
+
176
+ 性能更好:预生成一张径向渐变贴图,用 `drawImage` 叠加。
177
+
178
+ ```js
179
+ const glow = (() => {
180
+ const c = document.createElement('canvas');
181
+ c.width = c.height = 256;
182
+ const g = c.getContext('2d');
183
+ const rg = g.createRadialGradient(128, 128, 0, 128, 128, 128);
184
+ rg.addColorStop(0, 'rgba(255,255,255,1)');
185
+ rg.addColorStop(0.35, 'rgba(255,255,255,0.35)');
186
+ rg.addColorStop(1, 'rgba(255,255,255,0)');
187
+ g.fillStyle = rg; g.fillRect(0, 0, 256, 256);
188
+ return c;
189
+ })();
190
+
191
+ // 使用时
192
+ ctx.globalCompositeOperation = 'screen';
193
+ ctx.globalAlpha = 0.6;
194
+ ctx.drawImage(glow, cx - r, cy - r, r * 2, r * 2);
195
+ ctx.globalAlpha = 1;
196
+ ctx.globalCompositeOperation = 'source-over';
197
+ ```
198
+
199
+ ---
200
+
201
+ ## 七、暗角
202
+
203
+ 四角压暗 15–25%,中心不动。观众的视线会自然往中间走。
204
+
205
+ ```js
206
+ const v = ctx.createRadialGradient(W/2, H/2, Math.min(W,H) * 0.35, W/2, H/2, Math.max(W,H) * 0.75);
207
+ v.addColorStop(0, 'rgba(0,0,0,0)');
208
+ v.addColorStop(1, 'rgba(0,0,0,0.28)');
209
+ ctx.fillStyle = v;
210
+ ctx.fillRect(0, 0, W, H);
211
+ ```
212
+
213
+ 暗角和光晕不要同时开到最大,会像廉价滤镜。
214
+
215
+ ---
216
+
217
+ ## 八、色差
218
+
219
+ 快速运动的画面加 1–2px 的 RGB 通道偏移,有镜头感。静止画面加色差只会显脏。
220
+
221
+ ```js
222
+ ctx.globalCompositeOperation = 'lighter';
223
+ ctx.fillStyle = 'rgba(255,0,60,0.5)';
224
+ ctx.fillRect(-offset, 0, W, H);
225
+ ctx.fillStyle = 'rgba(0,220,255,0.5)';
226
+ ctx.fillRect(offset, 0, W, H);
227
+ ctx.globalCompositeOperation = 'source-over';
228
+ ```
229
+
230
+ 只在高光边缘有效,全屏铺一层会变成紫色。
231
+
232
+ ---
233
+
234
+ ## 九、灰度自检
235
+
236
+ 导出前把画面转成灰度看一眼:如果灰度下层次还在,说明结构是对的;
237
+ 如果糊成一团,说明你在靠颜色硬撑,需要把明度关系拉开重画。
238
+
239
+ ```js
240
+ ctx.filter = 'grayscale(1)';
241
+ // 画一遍,截图看
242
+ ctx.filter = 'none';
243
+ ```
244
+
245
+ 或者直接心算:把每个颜色换算成亮度 `0.299R + 0.587G + 0.114B`,看相邻层的差值
246
+ 是否 ≥ 40。
247
+
248
+ ---
249
+
250
+ ## 十、常见错误
251
+
252
+ | 错误 | 后果 | 修正 |
253
+ |---|---|---|
254
+ | 7 种以上主色 | 画面散 | 砍到 3–5 个 |
255
+ | 靠饱和度分层 | 吵、刺眼 | 靠明度 |
256
+ | 纯黑纯白 | 生硬、断层 | 带色相的黑白 |
257
+ | 无颗粒的大面积渐变 | 色带明显 | 加 3% 噪点 |
258
+ | `shadowBlur` 60 | 掉帧 | 用预渲染光斑 |
259
+ | 暗角 + 光晕 + 色差全开 | 廉价滤镜 | 最多两种 |
260
+ | 每帧重新生成噪声 | 雪花屏 | 固定种子 + 预生成 |
261
+
262
+ ---
263
+
264
+ ## 十一、自检清单
265
+
266
+ - [ ] 主色 ≤ 5 个,且 60/30/10 分配?
267
+ - [ ] 相邻明度层差 ≥ 20 个百分点?
268
+ - [ ] 暗部不是 `#000`,亮部不是 `#fff`?
269
+ - [ ] 大面积渐变有噪点压 banding?
270
+ - [ ] 噪点透明度在 3–4%?
271
+ - [ ] `shadowBlur` 用完复位了吗?
272
+ - [ ] 灰度下还能分清层次吗?