@gitruck/cli 0.2.17 → 0.2.18

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.
package/README.md CHANGED
@@ -355,7 +355,7 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
355
355
  >
356
356
  > **素材落盘自检**:写回工程之后自动查一遍 `materials[].path` 是不是真的都落盘了(**只读、只报不动**)。相对路径恒以 **`.gtrk` 文件所在目录**(`<产物目录>/gtrk/`)为基准解析。`--json` 出 `integrity:{ checked, counts, dangling:[…], danglingReferenced, danglingOrphan, external:[…], noPathIds:[…] }`——`dangling` 是工程自带素材的**悬空引用**(登记在、文件不在)全量清单,每条标出**是否被时间线引用**及引用位置(被引用 = 那一段没素材可放,比孤儿严重得多);绝对路径缺失另计 `external`(外接盘没挂载也会这样,不混进主判);http(s) 素材只计数、**不发网络请求**。**这是告知不是拦阻**:查出悬空不改 `ok`、不改退出码、不删任何素材条目或文件。悬空多半是历史遗留(如客户端「确认原片」下载中断),修法是在客户端重新确认原片或删掉那条 clip。没写回的运行(`--lay 0` / 拒铺 / 工程缺失)**不出 `integrity` 字段**——缺席 = 本次没查,不是「查过且干净」。
357
357
  >
358
- > **纯黑底垫轨**:默认在全部候选轨之下、口播主轨之上垫一条纯黑底轨(`struct_meta.broll.black_track` 记其 `track_index`),按已落成的 beat 包络整条铺满,使 B-roll 期间(含候选轨留空处)不漏出底下的口播画面。**代价是「黑底空洞」**:候选轨没填满的地方就是纯黑压口播,铺轨会把它算出来——`--json` 恒出 `lay.blackBedHoleSec` 与逐段的 `lay.blackBedHoles`,单段 ≥ 3s 或单 beat 占比 ≥ 15% 时另出一条非致命告警(不改退出码、不阻断铺轨),可据此调 `--score-floor`、改用 `--no-black-bed`、或到客户端手动补片。字节落 `assets/builtin/solid-000000-<W>x<H>.png`,与客户端内置纯色素材同 id 命名空间、幂等复用。删候选轨时别误删它;换片请拖到候选轨颗粒上、**别拖到黑底条上**——含拖拽保护的客户端会直接拒绝并提示,尚未升级到该版本的客户端会被误拖打出黑底破洞(该处漏口播)。不想要加 `--no-black-bed` 重跑即剥净。
358
+ > **纯黑底垫轨**:默认在全部候选轨之下、口播主轨之上垫一条纯黑底轨(`struct_meta.broll.black_track` 记其 `track_index`),按已落成的 beat 包络整条铺满,使 B-roll 期间(含候选轨留空处)不漏出底下的口播画面。**代价是「黑底空洞」**:候选轨没填满的地方就是纯黑压口播,铺轨会把它算出来——`--json` 恒出 `lay.blackBedHoleSec` 与逐段的 `lay.blackBedHoles`,单段 ≥ 3s 或单 beat 占比 ≥ 15% 时另出一条非致命告警(不改退出码、不阻断铺轨),可据此调 `--score-floor`、改用 `--no-black-bed`、或到客户端手动补片。字节落 `assets/builtin/solid-000000-<W>x<H>.png`,与客户端内置纯色素材同 id 命名空间、幂等复用。删候选轨时别误删它;换片请拖到候选轨颗粒上、**别拖到黑底条上**——**当前所有已发版客户端都没有拖拽保护**(保护代码已落、**待发版**):拖到黑底条上**既不换片也不提示**,而是**静默新建一条 video 轨**把这条 B-roll 插进去;落点在黑底条下半区时新轨排在不透明黑底之下、预览里完全看不见,看着像「什么都没发生」。**按一次 `Ctrl+Z` 即整条撤销**(插入 + 建轨一起回、不留空轨)。不想要加 `--no-black-bed` 重跑即剥净。
359
359
 
360
360
  ### `gtrk mg` — MG 动态图颗粒(铺轨 / lint / status)
361
361
 
@@ -1,233 +1,301 @@
1
- # GSAP-emit 契约 v1 · HTML 动画颗粒的逐帧 seek 渲染合规
2
-
3
- > **契约版本**:gsap-emit v1(2026-07-10;2026-07-24 增补铁律 7「占满坑位 + 终态驻留」,主理人硬性规定;同日铁律 6 增补字体注册表命中规则,对齐 gitruck-infra change `align-render-font-contract`;**2026-07-26 增补「回调与 seek 语义」一节**——补此前留白,首次对**渲染引擎侧**提出 MUST 条款,对齐 change `define-seek-suppress-events-contract`;**同日该节核实状态转「已核实(行为层)」**——真渲染引擎(`@hyperframes/producer@0.6.101`)三帧实测回调可达、结论绑定该引擎版本;
4
- > **2026-07-26 铁律 4 按真机实测改写**——实心底 MUST 下沉为根下第一个全幅子层、根元素 MUST 保持零视觉、overlay 颗粒 MUST 在根显式写 `background:transparent`,
5
- > 附证据锚并消歧铁律编号,对齐 change `align-particle-solid-backdrop-contract`;**同日增补铁律 8「重复图元合并」**——把整组同步驱动的重复图元合并成单元素,以规避一类真渲染横带、纵向重复叠印与逐帧闪烁缺陷;触发轴未知,本条只按零成本写法成文、不设数字门槛,对齐 change `add-particle-primitive-merge-law`)。产 HTML 动画颗粒、经同合云渲染管线(html_animate_render)逐帧 seek 合成的 skill/工具,其产物 MUST 满足本契约。
6
- > **边界**:本契约只约束**机器可判定的管线消费属性**(封装/注册/确定性/自包含/依赖可达/禁 var()/字体名命中注册表)。画面长什么样——颜色、字体取值、构图、节奏——**一律由调用方按其栏目自身规则决定**,本契约不点名任何具体字体/颜色;文中示例取值均为中性占位。
7
-
8
- ## 原理(为什么不能用 CSS animation)
9
-
10
- 渲染引擎逐帧渲染时,靠调用每个子合成在 `window.__timelines` 注册的 GSAP 时间线的 `.seek(t)` 把画面定格到第 t 秒。GSAP `paused` 时间线 = 可被外部 seek 的虚拟时钟 → 逐帧正确;纯 CSS `animation-delay` 动画不在 `window.__timelines` 里,引擎 seek 不到 → 画面冻结(实测)。
11
-
12
- ## 回调与 seek 语义(2026-07-26 增补 · 补此前留白)
13
-
14
- > **一句话**:颗粒**可以**用时间线回调(`onUpdate` 等)驱动画面;**保证这些回调在定帧时被触发是渲染引擎侧的义务**,不是颗粒作者的义务。
15
- > 本节是该主题的**唯一口径来源**——契约引用(`skills/gtrk-style-maker/references/contracts-ref.md`)与消费侧检查(`gtrk mg lint`)的文案都指向这里,不得各自表述。
16
-
17
- ### 一、GSAP 3.13.0 实测事实(写死在此,免后来者重测)
18
-
19
- 复现方式:拉铁律⑤指定 CDN 的 `gsap.min.js` 到本地,Node `vm` 里造 `paused` timeline + proxy 对象 + 计数回调,**每种定位方式各新建一条 timeline 单独跑**(回调计数互不串)。
20
-
21
- **样本一**——10s、`ease:"none"` 的单条 tween(`{v:0}` → `{v:100}`),分别 seek 到 t=5(中途)与 t=10(末端):
22
-
23
- | 定位方式 | `onUpdate` / `onStart` | `onComplete` | 补间目标属性 |
24
- |---|---|---|---|
25
- | `tl.seek(t)`(默认,`suppressEvents` 缺省为 `true`) | **不触发** | **不触发**——连 `seek(10)`(到末端)也不触发 | 已更新(t=5 → `v=50.00`;t=10 → `v=100.00`) |
26
- | `tl.seek(t, true)` | 不触发 | 不触发 | 已更新 |
27
- | `tl.seek(t, false)` | 触发 | `seek(10,false)` 触发;`seek(5,false)` 不触发 | 已更新 |
28
- | `tl.time(t)` / `tl.progress(p)` / `tl.totalTime(t)` | 触发 | 同上(按是否到末端) | 已更新 |
29
-
30
- ⚠️ **`onComplete` 那两行必须分清**:默认 seek 下它**到末端也不触发**,这才是「被抑制」的证据;而 `false` 模式下 `seek(5,false)` 的 `onComplete` 不触发,只是「补间尚未走完」的正常语义。混为一谈,后来者会拿后者误判成「垫片无效」。
31
-
32
- **样本二(含真实 DOM 写入 · 引擎侧条款的直接依据)**——同一 t=1.0 定帧点,补间在 t=1.0 处的目标属性期望值 75.0,`onUpdate` 体内做一次真实 DOM 写入:
33
-
34
- | 定位方式(同一 t=1.0 定帧点) | `onUpdate` 触发次数 | 补间目标属性 | 回调内的 DOM 写入 |
35
- |---|---|---|---|
36
- | `tl.seek(1.0)`(默认) | **0 次** | 已更新为 **75.0** | **未执行** |
37
- | `tl.seek(1.0, false)` | 1 次 | 75.0 | 正常 |
38
- | `tl.time(1.0)` | 1 次 | 75.0 | 正常 |
39
- | `tl.progress(0.5)` | 1 次 | 75.0 | 正常 |
40
-
41
- 要害是**「值更新了、回调没跑」**:补间目标对象的属性照常插值,但写在 `onUpdate` 里的 DOM 写入一次都不执行。所以这种翻车**不是黑屏,是画面定在初始态**——本地播放器与客户端预览都完全正常,比黑屏更难发现。
42
-
43
- 样本二同时给出条款一的**可行性依据**:三种「不抑制」写法(`seek(t,false)` / `time(t)` / `progress(p)`)行为**完全一致**,故对引擎侧的要求**可实现且不挑实现**——引擎爱用哪种用哪种,只要不是裸 `seek(t)`。
44
-
45
- ### 二、条款
46
-
47
- 1. **引擎侧(MUST)**:渲染引擎定帧时 **MUST 保证 GSAP 时间线回调可达**——MUST 用 `seek(t, false)` 或 `time(t)` / `progress(p)` / `totalTime(t)`,**MUST NOT 用默认的 `seek(t)`**。违反后果:补间目标属性照常更新、回调内的 DOM 写入不执行 → 画面**定在初始态而非黑屏**,本地播放器与客户端预览均看不出异常。
48
- 2. **颗粒侧(合规声明)**:颗粒**可以**用时间线回调(`onUpdate` / `onStart` / `onComplete` / `onRepeat`)写 DOM / 属性来驱动画面——这是**合规**写法(相机推进、数值读数这类效果用纯属性补间难以表达)。其可达性由条款 1 承担;契约 **MUST NOT** 反过来要求颗粒作者自行保证。
49
- 3. **颗粒侧(不应)**:颗粒**不应**在运行时覆写引擎所调用的 API——重新赋值 `tl.seek`、或把 `window.__timelines[…]` 换成包装对象。两条实测理由:① 覆写会推翻引擎**显式**传入的 `seek(t, true)`(实测:打上垫片后 `seek(5)` 与 `seek(6, true)` 两次调用回调都触发),颗粒无权静默否决引擎的意图;② 垫片只作用于 `seek`,引擎改走 `time()` / `progress()` 时**完全失效**,而作者不会知道。
50
- 已存在的垫片属**过渡态**:**新颗粒不应再加**。**2026-07-26 更新**——条款 1 的前提已经真引擎核实(见下「三」),垫片赖以存在的「真空期」已结束,故已存在的垫片**可择期清理**(清理是**可做**、不是 MUST 做;删后须重跑 lint 并**重渲复验**)。垫片留着也无害:实测引擎既然本就不抑制回调,垫片对画面结果无影响,只会被 lint 记一条非致命的 `x-engine-api-override`。
51
- 4. **作者侧应知(这不是甩锅)**:作者 MUST 知晓——回调可达性的保证方是**引擎侧**,且该保证当前的核实状态见下「三」(2026-07-26 起为**已核实(行为层)· 绑定引擎 0.6.101**)。`gtrk mg lint` 会对「回调驱动画面且无任何兜底」的颗粒给一条**非致命**提醒,那是**哨兵**(引擎换实现或失守时有人喊一声),**不是**「请作者自行保证 seek 下回调可达」——**核实之后哨兵照留**,因为结论绑死引擎版本,换版即须重测。
52
-
53
- ### 三、核实状态:**已核实(行为层)· 2026-07-26 · 绑定引擎 `@hyperframes/producer@0.6.101`**
54
-
55
- **把某条引擎行为写成 MUST,MUST NOT 被当作它已被核实**——二者在本节分别成文。本小节记的是**核实到了什么、以及没核实到什么**。
56
-
57
- **结论:真渲染引擎在定帧时 `onUpdate` 回调可达,条款 1 的前提成立。** 结论**绑定上述引擎版本**,换版即失效、须重测。
58
-
59
- - **核实方式(真引擎,非本地无头模拟)**:造一颗最小颗粒 `seekcb-probe`,**不打任何 seek 垫片**,画面正中的大号数字**只**由一条 tween 的 `onUpdate` 写入(proxy `{v:0}` → `480`,`ease:"none"`,16s),初值静态写死 `CB 000`;同屏并排一组**纯属性补间**对照物(方块横移 + 进度条拉长,均无回调),刻度尺与回调数字**同单位 0..480**,故单帧之内即可比对「应该是多少」与「实际是多少」。该颗粒先过 `gtrk mg lint`(`lint.ok=true`,`opaque=true`,只出 `x-callback-driven` 哨兵、无 `x-engine-api-override`),排除「因不合规才没渲」。走生产同款装配(`CompositionRender` 整轨路径 / 同底轨 `bed.mp4` / 1920×1080 / fps=60 / 15.97s / 958 帧分片 13 chunk)。
60
- - **实测结果(三帧,肉眼判读整帧 PNG)**:
61
-
62
- | 抽帧时刻 | 回调驱动的数字(onUpdate 唯一驱动) | 纯属性补间对照物 | 期望值 |
63
- |---|---|---|---|
64
- | t=2s | **CB 060** | 进度条/方块 @ 60 | 060 |
65
- | t=8s | **CB 240** | 进度条/方块 @ 240 | 240 |
66
- | t=14s | **CB 420** | 进度条/方块 @ 420 | 420 |
67
-
68
- 三帧数字**各不相同**、**无一帧停在初值 `CB 000`**,且**逐帧与同单位对照物精确重合**——即回调不仅被触发,还是在**当前帧对应的时间点**上触发的(不是滞后一帧的陈值)。对照物同时移动,排除「整颗没渲/时间线没被定位」这一混淆解释。
69
- - **证据留档**:帧 `D:/file/ops/_seekcb/SEEKCB-t2.png` / `-t8.png` / `-t14.png`;成片 r69 `/opt/gitruck/tmp/backdrop-ab-2026-07-26/SEEKCB.mp4`;颗粒 `D:/file/ops/_seekcb/P-seekcb.html`;渲染脚本 `_seekcb_render.py`。引擎版本取自渲染日志自报的 `"producerVersion":"0.6.101"`。
70
- - **⚠️ 本次核实的边界(MUST NOT 越界引用)**:核实的是**可观测行为**(回调可达),**不是调用形态**。引擎仍是无源码第三方包,本轮**没有**读到定帧调用点,故**无法区分**它走的是 `seek(t,false)` / `time(t)` / `progress(p)`,还是压根不经 GSAP 定位 API 的自有推进方式。条款 1 里那串 API 白名单仍是**应然的实现约束**,只有「回调 MUST 可达」这个**结果**被真机背书。调用形态的核实仍由 gitruck-infra 侧联动 change `link-guarantee-seek-callback-reachability` 承接(读调用点 + 把「纯回调驱动无垫片颗粒渲两帧、像素 MUST 不同」挂进引擎版本变更门禁)。
71
- - **旁证(本轮之前的间接证据,保留备查)**:gitruck-infra 自家 9 个 music_visualizer 模板 **9/9** 把整块画面挂在单条 tween 的 `onUpdate` 上、且 **0/9** 带任何垫片,走的正是同一渲染内核,而 `gtrk music-visualizer` 是**已上线**能力——若引擎默认抑制回调,这 9 个模板早该渲成静止首帧。本仓 exemplar b06/b07/b08 同理。本轮直接核实**与旁证同向**。
72
- - **假设失效的后果(哨兵为何 MUST 留)**:结论绑定 `0.6.101`;引擎升版换调用方式 → 全部回调驱动型颗粒(MG 与 music_visualizer 模板)**一起静默冻结**,lint 全绿、退出码 0、无任何告警。故 `gtrk mg lint` 的 `x-callback-driven` 哨兵(条款 4)**MUST NOT 因本次核实而撤**——它防的正是「换版后无人喊一声」。
73
-
74
- ### 四、连带风险:回调可达 = 逐帧反复触发
75
-
76
- 条款 1 的另一面:逐帧 scrub 时 `onComplete` / `onStart` / `onRepeat` 会**反复触发**(每帧一次)。故颗粒 **MUST NOT** 把「只跑一次」的逻辑写进回调——累加计数、`push` 进数组、一次性 DOM 插入、抽样。那会把「静默冻结」换成「静默错乱」。
77
-
78
- **2026-07-26 起这条从「假设的另一面」变成「已确证的现实风险」**:上「三」已实测该引擎**确实**在逐帧定位时触发回调,故本条不是防患于未然,而是**当下就在生效**的约束。
79
-
80
- **推荐写法**:回调做成**幂等**的——每次都从补间状态**重算**整个画面,不依赖上一次的结果(真机与 exemplar 的 `render()` / `apply()` / `paint()` 均如此)。
81
-
82
- ### 五、优先级
83
-
84
- **本节口径优先于任何栏目指南的招式建议。** 栏目指南若与本节冲突,以本节为准。(当前二者同向:栏目指南推荐的 `onUpdate` 驱动写法在本节下**合规**,作坊侧**无需修订**——本节不向作坊侧派发任何待办。此条照写不误,它防的是将来的分歧。)
85
-
86
- ## 八条铁律(违反任一条 → 整片渲染失败 / 颗粒冻结 / 全黑 / 坑位内突兀消失 / 画面被切成横带、内容纵向重复叠印并逐帧闪烁)
87
-
88
- 1. **`<template>` 包裹根元素**:`<template><div data-composition-id="<id>" data-width="1920" data-height="1080">…</div></template>`。编译器取的是 `<template>` 内容;裸 `<div>` 会被判 "empty or could not be parsed" 整片失败。(1920×1080 为**当前引擎版本约束**,分辨率参数化预留——升版时以本契约新版为准。)
89
- 2. **GSAP `paused` 时间线 + 注册**:`var tl = gsap.timeline({paused:true}); … window.__timelines = window.__timelines || {}; window.__timelines["<id>"] = tl;`。`<id>` 必须等于根的 `data-composition-id`。
90
- 3. **确定性**:禁用 `Math.random` / `Date.now` / 无参 `new Date()`(过不了引擎 StaticGuard)。要"随机感"用固定种子/解析式/递归生成(见「确定性配方」)。
91
- 4. **自包含 + 实心底下沉子层 + 透明度显式声明**(**2026-07-26 按真机实测改写**;原文「全屏颗粒给根设明确 `background`;透明颗粒根不设 background」**已作废**,见下方证据锚):颗粒不依赖外部文件(除脚本 CDN)。底色的**取值**由调用方按其栏目规则决定,本契约只管**摆在哪个元素上**。
92
- - **① 实心底 MUST 下沉为根下第一个全幅子层**(`position:absolute;inset:0` 或等价的 `top/left/width/height` 铺满),**MUST NOT 写在根元素的 `style` 上**。理由是实测事实:**根元素的绘制属性在子合成挂载时被丢弃**,写在根上的实心底**一个像素都不落地**——成片里表现为「浮空面板」(前景照常渲出、底没了、底轨透出来),本地播放器与客户端预览都看不出。
93
- - **② 根元素 MUST 保持零视觉**:根 `style` MUST NOT 出现任何会绘制像素的属性(实心 `background` / `border` / `box-shadow` / `outline` …)。写了不报错,但**不生效**,只会误导后来者以为底已经有了。
94
- - **③ 透明与否 MUST 显式声明**:`background` 声明 MUST 至少出现一处——**满屏(不透明)颗粒**写在 ① 的全幅子层上;**透明叠加(overlay)颗粒** MUST 在**根**显式写 `background:transparent`,MUST NOT 靠「不写」表达透明。根上的 `transparent` 虽然同样不落成像素(它本就不绘制),但它是**给人和机器读的意图声明**——缺了它,作者与 `gtrk mg lint` 都无从区分「想透明」与「忘了想」。
95
- - **④ 消费侧同源**:`gtrk mg lint` 的 `opaque` 推导面 = 「根 `style` ∪ 根下首个全幅子层 `style`」,与本条 ①③ 同源;两处皆无 `background` 声明才报 `4-bg-explicit`,满屏颗粒把实心底写在根上另报非致命 `4-bg-on-root`。
96
- 5. **脚本用渲染机可达的 CDN(编译期内联)**:`<script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>` 或由渲染管线 vendor 本地。⚠️ jsdelivr 在渲染服务器不稳(实测 compile 期 `fetch failed` → GSAP 未加载 → 整片全黑)。编译器**只内联 http(s) CDN、不内联相对本地路径**(写 `src="gsap.min.js"` 运行时 404)。
97
- 6. **颜色/字体用字面值,禁 CSS `var()` 自定义变量;字体名 MUST 命中服务端注册字体表**:编译器/挂载不可靠地解析 var()(字体映射把 `var(--font-body)` 当字面字体名;颜色 var() 不应用 → 整片全黑,实测)。直接写字面值(如 `#RRGGBB` / `'某字体名'`);SVG 属性里同样禁 var()。栏目级换色/换主题 = **生成期**替换字面值(查调用方自己的词表/token 注入),不是运行时变量。**字体名规则(2026-07-24 增补)**:font-family 的每个具名家族 MUST 逐字符命中渲染服务端注册字体表(gitruck-infra 仓 `utils/assets/text/classic_template/font_manifest.json`,中英别名等价),并 SHOULD 以 `sans-serif`/`serif` 通用族收尾兜底;表外名字渲染不失败但**字形不保证**(服务端 fail-open 系统回退,2026-07-24 真机实锤:错名导致宋体被渲成回退黑体)。具体选哪款仍由调用方栏目规则决定,本契约不点名。
98
- 7. **占满坑位 + 终态驻留(2026-07-24 主理人硬性规定;2026-07-26 增补无限循环定性)**:颗粒时间线总长 MUST ≥ 它在成片中的**坑位时长**(落轨 clip 的实际时长,通常 = 派单槽位包络 `track_ed − track_st`,**不是** `duration_hint`);动画主叙事播完后,颗粒 MUST 以「**定格保持**」或「**有限次循环**」驻留到坑位末尾。**禁用无限循环 `repeat:-1`(2026-07-26 增补,补此前留白)**:无限循环让时间线总长为 `Infinity`,本条「总长 ≥ 坑位」就**变得不可验证**(机器与人都无从判断它是否真按坑位算过),且掩盖「作者根本没算坑位」这件事。循环次数 MUST **按坑位算死**——`repeat = ceil((坑位时长 − 循环起点) / 单圈时长) − 1`,宁可多算一两圈(末尾被 clip 裁掉无害),也不许写 `-1` 蒙混——坑位内任意时刻(含最后一帧)核心内容必须可见。**禁止**「整体渐隐到空 / 全局退场 / 清空画面」类收尾:渐隐会与剪辑层转场冲突,淡出与否由剪辑/装配层决定,不在颗粒内做。局部元素可按叙事退场(黯淡/让位),但画面在坑位内不得归零;**定格不动是完全合法的终态**(不必为凑动作密度在尾段硬加动画)。违反表现 = 观感上「动画一过完整个颗粒突兀消失」(2026-07-24 回声定位真机实测)。
99
- 8. **重复图元合并(2026-07-26 增补 · 规避条款,根治在渲染侧)**:同一批**重复图元**——同色、同 `stroke` / `fill`、**整组同步驱动**(只有组级动画或完全静态)的网格、排线、刻度、点阵——**MUST 合并成单个元素**(SVG 用一条 `<path>` 的多子路径 `M…H…V…`;同色分档时可按档合成若干条),**MUST NOT** 用「每条线 / 每个格子一个 `<element>`」的批量构建,无论它写成字面标签还是由 `createElementNS` 循环生成。合并前后画面**逐像素相同**,故本条不涉任何视觉取值。
100
- - **豁免**:需要**逐元素动画**(stagger / 逐条画入 / 逐个变色)的批次不在本条范围内——元素身份是该批动画所必需的。此类批次若同时是满幅大面积绘制,MUST 以真渲染出片抽帧验收;这是保守兜底,**不是**「满幅即危险」的风险判据,反向的「不满幅即安全」同样不成立。
101
- - **违反表现**:画幅被切成等高横带、相邻带内容互相纵向重复叠印(同一元素在 `y` 与 `y±带高` 各画一次,或被顶部裁半)并逐帧闪烁;该表现与动画逻辑无关,同帧里零动画的静态元素也会重复。DOM 元素数与 `getBoundingClientRect` 均正常,本地播放器与客户端预览看不出。
102
- - **这是规避,不是根治**:缺陷出在渲染侧已由真机行为确证——颗粒的 DOM 元素数与布局值正常,不是颗粒写错。机制层面**推断**为渲染侧按等高带做局部栅格刷新时失效区域算错,**属推断、未经渲染侧确认**;**真实触发轴至今未知**,「图元总数」「同页累积负载」「同父同类聚簇」均已被跨颗粒真渲反例否证。本条 MUST NOT 被读成「图元少 / 负载低就安全」。
103
- - **根治登记**:根治方 = 渲染管线侧;当前状态 = **未立项(2026-07-26;决策角色 = 本 change 实施者,按 proposal 的跨仓授权边界不代提;是否立项仍待主理人另行授权)**。
104
- - **根治后处置预案**:待渲染管线侧根治上线且经真机确认缺陷不再复现后,本条由 **MUST 降为 SHOULD**,并保留「本条曾于 2026-07-26、本证据锚所载引擎版本口径下作为规避所必需」的历史记录。
105
-
106
- > **⚠️ 铁律编号的唯一定义在本文件。** 本契约的**铁律 7 = 「占满坑位 + 终态驻留」**,这是框架侧对该条号的**唯一**定义。
107
- > 创作侧(栏目 skill / 作坊)的模板与 references **MUST NOT 另立同号铁律**——引用请写「gsap-emit v1 铁律 7」并指回本文件,
108
- > 自己的栏目约束请用**栏目注记**或另起独立编号空间。
109
- > **已知撞号(2026-07-26 记录,收口中)**:某栏目 references 曾把「根元素零视觉」编为其「铁律 7」、同一条在其模板里又编为「铁律③」——
110
- > 该约束现已**升为本契约的铁律 4**(见上),栏目侧的两个同名条号 SHALL 改为引用铁律 4,不再另立。
111
-
112
- ### 铁律 4「实心底下沉子层」的真机证据锚(2026-07-26)
113
-
114
- 本条断言的是**渲染引擎行为**(「根元素的绘制属性在挂载时被丢弃」),故按契约库要求附可追溯证据锚:
115
-
116
- - **结论**:根 `style` 的 `background` **不落成像素**;同一颗粒把同一底色改写到根下首个全幅子层则**满屏落地并盖住底轨**。
117
- - **实验日期 / 机器**:2026-07-26 · r69(真渲,非本地无头模拟)。
118
- - **引擎版本口径**:hyperframes CLI `0.6.101` · `@hyperframes/producer` `0.6.101` · chrome-headless-shell `linux-131.0.6778.85`。
119
- - **设计**:同颗粒母本造两个变体,**唯一变量 = 实心底摆在根 `style` 还是根下首个全幅子层**,其余逐字节相同;
120
- 底色临时取高饱和标志色以便与「根合成兜底黑」区分;底轨用一段有明显画面、非黑的真人镜头;抽同一时间点 `t=8.0s` 的帧,
121
- 取四角 `crop=40:40` 于 `(20,20) (1860,20) (20,1020) (1860,1020)` 后 `scale=1:1` 读像素。
122
- - **观测**(整轨拆分渲染路径,两格同素材同引擎同批)。
123
- ⚠️ 下表的六位数字是**抽帧读出的 RGB 像素读数**(实验观测),**不是本契约规定的任何底色**——
124
- 轴一边界照旧:契约不点名任何色值,底色取值永远由调用方栏目决定:
125
-
126
- | 变体 | 四角像素 | 判读 |
127
- |---|---|---|
128
- | 实心底写**根 `style`** | `dfdad4 / fafcfb / 4e4f52 / f9fbfb` | ≈ 底轨基线 → **底丢失、底轨透出** |
129
- | 实心底写**根下首个全幅子层** | `01ff81 / 00fe80 / 01fd80 / 01fd80` | ≈ 标志色 → **底存活、满屏盖住底轨** |
130
- | 底轨本身(阴性对照) | `e0dbd4 / fbfdfc / 4f5052 / fbfdfd` | 四角始终高亮非黑,对照有效 |
131
-
132
- - **阳性对照(关键,排除平凡解)**:「写根」那格的整帧里颗粒**前景元素完整渲出**(标题文字、网格、连线、色块都在)——
133
- 所以它**不是「颗粒没渲上」,而是「颗粒渲上了、根 `background` 被丢掉」**。缺这一步,两种情况在像素上同解。
134
- - **复现方式**:造上述两变体 → 同一批走整轨拆分渲染 → `ffmpeg -ss 8.0 -i <out>.mp4 -frames:v 1 -vf "crop=40:40:<x>:<y>,scale=1:1" -f rawvideo -pix_fmt rgb24 - | xxd -p` 读四角。
135
- - **历史**:本条现象最早由创作侧于 **2026-07-15** 记载(一颗满屏颗粒的根 `background` 丢帧、成片只剩浮空面板),
136
- 当时未进框架契约,框架正本反而长期要求「写在根上」——2026-07-26 的实验复现并坐实了该记载,本条据此改写。
137
- - **相关跨仓 change**:`gitruck-cli` / `align-particle-solid-backdrop-contract`(本条的来源);
138
- `gitruck-infra` / `add-html-animate-opaque-fullscreen-cover`(**另一个独立问题**:渲染侧要不要按 `opaque` 位替颗粒兜一层黑底)。
139
- ⚠️ 两者**互不依赖**:上表的观测正是在后者**未 apply** 时取得的——即**本条的修法(下沉子层)当场生效,不需要渲染侧任何配合**。
140
-
141
- ### 铁律 8「重复图元合并」的真机证据锚(2026-07-26)
142
-
143
- 本条断言的是一条**作者侧规避写法**及其在真渲染中的效果,不断言已知触发轴。证据锚如下:
144
-
145
- - **结论**:同一故障样本把 37 条满幅 `<line>` 合为一条 `<path>` 多子路径后,目标画面逐像素不变,横带重复率由 **65%~68% 降至 2%**;规避当场有效,但合并同时改变了图元数、DOM 结构与栅格化路径,故本实验**不能证明是哪一个变量生效**。
146
- - **实验日期 / 机器**:2026-07-26 · r69(真渲,非本地无头模拟)。
147
- - **引擎版本口径**:渲染引擎 CLI producer 同版 `0.6.101` · 无头浏览器内核 `131.x`。
148
- - **二分设计**:以同一颗粒为母本逐项只改一个构建块,每个变体各做一次真渲;每格抽取 168 帧,按等高横带间的重复像素统计复现率。装配路径、分辨率、帧率、底轨与其余颗粒代码保持一致。
149
- - **范围限定(硬约束)**:下表全部是**同一颗粒内部**的二分结果,**MUST NOT 跨颗粒外推**;它只能说明这些改动在该颗粒内与复现率同向变化,不能推出「元素数少就安全」或任何数字门槛。
150
-
151
- | 同一颗粒内的变体 | 重复率 | 限定读法 |
152
- |---|---:|---|
153
- | 原件:背景网格为 37 条满幅 `<line>` | **65%~68%**(复跑一致) | 故障基线 |
154
- | 37 `<line>` 合为一条 `<path>` 多子路径 | **2%** | 本条规避正例;目标画面逐像素不变 |
155
- | 整块删除背景网格 | **2%** | 与合并同档;只说明该块在本颗粒内参与复现 |
156
- | 删除两个静态术语文本块 | **0%~1%** | 删除任一块负载都退烧;作用域仅限本颗粒,机制未知 |
157
- | 删除三行静态标签文本块 | **0%~1%** | 同上 |
158
- | 换字体族 / 去斜体 / 关闭 3D 加速 / 改入场位移 / 把文字改为 SVG `<text>` / 给根加变换提示 / 把网格改静态 | **仍为 65%+** | 均无效 |
159
- | 增加满屏失效脉冲层 | **14%** | 作者侧强制全幅失效仍不能根治 |
160
- | 极简结构(20 个静态 `<div>` + 1 个网格) | **0%** | 空载变体不复现;不得外推为通用预算 |
161
-
162
- - **阳性对照(排除平凡解)**:合并版仍完整绘制与原件相同的整张网格,逐像素对拍一致;它不是「把内容删没了所以不重复」。同一颗粒内另有 24 条局部短线在合并版中继续保留,故 24 与 37 也 **MUST NOT** 被写成安全 / 故障分界。
163
- - **跨颗粒反证(同批全量体检)**:同装配、同引擎、同机完成 **21 颗粒 + beat 同页聚合,共 24 次渲染 / 约 22,000 帧**。这些结果专门限制上表的外推范围:
164
-
165
- | 样本 | 真渲结果 | 可否证的候选轴 |
166
- |---|---:|---|
167
- | 另一密阵样本:884 条短划、全颗共 921 个绘制图元 | **0.0% 脏帧** | 图元总数、同父同类聚簇 |
168
- | beat 同页聚合:约 1,400 个绘制图元 | **0.0% 脏帧** | 同页累积负载 |
169
- | 当前修复版样本(回归 / 阴性对照) | **0.0% 脏帧** | 规避在整轨分片路径下仍稳 |
170
- | 未修版样本(阳性对照) | **70.3% 脏帧** | 缺陷仍活且可稳定复现 |
171
-
172
- **真实触发轴至今未知**;当前已否证的候选轴 = **图元总数 / 同页累积负载 / 同父同类聚簇**。上表中「921 个图元干净」与「较少图元的阳性对照中招」同时成立,因此数字方向不存在可据此写入契约的安全门槛。
173
- - **复现方式**:从同一母本生成上述单变量变体 走生产同款整轨分片渲染 对每格 168 帧计算等高横带间的重复率并抽取高分帧肉眼复核;再以同一口径跑 21 颗全量语料、六 beat 同页聚合、修复版阴性对照与未修版阳性对照。复现件的本地路径与文件名不进入分发契约。
174
- - **相关 change / 根治状态**:本仓 `add-particle-primitive-merge-law` 收录作者侧规避;渲染管线侧根治**未立项(2026-07-26,本 change 实施会话按非目标不代提;待主理人另行决定)**。根治上线并经真机确认后,按铁律 8 的预案把 MUST 降为 SHOULD。
175
-
176
- ## 颗粒骨架(中性模板)
177
-
178
- ```html
179
- <template id="p">
180
- <!-- 铁律4:根 MUST 零视觉。这里的 background 恒为 transparent(显式声明,不是省略),实心底一律下沉到下面的 .bgfill 子层 -->
181
- <div data-composition-id="<id>" data-width="1920" data-height="1080"
182
- style="position:absolute;inset:0;background:transparent;overflow:hidden;font-family:'<你的字体·须命中服务端 font_manifest.json>',sans-serif;">
183
- <!-- 铁律4①:实心底 MUST 是根下第一个全幅子层。满屏颗粒填你栏目的底色;透明叠加(overlay)颗粒把整行删掉 -->
184
- <div class="bgfill" style="position:absolute;inset:0;background:<你的底色>;z-index:0;"></div>
185
- <style> [data-composition-id="<id>"] .xxx{ } </style> <!-- 样式用属性选择器作用域,防跨颗粒污染 -->
186
- <svg viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid meet" style="position:absolute;inset:0;width:100%;height:100%;">…</svg>
187
- <script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>
188
- <script>(function(){
189
- var ROOT='[data-composition-id="<id>"]';
190
- /* 1) 确定性构建静态结构 */
191
- /* 2) gsap.set 初始态 */
192
- /* 3) var tl = gsap.timeline({paused:true}); … 编排 … */
193
- window.__timelines = window.__timelines || {};
194
- window.__timelines["<id>"] = tl;
195
- })();</script>
196
- </div>
197
- </template>
198
- ```
199
-
200
- > **骨架里 `.bgfill` 那一行的三个要点**:① 它 MUST 是**根下第一个**子元素(后续前景层自然压在它之上,不必给前景排 z-index);
201
- > ② 它 MUST **全幅**(`position:absolute;inset:0`,或等价的 `top/left/width/height` 铺满)——不铺满就不是「实心底」;
202
- > 类名 `bgfill` 只是**惯例**,其底色写在**行内 style** 上(不靠 `<style>` 里的类规则),故不受「样式作用域」那条专属坑影响;
203
- > 换名字不违约,但 `gtrk mg lint` 认的是「根下首个全幅子层的 `background`」这个**结构**,不认类名。
204
-
205
- 缓动可用 CustomEase 精确还原你栏目自己的 cubic-bezier(缺插件时给近似回退)——bezier 数值属于栏目审美,本契约不规定。
206
-
207
- ## 确定性配方(替代 random)
208
-
209
- - 递归结构:固定角度、比例、深度参数 完全确定。
210
- - "噪声感":`Math.sin(i*0.18 + j*0.2)` 类解析式伪随机。
211
- - 打散:用 index 派生(如 `i*137.5°` 黄金角),不要 `Math.random()`。
212
-
213
- ## 验证(交付前必做)
214
-
215
- 墙钟截图类工具驱动不了 paused 时间线(只看到 t=0),**不能**用来验收。必须真渲染引擎 seek 验证:
216
- 1. 颗粒放进最小 composition(root `index.html` 用 `data-composition-src` 引它);
217
- 2. 走渲染管线(html_animate_render)渲染;
218
- 3. 抽不同时间点的帧**比对应当不同**(相同=冻结=铁律没守住)。客观自检:根有 `<template>`、`window.__timelines["<id>"]` 已注册且 id 匹配、无 random/Date、tl 总长 颗粒时长。
219
- > ⚠️ 不要用「本地等价 seek 脚本 / Node 无头模拟」替代真引擎渲染——它不经真编译+挂载,测不出 var()-不解析、CDN-内联失败、StaticGuard 这类只在真引擎暴露的问题(实测教训:本地等价测试报 OK,真引擎全黑)。
220
- >
221
- > ⚠️ **客户端预览对「底色 / 透明度」类问题结构性失明,MUST NOT 用作这类验收的判据**(2026-07-26 增补)。
222
- > 客户端预览会按 clip 登记的 `opaque` 位**给颗粒根盒强行打底**(`!important`,恒胜过颗粒自身的设计期底色)——
223
- > 于是无论根 `background` 有没有被丢弃、有没有实心底子层,**预览看到的都是「按登记值应该长的样子」**。
224
- > 铁律 4 的证据锚正是靠**真渲染出片抽帧**取得的,不是靠预览。预览截图只作对照留档,不入判据。
225
-
226
- ## 专属坑
227
-
228
- - **样式作用域**:颗粒与其他颗粒/根同处一个文档,全局类会撞——用 `[data-composition-id="<id>"] .xxx` 属性选择器作用域。
229
- - **`<template>` 内的 `<script>` 默认不执行**——引擎把 template 内容克隆进文档后才执行;本地直接开浏览器不会跑,必须经引擎/player。
230
- - **transform-origin(SVG)**:缩放 `<g>` GSAP `svgOrigin:"x y"`(SVG 用户坐标),别用 CSS transform-origin。
231
- - **时间线总长 坑位时长**:颗粒 tl 总时长 ≥ 落轨 clip 时长(坑位包络),否则 seek 越界(已升格为铁律 7,含终态驻留要求)。
232
- - **整组同步驱动的重复图元必须合并**:网格、排线、刻度、点阵用单元素表达,SVG 以一条 `<path>` 的多子路径合成;逐元素动画批次豁免。详见 gsap-emit v1 铁律 8。
233
- - **别用 `requestAnimationFrame`/`setInterval` 驱动画面**——不被 seek,等于冻结。所有视觉变化必须挂在 tl 上。(与「回调与 seek 语义」一节同源:任何**不经 tl** 的自有时钟都不被定帧驱动;而挂在 tl 上的回调是否被触发,则由该节的引擎侧条款保证。`gtrk mg lint` 对本条给**非致命**项 `x-raf-interval`——静态正则分不清「驱动画面」与其它用途,故只提醒不拦。)
1
+ # GSAP-emit 契约 v1 · HTML 动画颗粒的逐帧 seek 渲染合规
2
+
3
+ > **契约版本**:gsap-emit v1(2026-07-10;2026-07-24 增补铁律 7「占满坑位 + 终态驻留」,主理人硬性规定;同日铁律 6 增补字体注册表命中规则,对齐 gitruck-infra change `align-render-font-contract`;**2026-07-26 增补「回调与 seek 语义」一节**——补此前留白,首次对**渲染引擎侧**提出 MUST 条款,对齐 change `define-seek-suppress-events-contract`;**同日该节核实状态转「已核实(行为层)」**——真渲染引擎(producer `0.6.101`)三帧实测回调可达、结论绑定该引擎版本;
4
+ > **2026-07-26 铁律 4 按真机实测改写**——实心底 MUST 下沉为根下第一个全幅子层、根元素 MUST 保持零视觉、overlay 颗粒 MUST 在根显式写 `background:transparent`,
5
+ > 附证据锚并消歧铁律编号,对齐 change `align-particle-solid-backdrop-contract`;**同日增补铁律 8「重复图元合并」**——把整组同步驱动的重复图元合并成单元素,以规避一类真渲染横带、纵向重复叠印与逐帧闪烁缺陷;触发轴未知,本条只按零成本写法成文、不设数字门槛,对齐 change `add-particle-primitive-merge-law`)。产 HTML 动画颗粒、经同合云渲染管线(html_animate_render)逐帧 seek 合成的 skill/工具,其产物 MUST 满足本契约。
6
+ > **边界**:本契约只约束**机器可判定的管线消费属性**(封装/注册/确定性/自包含/依赖可达/禁 var()/字体名命中注册表)。画面长什么样——颜色、字体取值、构图、节奏——**一律由调用方按其栏目自身规则决定**,本契约不点名任何具体字体/颜色;文中示例取值均为中性占位。
7
+
8
+ ## 原理(为什么不能用 CSS animation)
9
+
10
+ 渲染引擎逐帧渲染时,靠调用每个子合成在 `window.__timelines` 注册的 GSAP 时间线的 `.seek(t)` 把画面定格到第 t 秒。GSAP `paused` 时间线 = 可被外部 seek 的虚拟时钟 → 逐帧正确;纯 CSS `animation-delay` 动画不在 `window.__timelines` 里,引擎 seek 不到 → 画面冻结(实测)。
11
+
12
+ ## 回调与 seek 语义(2026-07-26 增补 · 补此前留白)
13
+
14
+ > **一句话**:颗粒**可以**用时间线回调(`onUpdate` 等)驱动画面;**保证这些回调在定帧时被触发是渲染引擎侧的义务**,不是颗粒作者的义务。
15
+ > 本节是该主题的**唯一口径来源**——契约引用(`skills/gtrk-style-maker/references/contracts-ref.md`)与消费侧检查(`gtrk mg lint`)的文案都指向这里,不得各自表述。
16
+
17
+ ### 一、GSAP 3.13.0 实测事实(写死在此,免后来者重测)
18
+
19
+ 复现方式:拉铁律⑤指定 CDN 的 `gsap.min.js` 到本地,Node `vm` 里造 `paused` timeline + proxy 对象 + 计数回调,**每种定位方式各新建一条 timeline 单独跑**(回调计数互不串)。
20
+
21
+ **样本一**——10s、`ease:"none"` 的单条 tween(`{v:0}` → `{v:100}`),分别 seek 到 t=5(中途)与 t=10(末端):
22
+
23
+ | 定位方式 | `onUpdate` / `onStart` | `onComplete` | 补间目标属性 |
24
+ |---|---|---|---|
25
+ | `tl.seek(t)`(默认,`suppressEvents` 缺省为 `true`) | **不触发** | **不触发**——连 `seek(10)`(到末端)也不触发 | 已更新(t=5 → `v=50.00`;t=10 → `v=100.00`) |
26
+ | `tl.seek(t, true)` | 不触发 | 不触发 | 已更新 |
27
+ | `tl.seek(t, false)` | 触发 | `seek(10,false)` 触发;`seek(5,false)` 不触发 | 已更新 |
28
+ | `tl.time(t)` / `tl.progress(p)` / `tl.totalTime(t)` | 触发 | 同上(按是否到末端) | 已更新 |
29
+
30
+ ⚠️ **`onComplete` 那两行必须分清**:默认 seek 下它**到末端也不触发**,这才是「被抑制」的证据;而 `false` 模式下 `seek(5,false)` 的 `onComplete` 不触发,只是「补间尚未走完」的正常语义。混为一谈,后来者会拿后者误判成「垫片无效」。
31
+
32
+ **样本二(含真实 DOM 写入 · 引擎侧条款的直接依据)**——同一 t=1.0 定帧点,补间在 t=1.0 处的目标属性期望值 75.0,`onUpdate` 体内做一次真实 DOM 写入:
33
+
34
+ | 定位方式(同一 t=1.0 定帧点) | `onUpdate` 触发次数 | 补间目标属性 | 回调内的 DOM 写入 |
35
+ |---|---|---|---|
36
+ | `tl.seek(1.0)`(默认) | **0 次** | 已更新为 **75.0** | **未执行** |
37
+ | `tl.seek(1.0, false)` | 1 次 | 75.0 | 正常 |
38
+ | `tl.time(1.0)` | 1 次 | 75.0 | 正常 |
39
+ | `tl.progress(0.5)` | 1 次 | 75.0 | 正常 |
40
+
41
+ 要害是**「值更新了、回调没跑」**:补间目标对象的属性照常插值,但写在 `onUpdate` 里的 DOM 写入一次都不执行。所以这种翻车**不是黑屏,是画面定在初始态**——本地播放器与客户端预览都完全正常,比黑屏更难发现。
42
+
43
+ 样本二同时给出条款一的**可行性依据**:三种「不抑制」写法(`seek(t,false)` / `time(t)` / `progress(p)`)行为**完全一致**,故对引擎侧的要求**可实现且不挑实现**——引擎爱用哪种用哪种,只要不是裸 `seek(t)`。
44
+
45
+ ### 二、条款
46
+
47
+ 1. **引擎侧(MUST)**:渲染引擎定帧时 **MUST 保证 GSAP 时间线回调可达**——MUST 用 `seek(t, false)` 或 `time(t)` / `progress(p)` / `totalTime(t)`,**MUST NOT 用默认的 `seek(t)`**。违反后果:补间目标属性照常更新、回调内的 DOM 写入不执行 → 画面**定在初始态而非黑屏**,本地播放器与客户端预览均看不出异常。
48
+ 2. **颗粒侧(合规声明)**:颗粒**可以**用时间线回调(`onUpdate` / `onStart` / `onComplete` / `onRepeat`)写 DOM / 属性来驱动画面——这是**合规**写法(相机推进、数值读数这类效果用纯属性补间难以表达)。其可达性由条款 1 承担;契约 **MUST NOT** 反过来要求颗粒作者自行保证。
49
+ 3. **颗粒侧(不应)**:颗粒**不应**在运行时覆写引擎所调用的 API——重新赋值 `tl.seek`、或把 `window.__timelines[…]` 换成包装对象。两条实测理由:① 覆写会推翻引擎**显式**传入的 `seek(t, true)`(实测:打上垫片后 `seek(5)` 与 `seek(6, true)` 两次调用回调都触发),颗粒无权静默否决引擎的意图;② 垫片只作用于 `seek`,引擎改走 `time()` / `progress()` 时**完全失效**,而作者不会知道。
50
+ 已存在的垫片属**过渡态**:**新颗粒不应再加**。**2026-07-26 更新**——条款 1 的前提已经真引擎核实(见下「三」),垫片赖以存在的「真空期」已结束,故已存在的垫片**可择期清理**(清理是**可做**、不是 MUST 做;删后须重跑 lint 并**重渲复验**)。垫片留着也无害:实测引擎既然本就不抑制回调,垫片对画面结果无影响,只会被 lint 记一条非致命的 `x-engine-api-override`。
51
+ 4. **作者侧应知(这不是甩锅)**:作者 MUST 知晓——回调可达性的保证方是**引擎侧**,且该保证当前的核实状态见下「三」(2026-07-26 起为**已核实(行为层)· 绑定引擎 0.6.101**)。`gtrk mg lint` 会对「回调驱动画面且无任何兜底」的颗粒给一条**非致命**提醒,那是**哨兵**(引擎换实现或失守时有人喊一声),**不是**「请作者自行保证 seek 下回调可达」——**核实之后哨兵照留**,因为结论绑死引擎版本,换版即须重测。
52
+
53
+ ### 三、核实状态:**已核实(行为层)· 2026-07-26 · 绑定渲染引擎 producer `0.6.101`**
54
+
55
+ **把某条引擎行为写成 MUST,MUST NOT 被当作它已被核实**——二者在本节分别成文。本小节记的是**核实到了什么、以及没核实到什么**。
56
+
57
+ **结论:真渲染引擎在定帧时 `onUpdate` 回调可达,条款 1 的前提成立。** 结论**绑定上述引擎版本**,换版即失效、须重测。
58
+
59
+ - **核实方式(真引擎,非本地无头模拟)**:造一颗最小颗粒 `seekcb-probe`,**不打任何 seek 垫片**,画面正中的大号数字**只**由一条 tween 的 `onUpdate` 写入(proxy `{v:0}` → `480`,`ease:"none"`,16s),初值静态写死 `CB 000`;同屏并排一组**纯属性补间**对照物(方块横移 + 进度条拉长,均无回调),刻度尺与回调数字**同单位 0..480**,故单帧之内即可比对「应该是多少」与「实际是多少」。该颗粒先过 `gtrk mg lint`(`lint.ok=true`,`opaque=true`,只出 `x-callback-driven` 哨兵、无 `x-engine-api-override`),排除「因不合规才没渲」。走生产同款装配(`CompositionRender` 整轨路径 / 同底轨 `bed.mp4` / 1920×1080 / fps=60 / 15.97s / 958 帧分片 13 chunk)。
60
+ - **实测结果(三帧,肉眼判读整帧 PNG)**:
61
+
62
+ | 抽帧时刻 | 回调驱动的数字(onUpdate 唯一驱动) | 纯属性补间对照物 | 期望值 |
63
+ |---|---|---|---|
64
+ | t=2s | **CB 060** | 进度条/方块 @ 60 | 060 |
65
+ | t=8s | **CB 240** | 进度条/方块 @ 240 | 240 |
66
+ | t=14s | **CB 420** | 进度条/方块 @ 420 | 420 |
67
+
68
+ 三帧数字**各不相同**、**无一帧停在初值 `CB 000`**,且**逐帧与同单位对照物精确重合**——即回调不仅被触发,还是在**当前帧对应的时间点**上触发的(不是滞后一帧的陈值)。对照物同时移动,排除「整颗没渲/时间线没被定位」这一混淆解释。
69
+ - **证据留档**:三帧整帧 PNG、成片、颗粒与渲染脚本一并留档,落点见本仓 change `define-seek-suppress-events-contract` 的工件(复现件的本地路径与文件名不进入分发契约)。引擎版本取自渲染日志自报的 producer `0.6.101`。
70
+ - **⚠️ 本次核实的边界(MUST NOT 越界引用)**:核实的是**可观测行为**(回调可达),**不是调用形态**。引擎仍是无源码第三方包,本轮**没有**读到定帧调用点,故**无法区分**它走的是 `seek(t,false)` / `time(t)` / `progress(p)`,还是压根不经 GSAP 定位 API 的自有推进方式。条款 1 里那串 API 白名单仍是**应然的实现约束**,只有「回调 MUST 可达」这个**结果**被真机背书。调用形态的核实仍由 gitruck-infra 侧联动 change `link-guarantee-seek-callback-reachability` 承接(读调用点 + 把「纯回调驱动无垫片颗粒渲两帧、像素 MUST 不同」挂进引擎版本变更门禁)。
71
+ - **旁证(本轮之前的间接证据,保留备查)**:gitruck-infra 自家 9 个 music_visualizer 模板 **9/9** 把整块画面挂在单条 tween 的 `onUpdate` 上、且 **0/9** 带任何垫片,走的正是同一渲染内核,而 `gtrk music-visualizer` 是**已上线**能力——若引擎默认抑制回调,这 9 个模板早该渲成静止首帧。本仓 exemplar b06/b07/b08 同理。本轮直接核实**与旁证同向**。
72
+ - **假设失效的后果(哨兵为何 MUST 留)**:结论绑定 `0.6.101`;引擎升版换调用方式 → 全部回调驱动型颗粒(MG 与 music_visualizer 模板)**一起静默冻结**,lint 全绿、退出码 0、无任何告警。故 `gtrk mg lint` 的 `x-callback-driven` 哨兵(条款 4)**MUST NOT 因本次核实而撤**——它防的正是「换版后无人喊一声」。
73
+
74
+ ### 四、连带风险:回调可达 = 逐帧反复触发
75
+
76
+ 条款 1 的另一面:逐帧 scrub 时 `onComplete` / `onStart` / `onRepeat` 会**反复触发**(每帧一次)。故颗粒 **MUST NOT** 把「只跑一次」的逻辑写进回调——累加计数、`push` 进数组、一次性 DOM 插入、抽样。那会把「静默冻结」换成「静默错乱」。
77
+
78
+ **2026-07-26 起这条从「假设的另一面」变成「已确证的现实风险」**:上「三」已实测该引擎**确实**在逐帧定位时触发回调,故本条不是防患于未然,而是**当下就在生效**的约束。
79
+
80
+ **推荐写法**:回调做成**幂等**的——每次都从补间状态**重算**整个画面,不依赖上一次的结果(真机与 exemplar 的 `render()` / `apply()` / `paint()` 均如此)。
81
+
82
+ ### 五、优先级
83
+
84
+ **本节口径优先于任何栏目指南的招式建议。** 栏目指南若与本节冲突,以本节为准。(当前二者同向:栏目指南推荐的 `onUpdate` 驱动写法在本节下**合规**,作坊侧**无需修订**——本节不向作坊侧派发任何待办。此条照写不误,它防的是将来的分歧。)
85
+
86
+ ## 八条铁律(违反任一条 → 整片渲染失败 / 颗粒冻结 / 全黑 / 坑位内突兀消失 / 画面被切成横带、内容纵向重复叠印并逐帧闪烁)
87
+
88
+ 1. **`<template>` 包裹根元素**:`<template><div data-composition-id="<id>" data-width="1920" data-height="1080">…</div></template>`。编译器取的是 `<template>` 内容;裸 `<div>` 会被判 "empty or could not be parsed" 整片失败。(1920×1080 为**当前引擎版本约束**,分辨率参数化预留——升版时以本契约新版为准。)
89
+ 2. **GSAP `paused` 时间线 + 注册**:`var tl = gsap.timeline({paused:true}); … window.__timelines = window.__timelines || {}; window.__timelines["<id>"] = tl;`。`<id>` 必须等于根的 `data-composition-id`。
90
+ 3. **确定性**:禁用 `Math.random` / `Date.now` / 无参 `new Date()`(过不了引擎 StaticGuard)。要"随机感"用固定种子/解析式/递归生成(见「确定性配方」)。
91
+ 4. **自包含 + 实心底下沉子层 + 透明度显式声明**(**2026-07-26 按真机实测改写**;原文「全屏颗粒给根设明确 `background`;透明颗粒根不设 background」**已作废**,见下方证据锚):颗粒不依赖外部文件(除脚本 CDN)。底色的**取值**由调用方按其栏目规则决定,本契约只管**摆在哪个元素上**。
92
+ - **① 实心底 MUST 下沉为根下第一个全幅子层**(`position:absolute;inset:0` 或等价的 `top/left/width/height` 铺满),**MUST NOT 写在根元素的 `style` 上**。理由是实测事实:**根元素的绘制属性在子合成挂载时被丢弃**,写在根上的实心底**一个像素都不落地**——成片里表现为「浮空面板」(前景照常渲出、底没了、底轨透出来),本地播放器与客户端预览都看不出。
93
+ - **② 根元素 MUST 保持零视觉**:根 `style` MUST NOT 出现任何会绘制像素的属性(实心 `background` / `border` / `box-shadow` / `outline` …)。写了不报错,但**不生效**,只会误导后来者以为底已经有了。
94
+ - **③ 透明与否 MUST 显式声明**:`background` 声明 MUST 至少出现一处——**满屏(不透明)颗粒**写在 ① 的全幅子层上;**透明叠加(overlay)颗粒** MUST 在**根**显式写 `background:transparent`,MUST NOT 靠「不写」表达透明。根上的 `transparent` 虽然同样不落成像素(它本就不绘制),但它是**给人和机器读的意图声明**——缺了它,作者与 `gtrk mg lint` 都无从区分「想透明」与「忘了想」。
95
+ - **④ 消费侧同源**:`gtrk mg lint` 的 `opaque` 推导面 = 「根 `style` ∪ 根下首个全幅子层 `style`」,与本条 ①③ 同源;两处皆无 `background` 声明才报 `4-bg-explicit`,满屏颗粒把实心底写在根上另报非致命 `4-bg-on-root`。
96
+ 5. **脚本用渲染机可达的 CDN(编译期内联)**:`<script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>` 或由渲染管线 vendor 本地。⚠️ jsdelivr 在渲染服务器不稳(实测 compile 期 `fetch failed` → GSAP 未加载 → 整片全黑)。编译器**只内联 http(s) CDN、不内联相对本地路径**(写 `src="gsap.min.js"` 运行时 404)。
97
+ 6. **颜色/字体用字面值,禁 CSS `var()` 自定义变量;字体名 MUST 命中服务端注册字体表**:编译器/挂载不可靠地解析 var()(字体映射把 `var(--font-body)` 当字面字体名;颜色 var() 不应用 → 整片全黑,实测)。直接写字面值(如 `#RRGGBB` / `'某字体名'`);SVG 属性里同样禁 var()。栏目级换色/换主题 = **生成期**替换字面值(查调用方自己的词表/token 注入),不是运行时变量。**字体名规则(2026-07-24 增补)**:font-family 的每个具名家族 MUST 逐字符命中渲染服务端注册字体表(gitruck-infra 仓 `utils/assets/text/classic_template/font_manifest.json`,中英别名等价),并 SHOULD 以 `sans-serif`/`serif` 通用族收尾兜底;表外名字渲染不失败但**字形不保证**(服务端 fail-open 系统回退,2026-07-24 真机实锤:错名导致目标字形被回退成另一款系统字形)。具体选哪款仍由调用方栏目规则决定,本契约不点名。
98
+ 7. **占满坑位 + 终态驻留(2026-07-24 主理人硬性规定;2026-07-26 增补无限循环定性)**:颗粒时间线总长 MUST ≥ 它在成片中的**坑位时长**(落轨 clip 的实际时长,通常 = 派单槽位包络 `track_ed − track_st`,**不是** `duration_hint`);动画主叙事播完后,颗粒 MUST 以「**定格保持**」或「**有限次循环**」驻留到坑位末尾。**禁用无限循环 `repeat:-1`(2026-07-26 增补,补此前留白)**:无限循环让时间线总长为 `Infinity`,本条「总长 ≥ 坑位」就**变得不可验证**(机器与人都无从判断它是否真按坑位算过),且掩盖「作者根本没算坑位」这件事。循环次数 MUST **按坑位算死**——`repeat = ceil((坑位时长 − 循环起点) / 单圈时长) − 1`,宁可多算一两圈(末尾被 clip 裁掉无害),也不许写 `-1` 蒙混——坑位内任意时刻(含最后一帧)核心内容必须可见。**禁止**「整体渐隐到空 / 全局退场 / 清空画面」类收尾:渐隐会与剪辑层转场冲突,淡出与否由剪辑/装配层决定,不在颗粒内做。局部元素可按叙事退场(黯淡/让位),但画面在坑位内不得归零;**定格不动是完全合法的终态**(不必为凑动作密度在尾段硬加动画)。违反表现 = 观感上「动画一过完整个颗粒突兀消失」(2026-07-24 真机实测,事故工程见本仓 change `fix-mg-clip-fill-slot`)。
99
+ 8. **重复图元合并(2026-07-26 增补 · 规避条款,根治在渲染侧)**:同一批**重复图元**——同色、同 `stroke` / `fill`、**整组同步驱动**(只有组级动画或完全静态)的网格、排线、刻度、点阵——**MUST 合并成单个元素**(SVG 用一条 `<path>` 的多子路径 `M…H…V…`;同色分档时可按档合成若干条),**MUST NOT** 用「每条线 / 每个格子一个 `<element>`」的批量构建,无论它写成字面标签还是由 `createElementNS` 循环生成。合并前后画面**逐像素相同**,故本条不涉任何视觉取值。
100
+ - **豁免**:需要**逐元素动画**(stagger / 逐条画入 / 逐个变色)的批次不在本条范围内——元素身份是该批动画所必需的。此类批次若同时是满幅大面积绘制,MUST 以真渲染出片抽帧验收;这是保守兜底,**不是**「满幅即危险」的风险判据,反向的「不满幅即安全」同样不成立。
101
+ - **违反表现**:画幅被切成等高横带、相邻带内容互相纵向重复叠印(同一元素在 `y` 与 `y±带高` 各画一次,或被顶部裁半)并逐帧闪烁;该表现与动画逻辑无关,同帧里零动画的静态元素也会重复。DOM 元素数与 `getBoundingClientRect` 均正常,本地播放器与客户端预览看不出。
102
+ - **这是规避,不是根治**:缺陷出在渲染侧已由真机行为确证——颗粒的 DOM 元素数与布局值正常,不是颗粒写错。机制层面**推断**为渲染侧按等高带做局部栅格刷新时失效区域算错,**属推断、未经渲染侧确认**;**真实触发轴至今未知**,「图元总数」「同页累积负载」「同父同类聚簇」均已被跨颗粒真渲反例否证。本条 MUST NOT 被读成「图元少 / 负载低就安全」。
103
+ - **根治登记**:根治方 = 渲染管线侧;当前状态 = **未立项(2026-07-26;决策角色 = 本 change 实施者,按 proposal 的跨仓授权边界不代提;是否立项仍待主理人另行授权)**。
104
+ - **根治后处置预案**:待渲染管线侧根治上线且经真机确认缺陷不再复现后,本条由 **MUST 降为 SHOULD**,并保留「本条曾于 2026-07-26、本证据锚所载引擎版本口径下作为规避所必需」的历史记录。
105
+
106
+ > **⚠️ 铁律编号的唯一定义在本文件。** 本契约的**铁律 7 = 「占满坑位 + 终态驻留」**,这是框架侧对该条号的**唯一**定义。
107
+ > 创作侧(栏目 skill / 作坊)的模板与 references **MUST NOT 另立同号铁律**——引用请写「gsap-emit v1 铁律 7」并指回本文件,
108
+ > 自己的栏目约束请用**栏目注记**或另起独立编号空间。
109
+ > **已知撞号(2026-07-26 记录,收口中)**:某栏目 references 曾把「根元素零视觉」编为其「铁律 7」、同一条在其模板里又编为「铁律③」——
110
+ > 该约束现已**升为本契约的铁律 4**(见上),栏目侧的两个同名条号 SHALL 改为引用铁律 4,不再另立。
111
+
112
+ ### 铁律 4「实心底下沉子层」的真机证据锚(2026-07-26)
113
+
114
+ 本条断言的是**渲染引擎行为**(「根元素的绘制属性在挂载时被丢弃」),故按契约库要求附可追溯证据锚:
115
+
116
+ - **结论**:根 `style` 的 `background` **不落成像素**;同一颗粒把同一底色改写到根下首个全幅子层则**满屏落地并盖住底轨**。
117
+ - **实验日期 / 机器**:2026-07-26 · 真机(生产同款渲染主机;真渲,非本地无头模拟)。
118
+ - **引擎版本口径**:渲染引擎 CLI producer 同版 `0.6.101` · 无头浏览器内核 `131.x`。
119
+ - **设计**:同颗粒母本造两个变体,**唯一变量 = 实心底摆在根 `style` 还是根下首个全幅子层**,其余逐字节相同;
120
+ 底色临时取高饱和标志色以便与「根合成兜底黑」区分;底轨用一段有明显画面、非黑的真人镜头;抽同一时间点 `t=8.0s` 的帧,
121
+ 取四角 `crop=40:40` 于 `(20,20) (1860,20) (20,1020) (1860,1020)` 后 `scale=1:1` 读像素。
122
+ - **观测**(整轨拆分渲染路径,两格同素材同引擎同批)。
123
+ ⚠️ 下表的六位数字是**抽帧读出的 RGB 像素读数**(实验观测),**不是本契约规定的任何底色**——
124
+ 轴一边界照旧:契约不点名任何色值,底色取值永远由调用方栏目决定:
125
+
126
+ | 变体 | 四角像素 | 判读 |
127
+ |---|---|---|
128
+ | 实心底写**根 `style`** | `dfdad4 / fafcfb / 4e4f52 / f9fbfb` | ≈ 底轨基线 → **底丢失、底轨透出** |
129
+ | 实心底写**根下首个全幅子层** | `01ff81 / 00fe80 / 01fd80 / 01fd80` | ≈ 标志色 → **底存活、满屏盖住底轨** |
130
+ | 底轨本身(阴性对照) | `e0dbd4 / fbfdfc / 4f5052 / fbfdfd` | 四角始终高亮非黑,对照有效 |
131
+
132
+ - **阳性对照(关键,排除平凡解)**:「写根」那格的整帧里颗粒**前景元素完整渲出**(标题文字、网格、连线、色块都在)——
133
+ 所以它**不是「颗粒没渲上」,而是「颗粒渲上了、根 `background` 被丢掉」**。缺这一步,两种情况在像素上同解。
134
+ - **复现方式**:造上述两变体 → 同一批走整轨拆分渲染 → 按上「设计」段的抽帧与四角取样口径(`t=8.0s` 单帧 / `crop=40:40` / `scale=1:1`)取样,
135
+ 再以 RGB24 原始字节读出四角的十六进制像素值。取样工具与复现件的本地路径不进入分发契约(同铁律 8 证据锚口径)。
136
+ - **历史**:本条现象最早由创作侧于 **2026-07-15** 记载(一颗满屏颗粒的根 `background` 丢帧、成片只剩浮空面板),
137
+ 当时未进框架契约,框架正本反而长期要求「写在根上」——2026-07-26 的实验复现并坐实了该记载,本条据此改写。
138
+ - **相关跨仓 change**:`gitruck-cli` / `align-particle-solid-backdrop-contract`(本条的来源);
139
+ `gitruck-infra` / `add-html-animate-opaque-fullscreen-cover`(**另一个独立问题**:渲染侧要不要按 `opaque` 位替颗粒兜一层黑底)。
140
+ ⚠️ 两者**互不依赖**:上表的观测正是在后者**未 apply** 时取得的——即**本条的修法(下沉子层)当场生效,不需要渲染侧任何配合**。
141
+
142
+ ### 铁律 8「重复图元合并」的真机证据锚(2026-07-26)
143
+
144
+ 本条断言的是一条**作者侧规避写法**及其在真渲染中的效果,不断言已知触发轴。证据锚如下:
145
+
146
+ - **结论**:同一故障样本把 37 条满幅 `<line>` 合为一条 `<path>` 多子路径后,目标画面逐像素不变,横带重复率由 **65%~68% 降至 2%**;规避当场有效,但合并同时改变了图元数、DOM 结构与栅格化路径,故本实验**不能证明是哪一个变量生效**。
147
+ - **实验日期 / 机器**:2026-07-26 · 真机(生产同款渲染主机;真渲,非本地无头模拟)。
148
+ - **引擎版本口径**:渲染引擎 CLI 与 producer 同版 `0.6.101` · 无头浏览器内核 `131.x`。
149
+ - **二分设计**:以同一颗粒为母本逐项只改一个构建块,每个变体各做一次真渲;每格抽取 168 帧,按等高横带间的重复像素统计复现率。装配路径、分辨率、帧率、底轨与其余颗粒代码保持一致。
150
+ - **范围限定(硬约束)**:下表全部是**同一颗粒内部**的二分结果,**MUST NOT 跨颗粒外推**;它只能说明这些改动在该颗粒内与复现率同向变化,不能推出「元素数少就安全」或任何数字门槛。
151
+
152
+ | 同一颗粒内的变体 | 重复率 | 限定读法 |
153
+ |---|---:|---|
154
+ | 原件:背景网格为 37 条满幅 `<line>` | **65%~68%**(复跑一致) | 故障基线 |
155
+ | 37 条 `<line>` 合为一条 `<path>` 多子路径 | **2%** | 本条规避正例;目标画面逐像素不变 |
156
+ | 整块删除背景网格 | **2%** | 与合并同档;只说明该块在本颗粒内参与复现 |
157
+ | 删除两个静态术语文本块 | **0%~1%** | 删除任一块负载都退烧;作用域仅限本颗粒,机制未知 |
158
+ | 删除三行静态标签文本块 | **0%~1%** | 同上 |
159
+ | 换字体族 / 去斜体 / 关闭 3D 加速 / 改入场位移 / 把文字改为 SVG `<text>` / 给根加变换提示 / 把网格改静态 | **仍为 65%+** | 均无效 |
160
+ | 增加满屏失效脉冲层 | **14%** | 作者侧强制全幅失效仍不能根治 |
161
+ | 极简结构(20 个静态 `<div>` + 1 个网格) | **0%** | 空载变体不复现;不得外推为通用预算 |
162
+
163
+ - **阳性对照(排除平凡解)**:合并版仍完整绘制与原件相同的整张网格,逐像素对拍一致;它不是「把内容删没了所以不重复」。同一颗粒内另有 24 条局部短线在合并版中继续保留,故 24 37 **MUST NOT** 被写成安全 / 故障分界。
164
+ - **跨颗粒反证(同批全量体检)**:同装配、同引擎、同机完成 **21 颗粒 + 六 beat 同页聚合,共 24 次渲染 / 约 22,000 帧**。这些结果专门限制上表的外推范围:
165
+
166
+ | 样本 | 真渲结果 | 可否证的候选轴 |
167
+ |---|---:|---|
168
+ | 另一密阵样本:884 条短划、全颗共 921 个绘制图元 | **0.0% 脏帧** | 图元总数、同父同类聚簇 |
169
+ | beat 同页聚合:约 1,400 个绘制图元 | **0.0% 脏帧** | 同页累积负载 |
170
+ | 当前修复版样本(回归 / 阴性对照) | **0.0% 脏帧** | 规避在整轨分片路径下仍稳 |
171
+ | 未修版样本(阳性对照) | **70.3% 脏帧** | 缺陷仍活且可稳定复现 |
172
+
173
+ **真实触发轴至今未知**;当前已否证的候选轴 = **图元总数 / 同页累积负载 / 同父同类聚簇**。上表中「921 个图元干净」与「较少图元的阳性对照中招」同时成立,因此数字方向不存在可据此写入契约的安全门槛。
174
+ - **复现方式**:从同一母本生成上述单变量变体 走生产同款整轨分片渲染 对每格 168 帧计算等高横带间的重复率并抽取高分帧肉眼复核;再以同一口径跑 21 颗全量语料、六 beat 同页聚合、修复版阴性对照与未修版阳性对照。复现件的本地路径与文件名不进入分发契约。
175
+ - **相关 change / 根治状态**:本仓 `add-particle-primitive-merge-law` 收录作者侧规避;渲染管线侧根治**未立项(2026-07-26,本 change 实施会话按非目标不代提;待主理人另行决定)**。根治上线并经真机确认后,按铁律 8 的预案把 MUST 降为 SHOULD。
176
+
177
+ ## 渲染成本:真卷积滤镜(2026-07-30 增补)
178
+
179
+ > **本节不是铁律**(铁律仍是**八条**,编号空间不动)。铁律管的是**正确性失效**(渲不出 / 冻结 / 全黑 / 突兀消失 / 横带叠印);
180
+ > 本节管的是**每帧成本**——命中本节的颗粒**画面是对的,只是贵**。故本节用 MUST NOT / SHOULD 约束写法,
181
+ > 但**不进「违反即废片」的铁律清单**。
182
+
183
+ ### 射程(本节说的「真卷积滤镜」精确指这三类)
184
+
185
+ | 进射程(真卷积) | 射程外(本节不管、消费侧也不报) |
186
+ |---|---|
187
+ | CSS `filter` / `-webkit-filter` / `backdrop-filter` 的值里含 `blur(<非零>)` 或 `drop-shadow(… <非零模糊半径> …)` | `filter: blur(0)` / `blur(0px)`、`drop-shadow(x y)`(模糊半径缺省 = 0)、`filter: none` |
188
+ | SVG `<feGaussianBlur stdDeviation="<非零>">`、`<feDropShadow>`(含经 `filter="url(#id)"` 引用者) | **任何 `box-shadow`** |
189
+ | 上述两类出现在 `<style>` 规则、元素内联 `style`、或时间线补间属性对象里的任一位置 | `opacity` / `transform` / `mix-blend-mode` / `border-radius` |
190
+
191
+ - **`box-shadow` 整族豁免、不报警**——包括 `box-shadow: 0 0 0 Npx <color>` 这类零卷积的纯 spread 描边/光圈写法
192
+ (脉冲扩散的标准句式,现网大量在用),**也包括 blur-radius > 0 的写法**。后者**确实是卷积**,
193
+ 这一点如实记录在此;但把 `box-shadow` 纳入射程需要正确解析它的多值语法(`inset`、多组逗号、颜色可前可后、长度 2~4 个),
194
+ 误报率高而现网收益为零,故整族排除。⚠️ 这是**已定的射程边界**,**不是**「待纳入 / 下版收编」;
195
+ 要改变它,先证明存在受益的现网用法。
196
+ - **与铁律 4② 的语义分工(两条正交,不冲突)**:铁律 4② 把 `box-shadow` 列进「根元素 MUST 保持零视觉」的**绘制属性清单**,
197
+ 管的是「绘制属性摆在根上不生效」;**本节管的是每帧卷积成本**。同一个属性名出现在两处不是口径打架——
198
+ 一处论**位置**,一处论**成本**。
199
+
200
+ ### 条文
201
+
202
+ - **成本模型**:真卷积滤镜的每帧代价 **被覆盖面积 × 模糊半径 × 帧数**。逐帧渲染要把每一帧都算一遍,
203
+ 所以它与「本地播放器里看着很流畅」毫无关系。
204
+ - **MUST NOT 用时间线补间驱动 `filter` 属性**(也包括补间驱动被引用 SVG 滤镜的 `stdDeviation`):
205
+ 滤镜值一变,上一帧的卷积结果**当场失效**,缓存彻底失灵,这是本节里最贵的形态。
206
+ 同一叙事动作请改用 `opacity` / `transform`(`scale` / 位移)表达;确需滤镜就做成**静态两态切换**,别让它随时间连续变。
207
+ - **整幅静态滤镜 SHOULD 缩小被滤元素的几何覆盖面积**:把滤镜收到它真正需要的那块矩形上。
208
+ 面积一项**同时**压掉「卷积」与「整幅逐帧合成」两笔成本,故它是首选改法。
209
+ - **MUST NOT 把「预烘成图」写成一律解法**:整幅静态滤镜的每帧成本由**两项**构成——① 卷积本身;② 整幅逐帧合成。
210
+ 预烘只消得掉 ①,② **照样每帧付**,故预烘的收益**存在上限、必有残差**;而且把活颗粒烤死会牺牲「全程可拖可调」这一产品前提。
211
+ 预烘是**次选**,不是缺省答案。
212
+ - **确要预烘时 MUST 用 RGBA PNG**,**MUST NOT** 用 JPEG 或任何无 alpha 格式:透明叠加颗粒的 alpha 是成片合成的**必需通道**,
213
+ 丢了 alpha 的图在成片里会塌成一块不透明色块(前景盖住底轨)。
214
+ - **判断权在作者**:去不去某个滤镜属**视觉取值**,本契约不规定视觉取值——本节只给成本机制与改法。
215
+
216
+ ### 证据锚(只收有出处的数;每条带测量日期 + 语料标注)
217
+
218
+ - **测量日期**:2026-07-30。**版本口径**:渲染引擎 CLI producer 同版 `0.7.82` · 无头浏览器内核 `152.x`。
219
+ 测量在真机(生产同款渲染主机)取得,非本地无头模拟。
220
+ - **现网既有栏目语料(21 颗生产颗粒)**:去掉真卷积滤镜的可省墙钟 **0~3.1%**——即**本节对现有语料近似零收益**,
221
+ 它立在这里是**前置门**(防止大批带滤镜的新语料进渲染管线后整批返工),**不是**提速手段。
222
+ - **单次误用的止损量级(同一颗粒内对照)**:**1.3~3.3×**。
223
+ - **滤镜重语料(技法颗粒,单颗 646 帧)**:**7.0×**(339.6s → 48.3s)。
224
+ ⚠️ 该数**只对该语料成立**,MUST NOT 外推成通用提速承诺。
225
+ - **消费侧沉默不等于便宜**:`gtrk mg lint` 的三个成本项(`c-filter-animated` / `c-filter-static-fullbleed` /
226
+ `c-filter-indeterminate`)是**形态提示**,**当期一律非致命、不拦铺轨**;没报**不代表**这颗渲得快——
227
+ 真判据永远是**真渲染出片计时**。
228
+ - **致命性的升级触发器(登记在此,免得日后凭印象改)**:把 `c-filter-animated` 提为**致命**
229
+ SHALL 另走一个独立 change,且 SHALL 同时满足两项前提——① 「滤镜重语料批量进渲染管线」那条路线**已立项**;
230
+ 提级前对现网生产语料**全量复扫且零命中**(复扫在生产颗粒上命中 > 0 MUST NOT 提级)。
231
+ 触发一次评估的时机 = 上述路线立项 **或** 本节首次改版,以先到者为准。
232
+ **MUST NOT** 以「规范就该严」为由径直提级,也 **MUST NOT** 把致命性做成可现场翻转的运行期开关。
233
+
234
+ ### 根治登记
235
+
236
+ - **① 这是降本规范、不是缺陷规避**:命中项的画面本身是对的,逐帧也正确,只是每帧贵。
237
+ - **② 成本的根在渲染管线侧**:逐帧路径走**软件光栅**(无图形硬件加速),卷积按面积×半径吃 CPU。
238
+ - **③ 根治状态 = 明确不立项(2026-07-30)**:改走图形硬件加速路径会破坏「逐帧确定性」与「产出端版本对齐」这两项前提
239
+ (异构机器还会破坏分片与单次的产物等价),渲染侧调研已把它列为**已排除项**。故本节以作者侧写法收敛,不等渲染侧。
240
+ - **④ 根治后的处置预案**:若渲染侧将来换掉光栅路径并经真机复测确认成本塌陷,本节由 MUST NOT/SHOULD **降为提示**,
241
+ 并保留「本节曾于 2026-07-30、本证据锚所载版本口径下作为降本规范所必需」的历史记录。
242
+
243
+ ## 颗粒骨架(中性模板)
244
+
245
+ ```html
246
+ <template id="p">
247
+ <!-- 铁律4:根 MUST 零视觉。这里的 background 恒为 transparent(显式声明,不是省略),实心底一律下沉到下面的 .bgfill 子层 -->
248
+ <div data-composition-id="<id>" data-width="1920" data-height="1080"
249
+ style="position:absolute;inset:0;background:transparent;overflow:hidden;font-family:'<你的字体·须命中服务端 font_manifest.json>',sans-serif;">
250
+ <!-- 铁律4①:实心底 MUST 是根下第一个全幅子层。满屏颗粒填你栏目的底色;透明叠加(overlay)颗粒把整行删掉 -->
251
+ <div class="bgfill" style="position:absolute;inset:0;background:<你的底色>;z-index:0;"></div>
252
+ <style> [data-composition-id="<id>"] .xxx{ … } </style> <!-- 样式用属性选择器作用域,防跨颗粒污染 -->
253
+ <svg viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid meet" style="position:absolute;inset:0;width:100%;height:100%;">…</svg>
254
+ <script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>
255
+ <script>(function(){
256
+ var ROOT='[data-composition-id="<id>"]';
257
+ /* 1) 确定性构建静态结构 */
258
+ /* 2) gsap.set 初始态 */
259
+ /* 3) var tl = gsap.timeline({paused:true}); … 编排 … */
260
+ window.__timelines = window.__timelines || {};
261
+ window.__timelines["<id>"] = tl;
262
+ })();</script>
263
+ </div>
264
+ </template>
265
+ ```
266
+
267
+ > **骨架里 `.bgfill` 那一行的三个要点**:① 它 MUST 是**根下第一个**子元素(后续前景层自然压在它之上,不必给前景排 z-index);
268
+ > ② 它 MUST **全幅**(`position:absolute;inset:0`,或等价的 `top/left/width/height` 铺满)——不铺满就不是「实心底」;
269
+ > ③ 类名 `bgfill` 只是**惯例**,其底色写在**行内 style** 上(不靠 `<style>` 里的类规则),故不受「样式作用域」那条专属坑影响;
270
+ > 换名字不违约,但 `gtrk mg lint` 认的是「根下首个全幅子层的 `background`」这个**结构**,不认类名。
271
+
272
+ 缓动可用 CustomEase 精确还原你栏目自己的 cubic-bezier(缺插件时给近似回退)——bezier 数值属于栏目审美,本契约不规定。
273
+
274
+ ## 确定性配方(替代 random)
275
+
276
+ - 递归结构:固定角度、比例、深度参数 → 完全确定。
277
+ - "噪声感":`Math.sin(i*0.18 + j*0.2)` 类解析式伪随机。
278
+ - 打散:用 index 派生(如 `i*137.5°` 黄金角),不要 `Math.random()`。
279
+
280
+ ## 验证(交付前必做)
281
+
282
+ 墙钟截图类工具驱动不了 paused 时间线(只看到 t=0),**不能**用来验收。必须真渲染引擎 seek 验证:
283
+ 1. 颗粒放进最小 composition(root `index.html` 用 `data-composition-src` 引它);
284
+ 2. 走渲染管线(html_animate_render)渲染;
285
+ 3. 抽不同时间点的帧**比对应当不同**(相同=冻结=铁律没守住)。客观自检:根有 `<template>`、`window.__timelines["<id>"]` 已注册且 id 匹配、无 random/Date、tl 总长 ≥ 颗粒时长。
286
+ > ⚠️ 不要用「本地等价 seek 脚本 / Node 无头模拟」替代真引擎渲染——它不经真编译+挂载,测不出 var()-不解析、CDN-内联失败、StaticGuard 这类只在真引擎暴露的问题(实测教训:本地等价测试报 OK,真引擎全黑)。
287
+ >
288
+ > ⚠️ **客户端预览对「底色 / 透明度」类问题结构性失明,MUST NOT 用作这类验收的判据**(2026-07-26 增补)。
289
+ > 客户端预览会按 clip 登记的 `opaque` 位**给颗粒根盒强行打底**(`!important`,恒胜过颗粒自身的设计期底色)——
290
+ > 于是无论根 `background` 有没有被丢弃、有没有实心底子层,**预览看到的都是「按登记值应该长的样子」**。
291
+ > 铁律 4 的证据锚正是靠**真渲染出片抽帧**取得的,不是靠预览。预览截图只作对照留档,不入判据。
292
+
293
+ ## 专属坑
294
+
295
+ - **样式作用域**:颗粒与其他颗粒/根同处一个文档,全局类会撞——用 `[data-composition-id="<id>"] .xxx` 属性选择器作用域。
296
+ - **`<template>` 内的 `<script>` 默认不执行**——引擎把 template 内容克隆进文档后才执行;本地直接开浏览器不会跑,必须经引擎/player。
297
+ - **transform-origin(SVG)**:缩放 `<g>` 用 GSAP `svgOrigin:"x y"`(SVG 用户坐标),别用 CSS transform-origin。
298
+ - **时间线总长 ≥ 坑位时长**:颗粒 tl 总时长 ≥ 落轨 clip 时长(坑位包络),否则 seek 越界(已升格为铁律 7,含终态驻留要求)。
299
+ - **整组同步驱动的重复图元必须合并**:网格、排线、刻度、点阵用单元素表达,SVG 以一条 `<path>` 的多子路径合成;逐元素动画批次豁免。详见 gsap-emit v1 铁律 8。
300
+ - **真卷积滤镜别用补间驱动、整幅静态滤镜先缩面积**:`blur()` / `drop-shadow()` / `feGaussianBlur` 按「面积 × 半径 × 帧数」吃每帧墙钟;预烘是次选且必须 RGBA PNG。`box-shadow` 整族不在射程内。详见本文件「渲染成本:真卷积滤镜」一节。
301
+ - **别用 `requestAnimationFrame`/`setInterval` 驱动画面**——不被 seek,等于冻结。所有视觉变化必须挂在 tl 上。(与「回调与 seek 语义」一节同源:任何**不经 tl** 的自有时钟都不被定帧驱动;而挂在 tl 上的回调是否被触发,则由该节的引擎侧条款保证。`gtrk mg lint` 对本条给**非致命**项 `x-raf-interval`——静态正则分不清「驱动画面」与其它用途,故只提醒不拦。)
package/dist/index.js CHANGED
@@ -9159,8 +9159,8 @@ function firstChildTag(html, rootTagStr) {
9159
9159
  i += close ? close.index + close[0].length : m[0].length;
9160
9160
  }
9161
9161
  }
9162
- function isFullBleed(tagStr) {
9163
- const style = (attr(tagStr, "style") ?? "").toLowerCase();
9162
+ function isFullBleedStyle(styleDecls) {
9163
+ const style = styleDecls.toLowerCase();
9164
9164
  if (!/position\s*:\s*(?:absolute|fixed)/.test(style))
9165
9165
  return false;
9166
9166
  if (/\binset\s*:\s*0(?:px|%)?\b/.test(style))
@@ -9169,6 +9169,9 @@ function isFullBleed(tagStr) {
9169
9169
  const full = (p) => new RegExp(`\\b${p}\\s*:\\s*(?:100%|100vw|100vh|1920px|1080px)\\s*(?:;|$)`).test(style);
9170
9170
  return has("top") && has("left") && full("width") && full("height");
9171
9171
  }
9172
+ function isFullBleed(tagStr) {
9173
+ return isFullBleedStyle(attr(tagStr, "style") ?? "");
9174
+ }
9172
9175
  function bgOf(style) {
9173
9176
  const bg = style.match(/background(?:-color)?\s*:\s*([^;"']+)/i);
9174
9177
  if (!bg)
@@ -10091,6 +10094,219 @@ function detectSeekSignals(html) {
10091
10094
  overrideForms
10092
10095
  };
10093
10096
  }
10097
+ var CSS_LEN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:px|em|rem|pt|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pc|q)?$/i;
10098
+ function nonZeroLength(raw) {
10099
+ const s = raw.trim();
10100
+ if (!CSS_LEN.test(s))
10101
+ return false;
10102
+ return Number.parseFloat(s) !== 0;
10103
+ }
10104
+ function parenArg(src, open2) {
10105
+ const close = closeParen(src, open2);
10106
+ return close < 0 ? null : src.slice(open2 + 1, close);
10107
+ }
10108
+ function topLevelTokens(src) {
10109
+ const out = [];
10110
+ let depth = 0;
10111
+ let cur = "";
10112
+ for (let i = 0;i < src.length; i++) {
10113
+ const c3 = src[i];
10114
+ if (c3 === "(")
10115
+ depth++;
10116
+ else if (c3 === ")")
10117
+ depth = Math.max(0, depth - 1);
10118
+ if (depth === 0 && /\s/.test(c3)) {
10119
+ if (cur)
10120
+ out.push(cur);
10121
+ cur = "";
10122
+ continue;
10123
+ }
10124
+ cur += c3;
10125
+ }
10126
+ if (cur)
10127
+ out.push(cur);
10128
+ return out;
10129
+ }
10130
+ function dropShadowConvolves(arg) {
10131
+ const lens = topLevelTokens(arg).filter((t) => CSS_LEN.test(t));
10132
+ return lens.length >= 3 && nonZeroLength(lens[2]);
10133
+ }
10134
+ function filterValueConvolves(value, svgConvIds) {
10135
+ const v = value.toLowerCase();
10136
+ for (const m of v.matchAll(/\bblur\s*\(/g)) {
10137
+ const arg = parenArg(v, m.index + m[0].length - 1);
10138
+ if (arg !== null && nonZeroLength(arg))
10139
+ return true;
10140
+ }
10141
+ for (const m of v.matchAll(/\bdrop-shadow\s*\(/g)) {
10142
+ const arg = parenArg(v, m.index + m[0].length - 1);
10143
+ if (arg !== null && dropShadowConvolves(arg))
10144
+ return true;
10145
+ }
10146
+ for (const m of v.matchAll(/\burl\s*\(/g)) {
10147
+ const arg = parenArg(v, m.index + m[0].length - 1);
10148
+ if (arg === null)
10149
+ continue;
10150
+ const id = arg.trim().replace(/^["']|["']$/g, "").replace(/^#/, "");
10151
+ if (id && svgConvIds.has(id))
10152
+ return true;
10153
+ }
10154
+ return false;
10155
+ }
10156
+ function svgPrimitivesConvolve(filterBody) {
10157
+ for (const m of filterBody.matchAll(/<fe(GaussianBlur|DropShadow)\b[^>]*>/gi)) {
10158
+ const std = attr(m[0], "stdDeviation");
10159
+ if (std === undefined) {
10160
+ if (m[1].toLowerCase() === "dropshadow")
10161
+ return true;
10162
+ continue;
10163
+ }
10164
+ if (std.trim().split(/[\s,]+/).some((t) => /^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(t) && Number.parseFloat(t) !== 0))
10165
+ return true;
10166
+ }
10167
+ return false;
10168
+ }
10169
+ function svgConvolutionFilterIds(html) {
10170
+ const ids = new Set;
10171
+ for (const m of html.matchAll(/<filter\b([^>]*)>([\s\S]*?)<\/filter\s*>/gi)) {
10172
+ const id = attr(`<filter${m[1]}>`, "id");
10173
+ if (id && svgPrimitivesConvolve(m[2]))
10174
+ ids.add(id);
10175
+ }
10176
+ return ids;
10177
+ }
10178
+ function filterDecls(styleDecls) {
10179
+ const out = [];
10180
+ for (const m of styleDecls.matchAll(/(?:^|[;{}\s])\s*(?:-webkit-)?(?:backdrop-)?filter\s*:\s*([^;}]*)/gi))
10181
+ out.push(m[1].trim());
10182
+ return out;
10183
+ }
10184
+ function styleBlockOwnDecls(html) {
10185
+ const out = [];
10186
+ for (const sm of html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)) {
10187
+ const css = sm[1].replace(/\/\*[\s\S]*?\*\//g, " ");
10188
+ const stack = [];
10189
+ for (let i = 0;i < css.length; i++) {
10190
+ const c3 = css[i];
10191
+ if (c3 === '"' || c3 === "'") {
10192
+ i = skipString(css, i);
10193
+ continue;
10194
+ }
10195
+ if (c3 === "{")
10196
+ stack.push(i);
10197
+ else if (c3 === "}") {
10198
+ const open2 = stack.pop();
10199
+ if (open2 === undefined)
10200
+ continue;
10201
+ let own = css.slice(open2 + 1, i);
10202
+ for (let prev = "";own !== prev; ) {
10203
+ prev = own;
10204
+ own = own.replace(/\{[^{}]*\}/g, " ");
10205
+ }
10206
+ out.push(own);
10207
+ }
10208
+ }
10209
+ }
10210
+ return out;
10211
+ }
10212
+ function readObjValue(body, from) {
10213
+ let depth = 0;
10214
+ for (let i = from;i < body.length; i++) {
10215
+ const c3 = body[i];
10216
+ if (c3 === '"' || c3 === "'" || c3 === "`") {
10217
+ i = skipString(body, i);
10218
+ continue;
10219
+ }
10220
+ if (c3 === "(" || c3 === "[" || c3 === "{")
10221
+ depth++;
10222
+ else if (c3 === ")" || c3 === "]" || c3 === "}") {
10223
+ if (depth === 0)
10224
+ return body.slice(from, i);
10225
+ depth--;
10226
+ } else if (c3 === "," && depth === 0)
10227
+ return body.slice(from, i);
10228
+ }
10229
+ return body.slice(from);
10230
+ }
10231
+ function stringLiteral(raw) {
10232
+ const s = raw.trim();
10233
+ if (s.length < 2)
10234
+ return null;
10235
+ const q = s[0];
10236
+ if ((q === '"' || q === "'") && s.endsWith(q) && skipString(s, 0) === s.length - 1)
10237
+ return s.slice(1, -1);
10238
+ if (q === "`" && s.endsWith("`") && skipString(s, 0) === s.length - 1 && !s.includes("${"))
10239
+ return s.slice(1, -1);
10240
+ return null;
10241
+ }
10242
+ function tweenVarObjects(js) {
10243
+ const out = [];
10244
+ const re = /\.\s*(?:to|from|fromTo|set)\s*\(/g;
10245
+ let m;
10246
+ while (m = re.exec(js)) {
10247
+ const open2 = m.index + m[0].length - 1;
10248
+ const args = parenArg(js, open2);
10249
+ if (args === null)
10250
+ continue;
10251
+ for (let i = 0;i < args.length; i++) {
10252
+ const c3 = args[i];
10253
+ if (c3 === '"' || c3 === "'" || c3 === "`") {
10254
+ i = skipString(args, i);
10255
+ continue;
10256
+ }
10257
+ if (c3 !== "{")
10258
+ continue;
10259
+ const body = braceBlock(args, i);
10260
+ out.push(body);
10261
+ i += body.length + 1;
10262
+ }
10263
+ re.lastIndex = open2 + 1;
10264
+ }
10265
+ return out;
10266
+ }
10267
+ function detectFilterCost(html) {
10268
+ const src = maskHtmlComments(html);
10269
+ const svgConvIds = svgConvolutionFilterIds(src);
10270
+ const animated = [];
10271
+ const staticFullBleed = [];
10272
+ const indeterminate = [];
10273
+ const add = (bucket, site) => {
10274
+ const s = site.replace(/\s+/g, " ").trim().slice(0, 120);
10275
+ if (!bucket.includes(s))
10276
+ bucket.push(s);
10277
+ };
10278
+ for (const own of styleBlockOwnDecls(src)) {
10279
+ const hit = filterDecls(own).filter((val) => filterValueConvolves(val, svgConvIds));
10280
+ if (hit.length && isFullBleedStyle(own))
10281
+ add(staticFullBleed, `<style> 规则:filter:${hit[0]}`);
10282
+ }
10283
+ for (const m of src.matchAll(/<[a-zA-Z][^>]*>/g)) {
10284
+ const tag = m[0];
10285
+ const style = attr(tag, "style") ?? "";
10286
+ const inline = filterDecls(style).filter((val) => filterValueConvolves(val, svgConvIds));
10287
+ const refAttr = attr(tag, "filter");
10288
+ const byAttr = refAttr !== undefined && filterValueConvolves(refAttr, svgConvIds) ? refAttr : null;
10289
+ if (!inline.length && byAttr === null)
10290
+ continue;
10291
+ if (!isFullBleedStyle(style))
10292
+ continue;
10293
+ add(staticFullBleed, inline.length ? `内联 style:filter:${inline[0]}` : `SVG 属性:filter="${byAttr}"`);
10294
+ }
10295
+ const js = maskJsComments(scriptBodiesOnly(src));
10296
+ for (const body of tweenVarObjects(js)) {
10297
+ for (const km of body.matchAll(/(?:^|[{,])\s*["']?(filter|webkitFilter|WebkitFilter|-webkit-filter)["']?\s*:/g)) {
10298
+ const raw = readObjValue(body, km.index + km[0].length);
10299
+ const lit = stringLiteral(raw);
10300
+ if (lit === null)
10301
+ add(indeterminate, `补间属性 ${km[1]}:${raw}`);
10302
+ else if (filterValueConvolves(lit, svgConvIds))
10303
+ add(animated, `补间属性 ${km[1]}:"${lit}"`);
10304
+ }
10305
+ if (svgConvIds.size && /(?:^|[{,])\s*["']?stdDeviation["']?\s*:/.test(body))
10306
+ add(animated, "补间驱动 SVG 滤镜基元的 stdDeviation");
10307
+ }
10308
+ return { animated, staticFullBleed, indeterminate };
10309
+ }
10094
10310
  function lintParticle(html, opts = {}) {
10095
10311
  const v = [];
10096
10312
  const push = (law, fatal, msg) => v.push({ law, fatal, msg });
@@ -10190,6 +10406,13 @@ function lintParticle(html, opts = {}) {
10190
10406
  const amount = batch.count === null ? "条数未知(循环边界非数字字面量)" : `静态估算 ${batch.count} 个`;
10191
10407
  push("8-primitive-merge", false, `${batch.site} 向同一父节点「${batch.parent}」循环生成 ${amount} <${batch.tag}>。` + "这里有一批可无损合并的重复图元:请按相同 stroke/fill 分档合并成单个 <path> 的多子路径;" + "合并后画面逐像素不变,属零成本改法。本项只提示写法形态,最终画面仍以真渲染出片为准");
10192
10408
  }
10409
+ const cost = detectFilterCost(html);
10410
+ if (cost.animated.length)
10411
+ push("c-filter-animated", false, `时间线补间直接驱动了真卷积滤镜(${cost.animated.slice(0, 3).join(";")})——` + "blur / drop-shadow / feGaussianBlur 是**真卷积**,每帧成本 ∝「被覆盖面积 × 半径 × 帧数」;" + "一旦被补间驱动,卷积结果**每帧失效**、缓存彻底失灵,这是本组三项里成本机制最重的一项。" + "改法:用 `opacity` / `transform`(`scale` / 位移)表达同一叙事动作;" + "确需滤镜就做成**静态两态切换**(滤镜值不随时间连续变),并把被滤元素的几何覆盖面积收到真正需要的那块矩形上。" + "本项是**成本提示**,不影响 ok / 退出码 / 铺轨;**未报 ≠ 这颗便宜**——真判据是真渲染出片计时");
10412
+ if (cost.staticFullBleed.length)
10413
+ push("c-filter-static-fullbleed", false, `整幅静态真卷积滤镜(${cost.staticFullBleed.slice(0, 3).join(";")})——该声明自身即全幅` + "(`position:absolute|fixed` + `inset:0` 或等价铺满),于是每帧都要对整幅做一次卷积。" + "**首选改法:缩小被滤镜覆盖的几何面积**(把滤镜收到它真正需要的那块矩形上——面积一项同时压掉卷积与整幅逐帧合成两笔成本);" + "次选把静态滤镜结果**预烘成图**,且 MUST 用 **RGBA PNG**(透明叠加颗粒的 alpha 是成片合成的必需通道," + "烘成 JPEG 或任何无 alpha 格式会在成片里塌成不透明色块),但预烘只消得掉卷积、消不掉整幅逐帧合成,收益有上限、必有残差。" + "去不去滤镜属审美取值,判断权在作者:本项只给信息与改法,**恒非致命**、不拦铺轨;未报 ≠ 便宜,真判据是真渲染出片计时");
10414
+ if (cost.indeterminate.length)
10415
+ push("c-filter-indeterminate", false, `该处 filter 值无法静态判定是否含真卷积(${cost.indeterminate.slice(0, 3).join(";")})——` + "补间值不是字符串字面量(变量 / 模板串 / 函数返回),本 lint 不做表达式求值与常量折叠," + "故对该处**未作判定**:既不是「判过且通过」,也不是命中。请人工确认它是否会驱动 blur / drop-shadow;" + "若是,按 `c-filter-animated` 的改法处理。本项恒非致命、不拦铺轨");
10193
10416
  return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
10194
10417
  }
10195
10418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitruck/cli",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通。",
5
5
  "license": "MIT",
6
6
  "author": "Gitruck (同合云)",
@@ -111,7 +111,10 @@ gtrk matrix --project "<split 产物目录>" [--lay N] [--score-floor F] [--top-
111
111
  - **在同合云桌面客户端(opencut / OpenCut Gitruck Edition)里打开这个工程**,B-roll 候选轨已经铺好。
112
112
  - **用轨道头的「小眼睛」开关逐条切换对比**:看哪条候选最贴这段口播的情绪/画面,留下满意的、关掉不要的。
113
113
  - 候选默认是 preview 代理(轻量预览);**下载原片属挑选后的动作**(客户端挑选 UI),确认要哪条再拉原片。
114
- - **候选轨下方还垫了一条纯黑底轨**(`lay.blackTrack`),按 beat 包络整条铺满,作用是 B-roll 期间遮住底下的口播画面(含候选轨上的留空处)——删多余候选轨时**别误删它**;换片请拖到**候选轨的颗粒**上、**别拖到黑底条上**(含拖拽保护的客户端会直接拒绝并提示;尚未升级到该版本的客户端会被误拖打出黑底破洞、该处漏口播)。不想要黑底就加 `--no-black-bed` 重跑,会自动剥净。
114
+ - **候选轨下方还垫了一条纯黑底轨**(`lay.blackTrack`),按 beat 包络整条铺满,作用是 B-roll 期间遮住底下的口播画面(含候选轨上的留空处)——删多余候选轨时**别误删它**;换片请拖到**候选轨的颗粒**上、**别拖到黑底条上**。不想要黑底就加 `--no-black-bed` 重跑,会自动剥净。
115
+ - **拖到黑底条上现在既不换片、也没有任何提示**——**当前所有已发版客户端都没有这层拖拽保护**(保护代码已落,但**待发版**,用户手上的版本里没有)。实际发生的是:**静默新建一条 video 轨**,把这条 B-roll 插进去;落点在黑底条**下半区**时新轨排在不透明黑底**之下**,预览里完全看不见 ⇒ 用户会觉得「我拖了,没反应」。
116
+ - 所以用户说「拖了没变化 / 画面没换」时,**先按这条排查**:看轨道数是不是比刚才多了一条;**按一次 `Ctrl+Z` 就整条撤销**(插入 + 建轨一起回、不留空轨),再让他拖到候选轨的颗粒上。
117
+ - **MUST NOT 跟用户说「客户端会拒绝并提示」**——那是发版后才成立的行为,现在承诺就是骗他。
115
118
  - 觉得填充有问题(太杂/太空/漏段)先别急着往下——**回来告诉我**,我按下面「常见情况」调参重铺。
116
119
 
117
120
  **用户明确说「B-roll 就这样、可以了」之后**,才交棒 ④。别自作主张替他拍板往下冲。
@@ -85,6 +85,13 @@ gtrk mg lint "<产物目录>/mg/<composition_id>.html" --dispatch "<产物目录
85
85
  - `x-engine-api-override`:颗粒运行时覆写了 `tl.seek`(老颗粒常见的 `rr-seek-shim`)或把 `__timelines[…]` 换成了包装对象。它会推翻引擎显式传的 `seek(t, true)`,且引擎改走 `time()`/`progress()` 就完全失效。**既有垫片属过渡态**:2026-07-26 引擎侧结论已核实(引擎本就不抑制回调),垫片已无保护作用、也无害,**可择期清理**(删后须重跑 lint 并重渲复验),不清也不拦;新颗粒别再加。
86
86
  - `x-raf-interval`:颗粒里有 `requestAnimationFrame(` / `setInterval(`。这类自有时钟**不被 seek 驱动**,逐帧渲染时等于冻结(契约明令:所有视觉变化必须挂在 tl 上)。静态正则分不清用途,报了要**人眼确认它是不是在驱动画面**;若是 → 回步骤 2 让生产 skill 改挂 tl。
87
87
  - 另:引擎不抑制回调 = 逐帧 scrub 时 `onComplete`/`onStart`/`onRepeat` 会**反复触发**。颗粒里**别写「只跑一次」的回调**(累加计数 / `push` 数组 / 一次性 DOM 插入),要写成每次从补间状态**重算**的幂等形式。
88
+ - **成本项 `c-filter-*` 三项恒非致命、当期全部不拦铺轨**(正本条款 = `contracts/gsap-emit-v1.md`「渲染成本:真卷积滤镜」一节):这组查的是**每帧成本**、不是正确性——命中的颗粒**画面是对的,只是渲得慢**。
89
+ - `c-filter-animated`:**时间线补间直接驱动了真卷积滤镜**(`filter:blur()/drop-shadow()`,或补间驱动 SVG `feGaussianBlur` 的 `stdDeviation`)。滤镜值一变,上一帧的卷积结果当场失效、缓存彻底失灵,是这组里**最贵**的形态。**处置**:回步骤 2 让生产 skill 改用 `opacity` / `transform`(`scale`/位移)表达同一叙事动作;确需滤镜就做成**静态两态切换**。**不拦铺轨**。
90
+ - `c-filter-static-fullbleed`:**整幅静态真卷积滤镜**(该声明自身就是 `position:absolute;inset:0` 一类铺满形态)。每帧对整幅做一次卷积。**首选改法是缩小被滤镜覆盖的几何面积**(面积一项同时压掉卷积与整幅逐帧合成两笔成本);次选把静态滤镜结果预烘成图,且**必须是 RGBA PNG**(透明叠加颗粒的 alpha 是成片合成的必需通道,烘成 JPEG 或无 alpha 格式会在成片里塌成不透明色块),但预烘只消得掉卷积、消不掉整幅逐帧合成,**收益有上限**。去不去滤镜属审美取值,**判断权在作者**,本项只给信息与改法。**不拦铺轨**。
91
+ - `c-filter-indeterminate`:补间的 `filter` 值不是字符串字面量(变量 / 模板串 / 函数返回),lint **对该处没作判定**——既不是「判过且通过」,也不是命中。**人眼确认它到底会不会驱动 blur / drop-shadow**;会就按 `c-filter-animated` 的改法处理。**不拦铺轨**。
92
+ - **`box-shadow` 整族不在射程内**(含 `box-shadow: 0 0 0 Npx` 这类零卷积脉冲写法,也含带模糊半径的写法)——看到 `box-shadow` 没被报**不是漏报**,是既定射程边界。
93
+ - ⚠️ **没报 ≠ 这颗便宜**:这三项是**形态提示**,真判据永远是**真渲染出片计时**。收益也别外推——现网既有栏目语料(21 颗)去掉滤镜只省 **0~3.1%**(2026-07-30 真机实测,仅对该语料成立),这组规则的价值在**滤镜重的新语料进管线前先把门立好**。
94
+ - **id 前缀读法**(三个命名空间,别按前缀反推错阻断行为):**数字前缀 `1-`~`8-`** = 契约铁律项,致命性**按项声明**(`4-*` / `5-*` / `7-*` / `8-*` 就都不致命);**`x-`** = 哨兵项,**恒不拦**;**`c-`** = 成本项,致命性按项声明,**当前三项均不拦**。
88
95
  - **不过(非 0 退出、逐条报因)→ 把报错原样丢回栏目 MG 生产 skill 修,重产重 lint,别硬铺**。铺一颗不合规颗粒会污染工程。
89
96
  - 只想批量干校验不写回:`gtrk mg --project <dir> --lint-only`(有 beat 没过就 `ok:false` + **非 0 退出**,工程一个字节都不动)。
90
97