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 +429 -5
- package/README.md +23 -2
- package/app.js +1965 -103
- package/comp.js +195 -0
- package/edit-ops.js +50 -18
- package/index.html +146 -68
- package/library/README.md +2 -0
- package/library/elements/logo-placeholder.svg +12 -0
- package/mcp-server.js +437 -17
- package/motion.js +803 -0
- package/package.json +1 -1
- package/paths.js +25 -2
- package/server.js +20 -1
- package/style.css +177 -24
- package/templates.js +576 -0
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
|
|
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
|
-
**
|
|
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
|
|
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
|
|