| name | jianying-reference |
| description | 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. |
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"
cd "$HOME/Movies/JianyingPro/User Data/Cache/effect"
NEW=$(find . -maxdepth 1 -type d -newer "$SCRATCH/.marker" ! -name . | head -1)
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"
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:
const jy = (f: number) => Math.min(N, Math.floor(Math.min(1, f / F) * (N + 1)));
- 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 —
resolves in ~1s where filtering
by role times out past 40s on a populated grid.
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.