@amg555/frameforge-mcp 2.4.10 → 2.7.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/.claude-plugin/marketplace.json +22 -4
- package/CHANGELOG.md +174 -0
- package/CLAUDE.md +192 -9
- package/README.md +47 -6
- package/package.json +4 -3
- package/plugins/video-pipeline/.claude-plugin/plugin.json +8 -2
- package/plugins/video-pipeline/skills/.upstream-skills.json +143 -0
- package/plugins/video-pipeline/skills/html-renderer/SKILL.md +59 -15
- package/plugins/video-pipeline/skills/html-renderer/references/animation-patterns.md +609 -0
- package/plugins/video-pipeline/skills/html-renderer/references/character-animation.md +388 -0
- package/plugins/video-pipeline/skills/html-renderer/references/collage-broll.md +41 -0
- package/plugins/video-pipeline/skills/html-renderer/references/design.md +217 -0
- package/plugins/video-pipeline/skills/html-renderer/references/directing.md +165 -0
- package/plugins/video-pipeline/skills/html-renderer/references/earshot-audio.md +201 -0
- package/plugins/video-pipeline/skills/html-renderer/references/engine-api.md +480 -0
- package/plugins/video-pipeline/skills/html-renderer/references/film-styles.md +95 -0
- package/plugins/video-pipeline/skills/html-renderer/references/hyperframes-motion.md +208 -0
- package/plugins/video-pipeline/skills/html-renderer/references/onetake-camera.md +59 -0
- package/plugins/video-pipeline/skills/html-renderer/references/procedural-sfx.md +135 -0
- package/plugins/video-pipeline/skills/html-renderer/references/remotion-patterns.md +159 -0
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +19 -2
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +15 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +21 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +1293 -20
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +36 -6
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +126 -2
- package/scripts/deep-validate.js +52 -2
- package/src/audio-mixer/mixer.js +594 -63
- package/src/cli/doctor.js +36 -2
- package/src/cli/make.js +113 -9
- package/src/harness/bin/harness.mjs +127 -5
- package/src/harness/lib/capture.mjs +215 -192
- package/src/harness/lib/verify-quality.mjs +47 -7
- package/src/mcp-server/server.js +1015 -59
- package/src/skills/sync-upstream.js +290 -0
- package/src/utils/media-downloader.js +162 -0
- package/src/utils/transcribe.js +341 -0
- package/src/video-editor/editor.js +96 -11
- package/src/workflows/ingest.js +10 -0
- package/.bob/artifacts/enterprise-ai-video-agent-platform-agnostic-implementation-guide.html +0 -464
- package/out/check/purity_report.json +0 -79
- package/out/frames/f000000.jpg +0 -0
- package/out/frames/f000001.jpg +0 -0
- package/out/frames/f000002.jpg +0 -0
- package/out/frames/f000003.jpg +0 -0
- package/out/frames/f000004.jpg +0 -0
- package/out/frames/f000005.jpg +0 -0
- package/out/frames/f000006.jpg +0 -0
- package/out/frames/f000007.jpg +0 -0
- package/out/frames/f000008.jpg +0 -0
- package/out/frames/f000009.jpg +0 -0
- package/out/frames/f000010.jpg +0 -0
- package/out/frames/f000011.jpg +0 -0
- package/out/frames/f000012.jpg +0 -0
- package/out/frames/f000013.jpg +0 -0
- package/out/frames/f000014.jpg +0 -0
- package/out/frames/f000015.jpg +0 -0
- package/out/frames/f000016.jpg +0 -0
- package/out/frames/f000017.jpg +0 -0
- package/out/frames/f000018.jpg +0 -0
- package/out/frames/f000019.jpg +0 -0
- package/out/frames/f000020.jpg +0 -0
- package/out/frames/f000021.jpg +0 -0
- package/out/frames/f000022.jpg +0 -0
- package/out/frames/f000023.jpg +0 -0
- package/out/frames/f000024.jpg +0 -0
- package/out/frames/f000025.jpg +0 -0
- package/out/frames/f000026.jpg +0 -0
- package/out/frames/f000027.jpg +0 -0
- package/out/frames/f000028.jpg +0 -0
- package/out/frames/f000029.jpg +0 -0
- package/out/frames/f000030.jpg +0 -0
- package/out/frames/f000031.jpg +0 -0
- package/out/frames/f000032.jpg +0 -0
- package/out/frames/f000033.jpg +0 -0
- package/out/frames/f000034.jpg +0 -0
- package/out/frames/f000035.jpg +0 -0
- package/out/frames/f000036.jpg +0 -0
- package/out/frames/f000037.jpg +0 -0
- package/out/frames/f000038.jpg +0 -0
- package/out/frames/f000039.jpg +0 -0
- package/out/frames/f000040.jpg +0 -0
- package/out/frames/f000041.jpg +0 -0
- package/out/frames/f000042.jpg +0 -0
- package/out/frames/f000043.jpg +0 -0
- package/out/frames/f000044.jpg +0 -0
- package/out/frames/f000045.jpg +0 -0
- package/out/frames/f000046.jpg +0 -0
- package/out/frames/f000047.jpg +0 -0
- package/out/frames/f000048.jpg +0 -0
- package/out/frames/f000049.jpg +0 -0
- package/out/frames/f000050.jpg +0 -0
- package/out/frames/f000051.jpg +0 -0
- package/out/frames/f000052.jpg +0 -0
- package/out/frames/f000053.jpg +0 -0
- package/out/frames/f000054.jpg +0 -0
- package/out/frames/f000055.jpg +0 -0
- package/out/frames/f000056.jpg +0 -0
- package/out/frames/f000057.jpg +0 -0
- package/out/frames/f000058.jpg +0 -0
- package/out/frames/f000059.jpg +0 -0
- package/src/harness/test/fixtures/broken-scene.html +0 -27
- package/src/harness/test/fixtures/reference-scene.html +0 -23
- package/src/harness/test/fixtures/sample-script.json +0 -18
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
# Advanced Animation Patterns — ClaudeAnimationBase + OneTake + Collage B-Roll + Remotion
|
|
2
|
+
|
|
3
|
+
Derived from:
|
|
4
|
+
- `JohnHeibel/ClaudeAnimationBase` — organic 12fps cel boil and character emotions (Opus 5.5 pattern)
|
|
5
|
+
- `feitangyuan/onetake` — continuous single-shot seamless camera choreography with 39-move library
|
|
6
|
+
- `pyang5166/gbro-collage-broll` — editorial paper collage + halftone 2.5D tilt + 3-gate QA
|
|
7
|
+
- `remotion-dev/skills` — React composition, spring physics, GSAP markup patterns
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. ClaudeAnimationBase — 12fps Organic Cel Boil
|
|
12
|
+
|
|
13
|
+
The ClaudeAnimationBase pattern was engineered specifically for Claude Code + Opus 5.5
|
|
14
|
+
code-driven animation. The core idea: hand-drawn frames never hold still. Apply
|
|
15
|
+
deterministic seeded wobble at exactly 12fps to simulate organic life.
|
|
16
|
+
|
|
17
|
+
### Iron Rules (ClaudeAnimationBase contract)
|
|
18
|
+
|
|
19
|
+
1. **Quantize to 12fps**: `const step = Math.floor(t * 12)` — animation updates only on 12fps grid, not every rendered frame.
|
|
20
|
+
2. **Seeded determinism**: `Math.sin(seed * 19.37 + step * 2.41) * amplitude` — seed is a fixed number per element, never `Math.random()`.
|
|
21
|
+
3. **3-property boil on all hand-drawn entities**: position boil ± 2px, rotation boil ± 3deg, scale boil 0.97–1.03.
|
|
22
|
+
4. **Character emotion states**: encode as integer (0=neutral, 1=happy, 2=surprised, 3=concerned, 4=excited) and blend linearly between keyframe poses.
|
|
23
|
+
5. **Never use CSS transitions** — all motion is pure JS returning computed styles per-frame.
|
|
24
|
+
|
|
25
|
+
### 12fps Boil Template
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// In edit.js — organic 12fps boil on a character/doodle element
|
|
29
|
+
layer('char_body').on((t, lt, dur) => {
|
|
30
|
+
if (lt < 0 || lt > dur) return { opacity: 0, display: 'none' };
|
|
31
|
+
|
|
32
|
+
// Quantize to 12fps grid (ClaudeAnimationBase rule #1)
|
|
33
|
+
const step = Math.floor(lt * 12);
|
|
34
|
+
const seed = 7.3; // fixed per element, never random
|
|
35
|
+
|
|
36
|
+
// Seeded position boil (rule #2)
|
|
37
|
+
const bx = Math.sin(seed * 19.37 + step * 2.41) * 2.4; // ±2.4px X
|
|
38
|
+
const by = Math.cos(seed * 11.29 + step * 1.87) * 1.8; // ±1.8px Y
|
|
39
|
+
|
|
40
|
+
// Scale boil (subtle breathing life, rule #3)
|
|
41
|
+
const sc = 1.0 + Math.sin(seed * 7.11 + step * 3.14) * 0.025;
|
|
42
|
+
|
|
43
|
+
// Rotation boil (organic wobble)
|
|
44
|
+
const rot = Math.sin(seed * 13.71 + step * 0.79) * 1.8; // ±1.8deg
|
|
45
|
+
|
|
46
|
+
// Spring entrance (motion craft rule #1)
|
|
47
|
+
const enter = spring(lt, { tension: 200, friction: 20 });
|
|
48
|
+
const exit = lt > dur - 0.25 ? 1 - (dur - lt) / 0.25 : 0;
|
|
49
|
+
const opacity = enter * (1 - exit);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
opacity,
|
|
53
|
+
transform: `translate(${bx}px, ${by}px) scale(${sc * (0.88 + 0.12 * enter)}) rotate(${rot}deg)`,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Character Emotion Blending
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
// Emotion state: 0=neutral, 1=happy, 2=surprised, 3=concerned
|
|
62
|
+
// Encoded as a fractional value for smooth cross-blend
|
|
63
|
+
const emotion = 0; // set per scene
|
|
64
|
+
|
|
65
|
+
// Eyebrow positions per emotion [neutral, happy, surprised, concerned]
|
|
66
|
+
const browY = [0, -4, -10, 2];
|
|
67
|
+
const mouthCurve = [0, 8, 4, -5]; // positive = smile, negative = frown
|
|
68
|
+
|
|
69
|
+
const eyebrowShift = browY[emotion] || 0;
|
|
70
|
+
const mouthShift = mouthCurve[emotion] || 0;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 2. OneTake Continuous Camera Choreography
|
|
76
|
+
|
|
77
|
+
The OneTake pattern eliminates every hard cut. The entire video lives in one
|
|
78
|
+
continuous 2.5D coordinate space. The camera moves through it via spring-physics
|
|
79
|
+
spline rather than mounting/unmounting DOM elements.
|
|
80
|
+
|
|
81
|
+
### Spatial Canvas Setup
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// The whole video is ONE scene positioned on a 2.5D plane.
|
|
85
|
+
// Each narrative beat is a region of this canvas, not a separate HTML page.
|
|
86
|
+
|
|
87
|
+
// Scene regions (scene index → canvas position in pixels)
|
|
88
|
+
const SCENES = [
|
|
89
|
+
{ x: 0, y: 0, zoom: 1.00, start: 0 }, // Hook
|
|
90
|
+
{ x: 1920, y: 0, zoom: 1.15, start: 12 }, // Mechanism
|
|
91
|
+
{ x: 1920, y: 400, zoom: 1.80, start: 25 }, // Zoom Detail
|
|
92
|
+
{ x: 960, y: 200, zoom: 0.85, start: 38 }, // Wide Resolution
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Camera spring navigation between scenes
|
|
96
|
+
layer('onetake_camera').on((t, lt, dur) => {
|
|
97
|
+
// Find current and next scene based on t
|
|
98
|
+
let curr = SCENES[SCENES.length - 1];
|
|
99
|
+
let next = curr;
|
|
100
|
+
for (let i = 0; i < SCENES.length - 1; i++) {
|
|
101
|
+
if (t >= SCENES[i].start && t < SCENES[i + 1].start) {
|
|
102
|
+
curr = SCENES[i];
|
|
103
|
+
next = SCENES[i + 1];
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Spring travel from curr to next
|
|
109
|
+
const travelStart = curr.start;
|
|
110
|
+
const travelDur = next.start - curr.start;
|
|
111
|
+
const travelT = Math.max(0, t - travelStart);
|
|
112
|
+
const s = spring(travelT, { tension: 140, friction: 14 });
|
|
113
|
+
|
|
114
|
+
const cx = curr.x + (next.x - curr.x) * s;
|
|
115
|
+
const cy = curr.y + (next.y - curr.y) * s;
|
|
116
|
+
const cz = curr.zoom + (next.zoom - curr.zoom) * s;
|
|
117
|
+
|
|
118
|
+
// Depth-parallax background (z=0.2 moves at 20% of camera speed)
|
|
119
|
+
// Apply via CSS transform to #stage element
|
|
120
|
+
return {
|
|
121
|
+
position: 'absolute', inset: '0',
|
|
122
|
+
transform: `translate(${-cx}px, ${-cy}px) scale(${cz})`,
|
|
123
|
+
transformOrigin: '50% 50%',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Zero-Cut Scene Handshake
|
|
129
|
+
|
|
130
|
+
When the camera reaches Scene N+1, Scene N's primary subject shrinks and slides
|
|
131
|
+
to a supporting position rather than disappearing:
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// Outgoing subject: compress to supporting node
|
|
135
|
+
layer('scene1_subject_exit').on((t, lt, dur) => {
|
|
136
|
+
if (t < SCENES[1].start - 1.0 || t > SCENES[1].start + 0.5) return { display: 'none' };
|
|
137
|
+
const exitP = Math.min(1, (t - (SCENES[1].start - 1.0)) / 1.0);
|
|
138
|
+
const s = spring(exitP, { tension: 200, friction: 22 });
|
|
139
|
+
return {
|
|
140
|
+
transform: `translateX(${-580 * s}px) scale(${1 - 0.35 * s})`,
|
|
141
|
+
opacity: 1 - s * 0.4,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
// SFX at peak camera velocity (OneTake rule #3)
|
|
145
|
+
cue('whoosh', SCENES[1].start - 0.04, 0.7);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 3. Editorial Paper Collage & Halftone (gbro-collage-broll)
|
|
151
|
+
|
|
152
|
+
### 12fps Stop-Motion Drop Entrance
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
// Collage card entrance — hand-placed, 12fps stepped, not smooth
|
|
156
|
+
layer('collage_card').on((t, lt, dur) => {
|
|
157
|
+
if (lt < 0 || lt > dur) return { opacity: 0, display: 'none' };
|
|
158
|
+
|
|
159
|
+
// 12fps step grid
|
|
160
|
+
const step = Math.floor(lt * 12);
|
|
161
|
+
const dropProgress = Math.min(1, step / 6); // settles in 0.5s (6 frames @ 12fps)
|
|
162
|
+
|
|
163
|
+
const seed = 3.7; // fixed per card
|
|
164
|
+
const scale = 1.15 - 0.15 * dropProgress; // overshoots then settles
|
|
165
|
+
const rot = -4 + Math.sin(seed * 11) * 3; // fixed tilt angle
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
opacity: dropProgress > 0 ? 1 : 0,
|
|
169
|
+
transform: `scale(${scale}) rotate(${rot}deg) translateY(${(1 - dropProgress) * -60}px)`,
|
|
170
|
+
filter: 'drop-shadow(4px 6px 0px rgba(0,0,0,0.35))',
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Halftone Overlay (CSS background)
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// Halftone texture overlay — applied on Layer 2 (environment/context)
|
|
179
|
+
layer('halftone_env').on((t, lt, dur) => {
|
|
180
|
+
if (lt < 0 || lt > dur) return { opacity: 0, display: 'none' };
|
|
181
|
+
return {
|
|
182
|
+
position: 'absolute', inset: '0',
|
|
183
|
+
backgroundImage: `radial-gradient(circle, #000 1.5px, transparent 1.5px)`,
|
|
184
|
+
backgroundSize: '12px 12px',
|
|
185
|
+
opacity: 0.08,
|
|
186
|
+
mixBlendMode: 'multiply',
|
|
187
|
+
pointerEvents: 'none',
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 2.5D Perspective Tilt
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// Apply to the whole collage card container
|
|
196
|
+
layer('collage_container').on((t, lt, dur) => {
|
|
197
|
+
const tiltX = 15 * Math.sin(lt * 0.3); // gentle breathing tilt
|
|
198
|
+
const tiltY = -10 * Math.cos(lt * 0.2);
|
|
199
|
+
return {
|
|
200
|
+
transform: `perspective(800px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`,
|
|
201
|
+
transformStyle: 'preserve-3d',
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 4. Reanimate Closed-Loop Pattern (reanimate.niazmorshed.dev)
|
|
209
|
+
|
|
210
|
+
The reanimate pattern is the Opus 5.5 correction loop for existing animations.
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Phase 1 — Record: Playwright records every moving element at fixed fps
|
|
214
|
+
Phase 2 — Tabulate: start, duration, distance, easing curve per element
|
|
215
|
+
Phase 3 — Rebuild: Agent rewrites edit.js from the tabulation (not from scratch)
|
|
216
|
+
Phase 4 — Re-record: Same Playwright setup, same seed
|
|
217
|
+
Phase 5 — Diff: Pixel diff + timing diff. Target: 0px, ≤17ms per element
|
|
218
|
+
|
|
219
|
+
Pass/fail thresholds:
|
|
220
|
+
pixel_diff_max: 0 (deterministic rendering must be identical)
|
|
221
|
+
timing_diff_ms: ≤17 (one frame at 60fps)
|
|
222
|
+
easing_diff_score: ≤0.05 (normalized spring curve comparison)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
In FrameForge this maps to:
|
|
226
|
+
```
|
|
227
|
+
render_animation({ mode: "golden", goldenSave: true })
|
|
228
|
+
// then fix edit.js, then:
|
|
229
|
+
render_animation({ mode: "golden", goldenCheck: true })
|
|
230
|
+
// must return: all hashes match
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 5. OneTake — 39-Move Library and Rhythm+Carry Oracle
|
|
236
|
+
|
|
237
|
+
The OneTake system (from `feitangyuan/onetake`) is built around one constraint: **six rejected films, six acceptance criteria**. Every feature came from a specific rejection.
|
|
238
|
+
|
|
239
|
+
### Why It Exists (Each Rule Cost a Cut)
|
|
240
|
+
|
|
241
|
+
| Rule | Origin |
|
|
242
|
+
|------|--------|
|
|
243
|
+
| Vary shot length by ≥4× | Uniform cadence = "PPT 式" (slideshow feel) — failed twice |
|
|
244
|
+
| Something must survive every section boundary | "Beats that replace each other" = Pocket Weather Club v1, v2 both rejected (carry score 0.00, 0.40) |
|
|
245
|
+
| Camera must chase the subject | One Dot film: "就缺灵动的跟踪运镜" — still camera rejected |
|
|
246
|
+
| Every beat is the collision that starts the next | "knockon" — causal carry |
|
|
247
|
+
| Small elements, big ground | "70% white" genre; full-bleed is the exception |
|
|
248
|
+
| The cause must be visible | Hand drawn from recorded path, or concept's own subject |
|
|
249
|
+
|
|
250
|
+
### The Rhythm + Carry Oracle (verify_promo.py)
|
|
251
|
+
|
|
252
|
+
Before showing any render, run the oracle:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
python3 scripts/verify_promo.py draft.mp4 --comp comp.html --ref ref.mp4
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**What it checks**:
|
|
259
|
+
|
|
260
|
+
1. **Rhythm legs**: fail uniform shot lengths, no dead-still stretch, clipped or never-quiet audio
|
|
261
|
+
2. **Continuity** (the critical leg): fail beats that replace each other (carry score < 0.5). This is the ONLY leg that failed both rejected Pocket Weather cuts (v1 0.00, v2 0.40) while accepted films passed (v3 0.75)
|
|
262
|
+
3. **Curves**: fail fast moves (> 80 px/frame) in a film without shutter blur
|
|
263
|
+
4. **Framing**: fail any frame where a declared subject leaves the frame (or is cut off)
|
|
264
|
+
|
|
265
|
+
**Carry score thresholds**:
|
|
266
|
+
- Score < 0.5 → FAIL (the film is a slideshow)
|
|
267
|
+
- Score 0.5–0.75 → WARNING (acceptable if rhythm is strong)
|
|
268
|
+
- Score > 0.75 → PASS
|
|
269
|
+
|
|
270
|
+
### The OM Motion Library (engine.js `OM` object)
|
|
271
|
+
|
|
272
|
+
**All moves are pure CSS: `(t, opts) → { transform, opacity, clipPath }`.
|
|
273
|
+
Feed `t` via `seg(currentTime, start, end)` to get normalised 0→1 progress.**
|
|
274
|
+
|
|
275
|
+
Never re-derive springs or eases inline. Use `OM.*` — they encode the correct
|
|
276
|
+
physics and carry the carry-score contract (exit velocity ≥ 0.5× peak velocity).
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
import { OM, seg } from './reel/engine.js';
|
|
280
|
+
|
|
281
|
+
// Usage pattern: seg() gives normalised t for a time window
|
|
282
|
+
layer('hero').on((t) => {
|
|
283
|
+
const p = seg(t, 0.5, 1.5); // 0→1 over 0.5–1.5 seconds
|
|
284
|
+
Object.assign(heroEl.style, OM.spring(p));
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Entrances** (element appearing, enter → stage):
|
|
289
|
+
```javascript
|
|
290
|
+
OM.spring(t, { scale0: 0.6, scale1: 1.0, opacity0: 0, opacity1: 1 })
|
|
291
|
+
// → { transform: 'scale(…)', opacity: … }
|
|
292
|
+
// Damped spring overshoot. Default tension:180, friction:18. Standard arrival.
|
|
293
|
+
|
|
294
|
+
OM.rise(t, { dist: 60, opacity0: 0 })
|
|
295
|
+
// → { transform: 'translateY(…)', opacity: … }
|
|
296
|
+
// Translates up from dist px below while fading in.
|
|
297
|
+
|
|
298
|
+
OM.iris(t, { cx: '50%', cy: '50%' })
|
|
299
|
+
// → { clipPath: 'circle(…% at cx cy)' }
|
|
300
|
+
// Radial clip-path expands from centre. 141% covers full diagonal.
|
|
301
|
+
|
|
302
|
+
OM.morphRect(t, { axis: 'y' })
|
|
303
|
+
// → { clipPath: 'inset(…% 0 …% 0)' }
|
|
304
|
+
// Clip-path from narrow rect to full frame. axis: 'x' or 'y'.
|
|
305
|
+
|
|
306
|
+
OM.silk(t, { direction: 'left' })
|
|
307
|
+
// → { clipPath: 'inset(0 …% 0 0)' }
|
|
308
|
+
// Curtain-pull clip from the specified edge.
|
|
309
|
+
|
|
310
|
+
OM.zoomThrough(t, { from: 0.8, through: 1.15, to: 1.0 })
|
|
311
|
+
// → { transform: 'scale(…)' }
|
|
312
|
+
// Scale through 1× — portal / push-through effect. Peak at t=0.5.
|
|
313
|
+
|
|
314
|
+
OM.scalePop(t)
|
|
315
|
+
// → { transform: 'scale(…)', opacity: … }
|
|
316
|
+
// Title scales from 1.6× down to 1× with expo ease (HyperFrames title pattern).
|
|
317
|
+
|
|
318
|
+
OM.wordByWord(t, { wordIndex: 0, totalWords: 1, delay: 0.08 })
|
|
319
|
+
// → { transform: 'translateY(…)', opacity: … }
|
|
320
|
+
// Per-word spring stagger. Call with different wordIndex for each word.
|
|
321
|
+
|
|
322
|
+
OM.revealWipe(t, { direction: 'left' })
|
|
323
|
+
// → { clipPath: 'inset(0 …% 0 0)' }
|
|
324
|
+
// Horizontal clip wipe — title text reveal.
|
|
325
|
+
|
|
326
|
+
OM.typewriter(t, { chars: 20 })
|
|
327
|
+
// → { '--tw-chars': revealed }
|
|
328
|
+
// Returns CSS custom property for character count (use in CSS clip on ::before).
|
|
329
|
+
|
|
330
|
+
OM.counterFlip(t, { from: 0, to: 100, height: 60 })
|
|
331
|
+
// → { transform: 'translateY(…)' }
|
|
332
|
+
// Odometer-style numeric reveal via vertical translate.
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Carries** (element surviving a scene boundary — carry score ≥ 0.5 required):
|
|
336
|
+
```javascript
|
|
337
|
+
OM.irisClose(t, { cx: '50%', cy: '50%' })
|
|
338
|
+
// → { clipPath: 'circle(…% at cx cy)' }
|
|
339
|
+
// Reverse of iris — stage contracts to a point.
|
|
340
|
+
|
|
341
|
+
OM.belt(t, { strips: 6, direction: 'left' })
|
|
342
|
+
// → { '--belt-strips': 6, '--belt-p': t, transform: … }
|
|
343
|
+
// Horizontal strip sequence reveal (film-strip / ticker).
|
|
344
|
+
|
|
345
|
+
OM.ripple(t, { cx: '50%', cy: '50%', opacity0: 0.6 })
|
|
346
|
+
// → { clipPath: 'circle(…% at cx cy)', opacity: … }
|
|
347
|
+
// Expanding + fading radial wave. Used for carry momentum.
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Camera** (keyed, operated feel):
|
|
351
|
+
```javascript
|
|
352
|
+
OM.camera(t, {
|
|
353
|
+
x0: 0, y0: 0, x1: 20, y1: -10, // pan range in px
|
|
354
|
+
scale0: 1.0, scale1: 1.08, // zoom range
|
|
355
|
+
seed: 'cam', // deterministic noise seed
|
|
356
|
+
shakeAmp: 2, // micro-shake amplitude (px)
|
|
357
|
+
shakeFreq: 1.5, // shake frequency (cycles/sec)
|
|
358
|
+
})
|
|
359
|
+
// → { transform: 'translate(…px, …px) scale(…)' }
|
|
360
|
+
// Slow Ken Burns push + seeded noise micro-shake.
|
|
361
|
+
|
|
362
|
+
OM.handheld(t, { amp: 6, freq: 8, seed: 'hh' })
|
|
363
|
+
// → { transform: 'translate(…px, …px) rotate(…deg)' }
|
|
364
|
+
// Vérité/documentary shake. 3-axis (x, y, z rotate).
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Exits** (faster than entrances — OneTake rule):
|
|
368
|
+
```javascript
|
|
369
|
+
OM.popOut(t, { scale1: 1.0, scale2: 1.4 })
|
|
370
|
+
// → { transform: 'scale(…)', opacity: … } Uses easeIn.
|
|
371
|
+
|
|
372
|
+
OM.drop(t, { dist: 120 })
|
|
373
|
+
// → { transform: 'translateY(…)', opacity: … } Gravity ease-in.
|
|
374
|
+
|
|
375
|
+
OM.implode(t, { cx: '50%', cy: '50%' })
|
|
376
|
+
// → { clipPath: 'circle(…%)', opacity: … } Iris collapse.
|
|
377
|
+
|
|
378
|
+
OM.slideExit(t, { direction: 'left', dist: 110 })
|
|
379
|
+
// → { transform: 'translateX(…%)' } Off-canvas slide.
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Ambient / Atmospheric** (idle states, breathing room):
|
|
383
|
+
```javascript
|
|
384
|
+
OM.breathe(t, { amp: 0.015, freq: 0.4 }) // subtle scale oscillation
|
|
385
|
+
OM.float(t, { amp: 8, freq: 0.3 }) // sinusoidal vertical drift
|
|
386
|
+
OM.pulseOpacity(t, { lo: 0.7, hi: 1.0, freq: 1.0 }) // rhythmic emphasis
|
|
387
|
+
OM.odometer(t, { from: 0, to: 9, height: 60 }) // digit column roll
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Carry score** (rhythm oracle — check at t=0.8–1.0):
|
|
391
|
+
```javascript
|
|
392
|
+
const score = OM.carryScore(1.0); // must be ≥ 0.5 to PASS
|
|
393
|
+
// carryScore < 0.5 → the film is a slideshow (FAIL)
|
|
394
|
+
// carryScore 0.5–0.75 → acceptable if rhythm is strong (WARNING)
|
|
395
|
+
// carryScore > 0.75 → PASS
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Sound: A Palette in a Room, Never One Beep Per Event
|
|
399
|
+
|
|
400
|
+
OneTake requires **5 materials in one room** — not a raw sine pop on every event (which was "低劣/cheap" within a minute of review):
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
# sfx_palette.py — 5 synthesised materials in one shared reverb
|
|
404
|
+
# Each material is a physical model of the medium
|
|
405
|
+
|
|
406
|
+
MATERIALS = {
|
|
407
|
+
'air': { freq: (800, 4000), attack: 0.005, decay: 0.08, model: 'filtered_noise' },
|
|
408
|
+
'wood': { freq: (200, 800), attack: 0.002, decay: 0.15, model: 'modal_resonator' },
|
|
409
|
+
'glass': { freq: (1000, 6000), attack: 0.001, decay: 0.35, model: 'modal_resonator' },
|
|
410
|
+
'sub': { freq: (40, 120), attack: 0.008, decay: 0.25, model: 'sine_decay' },
|
|
411
|
+
'bubble': { freq: (400, 1200), attack: 0.003, decay: 0.12, model: 'rising_sine' },
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
ROOM = {
|
|
415
|
+
'ir': 'voxengo_small_room.wav', # convolution reverb IR
|
|
416
|
+
'pre_delay': 8, # ms
|
|
417
|
+
'wet': 0.18, # 18% wet
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# All peaks at -8 dBFS so music can sit on top
|
|
421
|
+
# Master to -16 LUFS with ceiling -3.5 to -4 dBTP
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Sound events from the composition** — not guessed, read from `window.__events()`:
|
|
425
|
+
```javascript
|
|
426
|
+
// In your composition's f(t) function:
|
|
427
|
+
window.__events = () => [
|
|
428
|
+
{ t: 0.8, material: 'wood', intensity: 0.7, pan: -0.2 },
|
|
429
|
+
{ t: 1.5, material: 'air', intensity: 0.4, pan: 0.1 },
|
|
430
|
+
{ t: 2.1, material: 'glass', intensity: 0.9, pan: 0.0 },
|
|
431
|
+
];
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Hard Rules (Each One Cost a Cut)
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
❌ Never default to the template's story
|
|
438
|
+
❌ Never carry the last film's look into the next
|
|
439
|
+
❌ Never cut on a metronome (equal shot lengths = slides)
|
|
440
|
+
❌ Never replace a beat — carry it (something must survive every section boundary)
|
|
441
|
+
❌ Never show a page as a thumbnail expecting its motion to read
|
|
442
|
+
❌ Never let the frame be always in motion (rests make bursts land; reference is still 34% of the time)
|
|
443
|
+
❌ Small elements, big ground (genre frame is 70% white/negative space)
|
|
444
|
+
❌ The cause must be visible (the hand, or the concept's own subject)
|
|
445
|
+
❌ Moves from the library, curves chosen by t80
|
|
446
|
+
❌ Fast moves (> 80 px/frame) need the shutter (real motion blur)
|
|
447
|
+
❌ No particles/confetti/speck bursts (read as a template)
|
|
448
|
+
❌ One material set for sound, one room
|
|
449
|
+
❌ Everything deterministic: __seek(t) twice gives the same frame
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 6. Remotion — React Composition Patterns
|
|
455
|
+
|
|
456
|
+
From `remotion-dev/skills` (first-party skills, v4.0.543+).
|
|
457
|
+
|
|
458
|
+
### The Golden Rule: `useCurrentFrame()` as Deep as Possible
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
// ✅ Correct — useCurrentFrame in the smallest component that needs it
|
|
462
|
+
const FadeIn = () => {
|
|
463
|
+
const frame = useCurrentFrame();
|
|
464
|
+
const {fps} = useVideoConfig();
|
|
465
|
+
return (
|
|
466
|
+
<div style={{
|
|
467
|
+
opacity: interpolate(frame, [0, 2 * fps], [0, 1], {
|
|
468
|
+
extrapolateRight: 'clamp',
|
|
469
|
+
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
|
470
|
+
}),
|
|
471
|
+
}}>
|
|
472
|
+
Content
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// ❌ Wrong — subscribes the whole composition on every frame
|
|
478
|
+
const CompositionWrong = () => {
|
|
479
|
+
const frame = useCurrentFrame(); // re-renders everything
|
|
480
|
+
return <div><FadeIn /><OtherStuff /></div>;
|
|
481
|
+
};
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### CSS Transform Shorthands (Not `transform`)
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
// ✅ Use scale, translate, rotate — Studio can edit these
|
|
488
|
+
style={{
|
|
489
|
+
scale: interpolate(frame, [0, 100], [0, 1], {
|
|
490
|
+
extrapolateLeft: 'clamp',
|
|
491
|
+
extrapolateRight: 'clamp',
|
|
492
|
+
easing: Easing.spring({ damping: 200 }),
|
|
493
|
+
output: 'perceptual-scale', // ALWAYS for scale animations
|
|
494
|
+
}),
|
|
495
|
+
translate: interpolate(frame, [0, 100], ['0px 0px', '100px 100px']),
|
|
496
|
+
}}
|
|
497
|
+
|
|
498
|
+
// ❌ Wrong — transform strings break Studio editing
|
|
499
|
+
const scale = interpolate(frame, [0, 100], [0, 1]);
|
|
500
|
+
style={{ transform: `scale(${scale})` }}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Remotion Spring vs FrameForge Spring
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
// Remotion (React)
|
|
507
|
+
import { Easing } from 'remotion';
|
|
508
|
+
Easing.spring({ damping: 200 }) // damping: high = less bounce
|
|
509
|
+
|
|
510
|
+
// FrameForge (HTML compositor)
|
|
511
|
+
spring(lt, { tension: 200, friction: 20 }) // friction: high = less bounce
|
|
512
|
+
|
|
513
|
+
// Equivalent presets:
|
|
514
|
+
// Remotion damping 200 ≈ FrameForge { tension: 200, friction: 28 } (premium)
|
|
515
|
+
// Remotion damping 100 ≈ FrameForge { tension: 180, friction: 16 } (playful)
|
|
516
|
+
// Remotion damping 50 ≈ FrameForge { tension: 200, friction: 10 } (cartoon)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Available Built-in Effects (Remotion @remotion/effects)
|
|
520
|
+
|
|
521
|
+
Color and tone:
|
|
522
|
+
`brightness`, `colorCorrection`, `curves`, `duotone`, `exposure`, `grayscale`, `hue`, `invert`, `levels`, `lut` (3D .cube), `saturation`, `shadowsHighlights`, `tint`, `vibrance`, `whiteBalance`
|
|
523
|
+
|
|
524
|
+
Blur and trails:
|
|
525
|
+
`blur`, `lightTrail`, `linearProgressiveBlur`, `radialProgressiveBlur`, `regionBlur`, `zoomBlur`
|
|
526
|
+
|
|
527
|
+
Lighting:
|
|
528
|
+
`dropShadow`, `glow`, `lightLeak`, `longShadows`, `shine`, `starburst`, `vignette`
|
|
529
|
+
|
|
530
|
+
Distortion:
|
|
531
|
+
`barrelDistortion`, `chromaticAberration`, `cornerPin`, `fisheye`, `noiseDisplacement`, `skew`, `wave`
|
|
532
|
+
|
|
533
|
+
Textures:
|
|
534
|
+
`burlap`, `checkerboard`, `contourLines`, `dotGrid`, `emboss`, `flannel`, `halftone`, `lines`, `liquidContours`, `noise`, `paper`, `scanlines`, `speckle`, `tvSignalOff`, `waves`, `whiteNoise`, `zigzag`
|
|
535
|
+
|
|
536
|
+
Edges: `outline`, `roughenEdges`, `shrinkwrap`, `tear`
|
|
537
|
+
|
|
538
|
+
**Apply to** `<Video>`, `<CanvasImage>`, `<AnimatedImage>`, `<Solid>`, `<HtmlInCanvas>`, `@remotion/shapes`, `@remotion/rive`. For plain HTML elements, wrap in `<HtmlInCanvas>`.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## 7. GBro Collage B-Roll — 3-Gate Approval Workflow
|
|
543
|
+
|
|
544
|
+
The gbro-collage-broll system enforces mandatory human approval at three gates before any video generation.
|
|
545
|
+
|
|
546
|
+
### Why 3 Gates?
|
|
547
|
+
1. Prevents wrong metaphors from consuming video generation credits
|
|
548
|
+
2. Forces the agent to think visually before coding
|
|
549
|
+
3. Each gate narrows scope so errors are caught cheaply
|
|
550
|
+
|
|
551
|
+
### Gate 1: Visual Metaphor Only (No Images, No Video)
|
|
552
|
+
```
|
|
553
|
+
Input: ~5-second voiceover script (one sentence / one idea)
|
|
554
|
+
Output:
|
|
555
|
+
- Core meaning (what the viewer must understand)
|
|
556
|
+
- Emotion (calm, surprise, urgency, insight, absurd, ironic)
|
|
557
|
+
- One-sentence visual proposition
|
|
558
|
+
- 3–6 key objects
|
|
559
|
+
- Suggested background colour + accent colours
|
|
560
|
+
- Expected assembly order
|
|
561
|
+
|
|
562
|
+
STOP — wait for user: "可以" / "通过" / "pass" / corrections
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Gate 2: Static Frame Only (No Video)
|
|
566
|
+
```
|
|
567
|
+
After Gate 1 approval:
|
|
568
|
+
- Write visual-spec.json (self-contained)
|
|
569
|
+
- Generate static frame (imagegen)
|
|
570
|
+
- QA: is metaphor clear? no text/logos/watermarks? 3-6 large groups? enough negative space?
|
|
571
|
+
|
|
572
|
+
STOP — wait for user confirmation of the still image
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Gate 3: Video Generation (Default: Gemini Omni Flash)
|
|
576
|
+
```
|
|
577
|
+
After Gate 2 approval:
|
|
578
|
+
- Prepare first frame (solid colour background = same colour as still)
|
|
579
|
+
- Prepare last frame (confirmed Gate 2 still)
|
|
580
|
+
- Write Omni prompt: "assemble piece by piece, start-frame-empty → end-frame-complete"
|
|
581
|
+
- Generate 5s 9:16 720×1280 24fps silent MP4
|
|
582
|
+
|
|
583
|
+
QA after generation:
|
|
584
|
+
- First frame: near-empty colour field
|
|
585
|
+
- Middle: gradual assembly visible, not overall fade-in
|
|
586
|
+
- No cuts, no zoom, no 3D
|
|
587
|
+
- No text/logos/watermarks
|
|
588
|
+
- Last frame matches Gate 2 still
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Visual Standards
|
|
592
|
+
```
|
|
593
|
+
✓ Strong, flat, uniform colour field background
|
|
594
|
+
✓ Main subjects as black-and-white halftone photographic cut-outs
|
|
595
|
+
✓ Key cards/buttons/rules in coloured cardstock (red, yellow, cyan, orange, purple, cream)
|
|
596
|
+
✓ All paper pieces have crisp cut edges, cream keyline, soft drop shadow, paper grain
|
|
597
|
+
✓ Motion is assemble-from-empty (not drift, sway, or slow zoom)
|
|
598
|
+
✗ No subtitles, no full voiceover text, no logos, no watermarks, no UI
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Colour-to-Emotion Mapping
|
|
602
|
+
```
|
|
603
|
+
Burnt orange / red → time consumption, labour, urgency
|
|
604
|
+
Mustard yellow → tools, warning, experience loss
|
|
605
|
+
Forest green → cognition, aesthetics, system reset
|
|
606
|
+
Deep purple → norms, sedimentation, long-term memory
|
|
607
|
+
Cyan green → judgment, collaboration, automated execution
|
|
608
|
+
Cobalt blue → (avoid as default — too overused)
|
|
609
|
+
```
|