frameyard 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/GLOSSARY.md +46 -2
  3. package/README.md +174 -106
  4. package/dist/bindings.d.ts +42 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +144 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/cli.js +32 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/composition/adapter.d.ts +30 -0
  11. package/dist/composition/adapter.d.ts.map +1 -0
  12. package/dist/composition/adapter.js +2 -0
  13. package/dist/composition/adapter.js.map +1 -0
  14. package/dist/composition/animation.d.ts +19 -0
  15. package/dist/composition/animation.d.ts.map +1 -0
  16. package/dist/composition/animation.js +55 -0
  17. package/dist/composition/animation.js.map +1 -0
  18. package/dist/composition/attributes.d.ts +7 -0
  19. package/dist/composition/attributes.d.ts.map +1 -0
  20. package/dist/composition/attributes.js +24 -0
  21. package/dist/composition/attributes.js.map +1 -0
  22. package/dist/composition/descriptor.d.ts +30 -0
  23. package/dist/composition/descriptor.d.ts.map +1 -0
  24. package/dist/composition/descriptor.js +188 -0
  25. package/dist/composition/descriptor.js.map +1 -0
  26. package/dist/composition/diagnostics.d.ts +12 -0
  27. package/dist/composition/diagnostics.d.ts.map +1 -0
  28. package/dist/composition/diagnostics.js +38 -0
  29. package/dist/composition/diagnostics.js.map +1 -0
  30. package/dist/composition/index.d.ts +12 -0
  31. package/dist/composition/index.d.ts.map +1 -0
  32. package/dist/composition/index.js +7 -0
  33. package/dist/composition/index.js.map +1 -0
  34. package/dist/composition/media.d.ts +27 -0
  35. package/dist/composition/media.d.ts.map +1 -0
  36. package/dist/composition/media.js +162 -0
  37. package/dist/composition/media.js.map +1 -0
  38. package/dist/composition/paths.d.ts +5 -0
  39. package/dist/composition/paths.d.ts.map +1 -0
  40. package/dist/composition/paths.js +16 -0
  41. package/dist/composition/paths.js.map +1 -0
  42. package/dist/composition/transcripts.d.ts +19 -0
  43. package/dist/composition/transcripts.d.ts.map +1 -0
  44. package/dist/composition/transcripts.js +57 -0
  45. package/dist/composition/transcripts.js.map +1 -0
  46. package/dist/composition/walk.d.ts +10 -0
  47. package/dist/composition/walk.d.ts.map +1 -0
  48. package/dist/composition/walk.js +32 -0
  49. package/dist/composition/walk.js.map +1 -0
  50. package/dist/contracts.d.ts +75 -47
  51. package/dist/contracts.d.ts.map +1 -1
  52. package/dist/contracts.js +14 -159
  53. package/dist/contracts.js.map +1 -1
  54. package/dist/errors.d.ts +15 -4
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +8 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/html/document-adapter.d.ts +5 -0
  59. package/dist/html/document-adapter.d.ts.map +1 -0
  60. package/dist/html/document-adapter.js +17 -0
  61. package/dist/html/document-adapter.js.map +1 -0
  62. package/dist/html/index.d.ts +5 -0
  63. package/dist/html/index.d.ts.map +1 -0
  64. package/dist/html/index.js +3 -0
  65. package/dist/html/index.js.map +1 -0
  66. package/dist/html/parse.d.ts +26 -0
  67. package/dist/html/parse.d.ts.map +1 -0
  68. package/dist/html/parse.js +80 -0
  69. package/dist/html/parse.js.map +1 -0
  70. package/dist/html/validate.d.ts +14 -0
  71. package/dist/html/validate.d.ts.map +1 -0
  72. package/dist/html/validate.js +648 -0
  73. package/dist/html/validate.js.map +1 -0
  74. package/dist/index.d.ts +2 -2
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/preview/main.d.ts +10 -3
  79. package/dist/preview/main.d.ts.map +1 -1
  80. package/dist/preview/main.js +189 -89
  81. package/dist/preview/main.js.map +1 -1
  82. package/dist/project.d.ts +2 -0
  83. package/dist/project.d.ts.map +1 -1
  84. package/dist/project.js +4 -0
  85. package/dist/project.js.map +1 -1
  86. package/dist/renderer/browser.d.ts +11 -1
  87. package/dist/renderer/browser.d.ts.map +1 -1
  88. package/dist/renderer/browser.js +93 -14
  89. package/dist/renderer/browser.js.map +1 -1
  90. package/dist/renderer/index.d.ts +25 -5
  91. package/dist/renderer/index.d.ts.map +1 -1
  92. package/dist/renderer/index.js +82 -16
  93. package/dist/renderer/index.js.map +1 -1
  94. package/dist/runtime/bindings.d.ts +11 -0
  95. package/dist/runtime/bindings.d.ts.map +1 -0
  96. package/dist/runtime/bindings.js +164 -0
  97. package/dist/runtime/bindings.js.map +1 -0
  98. package/dist/runtime/diagnostics.d.ts +4 -0
  99. package/dist/runtime/diagnostics.d.ts.map +1 -0
  100. package/dist/runtime/diagnostics.js +107 -0
  101. package/dist/runtime/diagnostics.js.map +1 -0
  102. package/dist/runtime/document-adapter.d.ts +4 -0
  103. package/dist/runtime/document-adapter.d.ts.map +1 -0
  104. package/dist/runtime/document-adapter.js +19 -0
  105. package/dist/runtime/document-adapter.js.map +1 -0
  106. package/dist/runtime/host.d.ts +14 -0
  107. package/dist/runtime/host.d.ts.map +1 -0
  108. package/dist/runtime/host.js +596 -0
  109. package/dist/runtime/host.js.map +1 -0
  110. package/dist/runtime/index.d.ts +0 -29
  111. package/dist/runtime/index.d.ts.map +1 -1
  112. package/dist/runtime/index.js +0 -360
  113. package/dist/runtime/index.js.map +1 -1
  114. package/dist/runtime/media.d.ts.map +1 -1
  115. package/dist/runtime/media.js +20 -51
  116. package/dist/runtime/media.js.map +1 -1
  117. package/dist/scaffold.d.ts.map +1 -1
  118. package/dist/scaffold.js +177 -84
  119. package/dist/scaffold.js.map +1 -1
  120. package/dist/server/composition-html.d.ts +20 -0
  121. package/dist/server/composition-html.d.ts.map +1 -0
  122. package/dist/server/composition-html.js +45 -0
  123. package/dist/server/composition-html.js.map +1 -0
  124. package/dist/server/preview-html.d.ts.map +1 -1
  125. package/dist/server/preview-html.js +19 -14
  126. package/dist/server/preview-html.js.map +1 -1
  127. package/dist/server/server.d.ts.map +1 -1
  128. package/dist/server/server.js +94 -6
  129. package/dist/server/server.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  133. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
  134. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  135. package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
  136. package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
  137. package/docs/migration-0.4.md +279 -0
  138. package/docs/v0.1-design.md +78 -56
  139. package/examples/demo/README.md +8 -4
  140. package/examples/demo/compositions/demo.controller.js +120 -0
  141. package/examples/demo/compositions/demo.html +253 -0
  142. package/examples/demo/video.config.js +1 -1
  143. package/examples/video/README.md +61 -0
  144. package/examples/video/assets/clip.mp4 +0 -0
  145. package/examples/video/assets/clip.webm +0 -0
  146. package/examples/video/compositions/video-mp4.html +362 -0
  147. package/examples/video/compositions/video.html +362 -0
  148. package/examples/video/props/video.json +8 -0
  149. package/examples/video/scripts/make-clip.sh +109 -0
  150. package/examples/video/video.config.js +6 -0
  151. package/examples/webgl/README.md +30 -0
  152. package/examples/webgl/compositions/webgl.controller.js +200 -0
  153. package/examples/webgl/compositions/webgl.html +215 -0
  154. package/examples/webgl/props/webgl.json +6 -0
  155. package/examples/webgl/video.config.js +5 -0
  156. package/package.json +7 -3
  157. package/skills/frameyard/README.md +4 -0
  158. package/skills/frameyard/SKILL.md +40 -12
  159. package/skills/frameyard/references/audio-assets.md +25 -14
  160. package/skills/frameyard/references/bindings.md +73 -0
  161. package/skills/frameyard/references/common-mistakes.md +59 -0
  162. package/skills/frameyard/references/controllers.md +126 -0
  163. package/skills/frameyard/references/deterministic-authoring.md +143 -52
  164. package/skills/frameyard/references/diagnostics.md +90 -0
  165. package/skills/frameyard/references/failure-recovery.md +37 -14
  166. package/skills/frameyard/references/json-results.md +43 -9
  167. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  168. package/examples/demo/compositions/demo.js +0 -206
@@ -0,0 +1,200 @@
1
+ const VERTEX_SHADER = `
2
+ attribute vec2 a_position;
3
+
4
+ void main() {
5
+ gl_Position = vec4(a_position, 0.0, 1.0);
6
+ }
7
+ `;
8
+
9
+ const FRAGMENT_SHADER = `
10
+ precision highp float;
11
+
12
+ uniform vec2 u_resolution;
13
+ uniform float u_frame;
14
+ uniform float u_duration;
15
+ uniform vec3 u_accent;
16
+
17
+ const float TAU = 6.28318530718;
18
+
19
+ float hash(vec2 p) {
20
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
21
+ }
22
+
23
+ void main() {
24
+ vec2 uv = gl_FragCoord.xy / u_resolution;
25
+ vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / u_resolution.y;
26
+ float phase = (u_frame / max(u_duration, 1.0)) * TAU;
27
+
28
+ // A slow domain warp turns the contour field into a liquid surface.
29
+ vec2 q = p;
30
+ q.x += 0.045 * sin(p.y * 3.4 - phase * 0.8);
31
+ q.y += 0.10 * sin(p.x * 2.2 + phase * 0.55) + 0.035 * sin(p.x * 5.7 - phase * 0.9);
32
+
33
+ float terrain = q.y + 0.07 * sin(q.x * 3.6 + phase * 0.7) + 0.025 * sin(q.x * 8.4 - phase * 1.1);
34
+ float contour = abs(fract(terrain * 14.0 + phase * 0.5) - 0.5);
35
+ float line = smoothstep(0.455, 0.5, contour);
36
+ float glow = smoothstep(0.26, 0.5, contour);
37
+
38
+ vec3 ink = mix(vec3(0.016, 0.026, 0.038), vec3(0.035, 0.078, 0.085), uv.y);
39
+ vec3 color = ink;
40
+
41
+ float right = smoothstep(0.14, 0.66, uv.x);
42
+ color += u_accent * line * (0.10 + 0.38 * right);
43
+ color += u_accent * glow * 0.045 * right;
44
+
45
+ // A soft moving light and one thin orbit ring anchor the right side.
46
+ vec2 light = vec2(0.34 + 0.05 * cos(phase * 0.6), 0.06 + 0.04 * sin(phase * 0.45));
47
+ float radius = length(p - light);
48
+ float halo = 0.035 / (radius + 0.035);
49
+ vec3 white = mix(u_accent, vec3(0.92, 1.0, 0.98), 0.7);
50
+ color += white * halo * 0.42;
51
+
52
+ float ring = 1.0 - smoothstep(0.0, 0.007, abs(radius - 0.27 - 0.012 * sin(phase * 1.4)));
53
+ color += white * ring * 0.22;
54
+
55
+ // Static grain keeps the gradient from banding without adding temporal noise.
56
+ color += (hash(gl_FragCoord.xy) - 0.5) * 0.02;
57
+ float vignette = 1.0 - smoothstep(0.62, 1.28, length(p * vec2(0.86, 1.0)));
58
+ color *= 0.58 + 0.42 * vignette;
59
+
60
+ gl_FragColor = vec4(color, 1.0);
61
+ }
62
+ `;
63
+
64
+ let gl;
65
+ let program;
66
+ let buffer;
67
+ let uniforms;
68
+ let labels;
69
+ let durationFrames = 360;
70
+ let fps = 30;
71
+
72
+ function compileShader(type, source) {
73
+ const shader = gl.createShader(type);
74
+ gl.shaderSource(shader, source);
75
+ gl.compileShader(shader);
76
+ if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) !== true) {
77
+ const log = gl.getShaderInfoLog(shader);
78
+ gl.deleteShader(shader);
79
+ throw new Error(log ?? "Could not compile the WebGL shader.");
80
+ }
81
+ return shader;
82
+ }
83
+
84
+ function linkProgram() {
85
+ const vertex = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);
86
+ const fragment = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
87
+ const next = gl.createProgram();
88
+ gl.attachShader(next, vertex);
89
+ gl.attachShader(next, fragment);
90
+ gl.linkProgram(next);
91
+ gl.detachShader(next, vertex);
92
+ gl.detachShader(next, fragment);
93
+ gl.deleteShader(vertex);
94
+ gl.deleteShader(fragment);
95
+ if (gl.getProgramParameter(next, gl.LINK_STATUS) !== true) {
96
+ const log = gl.getProgramInfoLog(next);
97
+ gl.deleteProgram(next);
98
+ throw new Error(log ?? "Could not link the WebGL program.");
99
+ }
100
+ return next;
101
+ }
102
+
103
+ function hexToRgb(value) {
104
+ const match = /^#([0-9a-f]{6})$/i.exec(typeof value === "string" ? value : "");
105
+ if (!match) {
106
+ return [0.47, 0.94, 0.78];
107
+ }
108
+ const number = Number.parseInt(match[1], 16);
109
+ return [((number >> 16) & 0xff) / 255, ((number >> 8) & 0xff) / 255, (number & 0xff) / 255];
110
+ }
111
+
112
+ export default {
113
+ validateProps(props) {
114
+ const errors = [];
115
+ for (const key of ["kicker", "title", "subtitle"]) {
116
+ if (typeof props[key] !== "string") {
117
+ errors.push(key + " must be a string");
118
+ }
119
+ }
120
+ if (typeof props.accent !== "string" || !/^#[0-9a-f]{6}$/i.test(props.accent)) {
121
+ errors.push("accent must be a #rrggbb color");
122
+ }
123
+ return errors;
124
+ },
125
+
126
+ mount({ root, props }) {
127
+ const canvas = root.querySelector("#field");
128
+ fps = Number(root.getAttribute("fps") ?? 30);
129
+ durationFrames = Number(root.getAttribute("duration-in-frames") ?? 360);
130
+
131
+ gl = canvas.getContext("webgl", {
132
+ alpha: false,
133
+ antialias: false,
134
+ depth: false,
135
+ stencil: false,
136
+ preserveDrawingBuffer: true,
137
+ });
138
+ if (!gl) {
139
+ throw new Error("WebGL is unavailable. The production renderer pins SwiftShader.");
140
+ }
141
+
142
+ program = linkProgram();
143
+ gl.useProgram(program);
144
+
145
+ buffer = gl.createBuffer();
146
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
147
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
148
+ const position = gl.getAttribLocation(program, "a_position");
149
+ gl.enableVertexAttribArray(position);
150
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
151
+
152
+ uniforms = {
153
+ frame: gl.getUniformLocation(program, "u_frame"),
154
+ resolution: gl.getUniformLocation(program, "u_resolution"),
155
+ duration: gl.getUniformLocation(program, "u_duration"),
156
+ accent: gl.getUniformLocation(program, "u_accent"),
157
+ };
158
+
159
+ const accent = hexToRgb(props.accent);
160
+ gl.viewport(0, 0, canvas.width, canvas.height);
161
+ gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);
162
+ gl.uniform1f(uniforms.duration, durationFrames);
163
+ gl.uniform3f(uniforms.accent, accent[0], accent[1], accent[2]);
164
+ gl.clearColor(0.016, 0.026, 0.038, 1);
165
+
166
+ labels = {
167
+ frame: root.querySelector("#frame"),
168
+ total: root.querySelector("#total"),
169
+ clock: root.querySelector("#clock"),
170
+ };
171
+ labels.total.textContent = String(durationFrames - 1).padStart(3, "0");
172
+ },
173
+
174
+ update({ frame }) {
175
+ gl.clear(gl.COLOR_BUFFER_BIT);
176
+ gl.uniform1f(uniforms.frame, frame);
177
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
178
+
179
+ labels.frame.textContent = String(frame).padStart(3, "0");
180
+ labels.clock.textContent = (frame / fps).toFixed(2) + " S";
181
+ },
182
+
183
+ dispose() {
184
+ if (gl) {
185
+ if (buffer) {
186
+ gl.deleteBuffer(buffer);
187
+ }
188
+ if (program) {
189
+ gl.deleteProgram(program);
190
+ }
191
+ }
192
+ gl = undefined;
193
+ program = undefined;
194
+ buffer = undefined;
195
+ uniforms = undefined;
196
+ labels = undefined;
197
+ durationFrames = 360;
198
+ fps = 30;
199
+ },
200
+ };
@@ -0,0 +1,215 @@
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>
210
+ </div>
211
+ </fy-composition>
212
+
213
+ <script type="text/fy-controller" src="./webgl.controller.js"></script>
214
+ </body>
215
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "kicker": "Shader Study / 001",
3
+ "title": "Liquid Light",
4
+ "subtitle": "A frame-driven WebGL field, resolved in software so every captured pixel stays reproducible.",
5
+ "accent": "#78f0c8"
6
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ compositions: {
3
+ webgl: "./compositions/webgl.html",
4
+ },
5
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameyard",
3
- "version": "0.3.0",
3
+ "version": "0.5.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,16 +66,19 @@
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
+ "typecheck:browser": "tsc -p tsconfig.browser.json",
68
70
  "check:scripts": "node scripts/check-scripts.mjs",
69
71
  "lint": "oxlint",
70
72
  "format": "oxfmt",
71
73
  "format:check": "oxfmt --check",
72
- "check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test",
74
+ "check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test && pnpm typecheck:browser",
75
+ "compare-stills": "node scripts/compare-stills.mjs",
73
76
  "check": "pnpm check:static && pnpm test",
74
77
  "test": "pnpm build && vitest run",
75
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",
76
79
  "bench": "pnpm build && node scripts/bench-render.mjs",
77
80
  "set-version": "node scripts/set-version.mjs",
78
- "verify-release": "node scripts/verify-release.mjs"
81
+ "verify-release": "node scripts/verify-release.mjs",
82
+ "retag-release": "node scripts/retag-release.mjs"
79
83
  }
80
84
  }
@@ -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.3.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.5.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, WebGL, embedded video, 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>] [--concurrency <n>] --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`. The default output is `out/<id>.mp4` in the project root.
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,24 +32,40 @@ 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
+ - `render` writes to `out/<id>.mp4` in the project root unless `--out` is passed. Explicit `--out` paths are relative to the current working directory.
35
36
  - 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.
36
37
  - Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
37
38
  - Read [JSON results](references/json-results.md) for result and error shapes, and [failure recovery](references/failure-recovery.md) for actionable error codes.
38
39
 
40
+ ## Authoring rules
41
+
42
+ - 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.
43
+ - 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.
44
+ - 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.
45
+ - Frame-state variables on `:root`: `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`.
46
+ - 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.
47
+ - The controller is the only executable code. Other executable scripts and `on*` handlers are rejected.
48
+ - Media and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
49
+ - 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>`.
50
+
39
51
  ## Determinism guardrails
40
52
 
41
53
  - A frame may depend only on its integer `frame`, immutable `props`, and assets loaded before frame production.
42
54
  - Use `random("stable:key")` from `frameyard/runtime`; never use `Math.random()`.
43
55
  - Never use `Date.now()`, `performance.now()`, timers, independent `requestAnimationFrame` loops, or CSS transitions as animation inputs.
44
- - Finish remote data, fonts, and image loading during `mount()` or before the first frame. Network requests are blocked during frame capture.
56
+ - Finish remote data, fonts, and image loading during the loading phase. Network requests are blocked during frame capture.
45
57
  - Keep `update({ frame, props })` synchronous and seek-safe. Do not carry state between frames.
46
- - Let the runtime seek embedded `<video>` elements. Do not play, pause, or set `currentTime` in `update()`; use the `data-frameyard-*` attributes described in [deterministic authoring](references/deterministic-authoring.md).
58
+ - Let the runtime seek embedded `<video>` elements. Do not play, pause, or set `currentTime` in `update()`.
47
59
  - 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.
48
60
  - Do not run model inference inside `mount()`, `update()`, or frame production.
49
61
 
62
+ ## Diagnostics rule
63
+
64
+ `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`.
65
+
50
66
  ## Subtitle and karaoke rule
51
67
 
52
- 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).
68
+ 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).
53
69
 
54
70
  ## CLI invocation
55
71
 
@@ -61,3 +77,15 @@ node scripts/frameyard.mjs check <id> --json # fallback
61
77
  ```
62
78
 
63
79
  When the CLI version differs from `frameyard-api-version`, pin it with `npx frameyard@<version>`.
80
+
81
+ ## References
82
+
83
+ - [Deterministic HTML authoring](references/deterministic-authoring.md)
84
+ - [Bindings](references/bindings.md)
85
+ - [Controllers](references/controllers.md)
86
+ - [Diagnostics](references/diagnostics.md)
87
+ - [Common mistakes](references/common-mistakes.md)
88
+ - [Audio and generated assets](references/audio-assets.md)
89
+ - [Subtitles and karaoke](references/subtitles-karaoke.md)
90
+ - [JSON results](references/json-results.md)
91
+ - [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