frameyard 0.2.0 → 0.4.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 (112) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/GLOSSARY.md +58 -2
  3. package/README.md +185 -95
  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 +37 -14
  9. package/dist/cli.js.map +1 -1
  10. package/dist/contracts.d.ts +73 -47
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js +14 -159
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/errors.d.ts +15 -4
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +8 -0
  17. package/dist/errors.js.map +1 -1
  18. package/dist/html/index.d.ts +5 -0
  19. package/dist/html/index.d.ts.map +1 -0
  20. package/dist/html/index.js +3 -0
  21. package/dist/html/index.js.map +1 -0
  22. package/dist/html/parse.d.ts +30 -0
  23. package/dist/html/parse.d.ts.map +1 -0
  24. package/dist/html/parse.js +80 -0
  25. package/dist/html/parse.js.map +1 -0
  26. package/dist/html/validate.d.ts +14 -0
  27. package/dist/html/validate.d.ts.map +1 -0
  28. package/dist/html/validate.js +931 -0
  29. package/dist/html/validate.js.map +1 -0
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/preview/main.d.ts +12 -3
  35. package/dist/preview/main.d.ts.map +1 -1
  36. package/dist/preview/main.js +208 -85
  37. package/dist/preview/main.js.map +1 -1
  38. package/dist/project.d.ts +2 -0
  39. package/dist/project.d.ts.map +1 -1
  40. package/dist/project.js +4 -0
  41. package/dist/project.js.map +1 -1
  42. package/dist/renderer/audio.d.ts +5 -1
  43. package/dist/renderer/audio.d.ts.map +1 -1
  44. package/dist/renderer/audio.js +4 -0
  45. package/dist/renderer/audio.js.map +1 -1
  46. package/dist/renderer/browser.d.ts +12 -1
  47. package/dist/renderer/browser.d.ts.map +1 -1
  48. package/dist/renderer/browser.js +69 -11
  49. package/dist/renderer/browser.js.map +1 -1
  50. package/dist/renderer/index.d.ts +30 -5
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +204 -26
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/runtime/bindings.d.ts +11 -0
  55. package/dist/runtime/bindings.d.ts.map +1 -0
  56. package/dist/runtime/bindings.js +164 -0
  57. package/dist/runtime/bindings.js.map +1 -0
  58. package/dist/runtime/diagnostics.d.ts +4 -0
  59. package/dist/runtime/diagnostics.d.ts.map +1 -0
  60. package/dist/runtime/diagnostics.js +107 -0
  61. package/dist/runtime/diagnostics.js.map +1 -0
  62. package/dist/runtime/host.d.ts +12 -0
  63. package/dist/runtime/host.d.ts.map +1 -0
  64. package/dist/runtime/host.js +701 -0
  65. package/dist/runtime/host.js.map +1 -0
  66. package/dist/runtime/index.d.ts +0 -26
  67. package/dist/runtime/index.d.ts.map +1 -1
  68. package/dist/runtime/index.js +0 -286
  69. package/dist/runtime/index.js.map +1 -1
  70. package/dist/runtime/media.d.ts +16 -0
  71. package/dist/runtime/media.d.ts.map +1 -0
  72. package/dist/runtime/media.js +307 -0
  73. package/dist/runtime/media.js.map +1 -0
  74. package/dist/scaffold.d.ts.map +1 -1
  75. package/dist/scaffold.js +169 -78
  76. package/dist/scaffold.js.map +1 -1
  77. package/dist/server/composition-html.d.ts +20 -0
  78. package/dist/server/composition-html.d.ts.map +1 -0
  79. package/dist/server/composition-html.js +45 -0
  80. package/dist/server/composition-html.js.map +1 -0
  81. package/dist/server/preview-html.d.ts.map +1 -1
  82. package/dist/server/preview-html.js +19 -14
  83. package/dist/server/preview-html.js.map +1 -1
  84. package/dist/server/server.d.ts.map +1 -1
  85. package/dist/server/server.js +94 -6
  86. package/dist/server/server.js.map +1 -1
  87. package/dist/version.d.ts +1 -1
  88. package/dist/version.js +1 -1
  89. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  90. package/docs/adr/0003-render-browser-frames-offline.md +2 -0
  91. package/docs/adr/0005-keep-parallel-rendering-opt-in-and-frame-ordered.md +3 -0
  92. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +11 -0
  93. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  94. package/docs/migration-0.4.md +279 -0
  95. package/docs/v0.1-design.md +82 -57
  96. package/examples/demo/README.md +7 -3
  97. package/examples/demo/compositions/demo.controller.js +120 -0
  98. package/examples/demo/compositions/demo.html +253 -0
  99. package/examples/demo/video.config.js +1 -1
  100. package/package.json +9 -3
  101. package/skills/frameyard/README.md +4 -0
  102. package/skills/frameyard/SKILL.md +41 -11
  103. package/skills/frameyard/references/audio-assets.md +25 -14
  104. package/skills/frameyard/references/bindings.md +73 -0
  105. package/skills/frameyard/references/common-mistakes.md +59 -0
  106. package/skills/frameyard/references/controllers.md +126 -0
  107. package/skills/frameyard/references/deterministic-authoring.md +160 -41
  108. package/skills/frameyard/references/diagnostics.md +90 -0
  109. package/skills/frameyard/references/failure-recovery.md +38 -12
  110. package/skills/frameyard/references/json-results.md +43 -8
  111. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  112. package/examples/demo/compositions/demo.js +0 -206
@@ -0,0 +1,120 @@
1
+ import { random } from "frameyard/runtime";
2
+
3
+ const STAR_COUNT = 130;
4
+ const BAR_COUNT = 52;
5
+ const BASELINE = 858;
6
+
7
+ let context;
8
+ let signalPath;
9
+ let signalBars;
10
+ let frameLabel;
11
+ let stars;
12
+ let bars = [];
13
+ let signalLength = 0;
14
+
15
+ function clamp(value) {
16
+ return Math.max(0, Math.min(1, value));
17
+ }
18
+
19
+ function easeOut(value) {
20
+ const t = clamp(value);
21
+ return 1 - Math.pow(1 - t, 3);
22
+ }
23
+
24
+ function makeStars() {
25
+ stars = Array.from({ length: STAR_COUNT }, (_, index) => ({
26
+ x: random("star:" + index + ":x"),
27
+ y: random("star:" + index + ":y"),
28
+ radius: 0.6 + random("star:" + index + ":r") * 1.8,
29
+ speed: 6 + random("star:" + index + ":speed") * 34,
30
+ alpha: 0.12 + random("star:" + index + ":alpha") * 0.65,
31
+ }));
32
+ }
33
+
34
+ function buildBars(root) {
35
+ signalBars = root.querySelector("#signal-bars");
36
+ bars = [];
37
+
38
+ for (let index = 0; index < BAR_COUNT; index += 1) {
39
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
40
+ const height = 24 + random("bar:" + index + ":height") * 176;
41
+ const x = 168 + index * 31;
42
+ rect.setAttribute("x", String(x));
43
+ rect.setAttribute("width", "15");
44
+ rect.setAttribute("rx", "7.5");
45
+ rect.setAttribute("fill", "var(--fy-prop-accent)");
46
+ rect.setAttribute("height", "0");
47
+ rect.setAttribute("y", String(BASELINE));
48
+ rect.setAttribute("opacity", "0");
49
+ signalBars.append(rect);
50
+ bars.push({ element: rect, height });
51
+ }
52
+ }
53
+
54
+ function buildSignalPath(root) {
55
+ signalPath = root.querySelector("#signal-path");
56
+ const points = [];
57
+ for (let index = 0; index <= 120; index += 1) {
58
+ const x = 160 + (index / 120) * 1600;
59
+ const wave = Math.sin(index / 7) * 38 + Math.sin(index / 17 + 1.4) * 22 + (random("signal:" + index) - 0.5) * 12;
60
+ points.push([x, 535 + wave]);
61
+ }
62
+ const d = points.map(([x, y], index) => (index === 0 ? "M" : "L") + x + " " + y).join(" ");
63
+ signalPath.setAttribute("d", d);
64
+ signalLength = signalPath.getTotalLength();
65
+ signalPath.style.strokeDasharray = String(signalLength);
66
+ signalPath.style.strokeDashoffset = String(signalLength);
67
+ }
68
+
69
+ export default {
70
+ mount({ root }) {
71
+ context = root.querySelector("#field").getContext("2d");
72
+ frameLabel = root.querySelector("#frame");
73
+ makeStars();
74
+ buildBars(root);
75
+ buildSignalPath(root);
76
+ },
77
+
78
+ update({ frame }) {
79
+ const intro = easeOut(frame / 52);
80
+ const signalReveal = easeOut((frame - 96) / 260);
81
+ const width = context.canvas.width;
82
+ const height = context.canvas.height;
83
+
84
+ context.clearRect(0, 0, width, height);
85
+ for (const star of stars) {
86
+ const drift = (frame * star.speed) % width;
87
+ const x = (star.x * width + drift) % width;
88
+ const y = star.y * height;
89
+ context.beginPath();
90
+ context.fillStyle = "rgba(214, 236, 255, " + star.alpha * (0.45 + 0.55 * intro) + ")";
91
+ context.arc(x, y, star.radius, 0, Math.PI * 2);
92
+ context.fill();
93
+ }
94
+
95
+ signalPath.style.strokeDashoffset = String(signalLength * (1 - signalReveal));
96
+ signalPath.style.opacity = String(0.18 + signalReveal * 0.64);
97
+
98
+ const barReveal = clamp((frame - 120) / 240);
99
+ for (let index = 0; index < bars.length; index += 1) {
100
+ const bar = bars[index];
101
+ const local = clamp(barReveal * BAR_COUNT - index);
102
+ const height = bar.height * local;
103
+ bar.element.setAttribute("height", height.toFixed(2));
104
+ bar.element.setAttribute("y", String(BASELINE - height));
105
+ bar.element.setAttribute("opacity", (0.25 + local * 0.62).toFixed(3));
106
+ }
107
+
108
+ frameLabel.textContent = String(frame).padStart(3, "0");
109
+ },
110
+
111
+ dispose() {
112
+ context = undefined;
113
+ signalPath = undefined;
114
+ signalBars = undefined;
115
+ frameLabel = undefined;
116
+ stars = undefined;
117
+ bars = [];
118
+ signalLength = 0;
119
+ },
120
+ };
@@ -0,0 +1,253 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <script type="application/json" data-fy="props">
6
+ {
7
+ "title": "Frameyard",
8
+ "subtitle": "Deterministic video, authored with the web platform.",
9
+ "accent": "#b8f36b"
10
+ }
11
+ </script>
12
+ <style>
13
+ @keyframes orbit {
14
+ from {
15
+ transform: rotate(0deg) translateX(210px) rotate(0deg);
16
+ }
17
+ to {
18
+ transform: rotate(360deg) translateX(210px) rotate(-360deg);
19
+ }
20
+ }
21
+ @keyframes breathe {
22
+ 0%,
23
+ 100% {
24
+ opacity: 0.35;
25
+ transform: scale(1);
26
+ }
27
+ 50% {
28
+ opacity: 0.7;
29
+ transform: scale(1.12);
30
+ }
31
+ }
32
+ @keyframes title-in {
33
+ from {
34
+ opacity: 0;
35
+ transform: translateY(46px);
36
+ }
37
+ to {
38
+ opacity: 1;
39
+ transform: translateY(0);
40
+ }
41
+ }
42
+ @keyframes subtitle-in {
43
+ from {
44
+ opacity: 0;
45
+ transform: translateY(26px);
46
+ }
47
+ to {
48
+ opacity: 1;
49
+ transform: translateY(0);
50
+ }
51
+ }
52
+ @keyframes scene-out {
53
+ from {
54
+ opacity: 1;
55
+ }
56
+ to {
57
+ opacity: 0;
58
+ }
59
+ }
60
+ html,
61
+ body,
62
+ fy-composition {
63
+ background: #080a0d;
64
+ }
65
+ #scene {
66
+ position: relative;
67
+ width: 100%;
68
+ height: 100%;
69
+ overflow: hidden;
70
+ background: #080a0d;
71
+ color: #eef2f7;
72
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
73
+ animation: scene-out 1s linear both;
74
+ }
75
+ #field {
76
+ position: absolute;
77
+ inset: 0;
78
+ width: 100%;
79
+ height: 100%;
80
+ }
81
+ #orb {
82
+ position: absolute;
83
+ top: 46%;
84
+ left: 38%;
85
+ width: 460px;
86
+ height: 460px;
87
+ margin: -230px 0 0 -230px;
88
+ border-radius: 50%;
89
+ background: var(--fy-prop-accent);
90
+ filter: blur(110px);
91
+ opacity: 0.28;
92
+ animation: orbit 30s linear infinite;
93
+ }
94
+ #glow {
95
+ position: absolute;
96
+ top: 8%;
97
+ right: 6%;
98
+ width: 280px;
99
+ height: 280px;
100
+ border-radius: 50%;
101
+ background: var(--fy-prop-accent);
102
+ filter: blur(90px);
103
+ animation: breathe 9s ease-in-out infinite;
104
+ }
105
+ #rail {
106
+ position: absolute;
107
+ top: 92px;
108
+ left: 160px;
109
+ right: 160px;
110
+ height: 1px;
111
+ background: rgba(255, 255, 255, 0.14);
112
+ }
113
+ #eyebrow {
114
+ position: absolute;
115
+ top: 64px;
116
+ left: 160px;
117
+ font:
118
+ 600 14px/1 ui-monospace,
119
+ monospace;
120
+ letter-spacing: 0.28em;
121
+ text-transform: uppercase;
122
+ color: var(--fy-prop-accent);
123
+ }
124
+ #meta {
125
+ position: absolute;
126
+ top: 64px;
127
+ right: 160px;
128
+ font:
129
+ 500 14px/1 ui-monospace,
130
+ monospace;
131
+ letter-spacing: 0.12em;
132
+ color: #788595;
133
+ }
134
+ #copy {
135
+ position: absolute;
136
+ left: 160px;
137
+ top: 236px;
138
+ width: 1320px;
139
+ }
140
+ #title {
141
+ margin: 0;
142
+ font-size: 156px;
143
+ line-height: 0.92;
144
+ letter-spacing: -0.055em;
145
+ font-weight: 680;
146
+ opacity: 0;
147
+ animation: title-in 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
148
+ }
149
+ #subtitle {
150
+ margin: 34px 0 0;
151
+ max-width: 820px;
152
+ font-size: 31px;
153
+ line-height: 1.45;
154
+ color: #96a2b2;
155
+ font-weight: 420;
156
+ opacity: 0;
157
+ animation: subtitle-in 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
158
+ }
159
+ #signal {
160
+ position: absolute;
161
+ left: 0;
162
+ bottom: 0;
163
+ width: 1920px;
164
+ height: 1080px;
165
+ overflow: visible;
166
+ }
167
+ #caption {
168
+ position: absolute;
169
+ left: 160px;
170
+ bottom: 92px;
171
+ font:
172
+ 500 13px/1 ui-monospace,
173
+ monospace;
174
+ letter-spacing: 0.16em;
175
+ color: #7e8b9b;
176
+ }
177
+ #caption strong {
178
+ color: var(--fy-prop-accent);
179
+ font-weight: 600;
180
+ }
181
+ #progress-track {
182
+ position: absolute;
183
+ left: 160px;
184
+ right: 160px;
185
+ bottom: 72px;
186
+ height: 2px;
187
+ background: rgba(255, 255, 255, 0.1);
188
+ }
189
+ #progress {
190
+ position: absolute;
191
+ inset: 0 auto 0 0;
192
+ width: calc(var(--fy-progress) * 100%);
193
+ background: var(--fy-prop-accent);
194
+ box-shadow: 0 0 18px var(--fy-prop-accent);
195
+ }
196
+ #vignette {
197
+ position: absolute;
198
+ inset: 0;
199
+ background: radial-gradient(ellipse at center, transparent 48%, rgba(0, 0, 0, 0.42) 100%);
200
+ pointer-events: none;
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
206
+ <div id="scene" data-fy-anim-start="824" data-fy-anim-duration="70" data-fy-anim-easing="linear">
207
+ <canvas id="field" width="1920" height="1080"></canvas>
208
+ <div id="orb"></div>
209
+ <div id="glow"></div>
210
+ <div id="vignette"></div>
211
+ <div id="rail"></div>
212
+ <div id="eyebrow">Frameyard / Field Notes</div>
213
+ <div id="meta">1920×1080 · 30 FPS · 30.00 S</div>
214
+ <div id="copy">
215
+ <h1
216
+ id="title"
217
+ data-fy-text="title"
218
+ data-fy-anim-start="0"
219
+ data-fy-anim-duration="52"
220
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
221
+ ></h1>
222
+ <p
223
+ id="subtitle"
224
+ data-fy-text="subtitle"
225
+ data-fy-anim-start="26"
226
+ data-fy-anim-duration="62"
227
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
228
+ ></p>
229
+ </div>
230
+ <svg id="signal" viewBox="0 0 1920 1080" fill="none" aria-hidden="true">
231
+ <path
232
+ id="signal-path"
233
+ stroke="var(--fy-prop-accent)"
234
+ stroke-width="3"
235
+ stroke-linecap="round"
236
+ stroke-linejoin="round"
237
+ opacity=".82"
238
+ ></path>
239
+ <g id="signal-bars"></g>
240
+ </svg>
241
+ <div id="caption">FRAME <strong id="frame">000</strong> / 899 · SEEK-SAFE CANVAS + SVG + CSS</div>
242
+ <div id="progress-track"><div id="progress"></div></div>
243
+ </div>
244
+
245
+ <audio src="assets/chime.wav" data-fy-start="18" data-fy-volume="0.5"></audio>
246
+ <audio src="assets/chime.wav" data-fy-start="300" data-fy-volume="0.38"></audio>
247
+ <audio src="assets/chime.wav" data-fy-start="600" data-fy-volume="0.42"></audio>
248
+ <audio src="assets/chime.wav" data-fy-start="840" data-fy-volume="0.55"></audio>
249
+ </fy-composition>
250
+
251
+ <script type="text/fy-controller" src="./demo.controller.js"></script>
252
+ </body>
253
+ </html>
@@ -1,5 +1,5 @@
1
1
  export default {
2
2
  compositions: {
3
- demo: "./compositions/demo.js",
3
+ demo: "./compositions/demo.html",
4
4
  },
5
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameyard",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Local-first, deterministic video composition framework for HTML, CSS, and JavaScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,6 +52,7 @@
52
52
  "node": ">=22.12"
53
53
  },
54
54
  "dependencies": {
55
+ "parse5": "^8.0.1",
55
56
  "playwright": "1.64.1"
56
57
  },
57
58
  "devDependencies": {
@@ -65,12 +66,17 @@
65
66
  "build": "tsc -p tsconfig.json",
66
67
  "typecheck": "tsc -p tsconfig.json --noEmit",
67
68
  "typecheck:test": "tsc -p tsconfig.test.json",
69
+ "check:scripts": "node scripts/check-scripts.mjs",
68
70
  "lint": "oxlint",
69
71
  "format": "oxfmt",
70
72
  "format:check": "oxfmt --check",
73
+ "check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test",
74
+ "check": "pnpm check:static && pnpm test",
71
75
  "test": "pnpm build && vitest run",
72
- "test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/random.test.ts test/scaffold.test.ts test/transcript.test.ts",
76
+ "test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/random.test.ts test/release.test.ts test/scaffold.test.ts test/transcript.test.ts",
73
77
  "bench": "pnpm build && node scripts/bench-render.mjs",
74
- "set-version": "node scripts/set-version.mjs"
78
+ "set-version": "node scripts/set-version.mjs",
79
+ "verify-release": "node scripts/verify-release.mjs",
80
+ "retag-release": "node scripts/retag-release.mjs"
75
81
  }
76
82
  }
@@ -24,6 +24,10 @@ Run the CLI with `npx frameyard`. It resolves the latest published version; pin
24
24
 
25
25
  - `SKILL.md`: trigger description and operating workflow
26
26
  - `references/deterministic-authoring.md`
27
+ - `references/bindings.md`
28
+ - `references/controllers.md`
29
+ - `references/diagnostics.md`
30
+ - `references/common-mistakes.md`
27
31
  - `references/audio-assets.md`
28
32
  - `references/subtitles-karaoke.md`
29
33
  - `references/json-results.md`
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: frameyard
3
- description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML, CSS, and native JavaScript. Use when a user asks to build or operate a Frameyard project, produce parameterized video, add a local TTS or transcription asset through Frameyard's wrappers, or diagnose a Frameyard render. Do not use for general-purpose media generation outside a Frameyard project.
4
- frameyard-api-version: 0.2.0
3
+ description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML, CSS, and an optional JavaScript controller. Use when a user asks to build or operate a Frameyard project, produce parameterized video, add a local TTS or transcription asset through Frameyard's wrappers, or diagnose a Frameyard render. Do not use for general-purpose media generation outside a Frameyard project.
4
+ frameyard-api-version: 0.4.0
5
5
  ---
6
6
 
7
7
  # Frameyard
@@ -11,19 +11,19 @@ Use Frameyard's public project format and CLI. Do not introduce an agent-only ru
11
11
  ## First decisions
12
12
 
13
13
  1. Find the project: look for `video.config.js` from the working directory upward, or run `npx frameyard init <directory>` to scaffold one.
14
- 2. Read the composition entry points listed in `video.config.js`. There is no directory scanning.
15
- 3. Read the existing props file and composition source before changing behavior.
14
+ 2. Read the composition entry points listed in `video.config.js`. They are `.html` documents; there is no directory scanning.
15
+ 3. Read the composition HTML, its optional controller, and the props file before changing behavior.
16
16
  4. If the task needs speech or transcription, confirm that inference is requested or explicitly approved before producing assets.
17
17
 
18
18
  ## Workflow
19
19
 
20
- 1. **Author** the composition with HTML, CSS, SVG, Canvas 2D, and native JavaScript ES modules. Keep `width`, `height`, `fps`, and `durationInFrames` fixed. Read [deterministic authoring](references/deterministic-authoring.md).
21
- 2. **Provide props** as JSON. Validate domain-specific requirements with `validateProps()`.
20
+ 1. **Author** the composition as a full HTML document with one `<fy-composition>` root. Put metadata, default props, markup, CSS animation, bindings, audio, and transcripts in the document. Use an optional controller for Canvas 2D, WebGL, karaoke, or per-frame computation CSS cannot express. Read [deterministic authoring](references/deterministic-authoring.md), [bindings](references/bindings.md), and [controllers](references/controllers.md).
21
+ 2. **Provide props** as JSON. Validate domain-specific requirements with a controller's `validateProps()`.
22
22
  3. **Produce optional assets** with `npx frameyard tts` and `npx frameyard transcribe`, never with the upstream executables. Read [audio assets](references/audio-assets.md). Do not overwrite or regenerate existing generated assets without permission.
23
- 4. **Validate** with `npx frameyard check <id> --json`. It evaluates every frame, checks audio and declared transcripts, and does not encode.
23
+ 4. **Validate** with `npx frameyard check <id> --json`. Static validation runs before Chromium; then every frame is evaluated. Read the structured `diagnostics` array. Read [diagnostics](references/diagnostics.md) and [common mistakes](references/common-mistakes.md).
24
24
  5. **Inspect** representative frames with `npx frameyard still <id> --frame <n> --out <file.png> --json`. View the PNGs when the host supports image viewing. If visual inspection is impossible, say so explicitly.
25
- 6. **Render** with `npx frameyard render <id> --out <file.mp4> [--props <file>] --json`.
26
- 7. **Report** the project path, composition entry, props, generated assets, provenance manifest, validation results, still paths and inspection status, final video path, and any failures.
25
+ 6. **Render** with `npx frameyard render <id> --out <file.mp4> [--props <file>] [--concurrency <n>] --json`.
26
+ 7. **Report** the project path, composition entry, controller, props, generated assets, provenance manifest, validation results, diagnostics, still paths and inspection status, final video path, and any failures.
27
27
 
28
28
  Never claim success when `check` fails, a still is missing, a render exits nonzero, or visual inspection was skipped. A partial or unpublished render is not a successful result.
29
29
 
@@ -32,21 +32,39 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
32
32
  - Use `--json` for every non-interactive command. Parse stdout as the structured result; treat stderr as progress and diagnostics.
33
33
  - Keep the current output files unless the user explicitly approves replacement. Use `--force` only with that approval.
34
34
  - Use `--project <dir>` when not running from the project root.
35
+ - Use `--concurrency <n>` only when faster local rendering is useful; the default is `1` and the maximum is `16`. It changes capture scheduling only, not composition behavior or output frame order.
35
36
  - Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
36
37
  - Read [JSON results](references/json-results.md) for result and error shapes, and [failure recovery](references/failure-recovery.md) for actionable error codes.
37
38
 
39
+ ## Authoring rules
40
+
41
+ - A composition is a full HTML document with one `<fy-composition width height fps duration-in-frames>` root and a `data-fy="props"` default-props block.
42
+ - Bindings are explicit: `data-fy-text`, `data-fy-attr-*`, `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat` on `<template>`. Primitive top-level props are also `--fy-prop-<name>` CSS variables.
43
+ - CSS `@keyframes` and the Web Animations API are seeked from the frame clock. `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in frames.
44
+ - Frame-state variables on `:root`: `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`.
45
+ - A composition has exactly one optional controller, inline or external. It may export `mount`, `update`, `dispose`, and `validateProps`; at least one is required. Use inline for small controllers; extract to an external module for Canvas 2D, WebGL, karaoke, reuse, tests, or a large controller.
46
+ - The controller is the only executable code. Other executable scripts and `on*` handlers are rejected.
47
+ - Media and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
48
+ - Embedded video/audio use `data-fy-start` (frames), `data-fy-trim` (seconds), optional `data-fy-duration` (seconds), and `data-fy-volume` (0-1). Transcripts use `<fy-transcript src words>`.
49
+
38
50
  ## Determinism guardrails
39
51
 
40
52
  - A frame may depend only on its integer `frame`, immutable `props`, and assets loaded before frame production.
41
53
  - Use `random("stable:key")` from `frameyard/runtime`; never use `Math.random()`.
42
54
  - Never use `Date.now()`, `performance.now()`, timers, independent `requestAnimationFrame` loops, or CSS transitions as animation inputs.
43
- - Finish remote data, fonts, and image loading during `mount()` or before the first frame. Network requests are blocked during frame capture.
55
+ - Finish remote data, fonts, and image loading during the loading phase. Network requests are blocked during frame capture.
44
56
  - Keep `update({ frame, props })` synchronous and seek-safe. Do not carry state between frames.
57
+ - Let the runtime seek embedded `<video>` elements. Do not play, pause, or set `currentTime` in `update()`.
58
+ - Redraw WebGL canvases from the current frame. Production pins software WebGL; do not depend on GPU-specific results or the previous frame's drawing buffer.
45
59
  - Do not run model inference inside `mount()`, `update()`, or frame production.
46
60
 
61
+ ## Diagnostics rule
62
+
63
+ `check`, `still`, and `render` run static composition validation before Chromium. Errors fail before a browser starts; warnings allow frame evaluation. Every diagnostic has a stable `code`, a `message`, and optional `details` with source locations and suggestions. Treat warnings as actionable. Do not ignore `BINDING_PATH_MISSING`, `ANIMATION_NOT_FOUND`, `ANIMATION_MULTIPLE`, or `DETERMINISM_WARNING`.
64
+
47
65
  ## Subtitle and karaoke rule
48
66
 
49
- Use canonical transcripts produced by `npx frameyard transcribe`. Convert integer source-relative milliseconds to composition frames with the audio clip's `startFrame`, `trimStart`, and the composition `fps`. Karaoke highlighting requires actual `words` timings; never interpolate segment timings. Read [subtitles and karaoke](references/subtitles-karaoke.md).
67
+ Use canonical transcripts produced by `npx frameyard transcribe`. Declare them with `<fy-transcript src="assets/voice.transcript.json" words>`. Convert integer source-relative milliseconds to composition frames with the audio clip's start frame, trim start, and the composition `fps`. Karaoke highlighting requires actual `words` timings; never interpolate segment timings. Read [subtitles and karaoke](references/subtitles-karaoke.md).
50
68
 
51
69
  ## CLI invocation
52
70
 
@@ -58,3 +76,15 @@ node scripts/frameyard.mjs check <id> --json # fallback
58
76
  ```
59
77
 
60
78
  When the CLI version differs from `frameyard-api-version`, pin it with `npx frameyard@<version>`.
79
+
80
+ ## References
81
+
82
+ - [Deterministic HTML authoring](references/deterministic-authoring.md)
83
+ - [Bindings](references/bindings.md)
84
+ - [Controllers](references/controllers.md)
85
+ - [Diagnostics](references/diagnostics.md)
86
+ - [Common mistakes](references/common-mistakes.md)
87
+ - [Audio and generated assets](references/audio-assets.md)
88
+ - [Subtitles and karaoke](references/subtitles-karaoke.md)
89
+ - [JSON results](references/json-results.md)
90
+ - [Failure recovery](references/failure-recovery.md)
@@ -1,25 +1,36 @@
1
1
  # Audio and generated assets
2
2
 
3
- Audio clips are a declarative timeline. Generated speech and transcripts are frozen local inputs produced before composition loading.
3
+ Audio clips are a declarative `<audio>` timeline. Generated speech and transcripts are frozen local inputs produced before composition loading.
4
4
 
5
5
  ## Audio timeline
6
6
 
7
- ```js
8
- audio: [
9
- {
10
- src: "assets/voice.wav", // project-relative local file
11
- startFrame: 30, // composition timeline position
12
- trimStart: 0.25, // seconds trimmed from the source
13
- trimDuration: 4.2, // optional seconds to use
14
- volume: 0.8, // 0..1
15
- },
16
- ];
7
+ ```html
8
+ <audio
9
+ src="assets/voice.wav"
10
+ data-fy-start="30"
11
+ data-fy-trim="0.25"
12
+ data-fy-duration="4.2"
13
+ data-fy-volume="0.8"
14
+ ></audio>
17
15
  ```
18
16
 
19
- - Placement is in composition frames.
20
- - Trimming is in seconds from the source.
17
+ - `src` resolves from the project root.
18
+ - `data-fy-start` is the composition timeline position in frames.
19
+ - `data-fy-trim` is seconds trimmed from the source.
20
+ - `data-fy-duration` optionally limits how many source seconds are used.
21
+ - `data-fy-volume` is `0..1`.
21
22
  - Frameyard never time-stretches audio or changes composition duration to fit it.
22
- - `check` reports clip durations and overrun warnings.
23
+ - `check` reports clip durations and overrun diagnostics.
24
+
25
+ An unmuted `<video>` contributes its audio track to the same mix. Use `muted` or `data-fy-audio="off"` for a visual-only video.
26
+
27
+ Declare required transcripts with:
28
+
29
+ ```html
30
+ <fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
31
+ ```
32
+
33
+ `words` requires measured word timings. See [subtitles and karaoke](subtitles-karaoke.md).
23
34
 
24
35
  ## Local speech synthesis
25
36
 
@@ -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.