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,215 +1,207 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <script type="application/json" data-fy="props">
6
- {
7
- "kicker": "Shader Study / 001",
8
- "title": "Liquid Light",
9
- "subtitle": "A frame-driven WebGL field, resolved in software so every captured pixel stays reproducible.",
10
- "accent": "#78f0c8"
11
- }
12
- </script>
13
- <style>
14
- @keyframes rise {
15
- from {
16
- opacity: 0;
17
- transform: translateY(44px);
18
- }
19
- to {
20
- opacity: 1;
21
- transform: translateY(0);
22
- }
23
- }
24
- @keyframes fade {
25
- from {
26
- opacity: 0;
27
- }
28
- to {
29
- opacity: 1;
30
- }
31
- }
32
- @keyframes draw {
33
- from {
34
- opacity: 0;
35
- transform: scaleX(0);
36
- }
37
- to {
38
- opacity: 1;
39
- transform: scaleX(1);
40
- }
41
- }
42
- html,
43
- body,
44
- fy-composition {
45
- background: #060a0e;
46
- }
47
- #scene {
48
- position: relative;
49
- width: 100%;
50
- height: 100%;
51
- overflow: hidden;
52
- background: #060a0e;
53
- color: #edf6f3;
54
- font-family: Inter, ui-sans-serif, system-ui, sans-serif;
55
- }
56
- #field {
57
- position: absolute;
58
- inset: 0;
59
- display: block;
60
- width: 100%;
61
- height: 100%;
62
- }
63
- #scrim {
64
- position: absolute;
65
- inset: 0;
66
- background: linear-gradient(
67
- 90deg,
68
- rgba(4, 8, 11, 0.94) 0%,
69
- rgba(4, 8, 11, 0.78) 34%,
70
- rgba(4, 8, 11, 0.18) 66%,
71
- rgba(4, 8, 11, 0) 100%
72
- );
73
- }
74
- #vignette {
75
- position: absolute;
76
- inset: 0;
77
- background: radial-gradient(ellipse at 60% 45%, transparent 34%, rgba(0, 0, 0, 0.46) 100%);
78
- pointer-events: none;
79
- }
80
- #kicker {
81
- position: absolute;
82
- top: 72px;
83
- left: 148px;
84
- display: flex;
85
- align-items: center;
86
- gap: 16px;
87
- font:
88
- 600 14px/1 ui-monospace,
89
- monospace;
90
- letter-spacing: 0.26em;
91
- text-transform: uppercase;
92
- color: var(--fy-prop-accent);
93
- animation: fade 1s linear both;
94
- }
95
- #kicker::before {
96
- content: "";
97
- width: 8px;
98
- height: 8px;
99
- background: var(--fy-prop-accent);
100
- box-shadow: 0 0 18px var(--fy-prop-accent);
101
- }
102
- #meta {
103
- position: absolute;
104
- top: 72px;
105
- right: 148px;
106
- text-align: right;
107
- font:
108
- 500 13px/1.9 ui-monospace,
109
- monospace;
110
- letter-spacing: 0.12em;
111
- color: #80908e;
112
- animation: fade 1s linear both;
113
- }
114
- #meta strong {
115
- color: #dce9e6;
116
- font-weight: 500;
117
- }
118
- #title {
119
- position: absolute;
120
- top: 292px;
121
- left: 148px;
122
- width: 780px;
123
- margin: 0;
124
- font-size: 168px;
125
- line-height: 0.88;
126
- letter-spacing: -0.062em;
127
- font-weight: 660;
128
- animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
129
- }
130
- #subtitle {
131
- position: absolute;
132
- top: 604px;
133
- left: 152px;
134
- width: 720px;
135
- margin: 0;
136
- font-size: 30px;
137
- line-height: 1.45;
138
- font-weight: 420;
139
- color: #96a7a3;
140
- animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
141
- }
142
- #footer {
143
- position: absolute;
144
- left: 148px;
145
- right: 148px;
146
- bottom: 64px;
147
- animation: fade 1s linear both;
148
- }
149
- #rule {
150
- position: relative;
151
- height: 1px;
152
- background: rgba(237, 246, 243, 0.14);
153
- transform-origin: left center;
154
- animation: draw 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
155
- }
156
- #progress {
157
- position: absolute;
158
- inset: 0 auto 0 0;
159
- width: calc(var(--fy-progress) * 100%);
160
- background: var(--fy-prop-accent);
161
- box-shadow: 0 0 16px var(--fy-prop-accent);
162
- }
163
- #footer-row {
164
- display: flex;
165
- justify-content: space-between;
166
- align-items: baseline;
167
- gap: 24px;
168
- margin-top: 18px;
169
- font:
170
- 500 13px/1 ui-monospace,
171
- monospace;
172
- letter-spacing: 0.14em;
173
- color: #758581;
174
- animation: fade 1s linear both;
175
- }
176
- #footer-row strong {
177
- color: var(--fy-prop-accent);
178
- font-weight: 600;
179
- }
180
- #footer-meta {
181
- display: flex;
182
- gap: 28px;
183
- }
184
- </style>
185
- </head>
186
- <body>
187
- <fy-composition width="1920" height="1080" fps="30" duration-in-frames="360">
188
- <div id="scene">
189
- <canvas id="field" width="1920" height="1080"></canvas>
190
- <div id="scrim"></div>
191
- <div id="vignette"></div>
192
- <div id="kicker" data-fy-text="kicker" data-fy-anim-start="0" data-fy-anim-duration="28"></div>
193
- <div id="meta" data-fy-anim-start="18" data-fy-anim-duration="34">
194
- <div><strong>WEBGL 1.0</strong> / SWIFTSHADER</div>
195
- <div>1920 × 1080 · 30 FPS</div>
196
- <div>12.00 S · 360 FRAMES</div>
197
- </div>
198
- <h1 id="title" data-fy-text="title" data-fy-anim-start="6" data-fy-anim-duration="56"></h1>
199
- <p id="subtitle" data-fy-text="subtitle" data-fy-anim-start="34" data-fy-anim-duration="52"></p>
200
- <div id="footer" data-fy-anim-start="28" data-fy-anim-duration="30">
201
- <div id="rule" data-fy-anim-start="38" data-fy-anim-duration="60">
202
- <div id="progress"></div>
203
- </div>
204
- <div id="footer-row" data-fy-anim-start="44" data-fy-anim-duration="30">
205
- <span>FRAME <strong id="frame">000</strong> / <span id="total">359</span></span>
206
- <span id="clock">0.00 S</span>
207
- <span id="footer-meta"><span>FRAME-SEEKED</span><span>NO RAF</span></span>
208
- </div>
209
- </div>
1
+ <script type="application/json" data-fy="props">
2
+ {
3
+ "kicker": "Shader Study / 001",
4
+ "title": "Liquid Light",
5
+ "subtitle": "A frame-driven WebGL field, resolved in software so every captured pixel stays reproducible.",
6
+ "accent": "#78f0c8"
7
+ }
8
+ </script>
9
+ <style>
10
+ @keyframes rise {
11
+ from {
12
+ opacity: 0;
13
+ transform: translateY(44px);
14
+ }
15
+ to {
16
+ opacity: 1;
17
+ transform: translateY(0);
18
+ }
19
+ }
20
+ @keyframes fade {
21
+ from {
22
+ opacity: 0;
23
+ }
24
+ to {
25
+ opacity: 1;
26
+ }
27
+ }
28
+ @keyframes draw {
29
+ from {
30
+ opacity: 0;
31
+ transform: scaleX(0);
32
+ }
33
+ to {
34
+ opacity: 1;
35
+ transform: scaleX(1);
36
+ }
37
+ }
38
+ html,
39
+ body,
40
+ fy-composition {
41
+ background: #060a0e;
42
+ }
43
+ #scene {
44
+ position: relative;
45
+ width: 100%;
46
+ height: 100%;
47
+ overflow: hidden;
48
+ background: #060a0e;
49
+ color: #edf6f3;
50
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
51
+ }
52
+ #field {
53
+ position: absolute;
54
+ inset: 0;
55
+ display: block;
56
+ width: 100%;
57
+ height: 100%;
58
+ }
59
+ #scrim {
60
+ position: absolute;
61
+ inset: 0;
62
+ background: linear-gradient(
63
+ 90deg,
64
+ rgba(4, 8, 11, 0.94) 0%,
65
+ rgba(4, 8, 11, 0.78) 34%,
66
+ rgba(4, 8, 11, 0.18) 66%,
67
+ rgba(4, 8, 11, 0) 100%
68
+ );
69
+ }
70
+ #vignette {
71
+ position: absolute;
72
+ inset: 0;
73
+ background: radial-gradient(ellipse at 60% 45%, transparent 34%, rgba(0, 0, 0, 0.46) 100%);
74
+ pointer-events: none;
75
+ }
76
+ #kicker {
77
+ position: absolute;
78
+ top: 72px;
79
+ left: 148px;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 16px;
83
+ font:
84
+ 600 14px/1 ui-monospace,
85
+ monospace;
86
+ letter-spacing: 0.26em;
87
+ text-transform: uppercase;
88
+ color: var(--fy-prop-accent);
89
+ animation: fade 1s linear both;
90
+ }
91
+ #kicker::before {
92
+ content: "";
93
+ width: 8px;
94
+ height: 8px;
95
+ background: var(--fy-prop-accent);
96
+ box-shadow: 0 0 18px var(--fy-prop-accent);
97
+ }
98
+ #meta {
99
+ position: absolute;
100
+ top: 72px;
101
+ right: 148px;
102
+ text-align: right;
103
+ font:
104
+ 500 13px/1.9 ui-monospace,
105
+ monospace;
106
+ letter-spacing: 0.12em;
107
+ color: #80908e;
108
+ animation: fade 1s linear both;
109
+ }
110
+ #meta strong {
111
+ color: #dce9e6;
112
+ font-weight: 500;
113
+ }
114
+ #title {
115
+ position: absolute;
116
+ top: 292px;
117
+ left: 148px;
118
+ width: 780px;
119
+ margin: 0;
120
+ font-size: 168px;
121
+ line-height: 0.88;
122
+ letter-spacing: -0.062em;
123
+ font-weight: 660;
124
+ animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
125
+ }
126
+ #subtitle {
127
+ position: absolute;
128
+ top: 604px;
129
+ left: 152px;
130
+ width: 720px;
131
+ margin: 0;
132
+ font-size: 30px;
133
+ line-height: 1.45;
134
+ font-weight: 420;
135
+ color: #96a7a3;
136
+ animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
137
+ }
138
+ #footer {
139
+ position: absolute;
140
+ left: 148px;
141
+ right: 148px;
142
+ bottom: 64px;
143
+ animation: fade 1s linear both;
144
+ }
145
+ #rule {
146
+ position: relative;
147
+ height: 1px;
148
+ background: rgba(237, 246, 243, 0.14);
149
+ transform-origin: left center;
150
+ animation: draw 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
151
+ }
152
+ #progress {
153
+ position: absolute;
154
+ inset: 0 auto 0 0;
155
+ width: calc(var(--fy-progress) * 100%);
156
+ background: var(--fy-prop-accent);
157
+ box-shadow: 0 0 16px var(--fy-prop-accent);
158
+ }
159
+ #footer-row {
160
+ display: flex;
161
+ justify-content: space-between;
162
+ align-items: baseline;
163
+ gap: 24px;
164
+ margin-top: 18px;
165
+ font:
166
+ 500 13px/1 ui-monospace,
167
+ monospace;
168
+ letter-spacing: 0.14em;
169
+ color: #758581;
170
+ animation: fade 1s linear both;
171
+ }
172
+ #footer-row strong {
173
+ color: var(--fy-prop-accent);
174
+ font-weight: 600;
175
+ }
176
+ #footer-meta {
177
+ display: flex;
178
+ gap: 28px;
179
+ }
180
+ </style>
181
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="360">
182
+ <div id="scene">
183
+ <canvas id="field" width="1920" height="1080"></canvas>
184
+ <div id="scrim"></div>
185
+ <div id="vignette"></div>
186
+ <div id="kicker" data-fy-text="kicker" data-fy-anim-start="0" data-fy-anim-duration="28"></div>
187
+ <div id="meta" data-fy-anim-start="18" data-fy-anim-duration="34">
188
+ <div><strong>WEBGL 1.0</strong> / SWIFTSHADER</div>
189
+ <div>1920 × 1080 · 30 FPS</div>
190
+ <div>12.00 S · 360 FRAMES</div>
191
+ </div>
192
+ <h1 id="title" data-fy-text="title" data-fy-anim-start="6" data-fy-anim-duration="56"></h1>
193
+ <p id="subtitle" data-fy-text="subtitle" data-fy-anim-start="34" data-fy-anim-duration="52"></p>
194
+ <div id="footer" data-fy-anim-start="28" data-fy-anim-duration="30">
195
+ <div id="rule" data-fy-anim-start="38" data-fy-anim-duration="60">
196
+ <div id="progress"></div>
210
197
  </div>
211
- </fy-composition>
198
+ <div id="footer-row" data-fy-anim-start="44" data-fy-anim-duration="30">
199
+ <span>FRAME <strong id="frame">000</strong> / <span id="total">359</span></span>
200
+ <span id="clock">0.00 S</span>
201
+ <span id="footer-meta"><span>FRAME-SEEKED</span><span>NO RAF</span></span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </fy-composition>
212
206
 
213
- <script type="text/fy-controller" src="./webgl.controller.js"></script>
214
- </body>
215
- </html>
207
+ <script type="text/fy-controller" src="./webgl.controller.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameyard",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Local-first, deterministic video composition framework for HTML, CSS, and JavaScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -79,6 +79,7 @@
79
79
  "test:preview": "pnpm build && vitest run test/preview-runtime.test.ts test/timeline-preview.test.ts test/timeline-preview-runtime.test.ts",
80
80
  "test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/encoders.test.ts test/random.test.ts test/release.test.ts test/scaffold.test.ts test/transcript.test.ts",
81
81
  "bench": "pnpm build && node scripts/bench-render.mjs",
82
+ "bench:static": "pnpm build && node bench/static-composition.mjs",
82
83
  "set-version": "node scripts/set-version.mjs",
83
84
  "verify-release": "node scripts/verify-release.mjs",
84
85
  "retag-release": "node scripts/retag-release.mjs"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: frameyard
3
3
  description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML compositions and JSON timelines. Use when a user asks to build or operate a Frameyard project, produce parameterized video, assemble clips into a timeline, 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.7.0
4
+ frameyard-api-version: 0.8.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 `format`, `compositions`, and `timelines` maps in `video.config.js`. Entries are explicit `.html` and `.json` documents; there is no directory scanning.
15
- 3. Read the timeline JSON, the referenced composition HTML, its optional controller, and the props files before changing behavior.
14
+ 2. Read the `format`, `compositions`, and `timelines` maps in `video.config.js`. Entries are explicit `.html` composition fragments and `.json` timeline documents; there is no directory scanning.
15
+ 3. Read the timeline JSON, the referenced composition fragment, its optional controller, and the props files 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 compositions** as full HTML documents 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. **Assemble the timeline** in JSON: project format, video/audio tracks, composition, media, and nested-timeline clips, source ranges, static transforms, and audio. Read [timelines](references/timelines.md).
20
+ 1. **Author compositions** as partial HTML fragments with one top-level `<fy-composition>` root. Put metadata, default props, markup, CSS animation, bindings, audio, and transcripts in the fragment. Do not add `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`; Frameyard generates the document shell. Use an optional controller for Canvas 2D, WebGL, karaoke, or per-frame computation CSS cannot express. Omit both `duration-in-frames` and `fps` only when the composition is intentionally static; a static composition is captured once, renders to PNG, and cannot use animation scheduling, media, or transcripts. Read [deterministic authoring](references/deterministic-authoring.md), [bindings](references/bindings.md), and [controllers](references/controllers.md).
21
+ 2. **Assemble the timeline** in JSON: project format, video/audio tracks, composition, media, and nested-timeline clips, source ranges, static transforms and blend modes, and audio. Read [timelines](references/timelines.md).
22
22
  3. **Provide props** as JSON. Composition props merge over defaults; timeline render props are a map keyed by composition clip ID. Validate domain-specific requirements with a controller's `validateProps()`.
23
23
  4. **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.
24
- 5. **Validate** with `npx frameyard check <id> --json`, where `<id>` is a timeline or composition. 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
+ 5. **Validate** with `npx frameyard check <id> --json`, where `<id>` is a timeline or composition. Static validation runs before Chromium; then every animated frame is evaluated, or a static composition's single state is inspected once. Read the structured `diagnostics` array and the result `kind`. Read [diagnostics](references/diagnostics.md) and [common mistakes](references/common-mistakes.md).
25
25
  6. **Inspect** representative frames with `npx frameyard still <id> --frame <spec> --out <path> --json`, where `<spec>` is a frame number, an inclusive range like `2-12`, or a comma-separated list with at most 32 unique frames. With one frame, `--out` is a `.png` file; with multiple frames, it is a directory and the files are `still-<id>-<frame>.png`. View the PNGs when the host supports image viewing. If visual inspection is impossible, say so explicitly.
26
- 7. **Render** with `npx frameyard render <id> [--out <file>] [--props <file>] --json`. Composition renders accept `--concurrency <n>`; timeline renders accept `--workers <n>` and `--scratch <dir>`. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default, WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded, defaulting to `auto`. The output extension must match the container. The default output is `out/<id>.<ext>` in the project root.
26
+ 7. **Render** with `npx frameyard render <id> [--out <file>] [--props <file>] --json`. Animated composition renders accept `--concurrency <n>`; timeline renders accept `--workers <n>` and `--scratch <dir>`. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default, WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded, defaulting to `auto`. The output extension must match the container. The default output is `out/<id>.<ext>` for video and `out/<id>.png` for a static composition. A static composition render rejects `--container`, `--codec`, `--encoder`, and `--concurrency`; use a timeline when the deliverable must be video.
27
27
  8. **Report** the project path, timeline and composition entries, controller, props, generated assets, provenance manifest, validation results, diagnostics, still paths and inspection status, final video path, and any failures.
28
28
 
29
29
  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.
@@ -33,9 +33,9 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
33
33
  - Use `--json` for every non-interactive command. Parse stdout as the structured result; treat stderr as progress and diagnostics.
34
34
  - Keep the current output files unless the user explicitly approves replacement. Use `--force` only with that approval.
35
35
  - Use `--project <dir>` when not running from the project root.
36
- - `render` writes to `out/<id>.mp4` in the project root unless `--out` is passed. Explicit `--out` paths are relative to the current working directory.
37
- - Use `--concurrency <n>` only for composition renders; the default is `1` and the maximum is `16`. Use `--workers <n>` for timeline renders; the default is `8`. Timeline renders also accept `--scratch <dir>` for FFV1 intermediates.
38
- - `still --frame <spec>` accepts `n`, `n-m`, or a comma-separated list; ranges are inclusive, duplicates are removed preserving the first occurrence, order is preserved, and one call captures at most 32 unique frames. Do not use `still` as an image-sequence exporter. With multiple frames, `--out` is a directory and files are named `still-<id>-<frame>.png`; with one frame, `--out` is a `.png` file.
36
+ - `render` writes to `out/<id>.mp4` in the project root for timelines and animated compositions, and to `out/<id>.png` for static compositions, unless `--out` is passed. Explicit `--out` paths are relative to the current working directory.
37
+ - Use `--concurrency <n>` only for animated composition renders; the default is `1` and the maximum is `16`. Use `--workers <n>` for timeline renders; the default is `8`. Timeline renders also accept `--scratch <dir>` for FFV1 intermediates.
38
+ - `still --frame <spec>` accepts `n`, `n-m`, or a comma-separated list; ranges are inclusive, duplicates are removed preserving the first occurrence, order is preserved, and one call captures at most 32 unique frames. A static composition exposes only frame 0. Do not use `still` as an image-sequence exporter. With multiple frames, `--out` is a directory and files are named `still-<id>-<frame>.png`; with one frame, `--out` is a `.png` file.
39
39
  - `--encoder` selects the encoder: `auto` (default) probes NVENC, then Quick Sync, then the codec's software encoder; `software` forces `libx264`, `libx265`, or `libvpx-vp9`; `nvenc` and `qsv` force a vendor. `--codec <h264|hevc|vp9>` and `--container <mp4|mov|mkv|webm>` select the delivery pair: MP4 and MOV hold H.264/HEVC with AAC, MKV holds H.264/HEVC/VP9, and WebM holds VP9 with Opus and defaults to VP9. `--out` must use the container's extension. The probe is a real one-frame encode. Encoders, codecs, and containers change encoded bytes and file size, not captured frames; do not switch them to fix a visual difference. With `nvenc`, timeline `--workers` is capped at eight, the consumer driver session limit, and a larger value reports an `ENCODER_WARNING`. HEVC MP4/MOV files use the `hvc1` tag and do not play in browsers without platform HEVC decoding.
40
40
  - For a timeline, `--props` is a JSON object keyed by top-level composition clip ID. For a composition, it is the props object.
41
41
  - Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
@@ -43,15 +43,17 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
43
43
 
44
44
  ## Authoring rules
45
45
 
46
- - 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.
46
+ - A composition is a partial HTML fragment with one top-level `<fy-composition>` root and a top-level `data-fy="props"` default-props block. 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. Do not use the reserved `data-fy-host` attribute or `data-fy="format"` block. `width`, `height`, and `fps` are optional and inherit from the project `format`; declare them to override it.
47
+ - A composition with neither `duration-in-frames` nor `fps` is static. It is captured once and renders to PNG. It may use `<img>`, CSS, SVG, and canvas, but not `data-fy-anim-*`, `<video>`, `<audio>`, or `<fy-transcript>`. Its controller runs `mount()` and `validateProps()`; `update()` is not called and frame-state variables are not set. CSS animations are paused at time 0. Declaring `fps` without `duration-in-frames` is an error. A static composition clip in a timeline needs an explicit `duration` and `sourceIn: 0`.
47
48
  - A timeline is a versioned JSON document with explicit tracks and clips. Sources are composition IDs, nested timeline IDs, or local video/image/audio paths. Timeline frames, sourceIn, and duration are integer frames. No remote URLs.
48
49
  - 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.
49
50
  - 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.
50
51
  - Frame-state variables on `:root`: `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`.
51
52
  - 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.
52
53
  - The controller is the only executable code. Other executable scripts and `on*` handlers are rejected.
53
- - Media and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
54
+ - Media and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition URL.
54
55
  - 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>`.
56
+ - An optional top-level `<script type="importmap">` may add bare specifiers for controllers. Frameyard hoists it before the host module; `frameyard` and `frameyard/` specifiers are reserved.
55
57
 
56
58
  ## Determinism guardrails
57
59
 
@@ -66,7 +68,7 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
66
68
 
67
69
  ## Diagnostics rule
68
70
 
69
- `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`.
71
+ `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`, `STATIC_UPDATE_IGNORED`, `STATIC_ANIMATION_PAUSED`, or `DETERMINISM_WARNING`. `STATIC_ANIMATION_UNSUPPORTED` and `STATIC_MEDIA_UNSUPPORTED` mean the composition should be animated with `duration-in-frames`/`fps` or the media should move to a timeline.
70
72
 
71
73
  ## Subtitle and karaoke rule
72
74
 
@@ -2,14 +2,19 @@
2
2
 
3
3
  ## Using the old authoring model
4
4
 
5
- - **JavaScript composition modules / `defineComposition`** were removed in 0.4. A composition is a `.html` document listed in `video.config.js`.
5
+ - **JavaScript composition modules / `defineComposition`** were removed in 0.4. A composition is a `.html` fragment listed in `video.config.js`.
6
6
  - **`data-frameyard-*`** media attributes were removed in 0.4. Use `data-fy-*`.
7
7
 
8
8
  ## Document structure
9
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.
10
+ - Adding `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`; compositions are fragments and the framework owns the shell (`COMPOSITION_DOCUMENT_SHELL`).
11
+ - Missing/multiple `<fy-composition>` roots, a root nested inside another element, or missing `duration-in-frames` on an animated composition. `width`, `height`, and `fps` may be omitted when the project declares `format`, but declaring only one of `width` and `height` is an error.
12
+ - Forgetting both `duration-in-frames` and `fps` on a composition that should animate; that composition is now static and renders to PNG. Declaring `fps` without `duration-in-frames` is an error.
13
+ - Rendering a composition that declares no dimensions in a project without `format`; add `width`/`height` or the project `format` (`COMPOSITION_FORMAT_UNRESOLVED`).
14
+ - Missing the top-level `data-fy="props"` block when the composition relies on default props; a nested props block is rejected.
15
+ - Putting props, the controller, or an import map inside `<fy-composition>` instead of at the top level.
16
+ - Using the reserved `data-fy-host` attribute or `data-fy="format"` block.
17
+ - Declaring an import map with `frameyard` or `frameyard/` specifiers; those are reserved (`IMPORT_MAP_INVALID`).
13
18
  - Visual content outside `<fy-composition>`; it is not captured.
14
19
  - Using `<base>`, refresh/CSP meta tags, `<iframe>`, `<object>`, or `<embed>`.
15
20
 
@@ -46,10 +51,28 @@
46
51
 
47
52
  - Using remote, live, or unseekable media sources.
48
53
  - 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()`.
54
+ - Adding or changing media in `update()`; prepare it in the fragment or in `mount()`.
50
55
  - Calling `play()`, `pause()`, or setting `currentTime` on a video.
51
56
  - 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.
57
+ - Assuming the controller `src` is project-root-relative. It is relative to the composition URL.
58
+
59
+ ## Timelines
60
+
61
+ - Putting `blend` on an audio clip. It is only valid on visual clips.
62
+ - Using `hue`, `saturation`, `color`, or `luminosity`. They need cross-channel math and are rejected; use one of the separable CSS modes.
63
+ - Expecting a blend on a transparent nested timeline to see the parent's layers. A nested timeline is its own document and blends only against its own backdrop.
64
+ - Expecting a blend on a composition with an opaque background to reach lower tracks. The composition's alpha is opaque; leave its background transparent.
65
+ - Expecting preview and final render blend pixels to match exactly. CSS is the reference and FFmpeg approximates it; the guarantee is at clip capture.
66
+ - Omitting `duration` on a static composition clip, or setting `sourceIn` above 0. A static source has no intrinsic duration or frames.
67
+ - Expecting a static composition clip to contribute audio or transcripts. It is visual-only.
68
+
69
+ ## Static compositions
70
+
71
+ - Expecting `update()` or `--fy-*` frame-state variables to run. A static composition only runs `mount()` and prop validation; use CSS, markup, or mount-time canvas drawing.
72
+ - Adding `data-fy-anim-*`, `<video>`, `<audio>`, or `<fy-transcript>` to a static composition. These are `STATIC_ANIMATION_UNSUPPORTED` or `STATIC_MEDIA_UNSUPPORTED`.
73
+ - Relying on CSS animations or transitions at capture time; they are paused at 0 with `STATIC_ANIMATION_PAUSED`.
74
+ - Passing `--container`, `--codec`, `--encoder`, or `--concurrency` to `render` for a static composition. Use a timeline image clip if the deliverable must be video.
75
+ - Requesting a still frame other than 0 from a static composition.
53
76
 
54
77
  ## Validation
55
78
 
@@ -36,17 +36,18 @@ export default {
36
36
  };
37
37
  ```
38
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.
39
+ Exactly one controller is allowed, and it must be a top-level sibling of `<fy-composition>` in the fragment. 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
40
 
41
41
  ## Lifecycle
42
42
 
43
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
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.
45
+ - `dispose()` releases resources and resets module-scope state. The host calls it before remounting or disposing the composition host.
46
+ - A static composition runs `mount()` and `validateProps()` only. `update()` is not called and frame-state variables are not set; the host reports `STATIC_UPDATE_IGNORED`. Do not create media in a static controller: the host rejects it with `STATIC_MEDIA_UNSUPPORTED`. Draw canvas/WebGL content once from `mount()`.
46
47
  - `validateProps(props)` is optional and synchronous. Return an array of human-readable errors, or `void`.
47
48
  - A validation-only controller may export just `validateProps`.
48
49
 
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
+ Keep mutable state in module scope, never on `this`. The controller module is imported once per page; `load` remounts it without re-importing, so `dispose` must reset module state.
50
51
 
51
52
  ## Props validation
52
53
 
@@ -105,7 +106,7 @@ Never use `Math.random()`, `Date.now()`, `performance.now()`, timers, independen
105
106
 
106
107
  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
 
108
- Media and transcript `src` values resolve from the project root. The controller `src` resolves relative to the composition document.
109
+ Media and transcript `src` values resolve from the project root. The controller `src` resolves relative to the composition URL.
109
110
 
110
111
  ## Karaoke
111
112