frameyard 0.6.0 → 0.8.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/GLOSSARY.md +40 -16
  3. package/README.md +64 -55
  4. package/dist/assets/process.js.map +1 -1
  5. package/dist/assets/provenance.js.map +1 -1
  6. package/dist/cli.js +255 -59
  7. package/dist/cli.js.map +1 -1
  8. package/dist/composition/attributes.d.ts.map +1 -1
  9. package/dist/composition/descriptor.d.ts +17 -3
  10. package/dist/composition/descriptor.d.ts.map +1 -1
  11. package/dist/composition/descriptor.js +51 -28
  12. package/dist/composition/descriptor.js.map +1 -1
  13. package/dist/composition/diagnostics.js.map +1 -1
  14. package/dist/composition/format.d.ts +12 -0
  15. package/dist/composition/format.d.ts.map +1 -0
  16. package/dist/composition/format.js +28 -0
  17. package/dist/composition/format.js.map +1 -0
  18. package/dist/composition/index.d.ts +2 -1
  19. package/dist/composition/index.d.ts.map +1 -1
  20. package/dist/composition/index.js +1 -0
  21. package/dist/composition/index.js.map +1 -1
  22. package/dist/contracts.d.ts +55 -6
  23. package/dist/contracts.d.ts.map +1 -1
  24. package/dist/contracts.js +3 -0
  25. package/dist/contracts.js.map +1 -1
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/doctor.js +79 -0
  28. package/dist/doctor.js.map +1 -1
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/html/document-adapter.d.ts +3 -3
  33. package/dist/html/document-adapter.d.ts.map +1 -1
  34. package/dist/html/document-adapter.js +1 -1
  35. package/dist/html/document-adapter.js.map +1 -1
  36. package/dist/html/index.d.ts +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/parse.d.ts +10 -2
  39. package/dist/html/parse.d.ts.map +1 -1
  40. package/dist/html/parse.js +14 -5
  41. package/dist/html/parse.js.map +1 -1
  42. package/dist/html/validate.d.ts +12 -1
  43. package/dist/html/validate.d.ts.map +1 -1
  44. package/dist/html/validate.js +233 -61
  45. package/dist/html/validate.js.map +1 -1
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/preview/main.d.ts +2 -2
  51. package/dist/preview/main.d.ts.map +1 -1
  52. package/dist/preview/main.js +47 -19
  53. package/dist/preview/main.js.map +1 -1
  54. package/dist/preview/timeline-data.d.ts +5 -1
  55. package/dist/preview/timeline-data.d.ts.map +1 -1
  56. package/dist/preview/timeline-host.d.ts +2 -0
  57. package/dist/preview/timeline-host.d.ts.map +1 -1
  58. package/dist/preview/timeline-host.js +31 -10
  59. package/dist/preview/timeline-host.js.map +1 -1
  60. package/dist/preview/timeline-main.js +12 -7
  61. package/dist/preview/timeline-main.js.map +1 -1
  62. package/dist/project.d.ts +3 -5
  63. package/dist/project.d.ts.map +1 -1
  64. package/dist/project.js.map +1 -1
  65. package/dist/renderer/browser.d.ts +2 -2
  66. package/dist/renderer/browser.d.ts.map +1 -1
  67. package/dist/renderer/browser.js +10 -4
  68. package/dist/renderer/browser.js.map +1 -1
  69. package/dist/renderer/ffmpeg.d.ts +69 -2
  70. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  71. package/dist/renderer/ffmpeg.js +381 -69
  72. package/dist/renderer/ffmpeg.js.map +1 -1
  73. package/dist/renderer/index.d.ts +66 -5
  74. package/dist/renderer/index.d.ts.map +1 -1
  75. package/dist/renderer/index.js +226 -51
  76. package/dist/renderer/index.js.map +1 -1
  77. package/dist/renderer/timeline.d.ts +41 -4
  78. package/dist/renderer/timeline.d.ts.map +1 -1
  79. package/dist/renderer/timeline.js +242 -39
  80. package/dist/renderer/timeline.js.map +1 -1
  81. package/dist/runtime/host.d.ts.map +1 -1
  82. package/dist/runtime/host.js +155 -31
  83. package/dist/runtime/host.js.map +1 -1
  84. package/dist/runtime/media.d.ts.map +1 -1
  85. package/dist/runtime/media.js +3 -6
  86. package/dist/runtime/media.js.map +1 -1
  87. package/dist/runtime/video.d.ts +13 -0
  88. package/dist/runtime/video.d.ts.map +1 -0
  89. package/dist/runtime/video.js +41 -0
  90. package/dist/runtime/video.js.map +1 -0
  91. package/dist/scaffold.d.ts.map +1 -1
  92. package/dist/scaffold.js +107 -103
  93. package/dist/scaffold.js.map +1 -1
  94. package/dist/server/composition-html.d.ts +13 -7
  95. package/dist/server/composition-html.d.ts.map +1 -1
  96. package/dist/server/composition-html.js +65 -16
  97. package/dist/server/composition-html.js.map +1 -1
  98. package/dist/server/preview-html.js +1 -1
  99. package/dist/server/server.d.ts.map +1 -1
  100. package/dist/server/server.js +26 -6
  101. package/dist/server/server.js.map +1 -1
  102. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  103. package/dist/server/timeline-preview-data.js +11 -0
  104. package/dist/server/timeline-preview-data.js.map +1 -1
  105. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  106. package/dist/server/timeline-preview-html.js +2 -1
  107. package/dist/server/timeline-preview-html.js.map +1 -1
  108. package/dist/timeline/plan.js.map +1 -1
  109. package/dist/timeline/resolve.d.ts +4 -3
  110. package/dist/timeline/resolve.d.ts.map +1 -1
  111. package/dist/timeline/resolve.js +14 -4
  112. package/dist/timeline/resolve.js.map +1 -1
  113. package/dist/timeline/schema.d.ts +7 -0
  114. package/dist/timeline/schema.d.ts.map +1 -1
  115. package/dist/timeline/schema.js +50 -0
  116. package/dist/timeline/schema.js.map +1 -1
  117. package/dist/transcript.js.map +1 -1
  118. package/dist/version.d.ts +1 -1
  119. package/dist/version.js +1 -1
  120. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  121. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  122. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  123. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  124. package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
  125. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
  126. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  127. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  128. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  129. package/docs/migration-0.4.md +3 -1
  130. package/docs/migration-0.8.md +163 -0
  131. package/docs/preview-testing.md +60 -0
  132. package/docs/v0.1-design.md +52 -42
  133. package/examples/blend/README.md +106 -0
  134. package/examples/blend/assets/ink.mp4 +0 -0
  135. package/examples/blend/compositions/aurora.html +55 -0
  136. package/examples/blend/compositions/backdrop.html +74 -0
  137. package/examples/blend/compositions/labels.html +149 -0
  138. package/examples/blend/scripts/make-ink.sh +26 -0
  139. package/examples/blend/timelines/blend.timeline.json +155 -0
  140. package/examples/blend/video.config.js +11 -0
  141. package/examples/demo/compositions/demo.html +243 -251
  142. package/examples/static/README.md +17 -0
  143. package/examples/static/compositions/still.html +37 -0
  144. package/examples/static/timelines/still.timeline.json +13 -0
  145. package/examples/static/video.config.js +9 -0
  146. package/examples/video/compositions/video-mp4.html +349 -357
  147. package/examples/video/compositions/video-overlay.html +337 -345
  148. package/examples/video/compositions/video.html +349 -357
  149. package/examples/webgl/compositions/webgl.html +205 -213
  150. package/package.json +6 -3
  151. package/skills/frameyard/README.md +7 -5
  152. package/skills/frameyard/SKILL.md +17 -13
  153. package/skills/frameyard/references/common-mistakes.md +30 -6
  154. package/skills/frameyard/references/controllers.md +5 -4
  155. package/skills/frameyard/references/deterministic-authoring.md +42 -36
  156. package/skills/frameyard/references/diagnostics.md +11 -3
  157. package/skills/frameyard/references/failure-recovery.md +28 -7
  158. package/skills/frameyard/references/json-results.md +58 -7
  159. package/skills/frameyard/references/subtitles-karaoke.md +2 -2
  160. package/skills/frameyard/references/timelines.md +21 -15
  161. package/skills/frameyard/scripts/frameyard.mjs +20 -3
@@ -0,0 +1,60 @@
1
+ # Preview testing
2
+
3
+ The preview runs the production composition and timeline hosts in a browser. Use
4
+ the embedded hosts for deterministic Playwright probes; they are same-origin and
5
+ expose the same seek/settle contract the renderer uses.
6
+
7
+ ## Composition preview
8
+
9
+ - Page: `/__frameyard/?composition=<id>`.
10
+ - Stage: the `#stage-frame` iframe.
11
+ - API: `window.__frameyardPreview` exposes `load(entry, props?)`, `seek(frame)`,
12
+ `settle()`, and `dispose()`.
13
+ - `seek()` is synchronous. Call `await settle()` before reading pixels; `load()`
14
+ resets the host, so apply props through it.
15
+ - A static composition reports `metadata: { width, height, static: true }`.
16
+ `seek()` is a no-op, `settle()` resolves, and the page disables the transport.
17
+ Wait for the static composition's own marker instead of `waitForFrame()`,
18
+ which expects a controller-updated `#box`.
19
+
20
+ ## Timeline preview
21
+
22
+ - Page: `/__frameyard/timeline/<id>`; add `?__fy-embed=1` for the embedded host.
23
+ - API: `window.__fyTimeline` exposes `ready`, `format`, `durationInFrames`,
24
+ `load()`, `seek(frame)`, `settle()`, and `dispose()`.
25
+ - Order: `await ready`, `await load()`, `await seek(frame)`, `await settle()`.
26
+ - The non-embedded page owns transport and audio; only the embedded host exposes
27
+ `window.__fyTimeline`.
28
+ - A static composition source carries `static: true` and no
29
+ `durationInFrames`. The host loads the layer once and skips per-frame seek;
30
+ the layer remains visible while the timeline playhead moves.
31
+
32
+ ## Waiting for a frame
33
+
34
+ - `settle()` waits for video presentation through `requestVideoFrameCallback`
35
+ and then for two animation frames for composition and iframe paint. Do not add
36
+ `waitForTimeout` to assertions.
37
+ - For a composition layer, wait for a controller-updated marker. The shared
38
+ helper `test/helpers/preview.ts` exports `waitForFrame(page, frame)`.
39
+ - For a video layer, sample the rendered stage after `settle()`; do not trust
40
+ `video.currentTime` alone. `test/timeline-preview-runtime.test.ts` covers this.
41
+ - The same helper exports `openEmbeddedTimeline`, `loadTimeline`, and
42
+ `seekTimeline`.
43
+
44
+ ## Verification
45
+
46
+ For preview-only UI changes, run `pnpm check:preview`. It runs the static checks
47
+ and then builds before running `test/preview-runtime.test.ts`,
48
+ `test/timeline-preview.test.ts`, and `test/timeline-preview-runtime.test.ts`.
49
+ Use the full `pnpm check` when the change reaches shared contracts, the runtime,
50
+ the renderer, or the CLI, and before pushing or publishing.
51
+
52
+ ## Notes
53
+
54
+ - Preview iframes and the composition stage pin `color-scheme: light` to match
55
+ the production capture context. A dark inherited scheme makes Chromium paint a
56
+ white base behind transparent compositions.
57
+ - A running `dev` server keeps the `dist/` modules it loaded. Restart it after
58
+ `pnpm build`.
59
+ - The timeline preview host is not the production compositor; compare pixels at
60
+ the capture boundary, not encoded output.
@@ -2,9 +2,9 @@
2
2
 
3
3
  **Status:** Accepted
4
4
  **Initial acceptance:** 2026-10-27
5
- **Last design revision:** 2026-10-31
5
+ **Last design revision:** 2026-11-03
6
6
 
7
- This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.6. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
7
+ This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.8 and extended with static clip blend modes. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
8
8
 
9
9
  ## 1. Product contract
10
10
 
@@ -16,7 +16,7 @@ The target use cases are product demos, social clips, animated diagrams, data vi
16
16
 
17
17
  The core success criterion is an end-to-end example in which a developer can:
18
18
 
19
- 1. Create reusable, 1920×1080 HTML compositions with fixed metadata.
19
+ 1. Create reusable HTML compositions with a fixed duration and a format they either declare or inherit from the project.
20
20
  2. Animate them with HTML, CSS, SVG, and, where needed, a seek-safe controller for Canvas 2D and WebGL.
21
21
  3. Supply JSON props and synchronized audio.
22
22
  4. Assemble compositions and local media into a timeline with tracks, clips, trims, layers, and nested timelines.
@@ -30,18 +30,20 @@ The agent-enabled success criterion additionally requires an agent using the off
30
30
 
31
31
  ### Project
32
32
 
33
- - Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition document or in external modules.
33
+ - Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition fragment or in external modules.
34
34
  - `video.config.js` explicitly maps project IDs to source files. There is no directory scanning. It has three optional maps:
35
35
  - `format`: the project's fixed `width`, `height`, and `fps`.
36
- - `compositions`: IDs mapped to `.html` composition documents.
36
+ - `compositions`: IDs mapped to `.html` composition fragments.
37
37
  - `timelines`: IDs mapped to JSON timeline documents.
38
- - `format` is required when a project has timelines. Every timeline uses it. A composition used in a timeline must share the project frame rate exactly; a different size is fitted and transformed by the clip. A standalone composition render keeps using the composition's own metadata.
38
+ - `format` is required when a project has timelines. Every timeline uses it. It also fills `width`, `height`, and `fps` for animated compositions that omit them; a declared value always wins. A composition that declares an `fps` must share the project frame rate exactly; a different size is fitted and transformed by the clip. A composition that declares neither `duration-in-frames` nor `fps` is static; it uses width and height, declared or inherited, and ignores the inherited fps. A composition that declares no dimensions in a project without `format` cannot render and fails with `COMPOSITION_FORMAT_UNRESOLVED`.
39
39
  - Composition and timeline IDs share one namespace so `render <id>`, `check <id>`, and `still <id>` are unambiguous. The config rejects duplicates.
40
40
  - A project may contain any number of compositions and timelines.
41
41
 
42
42
  ### Composition
43
43
 
44
- - A composition is a full HTML document with one `<fy-composition>` root. The root declares fixed `width`, `height`, `fps`, and `duration-in-frames` metadata. Props cannot change this metadata.
44
+ - A composition is a partial HTML fragment with one top-level `<fy-composition>` root. The framework wraps it in the document shell, import map, sizing style, and host runtime at serve and render time. `width`, `height`, and `fps` are optional and inherit from the project `format` when absent; declared values win. `width` and `height` are declared together or not at all. Props cannot change metadata.
45
+ - A composition that declares neither `duration-in-frames` nor `fps` is static: it has no frame clock, is captured once in its initial state, and renders to a PNG. Its controller may `mount()` and validate props, but `update()` is not called and frame-state variables are not set. CSS animations and transitions are paused at time 0 with a warning; `data-fy-anim-*`, `<video>`, `<audio>`, and `<fy-transcript>` are errors. `fps` without `duration-in-frames` is an error. See [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).
46
+ - Props blocks, the controller, and import maps are top-level siblings of the root. Top-level `<style>` and `<link rel="stylesheet">` elements are hoisted into the generated head; styles inside the root stay in the body. A fragment must not contain `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, `<meta>`, or the reserved `data-fy-host` attribute.
45
47
  - Default props are a JSON block, `<script type="application/json" data-fy="props">`. A load's JSON props merge shallowly over the defaults, are validated as JSON, deep-frozen, and fixed for that load.
46
48
  - Props bind into the document through explicit `data-fy-*` attributes: `data-fy-text`, `data-fy-attr-*`, `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat`. Primitive top-level props are also exposed as `--fy-prop-<name>` CSS custom properties. Bindings apply once per load; props are immutable.
47
49
  - CSS and the Web Animations API are the animation language. `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in composition frames.
@@ -59,13 +61,14 @@ The agent-enabled success criterion additionally requires an agent using the off
59
61
  - a composition, identified by its project ID;
60
62
  - a nested timeline, identified by its project ID;
61
63
  - a local video, image, or audio asset.
62
- - A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings.
63
- - A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity and transform apply to the whole nested result. Cycles are rejected statically. Timeline clips do not override props of compositions inside the nested timeline in 0.6.
64
- - Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images are static and require an explicit duration. A video or audio clip that exceeds its source range is an error; an omitted duration uses the remaining source.
65
- - Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box. No keyframes, blend modes, masks, filters, or other effects in 0.6.
66
- - Visual clips contribute their audio to the timeline mix unless muted: composition audio, embedded video audio, and nested timeline audio. Audio-only clips live on audio tracks and may source audio or video files. Per-clip and per-audio-track gain and mute are supported. There are no linked A/V pairs; a J/L cut is expressed by muting a video clip and placing a separate audio clip with a different source in-point.
64
+ - A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings. A clip whose composition source is static behaves like an image: it must declare `duration`, must use `sourceIn: 0`, has no intrinsic source duration, and contributes no audio or transcripts. The renderer captures one transparent PNG per composition/props pair and reuses it across segments.
65
+ - A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity, transform, and blend mode apply to the whole nested result. A blend mode inside a nested timeline sees only that timeline's own backdrop; a transparent nested timeline does not see the parent's layers, and a declared nested background is part of the group. Cycles are rejected statically. Timeline clips do not override props of compositions inside the nested timeline.
66
+ - Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images and static composition clips require an explicit duration. A video or audio clip that exceeds its source range is an error; an omitted duration uses the remaining source.
67
+ - Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box.
68
+ - Visual clips may declare a static `blend` mode. It uses CSS `mix-blend-mode` semantics against the accumulated backdrop of all lower layers and the timeline background, respects the clip's source alpha, and defaults to `normal`. The supported modes are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`; `hue`, `saturation`, `color`, and `luminosity` are rejected. There are no keyframes, masks, filters, or other effects.
69
+ - Visual clips contribute their audio to the timeline mix unless muted: composition audio, embedded video audio, and nested timeline audio. Static compositions contribute no audio; clip audio settings on a static composition are ignored with a warning. Audio-only clips live on audio tracks and may source audio or video files. Per-clip and per-audio-track gain and mute are supported. There are no linked A/V pairs; a J/L cut is expressed by muting a video clip and placing a separate audio clip with a different source in-point.
67
70
  - A timeline's duration is derived from the latest clip end. An explicit `durationInFrames` may extend it but not cut clips. A timeline with no clips is invalid; an audio-only timeline is valid and renders black video with audio.
68
- - A timeline background is transparent by default. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
71
+ - A timeline background is transparent by default. Blend modes see the accumulated backdrop including this background. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
69
72
  - The preview provides a read-only timeline view: tracks, clips, playhead, program monitor, and diagnostics. It is not a visual editor.
70
73
 
71
74
  ## 3. Deterministic execution
@@ -78,8 +81,9 @@ The timeline frame is the master clock. A composition's frame is derived from th
78
81
  - JavaScript animation runs in the controller's `update()`, which must be synchronous and seek-safe. It cannot depend on previously visited frames.
79
82
  - Declarative bindings apply once per load, before the controller mounts. Controller-created DOM is the controller's responsibility.
80
83
  - Independent `requestAnimationFrame` loops, timers, CSS transitions, wall-clock reads, and unseeded randomness are not supported animation drivers.
81
- - Timeline clips use static transforms. Timeline-level animation, transitions, effects, and keyframes are not part of 0.6.
84
+ - Timeline clips use static transforms and static blend modes. Timeline-level animation, transitions, and other effects are not supported.
82
85
  - Preview and production seek composition clips explicitly. The timeline compositor is deterministic given the captured clip frames, media frames, clip settings, and segment boundaries.
86
+ - The resolved format—declared width, height, and fps combined with the project format—is part of the immutable frame environment, alongside props and loaded assets. Layouts that use viewport units or percentages therefore depend on the resolved format by design; preview and production always resolve the same values.
83
87
  - Assets, remote data, images, fonts, generated audio, transcripts, and media are resolved before frame production. For a segmented timeline render, each segment's composition and nested-timeline clips are loaded and settled before that segment's frames are captured; no frame production depends on a new network request. Network is blocked during capture.
84
88
  - AI model inference is never part of the loading phase, controller lifecycle, or frame production.
85
89
  - The baseline supports DOM, CSS, SVG, Canvas 2D, WebGL, and frame-seeked embedded video. Canvas 2D and WebGL are drawn from the current frame by the controller.
@@ -90,39 +94,39 @@ The timeline frame is the master clock. A composition's frame is derived from th
90
94
 
91
95
  ### Reproducibility guarantee
92
96
 
93
- For the same timeline or composition revision, props, assets, segment plan, and frame under a pinned runtime/browser/FFmpeg environment, the captured composition frames and the deterministic timeline composite are identical. The final MP4 is not promised to be bit-identical: encoded bytes depend on the encoder and its runtime.
97
+ For the same timeline or composition revision, props, assets, segment plan, and frame under a pinned runtime/browser/FFmpeg environment, the captured composition frames and the deterministic timeline composite are identical. The final output is not promised to be bit-identical: encoded bytes depend on the encoder and its runtime.
94
98
 
95
99
  Preview and production share composition behavior and capture composition frames the same way, but the timeline preview composites layers in Chromium while production composites them with FFmpeg. Small differences in scaling, alpha, and video color conversion are expected and do not violate clip-capture parity.
96
100
 
97
101
  ## 4. Preview and production rendering
98
102
 
99
- Preview and production execute the same composition documents and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured.
103
+ Preview and production execute the same composition sources and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured.
100
104
 
101
105
  The preview provides:
102
106
 
103
- - Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics.
104
- - Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics.
107
+ - Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics. A static composition loads once and disables the transport.
108
+ - Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics. A static composition layer stays loaded and is not seeked per frame.
105
109
  - A bounded pool of active composition and nested-timeline clips. It loads clips around the playhead instead of preloading a whole long timeline.
106
110
 
107
- The preview server serves each composition HTML document at its project path and injects the framework import map and host runtime. It serves timeline documents and a timeline preview host. The timeline preview host drives active composition/nested-timeline clips, seeks them, and exposes a timeline API to the preview. Preview audio is mixed by the preview page from the resolved timeline audio clips.
111
+ The preview server serves each composition fragment at its project path, wraps it in the fixed document shell, and injects the framework import map and host runtime. It serves timeline documents and a timeline preview host. The timeline preview host drives active composition/nested-timeline clips, seeks them, and exposes a timeline API to the preview. Preview audio is mixed by the preview page from the resolved timeline audio clips.
108
112
 
109
- Production composition rendering is unchanged: one Chromium session per composition, explicit frame seeks, PNG capture, and FFmpeg encoding.
113
+ Production composition rendering uses one Chromium session per composition. An animated composition seeks every frame, captures PNGs, and encodes them with FFmpeg. A static composition loads once, captures one PNG, and publishes it without FFmpeg or an encoder.
110
114
 
111
115
  Production timeline rendering is segmented and parallel:
112
116
 
113
117
  1. Run static timeline validation and resolve the render plan.
114
118
  2. Split the timeline into segments at active-layer changes or a maximum segment duration. Each segment has a fixed set of visual layers and a fixed filter graph.
115
- 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background.
116
- 4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264.
119
+ 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers.
120
+ 4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
117
121
  5. Join the ordered segments with a stream copy. Segment boundaries never change frame order or timing.
118
122
  6. Mix the timeline audio once with FFmpeg and mux it into the final output.
119
123
  7. Publish the output atomically; delete scratch intermediates on success and failure.
120
124
 
121
- The default timeline worker count is 8, configurable up to the machine's capacity. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
125
+ The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
122
126
 
123
- The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial MP4, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
127
+ The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial output, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
124
128
 
125
- `check` and `still` use the same clip capture and timeline composite path for a requested frame. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. `still` captures one frame as PNG without encoding a video.
129
+ `check` and `still` use the same clip capture and timeline composite path for requested frames. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. A static composition has one state and is inspected once. `still` captures one or more requested frames as PNGs without encoding a video; a static composition exposes only frame 0.
126
130
 
127
131
  ## 5. Audio and output
128
132
 
@@ -133,13 +137,15 @@ A timeline has one audio mix. It combines:
133
137
  - timeline audio clips on audio tracks;
134
138
  - audio from nested timeline clips, recursively.
135
139
 
136
- Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final MP4. Preview mixes the same resolved clips in the browser for monitoring; it is not the production source of truth.
140
+ Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final output. Preview mixes the same resolved clips in the browser for monitoring; it is not the production source of truth.
137
141
 
138
- Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
142
+ Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition URL.
139
143
 
140
144
  Generated speech behaves like any other audio clip. Frameyard does not automatically time-stretch it, change timeline duration, or regenerate it to fit. `check` reports clip durations and overrun warnings so users and agents can revise text, timing, or the timeline deliberately.
141
145
 
142
- 0.6 produces portable MP4 files with H.264 video and AAC audio. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. Single-frame PNG output through `still` is a debugging artifact, not general image-sequence support.
146
+ Timelines and animated compositions produce MP4, MOV, MKV, or WebM files. The default is MP4 with H.264 video and AAC audio; HEVC and VP9 are opt-in, and WebM is VP9 with Opus. HEVC in MP4 and MOV keeps the `hvc1` sample entry and is not playable in browsers without platform HEVC decoding. Frameyard exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. A static composition produces one PNG and uses no encoder.
147
+
148
+ PNG output through `still`, including a bounded set of inspection frames, is a debugging artifact, not general image-sequence support. A standalone static composition renders to one PNG; this is the composition's production artifact, not an image sequence.
143
149
 
144
150
  A render fails immediately on an uncaught exception, unhandled rejection, required-asset failure, invalid composition output, segment encoding failure, or scratch failure. Diagnostics identify the timeline, clip, segment, and, where applicable, the frame. The CLI exits nonzero, cleans temporary files, and never reports a partial video as successful. Existing output files are not overwritten unless explicitly requested.
145
151
 
@@ -149,16 +155,16 @@ Project and composition commands are:
149
155
 
150
156
  - `init`: scaffold a timeline-first project with a composition, a one-clip timeline, a project format, an agent guide, a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts.
151
157
  - `dev`: start the preview with composition and timeline modes.
152
- - `render <id> [--out video.mp4]`: render a timeline or a composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. The default output is `out/<id>.mp4`.
153
- - `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding.
154
- - `still <id> --frame <n>`: capture one requested frame as PNG through the clip capture and timeline composite path.
158
+ - `render <id> [--out video]`: render a timeline, an animated composition, or a static composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default; WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded; `auto` is the default and probes NVENC then Quick Sync before falling back to software. The output extension must match the container. The default output is `out/<id>.<ext>`. A static composition writes `out/<id>.png` and rejects `--container`, `--codec`, `--encoder`, and `--concurrency`. The JSON result is a discriminated union with `kind: "video"` or `kind: "image"`.
159
+ - `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding. A static composition is loaded and inspected once and returns `kind: "static"` with `framesChecked: 1`.
160
+ - `still <id> --frame <spec>`: capture one or more requested frames as PNGs through the clip capture and timeline composite path. A static composition exposes exactly frame 0. `<spec>` is a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`); duplicates are removed preserving the first occurrence, input order is preserved, and one invocation captures at most 32 unique frames. There is no step or sequence mode. A single frame keeps the existing `--out <file.png>` behavior; multiple frames use `--out <directory>` and write `still-<id>-<frame>.png` files, defaulting to the current working directory. Frames and output conflicts are validated before Chromium, all frames are captured to temporary files before publication, and a failed capture publishes no files. JSON results add `frames` and keep the singular `frame` and `path` only when one frame was captured. See [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
155
161
  - `doctor`: inspect core requirements and optional integrations without repairing or downloading anything.
156
162
 
157
- `check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe document reader. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
163
+ `check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe reader with document and fragment scopes. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
158
164
 
159
165
  Local asset-production commands are `tts` and `transcribe`. Every non-interactive command supports `--json` with stable result and error schemas. Structured results go to stdout; human diagnostics and inference progress go to stderr.
160
166
 
161
- The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It injects an import map and composition host into composition entries and an inline controller through a virtual module URL. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering.
167
+ The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It wraps each configured composition fragment in the fixed document shell, injects the framework import map and composition host, and serves an inline controller through a virtual module URL. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering.
162
168
 
163
169
  Frameyard uses one repository and one publishable npm package with four internal layers:
164
170
 
@@ -255,20 +261,20 @@ Release documentation must identify the exact `qwentts.cpp` and `whisper.cpp` re
255
261
  Tests prioritize public behavior and end-to-end evidence over implementation-only unit coverage:
256
262
 
257
263
  - HTML compositions validate metadata, default props, bindings, conditionals, repeats, and controller declarations, including inline/external parity and the exactly-one-controller rule.
258
- - Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium.
264
+ - Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium. It resolves declared format values against the project format and reports an unresolved format to the command rather than the document. A composition with neither time attribute is static; `fps` without `duration-in-frames`, `data-fy-anim-*`, media, and transcripts on a static composition are errors.
259
265
  - Timeline JSON validation catches schema, track, clip, reference, cycle, frame-rate, overlap, bounds, media, transform, audio, and props errors before Chromium.
260
266
  - Diagnostics carry stable codes and source locations or JSON paths; JSON results expose the composition or timeline entry, clips, tracks, and top-level diagnostics.
261
267
  - Sequential, individual, random-order, and parallel composition renders produce equivalent images; timeline segment workers produce ordered output and identical captured composition frames.
262
- - Determinism checks compare captured composition pixels or stills. The final H.264 bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
263
- - Preview and renderer use the same composition document and host behavior; timeline preview uses the same active clip hosts.
268
+ - Determinism checks compare captured composition pixels or stills. The final encoded bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
269
+ - Preview and renderer use the same composition source and host behavior; timeline preview uses the same active clip hosts.
264
270
  - CSS, controller JavaScript, SVG, Canvas, WebGL, embedded video, and audio frames seek correctly.
265
271
  - Frame-scheduled `data-fy-anim-*` animations and frame-state CSS variables produce the expected frames.
266
272
  - Timeline capture preserves alpha for transparent compositions; FFV1 intermediates decode pixel-identically to the captured frames.
267
- - Timeline segment encoding, ordered concatenation, global audio mix, and scratch cleanup produce the expected MP4 dimensions, frame count, duration, and audio stream.
273
+ - Timeline segment encoding, ordered concatenation, global audio mix, and scratch cleanup produce the expected output dimensions, frame count, duration, and audio stream.
268
274
  - Nested timelines preserve group opacity and transform semantics; cycles fail static validation.
269
275
  - Media clips trim and fit deterministically; images require durations; source overruns fail.
270
276
  - Props overrides for timeline composition clips merge over clip props and are reflected in the render.
271
- - `check` exercises every frame; `still` follows the clip capture and timeline composite path.
277
+ - `check` exercises every animated frame and inspects a static composition once; `still` follows the clip capture and timeline composite path. A static composition render captures one PNG; a timeline reuses one static image capture per props variant.
272
278
  - The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact output and clean scratch, documenting composition-heavy performance above 30 minutes.
273
279
  - Golden-frame tests cover representative composition and timeline visual behavior.
274
280
  - Normal CI uses fake `qwen-tts` and `whisper-cli` executables and small fixtures to verify arguments, artifacts, manifests, cancellation, and errors without model downloads or GPU hardware.
@@ -279,13 +285,13 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
279
285
 
280
286
  These are scope boundaries, not permanent rejections:
281
287
 
282
- - Transitions, effects, keyframes, blend modes, masks, and audio fades
288
+ - Transitions, filters, keyframes, masks, and audio fades
283
289
  - Full visual timeline editing, drag-and-drop, clip handles, and undo/redo
284
290
  - Timeline-level variables and prop overrides for compositions inside nested timelines
285
291
  - Track solo and video-track visibility
286
292
  - Per-timeline formats and frame-rate conversion/retiming
287
293
  - Persistent clip caches; the segmented renderer cleans its intermediates
288
- - Hardware encoders, GPU compositing, and non-H.264 output
294
+ - GPU compositing and codecs beyond H.264, HEVC, and VP9
289
295
  - Hosted rendering, collaboration, and asset marketplaces
290
296
  - Cloud generative-AI services, text-to-video generation, and image generation
291
297
  - `qwentts.cpp` reference-audio voice cloning and VoiceDesign
@@ -301,8 +307,12 @@ These are scope boundaries, not permanent rejections:
301
307
  - Dynamic composition or timeline metadata derived from props
302
308
  - Live streams, remote media, and other visual sources that cannot be deterministically seeked
303
309
  - Dynamic per-frame asset loading or AI inference
304
- - Batch still capture; agents can run `still` once per representative frame
310
+ - Unbounded batch still capture; `still` accepts at most 32 inspection frames per invocation and is not an image-sequence exporter
305
311
  - Additional final-video formats, general image-sequence output, and arbitrary FFmpeg argument passthrough
312
+ - Audio, transcripts, or embedded video in a static composition; use a timeline
313
+ - A `--video` escape hatch for static composition renders; use a timeline image clip
314
+ - Exporting a fully static timeline as a PNG
315
+ - Recursive static optimization of nested timelines
306
316
  - Built-in TypeScript transpilation and npm dependency optimization
307
317
  - Rendering untrusted compositions
308
318
  - Bit-identical cross-platform encoded output or AI regeneration
@@ -310,4 +320,4 @@ These are scope boundaries, not permanent rejections:
310
320
 
311
321
  ## 13. Release posture
312
322
 
313
- Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md).
323
+ Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md). 0.7.0 adds delivery container, codec, and encoder selection, bounded multi-frame still inspection, and optional encoder probes in `doctor` without breaking the timeline or composition contracts; bounded still inspection is recorded in [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md). Static clip blend modes are recorded in [ADR 0014](./adr/0014-compose-timeline-clips-with-css-blend-modes.md). Optional composition `width`, `height`, and `fps` with project-format inheritance are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md). 0.8.0 changes compositions to HTML fragments, inherits their format from the project, and renders timeless compositions as still images; the contracts are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md), [ADR 0016](./adr/0016-author-compositions-as-html-fragments.md), and [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).
@@ -0,0 +1,106 @@
1
+ # Frameyard blend example
2
+
3
+ A 12-second, 1920×1080, 30 fps timeline that demonstrates every supported CSS
4
+ blend mode on composition and video clips. Four video tracks build one frame:
5
+
6
+ - `v1` **backdrop** — a static opaque HTML composition with a grid, concentric
7
+ rings, and a dark vignette. It omits the time metadata and all dimensions, so
8
+ it inherits the project format and is captured once at the timeline's size.
9
+ - `v2` **aurora** — a static transparent HTML composition with three blurred
10
+ color blobs. It declares its dimensions but no time metadata, so it is also
11
+ captured once. Its clip uses `blend: "screen"` for the full 12 seconds.
12
+ - `v3` **ink** — a local 1280×720 H.264 clip scaled to fill the project frame.
13
+ Twelve touching clips play one second each, cycling through all 12 supported
14
+ modes in timeline order.
15
+ - `v4` **labels** — an animated transparent HTML composition in normal mode. It
16
+ lists the modes and their time ranges, and draws a progress bar from the
17
+ `--fy-progress` frame-state variable, so it is evaluated per frame.
18
+
19
+ The mode sequence on `v3` is:
20
+
21
+ | Time | Mode |
22
+ | --- | --- |
23
+ | 0–1 s | `normal` |
24
+ | 1–2 s | `multiply` |
25
+ | 2–3 s | `screen` |
26
+ | 3–4 s | `overlay` |
27
+ | 4–5 s | `darken` |
28
+ | 5–6 s | `lighten` |
29
+ | 6–7 s | `color-dodge` |
30
+ | 7–8 s | `color-burn` |
31
+ | 8–9 s | `hard-light` |
32
+ | 9–10 s | `soft-light` |
33
+ | 10–11 s | `difference` |
34
+ | 11–12 s | `exclusion` |
35
+
36
+ The aurora is transparent, so its `screen` blends only where its blobs have
37
+ alpha. The video is opaque, so its whole scaled frame participates in the blend.
38
+ The labels layer stays in `normal` mode so the copy remains readable.
39
+
40
+ Blend modes are static per clip. The example uses consecutive clips on `v3` to
41
+ show every mode in one timeline; it does not animate `blend` itself. `hue`,
42
+ `saturation`, `color`, and `luminosity` are rejected because they need
43
+ cross-channel math and are not part of the supported set.
44
+
45
+ `backdrop` and `aurora` are static compositions: they declare no
46
+ `duration-in-frames` or `fps`, are captured once, and the same image is reused
47
+ for every timeline frame. `labels` remains animated because its progress bar
48
+ reads `--fy-progress`; making it static would freeze the bar.
49
+
50
+ Preview composites with Chromium `mix-blend-mode`; production uses FFmpeg's
51
+ `blend` filter plus an alpha mask. The preview stage stacks by track order and
52
+ is isolated from the preview chrome. Small scaling, alpha, and color differences
53
+ between preview and the final encode are expected.
54
+
55
+ ## Files
56
+
57
+ ```text
58
+ examples/blend/
59
+ compositions/backdrop.html opaque base composition
60
+ compositions/aurora.html transparent screen layer
61
+ compositions/labels.html transparent mode list and progress layer
62
+ timelines/blend.timeline.json
63
+ assets/ink.mp4 procedural 12-second source clip
64
+ scripts/make-ink.sh regenerate assets/ink.mp4
65
+ video.config.js
66
+ ```
67
+
68
+ The timeline lives in `timelines/blend.timeline.json`; the project format and
69
+ entries are fixed in `video.config.js`. `backdrop.html` is a static composition
70
+ that omits all dimensions and time metadata, so it inherits the project's
71
+ 1920×1080 size. `aurora.html` is a static composition that declares its
72
+ dimensions but no time metadata. `labels.html` is an animated composition and
73
+ keeps its `fps` and `duration-in-frames`.
74
+
75
+ ## Commands
76
+
77
+ From the repository root:
78
+
79
+ ```sh
80
+ pnpm build
81
+ node dist/cli.js check blend-timeline --project examples/blend
82
+ node dist/cli.js still blend-timeline --frame 15,45,75,105,135,165,195,225,255,285,315,345 --project examples/blend --out blend-stills
83
+ node dist/cli.js still blend-timeline --frame 285 --project examples/blend --out blend-soft-light.png
84
+ node dist/cli.js render blend-timeline --project examples/blend
85
+ node dist/cli.js dev --project examples/blend
86
+ ```
87
+
88
+ The batch still captures one representative frame per mode into `blend-stills/`
89
+ using the same clip capture and composite path as the render. `render` writes
90
+ `out/blend-timeline.mp4` inside the project by default; use `--out <file.mp4>`
91
+ to choose another path. `dev` lists the compositions and the timeline in the
92
+ preview.
93
+
94
+ ## Source clip
95
+
96
+ `assets/ink.mp4` is a 12-second, 1280×720, 30 fps rotating gradient with a
97
+ silent AAC track, generated by FFmpeg. Regenerate it with:
98
+
99
+ ```sh
100
+ bash examples/blend/scripts/make-ink.sh
101
+ ```
102
+
103
+ Pass an output path as the first argument to write elsewhere. Replace
104
+ `assets/ink.mp4` with any local, seekable MP4 or WebM that is at least 12 seconds
105
+ long; remote URLs and live streams are not supported. If you use a different
106
+ length, update the clip durations and source in-points in the timeline.
Binary file
@@ -0,0 +1,55 @@
1
+ <style>
2
+ html,
3
+ body,
4
+ fy-composition {
5
+ margin: 0;
6
+ width: 100%;
7
+ height: 100%;
8
+ background: transparent;
9
+ }
10
+ fy-composition {
11
+ display: block;
12
+ overflow: hidden;
13
+ }
14
+ .scene {
15
+ position: relative;
16
+ width: 100%;
17
+ height: 100%;
18
+ overflow: hidden;
19
+ isolation: isolate;
20
+ }
21
+ .blob {
22
+ position: absolute;
23
+ border-radius: 50%;
24
+ filter: blur(78px);
25
+ opacity: 0.9;
26
+ }
27
+ .pink {
28
+ left: 6%;
29
+ top: -18%;
30
+ width: 980px;
31
+ height: 980px;
32
+ background: radial-gradient(circle, rgba(255, 64, 164, 0.92), rgba(255, 64, 164, 0) 68%);
33
+ }
34
+ .blue {
35
+ right: -12%;
36
+ top: 4%;
37
+ width: 1180px;
38
+ height: 1180px;
39
+ background: radial-gradient(circle, rgba(46, 146, 255, 0.88), rgba(46, 146, 255, 0) 68%);
40
+ }
41
+ .amber {
42
+ left: 30%;
43
+ bottom: -30%;
44
+ width: 900px;
45
+ height: 900px;
46
+ background: radial-gradient(circle, rgba(255, 186, 72, 0.7), rgba(255, 186, 72, 0) 70%);
47
+ }
48
+ </style>
49
+ <fy-composition width="1920" height="1080">
50
+ <div class="scene">
51
+ <div class="blob pink"></div>
52
+ <div class="blob blue"></div>
53
+ <div class="blob amber"></div>
54
+ </div>
55
+ </fy-composition>
@@ -0,0 +1,74 @@
1
+ <style>
2
+ html,
3
+ body {
4
+ margin: 0;
5
+ width: 100%;
6
+ height: 100%;
7
+ background: #05070d;
8
+ }
9
+ fy-composition {
10
+ display: block;
11
+ width: 100%;
12
+ height: 100%;
13
+ overflow: hidden;
14
+ }
15
+ .scene {
16
+ position: relative;
17
+ width: 100%;
18
+ height: 100%;
19
+ overflow: hidden;
20
+ background:
21
+ radial-gradient(1200px 760px at 72% 34%, rgba(42, 104, 220, 0.5), transparent 62%),
22
+ radial-gradient(860px 620px at 18% 82%, rgba(255, 76, 140, 0.14), transparent 64%),
23
+ linear-gradient(145deg, #060a12 0%, #0b1424 52%, #05070d 100%);
24
+ }
25
+ .grid {
26
+ position: absolute;
27
+ inset: -1px;
28
+ background-image:
29
+ linear-gradient(rgba(150, 190, 255, 0.075) 1px, transparent 1px),
30
+ linear-gradient(90deg, rgba(150, 190, 255, 0.075) 1px, transparent 1px);
31
+ background-size: 72px 72px;
32
+ }
33
+ .fade {
34
+ position: absolute;
35
+ inset: 0;
36
+ background:
37
+ radial-gradient(ellipse at 58% 38%, transparent 18%, rgba(5, 7, 13, 0.55) 62%, rgba(5, 7, 13, 0.92) 100%),
38
+ linear-gradient(90deg, rgba(5, 7, 13, 0.5), transparent 38%, transparent 74%, rgba(5, 7, 13, 0.55));
39
+ }
40
+ .ring {
41
+ position: absolute;
42
+ left: 58%;
43
+ top: 44%;
44
+ width: 1060px;
45
+ height: 1060px;
46
+ border: 1px solid rgba(160, 205, 255, 0.18);
47
+ border-radius: 50%;
48
+ box-shadow:
49
+ 0 0 140px rgba(54, 118, 255, 0.16) inset,
50
+ 0 0 90px rgba(54, 118, 255, 0.08);
51
+ transform: translate(-50%, -50%);
52
+ }
53
+ .ring::before,
54
+ .ring::after {
55
+ content: "";
56
+ position: absolute;
57
+ border: 1px solid rgba(255, 118, 190, 0.13);
58
+ border-radius: 50%;
59
+ }
60
+ .ring::before {
61
+ inset: 138px;
62
+ }
63
+ .ring::after {
64
+ inset: 286px;
65
+ border-color: rgba(255, 179, 71, 0.12);
66
+ }
67
+ </style>
68
+ <fy-composition>
69
+ <div class="scene">
70
+ <div class="grid"></div>
71
+ <div class="ring"></div>
72
+ <div class="fade"></div>
73
+ </div>
74
+ </fy-composition>