frameyard 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/GLOSSARY.md +40 -16
  3. package/README.md +64 -55
  4. package/dist/assets/process.js.map +1 -1
  5. package/dist/assets/provenance.js.map +1 -1
  6. package/dist/cli.js +255 -59
  7. package/dist/cli.js.map +1 -1
  8. package/dist/composition/attributes.d.ts.map +1 -1
  9. package/dist/composition/descriptor.d.ts +17 -3
  10. package/dist/composition/descriptor.d.ts.map +1 -1
  11. package/dist/composition/descriptor.js +51 -28
  12. package/dist/composition/descriptor.js.map +1 -1
  13. package/dist/composition/diagnostics.js.map +1 -1
  14. package/dist/composition/format.d.ts +12 -0
  15. package/dist/composition/format.d.ts.map +1 -0
  16. package/dist/composition/format.js +28 -0
  17. package/dist/composition/format.js.map +1 -0
  18. package/dist/composition/index.d.ts +2 -1
  19. package/dist/composition/index.d.ts.map +1 -1
  20. package/dist/composition/index.js +1 -0
  21. package/dist/composition/index.js.map +1 -1
  22. package/dist/contracts.d.ts +55 -6
  23. package/dist/contracts.d.ts.map +1 -1
  24. package/dist/contracts.js +3 -0
  25. package/dist/contracts.js.map +1 -1
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/doctor.js +79 -0
  28. package/dist/doctor.js.map +1 -1
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/html/document-adapter.d.ts +3 -3
  33. package/dist/html/document-adapter.d.ts.map +1 -1
  34. package/dist/html/document-adapter.js +1 -1
  35. package/dist/html/document-adapter.js.map +1 -1
  36. package/dist/html/index.d.ts +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/parse.d.ts +10 -2
  39. package/dist/html/parse.d.ts.map +1 -1
  40. package/dist/html/parse.js +14 -5
  41. package/dist/html/parse.js.map +1 -1
  42. package/dist/html/validate.d.ts +12 -1
  43. package/dist/html/validate.d.ts.map +1 -1
  44. package/dist/html/validate.js +233 -61
  45. package/dist/html/validate.js.map +1 -1
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/preview/main.d.ts +2 -2
  51. package/dist/preview/main.d.ts.map +1 -1
  52. package/dist/preview/main.js +47 -19
  53. package/dist/preview/main.js.map +1 -1
  54. package/dist/preview/timeline-data.d.ts +5 -1
  55. package/dist/preview/timeline-data.d.ts.map +1 -1
  56. package/dist/preview/timeline-host.d.ts +2 -0
  57. package/dist/preview/timeline-host.d.ts.map +1 -1
  58. package/dist/preview/timeline-host.js +31 -10
  59. package/dist/preview/timeline-host.js.map +1 -1
  60. package/dist/preview/timeline-main.js +12 -7
  61. package/dist/preview/timeline-main.js.map +1 -1
  62. package/dist/project.d.ts +3 -5
  63. package/dist/project.d.ts.map +1 -1
  64. package/dist/project.js.map +1 -1
  65. package/dist/renderer/browser.d.ts +2 -2
  66. package/dist/renderer/browser.d.ts.map +1 -1
  67. package/dist/renderer/browser.js +10 -4
  68. package/dist/renderer/browser.js.map +1 -1
  69. package/dist/renderer/ffmpeg.d.ts +69 -2
  70. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  71. package/dist/renderer/ffmpeg.js +381 -69
  72. package/dist/renderer/ffmpeg.js.map +1 -1
  73. package/dist/renderer/index.d.ts +66 -5
  74. package/dist/renderer/index.d.ts.map +1 -1
  75. package/dist/renderer/index.js +226 -51
  76. package/dist/renderer/index.js.map +1 -1
  77. package/dist/renderer/timeline.d.ts +41 -4
  78. package/dist/renderer/timeline.d.ts.map +1 -1
  79. package/dist/renderer/timeline.js +242 -39
  80. package/dist/renderer/timeline.js.map +1 -1
  81. package/dist/runtime/host.d.ts.map +1 -1
  82. package/dist/runtime/host.js +155 -31
  83. package/dist/runtime/host.js.map +1 -1
  84. package/dist/runtime/media.d.ts.map +1 -1
  85. package/dist/runtime/media.js +3 -6
  86. package/dist/runtime/media.js.map +1 -1
  87. package/dist/runtime/video.d.ts +13 -0
  88. package/dist/runtime/video.d.ts.map +1 -0
  89. package/dist/runtime/video.js +41 -0
  90. package/dist/runtime/video.js.map +1 -0
  91. package/dist/scaffold.d.ts.map +1 -1
  92. package/dist/scaffold.js +107 -103
  93. package/dist/scaffold.js.map +1 -1
  94. package/dist/server/composition-html.d.ts +13 -7
  95. package/dist/server/composition-html.d.ts.map +1 -1
  96. package/dist/server/composition-html.js +65 -16
  97. package/dist/server/composition-html.js.map +1 -1
  98. package/dist/server/preview-html.js +1 -1
  99. package/dist/server/server.d.ts.map +1 -1
  100. package/dist/server/server.js +26 -6
  101. package/dist/server/server.js.map +1 -1
  102. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  103. package/dist/server/timeline-preview-data.js +11 -0
  104. package/dist/server/timeline-preview-data.js.map +1 -1
  105. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  106. package/dist/server/timeline-preview-html.js +2 -1
  107. package/dist/server/timeline-preview-html.js.map +1 -1
  108. package/dist/timeline/plan.js.map +1 -1
  109. package/dist/timeline/resolve.d.ts +4 -3
  110. package/dist/timeline/resolve.d.ts.map +1 -1
  111. package/dist/timeline/resolve.js +14 -4
  112. package/dist/timeline/resolve.js.map +1 -1
  113. package/dist/timeline/schema.d.ts +7 -0
  114. package/dist/timeline/schema.d.ts.map +1 -1
  115. package/dist/timeline/schema.js +50 -0
  116. package/dist/timeline/schema.js.map +1 -1
  117. package/dist/transcript.js.map +1 -1
  118. package/dist/version.d.ts +1 -1
  119. package/dist/version.js +1 -1
  120. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  121. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  122. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  123. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  124. package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
  125. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
  126. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  127. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  128. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  129. package/docs/migration-0.4.md +3 -1
  130. package/docs/migration-0.8.md +163 -0
  131. package/docs/preview-testing.md +60 -0
  132. package/docs/v0.1-design.md +52 -42
  133. package/examples/blend/README.md +106 -0
  134. package/examples/blend/assets/ink.mp4 +0 -0
  135. package/examples/blend/compositions/aurora.html +55 -0
  136. package/examples/blend/compositions/backdrop.html +74 -0
  137. package/examples/blend/compositions/labels.html +149 -0
  138. package/examples/blend/scripts/make-ink.sh +26 -0
  139. package/examples/blend/timelines/blend.timeline.json +155 -0
  140. package/examples/blend/video.config.js +11 -0
  141. package/examples/demo/compositions/demo.html +243 -251
  142. package/examples/static/README.md +17 -0
  143. package/examples/static/compositions/still.html +37 -0
  144. package/examples/static/timelines/still.timeline.json +13 -0
  145. package/examples/static/video.config.js +9 -0
  146. package/examples/video/compositions/video-mp4.html +349 -357
  147. package/examples/video/compositions/video-overlay.html +337 -345
  148. package/examples/video/compositions/video.html +349 -357
  149. package/examples/webgl/compositions/webgl.html +205 -213
  150. package/package.json +6 -3
  151. package/skills/frameyard/README.md +7 -5
  152. package/skills/frameyard/SKILL.md +17 -13
  153. package/skills/frameyard/references/common-mistakes.md +30 -6
  154. package/skills/frameyard/references/controllers.md +5 -4
  155. package/skills/frameyard/references/deterministic-authoring.md +42 -36
  156. package/skills/frameyard/references/diagnostics.md +11 -3
  157. package/skills/frameyard/references/failure-recovery.md +28 -7
  158. package/skills/frameyard/references/json-results.md +58 -7
  159. package/skills/frameyard/references/subtitles-karaoke.md +2 -2
  160. package/skills/frameyard/references/timelines.md +21 -15
  161. package/skills/frameyard/scripts/frameyard.mjs +20 -3
@@ -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.6.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",
@@ -59,7 +59,7 @@
59
59
  "@types/node": "26.6.5",
60
60
  "oxfmt": "0.75.0",
61
61
  "oxlint": "1.90.0",
62
- "typescript": "5.9.3",
62
+ "typescript": "7.0.2",
63
63
  "vitest": "5.0.5"
64
64
  },
65
65
  "scripts": {
@@ -74,9 +74,12 @@
74
74
  "check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test && pnpm typecheck:browser",
75
75
  "compare-stills": "node scripts/compare-stills.mjs",
76
76
  "check": "pnpm check:static && pnpm test",
77
+ "check:preview": "pnpm check:static && pnpm test:preview",
77
78
  "test": "pnpm build && vitest run",
78
- "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",
79
+ "test:preview": "pnpm build && vitest run test/preview-runtime.test.ts test/timeline-preview.test.ts test/timeline-preview-runtime.test.ts",
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",
79
81
  "bench": "pnpm build && node scripts/bench-render.mjs",
82
+ "bench:static": "pnpm build && node bench/static-composition.mjs",
80
83
  "set-version": "node scripts/set-version.mjs",
81
84
  "verify-release": "node scripts/verify-release.mjs",
82
85
  "retag-release": "node scripts/retag-release.mjs"
@@ -10,16 +10,18 @@ Install from the repository with the open agent-skills CLI:
10
10
  npx skills add vladiantio/frameyard
11
11
  ```
12
12
 
13
- The skill lives at `skills/frameyard` in the repository and npm package. Alternatively, copy or symlink the whole `skills/frameyard/` directory into the skill location your agent host uses. Examples:
13
+ The skill lives at `skills/frameyard` in the repository and npm package. Alternatively, copy or symlink the whole `skills/frameyard/` directory into the skill location your agent host uses:
14
14
 
15
- - Claude Code project skills: `.claude/skills/frameyard`
16
- - Agent hosts that read `.agents/skills`: `.agents/skills/frameyard`
17
- - Global installations: place it under your host's documented personal skills directory
15
+ - Project-scoped: `.claude/skills/frameyard` for Claude Code, or `.agents/skills/frameyard` for hosts that read the shared `.agents` container.
16
+ - Personal or global: `~/.claude/skills/frameyard`, `~/.agents/skills/frameyard`, or another host's personal directory; for example, OpenCode reads `~/.config/opencode/skills/frameyard`.
17
+ - Keep one canonical copy and symlink it into the host directories you use, so updates do not drift.
18
18
 
19
- The skill is host-neutral: it uses only the normal `frameyard` project format and CLI. There is no agent-only runtime.
19
+ The skill is host-neutral: it uses only the normal `frameyard` project format and CLI. There is no agent-only runtime. All `references/`, `scripts/`, and `SKILL.md` paths inside the skill are relative, so the directory works wherever it is installed.
20
20
 
21
21
  Run the CLI with `npx frameyard`. It resolves the latest published version; pin it with `npx frameyard@<version>` or install the package locally when the CLI version must match the skill. When the skill is copied outside the npm package, `scripts/frameyard.mjs` falls back to the `frameyard` command on `PATH`; if that command is unavailable, set `FRAMEYARD_CLI` to an installed CLI path.
22
22
 
23
+ When you work inside the Frameyard repository itself, `npx frameyard` resolves the published package, not your working tree. Run the built CLI (`node dist/cli.js`, after `pnpm build`) or `skills/frameyard/scripts/frameyard.mjs`, which prefers the repository build. The repository `AGENTS.md` is the authority for development commands.
24
+
23
25
  ## Contents
24
26
 
25
27
  - `SKILL.md`: trigger description and operating workflow
@@ -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.6.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).
25
- 6. **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.
26
- 7. **Render** with `npx frameyard render <id> [--out <file.mp4>] [--props <file>] --json`. Composition renders accept `--concurrency <n>`; timeline renders accept `--workers <n>` and `--scratch <dir>`. The default output is `out/<id>.mp4` in the project root.
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
+ 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`. 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,23 +33,27 @@ 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.
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
+ - `--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.
38
40
  - For a timeline, `--props` is a JSON object keyed by top-level composition clip ID. For a composition, it is the props object.
39
41
  - Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
40
42
  - Read [JSON results](references/json-results.md) for result and error shapes, and [failure recovery](references/failure-recovery.md) for actionable error codes.
41
43
 
42
44
  ## Authoring rules
43
45
 
44
- - 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`.
45
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.
46
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.
47
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.
48
51
  - Frame-state variables on `:root`: `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`.
49
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.
50
53
  - The controller is the only executable code. Other executable scripts and `on*` handlers are rejected.
51
- - 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.
52
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.
53
57
 
54
58
  ## Determinism guardrails
55
59
 
@@ -64,7 +68,7 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
64
68
 
65
69
  ## Diagnostics rule
66
70
 
67
- `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.
68
72
 
69
73
  ## Subtitle and karaoke rule
70
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,14 +51,33 @@
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
 
56
79
  - Replacing `check` with a single `still`.
80
+ - Using `still` as an image-sequence exporter, adding `--step`-style workflows, or requesting more than 32 unique frames in one call.
57
81
  - Ignoring warnings or `diagnostics` in `--json` output.
58
82
  - Claiming success after a nonzero exit, a missing still, or skipped visual inspection.
59
83
  - Using `--overwrite` or `--force` without permission.