Jianying Reference Harvesting
Get the authoritative implementation of a Jianying effect instead of guessing
from pixels. Every effect the user has ever applied is cached on disk as a full
package — often with readable Lua source that states the exact easing, distances,
and per-character timing. Combine that with stepped-frame captures from the UI
and you have both the math and the visual ground truth.
Two evidence tiers, always collect both when possible:
- Effect package source (exact math, when the package is legacy Lua format)
- Stepped-frame captures (visual ground truth, works for every format)
Key Paths
~/Movies/JianyingPro/User Data/Cache/effect/ # one dir per effect package
~/Movies/JianyingPro/User Data/Projects/ # drafts (encrypted in new versions)
Each package: effect/<numeric-id>/<md5-hash>/ containing one of:
- Legacy Lua format (goldmine):
TextAnim.lua, Transform.lua,
PrinterOne.lua, EnlargeIn.lua, BounceIn.lua, … plus config.json,
anim.prefab. The Lua declares tweens explicitly:
tween:fromTo(obj, {alpha=0}, {alpha=1}, dur, Amaz.Ease.quadOut, ...).
- Node-graph format (partial → often better than it looks):
textAnim.lsproj
studioAnim.lsanim + res/. The driver (.lsproj, res/*.jsdat) is
encrypted, but three readable layers usually remain:
res/ node names expose the effect graph — e.g. 粒子碎落 =
LinearWipe + Dust + DeepGlowSimple, which is the architecture.
- Compiled shader products are PLAINTEXT even when
.ausl sources are
encrypted: read …/xshader/shaderLib/shaderMetal/*.vert|*.frag (or
shaderGLES/). For GPU-particle effects the entire closed-form motion
lives in the vertex shader (instanceID → noise sample → offset ×
release-front weight + rotated gravity) — a line-by-line whitebox.
strings on .prefab files yields the full emitter parameter
schema (particleTotalNum, pSize*, pOpacityOverLife, gravity,
gravityRot, emitterScale/Translation, pLifeRandom, …); calibrate
the numeric values from stepped frames.
Only fall back to pure frame captures when all three layers come up empty.
- AE-keyframe hybrids:
TextAnim.lua that samples baked curves via
self.Position:getCurPartVal(progress) — curve data lives in sibling JSON;
treat as node-graph tier.
Step 1 — Map an effect card to its package (mtime marker)
Package dirs are numeric ids with no name field, and drafts are encrypted, so
map UI card → package by modification time:
SCRATCH=<scratch-dir>/jy-anim; mkdir -p "$SCRATCH"
touch "$SCRATCH/.marker"
# ... apply exactly ONE effect in the Jianying UI ...
cd "$HOME/Movies/JianyingPro/User Data/Cache/effect"
NEW=$(find . -maxdepth 1 -type d -newer "$SCRATCH/.marker" ! -name . | head -1)
# An already-cached effect downloads nothing, so an empty result is normal —
# bail instead of letting "$NEW"/* expand to /* and copy the filesystem root.
if [ -z "$NEW" ]; then
echo "no new package: this effect was already cached (harvest impossible)"
else
INNER=$(find "$NEW" -mindepth 1 -maxdepth 1 -type d | head -1)
if [ -z "$INNER" ]; then
echo "unexpected layout under $NEW"
else
mkdir -p "$SCRATCH/<effect-name>"
cp -R "$INNER/." "$SCRATCH/<effect-name>/"
fi
fi
touch "$SCRATCH/.marker" # reset for the next effect
Apply → harvest → reset, one effect at a time. Never batch-apply then guess.
Step 2 — Read the Lua for exact semantics
Worked examples decoded this way (text animations):
| Effect |
File |
Authoritative math |
| 打字机 I |
PrinterOne.lua |
char i alpha 0→1, duration dur·i/(N+1), Ease.Floor → hard step at (i)/(N+1); nothing at t=0, all visible at N/(N+1) |
| 向上滑动 |
Transform.lua |
whole block, localPosition from (0, −2.66·textureH/screenH) → 0 ⇒ 1.33 × block height, quadOut; alpha 0→1 quadOut |
| 放大 |
EnlargeIn.lua |
localScale 0.5→1.0 quadOut, alpha quadOut, no overshoot |
| 弹入 |
BounceIn.lua |
elastic-out exp(−7t)·sin((t−0.075)·2π/0.3)+1 |
| 打字机光标 |
TextAnim.lua + sprite |
cursor is a solid block sprite, solid while typing, gone at end; per-char times from a random table |
Conventions to know when reading:
localPosition Y is in half-screen units: offset_px = value × screenH / 2.
Amaz.Ease.Floor = pure step at tween end (this is how typewriters "蹦字").
seek(time)/setDuration mean every tween is scrubbed, so formulas are exact
at any frame — no integration drift.
Step 3 — Stepped-frame capture protocol (UI)
Precision rules learned the hard way:
- Move the text element past the video's end so the player shows it on
black; snap-drag its left edge near the video end.
- Find the element's first frame: click the ruler past it, press
↑
(previous edit point) — the timecode (bottom-left, e.g. 01:24:33:04) is
your F0 anchor. Verify it after EVERY apply.
- Apply the effect card, then wait ≥3 s — applying auto-plays a preview.
Before stepping, confirm the transport shows ▶ (stopped), not ⏸.
A ⏸ means your frames are contaminated by live playback — redo.
Loop animations are the exception: Jianying does not evaluate 循环
animations while paused-and-stepping — stepped frames come back as static
text. Capture loops by playing and pausing (space → wait a fraction of the
cycle → space); the paused frame keeps the loop pose frozen and can be
screencaptured cleanly.
- Step with
→ (1 frame each), capture a tight zoom of the player region at
every 2–3 frames across the whole duration (default 0.5 s = 15 frames
@30fps). Capture one frame past the end for the steady state.
- Record the 动画时长 field right after applying — that's the preset's default
duration and should match QCut's
defaultDuration.
- Re-selecting: clicking an animation card only applies when the element is
still selected. If the panel click did nothing, re-click the element first.
- Never use cmd+z chains to reset state (undo-stack desync) — delete and
re-add instead. Beware other windows stealing frontmost focus mid-batch.
Longer text reveals stagger better: use ~12 mixed CJK+ASCII graphemes like
剪映动画测试ABC123.
Step 4 — Port to QCut and verify frame-by-frame
QCut's text-animation engine lives in
packages/editor-core/src/text-animation/ (compile → evaluate, pure functions)
with presets in apps/web/src/lib/text/text-animation-presets/.
- Translate the Lua math into effect params / easing / stagger. Notable
precedents: Jianying's typewriter slotting
(rank+1)/(unitCount+1) is
implemented in evaluate.ts (typewriterUnitProgress), and
EASE_OUT_QUAD in effects.ts mirrors Amaz.Ease.quadOut. For loop
animations, QCut treats sequence stagger as a cyclic phase offset
(phaseUnitProgress with wrap), which is how per-char sin/cos channels
with rank-spread phases become Jianying's ring layouts; orbit's ring
mode additionally cancels each unit's layout offset so every glyph rides
one centered circle (环绕). Do not trust preset names — 空间翻转 III is a
planar rotate oscillation, not a flip.
- Verify numerically before trusting pixels — evaluate the compiled animation
at every frame and compare against the Lua formula:
// bun script: expected visible chars per frame for 打字机 I
const jy = (f: number) => Math.min(N, Math.floor(Math.min(1, f / F) * (N + 1)));
// evaluateTextAnimationFrame(...) at each frame must match jy(f) exactly
- Lock the semantics with unit tests in
packages/editor-core/src/__tests__/ (see
"reveals typewriter units on Jianying's (rank+1)/(count+1) slots").
- For visual confirmation inside QCut, drive the real editor:
bun run build && bun run electron, then bun run pipeline editor:project:create / editor:timeline:apply / editor:timeline:seek with a
manifest generated from the real preset helpers
(applyTextAnimationPreset), and screenshot the preview at the same
normalized times as the Jianying captures. Gotchas: the editor API listens
on port 8765 (QCUT_API_PORT=8765 if discovery picks the wrong port);
editor:timeline:apply --verify compares JSON strings, so preset effect
objects must emit keys in the normalizer's order or verification fails on
a semantically identical animation.
Capture Traps
- A fully occluded Electron window stops rendering; window-id screencapture
(
screencapture -l <id>) then returns a stale backing store. Keep the
window on a visible display (move it with System Events if the user is
working on the main one) before seek-and-capture.
- Directory names lie:
AmazingFeature_particle in several packages is a
noise-grain shader, not a particle emitter. Verify by reading the shader,
not the folder name.
- The panel window can get resized between sessions — re-anchor card
coordinates from a fresh screenshot instead of reusing yesterday's grid.
screencapture -l <windowID> pads the image with the window shadow, and
the margin CHANGES with focus state (focused windows cast bigger shadows)
— a mapping calibrated on one capture silently drifts on the next. Prefer
a display-rect capture (screencapture -R -1920,0,1920,1080) of only the
display Jianying is on: pixel == global coordinate, stable, and it cannot
photograph the user's other displays.
cliclick treats a negative coordinate as a RELATIVE move unless prefixed
with = (c:=-654,160). On a display left of main, un-prefixed clicks
land at arbitrary offsets and look like the app "ignored" them.
- macOS keeps re-fronting the Claude app between tool calls. Start every
interaction batch with
set frontmost of process "VideoFusion-macOS",
verify frontmost, and do all clicks/keys inside that same shell call.
- Frame stepping:
cliclick kp:arrow-left mostly gets swallowed by
Jianying; System Events key code 123/124 with ~0.3s delays is reliable
and frame-exact (verify by cropping the player timecode).
- Jianying ignores every synthetic scroll injection (line, pixel, phased
trackpad CGEvents, drag-scroll) — deep list items are unreachable by
scrolling. Use the animation panel's magnifier search instead: click it,
osascript -e 'set the clipboard to "彩带喷射"', Cmd+V, Return.
(printf | pbcopy yields mojibake — always set the clipboard via
AppleScript for CJK.)
- The accessibility API is NOT an escape hatch from the line above (verified
2026-08-17 against the 音效库 list). Every
AXScrollBar reports AXValue
settable=true and accepts the write without error, then discards it — the
grid's pixels were byte-identical before and after (same md5). The tree is
label-only in general: root_音频 and the other panel tabs are AXStaticText
with an EMPTY action list, so AXPress does nothing either. What AX IS good
for is reading live element coordinates without stealing focus, which fixes
the "panel was resized, yesterday's grid is wrong" trap above —
scroll bars of window 1 resolves in ~1s where filtering
UI elements of window 1 by role times out past 40s on a populated grid.
- Never drag with a y-range that can leave the panel you are targeting. A drag
to y=700 aimed at the media panel (bottom edge ~680) grabs the timeline
divider and UNDOCKS the timeline into a floating window — no data loss, but
the layout must be restored by hand. Clamp drag bounds to coordinates read
from the AX tree, never to hand-written config.
- NEVER send Cmd+A unless a text field is visibly focused: if the search
box closed itself, Cmd+A selects every timeline clip and a follow-up
Delete wipes the project (Cmd+Z recovers — check the timeline crop
immediately after any destructive-capable keystroke).
- Re-applying an already-cached effect leaves ZERO disk trace (no new files
anywhere under
Cache/), so an mtime marker proves nothing either way —
only the card's download arrow disappearing confirms the apply.
- Not everything is procedural: 彩带喷射 ships a side-by-side alpha MP4
(left=matte right=color) plus a RadialBlur node, and 福袋炸开 composites
prerendered 3D assets. Check
find <pkg> -name "*alpha*.mp4" before
hunting for emitter math; if the reference is baked footage, port a
procedural approximation and record it as a known difference.
Scope Notes
- The harvesting/capture workflow above is shared by filters (pure color grades have their own skill:
jianying-filter-parity), transitions (jianying-transition-reference),
画面特效/人物特效 (jianying-video-effect-reference — catalog/draft/package
anatomy for the 特效 panel), and stickers — but only that workflow is
shared; render contracts and cache layouts differ per family.
Cache/effect only contains effects that have been applied at least once on
this machine; apply the card once to force the download.
- Read-only analysis of locally cached files for interop/parity purposes; do
not redistribute Jianying's assets or ship any harvested content in QCut.
- That rule covers the repo, not just the product: never commit decompiled
shaders,
strings dumps, prefab blobs, or frames extracted from their bundled
media. Transcribe the behaviour into your own equations/prose in the task doc
and leave the raw files in the session scratch dir. Screenshots of on-screen
output for side-by-side comparison are fine.
1---2name: jianying-reference3description: Reverse-engineer how Jianying (剪映专业版) implements an effect — text animations, filters, transitions, stickers — by harvesting its on-disk effect packages (TextAnim.lua, config.json, node-graph lsproj) and capturing stepped reference frames from its UI, then porting the exact math into QCut and locking it with tests. Use for 剪映对标, 对齐剪映, 剪映参照, 剪映动画怎么实现的, 逆向剪映特效, matching Jianying behavior, or verifying a QCut effect against Jianying frame by frame.4---56# Jianying Reference Harvesting78Get the *authoritative* implementation of a Jianying effect instead of guessing9from pixels. Every effect the user has ever applied is cached on disk as a full10package — often with readable Lua source that states the exact easing, distances,11and per-character timing. Combine that with stepped-frame captures from the UI12and you have both the math and the visual ground truth.1314Two evidence tiers, always collect both when possible:15161. **Effect package source** (exact math, when the package is legacy Lua format)172. **Stepped-frame captures** (visual ground truth, works for every format)1819## Key Paths2021```text22~/Movies/JianyingPro/User Data/Cache/effect/ # one dir per effect package23~/Movies/JianyingPro/User Data/Projects/ # drafts (encrypted in new versions)24```2526Each package: `effect/<numeric-id>/<md5-hash>/` containing one of:2728- **Legacy Lua format** (goldmine): `TextAnim.lua`, `Transform.lua`,29 `PrinterOne.lua`, `EnlargeIn.lua`, `BounceIn.lua`, … plus `config.json`,30 `anim.prefab`. The Lua declares tweens explicitly:31 `tween:fromTo(obj, {alpha=0}, {alpha=1}, dur, Amaz.Ease.quadOut, ...)`.32- **Node-graph format** (partial → often better than it looks): `textAnim.lsproj`33 + `studioAnim.lsanim` + `res/`. The driver (`.lsproj`, `res/*.jsdat`) is34 encrypted, but three readable layers usually remain:35 1. **`res/` node names** expose the effect graph — e.g. 粒子碎落 =36 `LinearWipe` + `Dust` + `DeepGlowSimple`, which is the architecture.37 2. **Compiled shader products are PLAINTEXT** even when `.ausl` sources are38 encrypted: read `…/xshader/shaderLib/shaderMetal/*.vert|*.frag` (or39 `shaderGLES/`). For GPU-particle effects the entire closed-form motion40 lives in the vertex shader (instanceID → noise sample → offset ×41 release-front weight + rotated gravity) — a line-by-line whitebox.42 3. **`strings` on `.prefab` files** yields the full emitter parameter43 schema (`particleTotalNum`, `pSize*`, `pOpacityOverLife`, `gravity`,44 `gravityRot`, `emitterScale/Translation`, `pLifeRandom`, …); calibrate45 the numeric values from stepped frames.46 Only fall back to pure frame captures when all three layers come up empty.47- AE-keyframe hybrids: `TextAnim.lua` that samples baked curves via48 `self.Position:getCurPartVal(progress)` — curve data lives in sibling JSON;49 treat as node-graph tier.5051## Step 1 — Map an effect card to its package (mtime marker)5253Package dirs are numeric ids with no name field, and drafts are encrypted, so54map UI card → package by modification time:5556```bash57SCRATCH=<scratch-dir>/jy-anim; mkdir -p "$SCRATCH"58touch "$SCRATCH/.marker"59# ... apply exactly ONE effect in the Jianying UI ...60cd "$HOME/Movies/JianyingPro/User Data/Cache/effect"61NEW=$(find . -maxdepth 1 -type d -newer "$SCRATCH/.marker" ! -name . | head -1)62# An already-cached effect downloads nothing, so an empty result is normal —63# bail instead of letting "$NEW"/* expand to /* and copy the filesystem root.64if [ -z "$NEW" ]; then65 echo "no new package: this effect was already cached (harvest impossible)"66else67 INNER=$(find "$NEW" -mindepth 1 -maxdepth 1 -type d | head -1)68 if [ -z "$INNER" ]; then69 echo "unexpected layout under $NEW"70 else71 mkdir -p "$SCRATCH/<effect-name>"72 cp -R "$INNER/." "$SCRATCH/<effect-name>/"73 fi74fi75touch "$SCRATCH/.marker" # reset for the next effect76```7778Apply → harvest → reset, one effect at a time. Never batch-apply then guess.7980## Step 2 — Read the Lua for exact semantics8182Worked examples decoded this way (text animations):8384| Effect | File | Authoritative math |85|---|---|---|86| 打字机 I | `PrinterOne.lua` | char *i* alpha 0→1, duration `dur·i/(N+1)`, `Ease.Floor` → hard step at `(i)/(N+1)`; nothing at t=0, all visible at `N/(N+1)` |87| 向上滑动 | `Transform.lua` | whole block, `localPosition` from `(0, −2.66·textureH/screenH)` → 0 ⇒ **1.33 × block height**, `quadOut`; alpha 0→1 `quadOut` |88| 放大 | `EnlargeIn.lua` | `localScale` 0.5→1.0 `quadOut`, alpha `quadOut`, no overshoot |89| 弹入 | `BounceIn.lua` | elastic-out `exp(−7t)·sin((t−0.075)·2π/0.3)+1` |90| 打字机光标 | `TextAnim.lua` + sprite | cursor is a **solid block sprite**, solid while typing, gone at end; per-char times from a random table |9192Conventions to know when reading:93- `localPosition` Y is in half-screen units: offset_px = value × screenH / 2.94- `Amaz.Ease.Floor` = pure step at tween end (this is how typewriters "蹦字").95- `seek(time)`/`setDuration` mean every tween is scrubbed, so formulas are exact96 at any frame — no integration drift.9798## Step 3 — Stepped-frame capture protocol (UI)99100Precision rules learned the hard way:1011021. **Move the text element past the video's end** so the player shows it on103 black; snap-drag its left edge near the video end.1042. Find the element's first frame: click the ruler past it, press `↑`105 (previous edit point) — the timecode (bottom-left, e.g. `01:24:33:04`) is106 your F0 anchor. Verify it after EVERY apply.1073. Apply the effect card, then **wait ≥3 s** — applying auto-plays a preview.108 Before stepping, confirm the transport shows **▶ (stopped)**, not ⏸.109 A ⏸ means your frames are contaminated by live playback — redo.110 **Loop animations are the exception**: Jianying does not evaluate 循环111 animations while paused-and-stepping — stepped frames come back as static112 text. Capture loops by playing and pausing (space → wait a fraction of the113 cycle → space); the paused frame keeps the loop pose frozen and can be114 screencaptured cleanly.1154. Step with `→` (1 frame each), capture a tight zoom of the player region at116 every 2–3 frames across the whole duration (default 0.5 s = 15 frames117 @30fps). Capture one frame past the end for the steady state.1185. Record the 动画时长 field right after applying — that's the preset's default119 duration and should match QCut's `defaultDuration`.1206. Re-selecting: clicking an animation card only applies when the element is121 still selected. If the panel click did nothing, re-click the element first.1227. Never use cmd+z chains to reset state (undo-stack desync) — delete and123 re-add instead. Beware other windows stealing frontmost focus mid-batch.124125Longer text reveals stagger better: use ~12 mixed CJK+ASCII graphemes like126`剪映动画测试ABC123`.127128## Step 4 — Port to QCut and verify frame-by-frame129130QCut's text-animation engine lives in131`packages/editor-core/src/text-animation/` (compile → evaluate, pure functions)132with presets in `apps/web/src/lib/text/text-animation-presets/`.1331341. Translate the Lua math into effect params / easing / stagger. Notable135 precedents: Jianying's typewriter slotting `(rank+1)/(unitCount+1)` is136 implemented in `evaluate.ts` (`typewriterUnitProgress`), and137 `EASE_OUT_QUAD` in `effects.ts` mirrors `Amaz.Ease.quadOut`. For loop138 animations, QCut treats sequence stagger as a **cyclic phase offset**139 (`phaseUnitProgress` with `wrap`), which is how per-char sin/cos channels140 with rank-spread phases become Jianying's ring layouts; orbit's `ring`141 mode additionally cancels each unit's layout offset so every glyph rides142 one centered circle (环绕). Do not trust preset names — 空间翻转 III is a143 planar rotate oscillation, not a flip.1442. Verify numerically before trusting pixels — evaluate the compiled animation145 at every frame and compare against the Lua formula:146147```ts148// bun script: expected visible chars per frame for 打字机 I149const jy = (f: number) => Math.min(N, Math.floor(Math.min(1, f / F) * (N + 1)));150// evaluateTextAnimationFrame(...) at each frame must match jy(f) exactly151```1521533. Lock the semantics with unit tests in154 `packages/editor-core/src/__tests__/` (see155 "reveals typewriter units on Jianying's (rank+1)/(count+1) slots").1564. For visual confirmation inside QCut, drive the real editor:157 `bun run build && bun run electron`, then `bun run pipeline158 editor:project:create / editor:timeline:apply / editor:timeline:seek` with a159 manifest generated from the real preset helpers160 (`applyTextAnimationPreset`), and screenshot the preview at the same161 normalized times as the Jianying captures. Gotchas: the editor API listens162 on port 8765 (`QCUT_API_PORT=8765` if discovery picks the wrong port);163 `editor:timeline:apply --verify` compares JSON strings, so preset effect164 objects must emit keys in the normalizer's order or verification fails on165 a semantically identical animation.166167## Capture Traps168169- A fully occluded Electron window stops rendering; window-id screencapture170 (`screencapture -l <id>`) then returns a stale backing store. Keep the171 window on a visible display (move it with System Events if the user is172 working on the main one) before seek-and-capture.173- Directory names lie: `AmazingFeature_particle` in several packages is a174 noise-grain shader, not a particle emitter. Verify by reading the shader,175 not the folder name.176- The panel window can get resized between sessions — re-anchor card177 coordinates from a fresh screenshot instead of reusing yesterday's grid.178- `screencapture -l <windowID>` pads the image with the window shadow, and179 the margin CHANGES with focus state (focused windows cast bigger shadows)180 — a mapping calibrated on one capture silently drifts on the next. Prefer181 a display-rect capture (`screencapture -R -1920,0,1920,1080`) of only the182 display Jianying is on: pixel == global coordinate, stable, and it cannot183 photograph the user's other displays.184- `cliclick` treats a negative coordinate as a RELATIVE move unless prefixed185 with `=` (`c:=-654,160`). On a display left of main, un-prefixed clicks186 land at arbitrary offsets and look like the app "ignored" them.187- macOS keeps re-fronting the Claude app between tool calls. Start every188 interaction batch with `set frontmost of process "VideoFusion-macOS"`,189 verify frontmost, and do all clicks/keys inside that same shell call.190- Frame stepping: `cliclick kp:arrow-left` mostly gets swallowed by191 Jianying; System Events `key code 123/124` with ~0.3s delays is reliable192 and frame-exact (verify by cropping the player timecode).193- Jianying ignores every synthetic scroll injection (line, pixel, phased194 trackpad CGEvents, drag-scroll) — deep list items are unreachable by195 scrolling. Use the animation panel's magnifier search instead: click it,196 `osascript -e 'set the clipboard to "彩带喷射"'`, Cmd+V, Return.197 (`printf | pbcopy` yields mojibake — always set the clipboard via198 AppleScript for CJK.)199- The accessibility API is NOT an escape hatch from the line above (verified200 2026-08-17 against the 音效库 list). Every `AXScrollBar` reports `AXValue`201 settable=true and accepts the write without error, then discards it — the202 grid's pixels were byte-identical before and after (same md5). The tree is203 label-only in general: `root_音频` and the other panel tabs are AXStaticText204 with an EMPTY action list, so AXPress does nothing either. What AX IS good205 for is reading live element coordinates without stealing focus, which fixes206 the "panel was resized, yesterday's grid is wrong" trap above —207 `scroll bars of window 1` resolves in ~1s where filtering208 `UI elements of window 1` by role times out past 40s on a populated grid.209- Never drag with a y-range that can leave the panel you are targeting. A drag210 to y=700 aimed at the media panel (bottom edge ~680) grabs the timeline211 divider and UNDOCKS the timeline into a floating window — no data loss, but212 the layout must be restored by hand. Clamp drag bounds to coordinates read213 from the AX tree, never to hand-written config.214- NEVER send Cmd+A unless a text field is visibly focused: if the search215 box closed itself, Cmd+A selects every timeline clip and a follow-up216 Delete wipes the project (Cmd+Z recovers — check the timeline crop217 immediately after any destructive-capable keystroke).218- Re-applying an already-cached effect leaves ZERO disk trace (no new files219 anywhere under `Cache/`), so an mtime marker proves nothing either way —220 only the card's download arrow disappearing confirms the apply.221- Not everything is procedural: 彩带喷射 ships a side-by-side alpha MP4222 (left=matte right=color) plus a RadialBlur node, and 福袋炸开 composites223 prerendered 3D assets. Check `find <pkg> -name "*alpha*.mp4"` before224 hunting for emitter math; if the reference is baked footage, port a225 procedural approximation and record it as a known difference.226227## Scope Notes228229- The harvesting/capture workflow above is shared by filters (pure color grades have their own skill:230 `jianying-filter-parity`), transitions (`jianying-transition-reference`),231 画面特效/人物特效 (`jianying-video-effect-reference` — catalog/draft/package232 anatomy for the 特效 panel), and stickers — but only that workflow is233 shared; render contracts and cache layouts differ per family.234- `Cache/effect` only contains effects that have been applied at least once on235 this machine; apply the card once to force the download.236- Read-only analysis of locally cached files for interop/parity purposes; do237 not redistribute Jianying's assets or ship any harvested content in QCut.238- That rule covers the **repo**, not just the product: never commit decompiled239 shaders, `strings` dumps, prefab blobs, or frames extracted from their bundled240 media. Transcribe the behaviour into your own equations/prose in the task doc241 and leave the raw files in the session scratch dir. Screenshots of on-screen242 output for side-by-side comparison are fine.