fablecut 1.12.1 → 1.14.0
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/CLAUDE.md +657 -11
- package/README.md +45 -10
- package/app.js +3352 -212
- package/color.js +47 -14
- package/comp.js +195 -0
- package/edit-ops.js +70 -17
- package/export-jobs.js +8 -5
- package/index.html +148 -62
- package/library/README.md +2 -0
- package/library/elements/logo-placeholder.svg +12 -0
- package/mask.js +467 -0
- package/mcp-server.js +639 -19
- package/motion.js +803 -0
- package/package.json +1 -1
- package/paths.js +25 -2
- package/server.js +42 -2
- package/style.css +188 -24
- package/templates.js +576 -0
- package/tracker.js +253 -0
package/CLAUDE.md
CHANGED
|
@@ -37,7 +37,10 @@ Every Claude Code session then has these tools:
|
|
|
37
37
|
- `fablecut_export` — render the timeline to a file in `exports/` with the editor's own
|
|
38
38
|
Fast export (open tab, or headless Chrome / Edge). See "Export" below.
|
|
39
39
|
- `fablecut_scopes` — measure the graded picture at a moment (levels, clipping, colour
|
|
40
|
-
cast), as the editor's scopes see it
|
|
40
|
+
cast), as the editor's scopes see it; also what a grade layer or a clip's masks
|
|
41
|
+
select. See "Color" and "Masks" below.
|
|
42
|
+
- `fablecut_track` — motion tracking: follow a region of a video clip, store it as
|
|
43
|
+
a track, and make a mask or another clip follow it. See "Tracking" below.
|
|
41
44
|
|
|
42
45
|
### Token-efficient editing (important for agents)
|
|
43
46
|
|
|
@@ -174,8 +177,10 @@ Reusable assets, visible in the editor's left-panel tabs and never copied:
|
|
|
174
177
|
| `library/elements/` | **Elements** | overlay art: alpha PNGs, light leaks, textures, stickers |
|
|
175
178
|
| `library/svg/` | **SVG** | animated vector graphics **you author** (convention below) |
|
|
176
179
|
| `library/fonts/` | font editor | `.ttf/.otf/.woff/.woff2`, auto-registered, family name = file name; a variable font draws every `weight` |
|
|
180
|
+
| `library/presets/` | Inspector → Motion → Presets | saved animation presets, one `.json` each (see "Presets and templates") |
|
|
181
|
+
| `library/templates/`| **Templates** | saved templates, one `.json` each, listed after the built-in ones |
|
|
177
182
|
|
|
178
|
-
- List via `GET /api/library?dir=sfx|elements|svg|fonts` (recursive; subfolders OK).
|
|
183
|
+
- List via `GET /api/library?dir=sfx|elements|svg|fonts|presets|templates` (recursive; subfolders OK).
|
|
179
184
|
- To use one in the timeline, add a media entry whose `src` is its library path,
|
|
180
185
|
e.g. `{ "id":"m_x", "name":"whoosh.mp3", "kind":"audio", "src":"/library/sfx/whoosh.mp3" }`
|
|
181
186
|
— then reference it from clips like any other media.
|
|
@@ -263,6 +268,8 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
263
268
|
{ "id": "A4", "kind": "audio" }
|
|
264
269
|
],
|
|
265
270
|
"buses": [ { "id": "B1", "name": "Dialogue", "gain": -2, "fx": [ { "type": "compressor" } ] } ],
|
|
271
|
+
"comps": [ { "id": "lt", "name": "Lower third", "width": 1280, "height": 720, "duration": 5, "clips": [ /* … */ ] } ],
|
|
272
|
+
// ^ optional — compositions (nested sequences), placed with kind:"comp" clips; see "Compositions (nested sequences)"
|
|
266
273
|
// ^ optional — submix buses B1…B8: name, gain (dB), pan, mute, fx; tracks route in with `out`
|
|
267
274
|
"media": [
|
|
268
275
|
{ "id": "m_abc", "name": "intro.mp4", "kind": "video", // video|audio|image|svg
|
|
@@ -275,8 +282,9 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
275
282
|
"clips": [
|
|
276
283
|
{
|
|
277
284
|
"id": "c_xyz", // unique string
|
|
278
|
-
"mediaId": "m_abc", // null for kind:"text" and
|
|
279
|
-
"kind": "video", // video | audio | image | svg | text | adjust
|
|
285
|
+
"mediaId": "m_abc", // null for kind:"text", "adjust", "shape", "null" and "comp"
|
|
286
|
+
"kind": "video", // video | audio | image | svg | text | adjust | shape | null | comp
|
|
287
|
+
// kind:"comp" also has "compId" (a comps[] entry) — see "Compositions (nested sequences)"
|
|
280
288
|
"track": "V1", // Vn…V1 (video) | A1…An (audio); extras via UI +V/+A or tracks[]
|
|
281
289
|
"start": 0, // timeline position, seconds
|
|
282
290
|
"in": 2.5, // offset into source media, seconds (0 for image/svg/text)
|
|
@@ -286,10 +294,13 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
286
294
|
"locked": true, // OPTIONAL — locked by the user: leave it alone (MCP patch refuses edits)
|
|
287
295
|
"disabled": true, // OPTIONAL — stays on the timeline, hidden from preview / audio / export
|
|
288
296
|
"unlinked": true, // OPTIONAL — the user unlinked this clip; never auto-relinked on load
|
|
297
|
+
"parent": "c_null", // OPTIONAL — moves, turns and scales with that clip (see "Shapes, nulls and parenting")
|
|
289
298
|
"props": { /* all optional — see the props reference below */ },
|
|
290
299
|
// OPTIONAL — keyframe animation. Times are seconds RELATIVE TO CLIP START.
|
|
291
300
|
// "ease" sits on the DESTINATION keyframe of each segment:
|
|
292
301
|
// linear | ease-in | ease-out | ease-in-out (default ease-in-out).
|
|
302
|
+
// x and y keys at the same time are a position key; pin / pout on them (px)
|
|
303
|
+
// are motion-path curve handles (see "Motion paths, motion blur and text on a path").
|
|
293
304
|
"keyframes": {
|
|
294
305
|
"scale": [ { "t": 0, "v": 1 }, { "t": 5, "v": 1.25, "ease": "linear" } ],
|
|
295
306
|
"opacity": [ { "t": 0, "v": 0 }, { "t": 0.8, "v": 1 } ]
|
|
@@ -313,7 +324,11 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
313
324
|
| `scale` | 1 | |
|
|
314
325
|
| `rotation` | 0 | degrees |
|
|
315
326
|
| `opacity` | 1 | 0–1 |
|
|
316
|
-
| `
|
|
327
|
+
| `anchorX`, `anchorY` | 0 | anchor point, px from the picture's centre: the point `x`/`y` place and `rotation`/`scale` turn around |
|
|
328
|
+
| `autoOrient` | false | turn the clip with its direction of travel along its motion path (`rotation` adds on top) |
|
|
329
|
+
| `motionBlur` · `motionBlurSamples` | 0 · 8 | motion blur: shutter angle in degrees (0 = off, 180 = film look, up to 720) · sub-frame samples 2–32 |
|
|
330
|
+
| `blend` | "normal" | how the clip lays over what is under it (see Track mattes and blending) |
|
|
331
|
+
| `matte` · `matteTrack` | — | a track matte (see Track mattes and blending) |
|
|
317
332
|
|
|
318
333
|
**Layout** (video/image/svg):
|
|
319
334
|
| prop | default | notes |
|
|
@@ -386,6 +401,8 @@ These are quick looks. For real grading use `props.grade` (see "Color" below).
|
|
|
386
401
|
| `bgColor` `bgOpacity` | "#000", 0 | rounded pill behind each line |
|
|
387
402
|
| `textAnim` | "none" | typewriter · word-pop · word-slide · karaoke · **letter-pop** (per-character entrance; Arabic/Indic auto-fallback to per-word clusters so joined letters shape correctly) · **wave** (looping per-character ride; same shaping fallback) · **bounce** (looping per-word hop) · **shake** (looping jitter) · **clip-reveal** (wipe-mask sweep, per line) · **zoom-in** (scale + opacity settle) · **font-cut** (rhythmically swaps typeface, then settles — see `fontCutSet`) · **rise-mask** (line rises from behind its baseline, lower-third reveal) |
|
|
388
403
|
| `wordRate` | 0.15 | seconds per word (typewriter: /4, letter-pop: /3 per character); also staggers `clip-reveal`/`zoom-in`/`rise-mask` per line |
|
|
404
|
+
| `textPath` | — | set the title along a path: one shape item (`path`, `line`, `ellipse`, `rect`, `polygon`, `star`) whose outline is the baseline; more lines follow on parallel curves below it. See "Motion paths, motion blur and text on a path" |
|
|
405
|
+
| `pathOffset` · `pathAlign` | 0 · "start" | where along the path the text sits (% of its length, keyframable, wraps on closed paths) · which end of the text sits there: start · center · end |
|
|
389
406
|
| `fontCutSet` | (curated) | array of font family names cycled by `font-cut`, e.g. `["Anton","Bebas Neue","Archivo Black","Oswald"]`; each is auto-loaded |
|
|
390
407
|
|
|
391
408
|
**Title styles (one-tap cohesive looks).** Text clips created in the UI now
|
|
@@ -424,7 +441,20 @@ intensity. Keyframe/transition it like any clip. Put it on V2/V3 above the
|
|
|
424
441
|
footage. Example: 0.3 s impact shake over everything =
|
|
425
442
|
`{kind:"adjust", track:"V3", duration:0.3, props:{shake:18}}`.
|
|
426
443
|
|
|
427
|
-
**
|
|
444
|
+
**Masks** (`masks`, video/image/svg/text/adjust): shapes that cut what the clip
|
|
445
|
+
shows — rect, ellipse, bezier, free-hand; feather, expand, invert, add /
|
|
446
|
+
subtract / intersect, keyframes. See "Masks" below.
|
|
447
|
+
|
|
448
|
+
**Shape layers** (`kind:"shape"`): `props.shapes` (rectangles, ellipses,
|
|
449
|
+
polygons, stars, lines, bezier paths, groups) and `trimStart` / `trimEnd` /
|
|
450
|
+
`trimOffset` (%, a draw-on of every path). **Null objects** (`kind:"null"`)
|
|
451
|
+
never render; other clips are parented to them. See "Shapes, nulls and parenting".
|
|
452
|
+
|
|
453
|
+
**Comp clips** (`kind:"comp"`, `compId`): a composition placed as one clip — it
|
|
454
|
+
draws like a picture of the comp's size (`fit` defaults to `none`) and its media
|
|
455
|
+
plays and exports. See "Compositions (nested sequences)".
|
|
456
|
+
|
|
457
|
+
**Animatable props** (usable in `keyframes`): x, y, scale, rotation, opacity, anchorX, anchorY, trimStart, trimEnd, trimOffset, pathOffset,
|
|
428
458
|
volume, pan, duck, speed, brightness, contrast, saturation, hue, blur, grayscale, sepia,
|
|
429
459
|
invert, temperature, tint, vignette, cornerRadius, shake, rgbSplit, grain,
|
|
430
460
|
fontSize, letterSpacing, glow.
|
|
@@ -619,12 +649,12 @@ each one's qualifier looks at the picture as it reaches that layer.
|
|
|
619
649
|
| `qualifier.hue` | `[centre°, width°, soft°]` — hues within ±width/2 of the centre, easing out over `soft`; wraps through red. Greys carry no hue, so a hue range leaves them out |
|
|
620
650
|
| `qualifier.sat` · `qualifier.luma` | `[low, high, soft]` on 0…1 — saturation (HSV) / Rec.709 luma between low and high |
|
|
621
651
|
| `qualifier.invert` | select everything else |
|
|
622
|
-
| `mask.shape` | `ellipse` · `rect` · `poly` |
|
|
652
|
+
| `mask.shape` | `ellipse` · `rect` · `poly` · `bezier` (or `freehand` with a `stroke`, fitted into a bezier — see "Masks") |
|
|
623
653
|
| `mask.x` · `mask.y` | centre, as fractions of the clip's picture (0…1, y down) |
|
|
624
654
|
| `mask.w` · `mask.h` | size, fractions of the picture's width / height (ellipse, rect) |
|
|
625
655
|
| `mask.rotation` | degrees, clockwise |
|
|
626
656
|
| `mask.feather` | soft edge, a fraction of the picture's height (0…0.5), centred on the outline |
|
|
627
|
-
| `mask.points` | poly: 3–16 `[dx, dy]` corners around `x, y` (fractions of width / height) |
|
|
657
|
+
| `mask.points` | poly: 3–16 `[dx, dy]` corners around `x, y` (fractions of width / height); bezier: 3–24 anchors `[dx, dy]` or `[dx, dy, inX, inY, outX, outY]` with curve handles |
|
|
628
658
|
| `mask.invert` | grade outside the shape |
|
|
629
659
|
| `mask.keys` | animate it: `[{t, x?, y?, w?, h?, rotation?, feather?, ease?}]`, `t` in seconds from the clip's start; each param eases between the keys that set it (`ease` on the arriving key, as for clip keyframes) |
|
|
630
660
|
|
|
@@ -639,8 +669,9 @@ curves edit that layer, and three more sections appear: the layer's name,
|
|
|
639
669
|
reorder, **Delete**; **Qualifier** — **Pick** then click a colour in the
|
|
640
670
|
monitor to select its hue, saturation and luma, then tune the ranges; and
|
|
641
671
|
**Mask** — pick a shape and drag it on the monitor (inside moves it, the
|
|
642
|
-
handles resize it, the knob rotates it, Shift snaps to 15°; on a poly
|
|
643
|
-
Ctrl-click adds a point and double-click removes one)
|
|
672
|
+
handles resize it, the knob rotates it, Shift snaps to 15°; on a poly or
|
|
673
|
+
bezier, Ctrl-click adds a point and double-click removes one), or **Draw** a
|
|
674
|
+
free-hand outline on the monitor (it becomes a bezier). **◆ Key** keys the
|
|
644
675
|
shape at the playhead; once it has keys, every change to the shape keys.
|
|
645
676
|
|
|
646
677
|
**Wheel colours.** A wheel's `r, g, b` normally carry no brightness: they are a
|
|
@@ -703,6 +734,618 @@ other shot of the scene until its median and cast land near the hero's. In the
|
|
|
703
734
|
editor, **Copy** / **Paste** on the Color tab pastes one clip's grade onto every
|
|
704
735
|
selected clip.
|
|
705
736
|
|
|
737
|
+
### Masks
|
|
738
|
+
|
|
739
|
+
A clip's masks cut what it shows: only the parts the masks keep reach the
|
|
740
|
+
frame, so a clip can be isolated, revealed or cut through, and anything under
|
|
741
|
+
it shows in the gaps. They live in `props.masks` on any video, image, svg,
|
|
742
|
+
text or adjustment clip. On an adjustment layer they limit where its effects
|
|
743
|
+
and grade apply. They run in preview and export alike. The clip draws as usual,
|
|
744
|
+
the masks cut it, and then its `opacity` and `blend` composite it.
|
|
745
|
+
|
|
746
|
+
```jsonc
|
|
747
|
+
"masks": [
|
|
748
|
+
{ "name": "Subject", "shape": "ellipse", "x": 0.52, "y": 0.45, "w": 0.4, "h": 0.7, "feather": 40 },
|
|
749
|
+
{ "name": "Hole", "shape": "rect", "mode": "subtract", "x": 0.6, "y": 0.4, "w": 0.1, "h": 0.1 },
|
|
750
|
+
{ "shape": "bezier", "x": 0.3, "y": 0.7, "expand": 8,
|
|
751
|
+
"points": [[0, -0.1], [0.12, 0.08, 0, -0.05, 0, 0.05], [-0.12, 0.08]],
|
|
752
|
+
"keys": [{ "t": 0, "x": 0.3 }, { "t": 2, "x": 0.6, "ease": "linear" }] }
|
|
753
|
+
]
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
| key | default | does |
|
|
757
|
+
|---|---|---|
|
|
758
|
+
| `shape` | — | `rect` · `ellipse` · `bezier` (write `freehand` with a `stroke` to have one drawn for you) |
|
|
759
|
+
| `x` · `y` | 0.5 | centre, as fractions of the clip's picture (0…1, y down) |
|
|
760
|
+
| `w` · `h` | 0.5 | size, fractions of the picture's width / height (rect, ellipse) |
|
|
761
|
+
| `points` | — | bezier: 3–64 anchors around `x, y`, each `[dx, dy]` (a corner) or `[dx, dy, inX, inY, outX, outY]` (with curve handles relative to the anchor), all fractions of the picture's width / height |
|
|
762
|
+
| `stroke` | — | freehand only: `[[u, v], …]` picture fractions along an outline; it is simplified and stored as an editable `bezier` |
|
|
763
|
+
| `scale` · `rotation` | 1 · 0 | about the centre; degrees clockwise |
|
|
764
|
+
| `mode` | `add` | how this mask combines with the ones before it: `add` · `subtract` · `intersect` · `difference` |
|
|
765
|
+
| `invert` | false | use everything outside the shape |
|
|
766
|
+
| `opacity` | 1 | 0…1, how strongly this mask counts |
|
|
767
|
+
| `feather` | 0 | soft edge, project pixels, centred on the outline |
|
|
768
|
+
| `expand` | 0 | grow (+) or shrink (−) the outline, project pixels |
|
|
769
|
+
| `name` · `on` | — | a label; `on:false` turns the mask off |
|
|
770
|
+
| `keys` | — | animate it: `[{t, x?, y?, w?, h?, scale?, rotation?, feather?, expand?, opacity?, points?, ease?}]`, `t` in seconds from the clip's start; each param eases between the keys that set it (`ease` on the arriving key, as for clip keyframes). `points` keys morph the outline and need the shape's point count |
|
|
771
|
+
|
|
772
|
+
Masks combine bottom-up (first in the list first). The first one starts from
|
|
773
|
+
nothing when it adds and from the whole picture otherwise, so a lone
|
|
774
|
+
`subtract` cuts a hole in a full picture. At most 8 masks per clip.
|
|
775
|
+
|
|
776
|
+
The picture a mask lives in is the clip's drawn rectangle for video, image
|
|
777
|
+
and svg (after fit and crop), the text block for text, and the whole canvas
|
|
778
|
+
for an adjustment layer. Masks follow the clip when it is moved, scaled,
|
|
779
|
+
rotated or flipped, and keep the clip's keyframes.
|
|
780
|
+
|
|
781
|
+
In the editor, the **Masks** section of the Inspector has **+ Rect**,
|
|
782
|
+
**+ Ellipse**, **Pen** (click points on the monitor, drag while placing one to
|
|
783
|
+
pull curve handles, click the first point or press Enter to close) and
|
|
784
|
+
**Draw** (drag a free-hand outline). **Matte** shows the clip's matte in the
|
|
785
|
+
monitor (white = visible). Pick a mask's chip to edit it: mode, **Invert**,
|
|
786
|
+
the sliders, ↑ ↓ to reorder, **Copy**, **Delete**, and on the monitor, drag
|
|
787
|
+
inside to move it, the edge handles to resize it and the knob to rotate it
|
|
788
|
+
(Shift snaps to 15°). On a bezier, drag a point to move it, drag its handles
|
|
789
|
+
to curve it (Alt breaks the pair), Alt-drag a point to pull fresh handles,
|
|
790
|
+
Ctrl-click the outline to add a point, and double-click a point to remove
|
|
791
|
+
it. **◆ Key** keys the whole mask at the playhead; once it has keys, every
|
|
792
|
+
change keys. Esc steps back (point, then mask).
|
|
793
|
+
|
|
794
|
+
**Agents** edit masks with patch ops:
|
|
795
|
+
|
|
796
|
+
- `{op:"setMask", id, set:{shape:"ellipse", x:0.5, y:0.45, w:0.4, h:0.7, feather:40}}`
|
|
797
|
+
adds a mask (no `mask`, an index one past the end or a new name adds one; a
|
|
798
|
+
new mask needs `shape`). `{op:"setMask", id, mask:"Subject", set:{x:0.6}}`
|
|
799
|
+
edits one, by index (0 = first) or name, merging key by key (`null` resets a
|
|
800
|
+
key). `replace:true` starts it over, keeping its name. `ids:[…]` edits several clips.
|
|
801
|
+
- A drawn outline: `set:{shape:"freehand", stroke:[[0.4, 0.3], [0.6, 0.32], …]}`.
|
|
802
|
+
It is stored as an editable bezier.
|
|
803
|
+
- `{op:"setMaskKeys", id, mask, keys:[{t:0, x:0.3}, {t:2, x:0.7}]}` animates
|
|
804
|
+
one (`keys:null` stops it); `{op:"removeMask", id, mask}` deletes one.
|
|
805
|
+
- `props.masks` written through `addClip` / `updateClip` is validated the same
|
|
806
|
+
way. Bad keys, shapes or values refuse the whole patch. Locks apply
|
|
807
|
+
(`force:true` as elsewhere).
|
|
808
|
+
- `fablecut_scopes {time, mask:{clip}}` measures them: the % of the clip's
|
|
809
|
+
picture its masks keep (the feathered matte, measured alone) and the % of
|
|
810
|
+
the frame the masked clip covers. The compact project view lists each
|
|
811
|
+
clip's masks as `[2 masks: ellipse feather 40, rect subtract]`.
|
|
812
|
+
|
|
813
|
+
Recipes: **spotlight** — a feathered ellipse on the clip; **split screen** —
|
|
814
|
+
two clips on V1 / V2, a `rect` on the top one covering its half; **cut a
|
|
815
|
+
logo out of the background** — a bezier around it, `feather` 2–4;
|
|
816
|
+
**reveal** — a rect whose `w` (or `x`) keys from 0 to full; **effects only
|
|
817
|
+
on the face** — an adjustment layer with a feathered ellipse, keyed to follow
|
|
818
|
+
it; **vignette through an adjustment layer** — an inverted ellipse with
|
|
819
|
+
`feather:200` and the layer's `brightness` down.
|
|
820
|
+
|
|
821
|
+
### Track mattes and blending
|
|
822
|
+
|
|
823
|
+
A **track matte** cuts a clip with the picture on another track: where that
|
|
824
|
+
picture is opaque (alpha) or bright (luma), the clip shows. Set it on the
|
|
825
|
+
clip being cut, the fill:
|
|
826
|
+
|
|
827
|
+
| prop | does |
|
|
828
|
+
|---|---|
|
|
829
|
+
| `matte` | `alpha` · `alpha-inverted` · `luma` · `luma-inverted`. Alpha keeps the clip where the matte's pixels are opaque, luma where they are bright (white = shows, black = gone, Rec. 709 weights). The inverted modes keep the opposite |
|
|
830
|
+
| `matteTrack` | the video track the matte comes from. Default: the video track directly above the clip (V2 for a clip on V1) |
|
|
831
|
+
|
|
832
|
+
The matte track does not draw into the frame while a clip on screen uses it;
|
|
833
|
+
it only cuts. Its clips keep their own transform, keyframes, opacity, masks
|
|
834
|
+
and effects, so the matte can move, scale and animate on its own. When the
|
|
835
|
+
matte track has nothing at that moment, `alpha` / `luma` show nothing of the
|
|
836
|
+
clip and the inverted modes show all of it. Track mattes and masks combine:
|
|
837
|
+
the matte cuts the clip, then its masks cut what is left, then `opacity` and
|
|
838
|
+
`blend` composite it. A clip used as a matte is drawn without its own track
|
|
839
|
+
matte. Audio clips take neither.
|
|
840
|
+
|
|
841
|
+
`blend` is how a clip lays over what is under it: `normal` · `multiply` ·
|
|
842
|
+
`screen` · `overlay` · `darken` · `lighten` · `color-dodge` · `color-burn` ·
|
|
843
|
+
`hard-light` · `soft-light` · `difference` · `exclusion` · `hue` ·
|
|
844
|
+
`saturation` · `color` · `luminosity` · `lighter` (add). Blending happens after
|
|
845
|
+
masks and mattes, so a masked clip only blends where it is kept.
|
|
846
|
+
|
|
847
|
+
In the editor, **Track matte** and **Matte track** sit under **Blend** in the
|
|
848
|
+
Inspector's Transform section (the Adjustment layer section for an adjustment
|
|
849
|
+
layer). The timeline marks a clip with a track matte ◐ and shows the clips
|
|
850
|
+
that serve as its matte hatched, labelled **Matte**.
|
|
851
|
+
|
|
852
|
+
**Agents:** `{op:"setMatte", id, matte:"luma"}` (or `ids:[…]`; `track:"V3"`
|
|
853
|
+
picks the matte track, `track:null` goes back to the one above;
|
|
854
|
+
`matte:null` removes it). `blend`, `matte` and `matteTrack` written through
|
|
855
|
+
`addClip` / `updateClip` are validated too. `fablecut_scopes` lists each clip
|
|
856
|
+
on screen with its `matte` (e.g. `luma from V2`), `blend`, and whether it is a
|
|
857
|
+
matte itself.
|
|
858
|
+
|
|
859
|
+
Recipes: **text filled with video** — the text on V2, the video on V1 with
|
|
860
|
+
`matte:"alpha"`; **a shape reveal** — an svg or a masked solid on V2 whose
|
|
861
|
+
scale keys up, the shot on V1 with `matte:"alpha"`; **light leaks / film
|
|
862
|
+
burn** on V2 with `blend:"screen"`; **a grade only in the bright areas** — an
|
|
863
|
+
adjustment layer with `matte:"luma"` over a copy of the shot;
|
|
864
|
+
**a cut-out through a texture** — `luma-inverted` against a black-and-white
|
|
865
|
+
texture.
|
|
866
|
+
|
|
867
|
+
### Tracking
|
|
868
|
+
|
|
869
|
+
Motion tracking follows something in a video clip frame by frame: a corner, a
|
|
870
|
+
logo, a face, a sign. The result is a **track** stored on the clip, and masks
|
|
871
|
+
and other clips **follow** it by having it written into their keys. Rendering
|
|
872
|
+
never reads a track, so a followed mask or title renders and exports exactly
|
|
873
|
+
like a hand-keyed one, and you can edit its keys afterwards.
|
|
874
|
+
|
|
875
|
+
```jsonc
|
|
876
|
+
"tracks": [
|
|
877
|
+
{ "name": "Logo", "kind": "point", "w": 0.08, "h": 0.14,
|
|
878
|
+
"samples": [[0, 0.31, 0.42, 1, 0, 1], [0.0333, 0.312, 0.421, 1, 0, 0.98]] }
|
|
879
|
+
]
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
| key | does |
|
|
883
|
+
|---|---|
|
|
884
|
+
| `name` | how masks, clips and agents refer to it (default `Track n`, or the mask's name) |
|
|
885
|
+
| `kind` | `point` (position) or `box` (position, size and rotation were followed) |
|
|
886
|
+
| `w` · `h` | the region that was matched, fractions of the clip's picture |
|
|
887
|
+
| `samples` | one per frame: `[t, x, y, s, r, q]`. `t` is seconds from the clip's start, `x, y` is the centre (fractions of the picture, like masks), `s` is the scale and `r` the rotation (°) relative to the first sample, `q` is the match quality 0…1 |
|
|
888
|
+
|
|
889
|
+
The tracker matches a small greyscale template of the region (normalized
|
|
890
|
+
cross-correlation, sub-pixel, optionally over scale and rotation) and keeps
|
|
891
|
+
the template fresh while anchoring it to the first frame so it does not
|
|
892
|
+
drift. It stops where the match quality drops below 0.45 (the region was
|
|
893
|
+
covered, left the frame or changed too much) and keeps what it tracked up to
|
|
894
|
+
there. Pick regions with detail: corners, text, edges. Flat sky or a blank
|
|
895
|
+
wall can't be tracked. A track without scale follows position only: if the
|
|
896
|
+
thing grows, shrinks or turns noticeably, turn on Scale / Rotate (`scale` /
|
|
897
|
+
`rotation`), or the match fades and the track stops early. Tracks are stored in clip time, so they move with the clip and follow
|
|
898
|
+
trims and splits (re-track after changing its speed).
|
|
899
|
+
|
|
900
|
+
In the editor:
|
|
901
|
+
|
|
902
|
+
- **Mask tracking.** Select a mask on a video clip. Under its mode row,
|
|
903
|
+
**◀ Back** and **On ▶** follow what the mask covers from the playhead to
|
|
904
|
+
the clip's start or end, and key the mask's `x` / `y`. Turn on **Scale**
|
|
905
|
+
and **Rotate** to key those too. The keys are linear and simplified (a key
|
|
906
|
+
only where the motion turns, within ~1.5 px). Keys the mask had in that range lose their
|
|
907
|
+
position and keep everything else.
|
|
908
|
+
- **Point tracking.** The Inspector's **Tracking** section (video clips):
|
|
909
|
+
**+ Track point**, then click the spot on the monitor. It tracks from the
|
|
910
|
+
playhead to the clip's end. **Region** sets the matched area (% of the
|
|
911
|
+
picture's width). Pick a track's chip to see its path and region on the
|
|
912
|
+
monitor, **◀ Track back** / **Track on ▶** to extend it from the playhead,
|
|
913
|
+
and **Delete** it.
|
|
914
|
+
- **Attach a clip.** With a track picked, choose a clip in **Attach**
|
|
915
|
+
(any visual clip that overlaps it in time) and press **Attach to the track**. The clip
|
|
916
|
+
gets `x` / `y` keyframes (plus `scale` / `rotation` with those toggles on)
|
|
917
|
+
that ride on the track and keep it where it is at the playhead. Stickers,
|
|
918
|
+
titles, a blur box or a highlight then stay pinned to the moving thing.
|
|
919
|
+
- Tracking runs from a private copy of the video, so the monitor does not
|
|
920
|
+
move. Progress shows in the Inspector, and **Esc** stops it and keeps what
|
|
921
|
+
it has. Each finished track is one undo step.
|
|
922
|
+
|
|
923
|
+
**Agents:** `fablecut_track` runs the tracker in the editor (an open tab, else
|
|
924
|
+
headless Chrome / Edge) and saves the result:
|
|
925
|
+
|
|
926
|
+
- `{clip, point:{x:0.62, y:0.4}, at:12}` follows a spot from `at` to the
|
|
927
|
+
clip's end. `region:{x, y, w, h}` gives the area instead, `size` sets a
|
|
928
|
+
point's region (default 0.08 of the width), `direction:"backward"|"both"`
|
|
929
|
+
and `to` (timeline seconds) set the range, and `scale` / `rotation` follow
|
|
930
|
+
those too.
|
|
931
|
+
- `apply:{mask:"Face"}` bakes the result into that mask's keys. Leave out
|
|
932
|
+
`point` / `region` and the mask's own outline is what gets followed.
|
|
933
|
+
- `apply:{clip:"c_sticker", scale?, rotation?, at?}` pins another clip to
|
|
934
|
+
the track.
|
|
935
|
+
- `name` names the track. Passing an existing name with `apply` and no
|
|
936
|
+
region re-applies the stored track without tracking again.
|
|
937
|
+
- The reply gives the range, the match quality (min / mean; ≥ 0.8 is solid),
|
|
938
|
+
where it was lost, and what was keyed.
|
|
939
|
+
- `{op:"removeTrack", id, track}` deletes a stored track; what followed it
|
|
940
|
+
keeps its keys. The compact project view lists tracks as
|
|
941
|
+
`[tracks: "Logo" point 0.00–4.20 s (127 samples)]`, never their samples.
|
|
942
|
+
|
|
943
|
+
Recipes: **a title that follows a car** — track a point on the car, attach
|
|
944
|
+
the title; **blur a face or a plate** — an ellipse mask on a blurred copy of
|
|
945
|
+
the shot on the track above, then `apply:{mask:"Face"}`; **a screen replacement** —
|
|
946
|
+
track the screen with Scale and Rotate on and attach the new picture, cropped
|
|
947
|
+
to fit; **a highlight that stays on a UI element** in a screen recording —
|
|
948
|
+
track the button, attach a rect svg.
|
|
949
|
+
|
|
950
|
+
### Shapes, nulls and parenting
|
|
951
|
+
|
|
952
|
+
Motion graphics are built from three pieces: **shape layers** (vector shapes
|
|
953
|
+
drawn by the editor, crisp at any size), **null objects** (invisible layers that
|
|
954
|
+
carry a transform) and **parenting** (a clip moves, turns and scales with
|
|
955
|
+
another clip). Every visual clip also has an **anchor point**. All of it runs
|
|
956
|
+
the same in preview and export.
|
|
957
|
+
|
|
958
|
+
**Shape layers.** A clip with `kind:"shape"` (no `mediaId`) draws
|
|
959
|
+
`props.shapes`, a list drawn bottom-up (first = bottom):
|
|
960
|
+
|
|
961
|
+
```jsonc
|
|
962
|
+
{ "id": "c_lt", "kind": "shape", "mediaId": null, "track": "V2", "start": 0, "duration": 5,
|
|
963
|
+
"props": { "x": -300, "y": 220, "shapes": [
|
|
964
|
+
{ "type": "rect", "name": "Bar", "w": 520, "h": 90, "radius": 12, "fill": "#ffd166",
|
|
965
|
+
"keys": [ { "t": 0, "w": 0 }, { "t": 0.6, "w": 520, "ease": "ease-out" } ] },
|
|
966
|
+
{ "type": "line", "y": 60, "points": [[-260, 0], [260, 0]], "stroke": "#ffffff", "strokeWidth": 6,
|
|
967
|
+
"keys": [ { "t": 0.3, "trimEnd": 0 }, { "t": 1.1, "trimEnd": 100 } ] },
|
|
968
|
+
{ "type": "group", "x": 300, "items": [
|
|
969
|
+
{ "type": "star", "sides": 5, "inner": 0.45, "w": 80, "h": 80, "fill": "#ff7ac8" },
|
|
970
|
+
{ "type": "ellipse", "w": 120, "h": 120, "fill": "none", "stroke": "#ff7ac8", "strokeWidth": 4 } ],
|
|
971
|
+
"keys": [ { "t": 0, "rotation": 0 }, { "t": 5, "rotation": 360, "ease": "linear" } ] }
|
|
972
|
+
] } }
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
| key | default | does |
|
|
976
|
+
|---|---|---|
|
|
977
|
+
| `type` | `rect` | `rect` · `ellipse` · `polygon` · `star` · `line` · `path` · `group` |
|
|
978
|
+
| `x` · `y` | 0 | position in layer px, from the layer's origin (the clip's `x`/`y`, y down) |
|
|
979
|
+
| `rotation` · `scale` · `opacity` | 0 · 1 · 1 | about the item's own `x, y` |
|
|
980
|
+
| `w` · `h` | 200 | size, px (rect, ellipse, polygon, star) |
|
|
981
|
+
| `radius` | 0 | rect corner radius, px |
|
|
982
|
+
| `sides` · `inner` | 6 / 5 · 0.5 | polygon / star point count (3–64); a star's inner radius as a fraction of the outer |
|
|
983
|
+
| `points` | — | line: exactly 2 · path: 2–128 points `[x, y]` or `[x, y, inX, inY, outX, outY]` (curve handles relative to the point), px around the item's `x, y` |
|
|
984
|
+
| `closed` | true | path only; `false` leaves it open (lines are always open) |
|
|
985
|
+
| `fill` · `stroke` | `#ffffff` · `none` | `"#rrggbb"`, `"#rrggbbaa"` or `"none"`; lines and open paths default to a white stroke and no fill |
|
|
986
|
+
| `strokeWidth` | 6 (8 open) | px; set with `stroke` |
|
|
987
|
+
| `lineCap` · `lineJoin` | `round` · `round` | `butt` · `round` · `square` / `miter` · `round` · `bevel` |
|
|
988
|
+
| `dash` | — | `[on, off, …]` px, e.g. `[24, 12]` |
|
|
989
|
+
| `trimStart` · `trimEnd` | 0 · 100 | the part of the outline drawn, % — key `trimEnd` 0→100 to draw a path on |
|
|
990
|
+
| `trimOffset` | 0 | % of the outline the drawn part is shifted round by (key it to make a dash run round a loop) |
|
|
991
|
+
| `items` | — | group only: its items. A group moves, turns, scales, fades and trims them together (groups nest 4 deep) |
|
|
992
|
+
| `name` · `on` | — | a label (agents address items by it); `on:false` hides it |
|
|
993
|
+
| `keys` | — | `[{t, ease?, …}]`, `t` in seconds from the clip's start. Keyable: `x y rotation scale opacity w h radius inner strokeWidth trimStart trimEnd trimOffset fill stroke points`. Each value eases between the keys that set it (`ease` on the arriving key); colours blend; `points` keys morph the path and need its point count |
|
|
994
|
+
|
|
995
|
+
At most 64 items per layer (group children included). The layer itself is a
|
|
996
|
+
normal visual clip: its `x`, `y`, `scale`, `rotation`, `opacity`, `blend`,
|
|
997
|
+
masks, transitions and keyframes all work, as do `blur` and `shake`. Its own
|
|
998
|
+
`trimStart` / `trimEnd` / `trimOffset` (clip props, keyframable) trim every
|
|
999
|
+
path in it at once. A trimmed shape fills the part that is drawn.
|
|
1000
|
+
|
|
1001
|
+
**Null objects.** `kind:"null"` (no `mediaId`) is a clip that never renders.
|
|
1002
|
+
Give it `x`, `y`, `rotation`, `scale` (and keyframes), and parent clips to it to
|
|
1003
|
+
move them as one. The monitor shows it as a red square while it is on screen.
|
|
1004
|
+
|
|
1005
|
+
**Parenting.** `"parent": "<clip id>"` (top-level, next to `track`) makes a
|
|
1006
|
+
clip move, turn and scale with that clip. Its own `x`/`y` are then measured
|
|
1007
|
+
in the parent's space: from the parent's centre, turned and scaled with it.
|
|
1008
|
+
Parents chain (a parent can have a parent), and loops are refused. Only the
|
|
1009
|
+
transform follows: the parent's opacity, transitions, shake and effects do
|
|
1010
|
+
not, and the parent's keyframes are read even outside its time range (held
|
|
1011
|
+
at its first or last value). Splitting a parent leaves the children on the
|
|
1012
|
+
left half.
|
|
1013
|
+
|
|
1014
|
+
**Anchor point.** `anchorX` / `anchorY` (px from the picture's centre, for any
|
|
1015
|
+
visual clip) pick the point that `x`/`y` place and that `rotation` and `scale`
|
|
1016
|
+
turn around: `anchorY: 200` on a 400 px tall shape spins it about its bottom
|
|
1017
|
+
edge. Moving the anchor without moving the picture means changing `x`/`y`
|
|
1018
|
+
with it (the editor's anchor tool does that).
|
|
1019
|
+
|
|
1020
|
+
In the editor: the bin's **Shapes** tab adds a rectangle, ellipse, polygon,
|
|
1021
|
+
star, line or pen-path layer, or a null object, at the playhead. The **Shape layer** section lists the
|
|
1022
|
+
items; pick one to edit it — the sliders, Fill / Stroke (colour or **None**),
|
|
1023
|
+
ends and joins, trim, **Group** / **Ungroup** / **Out**, ↑ ↓, **Copy**,
|
|
1024
|
+
**Delete** — and on the monitor drag it to move, the knob to turn it (Shift:
|
|
1025
|
+
15°) and the corners to resize it. On a line or path, drag points and their
|
|
1026
|
+
handles (Alt breaks the pair, Alt-drag pulls new handles); on a path,
|
|
1027
|
+
Ctrl-click the outline to add a point and double-click one to remove it.
|
|
1028
|
+
**Pen** draws a new path: click points (drag to curve), click the first point
|
|
1029
|
+
to close it or press Enter to keep it open. Each value has its own **◆**: key
|
|
1030
|
+
it at the playhead, and from then on changing it keys it. Every visual clip's
|
|
1031
|
+
Transform section has **Anchor** (**✥ Move**: drag on the monitor to move the
|
|
1032
|
+
anchor point while the picture stays put, Shift snaps to the centre;
|
|
1033
|
+
**Centre** puts it back) and **Parent**. Right-click clips → **Parent to a
|
|
1034
|
+
new null** groups them under a fresh null at their centre.
|
|
1035
|
+
|
|
1036
|
+
**Agents:**
|
|
1037
|
+
|
|
1038
|
+
- Add a layer with `addClip {kind:"shape", track, start, duration, props:{shapes:[…]}}`,
|
|
1039
|
+
a null with `addClip {kind:"null", track, start, duration, props:{x, y}}`.
|
|
1040
|
+
- `{op:"setShape", id, set:{type:"rect", w:400, h:80, fill:"#ffd166"}}` adds an
|
|
1041
|
+
item (no `shape`, an index one past the end or a new name adds one; a new
|
|
1042
|
+
item needs `type`). `{op:"setShape", id, shape:"Bar", set:{fill:"#22d3ee"}}`
|
|
1043
|
+
edits one — by index (0 = first), name, or a path into groups like `"2.0"`
|
|
1044
|
+
(item 0 of group 2; `"2.3"` with 3 items adds one there) — merging key by
|
|
1045
|
+
key (`null` resets a key; a new `type` keeps the position and paint).
|
|
1046
|
+
`replace:true` starts it over. `keys` in `set` replaces its keys.
|
|
1047
|
+
`{op:"removeShape", id, shape}` deletes one. `ids:[…]` edits several clips.
|
|
1048
|
+
- `{op:"setParent", id | ids, parent:"c_null"}` parents clips and keeps them
|
|
1049
|
+
where they are on screen (at time `at`, default each clip's start; keyed
|
|
1050
|
+
channels are left as they are). `keep:false` keeps the numbers instead.
|
|
1051
|
+
`parent:null` unparents. Removing a parent clip unparents its children
|
|
1052
|
+
where they stand.
|
|
1053
|
+
- `props.shapes` and `parent` written through `addClip` / `updateClip` are
|
|
1054
|
+
validated the same way; bad input refuses the whole patch. The compact
|
|
1055
|
+
view lists a layer as `[3 shapes: rect #ffd166 2 keys, line stroke #ffffff/6, group(star(5) #ff7ac8)]`
|
|
1056
|
+
and a child as `parent:c_null`.
|
|
1057
|
+
|
|
1058
|
+
Recipes: **draw-on logo** — the logo as `path` items with a stroke and no fill,
|
|
1059
|
+
`trimEnd` keyed 0→100 (stagger the items' keys), then `fill` keyed from
|
|
1060
|
+
`"#ffffff00"` to `"#ffffff"`; **lower third** — a `rect` bar whose `w` keys
|
|
1061
|
+
from 0, a `line` drawn on under it, a title on the track above parented to the
|
|
1062
|
+
shape layer so they slide out together; **loading ring** — an ellipse with a
|
|
1063
|
+
stroke, `trimEnd: 25`, `trimOffset` keyed 0→100 with `ease:"linear"`;
|
|
1064
|
+
**orbit** — a null at the centre with `rotation` keyed 0→360, a shape parented
|
|
1065
|
+
to it with `x: 300`; **swing from the top** — `anchorY` at the top edge
|
|
1066
|
+
(minus half the height) and `rotation` keyed −10↔10; **burst** — a group of
|
|
1067
|
+
lines at different `rotation`s with `scale` keyed 0→1 and `opacity` 1→0.
|
|
1068
|
+
|
|
1069
|
+
### Motion paths, motion blur and text on a path
|
|
1070
|
+
|
|
1071
|
+
**Motion paths.** When `keyframes.x` and `keyframes.y` both have a key at the
|
|
1072
|
+
same time, that is a **position key**. Give position keys curve handles and
|
|
1073
|
+
the clip travels along a bezier path between them instead of a straight line.
|
|
1074
|
+
The handles are `pin` (arriving) and `pout` (leaving) on the x and y keys,
|
|
1075
|
+
in px relative to the key's position, in the clip's `x`/`y` space:
|
|
1076
|
+
|
|
1077
|
+
```jsonc
|
|
1078
|
+
"keyframes": {
|
|
1079
|
+
"x": [ { "t": 0, "v": -450, "pout": 250 }, { "t": 2, "v": 0, "pin": -250, "pout": 250 }, { "t": 4, "v": 450, "pin": -250 } ],
|
|
1080
|
+
"y": [ { "t": 0, "v": 150, "pout": -300 }, { "t": 2, "v": 0 }, { "t": 4, "v": 150, "pin": -300 } ]
|
|
1081
|
+
}
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
Between two position keys the clip moves along the curve at the pace of the
|
|
1085
|
+
arriving key's `ease`, measured along the curve, so a `linear` key gives an
|
|
1086
|
+
even speed around a bend. A segment with an extra x or y key inside it, or
|
|
1087
|
+
with no handles, runs the channels as before. `autoOrient: true` turns the
|
|
1088
|
+
clip to face its direction of travel, and its `rotation` adds on top. A
|
|
1089
|
+
parent on a curved path carries its children along it.
|
|
1090
|
+
|
|
1091
|
+
**Motion blur.** `motionBlur` is a shutter angle: the clip is drawn at
|
|
1092
|
+
`motionBlurSamples` moments across `motionBlur / 360` of a frame, centred on
|
|
1093
|
+
the frame, and the samples are averaged. 180 is the film look; 360 smears a
|
|
1094
|
+
whole frame. It blurs whatever changes: position, scale, rotation, a parent's
|
|
1095
|
+
motion, keyed shapes and trims. A clip that does not move draws once. During
|
|
1096
|
+
playback the preview uses as many samples as fit in the frame time (all of them
|
|
1097
|
+
on a fast machine); a paused frame and export always use them all. It works on
|
|
1098
|
+
video, image, svg, text and shape clips.
|
|
1099
|
+
|
|
1100
|
+
**Text on a path.** `textPath` on a text clip is one shape item (`path`, `line`,
|
|
1101
|
+
`ellipse`, `rect`, `polygon` or `star`, with the same geometry keys and
|
|
1102
|
+
`keys` as in "Shapes, nulls and parenting", but no paint or trim) whose
|
|
1103
|
+
outline is the baseline, in the title's own px. The first line of text runs
|
|
1104
|
+
along it, each letter turned with the path; each further line (`\n`) runs on a
|
|
1105
|
+
parallel curve one line-height below, so its letters space by that curve's own
|
|
1106
|
+
length. `pathOffset` (% of the path's length, keyframable) is where the text
|
|
1107
|
+
sits, and `pathAlign` says which end of each line sits there. On a closed path
|
|
1108
|
+
the text wraps around; keying `pathOffset` 0 → 100 sends it once around a
|
|
1109
|
+
circle. Every text animation plays along the path. Font, size, colour,
|
|
1110
|
+
gradient, stroke, glow, shadow, `lineHeight` and `letterSpacing` all apply;
|
|
1111
|
+
text boxes and `align` do not (`pathAlign` takes their place).
|
|
1112
|
+
|
|
1113
|
+
```jsonc
|
|
1114
|
+
{ "kind": "text", "props": { "text": "AROUND AND AROUND • ", "font": "Anton", "fontSize": 40, "letterSpacing": 4,
|
|
1115
|
+
"textPath": { "type": "ellipse", "w": 300, "h": 300 }, "pathAlign": "start" },
|
|
1116
|
+
"keyframes": { "pathOffset": [ { "t": 0, "v": 0 }, { "t": 6, "v": 100, "ease": "linear" } ] } }
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
In the editor: when the selected clip has two or more position keys, its path
|
|
1120
|
+
shows on the monitor: the curve, a dot per frame (their spacing is the speed),
|
|
1121
|
+
a square per key and the keys' handles. Drag a key to move it, drag a handle to
|
|
1122
|
+
bend the path (Alt breaks the pair), Alt-drag a key to pull new handles, and
|
|
1123
|
+
double-click a key to make it a corner again. Moving the clip on the monitor
|
|
1124
|
+
or typing X / Y keys both channels together. The Inspector's **Motion**
|
|
1125
|
+
section has **Smooth** (handles through every position key), **Straight**,
|
|
1126
|
+
**Auto-orient**, **Motion blur** and **Blur samples**. A title's **Text path**
|
|
1127
|
+
section sets it on a **Line**, **Arc**, **Circle**, **Wave** or a **Pen** path
|
|
1128
|
+
(click points, drag to curve, Enter to finish, click the first point to close
|
|
1129
|
+
it). **Edit path** shows the path's points on the monitor, and **Offset** and
|
|
1130
|
+
**Align** place the text along it.
|
|
1131
|
+
|
|
1132
|
+
**Agents:**
|
|
1133
|
+
|
|
1134
|
+
- Write `pin` / `pout` on x / y keys directly, or
|
|
1135
|
+
`{op:"setMotionPath", id | ids, path:"smooth" | "straight", autoOrient?: true}`.
|
|
1136
|
+
`"smooth"` needs the position keys first (x and y keyed together at two or more times).
|
|
1137
|
+
- Motion blur: `updateClip` with `props:{motionBlur:180, motionBlurSamples:12}`.
|
|
1138
|
+
- `{op:"setTextPath", id | ids, path:"line" | "arc" | "circle" | "wave" | {type:"path", points:[…], closed:false, …} | null, offset?, align?}`
|
|
1139
|
+
sets, replaces or removes (`null`) a title's path; animate `pathOffset` with
|
|
1140
|
+
keyframes.
|
|
1141
|
+
- These are validated on `addClip` / `updateClip` too. The compact view shows
|
|
1142
|
+
`path:curved` after a clip's keyframes, and a title's `"textPath":"ellipse 300×300"`.
|
|
1143
|
+
|
|
1144
|
+
Recipes: **fly-in on a curve** — two position keys, `setMotionPath` smooth,
|
|
1145
|
+
`autoOrient:true`, `motionBlur:180`; **orbiting label** — circle `textPath`,
|
|
1146
|
+
`pathOffset` keyed 0 → 100 linear; **stamp along a road** — an open `path`
|
|
1147
|
+
textPath that follows the shape in the footage, `pathAlign:"center"`,
|
|
1148
|
+
`textAnim:"letter-pop"`; **whip without the transition** — a fast `x` move over
|
|
1149
|
+
4–6 frames with `motionBlur:360`.
|
|
1150
|
+
|
|
1151
|
+
### Compositions (nested sequences)
|
|
1152
|
+
|
|
1153
|
+
A **composition** (comp) is a timeline of its own, with its own size, length,
|
|
1154
|
+
tracks and clips, that is placed on the main timeline (or inside another comp)
|
|
1155
|
+
as one clip. Build a lower third, a logo sting or a whole scene once, then use
|
|
1156
|
+
it as many times as you like: every clip that shows the comp changes when the
|
|
1157
|
+
comp is edited. Comps live in `project.comps`:
|
|
1158
|
+
|
|
1159
|
+
```jsonc
|
|
1160
|
+
"comps": [
|
|
1161
|
+
{ "id": "lt", "name": "Lower third", "width": 1280, "height": 720, "duration": 5,
|
|
1162
|
+
"background": "#00000000", // optional; leave it out for transparent
|
|
1163
|
+
"tracks": [ { "id": "V2", "kind": "video" }, { "id": "V1", "kind": "video" }, { "id": "A1", "kind": "audio" } ],
|
|
1164
|
+
"clips": [
|
|
1165
|
+
{ "id": "bar", "kind": "shape", "track": "V1", "start": 0, "duration": 5, "props": { "shapes": [ … ] } },
|
|
1166
|
+
{ "id": "name", "kind": "text", "track": "V2", "start": 0.3, "duration": 4.7, "props": { "text": "Ada Lovelace" } },
|
|
1167
|
+
{ "id": "whoosh", "kind": "audio", "mediaId": "m_whoosh", "track": "A1", "start": 0, "in": 0, "duration": 0.6 }
|
|
1168
|
+
] }
|
|
1169
|
+
],
|
|
1170
|
+
"clips": [
|
|
1171
|
+
{ "id": "c_lt1", "kind": "comp", "compId": "lt", "mediaId": null, "track": "V3", "start": 4, "in": 0, "duration": 5,
|
|
1172
|
+
"props": { "fit": "none", "y": 220 } }
|
|
1173
|
+
]
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
| key | default | does |
|
|
1177
|
+
|---|---|---|
|
|
1178
|
+
| `id` · `name` | — · the id | how clips and agents refer to it · the label shown on its clips and in the bin |
|
|
1179
|
+
| `width` · `height` | the project's | the comp's frame, px (16…8192) |
|
|
1180
|
+
| `duration` | — | seconds; what the comp shows past its clips is transparent (or its `background`) |
|
|
1181
|
+
| `background` | none | a colour behind the comp's clips; leave it out for transparent |
|
|
1182
|
+
| `tracks` | V3…V1, A1, A2 | the comp's own lanes, like the project's `tracks` |
|
|
1183
|
+
| `clips` | [] | ordinary clips of any kind (comp clips too), with times inside the comp |
|
|
1184
|
+
| `template` | — | set on a placed template: `{id, hold, fields}` (see "Presets and templates") |
|
|
1185
|
+
|
|
1186
|
+
**A comp clip** (`kind:"comp"`, `compId`, no `mediaId`) shows the comp from its
|
|
1187
|
+
`in` (comp seconds) for its `duration`, at speed 1. It draws like a picture of
|
|
1188
|
+
the comp's size: `fit` defaults to `none` (the comp's own pixels), and its
|
|
1189
|
+
transform, keyframes, opacity, transitions, masks, track matte, blend, grade and
|
|
1190
|
+
filters all apply to the comp's frame as a whole. Trims and splits move its
|
|
1191
|
+
`in` like a video clip's; the window can't run past the comp's length.
|
|
1192
|
+
|
|
1193
|
+
The media inside a comp **plays and exports as if it were on the timeline**:
|
|
1194
|
+
its video frames, and its audio, which goes through the main timeline's audio
|
|
1195
|
+
track of the same name (its fader, effects and bus). A comp clip that is
|
|
1196
|
+
disabled, or sits on a disabled track, silences the comp's audio too. Comps
|
|
1197
|
+
nest up to 4 deep, and a comp can never contain itself.
|
|
1198
|
+
|
|
1199
|
+
In the editor: the bin's **Project** tab lists the comps; drag one onto the
|
|
1200
|
+
timeline to place it, and double-click it (or a comp clip, or **Open** in the
|
|
1201
|
+
Inspector's **Composition** section) to open it. An open comp takes over the
|
|
1202
|
+
timeline: every tool, shortcut and inspector works on its clips, and the
|
|
1203
|
+
breadcrumb above the timeline (or **Esc**, once nothing is selected) goes back
|
|
1204
|
+
out. Right-click clips → **Precompose** moves them into a new comp and puts a
|
|
1205
|
+
comp clip in their place; right-click the Project tab → **New composition**
|
|
1206
|
+
starts an empty one. The Composition section also sets the comp's name, size,
|
|
1207
|
+
length and background.
|
|
1208
|
+
|
|
1209
|
+
**Agents:**
|
|
1210
|
+
|
|
1211
|
+
- `{op:"addComp", comp:{id?, name, duration, width?, height?, background?, tracks?, clips?:[…]}}`
|
|
1212
|
+
adds one (its clips go through the same checks as `addClip`).
|
|
1213
|
+
`{op:"updateComp", id, set:{name?, width?, height?, duration?, background?, tracks?}}`
|
|
1214
|
+
changes it; `{op:"removeComp", id}` is refused while a clip still uses it.
|
|
1215
|
+
- Place it with `addClip {kind:"comp", compId, track, start, duration, in?}`.
|
|
1216
|
+
- `{op:"inComp", comp, ops:[…]}` runs clip and timeline ops on the comp's own
|
|
1217
|
+
clips: `addClip`, `updateClip`, `removeClip`, `split`, `rippleDelete`, the
|
|
1218
|
+
trims, `setShape`, `setMask`, `setGrade`, `setParent`, `setMotionPath`,
|
|
1219
|
+
`setTextPath`, clip `setFx`, `precompose`, and `inComp` for a comp inside it.
|
|
1220
|
+
- `{op:"precompose", ids:[…], name?, id?}` moves those clips (here, or in the
|
|
1221
|
+
comp an `inComp` is running in) into a new comp that starts at the earliest
|
|
1222
|
+
of them; a comp clip of the same length takes their place.
|
|
1223
|
+
- The compact view shows a comp clip as `comp(lt in:1.5)` and lists every comp
|
|
1224
|
+
under `COMPS` with its clips.
|
|
1225
|
+
|
|
1226
|
+
Recipes: **reusable lower third** — a template (next section) gives every
|
|
1227
|
+
speaker their own copy with their own name; a plain comp suits a graphic that
|
|
1228
|
+
is the same every time; **logo sting** — a draw-on logo comp at the head and tail of the edit;
|
|
1229
|
+
**picture in picture of a whole scene** — precompose the scene's clips, then
|
|
1230
|
+
scale the comp clip down and move it into a corner;
|
|
1231
|
+
**one grade for a sequence** — precompose it and grade the comp clip once.
|
|
1232
|
+
|
|
1233
|
+
### Presets and templates
|
|
1234
|
+
|
|
1235
|
+
Two ways to reuse motion: an **animation preset** keys a named animation onto a
|
|
1236
|
+
clip, and a **template** is a ready-made comp (lower third, logo animation,
|
|
1237
|
+
social layout) whose text, colours and pictures are **fields** you fill in.
|
|
1238
|
+
|
|
1239
|
+
**Presets.** A preset keys the clip's own channels, relative to where the clip
|
|
1240
|
+
already is: `scale` and `opacity` multiply the clip's value and everything else
|
|
1241
|
+
adds to it, so "Slide up" works wherever the clip sits and however big it is.
|
|
1242
|
+
Each has an anchor: **in** plays from the clip's start and settles on its
|
|
1243
|
+
values, **out** starts from them and ends at the clip's end, **at** is an
|
|
1244
|
+
emphasis at a time you pick (the playhead, or `at`), and **loop** spans the
|
|
1245
|
+
whole clip. Keys already in the window a preset covers are replaced; the rest
|
|
1246
|
+
stay, so an In, an Out and a loop on another channel live side by side.
|
|
1247
|
+
|
|
1248
|
+
| group | presets |
|
|
1249
|
+
|---|---|
|
|
1250
|
+
| In | `fade-in` · `pop-in` · `slide-up-in` · `slide-down-in` · `slide-left-in` (from the right) · `slide-right-in` (from the left) · `drop-in` · `zoom-in` · `spin-in` · `whip-in` (with motion blur) · `rise-blur` · `draw-on` (shape layers) |
|
|
1251
|
+
| Out | `fade-out` · `pop-out` · `slide-up-out` · `slide-down-out` · `slide-left-out` · `slide-right-out` · `zoom-out` · `spin-out` · `whip-out` · `sink-blur` · `draw-off` |
|
|
1252
|
+
| At (emphasis) | `pulse` · `shake` · `wobble` · `bounce` · `flash` |
|
|
1253
|
+
| Loop | `float` · `breathe` · `sway` · `spin` · `wiggle` |
|
|
1254
|
+
|
|
1255
|
+
Every In / Out / At preset is 0.35–1 s long; `length` stretches it (a clip
|
|
1256
|
+
shorter than the preset squeezes it in). A saved preset is a JSON file in
|
|
1257
|
+
`library/presets/`, shared by every project:
|
|
1258
|
+
|
|
1259
|
+
```jsonc
|
|
1260
|
+
{ "id": "fly-in", "name": "Fly in", "group": "Saved", "anchor": "in", "length": 0.6,
|
|
1261
|
+
"keys": { "x": [ { "t": 0, "v": -500 }, { "t": 0.6, "v": 0, "ease": "ease-out" } ],
|
|
1262
|
+
"opacity": [ { "t": 0, "v": 0 }, { "t": 0.3, "v": 1 } ] },
|
|
1263
|
+
"props": { "motionBlur": 180 } } // optional: motionBlur, motionBlurSamples
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
`t` is seconds into the preset (for a loop, 0…1 of the clip). Channels: `x y
|
|
1267
|
+
scale rotation opacity blur anchorX anchorY trimStart trimEnd trimOffset
|
|
1268
|
+
brightness saturation hue grayscale shake rgbSplit glow fontSize letterSpacing
|
|
1269
|
+
pathOffset`.
|
|
1270
|
+
|
|
1271
|
+
**Templates.** Placing one adds its own comp (so two lower thirds can show two
|
|
1272
|
+
names) and a comp clip showing it with `fit:"contain"`; a template made for
|
|
1273
|
+
another shape than the canvas sits inside it, letterboxed. The comp carries
|
|
1274
|
+
what makes it a template:
|
|
1275
|
+
|
|
1276
|
+
```jsonc
|
|
1277
|
+
{ "id": "comp_x1", "name": "Lower third — Bar", "width": 1920, "height": 1080, "duration": 5, "clips": [ … ],
|
|
1278
|
+
"template": { "id": "lower-third-bar", "hold": 4.5, "fields": [
|
|
1279
|
+
{ "key": "name", "label": "Name", "type": "text", "targets": [ "name:props.text" ], "value": "Ava Moreno" },
|
|
1280
|
+
{ "key": "accent", "label": "Accent", "type": "color", "targets": [ "bar:props.shapes.1.fill" ], "value": "#ffd166" } ] } }
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
| template | name | size | length | fields |
|
|
1284
|
+
|---|---|---|---|---|
|
|
1285
|
+
| `lower-third-bar` | Lower third — Bar | 1920×1080 | 5 s | `name` `title` `accent` `panel` |
|
|
1286
|
+
| `lower-third-underline` | Lower third — Underline | 1920×1080 | 5 s | `name` `title` `accent` |
|
|
1287
|
+
| `lower-third-boxes` | Lower third — Boxes | 1920×1080 | 5 s | `name` `title` `accent` `box` |
|
|
1288
|
+
| `logo-ring` | Logo — Ring reveal | 1920×1080 | 4 s | `logo` `brand` `accent` |
|
|
1289
|
+
| `logo-slam` | Logo — Slam | 1920×1080 | 3.5 s | `logo` `accent` |
|
|
1290
|
+
| `logo-lines` | Logo — Lines | 1920×1080 | 4 s | `logo` `tagline` `lines` |
|
|
1291
|
+
| `social-story` | Story — Headline (9:16) | 1080×1920 | 6 s | `headline` `subline` `button` `accent` `background` |
|
|
1292
|
+
| `social-quote` | Post — Quote (1:1) | 1080×1080 | 6 s | `quote` `author` `accent` `background` |
|
|
1293
|
+
| `social-end-card` | End card — Follow (16:9) | 1920×1080 | 6 s | `heading` `button` `accent` `background` |
|
|
1294
|
+
|
|
1295
|
+
Field types: `text` (a string), `color` (`#rgb`, `#rrggbb` or `#rrggbbaa`) and
|
|
1296
|
+
`media` (a media id of a picture: image, svg or video; the clip's kind follows
|
|
1297
|
+
it). A target is `"<clip id>:<path>"` inside the comp (`props.text`,
|
|
1298
|
+
`props.shapes.1.fill`, `mediaId`, …) or `"comp:background"`; one field can set
|
|
1299
|
+
several. The logo templates start with a placeholder,
|
|
1300
|
+
`/library/elements/logo-placeholder.svg`, until the `logo` field gets a
|
|
1301
|
+
picture. **`hold`** is when the exit starts: changing a template's length
|
|
1302
|
+
keeps everything after it at the same distance from the end, so a longer lower
|
|
1303
|
+
third just holds longer (it can't get shorter than its entrance needs).
|
|
1304
|
+
|
|
1305
|
+
A saved template (`library/templates/<id>.json`) is a template object:
|
|
1306
|
+
`{id, name, group, width, height, duration, hold, background?, tracks?, clips,
|
|
1307
|
+
media?, fields}`; `media` lists the `{id, name, kind, src}` entries its clips
|
|
1308
|
+
use (`/library/…` or `/media/…`), matched by `src` when it is placed. A saved
|
|
1309
|
+
preset or template whose name a built-in already has is saved as
|
|
1310
|
+
"<name> (saved)".
|
|
1311
|
+
|
|
1312
|
+
In the editor: the bin's **Templates** tab lists them by group (Lower thirds,
|
|
1313
|
+
Logo animations, Social, then Saved); drag one onto the timeline or click +.
|
|
1314
|
+
The Inspector's **Template** section on its clip edits the fields, its
|
|
1315
|
+
**Length**, and offers **Use W×H canvas** when the template was made for
|
|
1316
|
+
another shape. Right-click a comp in the Project tab → **Save as template**
|
|
1317
|
+
keeps it (every title's text and every picture become fields). The
|
|
1318
|
+
**Motion** section's **Presets** menus (In, Out, At playhead, Loop) put a
|
|
1319
|
+
preset on the selected clips, and **Save as preset** keeps this clip's
|
|
1320
|
+
keyframes (In: from its start to its last key; Out: from its first key to its
|
|
1321
|
+
end; Loop: the whole clip).
|
|
1322
|
+
|
|
1323
|
+
**Agents:**
|
|
1324
|
+
|
|
1325
|
+
- `{op:"applyPreset", id | ids, preset, length?, at?}` — `preset` is a name,
|
|
1326
|
+
a saved one's id, or an inline preset object; `at` is timeline seconds (for
|
|
1327
|
+
an At preset; default the clip's start). Visual clips only.
|
|
1328
|
+
- `{op:"addTemplate", template, start?, track?, duration?, fields?:{key: value}, id?, compId?, name?}`
|
|
1329
|
+
places one (default track V2, at 0, its own length).
|
|
1330
|
+
- `{op:"setTemplate", id, fields?:{key: value}, duration?}` — `id` is its comp
|
|
1331
|
+
clip or its comp; a new `duration` retimes it, and clips that showed it to
|
|
1332
|
+
its end keep doing so.
|
|
1333
|
+
- `{op:"savePreset", id, name, anchor?, from?, to?}` (`from`/`to` in clip
|
|
1334
|
+
seconds) or `{op:"savePreset", preset:{…}}`; `{op:"saveTemplate", comp,
|
|
1335
|
+
name?, fields?, hold?}` or `{op:"saveTemplate", template:{…}}`. Files are
|
|
1336
|
+
written once the patch is saved, and later ops in the same patch can use them.
|
|
1337
|
+
- `applyPreset`, `addTemplate` and `setTemplate` also run inside `inComp`.
|
|
1338
|
+
- The compact view prints a placed template's fields under its comp:
|
|
1339
|
+
`template lower-third-bar — fields (setTemplate): name="Ava Moreno" …`.
|
|
1340
|
+
|
|
1341
|
+
Recipes: **name every speaker** — `addTemplate lower-third-bar` once per
|
|
1342
|
+
speaker with `fields:{name, title}`; **branded intro** — `addTemplate logo-ring`
|
|
1343
|
+
with `fields:{logo:"<your logo's media id>", brand, accent}` at 0;
|
|
1344
|
+
**vertical teaser** — set the canvas to 1080×1920, `addTemplate social-story`;
|
|
1345
|
+
**house style** — build a comp the way you like it, `saveTemplate`, then place
|
|
1346
|
+
it in every project; **titles that arrive and leave the same way** —
|
|
1347
|
+
`applyPreset` an In and an Out on every title in one patch.
|
|
1348
|
+
|
|
706
1349
|
### Semantics
|
|
707
1350
|
|
|
708
1351
|
- Rendering order: V1 is drawn first, then V2, V3, … on top. SVG/image/text clips
|
|
@@ -852,7 +1495,10 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
|
|
|
852
1495
|
Append `?force=1` to overwrite unconditionally. Writes are atomic (tmp file +
|
|
853
1496
|
rename), so a crashed write never corrupts the file.
|
|
854
1497
|
- `GET /api/media` — list files in ./media (name, src, size)
|
|
855
|
-
- `GET /api/library?dir=sfx|elements|svg|fonts` — list library assets
|
|
1498
|
+
- `GET /api/library?dir=sfx|elements|svg|fonts|presets|templates` — list library assets
|
|
1499
|
+
- `GET /api/presets` · `GET /api/templates` — the saved presets / templates, validated (bad files skipped);
|
|
1500
|
+
`POST /api/presets {preset}` · `POST /api/templates {template}` — validate one and save it as
|
|
1501
|
+
`library/presets/<id>.json` / `library/templates/<id>.json`, returning `{id, name, file}`
|
|
856
1502
|
- `POST /api/upload?name=foo.mp4` — raw body saved into ./media, returns `{src}`.
|
|
857
1503
|
MP4/MOV/M4V uploads are auto-remuxed with `+faststart` (needs ffmpeg on PATH).
|
|
858
1504
|
Files copied straight into ./media by external tools skip this — remux big ones
|