# Jianying Reference

> 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.

- Skill: `quriosity-agent/jianying-reference` (Agent Skill)
- Install (CLI): `npx skillmds@latest add quriosity-agent/jianying-reference`
- Raw SKILL.md: https://api.skillmd.com/api/skills/quriosity-agent/jianying-reference/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: quriosity-agent (https://skillmd.com/u/quriosity-agent)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/quriosity-agent/jianying-reference

---


# 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:

1. **Effect package source** (exact math, when the package is legacy Lua format)
2. **Stepped-frame captures** (visual ground truth, works for every format)

## Key Paths

```text
~/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:
  1. **`res/` node names** expose the effect graph — e.g. 粒子碎落 =
     `LinearWipe` + `Dust` + `DeepGlowSimple`, which is the architecture.
  2. **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.
  3. **`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:

```bash
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:

1. **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.
2. 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.
3. 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.
4. 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.
5. Record the 动画时长 field right after applying — that's the preset's default
   duration and should match QCut's `defaultDuration`.
6. 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.
7. 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/`.

1. 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.
2. Verify numerically before trusting pixels — evaluate the compiled animation
   at every frame and compare against the Lua formula:

```ts
// 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
```

3. Lock the semantics with unit tests in
   `packages/editor-core/src/__tests__/` (see
   "reveals typewriter units on Jianying's (rank+1)/(count+1) slots").
4. 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.

