frameyard 0.5.0 → 0.7.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 (128) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/GLOSSARY.md +152 -24
  3. package/README.md +99 -31
  4. package/dist/assets/process.js.map +1 -1
  5. package/dist/assets/provenance.js.map +1 -1
  6. package/dist/cli.js +326 -40
  7. package/dist/cli.js.map +1 -1
  8. package/dist/composition/attributes.d.ts.map +1 -1
  9. package/dist/composition/diagnostics.js.map +1 -1
  10. package/dist/contracts.d.ts +12 -0
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js.map +1 -1
  13. package/dist/doctor.d.ts.map +1 -1
  14. package/dist/doctor.js +79 -0
  15. package/dist/doctor.js.map +1 -1
  16. package/dist/errors.d.ts +1 -1
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/html/validate.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/preview/main.js +96 -11
  25. package/dist/preview/main.js.map +1 -1
  26. package/dist/preview/timeline-data.d.ts +85 -0
  27. package/dist/preview/timeline-data.d.ts.map +1 -0
  28. package/dist/preview/timeline-data.js +2 -0
  29. package/dist/preview/timeline-data.js.map +1 -0
  30. package/dist/preview/timeline-host.d.ts +21 -0
  31. package/dist/preview/timeline-host.d.ts.map +1 -0
  32. package/dist/preview/timeline-host.js +308 -0
  33. package/dist/preview/timeline-host.js.map +1 -0
  34. package/dist/preview/timeline-main.d.ts +17 -0
  35. package/dist/preview/timeline-main.d.ts.map +1 -0
  36. package/dist/preview/timeline-main.js +368 -0
  37. package/dist/preview/timeline-main.js.map +1 -0
  38. package/dist/project.d.ts +16 -1
  39. package/dist/project.d.ts.map +1 -1
  40. package/dist/project.js +81 -26
  41. package/dist/project.js.map +1 -1
  42. package/dist/renderer/browser.d.ts +2 -0
  43. package/dist/renderer/browser.d.ts.map +1 -1
  44. package/dist/renderer/browser.js +6 -1
  45. package/dist/renderer/browser.js.map +1 -1
  46. package/dist/renderer/ffmpeg.d.ts +126 -1
  47. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  48. package/dist/renderer/ffmpeg.js +450 -6
  49. package/dist/renderer/ffmpeg.js.map +1 -1
  50. package/dist/renderer/index.d.ts +28 -3
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +138 -38
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/renderer/timeline.d.ts +146 -0
  55. package/dist/renderer/timeline.d.ts.map +1 -0
  56. package/dist/renderer/timeline.js +1098 -0
  57. package/dist/renderer/timeline.js.map +1 -0
  58. package/dist/runtime/host.js.map +1 -1
  59. package/dist/runtime/media.d.ts.map +1 -1
  60. package/dist/runtime/media.js +3 -6
  61. package/dist/runtime/media.js.map +1 -1
  62. package/dist/runtime/video.d.ts +13 -0
  63. package/dist/runtime/video.d.ts.map +1 -0
  64. package/dist/runtime/video.js +41 -0
  65. package/dist/runtime/video.js.map +1 -0
  66. package/dist/scaffold.d.ts.map +1 -1
  67. package/dist/scaffold.js +90 -24
  68. package/dist/scaffold.js.map +1 -1
  69. package/dist/server/composition-html.d.ts.map +1 -1
  70. package/dist/server/preview-html.d.ts.map +1 -1
  71. package/dist/server/preview-html.js +15 -1
  72. package/dist/server/preview-html.js.map +1 -1
  73. package/dist/server/server.d.ts.map +1 -1
  74. package/dist/server/server.js +39 -1
  75. package/dist/server/server.js.map +1 -1
  76. package/dist/server/timeline-preview-data.d.ts +4 -0
  77. package/dist/server/timeline-preview-data.d.ts.map +1 -0
  78. package/dist/server/timeline-preview-data.js +97 -0
  79. package/dist/server/timeline-preview-data.js.map +1 -0
  80. package/dist/server/timeline-preview-html.d.ts +4 -0
  81. package/dist/server/timeline-preview-html.d.ts.map +1 -0
  82. package/dist/server/timeline-preview-html.js +227 -0
  83. package/dist/server/timeline-preview-html.js.map +1 -0
  84. package/dist/timeline/plan.d.ts +32 -0
  85. package/dist/timeline/plan.d.ts.map +1 -0
  86. package/dist/timeline/plan.js +68 -0
  87. package/dist/timeline/plan.js.map +1 -0
  88. package/dist/timeline/resolve.d.ts +60 -0
  89. package/dist/timeline/resolve.d.ts.map +1 -0
  90. package/dist/timeline/resolve.js +523 -0
  91. package/dist/timeline/resolve.js.map +1 -0
  92. package/dist/timeline/schema.d.ts +71 -0
  93. package/dist/timeline/schema.d.ts.map +1 -0
  94. package/dist/timeline/schema.js +425 -0
  95. package/dist/timeline/schema.js.map +1 -0
  96. package/dist/transcript.js.map +1 -1
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/docs/adr/0002-make-frame-and-props-the-only-animation-state.md +2 -0
  100. package/docs/adr/0005-keep-parallel-rendering-opt-in-and-frame-ordered.md +2 -0
  101. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  102. package/docs/adr/0009-composite-production-frames-in-software.md +1 -1
  103. package/docs/adr/0010-make-timelines-the-render-unit.md +24 -0
  104. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +22 -0
  105. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +31 -0
  106. package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
  107. package/docs/preview-testing.md +53 -0
  108. package/docs/v0.1-design.md +138 -87
  109. package/examples/demo/README.md +10 -3
  110. package/examples/demo/timelines/demo.timeline.json +12 -0
  111. package/examples/demo/video.config.js +4 -0
  112. package/examples/video/README.md +50 -6
  113. package/examples/video/compositions/video-overlay.html +350 -0
  114. package/examples/video/props/video-timeline.json +10 -0
  115. package/examples/video/timelines/video-timeline-mp4.timeline.json +44 -0
  116. package/examples/video/timelines/video-timeline.timeline.json +44 -0
  117. package/examples/video/video.config.js +6 -0
  118. package/package.json +5 -3
  119. package/skills/frameyard/README.md +7 -5
  120. package/skills/frameyard/SKILL.md +19 -13
  121. package/skills/frameyard/references/common-mistakes.md +1 -0
  122. package/skills/frameyard/references/deterministic-authoring.md +2 -1
  123. package/skills/frameyard/references/diagnostics.md +1 -1
  124. package/skills/frameyard/references/failure-recovery.md +3 -2
  125. package/skills/frameyard/references/json-results.md +15 -6
  126. package/skills/frameyard/references/subtitles-karaoke.md +2 -2
  127. package/skills/frameyard/references/timelines.md +143 -0
  128. package/skills/frameyard/scripts/frameyard.mjs +20 -3
@@ -10,14 +10,21 @@ A 30-second, 1920×1080 HTML composition that exercises the current core:
10
10
  - frame-state CSS variables for the progress bar
11
11
 
12
12
  The composition lives in `compositions/demo.html`; the optional controller is
13
- `compositions/demo.controller.js`.
13
+ `compositions/demo.controller.js`. `timelines/demo.timeline.json` wraps the
14
+ composition as a clip in a 1920×1080, 30 fps timeline.
14
15
 
15
16
  From the repository root:
16
17
 
17
18
  ```sh
18
19
  pnpm build
19
- node dist/cli.js check demo --project examples/demo
20
- node dist/cli.js still demo --frame 240 --project examples/demo --out demo-still.png
20
+ node dist/cli.js check demo-timeline --project examples/demo
21
+ node dist/cli.js still demo-timeline --frame 240 --project examples/demo --out demo-still.png
22
+ node dist/cli.js render demo-timeline --project examples/demo --props examples/demo/props/demo.json
23
+ ```
24
+
25
+ The composition remains renderable on its own:
26
+
27
+ ```sh
21
28
  node dist/cli.js render demo --project examples/demo --props examples/demo/props/demo.json
22
29
  ```
23
30
 
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 1,
3
+ "tracks": [{ "id": "v1", "kind": "video" }],
4
+ "clips": [
5
+ {
6
+ "id": "demo",
7
+ "track": "v1",
8
+ "source": { "kind": "composition", "id": "demo" },
9
+ "start": 0
10
+ }
11
+ ]
12
+ }
@@ -1,5 +1,9 @@
1
1
  export default {
2
+ format: { width: 1920, height: 1080, fps: 30 },
2
3
  compositions: {
3
4
  demo: "./compositions/demo.html",
4
5
  },
6
+ timelines: {
7
+ "demo-timeline": "./timelines/demo.timeline.json",
8
+ },
5
9
  };
@@ -1,7 +1,7 @@
1
1
  # Frameyard video example
2
2
 
3
- A 12-second, 1920×1080 composition that treats a local clip as source footage
4
- and seeks it twice. The project includes the same layout for two source codecs:
3
+ A 12-second, 1920×1080 project that treats a local clip as source footage. The
4
+ project includes the same layout for two source codecs:
5
5
 
6
6
  - `video` reads `assets/clip.webm` (VP9 + Opus)
7
7
  - `video-mp4` reads `assets/clip.mp4` (H.264 + AAC)
@@ -25,11 +25,49 @@ controller is needed. The header, copy, and footer fade individually rather
25
25
  than the whole scene; a full-scene opacity animation above a video can leave
26
26
  the load frame on screen in interactive preview after a large seek.
27
27
 
28
+ ## Timeline with the video on a track
29
+
30
+ `video-timeline` and `video-timeline-mp4` move the footage out of the
31
+ composition and onto timeline tracks. The HTML is now a transparent overlay
32
+ above it:
33
+
34
+ - `v1` holds the hero clip: the whole 12-second source, audio at `0.85`
35
+ - `v2` holds the detail pass from source frame 174 (5.8 s) for 150 frames,
36
+ muted, placed in a 440×248 transform box in the bottom right
37
+ - `v3` holds `video-overlay`, an HTML-only composition with no `<video>`
38
+ elements and a transparent document background
39
+
40
+ `video-timeline` reads `assets/clip.webm` (VP9 + Opus) and
41
+ `video-timeline-mp4` reads `assets/clip.mp4` (H.264 + AAC). Both share the same
42
+ overlay composition and props file; the overlay clip sets its own `format` prop
43
+ so the header reports the codec of each timeline.
44
+
45
+ The detail clip uses `transform.width` and `transform.height` instead of
46
+ `scale`, with `fit: "cover"` filling that box. The box matches the overlay's
47
+ detail window exactly, so the video and its chrome stay aligned in production
48
+ and preview.
49
+
50
+ The overlay draws the scrim, typography, frame border, detail-window chrome,
51
+ progress bar, and outro fade. `video-overlay` declares its own frame animations,
52
+ so the title and footer still animate on the timeline clock. Production captures
53
+ the overlay with alpha and composites it over the video layers with FFmpeg; the
54
+ interactive timeline preview composites the same layers in Chromium.
55
+
56
+ Both timelines accept `props/video-timeline.json`, which overrides the overlay
57
+ composition's props. Timeline `--props` is keyed by composition clip id, so the
58
+ file targets the `overlay` clip.
59
+
60
+ Open the preview with `node dist/cli.js dev --project examples/video`; the
61
+ compositions and both timelines are listed.
62
+
28
63
  The compositions live in `compositions/video.html` and
29
- `compositions/video-mp4.html`. Both clips are 12-second 1280×720 files generated
30
- procedurally for this example; the MP4 is transcoded from the WebM source.
31
- Replace either asset with a local, seekable video file that is at least
32
- 12 seconds long; remote URLs and live streams are not supported.
64
+ `compositions/video-mp4.html`; the overlay lives in
65
+ `compositions/video-overlay.html`, and the timelines in
66
+ `timelines/video-timeline.timeline.json` and
67
+ `timelines/video-timeline-mp4.timeline.json`. Both clips are 12-second 1280×720
68
+ files generated procedurally for this example; the MP4 is transcoded from the
69
+ WebM source. Replace either asset with a local, seekable video file that is at
70
+ least 12 seconds long; remote URLs and live streams are not supported.
33
71
 
34
72
  Regenerate both clips from scratch with the bundled FFmpeg script:
35
73
 
@@ -48,10 +86,16 @@ From the repository root:
48
86
  pnpm build
49
87
  node dist/cli.js check video --project examples/video
50
88
  node dist/cli.js check video-mp4 --project examples/video
89
+ node dist/cli.js check video-timeline --project examples/video
90
+ node dist/cli.js check video-timeline-mp4 --project examples/video
51
91
  node dist/cli.js still video --frame 210 --project examples/video --out video-still.png
52
92
  node dist/cli.js still video-mp4 --frame 210 --project examples/video --out video-mp4-still.png
93
+ node dist/cli.js still video-timeline --frame 210 --project examples/video --out video-timeline-still.png
94
+ node dist/cli.js still video-timeline-mp4 --frame 210 --project examples/video --out video-timeline-mp4-still.png
53
95
  node dist/cli.js render video --project examples/video --props examples/video/props/video.json
54
96
  node dist/cli.js render video-mp4 --project examples/video --props examples/video/props/video.json
97
+ node dist/cli.js render video-timeline --project examples/video --props examples/video/props/video-timeline.json
98
+ node dist/cli.js render video-timeline-mp4 --project examples/video --props examples/video/props/video-timeline.json
55
99
  ```
56
100
 
57
101
  Start the interactive preview with:
@@ -0,0 +1,350 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <script type="application/json" data-fy="props">
6
+ {
7
+ "kicker": "Frameyard / Timeline Overlay 001",
8
+ "title": "Timeline",
9
+ "titleAccent": "Overlay",
10
+ "subtitle": "The clip lives on a timeline track. This HTML layer composites over it with alpha.",
11
+ "detail": "Detail / second pass",
12
+ "format": "30 FPS",
13
+ "accent": "#ffc37a"
14
+ }
15
+ </script>
16
+ <style>
17
+ @keyframes fade-in {
18
+ from {
19
+ opacity: 0;
20
+ }
21
+ to {
22
+ opacity: 1;
23
+ }
24
+ }
25
+ @keyframes rise {
26
+ from {
27
+ opacity: 0;
28
+ transform: translateY(42px);
29
+ }
30
+ to {
31
+ opacity: 1;
32
+ transform: translateY(0);
33
+ }
34
+ }
35
+ @keyframes draw {
36
+ from {
37
+ opacity: 0;
38
+ transform: scaleX(0);
39
+ }
40
+ to {
41
+ opacity: 1;
42
+ transform: scaleX(1);
43
+ }
44
+ }
45
+ @keyframes detail-cycle {
46
+ 0% {
47
+ opacity: 0;
48
+ }
49
+ 6% {
50
+ opacity: 1;
51
+ }
52
+ 94% {
53
+ opacity: 1;
54
+ }
55
+ 100% {
56
+ opacity: 0;
57
+ }
58
+ }
59
+ @keyframes outro-fade {
60
+ from {
61
+ opacity: 0;
62
+ }
63
+ to {
64
+ opacity: 1;
65
+ }
66
+ }
67
+ html,
68
+ body,
69
+ fy-composition {
70
+ background: transparent;
71
+ }
72
+ #scene {
73
+ position: relative;
74
+ width: 100%;
75
+ height: 100%;
76
+ overflow: hidden;
77
+ isolation: isolate;
78
+ color: #eef4f1;
79
+ font-family: Inter, "Helvetica Neue", ui-sans-serif, system-ui, sans-serif;
80
+ }
81
+ #scrim {
82
+ position: absolute;
83
+ inset: 0;
84
+ background: linear-gradient(
85
+ 90deg,
86
+ rgba(4, 8, 11, 0.94) 0%,
87
+ rgba(4, 8, 11, 0.82) 22%,
88
+ rgba(4, 8, 11, 0.35) 46%,
89
+ rgba(4, 8, 11, 0) 66%
90
+ );
91
+ pointer-events: none;
92
+ }
93
+ #footer-scrim {
94
+ position: absolute;
95
+ left: 0;
96
+ right: 0;
97
+ bottom: 0;
98
+ height: 150px;
99
+ background: linear-gradient(0deg, rgba(4, 8, 11, 0.88) 0%, rgba(4, 8, 11, 0) 100%);
100
+ pointer-events: none;
101
+ }
102
+ #frame-border {
103
+ position: absolute;
104
+ inset: 36px;
105
+ border: 1px solid rgba(238, 244, 241, 0.16);
106
+ pointer-events: none;
107
+ }
108
+ #header {
109
+ position: absolute;
110
+ top: 80px;
111
+ left: 148px;
112
+ right: 148px;
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ gap: 32px;
117
+ animation: fade-in 1s linear both;
118
+ }
119
+ #kicker {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 15px;
123
+ font:
124
+ 600 14px/1 ui-monospace,
125
+ monospace;
126
+ letter-spacing: 0.24em;
127
+ text-transform: uppercase;
128
+ color: var(--fy-prop-accent);
129
+ }
130
+ #kicker::before {
131
+ content: "";
132
+ width: 8px;
133
+ height: 8px;
134
+ background: var(--fy-prop-accent);
135
+ box-shadow: 0 0 16px var(--fy-prop-accent);
136
+ }
137
+ #format {
138
+ font:
139
+ 500 12px/1 ui-monospace,
140
+ monospace;
141
+ letter-spacing: 0.16em;
142
+ text-transform: uppercase;
143
+ color: #b9c6c8;
144
+ text-shadow: 0 2px 18px rgba(0, 0, 0, 0.65);
145
+ }
146
+ #copy {
147
+ position: absolute;
148
+ top: 278px;
149
+ left: 148px;
150
+ width: 860px;
151
+ }
152
+ #title {
153
+ margin: 0;
154
+ font-size: 174px;
155
+ line-height: 0.84;
156
+ letter-spacing: -0.064em;
157
+ font-weight: 660;
158
+ }
159
+ #title span,
160
+ #title em {
161
+ display: block;
162
+ animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
163
+ }
164
+ #title em {
165
+ margin-top: 0.06em;
166
+ font-family: "Iowan Old Style", "Palatino Linotype", Georgia, ui-serif, serif;
167
+ font-style: italic;
168
+ font-weight: 400;
169
+ letter-spacing: -0.035em;
170
+ color: var(--fy-prop-accent);
171
+ }
172
+ #subtitle {
173
+ width: 720px;
174
+ margin: 38px 0 0;
175
+ font-size: 29px;
176
+ line-height: 1.5;
177
+ font-weight: 420;
178
+ color: #b3c1c4;
179
+ animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
180
+ }
181
+ #detail {
182
+ position: absolute;
183
+ left: 1332px;
184
+ top: 646px;
185
+ width: 440px;
186
+ animation: detail-cycle 1s linear both;
187
+ }
188
+ #detail-bar {
189
+ display: flex;
190
+ justify-content: space-between;
191
+ align-items: center;
192
+ gap: 16px;
193
+ height: 22px;
194
+ margin-bottom: 10px;
195
+ padding: 0 10px;
196
+ background: rgba(4, 8, 11, 0.55);
197
+ font:
198
+ 600 11px/1 ui-monospace,
199
+ monospace;
200
+ letter-spacing: 0.18em;
201
+ text-transform: uppercase;
202
+ }
203
+ #detail-bar > span:first-child {
204
+ color: var(--fy-prop-accent);
205
+ }
206
+ #detail-time {
207
+ color: #c0cdcf;
208
+ font-weight: 500;
209
+ }
210
+ #detail-window {
211
+ position: relative;
212
+ width: 440px;
213
+ height: 248px;
214
+ box-sizing: border-box;
215
+ overflow: hidden;
216
+ border: 1px solid rgba(255, 195, 122, 0.75);
217
+ background: transparent;
218
+ box-shadow: 0 24px 90px rgba(0, 0, 0, 0.45);
219
+ }
220
+ #detail-tint {
221
+ position: absolute;
222
+ inset: 0;
223
+ background: linear-gradient(0deg, rgba(4, 8, 11, 0.32) 0%, rgba(4, 8, 11, 0) 45%);
224
+ pointer-events: none;
225
+ }
226
+ #footer {
227
+ position: absolute;
228
+ left: 148px;
229
+ right: 148px;
230
+ bottom: 70px;
231
+ contain: layout paint style;
232
+ animation: fade-in 1s linear both;
233
+ }
234
+ #rule {
235
+ position: relative;
236
+ height: 1px;
237
+ background: rgba(238, 244, 241, 0.24);
238
+ transform-origin: left center;
239
+ animation: draw 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
240
+ }
241
+ #progress {
242
+ position: absolute;
243
+ inset: 0 auto 0 0;
244
+ width: calc(var(--fy-progress) * 100%);
245
+ background: var(--fy-prop-accent);
246
+ box-shadow: 0 0 14px var(--fy-prop-accent);
247
+ }
248
+ #footer-row {
249
+ display: flex;
250
+ justify-content: space-between;
251
+ align-items: baseline;
252
+ gap: 24px;
253
+ margin-top: 18px;
254
+ font:
255
+ 500 12px/1 ui-monospace,
256
+ monospace;
257
+ letter-spacing: 0.14em;
258
+ color: #a4b1b3;
259
+ text-transform: uppercase;
260
+ text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7);
261
+ }
262
+ #footer-row strong {
263
+ color: #edf5f2;
264
+ font-weight: 600;
265
+ }
266
+ #footer-row .accent {
267
+ color: var(--fy-prop-accent);
268
+ }
269
+ #frame-counter {
270
+ display: inline-block;
271
+ min-width: 3ch;
272
+ text-align: right;
273
+ }
274
+ #frame-counter::after {
275
+ counter-reset: fy-frame var(--fy-frame);
276
+ content: counter(fy-frame);
277
+ }
278
+ #outro-fade {
279
+ position: absolute;
280
+ inset: 0;
281
+ background: #04080b;
282
+ pointer-events: none;
283
+ opacity: 0;
284
+ animation: outro-fade 1s linear both;
285
+ }
286
+ </style>
287
+ </head>
288
+ <body>
289
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="360">
290
+ <div id="scene">
291
+ <div id="scrim"></div>
292
+ <div id="footer-scrim"></div>
293
+ <div id="frame-border"></div>
294
+
295
+ <header id="header" data-fy-anim-start="8" data-fy-anim-duration="28">
296
+ <div id="kicker" data-fy-text="kicker"></div>
297
+ <div id="format">Timeline video / <span data-fy-text="format"></span></div>
298
+ </header>
299
+
300
+ <div id="copy">
301
+ <h1 id="title">
302
+ <span
303
+ data-fy-text="title"
304
+ data-fy-anim-start="14"
305
+ data-fy-anim-duration="44"
306
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
307
+ ></span>
308
+ <em
309
+ data-fy-text="titleAccent"
310
+ data-fy-anim-start="26"
311
+ data-fy-anim-duration="44"
312
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
313
+ ></em>
314
+ </h1>
315
+ <p
316
+ id="subtitle"
317
+ data-fy-text="subtitle"
318
+ data-fy-anim-start="40"
319
+ data-fy-anim-duration="40"
320
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
321
+ ></p>
322
+ </div>
323
+
324
+ <div id="detail" data-fy-anim-start="112" data-fy-anim-duration="170">
325
+ <div id="detail-bar">
326
+ <span data-fy-text="detail"></span>
327
+ <span id="detail-time">SRC 5.8–10.8 S</span>
328
+ </div>
329
+ <div id="detail-window">
330
+ <div id="detail-tint"></div>
331
+ </div>
332
+ </div>
333
+
334
+ <div id="footer" data-fy-anim-start="46" data-fy-anim-duration="30">
335
+ <div id="rule" data-fy-anim-start="52" data-fy-anim-duration="50">
336
+ <div id="progress"></div>
337
+ </div>
338
+ <div id="footer-row">
339
+ <span>Hero / v1 · SRC <strong>0.0–12.0 S</strong></span>
340
+ <span>Detail / v2 · SRC <strong>5.8–10.8 S</strong></span>
341
+ <span>Frame <strong id="frame-counter"></strong> / 359</span>
342
+ <span class="accent">HTML overlay / v3 · 30 FPS</span>
343
+ </div>
344
+ </div>
345
+
346
+ <div id="outro-fade" data-fy-anim-start="329" data-fy-anim-duration="30"></div>
347
+ </div>
348
+ </fy-composition>
349
+ </body>
350
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "overlay": {
3
+ "kicker": "Frameyard / Timeline Overlay 001",
4
+ "title": "Track",
5
+ "titleAccent": "Overlay",
6
+ "subtitle": "One clip on v1, a second pass on v2, and this HTML layer on v3.",
7
+ "detail": "Detail / second pass",
8
+ "accent": "#8fe6c8"
9
+ }
10
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "version": 1,
3
+ "name": "Video on a track, HTML overlay on top (MP4)",
4
+ "tracks": [
5
+ { "id": "v1", "kind": "video", "name": "Hero video" },
6
+ { "id": "v2", "kind": "video", "name": "Detail video" },
7
+ { "id": "v3", "kind": "video", "name": "HTML overlay" }
8
+ ],
9
+ "clips": [
10
+ {
11
+ "id": "hero",
12
+ "track": "v1",
13
+ "source": { "kind": "video", "src": "assets/clip.mp4" },
14
+ "start": 0,
15
+ "audio": { "gain": 0.85 }
16
+ },
17
+ {
18
+ "id": "detail",
19
+ "track": "v2",
20
+ "source": { "kind": "video", "src": "assets/clip.mp4" },
21
+ "start": 120,
22
+ "sourceIn": 174,
23
+ "duration": 150,
24
+ "transform": {
25
+ "fit": "cover",
26
+ "width": 440,
27
+ "height": 248,
28
+ "x": 592,
29
+ "y": 262,
30
+ "rotation": 0,
31
+ "opacity": 1
32
+ },
33
+ "audio": { "muted": true }
34
+ },
35
+ {
36
+ "id": "overlay",
37
+ "track": "v3",
38
+ "source": { "kind": "composition", "id": "video-overlay" },
39
+ "start": 0,
40
+ "duration": 360,
41
+ "props": { "format": "H.264 + AAC / 30 FPS" }
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "version": 1,
3
+ "name": "Video on a track, HTML overlay on top",
4
+ "tracks": [
5
+ { "id": "v1", "kind": "video", "name": "Hero video" },
6
+ { "id": "v2", "kind": "video", "name": "Detail video" },
7
+ { "id": "v3", "kind": "video", "name": "HTML overlay" }
8
+ ],
9
+ "clips": [
10
+ {
11
+ "id": "hero",
12
+ "track": "v1",
13
+ "source": { "kind": "video", "src": "assets/clip.webm" },
14
+ "start": 0,
15
+ "audio": { "gain": 0.85 }
16
+ },
17
+ {
18
+ "id": "detail",
19
+ "track": "v2",
20
+ "source": { "kind": "video", "src": "assets/clip.webm" },
21
+ "start": 120,
22
+ "sourceIn": 174,
23
+ "duration": 150,
24
+ "transform": {
25
+ "fit": "cover",
26
+ "width": 440,
27
+ "height": 248,
28
+ "x": 592,
29
+ "y": 262,
30
+ "rotation": 0,
31
+ "opacity": 1
32
+ },
33
+ "audio": { "muted": true }
34
+ },
35
+ {
36
+ "id": "overlay",
37
+ "track": "v3",
38
+ "source": { "kind": "composition", "id": "video-overlay" },
39
+ "start": 0,
40
+ "duration": 360,
41
+ "props": { "format": "VP9 + Opus / 30 FPS" }
42
+ }
43
+ ]
44
+ }
@@ -1,6 +1,12 @@
1
1
  export default {
2
+ format: { width: 1920, height: 1080, fps: 30 },
2
3
  compositions: {
3
4
  video: "./compositions/video.html",
4
5
  "video-mp4": "./compositions/video-mp4.html",
6
+ "video-overlay": "./compositions/video-overlay.html",
7
+ },
8
+ timelines: {
9
+ "video-timeline": "./timelines/video-timeline.timeline.json",
10
+ "video-timeline-mp4": "./timelines/video-timeline-mp4.timeline.json",
5
11
  },
6
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameyard",
3
- "version": "0.5.0",
3
+ "version": "0.7.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,8 +74,10 @@
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",
80
82
  "set-version": "node scripts/set-version.mjs",
81
83
  "verify-release": "node scripts/verify-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