frameyard 0.7.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 (138) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/GLOSSARY.md +40 -16
  3. package/README.md +54 -48
  4. package/dist/cli.js +23 -16
  5. package/dist/cli.js.map +1 -1
  6. package/dist/composition/descriptor.d.ts +17 -3
  7. package/dist/composition/descriptor.d.ts.map +1 -1
  8. package/dist/composition/descriptor.js +51 -28
  9. package/dist/composition/descriptor.js.map +1 -1
  10. package/dist/composition/format.d.ts +12 -0
  11. package/dist/composition/format.d.ts.map +1 -0
  12. package/dist/composition/format.js +28 -0
  13. package/dist/composition/format.js.map +1 -0
  14. package/dist/composition/index.d.ts +2 -1
  15. package/dist/composition/index.d.ts.map +1 -1
  16. package/dist/composition/index.js +1 -0
  17. package/dist/composition/index.js.map +1 -1
  18. package/dist/contracts.d.ts +55 -6
  19. package/dist/contracts.d.ts.map +1 -1
  20. package/dist/contracts.js +3 -0
  21. package/dist/contracts.js.map +1 -1
  22. package/dist/errors.d.ts +1 -1
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/html/document-adapter.d.ts +3 -3
  26. package/dist/html/document-adapter.d.ts.map +1 -1
  27. package/dist/html/document-adapter.js +1 -1
  28. package/dist/html/document-adapter.js.map +1 -1
  29. package/dist/html/index.d.ts +1 -1
  30. package/dist/html/index.d.ts.map +1 -1
  31. package/dist/html/parse.d.ts +10 -2
  32. package/dist/html/parse.d.ts.map +1 -1
  33. package/dist/html/parse.js +14 -5
  34. package/dist/html/parse.js.map +1 -1
  35. package/dist/html/validate.d.ts +12 -1
  36. package/dist/html/validate.d.ts.map +1 -1
  37. package/dist/html/validate.js +233 -61
  38. package/dist/html/validate.js.map +1 -1
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/preview/main.d.ts +2 -2
  44. package/dist/preview/main.d.ts.map +1 -1
  45. package/dist/preview/main.js +35 -14
  46. package/dist/preview/main.js.map +1 -1
  47. package/dist/preview/timeline-data.d.ts +5 -1
  48. package/dist/preview/timeline-data.d.ts.map +1 -1
  49. package/dist/preview/timeline-host.d.ts.map +1 -1
  50. package/dist/preview/timeline-host.js +10 -0
  51. package/dist/preview/timeline-host.js.map +1 -1
  52. package/dist/preview/timeline-main.js +3 -2
  53. package/dist/preview/timeline-main.js.map +1 -1
  54. package/dist/project.d.ts +3 -5
  55. package/dist/project.d.ts.map +1 -1
  56. package/dist/project.js.map +1 -1
  57. package/dist/renderer/browser.d.ts +2 -2
  58. package/dist/renderer/browser.d.ts.map +1 -1
  59. package/dist/renderer/browser.js +10 -4
  60. package/dist/renderer/browser.js.map +1 -1
  61. package/dist/renderer/ffmpeg.d.ts +3 -0
  62. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  63. package/dist/renderer/ffmpeg.js +122 -56
  64. package/dist/renderer/ffmpeg.js.map +1 -1
  65. package/dist/renderer/index.d.ts +40 -4
  66. package/dist/renderer/index.d.ts.map +1 -1
  67. package/dist/renderer/index.js +99 -24
  68. package/dist/renderer/index.js.map +1 -1
  69. package/dist/renderer/timeline.d.ts.map +1 -1
  70. package/dist/renderer/timeline.js +89 -0
  71. package/dist/renderer/timeline.js.map +1 -1
  72. package/dist/runtime/host.d.ts.map +1 -1
  73. package/dist/runtime/host.js +155 -31
  74. package/dist/runtime/host.js.map +1 -1
  75. package/dist/scaffold.d.ts.map +1 -1
  76. package/dist/scaffold.js +104 -101
  77. package/dist/scaffold.js.map +1 -1
  78. package/dist/server/composition-html.d.ts +13 -7
  79. package/dist/server/composition-html.d.ts.map +1 -1
  80. package/dist/server/composition-html.js +65 -16
  81. package/dist/server/composition-html.js.map +1 -1
  82. package/dist/server/server.d.ts.map +1 -1
  83. package/dist/server/server.js +26 -6
  84. package/dist/server/server.js.map +1 -1
  85. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  86. package/dist/server/timeline-preview-data.js +11 -0
  87. package/dist/server/timeline-preview-data.js.map +1 -1
  88. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  89. package/dist/server/timeline-preview-html.js +2 -1
  90. package/dist/server/timeline-preview-html.js.map +1 -1
  91. package/dist/timeline/resolve.d.ts +4 -3
  92. package/dist/timeline/resolve.d.ts.map +1 -1
  93. package/dist/timeline/resolve.js +14 -4
  94. package/dist/timeline/resolve.js.map +1 -1
  95. package/dist/timeline/schema.d.ts +7 -0
  96. package/dist/timeline/schema.d.ts.map +1 -1
  97. package/dist/timeline/schema.js +50 -0
  98. package/dist/timeline/schema.js.map +1 -1
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  102. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  103. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  104. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  105. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
  106. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  107. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  108. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  109. package/docs/migration-0.4.md +3 -1
  110. package/docs/migration-0.8.md +163 -0
  111. package/docs/preview-testing.md +7 -0
  112. package/docs/v0.1-design.md +42 -34
  113. package/examples/blend/README.md +106 -0
  114. package/examples/blend/assets/ink.mp4 +0 -0
  115. package/examples/blend/compositions/aurora.html +55 -0
  116. package/examples/blend/compositions/backdrop.html +74 -0
  117. package/examples/blend/compositions/labels.html +149 -0
  118. package/examples/blend/scripts/make-ink.sh +26 -0
  119. package/examples/blend/timelines/blend.timeline.json +155 -0
  120. package/examples/blend/video.config.js +11 -0
  121. package/examples/demo/compositions/demo.html +243 -251
  122. package/examples/static/README.md +17 -0
  123. package/examples/static/compositions/still.html +37 -0
  124. package/examples/static/timelines/still.timeline.json +13 -0
  125. package/examples/static/video.config.js +9 -0
  126. package/examples/video/compositions/video-mp4.html +349 -357
  127. package/examples/video/compositions/video-overlay.html +337 -345
  128. package/examples/video/compositions/video.html +349 -357
  129. package/examples/webgl/compositions/webgl.html +205 -213
  130. package/package.json +2 -1
  131. package/skills/frameyard/SKILL.md +15 -13
  132. package/skills/frameyard/references/common-mistakes.md +29 -6
  133. package/skills/frameyard/references/controllers.md +5 -4
  134. package/skills/frameyard/references/deterministic-authoring.md +41 -36
  135. package/skills/frameyard/references/diagnostics.md +11 -3
  136. package/skills/frameyard/references/failure-recovery.md +25 -5
  137. package/skills/frameyard/references/json-results.md +46 -4
  138. package/skills/frameyard/references/timelines.md +16 -12
@@ -1,6 +1,6 @@
1
1
  # Deterministic HTML authoring
2
2
 
3
- Frameyard compositions are full HTML documents. The preview server injects the framework import map and composition host; do not add a bundling step. Optional controllers are native JavaScript modules that import bare `frameyard/runtime`.
3
+ Frameyard compositions are partial HTML fragments. The preview server wraps them in the document shell and injects the framework import map and composition host; do not add a bundling step. Optional controllers are native JavaScript modules that import bare `frameyard/runtime`.
4
4
 
5
5
  ## Project configuration
6
6
 
@@ -14,42 +14,47 @@ export default {
14
14
  };
15
15
  ```
16
16
 
17
- There is no directory scanning. A new composition document is invisible until it is listed. Entries must be `.html` files; JavaScript composition modules are rejected.
17
+ There is no directory scanning. A new composition fragment is invisible until it is listed. Entries must be `.html` files; JavaScript composition modules are rejected.
18
18
 
19
19
  ## Composition shape
20
20
 
21
21
  ```html
22
- <!doctype html>
23
- <html>
24
- <head>
25
- <meta charset="utf-8">
26
- <script type="application/json" data-fy="props">
27
- { "title": "Hello", "accent": "#b8f36b" }
28
- </script>
29
- <style>
30
- @keyframes fade-up {
31
- from { opacity: 0; transform: translateY(24px); }
32
- to { opacity: 1; transform: translateY(0); }
33
- }
34
- #title { animation: fade-up 1s ease-out both; }
35
- </style>
36
- </head>
37
- <body>
38
- <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
39
- <h1 id="title"
40
- data-fy-text="title"
41
- data-fy-anim-start="0"
42
- data-fy-anim-duration="30"></h1>
43
- <div style="width: calc(var(--fy-progress) * 100%)"></div>
44
- </fy-composition>
45
- </body>
46
- </html>
22
+ <script type="application/json" data-fy="props">
23
+ { "title": "Hello", "accent": "#b8f36b" }
24
+ </script>
25
+ <style>
26
+ @keyframes fade-up {
27
+ from { opacity: 0; transform: translateY(24px); }
28
+ to { opacity: 1; transform: translateY(0); }
29
+ }
30
+ #title { animation: fade-up 1s ease-out both; }
31
+ </style>
32
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
33
+ <h1 id="title"
34
+ data-fy-text="title"
35
+ data-fy-anim-start="0"
36
+ data-fy-anim-duration="30"></h1>
37
+ <div style="width: calc(var(--fy-progress) * 100%)"></div>
38
+ </fy-composition>
47
39
  ```
48
40
 
49
- - `width`, `height`, `fps`, and `duration-in-frames` are fixed on the root.
50
- - The `data-fy="props"` block is a JSON object. External props merge shallowly over it and are deep-frozen.
51
- - There is exactly one `<fy-composition>` root, inside `<body>`.
52
- - A doctype is required. Unsupported document features such as `<base>`, refresh/CSP meta tags, `<iframe>`, `<object>`, and `<embed>` are rejected.
41
+ - `duration-in-frames` is required for an animated composition. `width`, `height`, and `fps` are optional: a missing value inherits the project `format`, and a declared value wins. Declare `width` and `height` together or not at all. A composition that declares neither `duration-in-frames` nor `fps` is static.
42
+ - The `data-fy="props"` block is a top-level JSON object. External props merge shallowly over it and are deep-frozen.
43
+ - There is exactly one `<fy-composition>` root, and it must be a top-level element in the fragment.
44
+ - Do not add `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`; Frameyard owns the shell. Props, 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.
45
+ - An optional top-level `<script type="importmap">` adds bare specifiers for controllers. `frameyard` and `frameyard/` specifiers are reserved.
46
+ - The reserved `data-fy-host` attribute and `data-fy="format"` block are framework-only. Unsupported features such as `<base>`, refresh/CSP meta tags, `<iframe>`, `<object>`, and `<embed>` are rejected.
47
+
48
+ ## Static compositions
49
+
50
+ A static composition declares neither `duration-in-frames` nor `fps`. It has no frame clock: Frameyard loads it, applies bindings, runs `mount()`, pauses CSS animations at time 0, and captures one PNG. `update()` is not called and `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames` are not set.
51
+
52
+ - Use `<img>`, CSS backgrounds, SVG, and canvas drawn once from `mount()`.
53
+ - Do not use `data-fy-anim-*`, `<video>`, `<audio>`, or `<fy-transcript>`; these are validation errors. Put media and captions on a timeline.
54
+ - Do not rely on `update()` or frame-state variables. A controller that exports `update()` loads with a `STATIC_UPDATE_IGNORED` warning.
55
+ - CSS animations and transitions are paused at 0 with a `STATIC_ANIMATION_PAUSED` warning; remove them for a predictable still.
56
+ - In a timeline, a static composition clip needs an explicit `duration` and `sourceIn: 0`.
57
+ - `render` writes a PNG and rejects video flags. Use a timeline image clip when the deliverable must be video.
53
58
 
54
59
  ## Bindings
55
60
 
@@ -126,7 +131,7 @@ External for a reusable or complex controller:
126
131
  - A validation-only controller may export just `validateProps(props)`.
127
132
  - Inline source is served through a virtual module URL, so relative imports and `import.meta.url` work like an external module. A literal `</script>` inside inline source must be escaped.
128
133
  - The controller is the only executable code. Other executable `<script>` elements and `on*` handlers are rejected.
129
- - Controller `src` resolves relative to the composition document. Media and transcript `src` values resolve from the project root.
134
+ - Controller `src` resolves relative to the composition URL. Media and transcript `src` values resolve from the project root.
130
135
  - Keep mutable state in module scope, never on `this`. Reset it in `dispose()`.
131
136
 
132
137
  ## Forbidden animation inputs
@@ -167,7 +172,7 @@ Do not preserve and accumulate WebGL drawing across frames unless that accumulat
167
172
 
168
173
  ## Embedded video and audio
169
174
 
170
- Declare local media in the composition document:
175
+ Declare local media in the composition fragment:
171
176
 
172
177
  ```html
173
178
  <video
@@ -210,7 +215,7 @@ Declare canonical transcript requirements with:
210
215
 
211
216
  Remote data, images, fonts, and other assets must resolve during the loading phase before frame production. The production browser blocks network requests after loading begins. A fetch from `update()` is a bug.
212
217
 
213
- Images declared in the document are awaited by the host. Static fonts should be loaded with `document.fonts.ready` or an explicit `FontFace` before frame production.
218
+ Images declared in the fragment are awaited by the host. Static fonts should be loaded with `document.fonts.ready` or an explicit `FontFace` before frame production.
214
219
 
215
220
  ## Validation and inspection
216
221
 
@@ -220,8 +225,8 @@ npx frameyard still hello --frame 42 --out hello-42.png --json
220
225
  npx frameyard still hello --frame 0,118-126 --out out/stills --json
221
226
  ```
222
227
 
223
- `check` runs static validation before Chromium, then evaluates every frame and validates audio and declared transcripts without capturing or encoding. Read the structured `diagnostics` array. Do not replace `check` with a single still. Inspect several representative frames in one `still` call with a range or list; the batch seeks sequentially in one session and is limited to 32 unique frames.
228
+ `check` runs static validation before Chromium, then evaluates every animated frame and validates audio and declared transcripts without capturing or encoding. A static composition is inspected once. Read the structured `diagnostics` array. Do not replace `check` with a single still. Inspect several representative frames in one `still` call with a range or list; the batch seeks sequentially in one session and is limited to 32 unique frames. A static composition exposes only frame 0.
224
229
 
225
230
  ## Metadata is fixed
226
231
 
227
- `width`, `height`, `fps`, and `duration-in-frames` cannot be derived from props. Choose a reusable resolution, frame rate, and duration; put variation in props. Frameyard does not change duration to fit audio.
232
+ `width`, `height`, `fps`, and `duration-in-frames` cannot be derived from props. An animated composition always declares `duration-in-frames`; a static composition declares neither time attribute. `width`, `height`, and `fps` are either declared or inherited from the project `format`, and the resolved values are fixed for the load. A composition that declares no dimensions in a project without `format` fails with `COMPOSITION_FORMAT_UNRESOLVED`. The resolved format is part of the immutable frame environment, so viewport units and percentage layout are valid but depend on it. Put variation in props. Frameyard does not change duration to fit audio.
@@ -8,12 +8,13 @@ Frameyard reports composition problems as structured diagnostics before and duri
8
8
 
9
9
  Static validation covers:
10
10
 
11
- - doctype, root count, metadata, and default-props JSON
11
+ - fragment structure, root count and placement, metadata (including `width`/`height` pairing), default-props JSON, and import maps
12
12
  - controller declaration and file existence
13
13
  - executable scripts, `on*` handlers, `javascript:` URLs, and unsupported visual elements
14
14
  - unknown `data-fy-*` attributes, binding paths, `if`/`repeat` placement, and animation attributes
15
15
  - audio/video/transcript references and ordinary image/stylesheet assets
16
16
  - visual content outside `<fy-composition>`
17
+ - document-level `<title>`/`<meta>` elements, the reserved `data-fy-host` attribute, and non-top-level props, controllers, or import maps
17
18
 
18
19
  ## Diagnostic shape
19
20
 
@@ -40,7 +41,10 @@ Static validation covers:
40
41
 
41
42
  | Code | Severity | Meaning |
42
43
  | --- | --- | --- |
43
- | `COMPOSITION_INVALID` | error | Missing/invalid doctype, root, metadata, or document structure |
44
+ | `COMPOSITION_INVALID` | error | Missing/invalid root, metadata, fragment structure, or reserved attributes |
45
+ | `COMPOSITION_DOCUMENT_SHELL` | error | A full document shell (`<!doctype>`, `<html>`, `<head>`, or `<body>`) is present; compositions are fragments |
46
+ | `IMPORT_MAP_INVALID` | error | Invalid import map placement, JSON, attributes, or reserved `frameyard` specifiers |
47
+ | `COMPOSITION_FORMAT_UNRESOLVED` | error | The composition omits `width`/`height` and the project has no `format`; this is a command error, not a document diagnostic |
44
48
  | `PROPS_INVALID` | error | Invalid default props JSON, invalid overrides, or `validateProps` failure |
45
49
  | `CONTROLLER_INVALID` | error | Missing/duplicate/both-inline-and-external controller, missing file, or invalid exports |
46
50
  | `BINDING_INVALID` | error | Unknown `data-fy-*` attribute or malformed binding path |
@@ -52,6 +56,10 @@ Static validation covers:
52
56
  | `ANIMATION_INVALID` | error | Invalid `data-fy-anim-*` value |
53
57
  | `ANIMATION_NOT_FOUND` | warning | `data-fy-anim-*` is present but the element has no CSS animation |
54
58
  | `ANIMATION_MULTIPLE` | warning | Multiple animations on one declaratively scheduled element |
59
+ | `STATIC_ANIMATION_UNSUPPORTED` | error | A static composition uses `data-fy-anim-*`; remove it or declare `duration-in-frames` and `fps` |
60
+ | `STATIC_MEDIA_UNSUPPORTED` | error | A static composition declares `<video>`, `<audio>`, or `<fy-transcript>`; move media to a timeline |
61
+ | `STATIC_UPDATE_IGNORED` | warning | A static composition controller exports `update()`; move initialization into `mount()` |
62
+ | `STATIC_ANIMATION_PAUSED` | warning | A static composition has CSS animations/transitions; they are paused at time 0 |
55
63
  | `SCRIPT_NOT_ALLOWED` | error | Executable `<script>` other than the controller |
56
64
  | `EVENT_HANDLER_NOT_ALLOWED` | error | Inline `on*` handler or `data-fy-attr-on*` |
57
65
  | `URL_NOT_ALLOWED` | error | `javascript:` URL |
@@ -75,7 +83,7 @@ Static validation covers:
75
83
  - `controller`: `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`
76
84
  - `diagnostics`: all static and runtime diagnostics, deduplicated
77
85
 
78
- `check` also exposes `metadata`, `framesChecked`, `audio.details`, and `transcripts.details`. `still` exposes `frames`, `width`, and `height`, plus `frame` and `path` when exactly one frame was captured. `render` exposes the output path, dimensions, frame count, concurrency, bytes, and audio/transcript clip counts.
86
+ `check` and `render` are discriminated unions. Animated/video results report `kind: "animated"` or `kind: "video"`; static composition results report `kind: "static"` or `kind: "image"`. A static check exposes `metadata: { width, height, static: true }` and `framesChecked: 1` with no audio/transcript fields. An image render exposes the PNG path, dimensions, bytes, diagnostics, and elapsed time with no fps, duration, frame count, codec, container, encoder, audio, or transcript fields. `still` exposes `frames`, `width`, and `height`, plus `frame` and `path` when exactly one frame was captured; a static composition only accepts frame 0.
79
87
 
80
88
  Failures use the same envelope with `error.code`, `error.message`, optional `error.details`, and top-level `diagnostics`.
81
89
 
@@ -10,12 +10,15 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
10
10
  | `CONFIG_NOT_FOUND` | No `video.config.js` found | Run from the project root or pass `--project`. |
11
11
  | `CONFIG_INVALID` | Invalid config or composition entry | Fix `video.config.js`; entries must be explicit, local, `.html`, and exist. |
12
12
  | `COMPOSITION_NOT_FOUND` | ID is not listed | Add the entry to `video.config.js`. |
13
- | `COMPOSITION_INVALID` | Invalid doctype, root, metadata, or document structure | Fix the composition document; check `details.line` and `details.attribute`. |
13
+ | `COMPOSITION_INVALID` | Invalid root, metadata, fragment structure, or reserved attribute | Fix the composition fragment; check `details.line` and `details.attribute`. |
14
+ | `COMPOSITION_DOCUMENT_SHELL` | A full document shell is present | Remove `<!doctype>`, `<html>`, `<head>`, and `<body>`; keep the fragment body and top-level metadata. See the 0.8 migration guide. |
15
+ | `IMPORT_MAP_INVALID` | Invalid import map placement, JSON, attributes, or reserved specifier | Keep one or more top-level import maps; remove `frameyard`/`frameyard/` keys and forbidden attributes. |
16
+ | `COMPOSITION_FORMAT_UNRESOLVED` | The composition omits `width`/`height` and the project has no `format` | Declare `width`/`height` on `<fy-composition>`, add `format { width, height, fps }` to `video.config.js`, or render through a timeline. |
14
17
  | `COMPOSITION_ERROR` | Mount, update, or browser page error | Read the frame location; remove forbidden clocks/randomness or fix the controller. |
15
18
  | `COMPOSITION_UPDATE_ASYNC` | `update()` returned a promise | Move async work to `mount()`. |
16
19
  | `COMPOSITION_NOT_READY` | `seek`/`settle` before a successful load | Await `load`/`ready`. |
17
20
  | `COMPOSITION_DISPOSED` | Operation after host disposal | Reload the composition. |
18
- | `CONTROLLER_INVALID` | Missing, duplicate, or invalid controller declaration/file/exports | Keep exactly one inline or external controller; verify its exports and path. |
21
+ | `CONTROLLER_INVALID` | Missing, duplicate, non-top-level, or invalid controller declaration/file/exports | Keep exactly one inline or external top-level controller; verify its exports and path. |
19
22
  | `BINDING_INVALID` | Unknown `data-fy-*` attribute or malformed path | Fix the attribute name or path. |
20
23
  | `BINDING_PATH_MISSING` | Binding path absent from resolved props | Fix the prop or path; use `details.suggestion`. |
21
24
  | `BINDING_VALUE_INVALID` | Object/array used where a primitive is required | Use a string/number/boolean or move the logic to a controller. |
@@ -25,13 +28,17 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
25
28
  | `ANIMATION_INVALID` | Invalid `data-fy-anim-*` value | Use non-negative integer frames and a positive duration. |
26
29
  | `ANIMATION_NOT_FOUND` | `data-fy-anim-*` without a CSS animation | Add an `animation` shorthand or remove the attributes. |
27
30
  | `ANIMATION_MULTIPLE` | Multiple animations on one scheduled element | Use one animation or move scheduling to CSS/the controller. |
31
+ | `STATIC_ANIMATION_UNSUPPORTED` | A static composition uses `data-fy-anim-*` | Remove the attributes or declare `duration-in-frames` and `fps` to make the composition animated. |
32
+ | `STATIC_MEDIA_UNSUPPORTED` | A static composition declares video, audio, or a transcript | Move media and captions to a timeline, or declare `duration-in-frames` and `fps`. |
33
+ | `STATIC_UPDATE_IGNORED` | A static composition exports `update()` | Move initialization into `mount()`; static compositions do not run `update()`. |
34
+ | `STATIC_ANIMATION_PAUSED` | A static composition has CSS animations/transitions | Remove them; the capture uses time 0. |
28
35
  | `SCRIPT_NOT_ALLOWED` | Executable script other than the controller | Move code into the controller. |
29
36
  | `EVENT_HANDLER_NOT_ALLOWED` | Inline `on*` handler or `data-fy-attr-on*` | Remove the handler; use a controller. |
30
37
  | `URL_NOT_ALLOWED` | `javascript:` URL | Use a safe URL. |
31
- | `ELEMENT_NOT_SUPPORTED` | `iframe`, `object`, `embed`, `base`, refresh/CSP meta, `source`, `track`, or declarative shadow DOM | Use supported DOM/SVG/canvas/video sources. |
38
+ | `ELEMENT_NOT_SUPPORTED` | `iframe`, `object`, `embed`, `base`, document-level `<title>`/`<meta>`, refresh/CSP meta, `source`, `track`, or declarative shadow DOM | Use supported DOM/SVG/canvas/video sources. |
32
39
  | `ELEMENT_UNKNOWN` | Unknown `fy-*` element | Use `fy-composition`/`fy-transcript` or a normal element. |
33
40
  | `CONTENT_OUTSIDE_ROOT` | Visual content outside `<fy-composition>` | Move it inside the root. |
34
- | `PROPS_INVALID` | Invalid props JSON or `validateProps()` failure | Fix the props file or validator. |
41
+ | `PROPS_INVALID` | Invalid/non-top-level props JSON or `validateProps()` failure | Put the props block at the top level of the fragment; fix the props file or validator. |
35
42
  | `FRAME_OUT_OF_RANGE` | Frame outside `0..durationInFrames - 1` | Choose valid frames; a batch error lists every invalid frame and the valid range. |
36
43
  | `AUDIO_INVALID` | Bad clip or unreadable/unsupported audio | Use a local project-root-relative file with a decodable audio stream. |
37
44
  | `AUDIO_WARNING` | Audio timeline overrun or silent embedded media | Adjust timing/trim or remove the silent source. |
@@ -62,7 +69,7 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
62
69
  - Failed TTS/transcription operations leave no artifact and no manifest entry.
63
70
  - Existing outputs are replaced only with `--overwrite` (renders/stills) or `--force` (generated assets).
64
71
  - Cancellation terminates the child inference process and cleans temporary work.
65
- - `check` validates every frame and declared transcripts without encoding.
72
+ - `check` validates every animated frame, or a static composition's single state, and declared transcripts without encoding.
66
73
  - Static validation failures exit before Chromium starts.
67
74
  - `check`, `still`, and `render` seek embedded videos and wait for decode before evaluating or capturing a frame.
68
75
  - `doctor` inspects but never repairs, downloads, or modifies the environment.
@@ -82,3 +89,16 @@ Report all of:
82
89
  - still paths and whether they were viewed;
83
90
  - final video path, frame count, and audio/transcript summary;
84
91
  - any skipped step or unresolved diagnostic.
92
+
93
+ ## 0.8 fragment migration
94
+
95
+ 0.8 changed compositions from full documents to partial HTML fragments. If `check` reports `COMPOSITION_DOCUMENT_SHELL`:
96
+
97
+ 1. Remove `<!doctype html>`, `<html>`, `<head>`, `<body>`, `<title>`, and `<meta>`.
98
+ 2. Keep one top-level `<fy-composition>` root.
99
+ 3. Move the props block, the controller, and import maps to the top level.
100
+ 4. Keep top-level `<style>` and `<link rel="stylesheet">` as siblings; Frameyard hoists them into the generated head. Styles inside the root stay in the body.
101
+ 5. Remove the reserved `data-fy-host` attribute and `data-fy="format"` block.
102
+ 6. Run `npx frameyard check <id> --json` again.
103
+
104
+ See `docs/migration-0.8.md` in the Frameyard repository for before/after examples.
@@ -11,7 +11,7 @@ Every non-interactive command supports `--json`. Stdout contains one JSON docume
11
11
  }
12
12
  ```
13
13
 
14
- Additional fields are command-specific and stable within the 0.4 contract. `check`, `still`, and `render` report `elapsedMs`, the wall time of the operation from static validation and preflight through the final frame or publication; CLI argument parsing is excluded.
14
+ Additional fields are command-specific and stable within the 0.8 contract. `check`, `still`, and `render` report `elapsedMs`, the wall time of the operation from static validation and preflight through the final frame or publication; CLI argument parsing is excluded.
15
15
 
16
16
  ## Error envelope
17
17
 
@@ -55,10 +55,13 @@ Static validation diagnostics include source locations. Runtime diagnostics may
55
55
 
56
56
  ## check
57
57
 
58
+ Animated composition results report `kind: "animated"`. `metadata` reports the resolved format, combining declared values with the project `format`.
59
+
58
60
  ```json
59
61
  {
60
62
  "ok": true,
61
63
  "command": "check",
64
+ "kind": "animated",
62
65
  "composition": "hello",
63
66
  "entry": "compositions/hello.html",
64
67
  "controller": { "kind": "inline" },
@@ -89,7 +92,24 @@ Static validation diagnostics include source locations. Runtime diagnostics may
89
92
  }
90
93
  ```
91
94
 
92
- `entry` is the project-relative HTML path. `controller` is `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`.
95
+ `entry` is the project-relative composition fragment path. `controller` is `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`.
96
+
97
+ A static composition result reports `kind: "static"`, dimensions only, `framesChecked: 1`, and no audio/transcript fields:
98
+
99
+ ```json
100
+ {
101
+ "ok": true,
102
+ "command": "check",
103
+ "kind": "static",
104
+ "composition": "still",
105
+ "entry": "compositions/still.html",
106
+ "controller": null,
107
+ "metadata": { "width": 1920, "height": 1080, "static": true },
108
+ "framesChecked": 1,
109
+ "diagnostics": [],
110
+ "elapsedMs": 600
111
+ }
112
+ ```
93
113
 
94
114
  ## still
95
115
 
@@ -111,14 +131,17 @@ Static validation diagnostics include source locations. Runtime diagnostics may
111
131
  }
112
132
  ```
113
133
 
114
- `frames` is always present and follows the expanded `--frame` order. `frame` and `path` are also present only when exactly one frame was captured. `--frame` accepts a number, an inclusive range (`2-12`), or a comma-separated list; at most 32 unique frames are captured per invocation. With multiple frames, `--out` is a directory and the files are named `still-<id>-<frame>.png`.
134
+ `frames` is always present and follows the expanded `--frame` order. `frame` and `path` are also present only when exactly one frame was captured. `--frame` accepts a number, an inclusive range (`2-12`), or a comma-separated list; at most 32 unique frames are captured per invocation. With multiple frames, `--out` is a directory and the files are named `still-<id>-<frame>.png`. A static composition exposes only frame `0`; other frames fail with `FRAME_OUT_OF_RANGE`.
115
135
 
116
136
  ## render
117
137
 
138
+ Animated composition and timeline results report `kind: "video"`:
139
+
118
140
  ```json
119
141
  {
120
142
  "ok": true,
121
143
  "command": "render",
144
+ "kind": "video",
122
145
  "composition": "hello",
123
146
  "entry": "compositions/hello.html",
124
147
  "controller": { "kind": "inline" },
@@ -141,7 +164,26 @@ Static validation diagnostics include source locations. Runtime diagnostics may
141
164
  }
142
165
  ```
143
166
 
144
- A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. Without `--out`, the final path is `out/<composition-id>.<ext>` inside the project root, using the container extension. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16. `container` is `mp4`, `mov`, `mkv`, or `webm`; `codec` is `h264`, `hevc`, or `vp9`; and `encoder` is the resolved encoder (`libx264`, `libx265`, `libvpx-vp9`, `h264_nvenc`, `hevc_nvenc`, `h264_qsv`, or `hevc_qsv`). Timeline results also report the effective `workers` count after the encoder session cap, with an `ENCODER_WARNING` diagnostic when it was reduced.
167
+ A static composition result reports `kind: "image"` and has no fps, duration, frame count, codec, container, encoder, audio, or transcript fields:
168
+
169
+ ```json
170
+ {
171
+ "ok": true,
172
+ "command": "render",
173
+ "kind": "image",
174
+ "composition": "still",
175
+ "entry": "compositions/still.html",
176
+ "controller": null,
177
+ "path": "/absolute/path/out/still.png",
178
+ "width": 1920,
179
+ "height": 1080,
180
+ "bytes": 48210,
181
+ "diagnostics": [],
182
+ "elapsedMs": 700
183
+ }
184
+ ```
185
+
186
+ A successful render means the final path exists and was published atomically. A nonzero exit means no new partial output was published. Without `--out`, the final path is `out/<composition-id>.<ext>` for video and `out/<composition-id>.png` for a static composition inside the project root. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16, and a static composition rejects it. `container` is `mp4`, `mov`, `mkv`, or `webm`; `codec` is `h264`, `hevc`, or `vp9`; and `encoder` is the resolved encoder (`libx264`, `libx265`, `libvpx-vp9`, `h264_nvenc`, `hevc_nvenc`, `h264_qsv`, or `hevc_qsv`). Timeline results also report the effective `workers` count after the encoder session cap, with an `ENCODER_WARNING` diagnostic when it was reduced.
145
187
 
146
188
  ## tts
147
189
 
@@ -16,9 +16,9 @@ export default {
16
16
  };
17
17
  ```
18
18
 
19
- - `format` fixes width, height, and fps for every timeline. It is required when the project lists timelines.
19
+ - `format` fixes width, height, and fps for every timeline and fills those values for compositions that omit them; a declared value always wins. It is required when the project lists timelines.
20
20
  - Composition and timeline IDs share one namespace and must be unique.
21
- - A composition used in a timeline must match the project fps exactly. A different size is fitted by the clip.
21
+ - An animated composition that declares an `fps` must match the project fps exactly; an undeclared `fps` inherits it. A different size is fitted by the clip. A static composition has no fps and skips the frame-rate comparison.
22
22
  - Composition-only projects remain valid without `format` or `timelines`.
23
23
 
24
24
  ## Timeline document
@@ -41,6 +41,7 @@ export default {
41
41
  "duration": 150,
42
42
  "sourceIn": 0,
43
43
  "props": { "title": "Hello" },
44
+ "blend": "screen",
44
45
  "transform": { "fit": "contain", "x": 0, "y": 0, "scale": 1, "rotation": 0, "opacity": 1 },
45
46
  "audio": { "muted": false, "gain": 1 }
46
47
  },
@@ -68,13 +69,13 @@ export default {
68
69
  - `tracks` and `clips` are required. Track order is bottom-to-top for video and irrelevant for audio.
69
70
  - `background` is an optional color; the final video composites over black.
70
71
  - `durationInFrames` is optional. The duration is derived from the latest clip end unless declared; a declared duration may extend but not cut clips.
71
- - Unknown fields are errors. No transitions, effects, keyframes, or track solo/visibility in 0.6.
72
+ - Unknown fields are errors. Blend modes are static per clip; no transitions, keyframes, other effects, or track solo/visibility.
72
73
 
73
74
  ## Sources
74
75
 
75
76
  | `source.kind` | Fields | Notes |
76
77
  | --- | --- | --- |
77
- | `composition` | `id` | Project composition ID. The clip may set `props`. |
78
+ | `composition` | `id` | Project composition ID. The clip may set `props`. A static composition behaves as an image source. |
78
79
  | `timeline` | `id` | Nested timeline ID. Rendered as a group. |
79
80
  | `video` | `src` | Local MP4/WebM, project-relative. |
80
81
  | `image` | `src` | Local PNG/JPEG/WebP. `duration` is required. |
@@ -85,9 +86,10 @@ Media paths resolve from the project root. No remote URLs.
85
86
  ## Time and trims
86
87
 
87
88
  - `start` is the timeline frame where the clip begins.
88
- - `sourceIn` is the first source frame. It defaults to `0` and must be `0` for images.
89
- - `duration` is the number of timeline frames. It is optional for compositions, nested timelines, video, and audio; it defaults to the rest of the source. Images require it.
90
- - A clip that exceeds its source is an error.
89
+ - `sourceIn` is the first source frame. It defaults to `0` and must be `0` for images and static compositions.
90
+ - `duration` is the number of timeline frames. It is optional for animated compositions, nested timelines, video, and audio; it defaults to the rest of the source. Images and static compositions require it.
91
+ - A clip that exceeds its source is an error. Static compositions have no intrinsic duration, so the clip duration is the only bound.
92
+ - A static composition clip is captured once per props variant as a transparent PNG and reused across segments. A clip whose source is static and whose `audio` settings are non-default receives an `AUDIO_WARNING` because there is no source audio.
91
93
  - Clips on one track cannot overlap. Touching endpoints are allowed. Use separate tracks for overlays.
92
94
 
93
95
  ## Presentation
@@ -100,12 +102,13 @@ Visual clips accept a static transform:
100
102
  - `scale`: multiplier after fitting to the project frame; an alternative to `width`/`height`.
101
103
  - `rotation`: degrees around the center.
102
104
  - `opacity`: 0–1.
105
+ - `blend`: CSS `mix-blend-mode`, defaulting to `normal`. Supported values 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.
103
106
 
104
- No keyframes or animated transforms in 0.6.
107
+ A blend mode composites the clip against everything below it in the stage, including the timeline background, and only where the clip's source alpha is non-zero. A composition or nested timeline that paints an opaque background blocks the lower layers; source alpha is what lets a blend reach them. A top-level timeline with no declared background renders over black; a nested or embedded timeline stays transparent. Blend modes are static; there are no keyframes or animated transforms.
105
108
 
106
109
  ## Audio
107
110
 
108
- - Composition, video, and nested-timeline clips contribute audio automatically unless `audio.muted` is true.
111
+ - Animated composition, video, and nested-timeline clips contribute audio automatically unless `audio.muted` is true. Static compositions contribute no audio; put audio on a timeline track.
109
112
  - Timeline audio clips live on audio tracks. Audio tracks support `gain` and `muted`.
110
113
  - `audio.gain` on a clip is 0–1 and multiplies the source audio.
111
114
  - There are no linked A/V pairs. For a J/L cut, mute the visual clip and add an audio clip with a different `sourceIn`.
@@ -113,7 +116,7 @@ No keyframes or animated transforms in 0.6.
113
116
 
114
117
  ## Nested timelines
115
118
 
116
- A nested timeline clip behaves as one layer: its internal tracks are composited first, then the parent clip's opacity and transform apply to the whole group. Nested audio is included. Cycles are rejected statically. Timeline clips do not override props of compositions inside a nested timeline in 0.6.
119
+ A nested timeline clip behaves as one layer: its internal tracks are composited first, then the parent clip's opacity, transform, and blend mode apply to the whole group. A blend mode inside the nested timeline sees only that timeline's own backdrop; it never sees the parent's layers. A declared nested background is part of the group, so a transparent nested timeline has no backdrop to blend against beyond its own layers. Nested audio is included. Cycles are rejected statically. Timeline clips do not override props of compositions inside a nested timeline.
117
120
 
118
121
  ## Commands
119
122
 
@@ -130,7 +133,7 @@ npx frameyard render main --props props/main.json --workers 8 --scratch /path/to
130
133
  - `--container <mp4|mov|mkv|webm>` selects the delivery container (default `mp4`); `--codec <h264|hevc|vp9>` selects the codec, and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded. MP4 and MOV hold H.264/HEVC with AAC; MKV holds all three; WebM holds VP9 with Opus and defaults to VP9. `auto` is the default and probes NVENC, then Quick Sync, then software; `software` forces `libx264`, `libx265`, or `libvpx-vp9`. FFV1 intermediates stay lossless and software-encoded, HEVC keeps the `hvc1` tag in MP4/MOV, and the output extension must match the container.
131
134
  - `--concurrency` is composition-only.
132
135
  - `render` defaults to `out/<timeline-id>.mp4`.
133
- - `check` evaluates every timeline frame and composition frame. `still` captures one or more requested frames through the same clip capture and composite path; a multi-frame still resolves the timeline, starts one server, and reuses one scratch directory. The `--frame` spec and 32-frame cap are the same as for compositions.
136
+ - `check` evaluates every timeline frame and every animated composition frame; a static composition layer is inspected once and not seeked per frame. `still` captures one or more requested frames through the same clip capture and composite path; a multi-frame still resolves the timeline, starts one server, and reuses one scratch directory. The `--frame` spec and 32-frame cap are the same as for compositions.
134
137
  - `npx frameyard dev` lists timelines in the preview; the timeline view has a program monitor, read-only tracks and clips, playhead, playback, audio controls, and diagnostics.
135
138
 
136
139
  ## Determinism and performance
@@ -138,6 +141,7 @@ npx frameyard render main --props props/main.json --workers 8 --scratch /path/to
138
141
  - A timeline frame derives only from its active clips, their composition frames, props, assets, and the render plan.
139
142
  - Composition frames are captured with Chromium; transparent compositions preserve alpha.
140
143
  - Timeline production captures active composition and nested-timeline clips to lossless FFV1, composites segments with FFmpeg, concatenates the ordered segments, and mixes audio once.
141
- - Preview composites in Chromium; production composites with FFmpeg. Small scaling, alpha, and video color differences are expected. Parity is guaranteed at clip capture, not at final encoded pixels.
144
+ - Preview composites in Chromium with CSS `mix-blend-mode`; production composites with FFmpeg's `blend` and `maskedmerge`. CSS is the reference; small scaling, alpha, color, and blend-math differences are expected. Parity is guaranteed at clip capture, not at final encoded pixels.
145
+ - Alpha-correct blending costs roughly 1.7× per blended 1080p layer in the production composite. Budget for it when a timeline stacks several blended layers.
142
146
  - Final output bytes are not a reproducibility guarantee.
143
147
  - Long timelines are segmented and bounded. The reference target and measured worst cases are documented in `docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md`.