@editframe/create 0.44.0 → 0.45.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.
Files changed (98) hide show
  1. package/dist/index.js +16 -28
  2. package/dist/index.js.map +1 -1
  3. package/dist/skills/editframe-brand-video-generator/README.md +155 -0
  4. package/dist/skills/editframe-brand-video-generator/SKILL.md +207 -0
  5. package/dist/skills/editframe-brand-video-generator/references/brand-examples.md +178 -0
  6. package/dist/skills/editframe-brand-video-generator/references/color-psychology.md +227 -0
  7. package/dist/skills/editframe-brand-video-generator/references/composition-patterns.md +383 -0
  8. package/dist/skills/editframe-brand-video-generator/references/editing.md +66 -0
  9. package/dist/skills/editframe-brand-video-generator/references/emotional-arcs.md +496 -0
  10. package/dist/skills/editframe-brand-video-generator/references/genre-selection.md +135 -0
  11. package/dist/skills/editframe-brand-video-generator/references/transition-styles.md +611 -0
  12. package/dist/skills/editframe-brand-video-generator/references/typography-personalities.md +326 -0
  13. package/dist/skills/editframe-brand-video-generator/references/video-archetypes.md +86 -0
  14. package/dist/skills/editframe-brand-video-generator/references/video-fundamentals.md +169 -0
  15. package/dist/skills/editframe-brand-video-generator/references/visual-metaphors.md +50 -0
  16. package/dist/skills/editframe-composition/SKILL.md +169 -0
  17. package/dist/skills/editframe-composition/references/audio.md +483 -0
  18. package/dist/skills/editframe-composition/references/captions.md +844 -0
  19. package/dist/skills/editframe-composition/references/composition-model.md +73 -0
  20. package/dist/skills/editframe-composition/references/configuration.md +403 -0
  21. package/dist/skills/editframe-composition/references/css-parts.md +105 -0
  22. package/dist/skills/editframe-composition/references/css-variables.md +640 -0
  23. package/dist/skills/editframe-composition/references/entry-points.md +810 -0
  24. package/dist/skills/editframe-composition/references/events.md +499 -0
  25. package/dist/skills/editframe-composition/references/getting-started.md +259 -0
  26. package/dist/skills/editframe-composition/references/hooks.md +234 -0
  27. package/dist/skills/editframe-composition/references/image.md +241 -0
  28. package/dist/skills/editframe-composition/references/r3f.md +580 -0
  29. package/dist/skills/editframe-composition/references/render-api.md +484 -0
  30. package/dist/skills/editframe-composition/references/render-strategies.md +119 -0
  31. package/dist/skills/editframe-composition/references/render-to-video.md +1101 -0
  32. package/dist/skills/editframe-composition/references/scripting.md +606 -0
  33. package/dist/skills/editframe-composition/references/sequencing.md +116 -0
  34. package/dist/skills/editframe-composition/references/server-rendering.md +753 -0
  35. package/dist/skills/editframe-composition/references/surface.md +329 -0
  36. package/dist/skills/editframe-composition/references/text.md +627 -0
  37. package/dist/skills/editframe-composition/references/time-model.md +99 -0
  38. package/dist/skills/editframe-composition/references/timegroup-modes.md +102 -0
  39. package/dist/skills/editframe-composition/references/timegroup.md +457 -0
  40. package/dist/skills/editframe-composition/references/timeline-root.md +398 -0
  41. package/dist/skills/editframe-composition/references/transcription.md +47 -0
  42. package/dist/skills/editframe-composition/references/transitions.md +608 -0
  43. package/dist/skills/editframe-composition/references/use-media-info.md +357 -0
  44. package/dist/skills/editframe-composition/references/video.md +506 -0
  45. package/dist/skills/editframe-composition/references/waveform.md +327 -0
  46. package/dist/skills/editframe-editor-gui/SKILL.md +152 -0
  47. package/dist/skills/editframe-editor-gui/references/active-root-temporal.md +657 -0
  48. package/dist/skills/editframe-editor-gui/references/canvas.md +947 -0
  49. package/dist/skills/editframe-editor-gui/references/controls.md +366 -0
  50. package/dist/skills/editframe-editor-gui/references/dial.md +756 -0
  51. package/dist/skills/editframe-editor-gui/references/editor-toolkit.md +587 -0
  52. package/dist/skills/editframe-editor-gui/references/filmstrip.md +460 -0
  53. package/dist/skills/editframe-editor-gui/references/fit-scale.md +772 -0
  54. package/dist/skills/editframe-editor-gui/references/focus-overlay.md +561 -0
  55. package/dist/skills/editframe-editor-gui/references/hierarchy.md +544 -0
  56. package/dist/skills/editframe-editor-gui/references/overlay-item.md +634 -0
  57. package/dist/skills/editframe-editor-gui/references/overlay-layer.md +429 -0
  58. package/dist/skills/editframe-editor-gui/references/pan-zoom.md +568 -0
  59. package/dist/skills/editframe-editor-gui/references/pause.md +397 -0
  60. package/dist/skills/editframe-editor-gui/references/play.md +370 -0
  61. package/dist/skills/editframe-editor-gui/references/preview.md +391 -0
  62. package/dist/skills/editframe-editor-gui/references/resizable-box.md +749 -0
  63. package/dist/skills/editframe-editor-gui/references/scrubber.md +588 -0
  64. package/dist/skills/editframe-editor-gui/references/thumbnail-strip.md +566 -0
  65. package/dist/skills/editframe-editor-gui/references/time-display.md +492 -0
  66. package/dist/skills/editframe-editor-gui/references/timeline-ruler.md +489 -0
  67. package/dist/skills/editframe-editor-gui/references/timeline.md +604 -0
  68. package/dist/skills/editframe-editor-gui/references/toggle-loop.md +618 -0
  69. package/dist/skills/editframe-editor-gui/references/toggle-play.md +526 -0
  70. package/dist/skills/editframe-editor-gui/references/transform-handles.md +924 -0
  71. package/dist/skills/editframe-editor-gui/references/trim-handles.md +725 -0
  72. package/dist/skills/editframe-editor-gui/references/workbench.md +453 -0
  73. package/dist/skills/editframe-motion-design/SKILL.md +101 -0
  74. package/dist/skills/editframe-motion-design/references/0-editframe.md +299 -0
  75. package/dist/skills/editframe-motion-design/references/1-intent.md +201 -0
  76. package/dist/skills/editframe-motion-design/references/2-physics-model.md +405 -0
  77. package/dist/skills/editframe-motion-design/references/3-attention.md +350 -0
  78. package/dist/skills/editframe-motion-design/references/4-process.md +418 -0
  79. package/dist/skills/editframe-vite-plugin/SKILL.md +75 -0
  80. package/dist/skills/editframe-vite-plugin/references/file-api.md +111 -0
  81. package/dist/skills/editframe-vite-plugin/references/getting-started.md +96 -0
  82. package/dist/skills/editframe-vite-plugin/references/jit-transcoding.md +91 -0
  83. package/dist/skills/editframe-vite-plugin/references/local-assets.md +75 -0
  84. package/dist/skills/editframe-vite-plugin/references/visual-testing.md +136 -0
  85. package/dist/skills/editframe-webhooks/SKILL.md +126 -0
  86. package/dist/skills/editframe-webhooks/references/events.md +382 -0
  87. package/dist/skills/editframe-webhooks/references/getting-started.md +232 -0
  88. package/dist/skills/editframe-webhooks/references/security.md +418 -0
  89. package/dist/skills/editframe-webhooks/references/testing.md +409 -0
  90. package/dist/skills/editframe-webhooks/references/troubleshooting.md +457 -0
  91. package/dist/templates/html/AGENTS.md +13 -0
  92. package/dist/templates/react/AGENTS.md +13 -0
  93. package/dist/utils.js +15 -16
  94. package/dist/utils.js.map +1 -1
  95. package/package.json +1 -1
  96. package/tsdown.config.ts +4 -0
  97. package/dist/detectAgent.js +0 -89
  98. package/dist/detectAgent.js.map +0 -1
@@ -0,0 +1,1101 @@
1
+ ---
2
+ title: Render to Video Tutorial
3
+ description: End-to-end guide to exporting Editframe compositions as MP4 video files using the browser renderer or Editframe CLI.
4
+ type: tutorial
5
+ nav:
6
+ parent: "Rendering"
7
+ priority: 10
8
+ related: ["render-api", "render-strategies"]
9
+ react:
10
+ generate: true
11
+ componentName: "Render to Video Tutorial"
12
+ importPath: "@editframe/react"
13
+ nav:
14
+ parent: "Guides / Tutorials"
15
+ priority: 15
16
+ related: ["hooks", "timegroup"]
17
+ ---
18
+
19
+ <!-- html-only -->
20
+ # Render to Video Tutorial
21
+
22
+ Build a composition and export it as MP4 video directly in the browser using WebCodecs.
23
+ <!-- /html-only -->
24
+ <!-- react-only -->
25
+ # Render to Video Tutorial (React)
26
+
27
+ Build a React composition and export it as MP4 video directly in the browser using WebCodecs.
28
+ <!-- /react-only -->
29
+
30
+ ## Prerequisites
31
+
32
+ <!-- html-only -->
33
+ Browser must support WebCodecs API (Chrome 94+, Edge 94+, Safari 16.4+). Check support:
34
+
35
+ ```javascript
36
+ const supported = 'VideoEncoder' in window && 'VideoDecoder' in window;
37
+ ```
38
+ <!-- /html-only -->
39
+ <!-- react-only -->
40
+ - React project with `@editframe/react` installed
41
+ - Browser supporting WebCodecs API (Chrome 94+, Edge 94+, Safari 16.4+)
42
+ - `TimelineRoot` wrapper (required for rendering)
43
+ <!-- /react-only -->
44
+
45
+ ## Step 1: Create a Composition
46
+
47
+ <!-- html-only -->
48
+ Start with a basic composition that you want to render:
49
+
50
+ ```html live
51
+ <ef-timegroup id="myComposition" mode="sequence" class="w-[1280px] h-[720px] bg-gradient-to-br from-purple-900 to-blue-900">
52
+ <!-- Scene 1: Title -->
53
+ <ef-timegroup mode="fixed" duration="3s" class="absolute w-full h-full flex items-center justify-center">
54
+ <ef-text duration="3s" class="text-white text-6xl font-bold animate-fade-in">
55
+ Welcome
56
+ </ef-text>
57
+ </ef-timegroup>
58
+
59
+ <!-- Scene 2: Content -->
60
+ <ef-timegroup mode="fixed" duration="4s" class="absolute w-full h-full flex items-center justify-center">
61
+ <ef-text duration="4s" class="text-white text-4xl">
62
+ This will be exported as video
63
+ </ef-text>
64
+ </ef-timegroup>
65
+ </ef-timegroup>
66
+ ```
67
+ <!-- /html-only -->
68
+ <!-- react-only -->
69
+ Build your video composition as a React component:
70
+
71
+ ```tsx
72
+ import { Timegroup, Text } from "@editframe/react";
73
+
74
+ export const MyVideo = () => {
75
+ return (
76
+ <Timegroup
77
+ mode="sequence"
78
+ className="w-[1280px] h-[720px] bg-gradient-to-br from-purple-900 to-blue-900"
79
+ >
80
+ {/* Scene 1: Title */}
81
+ <Timegroup
82
+ mode="fixed"
83
+ duration="3s"
84
+ className="absolute w-full h-full flex items-center justify-center"
85
+ >
86
+ <Text duration="3s" className="text-white text-6xl font-bold">
87
+ Welcome
88
+ </Text>
89
+ </Timegroup>
90
+
91
+ {/* Scene 2: Content */}
92
+ <Timegroup
93
+ mode="fixed"
94
+ duration="4s"
95
+ className="absolute w-full h-full flex items-center justify-center"
96
+ >
97
+ <Text duration="4s" className="text-white text-4xl">
98
+ This will be exported as video
99
+ </Text>
100
+ </Timegroup>
101
+ </Timegroup>
102
+ );
103
+ };
104
+ ```
105
+ <!-- /react-only -->
106
+
107
+ ## Step 2: <!-- html-only -->Add Export Button<!-- /html-only --><!-- react-only -->Get Timegroup Reference<!-- /react-only -->
108
+
109
+ <!-- html-only -->
110
+ Create UI for triggering the export:
111
+
112
+ ```html
113
+ <button id="exportBtn" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
114
+ Export Video
115
+ </button>
116
+
117
+ <div id="progressContainer" class="mt-4 hidden">
118
+ <div class="flex justify-between text-sm text-gray-300 mb-2">
119
+ <span id="progressText">Preparing...</span>
120
+ <span id="progressPercent">0%</span>
121
+ </div>
122
+ <div class="w-full bg-gray-700 rounded-full h-3">
123
+ <div id="progressBar" class="bg-blue-600 h-3 rounded-full transition-all" style="width: 0%"></div>
124
+ </div>
125
+ <div class="mt-2 text-xs text-gray-400">
126
+ <span id="timeInfo"></span>
127
+ </div>
128
+ </div>
129
+ ```
130
+ <!-- /html-only -->
131
+ <!-- react-only -->
132
+ Use a ref to access the underlying timegroup element:
133
+
134
+ ```tsx
135
+ import { useRef } from "react";
136
+ import { Timegroup, Text } from "@editframe/react";
137
+ import type { EFTimegroup } from "@editframe/elements";
138
+
139
+ export const MyVideo = () => {
140
+ const timegroupRef = useRef<EFTimegroup>(null);
141
+
142
+ const handleExport = async () => {
143
+ if (!timegroupRef.current) return;
144
+
145
+ await timegroupRef.current.renderToVideo({
146
+ fps: 30,
147
+ codec: "avc",
148
+ filename: "my-video.mp4"
149
+ });
150
+ };
151
+
152
+ return (
153
+ <>
154
+ <Timegroup
155
+ ref={timegroupRef}
156
+ mode="sequence"
157
+ className="w-[1280px] h-[720px] bg-black"
158
+ >
159
+ {/* composition content */}
160
+ </Timegroup>
161
+
162
+ <button onClick={handleExport}>Export Video</button>
163
+ </>
164
+ );
165
+ };
166
+ ```
167
+ <!-- /react-only -->
168
+
169
+ ## Step 3: <!-- html-only -->Implement Basic Render<!-- /html-only --><!-- react-only -->Add Progress State<!-- /react-only -->
170
+
171
+ <!-- html-only -->
172
+ Call `renderToVideo()` on the timegroup element:
173
+
174
+ ```javascript
175
+ const timegroup = document.getElementById('myComposition');
176
+ const exportBtn = document.getElementById('exportBtn');
177
+
178
+ exportBtn.addEventListener('click', async () => {
179
+ exportBtn.disabled = true;
180
+
181
+ try {
182
+ await timegroup.renderToVideo({
183
+ fps: 30,
184
+ codec: 'avc',
185
+ filename: 'my-video.mp4'
186
+ });
187
+
188
+ alert('Video exported successfully!');
189
+ } catch (error) {
190
+ alert('Export failed: ' + error.message);
191
+ } finally {
192
+ exportBtn.disabled = false;
193
+ }
194
+ });
195
+ ```
196
+
197
+ The video automatically downloads when rendering completes.
198
+
199
+ ## Step 4: Add Progress Tracking
200
+
201
+ Show real-time progress with the `onProgress` callback:
202
+
203
+ ```javascript
204
+ const progressContainer = document.getElementById('progressContainer');
205
+ const progressBar = document.getElementById('progressBar');
206
+ const progressText = document.getElementById('progressText');
207
+ const progressPercent = document.getElementById('progressPercent');
208
+ const timeInfo = document.getElementById('timeInfo');
209
+
210
+ exportBtn.addEventListener('click', async () => {
211
+ exportBtn.disabled = true;
212
+ progressContainer.classList.remove('hidden');
213
+
214
+ try {
215
+ await timegroup.renderToVideo({
216
+ fps: 30,
217
+ codec: 'avc',
218
+ filename: 'my-video.mp4',
219
+ onProgress: (progress) => {
220
+ // Update progress bar
221
+ const percent = Math.round(progress.progress * 100);
222
+ progressBar.style.width = `${percent}%`;
223
+ progressPercent.textContent = `${percent}%`;
224
+
225
+ // Update status text
226
+ progressText.textContent = `Rendering frame ${progress.currentFrame} of ${progress.totalFrames}`;
227
+
228
+ // Show timing information
229
+ const elapsed = Math.round(progress.elapsedMs / 1000);
230
+ const remaining = Math.round(progress.estimatedRemainingMs / 1000);
231
+ const speed = progress.speedMultiplier.toFixed(1);
232
+ timeInfo.textContent = `Elapsed: ${elapsed}s | Remaining: ${remaining}s | Speed: ${speed}x`;
233
+ }
234
+ });
235
+
236
+ progressText.textContent = 'Export complete!';
237
+ progressPercent.textContent = '100%';
238
+ } catch (error) {
239
+ progressText.textContent = 'Export failed: ' + error.message;
240
+ } finally {
241
+ exportBtn.disabled = false;
242
+ }
243
+ });
244
+ ```
245
+ <!-- /html-only -->
246
+ <!-- react-only -->
247
+ Track render progress with React state:
248
+
249
+ ```tsx
250
+ import { useState, useRef } from "react";
251
+ import { Timegroup, Text } from "@editframe/react";
252
+ import type { EFTimegroup } from "@editframe/elements";
253
+ import type { RenderProgress } from "@editframe/elements";
254
+
255
+ export const MyVideo = () => {
256
+ const timegroupRef = useRef<EFTimegroup>(null);
257
+ const [isRendering, setIsRendering] = useState(false);
258
+ const [progress, setProgress] = useState<RenderProgress | null>(null);
259
+ const [error, setError] = useState<string | null>(null);
260
+
261
+ const handleExport = async () => {
262
+ if (!timegroupRef.current) return;
263
+
264
+ setIsRendering(true);
265
+ setError(null);
266
+ setProgress(null);
267
+
268
+ try {
269
+ await timegroupRef.current.renderToVideo({
270
+ fps: 30,
271
+ codec: "avc",
272
+ filename: "my-video.mp4",
273
+ onProgress: (p) => {
274
+ setProgress(p);
275
+ }
276
+ });
277
+
278
+ setError(null);
279
+ } catch (err) {
280
+ setError(err instanceof Error ? err.message : "Export failed");
281
+ } finally {
282
+ setIsRendering(false);
283
+ }
284
+ };
285
+
286
+ return (
287
+ <>
288
+ <Timegroup
289
+ ref={timegroupRef}
290
+ mode="sequence"
291
+ className="w-[1280px] h-[720px] bg-gradient-to-br from-blue-600 to-purple-600"
292
+ >
293
+ <Timegroup
294
+ mode="fixed"
295
+ duration="3s"
296
+ className="absolute w-full h-full flex items-center justify-center"
297
+ >
298
+ <Text duration="3s" className="text-white text-6xl font-bold">
299
+ Editframe
300
+ </Text>
301
+ </Timegroup>
302
+ </Timegroup>
303
+
304
+ <div className="mt-4 space-y-4">
305
+ <button
306
+ onClick={handleExport}
307
+ disabled={isRendering}
308
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
309
+ >
310
+ {isRendering ? "Rendering..." : "Export Video"}
311
+ </button>
312
+
313
+ {progress && (
314
+ <div>
315
+ <div className="flex justify-between text-sm mb-2">
316
+ <span>
317
+ Frame {progress.currentFrame} of {progress.totalFrames}
318
+ </span>
319
+ <span>{Math.round(progress.progress * 100)}%</span>
320
+ </div>
321
+ <div className="w-full bg-gray-200 rounded-full h-3">
322
+ <div
323
+ className="bg-blue-600 h-3 rounded-full transition-all"
324
+ style={{ width: `${progress.progress * 100}%` }}
325
+ />
326
+ </div>
327
+ <div className="text-xs text-gray-600 mt-2">
328
+ Speed: {progress.speedMultiplier.toFixed(1)}x |
329
+ Remaining: {Math.round(progress.estimatedRemainingMs / 1000)}s
330
+ </div>
331
+ </div>
332
+ )}
333
+
334
+ {error && (
335
+ <div className="text-red-600 text-sm">Error: {error}</div>
336
+ )}
337
+ </div>
338
+ </>
339
+ );
340
+ };
341
+ ```
342
+
343
+ ## Step 4: Use useTimingInfo for Dynamic Content
344
+
345
+ Combine `useTimingInfo` with rendering for time-based animations:
346
+
347
+ ```tsx
348
+ import { Timegroup, Text, useTimingInfo } from "@editframe/react";
349
+
350
+ const FadeInScene = ({ children }: { children: React.ReactNode }) => {
351
+ const { ref, percentComplete } = useTimingInfo();
352
+
353
+ return (
354
+ <Timegroup
355
+ ref={ref}
356
+ mode="fixed"
357
+ duration="3s"
358
+ className="absolute w-full h-full flex items-center justify-center"
359
+ >
360
+ <div style={{ opacity: percentComplete }}>
361
+ {children}
362
+ </div>
363
+ </Timegroup>
364
+ );
365
+ };
366
+
367
+ export const AnimatedVideo = () => {
368
+ const timegroupRef = useRef<EFTimegroup>(null);
369
+
370
+ const handleExport = async () => {
371
+ if (!timegroupRef.current) return;
372
+
373
+ await timegroupRef.current.renderToVideo({
374
+ fps: 30,
375
+ codec: "avc",
376
+ filename: "animated-video.mp4"
377
+ });
378
+ };
379
+
380
+ return (
381
+ <>
382
+ <Timegroup
383
+ ref={timegroupRef}
384
+ mode="sequence"
385
+ className="w-[1280px] h-[720px] bg-black"
386
+ >
387
+ <FadeInScene>
388
+ <Text className="text-white text-6xl">Fades In</Text>
389
+ </FadeInScene>
390
+
391
+ <FadeInScene>
392
+ <Text className="text-white text-6xl">Also Fades In</Text>
393
+ </FadeInScene>
394
+ </Timegroup>
395
+
396
+ <button onClick={handleExport}>Export</button>
397
+ </>
398
+ );
399
+ };
400
+ ```
401
+ <!-- /react-only -->
402
+
403
+ ## Step 5: Add Codec Selection
404
+
405
+ Let users choose the codec based on browser support:
406
+
407
+ <!-- html-only -->
408
+ ```html
409
+ <select id="codecSelect" class="px-4 py-2 bg-gray-700 text-white rounded">
410
+ <option value="avc">H.264 (Best Compatibility)</option>
411
+ <option value="hevc">H.265 (Better Compression)</option>
412
+ <option value="vp9">VP9 (Open Codec)</option>
413
+ <option value="av1">AV1 (Best Quality)</option>
414
+ </select>
415
+ ```
416
+
417
+ ```javascript
418
+ const codecSelect = document.getElementById('codecSelect');
419
+
420
+ // Check codec support on load
421
+ const codecs = ['avc', 'hevc', 'vp9', 'av1'];
422
+ codecs.forEach(async (codec) => {
423
+ const config = {
424
+ codec: codec === 'avc' ? 'avc1.42E01E' :
425
+ codec === 'hevc' ? 'hvc1.1.6.L93.B0' :
426
+ codec === 'vp9' ? 'vp09.00.10.08' : 'av01.0.05M.08',
427
+ width: 1280,
428
+ height: 720,
429
+ bitrate: 5_000_000,
430
+ framerate: 30
431
+ };
432
+
433
+ const supported = await VideoEncoder.isConfigSupported(config);
434
+ if (!supported.supported) {
435
+ const option = codecSelect.querySelector(`option[value="${codec}"]`);
436
+ option.disabled = true;
437
+ option.textContent += ' (Not Supported)';
438
+ }
439
+ });
440
+
441
+ // Use selected codec when rendering
442
+ exportBtn.addEventListener('click', async () => {
443
+ const codec = codecSelect.value;
444
+ // ... rest of render code
445
+ await timegroup.renderToVideo({
446
+ fps: 30,
447
+ codec: codec,
448
+ filename: 'my-video.mp4',
449
+ onProgress: (progress) => { /* ... */ }
450
+ });
451
+ });
452
+ ```
453
+ <!-- /html-only -->
454
+ <!-- react-only -->
455
+ ```tsx
456
+ import { useState, useRef } from "react";
457
+ import { Timegroup, Text } from "@editframe/react";
458
+ import type { EFTimegroup } from "@editframe/elements";
459
+
460
+ type Codec = "avc" | "hevc" | "vp9" | "av1";
461
+
462
+ export const MyVideo = () => {
463
+ const timegroupRef = useRef<EFTimegroup>(null);
464
+ const [codec, setCodec] = useState<Codec>("avc");
465
+ const [isRendering, setIsRendering] = useState(false);
466
+
467
+ const handleExport = async () => {
468
+ if (!timegroupRef.current) return;
469
+
470
+ setIsRendering(true);
471
+ try {
472
+ await timegroupRef.current.renderToVideo({
473
+ fps: 30,
474
+ codec,
475
+ filename: `video-${codec}.mp4`
476
+ });
477
+ } finally {
478
+ setIsRendering(false);
479
+ }
480
+ };
481
+
482
+ return (
483
+ <>
484
+ <Timegroup
485
+ ref={timegroupRef}
486
+ mode="sequence"
487
+ className="w-[1280px] h-[720px] bg-black"
488
+ >
489
+ {/* composition */}
490
+ </Timegroup>
491
+
492
+ <div className="flex gap-4 mt-4">
493
+ <select
494
+ value={codec}
495
+ onChange={(e) => setCodec(e.target.value as Codec)}
496
+ className="px-4 py-2 border rounded"
497
+ >
498
+ <option value="avc">H.264 (Best Compatibility)</option>
499
+ <option value="hevc">H.265 (Better Compression)</option>
500
+ <option value="vp9">VP9 (Open Codec)</option>
501
+ <option value="av1">AV1 (Best Quality)</option>
502
+ </select>
503
+
504
+ <button onClick={handleExport} disabled={isRendering}>
505
+ {isRendering ? "Rendering..." : "Export Video"}
506
+ </button>
507
+ </div>
508
+ </>
509
+ );
510
+ };
511
+ ```
512
+ <!-- /react-only -->
513
+
514
+ ## Step 6: Include Audio
515
+
516
+ Audio from <!-- html-only -->`ef-video` and `ef-audio`<!-- /html-only --><!-- react-only -->Video and Audio<!-- /react-only --> elements is automatically mixed:
517
+
518
+ <!-- html-only -->
519
+ ```html
520
+ <ef-timegroup id="compositionWithAudio" mode="sequence" class="w-[1280px] h-[720px]">
521
+ <ef-timegroup mode="fixed" duration="5s" class="absolute w-full h-full">
522
+ <ef-video src="/assets/clip.mp4" class="size-full object-cover"></ef-video>
523
+ <ef-audio src="/assets/music.mp3" volume="0.3"></ef-audio>
524
+ </ef-timegroup>
525
+ </ef-timegroup>
526
+ ```
527
+
528
+ ```javascript
529
+ await timegroup.renderToVideo({
530
+ fps: 30,
531
+ codec: 'avc',
532
+ includeAudio: true, // Default: true
533
+ audioBitrate: 192_000, // 192 kbps for high quality
534
+ preferredAudioCodecs: ['opus', 'aac'], // Preference order
535
+ filename: 'video-with-audio.mp4'
536
+ });
537
+ ```
538
+ <!-- /html-only -->
539
+ <!-- react-only -->
540
+ ```tsx
541
+ import { Timegroup, Video, Audio, Text } from "@editframe/react";
542
+
543
+ export const VideoWithAudio = () => {
544
+ const timegroupRef = useRef<EFTimegroup>(null);
545
+
546
+ const handleExport = async () => {
547
+ if (!timegroupRef.current) return;
548
+
549
+ await timegroupRef.current.renderToVideo({
550
+ fps: 30,
551
+ codec: "avc",
552
+ includeAudio: true, // Default: true
553
+ audioBitrate: 192_000, // High quality (192 kbps)
554
+ preferredAudioCodecs: ["opus", "aac"],
555
+ filename: "video-with-audio.mp4"
556
+ });
557
+ };
558
+
559
+ return (
560
+ <>
561
+ <Timegroup
562
+ ref={timegroupRef}
563
+ mode="fixed"
564
+ duration="10s"
565
+ className="w-[1280px] h-[720px] bg-black"
566
+ >
567
+ <Video src="/assets/clip.mp4" className="size-full object-cover" />
568
+ <Audio src="/assets/music.mp3" volume={0.3} />
569
+ <Text className="absolute bottom-8 text-white text-2xl">
570
+ With Background Music
571
+ </Text>
572
+ </Timegroup>
573
+
574
+ <button onClick={handleExport}>Export with Audio</button>
575
+ </>
576
+ );
577
+ };
578
+ ```
579
+ <!-- /react-only -->
580
+
581
+ ## Step 7: Add Cancel Support
582
+
583
+ Allow users to abort long renders:
584
+
585
+ <!-- html-only -->
586
+ ```html
587
+ <button id="cancelBtn" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 hidden">
588
+ Cancel Render
589
+ </button>
590
+ ```
591
+
592
+ ```javascript
593
+ let abortController = null;
594
+ const cancelBtn = document.getElementById('cancelBtn');
595
+
596
+ exportBtn.addEventListener('click', async () => {
597
+ exportBtn.disabled = true;
598
+ cancelBtn.classList.remove('hidden');
599
+ progressContainer.classList.remove('hidden');
600
+
601
+ abortController = new AbortController();
602
+
603
+ try {
604
+ await timegroup.renderToVideo({
605
+ fps: 30,
606
+ codec: 'avc',
607
+ filename: 'my-video.mp4',
608
+ signal: abortController.signal,
609
+ onProgress: (progress) => {
610
+ // Update progress UI
611
+ const percent = Math.round(progress.progress * 100);
612
+ progressBar.style.width = `${percent}%`;
613
+ progressPercent.textContent = `${percent}%`;
614
+ }
615
+ });
616
+
617
+ progressText.textContent = 'Export complete!';
618
+ } catch (error) {
619
+ if (error.name === 'RenderCancelledError') {
620
+ progressText.textContent = 'Render cancelled by user';
621
+ } else {
622
+ progressText.textContent = 'Export failed: ' + error.message;
623
+ }
624
+ } finally {
625
+ exportBtn.disabled = false;
626
+ cancelBtn.classList.add('hidden');
627
+ abortController = null;
628
+ }
629
+ });
630
+
631
+ cancelBtn.addEventListener('click', () => {
632
+ if (abortController) {
633
+ abortController.abort();
634
+ }
635
+ });
636
+ ```
637
+ <!-- /html-only -->
638
+ <!-- react-only -->
639
+ Allow users to abort renders with AbortController:
640
+
641
+ ```tsx
642
+ import { useState, useRef } from "react";
643
+ import { Timegroup, Text } from "@editframe/react";
644
+ import type { EFTimegroup } from "@editframe/elements";
645
+
646
+ export const CancellableExport = () => {
647
+ const timegroupRef = useRef<EFTimegroup>(null);
648
+ const abortControllerRef = useRef<AbortController | null>(null);
649
+ const [isRendering, setIsRendering] = useState(false);
650
+ const [progress, setProgress] = useState(0);
651
+
652
+ const handleExport = async () => {
653
+ if (!timegroupRef.current) return;
654
+
655
+ setIsRendering(true);
656
+ setProgress(0);
657
+
658
+ abortControllerRef.current = new AbortController();
659
+
660
+ try {
661
+ await timegroupRef.current.renderToVideo({
662
+ fps: 30,
663
+ codec: "avc",
664
+ filename: "my-video.mp4",
665
+ signal: abortControllerRef.current.signal,
666
+ onProgress: (p) => {
667
+ setProgress(p.progress * 100);
668
+ }
669
+ });
670
+
671
+ alert("Export complete!");
672
+ } catch (error) {
673
+ if (error instanceof Error && error.name === "RenderCancelledError") {
674
+ alert("Render cancelled");
675
+ } else {
676
+ alert("Export failed");
677
+ }
678
+ } finally {
679
+ setIsRendering(false);
680
+ abortControllerRef.current = null;
681
+ }
682
+ };
683
+
684
+ const handleCancel = () => {
685
+ abortControllerRef.current?.abort();
686
+ };
687
+
688
+ return (
689
+ <>
690
+ <Timegroup
691
+ ref={timegroupRef}
692
+ mode="sequence"
693
+ className="w-[1280px] h-[720px] bg-black"
694
+ >
695
+ {/* composition */}
696
+ </Timegroup>
697
+
698
+ <div className="flex gap-4 mt-4">
699
+ <button onClick={handleExport} disabled={isRendering}>
700
+ Export Video
701
+ </button>
702
+
703
+ {isRendering && (
704
+ <>
705
+ <button onClick={handleCancel} className="bg-red-600">
706
+ Cancel
707
+ </button>
708
+ <div className="flex-1">
709
+ <div className="text-sm mb-1">{Math.round(progress)}%</div>
710
+ <div className="w-full bg-gray-200 rounded-full h-2">
711
+ <div
712
+ className="bg-blue-600 h-2 rounded-full"
713
+ style={{ width: `${progress}%` }}
714
+ />
715
+ </div>
716
+ </div>
717
+ </>
718
+ )}
719
+ </div>
720
+ </>
721
+ );
722
+ };
723
+ ```
724
+ <!-- /react-only -->
725
+
726
+ ## Complete Example
727
+
728
+ <!-- html-only -->
729
+ ```html live
730
+ <ef-timegroup id="finalComposition" mode="sequence" class="w-[1280px] h-[720px] bg-black">
731
+ <ef-timegroup mode="fixed" duration="3s" class="absolute w-full h-full bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center">
732
+ <ef-text duration="3s" class="text-white text-6xl font-bold">Editframe</ef-text>
733
+ </ef-timegroup>
734
+ <ef-timegroup mode="fixed" duration="3s" class="absolute w-full h-full bg-gradient-to-br from-purple-600 to-pink-600 flex items-center justify-center">
735
+ <ef-text duration="3s" class="text-white text-4xl">Export to Video</ef-text>
736
+ </ef-timegroup>
737
+ </ef-timegroup>
738
+
739
+ <div class="mt-4 space-y-4">
740
+ <div class="flex gap-4">
741
+ <button id="finalExportBtn" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
742
+ Export Video
743
+ </button>
744
+ <button id="finalCancelBtn" class="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 hidden">
745
+ Cancel
746
+ </button>
747
+ <select id="finalCodecSelect" class="px-4 py-2 bg-gray-700 text-white rounded">
748
+ <option value="avc">H.264</option>
749
+ <option value="vp9">VP9</option>
750
+ <option value="av1">AV1</option>
751
+ </select>
752
+ </div>
753
+
754
+ <div id="finalProgressContainer" class="hidden">
755
+ <div class="flex justify-between text-sm mb-2">
756
+ <span id="finalProgressText">Preparing...</span>
757
+ <span id="finalProgressPercent">0%</span>
758
+ </div>
759
+ <div class="w-full bg-gray-700 rounded-full h-3">
760
+ <div id="finalProgressBar" class="bg-blue-600 h-3 rounded-full transition-all" style="width: 0%"></div>
761
+ </div>
762
+ <div class="mt-2 text-xs text-gray-400" id="finalTimeInfo"></div>
763
+ </div>
764
+ </div>
765
+
766
+ <script type="module">
767
+ const timegroup = document.getElementById('finalComposition');
768
+ const exportBtn = document.getElementById('finalExportBtn');
769
+ const cancelBtn = document.getElementById('finalCancelBtn');
770
+ const codecSelect = document.getElementById('finalCodecSelect');
771
+ const progressContainer = document.getElementById('finalProgressContainer');
772
+ const progressBar = document.getElementById('finalProgressBar');
773
+ const progressText = document.getElementById('finalProgressText');
774
+ const progressPercent = document.getElementById('finalProgressPercent');
775
+ const timeInfo = document.getElementById('finalTimeInfo');
776
+
777
+ let abortController = null;
778
+
779
+ exportBtn.addEventListener('click', async () => {
780
+ exportBtn.disabled = true;
781
+ cancelBtn.classList.remove('hidden');
782
+ progressContainer.classList.remove('hidden');
783
+
784
+ abortController = new AbortController();
785
+
786
+ try {
787
+ await timegroup.renderToVideo({
788
+ fps: 30,
789
+ codec: codecSelect.value,
790
+ bitrate: 5_000_000,
791
+ filename: 'editframe-export.mp4',
792
+ signal: abortController.signal,
793
+ onProgress: (progress) => {
794
+ const percent = Math.round(progress.progress * 100);
795
+ progressBar.style.width = `${percent}%`;
796
+ progressPercent.textContent = `${percent}%`;
797
+ progressText.textContent = `Rendering frame ${progress.currentFrame}/${progress.totalFrames}`;
798
+
799
+ const elapsed = Math.round(progress.elapsedMs / 1000);
800
+ const remaining = Math.round(progress.estimatedRemainingMs / 1000);
801
+ timeInfo.textContent = `${elapsed}s elapsed | ${remaining}s remaining | ${progress.speedMultiplier.toFixed(1)}x speed`;
802
+ }
803
+ });
804
+
805
+ progressText.textContent = 'Export complete! Video downloaded.';
806
+ progressPercent.textContent = '100%';
807
+ } catch (error) {
808
+ if (error.name === 'RenderCancelledError') {
809
+ progressText.textContent = 'Render cancelled';
810
+ } else {
811
+ progressText.textContent = `Error: ${error.message}`;
812
+ }
813
+ } finally {
814
+ exportBtn.disabled = false;
815
+ cancelBtn.classList.add('hidden');
816
+ abortController = null;
817
+ }
818
+ });
819
+
820
+ cancelBtn.addEventListener('click', () => {
821
+ if (abortController) abortController.abort();
822
+ });
823
+ </script>
824
+ ```
825
+ <!-- /html-only -->
826
+ <!-- react-only -->
827
+ ```tsx
828
+ import { useState, useRef } from "react";
829
+ import { Timegroup, Text, useTimingInfo } from "@editframe/react";
830
+ import type { EFTimegroup, RenderProgress } from "@editframe/elements";
831
+
832
+ type Codec = "avc" | "hevc" | "vp9" | "av1";
833
+
834
+ const AnimatedScene = ({ children }: { children: React.ReactNode }) => {
835
+ const { ref, percentComplete } = useTimingInfo();
836
+
837
+ return (
838
+ <Timegroup
839
+ ref={ref}
840
+ mode="fixed"
841
+ duration="3s"
842
+ className="absolute w-full h-full flex items-center justify-center"
843
+ >
844
+ <div style={{ opacity: percentComplete, transform: `scale(${0.5 + percentComplete * 0.5})` }}>
845
+ {children}
846
+ </div>
847
+ </Timegroup>
848
+ );
849
+ };
850
+
851
+ export const CompleteExportExample = () => {
852
+ const timegroupRef = useRef<EFTimegroup>(null);
853
+ const abortControllerRef = useRef<AbortController | null>(null);
854
+
855
+ const [codec, setCodec] = useState<Codec>("avc");
856
+ const [isRendering, setIsRendering] = useState(false);
857
+ const [progress, setProgress] = useState<RenderProgress | null>(null);
858
+ const [error, setError] = useState<string | null>(null);
859
+
860
+ const handleExport = async () => {
861
+ if (!timegroupRef.current) return;
862
+
863
+ setIsRendering(true);
864
+ setError(null);
865
+ setProgress(null);
866
+
867
+ abortControllerRef.current = new AbortController();
868
+
869
+ try {
870
+ await timegroupRef.current.renderToVideo({
871
+ fps: 30,
872
+ codec,
873
+ bitrate: 5_000_000,
874
+ filename: `my-video-${codec}.mp4`,
875
+ signal: abortControllerRef.current.signal,
876
+ onProgress: (p) => setProgress(p)
877
+ });
878
+
879
+ alert("Video exported successfully!");
880
+ } catch (err) {
881
+ if (err instanceof Error && err.name === "RenderCancelledError") {
882
+ setError("Render cancelled by user");
883
+ } else {
884
+ setError(err instanceof Error ? err.message : "Export failed");
885
+ }
886
+ } finally {
887
+ setIsRendering(false);
888
+ abortControllerRef.current = null;
889
+ }
890
+ };
891
+
892
+ const handleCancel = () => {
893
+ abortControllerRef.current?.abort();
894
+ };
895
+
896
+ const percent = progress ? Math.round(progress.progress * 100) : 0;
897
+
898
+ return (
899
+ <div className="space-y-4">
900
+ <Timegroup
901
+ ref={timegroupRef}
902
+ mode="sequence"
903
+ className="w-[1280px] h-[720px] bg-gradient-to-br from-blue-600 to-purple-600"
904
+ >
905
+ <AnimatedScene>
906
+ <Text className="text-white text-6xl font-bold">Editframe</Text>
907
+ </AnimatedScene>
908
+
909
+ <AnimatedScene>
910
+ <Text className="text-white text-4xl">Export to Video</Text>
911
+ </AnimatedScene>
912
+
913
+ <AnimatedScene>
914
+ <Text className="text-white text-4xl">React + WebCodecs</Text>
915
+ </AnimatedScene>
916
+ </Timegroup>
917
+
918
+ <div className="flex gap-4 items-center">
919
+ <select
920
+ value={codec}
921
+ onChange={(e) => setCodec(e.target.value as Codec)}
922
+ disabled={isRendering}
923
+ className="px-4 py-2 border rounded"
924
+ >
925
+ <option value="avc">H.264</option>
926
+ <option value="hevc">H.265</option>
927
+ <option value="vp9">VP9</option>
928
+ <option value="av1">AV1</option>
929
+ </select>
930
+
931
+ <button
932
+ onClick={handleExport}
933
+ disabled={isRendering}
934
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
935
+ >
936
+ {isRendering ? "Rendering..." : "Export Video"}
937
+ </button>
938
+
939
+ {isRendering && (
940
+ <button
941
+ onClick={handleCancel}
942
+ className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700"
943
+ >
944
+ Cancel
945
+ </button>
946
+ )}
947
+ </div>
948
+
949
+ {progress && (
950
+ <div className="space-y-2">
951
+ <div className="flex justify-between text-sm">
952
+ <span>Frame {progress.currentFrame} of {progress.totalFrames}</span>
953
+ <span>{percent}%</span>
954
+ </div>
955
+ <div className="w-full bg-gray-200 rounded-full h-3">
956
+ <div
957
+ className="bg-blue-600 h-3 rounded-full transition-all"
958
+ style={{ width: `${percent}%` }}
959
+ />
960
+ </div>
961
+ <div className="text-xs text-gray-600">
962
+ Elapsed: {Math.round(progress.elapsedMs / 1000)}s |
963
+ Remaining: {Math.round(progress.estimatedRemainingMs / 1000)}s |
964
+ Speed: {progress.speedMultiplier.toFixed(1)}x
965
+ </div>
966
+ </div>
967
+ )}
968
+
969
+ {error && (
970
+ <div className="text-red-600 text-sm p-3 bg-red-50 rounded">
971
+ {error}
972
+ </div>
973
+ )}
974
+ </div>
975
+ );
976
+ };
977
+ ```
978
+ <!-- /react-only -->
979
+
980
+ ## Advanced Options
981
+
982
+ ### High-Resolution Export
983
+
984
+ <!-- html-only -->
985
+ Render at higher resolution than the composition:
986
+
987
+ ```javascript
988
+ await timegroup.renderToVideo({
989
+ scale: 2, // 2x resolution (2560x1440 from 1280x720)
990
+ bitrate: 10_000_000, // Increase bitrate for quality
991
+ filename: 'high-res.mp4'
992
+ });
993
+ ```
994
+ <!-- /html-only -->
995
+ <!-- react-only -->
996
+ ```tsx
997
+ await timegroupRef.current.renderToVideo({
998
+ scale: 2, // 2x resolution
999
+ bitrate: 10_000_000, // Higher bitrate for quality
1000
+ filename: "high-res.mp4"
1001
+ });
1002
+ ```
1003
+ <!-- /react-only -->
1004
+
1005
+ ### Partial Export
1006
+
1007
+ <!-- html-only -->
1008
+ Export only a portion of the composition:
1009
+
1010
+ ```javascript
1011
+ await timegroup.renderToVideo({
1012
+ fromMs: 2000, // Start at 2 seconds
1013
+ toMs: 8000, // End at 8 seconds
1014
+ filename: 'clip.mp4'
1015
+ });
1016
+ ```
1017
+ <!-- /html-only -->
1018
+ <!-- react-only -->
1019
+ ```tsx
1020
+ await timegroupRef.current.renderToVideo({
1021
+ fromMs: 2000, // Start at 2 seconds
1022
+ toMs: 8000, // End at 8 seconds
1023
+ filename: "clip.mp4"
1024
+ });
1025
+ ```
1026
+ <!-- /react-only -->
1027
+
1028
+ ### Programmatic <!-- html-only -->Access<!-- /html-only --><!-- react-only -->Buffer Access<!-- /react-only -->
1029
+
1030
+ <!-- html-only -->
1031
+ Get the video as a buffer instead of downloading:
1032
+
1033
+ ```javascript
1034
+ const videoBuffer = await timegroup.renderToVideo({
1035
+ returnBuffer: true,
1036
+ filename: 'video.mp4'
1037
+ });
1038
+
1039
+ // Upload to server
1040
+ const formData = new FormData();
1041
+ formData.append('video', new Blob([videoBuffer], { type: 'video/mp4' }));
1042
+ await fetch('/api/upload', { method: 'POST', body: formData });
1043
+ ```
1044
+ <!-- /html-only -->
1045
+ <!-- react-only -->
1046
+ ```tsx
1047
+ const videoBuffer = await timegroupRef.current.renderToVideo({
1048
+ returnBuffer: true,
1049
+ filename: "video.mp4"
1050
+ });
1051
+
1052
+ // Upload to server
1053
+ const formData = new FormData();
1054
+ formData.append("video", new Blob([videoBuffer], { type: "video/mp4" }));
1055
+ await fetch("/api/upload", { method: "POST", body: formData });
1056
+ ```
1057
+ <!-- /react-only -->
1058
+
1059
+ <!-- react-only -->
1060
+ ## TypeScript Types
1061
+
1062
+ Import types for proper typing:
1063
+
1064
+ ```tsx
1065
+ import type {
1066
+ EFTimegroup,
1067
+ RenderProgress,
1068
+ RenderToVideoOptions
1069
+ } from "@editframe/elements";
1070
+
1071
+ const options: RenderToVideoOptions = {
1072
+ fps: 60,
1073
+ codec: "avc",
1074
+ bitrate: 8_000_000,
1075
+ onProgress: (progress: RenderProgress) => {
1076
+ console.log(progress.progress);
1077
+ }
1078
+ };
1079
+ ```
1080
+
1081
+ ## Important Notes
1082
+
1083
+ 1. **TimelineRoot Required**: Always wrap your composition with `TimelineRoot` for rendering to work correctly with React state and hooks
1084
+ 2. **Refs for Access**: Use refs to access the underlying `EFTimegroup` element
1085
+ 3. **State Management**: React state updates work normally during rendering thanks to `TimelineRoot`
1086
+ 4. **Hook Support**: `useTimingInfo` and other hooks work in render clones
1087
+ <!-- /react-only -->
1088
+
1089
+ ## Next Steps
1090
+
1091
+ <!-- html-only -->
1092
+ - See [render-api.md](references/render-api.md) for all options
1093
+ - See [render-strategies.md](references/render-strategies.md) for choosing between browser, CLI, and cloud rendering
1094
+ - See the `editframe-cli` skill for server-side rendering
1095
+ <!-- /html-only -->
1096
+ <!-- react-only -->
1097
+ - See [hooks.md](references/hooks.md) for `useTimingInfo` and other hooks
1098
+ - See [timegroup.md](references/timegroup.md) for composition structure
1099
+ - See [timeline-root.md](references/timeline-root.md) for why it's required
1100
+ - See the `editframe-cli` skill for server-side rendering
1101
+ <!-- /react-only -->