@bycrux/montaj-skills 0.2.0 → 0.3.1
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/contract.md +4 -2
- package/package.json +1 -1
- package/skills/overlay.md +3 -1
- package/skills/select-takes.md +2 -0
- package/skills/write-overlay.md +55 -17
package/contract.md
CHANGED
|
@@ -42,9 +42,11 @@ Never save from a stale cached body. The operator may be editing the project con
|
|
|
42
42
|
|
|
43
43
|
These verbs are sufficient for the Phase-1 domain skills:
|
|
44
44
|
|
|
45
|
-
- **select-takes** — run step `crop_spec`, run step `virtual_to_original`; read the project.
|
|
45
|
+
- **select-takes** — run step `crop_spec`, run step `virtual_to_original`; read the project; write a file (the trim spec comes back inline from `waveform_trim` and must be on disk before `crop_spec` can take it as `--input`).
|
|
46
46
|
- **overlay** — read the project; save the project (delta); load skill `write-overlay`.
|
|
47
|
-
- **write-overlay** — save the project (delta); write a file (the JSX); reference asset paths via written/read files.
|
|
47
|
+
- **write-overlay** — save the project (delta); write a file (the JSX); run step `sample_overlay` (the end-of-authoring measure pass, required for any overlay that declares `googleFonts`); reference asset paths via written/read files.
|
|
48
48
|
- **image-search** — run step `search_images`, run step `fetch_image`; write a file / read a file.
|
|
49
49
|
|
|
50
50
|
If a future domain skill needs an operation not on this list, extend this contract first — do not let a domain skill invent its own transport-specific phrasing.
|
|
51
|
+
|
|
52
|
+
**This list is a starting index, not the final word.** It has drifted from the skill bodies before: `select-takes`'s "write a file" and `write-overlay`'s `sample_overlay` were both required by their skills and missing here. Anyone deriving the operations a workflow needs must confirm against the skill body itself — a set derived from this list alone can silently omit something the skill genuinely requires.
|
package/package.json
CHANGED
package/skills/overlay.md
CHANGED
|
@@ -45,7 +45,7 @@ If the prompt says "no overlays" — write nothing. Don't add an opening hook an
|
|
|
45
45
|
- **Go large** — 96–160px is a starting point, not a ceiling. If it looks a little too big, it's probably right. Small text gets scrolled past.
|
|
46
46
|
- **No backgrounds** — avoid dark cards, frosted panels, and semi-transparent boxes unless the prompt asks for them. A text shadow (`textShadow: '0 2px 16px rgba(0,0,0,0.9)'`) is enough to ensure legibility on any footage without boxing the text in.
|
|
47
47
|
- **Covering the face is fine** — text is more important than an unobstructed view of the speaker. Don't shrink or reposition text just to avoid the face.
|
|
48
|
-
- **Match the energy of the speech** — fast, punchy delivery gets tight entrance animations (
|
|
48
|
+
- **Match the energy of the speech** — fast, punchy delivery gets tight entrance animations (about 0.13–0.2s: `Math.round(fps * 0.13)` to `Math.round(fps * 0.2)`). Slower, deliberate speech gets a smoother slide or fade (about 0.33–0.5s). Never hardcode frame counts; projects can be 24, 30 or 60fps.
|
|
49
49
|
- **Use color sparingly** — one accent color maximum. White text with a colored word or icon reads better than multi-color text.
|
|
50
50
|
- **Avoid the bottom ~350px** — that's where captions render and where platform UI lives (TikTok progress bar, Instagram controls). Keep `bottom` values above 350px, or use `top`-anchored placement instead.
|
|
51
51
|
- **Avoid the right ~200px** — TikTok and Instagram stack action buttons (like, comment, share, follow) down the right edge. Don't push text or icons into that zone.
|
|
@@ -91,6 +91,8 @@ For multiple non-overlapping overlays, add them to the same track. For simultane
|
|
|
91
91
|
|
|
92
92
|
Follow save discipline: **read the project**, merge the updated `tracks` array into the fresh state, then **save the project (delta)**.
|
|
93
93
|
|
|
94
|
+
When this is the last editorial pass before the render, the project must be `final` before the render will run — see skill `native` → "Project lifecycle — status, and the render gate".
|
|
95
|
+
|
|
94
96
|
## Rules
|
|
95
97
|
|
|
96
98
|
- **Use icons, not emojis** — `Ph.*` (Phosphor) or `FaIcon` with `FaSolid`/`FaBrands` (Font Awesome). Both are available as globals — no imports needed. Only use emojis if the prompt asks.
|
package/skills/select-takes.md
CHANGED
|
@@ -122,6 +122,8 @@ Run step `virtual_to_original` with `{"input": "spec.json", "inverse": true, "ti
|
|
|
122
122
|
|
|
123
123
|
An ordered list of `_selected.json` trim spec paths — one per selected section, in narrative order. These become the inputs to `rm_fillers`, and the keeps that survive it become the `tracks[0]` items the render engine assembles.
|
|
124
124
|
|
|
125
|
+
When this is the last editorial pass before the render, the project must be `final` before the render will run — see skill `native` → "Project lifecycle — status, and the render gate".
|
|
126
|
+
|
|
125
127
|
## What to Log
|
|
126
128
|
|
|
127
129
|
Before writing specs, log your decisions clearly:
|
package/skills/write-overlay.md
CHANGED
|
@@ -7,6 +7,8 @@ description: "Write a custom JSX overlay component and add it to the project's o
|
|
|
7
7
|
|
|
8
8
|
An overlay is a React component rendered frame-by-frame by Puppeteer, composited over the footage at a specific timestamp. All overlays are custom JSX — there are no built-in templates.
|
|
9
9
|
|
|
10
|
+
> **Read [MOTION.md](MOTION.md) alongside this file whenever the overlay actually moves.** This file covers getting an overlay on screen correctly; MOTION.md covers making it move like someone designed it — the easing catalog (`interpolate` is strictly linear and has no easing option, which is why untutored overlays look flat), directional motion blur, per-character stagger, the no-dead-air rule, and how to verify motion by measurement instead of by eye. Every technique in it was rendered through the real sandbox before it was written down.
|
|
11
|
+
|
|
10
12
|
---
|
|
11
13
|
|
|
12
14
|
## Execution context
|
|
@@ -28,21 +30,24 @@ Custom overlay JSX runs in a sandboxed evaluator. All identifiers below are inje
|
|
|
28
30
|
| `THREE` | namespace | All [Three.js](https://threejs.org) primitives — `THREE.Vector3`, `THREE.MathUtils`, etc. Only reach for it when you genuinely need 3D — see "3D / Three.js" section. |
|
|
29
31
|
| `Canvas` | component | [@react-three/fiber](https://r3f.docs.pmnd.rs) Canvas. **Always pass `frameloop="never"`** and mount a `useThreeFrame()` child — see "3D / Three.js" section. |
|
|
30
32
|
| `useThreeFrame` | hook | Bridges r3f to Montaj's frame-stepped renderer. Mount exactly once inside any `<Canvas>`. |
|
|
33
|
+
| `useCanvas2DFrame` | hook | Drives a plain `CanvasRenderingContext2D` from `frame` — for pixel-level 2D drawing HTML/CSS can't do (per-pixel effects, arbitrary paths, `drawImage` compositing). See "2D Canvas" section. |
|
|
31
34
|
|
|
32
35
|
**No imports.** All `import` statements are stripped before evaluation. Do not import anything — use the globals above instead.
|
|
33
36
|
|
|
37
|
+
**Never hardcode frame counts; projects can be 24, 30 or 60fps.** A literal like `[0, 10]` means a different real-world duration on every project — 10 frames is 0.33s at 30fps but 0.17s at 60fps, so an overlay copied between projects (or a 30fps example copied into a 60fps project, which is the current default) plays at the wrong speed. Always derive the frame count from `fps`: `Math.round(fps * 0.33)` for "about a third of a second," not `10`.
|
|
38
|
+
|
|
34
39
|
### Top-level vs component-body
|
|
35
40
|
|
|
36
41
|
**All calls to `interpolate`, `spring`, and any read of `frame`, `fps`, `duration`, or `props` must be inside the component function body.** The module's top-level code runs before the render shim sets up these globals — calling them outside a function will throw `interpolate is not defined` and crash the entire render.
|
|
37
42
|
|
|
38
43
|
```jsx
|
|
39
44
|
// WRONG — crashes at render time
|
|
40
|
-
const opacity = interpolate(frame, [0,
|
|
45
|
+
const opacity = interpolate(frame, [0, Math.round(fps * 0.33)], [0, 1])
|
|
41
46
|
export default function Hook() { ... }
|
|
42
47
|
|
|
43
48
|
// CORRECT — inside the component, runs each frame
|
|
44
49
|
export default function Hook() {
|
|
45
|
-
const opacity = interpolate(frame, [0,
|
|
50
|
+
const opacity = interpolate(frame, [0, Math.round(fps * 0.33)], [0, 1])
|
|
46
51
|
return <div style={{ opacity }}>...</div>
|
|
47
52
|
}
|
|
48
53
|
```
|
|
@@ -74,8 +79,8 @@ The default aesthetic is **plain bold text directly on video** — no card, no b
|
|
|
74
79
|
// overlays/hook.jsx — plain text on video, no background
|
|
75
80
|
|
|
76
81
|
export default function Hook() {
|
|
77
|
-
const progress = interpolate(frame, [0,
|
|
78
|
-
const slideY = interpolate(frame, [0,
|
|
82
|
+
const progress = interpolate(frame, [0, Math.round(fps * 0.27)], [0, 1])
|
|
83
|
+
const slideY = interpolate(frame, [0, Math.round(fps * 0.33)], [40, 0])
|
|
79
84
|
|
|
80
85
|
return (
|
|
81
86
|
<div style={{
|
|
@@ -124,8 +129,8 @@ A control appears for each of those that is present (non-null) on `props`; anyth
|
|
|
124
129
|
```jsx
|
|
125
130
|
// Editable Hook — every text property is adjustable in the panel.
|
|
126
131
|
export default function Hook() {
|
|
127
|
-
const progress = interpolate(frame, [0,
|
|
128
|
-
const slideY = interpolate(frame, [0,
|
|
132
|
+
const progress = interpolate(frame, [0, Math.round(fps * 0.27)], [0, 1])
|
|
133
|
+
const slideY = interpolate(frame, [0, Math.round(fps * 0.33)], [40, 0])
|
|
129
134
|
|
|
130
135
|
return (
|
|
131
136
|
<div style={{ position: 'absolute', bottom: 180, left: 48, right: 48, opacity: progress, transform: `translateY(${slideY}px)` }}>
|
|
@@ -215,7 +220,7 @@ The most reliable way to use frosted-glass / blurred card backgrounds is to **pu
|
|
|
215
220
|
```jsx
|
|
216
221
|
// overlays/card-bg.jsx
|
|
217
222
|
// Just a frosted card that fades in. No children that animate opacity.
|
|
218
|
-
const opacity = interpolate(frame, [0,
|
|
223
|
+
const opacity = interpolate(frame, [0, Math.round(fps * 0.27)], [0, 1])
|
|
219
224
|
|
|
220
225
|
export default function CardBg() {
|
|
221
226
|
return (
|
|
@@ -270,19 +275,23 @@ background: 'rgba(10,10,10,0.88)' // solid dark — visually similar, no GPU la
|
|
|
270
275
|
Maps a frame number to any output value. Clamps at both ends by default.
|
|
271
276
|
|
|
272
277
|
```jsx
|
|
273
|
-
// Fade in over
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
278
|
+
// Fade in over the first ~0.5s
|
|
279
|
+
const holdIn = Math.round(fps * 0.5)
|
|
280
|
+
const opacity = interpolate(frame, [0, holdIn], [0, 1])
|
|
281
|
+
|
|
282
|
+
// Fade in then out, each over ~0.5s
|
|
283
|
+
const holdOut = Math.round(fps * 0.5)
|
|
284
|
+
const fadeIn = interpolate(frame, [0, holdIn], [0, 1])
|
|
285
|
+
const fadeOut = interpolate(frame, [duration - holdOut, duration], [1, 0])
|
|
279
286
|
const opacity = Math.min(fadeIn, fadeOut)
|
|
280
287
|
|
|
281
|
-
// Slide in from left
|
|
282
|
-
const x = interpolate(frame, [0,
|
|
288
|
+
// Slide in from left over ~0.67s
|
|
289
|
+
const x = interpolate(frame, [0, Math.round(fps * 0.67)], [-200, 0])
|
|
283
290
|
```
|
|
284
291
|
|
|
285
|
-
|
|
292
|
+
One option: `extrapolate` — `'clamp'` (default) or `'extend'`. That is the whole options object; the runtime destructures `{ extrapolate = 'clamp' }` and ignores everything else. Earlier versions of this file used `extrapolateRight`, which does not exist and was silently dropped — harmless only because the default was already `'clamp'`.
|
|
293
|
+
|
|
294
|
+
**`interpolate` is strictly linear** — `interpolate(5, [0,10], [0,1])` is exactly `0.5`. There is no easing parameter. For eased motion, ease the normalised value first: see **[MOTION.md](MOTION.md)** for the easing catalog, directional motion blur, per-character stagger, and the no-dead-air rule.
|
|
286
295
|
|
|
287
296
|
### `spring({ frame, fps, mass?, stiffness?, damping?, initialVelocity? })`
|
|
288
297
|
|
|
@@ -475,6 +484,35 @@ Three.js + r3f add ~250 KB to an overlay segment's bundle after esbuild tree-sha
|
|
|
475
484
|
|
|
476
485
|
---
|
|
477
486
|
|
|
487
|
+
## 2D Canvas
|
|
488
|
+
|
|
489
|
+
For plain 2D drawing HTML/CSS can't do — per-pixel effects, arbitrary paths, `drawImage` compositing — overlay JSX can drive a `CanvasRenderingContext2D` via `useCanvas2DFrame`. Unlike Three.js, this needs no library and behaves identically in preview and render — there's no internal render loop to disable.
|
|
490
|
+
|
|
491
|
+
```jsx
|
|
492
|
+
export default function CanvasExample() {
|
|
493
|
+
const draw = (ctx, { frame, fps, duration }) => {
|
|
494
|
+
ctx.fillStyle = '#EFE3CE'
|
|
495
|
+
ctx.fillRect(0, 0, 1080, 1080)
|
|
496
|
+
|
|
497
|
+
const x = interpolate(frame, [0, duration], [0, 1080 - 120])
|
|
498
|
+
ctx.fillStyle = '#3b82f6'
|
|
499
|
+
ctx.fillRect(x, 480, 120, 120)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const ref = useCanvas2DFrame(draw, frame, fps, duration)
|
|
503
|
+
|
|
504
|
+
return <canvas ref={ref} width={1080} height={1080} style={{ position: 'absolute', inset: 0 }} />
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**`useCanvas2DFrame(draw, frame, fps, duration)` — always pass `frame`/`fps`/`duration` explicitly**, the same as `interpolate`/`spring`. It returns a ref to attach to a `<canvas>` element; `draw(ctx, { frame, fps, duration })` runs synchronously every time that ref is attached, which happens on every frame.
|
|
509
|
+
|
|
510
|
+
**Draw the complete frame from scratch every call** — same rule as everywhere else in overlay JSX: same `frame` in, same pixels out. Don't rely on anything painted by a previous call still being there (start with a fill/clear, as in the example above).
|
|
511
|
+
|
|
512
|
+
**One `<canvas>` per `useCanvas2DFrame` call.** Each call returns its own ref; don't share one ref across multiple canvases or call the hook conditionally.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
478
516
|
## Charts / Recharts
|
|
479
517
|
|
|
480
518
|
Overlay JSX can use SVG-based charts via [Recharts](https://recharts.org). Available globals: `BarChart`, `Bar`, `LineChart`, `Line`, `PieChart`, `Pie`, `Cell`, `XAxis`, `YAxis`, `CartesianGrid`, `Tooltip`, `Legend`, `ResponsiveContainer`.
|
|
@@ -565,7 +603,7 @@ Assets (logos, images) are declared in `project.assets`. Reference them by passi
|
|
|
565
603
|
|
|
566
604
|
```jsx
|
|
567
605
|
// overlays/logo.jsx
|
|
568
|
-
const opacity = interpolate(frame, [0,
|
|
606
|
+
const opacity = interpolate(frame, [0, Math.round(fps * 0.2)], [0, 1])
|
|
569
607
|
|
|
570
608
|
export default function Logo() {
|
|
571
609
|
return (
|