fablecut 1.13.0 → 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 CHANGED
@@ -177,8 +177,10 @@ Reusable assets, visible in the editor's left-panel tabs and never copied:
177
177
  | `library/elements/` | **Elements** | overlay art: alpha PNGs, light leaks, textures, stickers |
178
178
  | `library/svg/` | **SVG** | animated vector graphics **you author** (convention below) |
179
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 |
180
182
 
181
- - 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).
182
184
  - To use one in the timeline, add a media entry whose `src` is its library path,
183
185
  e.g. `{ "id":"m_x", "name":"whoosh.mp3", "kind":"audio", "src":"/library/sfx/whoosh.mp3" }`
184
186
  — then reference it from clips like any other media.
@@ -266,6 +268,8 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
266
268
  { "id": "A4", "kind": "audio" }
267
269
  ],
268
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)"
269
273
  // ^ optional — submix buses B1…B8: name, gain (dB), pan, mute, fx; tracks route in with `out`
270
274
  "media": [
271
275
  { "id": "m_abc", "name": "intro.mp4", "kind": "video", // video|audio|image|svg
@@ -278,8 +282,9 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
278
282
  "clips": [
279
283
  {
280
284
  "id": "c_xyz", // unique string
281
- "mediaId": "m_abc", // null for kind:"text" and kind:"adjust"
282
- "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)"
283
288
  "track": "V1", // Vn…V1 (video) | A1…An (audio); extras via UI +V/+A or tracks[]
284
289
  "start": 0, // timeline position, seconds
285
290
  "in": 2.5, // offset into source media, seconds (0 for image/svg/text)
@@ -289,10 +294,13 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
289
294
  "locked": true, // OPTIONAL — locked by the user: leave it alone (MCP patch refuses edits)
290
295
  "disabled": true, // OPTIONAL — stays on the timeline, hidden from preview / audio / export
291
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")
292
298
  "props": { /* all optional — see the props reference below */ },
293
299
  // OPTIONAL — keyframe animation. Times are seconds RELATIVE TO CLIP START.
294
300
  // "ease" sits on the DESTINATION keyframe of each segment:
295
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").
296
304
  "keyframes": {
297
305
  "scale": [ { "t": 0, "v": 1 }, { "t": 5, "v": 1.25, "ease": "linear" } ],
298
306
  "opacity": [ { "t": 0, "v": 0 }, { "t": 0.8, "v": 1 } ]
@@ -316,6 +324,9 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
316
324
  | `scale` | 1 | |
317
325
  | `rotation` | 0 | degrees |
318
326
  | `opacity` | 1 | 0–1 |
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 |
319
330
  | `blend` | "normal" | how the clip lays over what is under it (see Track mattes and blending) |
320
331
  | `matte` · `matteTrack` | — | a track matte (see Track mattes and blending) |
321
332
 
@@ -390,6 +401,8 @@ These are quick looks. For real grading use `props.grade` (see "Color" below).
390
401
  | `bgColor` `bgOpacity` | "#000", 0 | rounded pill behind each line |
391
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) |
392
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 |
393
406
  | `fontCutSet` | (curated) | array of font family names cycled by `font-cut`, e.g. `["Anton","Bebas Neue","Archivo Black","Oswald"]`; each is auto-loaded |
394
407
 
395
408
  **Title styles (one-tap cohesive looks).** Text clips created in the UI now
@@ -432,7 +445,16 @@ footage. Example: 0.3 s impact shake over everything =
432
445
  shows — rect, ellipse, bezier, free-hand; feather, expand, invert, add /
433
446
  subtract / intersect, keyframes. See "Masks" below.
434
447
 
435
- **Animatable props** (usable in `keyframes`): x, y, scale, rotation, opacity,
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,
436
458
  volume, pan, duck, speed, brightness, contrast, saturation, hue, blur, grayscale, sepia,
437
459
  invert, temperature, tint, vignette, cornerRadius, shake, rgbSplit, grain,
438
460
  fontSize, letterSpacing, glow.
@@ -925,6 +947,405 @@ track the screen with Scale and Rotate on and attach the new picture, cropped
925
947
  to fit; **a highlight that stays on a UI element** in a screen recording —
926
948
  track the button, attach a rect svg.
927
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
+
928
1349
  ### Semantics
929
1350
 
930
1351
  - Rendering order: V1 is drawn first, then V2, V3, … on top. SVG/image/text clips
@@ -1074,7 +1495,10 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
1074
1495
  Append `?force=1` to overwrite unconditionally. Writes are atomic (tmp file +
1075
1496
  rename), so a crashed write never corrupts the file.
1076
1497
  - `GET /api/media` — list files in ./media (name, src, size)
1077
- - `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}`
1078
1502
  - `POST /api/upload?name=foo.mp4` — raw body saved into ./media, returns `{src}`.
1079
1503
  MP4/MOV/M4V uploads are auto-remuxed with `+faststart` (needs ffmpeg on PATH).
1080
1504
  Files copied straight into ./media by external tools skip this — remux big ones
package/README.md CHANGED
@@ -194,6 +194,27 @@ same time.
194
194
  difference, each with feather, expansion, invert and opacity, all
195
195
  keyframable (outlines morph). Draw and edit them on the monitor, with a
196
196
  matte view. Agents use the `setMask` patch op and measure with `fablecut_scopes`
197
+ - **Shapes and motion graphics** — shape layers of rectangles, ellipses,
198
+ polygons, stars, lines and pen-drawn bezier paths in groups, with fill,
199
+ stroke, dashes and trim paths for draw-ons; null objects, parenting and an
200
+ anchor point on every clip; every value keyframes, edited right on the
201
+ monitor. Agents use the `setShape` and `setParent` patch ops
202
+ - **Motion paths, motion blur and text on a path** — bend a clip's travel into
203
+ a curve with handles on the monitor, turn it along the way, blur fast moves
204
+ with a shutter angle (preview and export), and set titles along lines, arcs,
205
+ circles or drawn paths. Agents use the `setMotionPath` and `setTextPath`
206
+ patch ops
207
+ - **Compositions** — nested sequences: build a lower third, a logo sting or a
208
+ scene once and place it anywhere as one clip that moves, masks, grades and
209
+ blends like a picture, while its video and audio play and export as usual.
210
+ Open one to edit it on the timeline, or precompose a selection. Agents use
211
+ the `addComp`, `inComp` and `precompose` patch ops
212
+ - **Animation presets and templates** — 33 ready-made entrances, exits,
213
+ emphases and loops that fit any clip where it sits, and templates (lower
214
+ thirds, logo animations, 9:16 / 1:1 / 16:9 social layouts) whose text,
215
+ colours and logo you fill in, each placed as its own comp. Save your own
216
+ animations and comps to the library for every project. Agents use the
217
+ `applyPreset`, `addTemplate` and `setTemplate` patch ops
197
218
  - **Motion tracking** — follow a point or a region (with its scale and
198
219
  rotation) through a video clip, then make a mask follow it or pin a title,
199
220
  sticker or any clip to it; the result is ordinary editable keys. Agents use
@@ -323,8 +344,8 @@ you choose. Leave it unset and everything stays in the repo, exactly as before.
323
344
  /plugin install fablecut@fablecut
324
345
  ```
325
346
 
326
- That registers the MCP server for you and adds two skills — `edit-video` and
327
- `remake-reel`. Your timeline and footage live in the plugin's own data
347
+ That registers the MCP server for you and adds three skills — `edit-video`,
348
+ `remake-reel` and `motion-graphics`. Your timeline and footage live in the plugin's own data
328
349
  directory, so an update never touches them. Node 18+ and (optionally) ffmpeg
329
350
  still need to be on your machine.
330
351