frameyard 0.3.0 → 0.5.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 (168) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/GLOSSARY.md +46 -2
  3. package/README.md +174 -106
  4. package/dist/bindings.d.ts +42 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +144 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/cli.js +32 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/composition/adapter.d.ts +30 -0
  11. package/dist/composition/adapter.d.ts.map +1 -0
  12. package/dist/composition/adapter.js +2 -0
  13. package/dist/composition/adapter.js.map +1 -0
  14. package/dist/composition/animation.d.ts +19 -0
  15. package/dist/composition/animation.d.ts.map +1 -0
  16. package/dist/composition/animation.js +55 -0
  17. package/dist/composition/animation.js.map +1 -0
  18. package/dist/composition/attributes.d.ts +7 -0
  19. package/dist/composition/attributes.d.ts.map +1 -0
  20. package/dist/composition/attributes.js +24 -0
  21. package/dist/composition/attributes.js.map +1 -0
  22. package/dist/composition/descriptor.d.ts +30 -0
  23. package/dist/composition/descriptor.d.ts.map +1 -0
  24. package/dist/composition/descriptor.js +188 -0
  25. package/dist/composition/descriptor.js.map +1 -0
  26. package/dist/composition/diagnostics.d.ts +12 -0
  27. package/dist/composition/diagnostics.d.ts.map +1 -0
  28. package/dist/composition/diagnostics.js +38 -0
  29. package/dist/composition/diagnostics.js.map +1 -0
  30. package/dist/composition/index.d.ts +12 -0
  31. package/dist/composition/index.d.ts.map +1 -0
  32. package/dist/composition/index.js +7 -0
  33. package/dist/composition/index.js.map +1 -0
  34. package/dist/composition/media.d.ts +27 -0
  35. package/dist/composition/media.d.ts.map +1 -0
  36. package/dist/composition/media.js +162 -0
  37. package/dist/composition/media.js.map +1 -0
  38. package/dist/composition/paths.d.ts +5 -0
  39. package/dist/composition/paths.d.ts.map +1 -0
  40. package/dist/composition/paths.js +16 -0
  41. package/dist/composition/paths.js.map +1 -0
  42. package/dist/composition/transcripts.d.ts +19 -0
  43. package/dist/composition/transcripts.d.ts.map +1 -0
  44. package/dist/composition/transcripts.js +57 -0
  45. package/dist/composition/transcripts.js.map +1 -0
  46. package/dist/composition/walk.d.ts +10 -0
  47. package/dist/composition/walk.d.ts.map +1 -0
  48. package/dist/composition/walk.js +32 -0
  49. package/dist/composition/walk.js.map +1 -0
  50. package/dist/contracts.d.ts +75 -47
  51. package/dist/contracts.d.ts.map +1 -1
  52. package/dist/contracts.js +14 -159
  53. package/dist/contracts.js.map +1 -1
  54. package/dist/errors.d.ts +15 -4
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +8 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/html/document-adapter.d.ts +5 -0
  59. package/dist/html/document-adapter.d.ts.map +1 -0
  60. package/dist/html/document-adapter.js +17 -0
  61. package/dist/html/document-adapter.js.map +1 -0
  62. package/dist/html/index.d.ts +5 -0
  63. package/dist/html/index.d.ts.map +1 -0
  64. package/dist/html/index.js +3 -0
  65. package/dist/html/index.js.map +1 -0
  66. package/dist/html/parse.d.ts +26 -0
  67. package/dist/html/parse.d.ts.map +1 -0
  68. package/dist/html/parse.js +80 -0
  69. package/dist/html/parse.js.map +1 -0
  70. package/dist/html/validate.d.ts +14 -0
  71. package/dist/html/validate.d.ts.map +1 -0
  72. package/dist/html/validate.js +648 -0
  73. package/dist/html/validate.js.map +1 -0
  74. package/dist/index.d.ts +2 -2
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/preview/main.d.ts +10 -3
  79. package/dist/preview/main.d.ts.map +1 -1
  80. package/dist/preview/main.js +189 -89
  81. package/dist/preview/main.js.map +1 -1
  82. package/dist/project.d.ts +2 -0
  83. package/dist/project.d.ts.map +1 -1
  84. package/dist/project.js +4 -0
  85. package/dist/project.js.map +1 -1
  86. package/dist/renderer/browser.d.ts +11 -1
  87. package/dist/renderer/browser.d.ts.map +1 -1
  88. package/dist/renderer/browser.js +93 -14
  89. package/dist/renderer/browser.js.map +1 -1
  90. package/dist/renderer/index.d.ts +25 -5
  91. package/dist/renderer/index.d.ts.map +1 -1
  92. package/dist/renderer/index.js +82 -16
  93. package/dist/renderer/index.js.map +1 -1
  94. package/dist/runtime/bindings.d.ts +11 -0
  95. package/dist/runtime/bindings.d.ts.map +1 -0
  96. package/dist/runtime/bindings.js +164 -0
  97. package/dist/runtime/bindings.js.map +1 -0
  98. package/dist/runtime/diagnostics.d.ts +4 -0
  99. package/dist/runtime/diagnostics.d.ts.map +1 -0
  100. package/dist/runtime/diagnostics.js +107 -0
  101. package/dist/runtime/diagnostics.js.map +1 -0
  102. package/dist/runtime/document-adapter.d.ts +4 -0
  103. package/dist/runtime/document-adapter.d.ts.map +1 -0
  104. package/dist/runtime/document-adapter.js +19 -0
  105. package/dist/runtime/document-adapter.js.map +1 -0
  106. package/dist/runtime/host.d.ts +14 -0
  107. package/dist/runtime/host.d.ts.map +1 -0
  108. package/dist/runtime/host.js +596 -0
  109. package/dist/runtime/host.js.map +1 -0
  110. package/dist/runtime/index.d.ts +0 -29
  111. package/dist/runtime/index.d.ts.map +1 -1
  112. package/dist/runtime/index.js +0 -360
  113. package/dist/runtime/index.js.map +1 -1
  114. package/dist/runtime/media.d.ts.map +1 -1
  115. package/dist/runtime/media.js +20 -51
  116. package/dist/runtime/media.js.map +1 -1
  117. package/dist/scaffold.d.ts.map +1 -1
  118. package/dist/scaffold.js +177 -84
  119. package/dist/scaffold.js.map +1 -1
  120. package/dist/server/composition-html.d.ts +20 -0
  121. package/dist/server/composition-html.d.ts.map +1 -0
  122. package/dist/server/composition-html.js +45 -0
  123. package/dist/server/composition-html.js.map +1 -0
  124. package/dist/server/preview-html.d.ts.map +1 -1
  125. package/dist/server/preview-html.js +19 -14
  126. package/dist/server/preview-html.js.map +1 -1
  127. package/dist/server/server.d.ts.map +1 -1
  128. package/dist/server/server.js +94 -6
  129. package/dist/server/server.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  133. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
  134. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  135. package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
  136. package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
  137. package/docs/migration-0.4.md +279 -0
  138. package/docs/v0.1-design.md +78 -56
  139. package/examples/demo/README.md +8 -4
  140. package/examples/demo/compositions/demo.controller.js +120 -0
  141. package/examples/demo/compositions/demo.html +253 -0
  142. package/examples/demo/video.config.js +1 -1
  143. package/examples/video/README.md +61 -0
  144. package/examples/video/assets/clip.mp4 +0 -0
  145. package/examples/video/assets/clip.webm +0 -0
  146. package/examples/video/compositions/video-mp4.html +362 -0
  147. package/examples/video/compositions/video.html +362 -0
  148. package/examples/video/props/video.json +8 -0
  149. package/examples/video/scripts/make-clip.sh +109 -0
  150. package/examples/video/video.config.js +6 -0
  151. package/examples/webgl/README.md +30 -0
  152. package/examples/webgl/compositions/webgl.controller.js +200 -0
  153. package/examples/webgl/compositions/webgl.html +215 -0
  154. package/examples/webgl/props/webgl.json +6 -0
  155. package/examples/webgl/video.config.js +5 -0
  156. package/package.json +7 -3
  157. package/skills/frameyard/README.md +4 -0
  158. package/skills/frameyard/SKILL.md +40 -12
  159. package/skills/frameyard/references/audio-assets.md +25 -14
  160. package/skills/frameyard/references/bindings.md +73 -0
  161. package/skills/frameyard/references/common-mistakes.md +59 -0
  162. package/skills/frameyard/references/controllers.md +126 -0
  163. package/skills/frameyard/references/deterministic-authoring.md +143 -52
  164. package/skills/frameyard/references/diagnostics.md +90 -0
  165. package/skills/frameyard/references/failure-recovery.md +37 -14
  166. package/skills/frameyard/references/json-results.md +43 -9
  167. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  168. package/examples/demo/compositions/demo.js +0 -206
@@ -0,0 +1,73 @@
1
+ # Bindings
2
+
3
+ Bindings map immutable props into the composition DOM once per load. They are explicit HTML attributes; there is no template expression language.
4
+
5
+ ## Attributes
6
+
7
+ | Attribute | Effect |
8
+ | --- | --- |
9
+ | `data-fy-text="path"` | Sets `textContent`. |
10
+ | `data-fy-attr-<name>="path"` | Sets an attribute. |
11
+ | `data-fy-class-<name>="path"` | Toggles one class. |
12
+ | `data-fy-if="path"` | Removes the element when false. |
13
+ | `data-fy-repeat="path"` on `<template>` | Clones the template content for each array item. |
14
+
15
+ ```html
16
+ <h1 data-fy-text="title"></h1>
17
+ <img data-fy-attr-src="image" data-fy-attr-alt="alt">
18
+ <div data-fy-class-active="isActive"></div>
19
+ <p data-fy-if="showSubtitle" data-fy-text="subtitle"></p>
20
+
21
+ <template data-fy-repeat="items">
22
+ <span data-fy-text="item.label" data-fy-attr-data-index="index"></span>
23
+ </template>
24
+ ```
25
+
26
+ ## Paths
27
+
28
+ - Dot-separated: `title`, `user.name`, `items.0.label`.
29
+ - Numeric segments index arrays.
30
+ - Only own properties are read. `__proto__`, `constructor`, and `prototype` are rejected.
31
+ - Inside a repeat clone, `item` and `index` shadow props.
32
+ - Missing paths are warnings. Text becomes empty, attributes/classes are removed, and `if` is false. A missing or non-array `repeat` path is an error.
33
+
34
+ ## Value coercion
35
+
36
+ | Binding | Rule |
37
+ | --- | --- |
38
+ | `text` | Strings, numbers, and booleans are stringified; null/undefined become empty; objects/arrays warn and become empty. |
39
+ | `attr` | `true` writes an empty attribute; `false`/null/undefined remove it; strings/numbers are stringified; objects/arrays warn and remove. |
40
+ | `class` | JavaScript truthiness; objects/arrays warn and remove. |
41
+ | `if` | JavaScript truthiness. Empty arrays are truthy. |
42
+ | `repeat` | Must be an array; otherwise an error. |
43
+
44
+ `data-fy-attr-on*` is rejected. A bound `href`/`src` that resolves to a `javascript:` URL is rejected.
45
+
46
+ ## Repeat scope
47
+
48
+ ```html
49
+ <ul>
50
+ <template data-fy-repeat="items">
51
+ <li data-fy-text="item.label"></li>
52
+ </template>
53
+ </ul>
54
+ ```
55
+
56
+ - The `<template>` is replaced by the clones.
57
+ - `--fy-index` is set on each top-level clone for CSS stagger.
58
+ - Nested repeats are rejected. Use a controller for nested data.
59
+ - A repeat template cannot also use `data-fy-if` or `data-fy-text`.
60
+
61
+ ## CSS variables
62
+
63
+ Primitive top-level props are also set as `--fy-prop-<name>` on the composition root:
64
+
65
+ ```html
66
+ <div style="background: var(--fy-prop-accent)"></div>
67
+ ```
68
+
69
+ Booleans are `1`/`0`. Null, undefined, objects, and arrays are omitted. Bindings are applied before the controller mounts; controller-created DOM is not re-bound.
70
+
71
+ ## Ordering
72
+
73
+ Bindings apply top-down: repeat, then `if`, then text/attribute/class. A removed element does not process its children. `data-fy-if` and `data-fy-text` on the composition root are errors.
@@ -0,0 +1,59 @@
1
+ # Common mistakes
2
+
3
+ ## Using the old authoring model
4
+
5
+ - **JavaScript composition modules / `defineComposition`** were removed in 0.4. A composition is a `.html` document listed in `video.config.js`.
6
+ - **`data-frameyard-*`** media attributes were removed in 0.4. Use `data-fy-*`.
7
+
8
+ ## Document structure
9
+
10
+ - Missing `<!doctype html>` or missing/multiple `<fy-composition>` roots.
11
+ - Missing `width`, `height`, `fps`, or `duration-in-frames` on the root.
12
+ - Missing the `data-fy="props"` block when the composition relies on default props.
13
+ - Visual content outside `<fy-composition>`; it is not captured.
14
+ - Using `<base>`, refresh/CSP meta tags, `<iframe>`, `<object>`, or `<embed>`.
15
+
16
+ ## Bindings
17
+
18
+ - Unknown `data-fy-*` attribute, usually a typo such as `data-fy-txt`.
19
+ - A binding path that does not exist in the resolved props. Check the `BINDING_PATH_MISSING` warning and its suggestion.
20
+ - Using `data-fy-repeat` on a normal element instead of `<template>`.
21
+ - Nesting `data-fy-repeat`; use a controller for nested data.
22
+ - Putting `data-fy-if` or `data-fy-text` on a repeat template.
23
+ - Putting `data-fy-text` on an `<img>` or another void element.
24
+ - Expecting bindings to update every frame or to re-bind controller-created DOM.
25
+
26
+ ## Timing
27
+
28
+ - Writing seconds where frames are required, or frames where seconds are required.
29
+ - `data-fy-start` and `data-fy-anim-start`/`duration` are frames.
30
+ - `data-fy-trim`/`duration` are seconds.
31
+ - Using CSS `animation-delay`/`duration` in seconds and expecting the declarative scheduler to ignore its own attributes. `data-fy-anim-*` overrides the shorthand's delay/duration.
32
+ - Adding `data-fy-anim-*` without a CSS animation; this produces `ANIMATION_NOT_FOUND`.
33
+ - Using multiple animations on an element with one declarative schedule.
34
+
35
+ ## Controllers
36
+
37
+ - Declaring both inline source and `src` on the same controller.
38
+ - Declaring more than one controller.
39
+ - Making `update()` async; do asynchronous work in `mount()`.
40
+ - Leaving module-scope state behind in `dispose()`.
41
+ - Writing a literal `</script>` inside inline controller source; escape it.
42
+ - Importing libraries from executable `<script>` tags; import them from the controller instead.
43
+ - Using `Math.random()`, `Date.now()`, `performance.now()`, timers, independent `requestAnimationFrame` loops, or CSS transitions.
44
+
45
+ ## Media and transcripts
46
+
47
+ - Using remote, live, or unseekable media sources.
48
+ - Using `<source>` or `<track>` children; use a local `src` and `<fy-transcript>`.
49
+ - Adding or changing media in `update()`; prepare it in the document or in `mount()`.
50
+ - Calling `play()`, `pause()`, or setting `currentTime` on a video.
51
+ - Assuming media/transcript `src` is document-relative. It is project-root-relative.
52
+ - Assuming the controller `src` is project-root-relative. It is document-relative.
53
+
54
+ ## Validation
55
+
56
+ - Replacing `check` with a single `still`.
57
+ - Ignoring warnings or `diagnostics` in `--json` output.
58
+ - Claiming success after a nonzero exit, a missing still, or skipped visual inspection.
59
+ - Using `--overwrite` or `--force` without permission.
@@ -0,0 +1,126 @@
1
+ # Controllers
2
+
3
+ A composition controller is optional JavaScript for what HTML and CSS cannot express: Canvas 2D, WebGL, karaoke, complex computed motion, or data-driven DOM. It is the only executable code in a composition.
4
+
5
+ ## Inline or external
6
+
7
+ Inline for a small, single-use controller:
8
+
9
+ ```html
10
+ <script type="text/fy-controller">
11
+ export default {
12
+ mount({ root, props }) {
13
+ // create canvases, query elements, load data
14
+ },
15
+ update({ frame, props, root }) {
16
+ // synchronous and seek-safe
17
+ },
18
+ dispose() {
19
+ // release module-scope state and listeners
20
+ },
21
+ };
22
+ </script>
23
+ ```
24
+
25
+ External for reuse, tests, or a large controller:
26
+
27
+ ```html
28
+ <script type="text/fy-controller" src="./hello.controller.js"></script>
29
+ ```
30
+
31
+ ```js
32
+ export default {
33
+ mount({ root, props }) {},
34
+ update({ frame, props, root }) {},
35
+ dispose() {},
36
+ };
37
+ ```
38
+
39
+ Exactly one controller is allowed. 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.
40
+
41
+ ## Lifecycle
42
+
43
+ - `mount({ root, props })` runs once per load and may be asynchronous. Use it to query the root, create canvases, and start loading local data.
44
+ - `update({ frame, props, root })` runs on every seek. It must be synchronous and seek-safe: derive the frame from `frame` and `props` only.
45
+ - `dispose()` releases resources and resets module-scope state. The host calls it before remounting or disposing the document.
46
+ - `validateProps(props)` is optional and synchronous. Return an array of human-readable errors, or `void`.
47
+ - A validation-only controller may export just `validateProps`.
48
+
49
+ Keep mutable state in module scope, never on `this`. The controller module is imported once per document; `load` remounts it without re-importing, so `dispose` must reset module state.
50
+
51
+ ## Props validation
52
+
53
+ ```html
54
+ <script type="text/fy-controller">
55
+ export default {
56
+ validateProps(props) {
57
+ const errors = [];
58
+ if (typeof props.title !== "string") errors.push("title must be a string");
59
+ return errors;
60
+ },
61
+ };
62
+ </script>
63
+ ```
64
+
65
+ Validation runs before the old mount is disposed. Invalid props reject `load` with `PROPS_INVALID` and keep the current composition mounted.
66
+
67
+ ## Canvas and WebGL
68
+
69
+ ```js
70
+ let canvas;
71
+ let context;
72
+
73
+ export default {
74
+ mount({ root }) {
75
+ canvas = root.querySelector("#field");
76
+ context = canvas.getContext("2d");
77
+ },
78
+ update({ frame }) {
79
+ context.clearRect(0, 0, canvas.width, canvas.height);
80
+ context.fillStyle = frame % 2 === 0 ? "#101820" : "#203040";
81
+ context.fillRect(0, 0, canvas.width, canvas.height);
82
+ },
83
+ dispose() {
84
+ canvas = undefined;
85
+ context = undefined;
86
+ },
87
+ };
88
+ ```
89
+
90
+ Redraw WebGL from the current frame; do not depend on the previous drawing buffer. Production pins SwiftShader.
91
+
92
+ ## Randomness and clocks
93
+
94
+ Import `random` from `frameyard/runtime`:
95
+
96
+ ```js
97
+ import { random } from "frameyard/runtime";
98
+
99
+ const x = random("star:17:x") * 1920;
100
+ ```
101
+
102
+ Never use `Math.random()`, `Date.now()`, `performance.now()`, timers, independent `requestAnimationFrame` loops, or CSS transitions as animation inputs. The host diagnoses common cases in `mount` and `update`.
103
+
104
+ ## Media
105
+
106
+ Prefer declarative `<video>`/`<audio>` elements with `data-fy-*` attributes. The host pauses and seeks them; do not call `play()`, `pause()`, or set `currentTime` in `update()`. Media created by the controller is allowed in `mount`; media added or changed in `update` cannot be prepared without network access.
107
+
108
+ Media and transcript `src` values resolve from the project root. The controller `src` resolves relative to the composition document.
109
+
110
+ ## Karaoke
111
+
112
+ Read the declared transcript JSON, convert source milliseconds to frames, and render the active segment or word from `frame`:
113
+
114
+ ```js
115
+ const FPS = 30;
116
+ const clipStartFrame = 30;
117
+ const trimStart = 0.25;
118
+
119
+ function wordProgress(word, frame) {
120
+ const start = clipStartFrame + (word.startMs / 1000 - trimStart) * FPS;
121
+ const end = clipStartFrame + (word.endMs / 1000 - trimStart) * FPS;
122
+ return Math.max(0, Math.min(1, (frame - start) / Math.max(1, end - start)));
123
+ }
124
+ ```
125
+
126
+ Never interpolate segment timings to fake word timings.
@@ -1,6 +1,6 @@
1
- # Deterministic authoring
1
+ # Deterministic HTML authoring
2
2
 
3
- Frameyard compositions are ordinary native JavaScript ES modules. The preview server injects an import map for `frameyard` and `frameyard/runtime`; do not add a bundling step or import from `node_modules` in composition code.
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`.
4
4
 
5
5
  ## Project configuration
6
6
 
@@ -9,50 +9,125 @@ Frameyard compositions are ordinary native JavaScript ES modules. The preview se
9
9
  ```js
10
10
  export default {
11
11
  compositions: {
12
- hello: "./compositions/hello.js",
12
+ hello: "./compositions/hello.html",
13
13
  },
14
14
  };
15
15
  ```
16
16
 
17
- There is no directory scanning. A new composition file is invisible until it is listed.
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.
18
18
 
19
19
  ## Composition shape
20
20
 
21
- ```js
22
- import { defineComposition } from "frameyard";
23
- import { random } from "frameyard/runtime";
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>
47
+ ```
24
48
 
25
- let title;
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.
26
53
 
27
- export default defineComposition({
28
- width: 1920,
29
- height: 1080,
30
- fps: 30,
31
- durationInFrames: 900,
32
- defaultProps: { title: "Hello" },
33
- validateProps(props) {
34
- return typeof props.title === "string" ? [] : ["title must be a string"];
35
- },
36
- mount({ container, props }) {
37
- container.innerHTML = "<h1 id='title'></h1>";
38
- title = container.querySelector("#title");
39
- title.textContent = props.title;
54
+ ## Bindings
55
+
56
+ ```html
57
+ <h1 data-fy-text="title"></h1>
58
+ <img data-fy-attr-src="image" data-fy-attr-alt="alt">
59
+ <div data-fy-class-active="isActive"></div>
60
+ <p data-fy-if="showSubtitle" data-fy-text="subtitle"></p>
61
+ <template data-fy-repeat="items">
62
+ <span data-fy-text="item.label" data-fy-attr-data-index="index"></span>
63
+ </template>
64
+ ```
65
+
66
+ - Paths are dot-separated (`user.name`, `items.0.label`); numeric segments index arrays.
67
+ - `data-fy-text` sets `textContent`.
68
+ - `data-fy-attr-<name>` sets an attribute; `true` writes an empty attribute, `false`/`null`/`undefined` remove it.
69
+ - `data-fy-class-<name>` toggles one class.
70
+ - `data-fy-if` removes the element when the value is false.
71
+ - `data-fy-repeat` must be on a `<template>` and must reference an array. Each clone gets `item`, `index`, and `--fy-index`. Nested repeats are rejected; use a controller.
72
+ - Bindings apply once per load, before the controller mounts. Controller-created DOM is not re-bound.
73
+ - Missing paths warn and resolve to empty text, a removed attribute/class, or false for `if`.
74
+ - Primitive top-level props are also exposed as `--fy-prop-<name>` CSS custom properties. Booleans are `1`/`0`; objects and arrays are omitted.
75
+
76
+ ## Animation
77
+
78
+ CSS `@keyframes` and the Web Animations API are paused and seeked by the runtime inside the composition root.
79
+
80
+ ```css
81
+ @keyframes fade-up {
82
+ from { opacity: 0; transform: translateY(24px); }
83
+ to { opacity: 1; transform: translateY(0); }
84
+ }
85
+ #title { animation: fade-up 1s ease-out both; }
86
+ ```
87
+
88
+ ```html
89
+ <h1 data-fy-anim-start="30" data-fy-anim-duration="60" data-fy-anim-easing="ease-out"></h1>
90
+ ```
91
+
92
+ - `data-fy-anim-start` is a non-negative frame count. `data-fy-anim-duration` is a positive frame count. `data-fy-anim-easing` is a CSS easing.
93
+ - One declarative animation per element. The CSS `animation` shorthand supplies keyframes, fill mode, and iteration count; the attributes override delay/duration/easing.
94
+ - The root exposes `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`. Use them in `calc()` for simple computed motion and progress bars.
95
+ - Do not use CSS transitions. They are diagnosed as non-deterministic.
96
+
97
+ ## Controllers
98
+
99
+ An optional controller handles Canvas 2D, WebGL, karaoke, and per-frame computation CSS cannot express.
100
+
101
+ Inline for a small, single-use controller:
102
+
103
+ ```html
104
+ <script type="text/fy-controller">
105
+ export default {
106
+ mount({ root, props }) {
107
+ // query elements, create canvases, load data
40
108
  },
41
- update({ frame, props }) {
42
- title.textContent = props.title;
43
- title.style.transform = `translateX(${random("title:" + frame) * 40}px)`;
109
+ update({ frame, props, root }) {
110
+ // synchronous and seek-safe
44
111
  },
45
112
  dispose() {
46
- title = undefined;
113
+ // release module-scope state and listeners
47
114
  },
48
- });
115
+ };
116
+ </script>
117
+ ```
118
+
119
+ External for a reusable or complex controller:
120
+
121
+ ```html
122
+ <script type="text/fy-controller" src="./hello.controller.js"></script>
49
123
  ```
50
124
 
51
- - `mount()` runs once and may await loading work.
52
- - `update({ frame, props })` is synchronous and seek-safe.
53
- - `dispose()` releases resources and module-scope references.
54
- - Props are JSON-serializable, deep-frozen, and merged shallowly over `defaultProps`.
55
- - Keep mutable state in module-scope variables, never on `this`.
125
+ - Exactly one controller is allowed. It may export `mount`, `update`, `dispose`, and `validateProps`; at least one is required. `mount` may be async; `update` and `validateProps` are synchronous.
126
+ - A validation-only controller may export just `validateProps(props)`.
127
+ - 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
+ - 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.
130
+ - Keep mutable state in module scope, never on `this`. Reset it in `dispose()`.
56
131
 
57
132
  ## Forbidden animation inputs
58
133
 
@@ -65,19 +140,17 @@ Do not use any of these as animation state:
65
140
  - CSS transitions
66
141
  - state left from a previously visited frame
67
142
 
68
- Use `random("stable:key")` for deterministic randomness. The value depends only on the key, not call order.
143
+ Use `random("stable:key")` from `frameyard/runtime` for deterministic randomness. The value depends only on the key, not call order.
69
144
 
70
145
  ```js
71
146
  import { random } from "frameyard/runtime";
72
147
 
73
- const x = random("star:17:x") * width;
148
+ const x = random("star:17:x") * 1920;
74
149
  ```
75
150
 
76
151
  ## Canvas and WebGL
77
152
 
78
- CSS `@keyframes` and the Web Animations API are paused and seeked by the runtime inside the composition container. They are valid animation drivers. CSS transitions are diagnosed as non-deterministic and should not be used.
79
-
80
- Canvas 2D and WebGL must be redrawn from the current frame:
153
+ Redraw Canvas 2D and WebGL from the current frame in the controller's `update()`:
81
154
 
82
155
  ```js
83
156
  update({ frame }) {
@@ -92,34 +165,52 @@ update({ frame }) {
92
165
 
93
166
  Do not preserve and accumulate WebGL drawing across frames unless that accumulation is itself deterministic. Production sessions use software WebGL so output does not depend on the host GPU.
94
167
 
95
- ## Embedded video
168
+ ## Embedded video and audio
96
169
 
97
- Place local `<video>` elements in `mount()` and let the runtime seek them:
170
+ Declare local media in the composition document:
98
171
 
99
172
  ```html
100
173
  <video
101
174
  src="assets/clip.webm"
102
- data-frameyard-start="30"
103
- data-frameyard-trim="0.5"
104
- data-frameyard-duration="4"
105
- data-frameyard-volume="0.8"
175
+ data-fy-start="30"
176
+ data-fy-trim="0.5"
177
+ data-fy-duration="4"
178
+ data-fy-volume="0.8"
106
179
  ></video>
180
+
181
+ <audio
182
+ src="assets/voice.wav"
183
+ data-fy-start="30"
184
+ data-fy-trim="0.25"
185
+ data-fy-duration="4.2"
186
+ data-fy-volume="0.8"
187
+ ></audio>
107
188
  ```
108
189
 
109
- - `data-frameyard-start` is the composition frame where the source starts. Default `0`.
110
- - `data-frameyard-trim` is seconds trimmed from the source start. Default `0`.
111
- - `data-frameyard-duration` optionally limits how many source seconds are used.
112
- - `data-frameyard-volume` sets embedded-audio volume from `0` to `1`. Default `1`.
190
+ - `data-fy-start` is the composition frame where the source starts. Default `0`.
191
+ - `data-fy-trim` is seconds trimmed from the source start. Default `0`.
192
+ - `data-fy-duration` optionally limits how many source seconds are used.
193
+ - `data-fy-volume` sets volume from `0` to `1`. Default `1`.
194
+
195
+ Do not call `play()`, `pause()`, or set `currentTime` in a controller. Media must be in the light DOM; shadow roots are not scanned. Sources must be local project-relative files; live streams, remote URLs, `<source>` children, and `<track>` children are not supported. The runtime loads the complete file during the loading phase and seeks video after each `update()`.
196
+
197
+ An unmuted video's audio is extracted and mixed into the output timeline. Set `muted` or `data-fy-audio="off"` for a silent visual asset.
113
198
 
114
- Do not call `play()`, `pause()`, or set `currentTime` in `update()`. Create video elements and set the `data-frameyard-*` attributes in `mount()`. Videos must be in the composition container's light DOM; shadow roots are not scanned. The runtime pauses every video, loads the complete local file during the loading phase, and seeks it after each `update()` before capture. Video sources must be local, seekable project files; live streams, remote URLs, and videos added or changed during `update()` are not supported.
199
+ ## Transcripts
200
+
201
+ Declare canonical transcript requirements with:
202
+
203
+ ```html
204
+ <fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
205
+ ```
115
206
 
116
- An unmuted video's audio is extracted and mixed into the output timeline. Set `muted` or `data-frameyard-audio="off"` for a silent visual asset. The same start, trim, duration, and volume attributes place its audio.
207
+ `words` requires measured word timings. `check`, `still`, and `render` validate the transcript file. Visual presentation is the composition's responsibility; a controller can fetch the JSON and render timed text or karaoke. See [subtitles and karaoke](subtitles-karaoke.md).
117
208
 
118
209
  ## Loading assets
119
210
 
120
- Remote data, images, fonts, and other assets must resolve during `mount()` or before the first frame. The production browser blocks network requests after loading begins. A fetch from `update()` is a bug.
211
+ 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.
121
212
 
122
- Images inserted in `mount()` are awaited by the runtime. Static fonts should be loaded with `document.fonts.ready` or an explicit `FontFace` before frame production.
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.
123
214
 
124
215
  ## Validation and inspection
125
216
 
@@ -128,8 +219,8 @@ npx frameyard check hello --json
128
219
  npx frameyard still hello --frame 42 --out hello-42.png --json
129
220
  ```
130
221
 
131
- `check` mounts the composition, evaluates every frame, and validates audio and declared transcripts without capturing or encoding. Do not replace it with a single still.
222
+ `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.
132
223
 
133
224
  ## Metadata is fixed
134
225
 
135
- `width`, `height`, `fps`, and `durationInFrames` 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.
226
+ `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.
@@ -0,0 +1,90 @@
1
+ # Diagnostics
2
+
3
+ Frameyard reports composition problems as structured diagnostics before and during frame production.
4
+
5
+ ## Static validation
6
+
7
+ `check`, `still`, and `render` run a Node-side static composition validation before launching Chromium. `dev` runs it when a composition is requested. Static errors fail the command without starting a browser; warnings allow frame evaluation.
8
+
9
+ Static validation covers:
10
+
11
+ - doctype, root count, metadata, and default-props JSON
12
+ - controller declaration and file existence
13
+ - executable scripts, `on*` handlers, `javascript:` URLs, and unsupported visual elements
14
+ - unknown `data-fy-*` attributes, binding paths, `if`/`repeat` placement, and animation attributes
15
+ - audio/video/transcript references and ordinary image/stylesheet assets
16
+ - visual content outside `<fy-composition>`
17
+
18
+ ## Diagnostic shape
19
+
20
+ ```json
21
+ {
22
+ "severity": "warning",
23
+ "code": "BINDING_PATH_MISSING",
24
+ "message": "data-fy-text references prop \"titel\", which is not in the resolved props.",
25
+ "details": {
26
+ "path": "compositions/hello.html",
27
+ "line": 12,
28
+ "column": 5,
29
+ "element": "h1",
30
+ "attribute": "data-fy-text",
31
+ "value": "titel",
32
+ "suggestion": "title"
33
+ }
34
+ }
35
+ ```
36
+
37
+ `details` may include `path`, `line`, `column`, `endLine`, `endColumn`, `element`, `attribute`, `value`, `suggestion`, `frame`, and `controller`.
38
+
39
+ ## Common codes
40
+
41
+ | Code | Severity | Meaning |
42
+ | --- | --- | --- |
43
+ | `COMPOSITION_INVALID` | error | Missing/invalid doctype, root, metadata, or document structure |
44
+ | `PROPS_INVALID` | error | Invalid default props JSON, invalid overrides, or `validateProps` failure |
45
+ | `CONTROLLER_INVALID` | error | Missing/duplicate/both-inline-and-external controller, missing file, or invalid exports |
46
+ | `BINDING_INVALID` | error | Unknown `data-fy-*` attribute or malformed binding path |
47
+ | `BINDING_PATH_MISSING` | warning | A binding path is absent from the resolved props |
48
+ | `BINDING_VALUE_INVALID` | warning | A text/attribute/class binding resolved to an object or array |
49
+ | `BINDING_REPEAT_INVALID` | error | Repeat is not on `<template>`, or its path is missing/not an array |
50
+ | `BINDING_REPEAT_NESTED` | error | Nested `data-fy-repeat` |
51
+ | `BINDING_TARGET_INVALID` | error | `data-fy-text` on a void element or the root; invalid binding target |
52
+ | `ANIMATION_INVALID` | error | Invalid `data-fy-anim-*` value |
53
+ | `ANIMATION_NOT_FOUND` | warning | `data-fy-anim-*` is present but the element has no CSS animation |
54
+ | `ANIMATION_MULTIPLE` | warning | Multiple animations on one declaratively scheduled element |
55
+ | `SCRIPT_NOT_ALLOWED` | error | Executable `<script>` other than the controller |
56
+ | `EVENT_HANDLER_NOT_ALLOWED` | error | Inline `on*` handler or `data-fy-attr-on*` |
57
+ | `URL_NOT_ALLOWED` | error | `javascript:` URL |
58
+ | `ELEMENT_NOT_SUPPORTED` | error | `iframe`, `object`, `embed`, `base`, refresh/CSP meta, `source`, `track`, or declarative shadow DOM |
59
+ | `ELEMENT_UNKNOWN` | warning | Unknown `fy-*` element |
60
+ | `CONTENT_OUTSIDE_ROOT` | warning | Visual content outside `<fy-composition>` |
61
+ | `MEDIA_INVALID` | error | Invalid or missing audio/video source or attributes |
62
+ | `TRANSCRIPT_INVALID` | error | Invalid or missing transcript source |
63
+ | `ASSET_NOT_FOUND` | error | Missing image, stylesheet, media, or transcript file |
64
+ | `AUDIO_WARNING` | warning | Audio timeline overrun or silent embedded media |
65
+ | `TRANSCRIPT_WARNING` | warning | Empty transcript text or no timed segments |
66
+ | `DETERMINISM_WARNING` | warning | Forbidden clock/randomness, CSS transition, or runtime diagnostic |
67
+ | `COMPOSITION_NOT_READY` | error | `seek`/`settle` before a successful load |
68
+ | `COMPOSITION_DISPOSED` | error | Operation after the host was disposed |
69
+
70
+ ## Results
71
+
72
+ `check`, `still`, and `render` JSON results expose:
73
+
74
+ - `entry`: project-relative HTML path
75
+ - `controller`: `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`
76
+ - `diagnostics`: all static and runtime diagnostics, deduplicated
77
+
78
+ `check` also exposes `metadata`, `framesChecked`, `audio.details`, and `transcripts.details`. `still` exposes `frame`, `path`, `width`, and `height`. `render` exposes the output path, dimensions, frame count, concurrency, bytes, and audio/transcript clip counts.
79
+
80
+ Failures use the same envelope with `error.code`, `error.message`, optional `error.details`, and top-level `diagnostics`.
81
+
82
+ ## Working with diagnostics
83
+
84
+ 1. Run `npx frameyard check <id> --json`.
85
+ 2. Read every error first; fix the first one and rerun.
86
+ 3. Treat warnings as actionable. `BINDING_PATH_MISSING` is usually a typo; use the `suggestion` field.
87
+ 4. Use `details.path`, `line`, and `column` to edit the exact source.
88
+ 5. Re-run `check`, inspect representative stills, then render.
89
+
90
+ Do not ignore a failing `check`, a missing still, or a nonzero render. Do not describe visual correctness you did not inspect.