frameyard 0.7.0 → 0.9.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 (334) hide show
  1. package/CHANGELOG.md +164 -7
  2. package/GLOSSARY.md +152 -18
  3. package/README.md +328 -285
  4. package/dist/assets/integrations.d.ts +8 -4
  5. package/dist/assets/integrations.d.ts.map +1 -1
  6. package/dist/assets/integrations.js +8 -4
  7. package/dist/assets/integrations.js.map +1 -1
  8. package/dist/assets/provenance.d.ts +2 -2
  9. package/dist/assets/provenance.d.ts.map +1 -1
  10. package/dist/assets/provenance.js +5 -3
  11. package/dist/assets/provenance.js.map +1 -1
  12. package/dist/assets/tts-supertonic.d.ts +28 -0
  13. package/dist/assets/tts-supertonic.d.ts.map +1 -0
  14. package/dist/assets/tts-supertonic.js +156 -0
  15. package/dist/assets/tts-supertonic.js.map +1 -0
  16. package/dist/assets/tts.d.ts +18 -6
  17. package/dist/assets/tts.d.ts.map +1 -1
  18. package/dist/assets/tts.js +219 -97
  19. package/dist/assets/tts.js.map +1 -1
  20. package/dist/cli.js +1705 -106
  21. package/dist/cli.js.map +1 -1
  22. package/dist/composition/descriptor.d.ts +17 -3
  23. package/dist/composition/descriptor.d.ts.map +1 -1
  24. package/dist/composition/descriptor.js +51 -28
  25. package/dist/composition/descriptor.js.map +1 -1
  26. package/dist/composition/format.d.ts +12 -0
  27. package/dist/composition/format.d.ts.map +1 -0
  28. package/dist/composition/format.js +28 -0
  29. package/dist/composition/format.js.map +1 -0
  30. package/dist/composition/index.d.ts +2 -1
  31. package/dist/composition/index.d.ts.map +1 -1
  32. package/dist/composition/index.js +1 -0
  33. package/dist/composition/index.js.map +1 -1
  34. package/dist/context.d.ts +82 -0
  35. package/dist/context.d.ts.map +1 -0
  36. package/dist/context.js +277 -0
  37. package/dist/context.js.map +1 -0
  38. package/dist/contracts.d.ts +80 -6
  39. package/dist/contracts.d.ts.map +1 -1
  40. package/dist/contracts.js +3 -0
  41. package/dist/contracts.js.map +1 -1
  42. package/dist/doctor.d.ts +14 -1
  43. package/dist/doctor.d.ts.map +1 -1
  44. package/dist/doctor.js +166 -7
  45. package/dist/doctor.js.map +1 -1
  46. package/dist/errors.d.ts +1 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/fonts/add.d.ts +41 -0
  50. package/dist/fonts/add.d.ts.map +1 -0
  51. package/dist/fonts/add.js +259 -0
  52. package/dist/fonts/add.js.map +1 -0
  53. package/dist/fonts/binary.d.ts +4 -0
  54. package/dist/fonts/binary.d.ts.map +1 -0
  55. package/dist/fonts/binary.js +21 -0
  56. package/dist/fonts/binary.js.map +1 -0
  57. package/dist/fonts/css.d.ts +4 -0
  58. package/dist/fonts/css.d.ts.map +1 -0
  59. package/dist/fonts/css.js +50 -0
  60. package/dist/fonts/css.js.map +1 -0
  61. package/dist/fonts/fontsource.d.ts +107 -0
  62. package/dist/fonts/fontsource.d.ts.map +1 -0
  63. package/dist/fonts/fontsource.js +565 -0
  64. package/dist/fonts/fontsource.js.map +1 -0
  65. package/dist/fonts/index.d.ts +11 -0
  66. package/dist/fonts/index.d.ts.map +1 -0
  67. package/dist/fonts/index.js +11 -0
  68. package/dist/fonts/index.js.map +1 -0
  69. package/dist/fonts/install.d.ts +24 -0
  70. package/dist/fonts/install.d.ts.map +1 -0
  71. package/dist/fonts/install.js +145 -0
  72. package/dist/fonts/install.js.map +1 -0
  73. package/dist/fonts/manifest.d.ts +21 -0
  74. package/dist/fonts/manifest.d.ts.map +1 -0
  75. package/dist/fonts/manifest.js +96 -0
  76. package/dist/fonts/manifest.js.map +1 -0
  77. package/dist/fonts/options.d.ts +9 -0
  78. package/dist/fonts/options.d.ts.map +1 -0
  79. package/dist/fonts/options.js +66 -0
  80. package/dist/fonts/options.js.map +1 -0
  81. package/dist/fonts/schema.d.ts +4 -0
  82. package/dist/fonts/schema.d.ts.map +1 -0
  83. package/dist/fonts/schema.js +316 -0
  84. package/dist/fonts/schema.js.map +1 -0
  85. package/dist/fonts/summary.d.ts +16 -0
  86. package/dist/fonts/summary.d.ts.map +1 -0
  87. package/dist/fonts/summary.js +22 -0
  88. package/dist/fonts/summary.js.map +1 -0
  89. package/dist/fonts/types.d.ts +56 -0
  90. package/dist/fonts/types.d.ts.map +1 -0
  91. package/dist/fonts/types.js +8 -0
  92. package/dist/fonts/types.js.map +1 -0
  93. package/dist/fonts/verify.d.ts +33 -0
  94. package/dist/fonts/verify.d.ts.map +1 -0
  95. package/dist/fonts/verify.js +200 -0
  96. package/dist/fonts/verify.js.map +1 -0
  97. package/dist/html/document-adapter.d.ts +3 -3
  98. package/dist/html/document-adapter.d.ts.map +1 -1
  99. package/dist/html/document-adapter.js +1 -1
  100. package/dist/html/document-adapter.js.map +1 -1
  101. package/dist/html/index.d.ts +1 -1
  102. package/dist/html/index.d.ts.map +1 -1
  103. package/dist/html/parse.d.ts +10 -2
  104. package/dist/html/parse.d.ts.map +1 -1
  105. package/dist/html/parse.js +14 -5
  106. package/dist/html/parse.js.map +1 -1
  107. package/dist/html/validate.d.ts +12 -1
  108. package/dist/html/validate.d.ts.map +1 -1
  109. package/dist/html/validate.js +292 -61
  110. package/dist/html/validate.js.map +1 -1
  111. package/dist/index.d.ts +4 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -2
  114. package/dist/index.js.map +1 -1
  115. package/dist/inspect.d.ts +61 -0
  116. package/dist/inspect.d.ts.map +1 -0
  117. package/dist/inspect.js +325 -0
  118. package/dist/inspect.js.map +1 -0
  119. package/dist/lint.d.ts +63 -0
  120. package/dist/lint.d.ts.map +1 -0
  121. package/dist/lint.js +265 -0
  122. package/dist/lint.js.map +1 -0
  123. package/dist/policy.d.ts +69 -0
  124. package/dist/policy.d.ts.map +1 -0
  125. package/dist/policy.js +361 -0
  126. package/dist/policy.js.map +1 -0
  127. package/dist/preview/main.d.ts +2 -2
  128. package/dist/preview/main.d.ts.map +1 -1
  129. package/dist/preview/main.js +35 -14
  130. package/dist/preview/main.js.map +1 -1
  131. package/dist/preview/timeline-data.d.ts +17 -65
  132. package/dist/preview/timeline-data.d.ts.map +1 -1
  133. package/dist/preview/timeline-host.d.ts.map +1 -1
  134. package/dist/preview/timeline-host.js +73 -38
  135. package/dist/preview/timeline-host.js.map +1 -1
  136. package/dist/preview/timeline-main.d.ts.map +1 -1
  137. package/dist/preview/timeline-main.js +47 -3
  138. package/dist/preview/timeline-main.js.map +1 -1
  139. package/dist/project.d.ts +13 -8
  140. package/dist/project.d.ts.map +1 -1
  141. package/dist/project.js +20 -17
  142. package/dist/project.js.map +1 -1
  143. package/dist/renderer/browser.d.ts +2 -2
  144. package/dist/renderer/browser.d.ts.map +1 -1
  145. package/dist/renderer/browser.js +10 -4
  146. package/dist/renderer/browser.js.map +1 -1
  147. package/dist/renderer/ffmpeg.d.ts +44 -1
  148. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  149. package/dist/renderer/ffmpeg.js +371 -73
  150. package/dist/renderer/ffmpeg.js.map +1 -1
  151. package/dist/renderer/index.d.ts +40 -4
  152. package/dist/renderer/index.d.ts.map +1 -1
  153. package/dist/renderer/index.js +100 -25
  154. package/dist/renderer/index.js.map +1 -1
  155. package/dist/renderer/keyframe-expressions.d.ts +39 -0
  156. package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
  157. package/dist/renderer/keyframe-expressions.js +180 -0
  158. package/dist/renderer/keyframe-expressions.js.map +1 -0
  159. package/dist/renderer/timeline.d.ts +2 -0
  160. package/dist/renderer/timeline.d.ts.map +1 -1
  161. package/dist/renderer/timeline.js +168 -106
  162. package/dist/renderer/timeline.js.map +1 -1
  163. package/dist/runtime/host.d.ts.map +1 -1
  164. package/dist/runtime/host.js +159 -31
  165. package/dist/runtime/host.js.map +1 -1
  166. package/dist/scaffold.d.ts +3 -0
  167. package/dist/scaffold.d.ts.map +1 -1
  168. package/dist/scaffold.js +48 -317
  169. package/dist/scaffold.js.map +1 -1
  170. package/dist/server/composition-html.d.ts +15 -7
  171. package/dist/server/composition-html.d.ts.map +1 -1
  172. package/dist/server/composition-html.js +72 -16
  173. package/dist/server/composition-html.js.map +1 -1
  174. package/dist/server/server.d.ts.map +1 -1
  175. package/dist/server/server.js +56 -6
  176. package/dist/server/server.js.map +1 -1
  177. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  178. package/dist/server/timeline-preview-data.js +12 -23
  179. package/dist/server/timeline-preview-data.js.map +1 -1
  180. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  181. package/dist/server/timeline-preview-html.js +20 -3
  182. package/dist/server/timeline-preview-html.js.map +1 -1
  183. package/dist/templates.d.ts +16 -0
  184. package/dist/templates.d.ts.map +1 -0
  185. package/dist/templates.js +186 -0
  186. package/dist/templates.js.map +1 -0
  187. package/dist/timeline/easing.d.ts +36 -0
  188. package/dist/timeline/easing.d.ts.map +1 -0
  189. package/dist/timeline/easing.js +225 -0
  190. package/dist/timeline/easing.js.map +1 -0
  191. package/dist/timeline/keyframes.d.ts +23 -0
  192. package/dist/timeline/keyframes.d.ts.map +1 -0
  193. package/dist/timeline/keyframes.js +95 -0
  194. package/dist/timeline/keyframes.js.map +1 -0
  195. package/dist/timeline/plan.d.ts +6 -12
  196. package/dist/timeline/plan.d.ts.map +1 -1
  197. package/dist/timeline/plan.js +17 -30
  198. package/dist/timeline/plan.js.map +1 -1
  199. package/dist/timeline/resolve.d.ts +16 -3
  200. package/dist/timeline/resolve.d.ts.map +1 -1
  201. package/dist/timeline/resolve.js +119 -13
  202. package/dist/timeline/resolve.js.map +1 -1
  203. package/dist/timeline/scene.d.ts +126 -0
  204. package/dist/timeline/scene.d.ts.map +1 -0
  205. package/dist/timeline/scene.js +109 -0
  206. package/dist/timeline/scene.js.map +1 -0
  207. package/dist/timeline/schema.d.ts +38 -0
  208. package/dist/timeline/schema.d.ts.map +1 -1
  209. package/dist/timeline/schema.js +290 -9
  210. package/dist/timeline/schema.js.map +1 -1
  211. package/dist/timeline/transitions.d.ts +21 -0
  212. package/dist/timeline/transitions.d.ts.map +1 -0
  213. package/dist/timeline/transitions.js +23 -0
  214. package/dist/timeline/transitions.js.map +1 -0
  215. package/dist/version.d.ts +1 -1
  216. package/dist/version.js +1 -1
  217. package/docs/adr/0007-make-html-documents-the-composition-unit.md +2 -2
  218. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  219. package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
  220. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  221. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  222. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +38 -0
  223. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  224. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  225. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  226. package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
  227. package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
  228. package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
  229. package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
  230. package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
  231. package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
  232. package/docs/migration-0.4.md +5 -3
  233. package/docs/migration-0.8.md +163 -0
  234. package/docs/preview-testing.md +23 -3
  235. package/docs/{v0.1-design.md → v0.x-design.md} +86 -53
  236. package/examples/blend/README.md +106 -0
  237. package/examples/blend/assets/ink.mp4 +0 -0
  238. package/examples/blend/compositions/aurora.html +55 -0
  239. package/examples/blend/compositions/backdrop.html +74 -0
  240. package/examples/blend/compositions/labels.html +149 -0
  241. package/examples/blend/fy.config.js +11 -0
  242. package/examples/blend/scripts/make-ink.sh +26 -0
  243. package/examples/blend/timelines/blend.timeline.json +155 -0
  244. package/examples/demo/README.md +2 -2
  245. package/examples/demo/compositions/demo.html +243 -251
  246. package/examples/intro/README.md +71 -0
  247. package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
  248. package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
  249. package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
  250. package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
  251. package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
  252. package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
  253. package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
  254. package/examples/intro/assets/grain.png +0 -0
  255. package/examples/intro/assets/score.ogg +0 -0
  256. package/examples/intro/compositions/brand.html +412 -0
  257. package/examples/intro/compositions/chrome.controller.js +63 -0
  258. package/examples/intro/compositions/chrome.html +256 -0
  259. package/examples/intro/compositions/lattice.controller.js +148 -0
  260. package/examples/intro/compositions/lattice.html +308 -0
  261. package/examples/intro/compositions/opening.html +504 -0
  262. package/examples/intro/compositions/studio.html +930 -0
  263. package/examples/intro/compositions/theme.css +218 -0
  264. package/examples/intro/fy.config.js +13 -0
  265. package/examples/intro/fy.fonts.json +100 -0
  266. package/examples/intro/scripts/make-grain.mjs +79 -0
  267. package/examples/intro/scripts/make-score.mjs +702 -0
  268. package/examples/intro/timelines/intro.timeline.json +60 -0
  269. package/examples/keyframes/README.md +76 -0
  270. package/examples/keyframes/compositions/backdrop.html +60 -0
  271. package/examples/keyframes/compositions/card.html +99 -0
  272. package/examples/keyframes/fy.config.js +10 -0
  273. package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
  274. package/examples/nested/README.md +112 -0
  275. package/examples/nested/assets/pulse.wav +0 -0
  276. package/examples/nested/compositions/backdrop.html +84 -0
  277. package/examples/nested/compositions/card.html +275 -0
  278. package/examples/nested/compositions/overlay.html +307 -0
  279. package/examples/nested/fy.config.js +13 -0
  280. package/examples/nested/scripts/make-pulse.sh +20 -0
  281. package/examples/nested/timelines/chapter-a.timeline.json +39 -0
  282. package/examples/nested/timelines/chapter-b.timeline.json +39 -0
  283. package/examples/nested/timelines/nested-reel.timeline.json +52 -0
  284. package/examples/static/README.md +17 -0
  285. package/examples/static/compositions/still.html +37 -0
  286. package/examples/static/fy.config.js +9 -0
  287. package/examples/static/timelines/still.timeline.json +13 -0
  288. package/examples/transitions/README.md +128 -0
  289. package/examples/transitions/assets/alpha.wav +0 -0
  290. package/examples/transitions/assets/beta.wav +0 -0
  291. package/examples/transitions/assets/gamma.wav +0 -0
  292. package/examples/transitions/compositions/alpha.html +56 -0
  293. package/examples/transitions/compositions/beta.html +56 -0
  294. package/examples/transitions/compositions/gamma.html +56 -0
  295. package/examples/transitions/fy.config.js +12 -0
  296. package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
  297. package/examples/transitions/timelines/transitions.timeline.json +31 -0
  298. package/examples/video/README.md +4 -4
  299. package/examples/video/compositions/video-mp4.html +349 -357
  300. package/examples/video/compositions/video-overlay.html +337 -345
  301. package/examples/video/compositions/video.html +349 -357
  302. package/examples/webgl/README.md +1 -1
  303. package/examples/webgl/compositions/webgl.html +205 -213
  304. package/package.json +13 -5
  305. package/scaffolds/init/AGENTS.md +131 -0
  306. package/scaffolds/init/README.md +56 -0
  307. package/scaffolds/init/compositions/hello.html +79 -0
  308. package/scaffolds/init/fy.config.js +9 -0
  309. package/scaffolds/init/gitignore.template +1 -0
  310. package/scaffolds/init/package.json +11 -0
  311. package/scaffolds/init/props/hello.json +4 -0
  312. package/scaffolds/init/props/main.json +6 -0
  313. package/scaffolds/init/timelines/main.timeline.json +20 -0
  314. package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
  315. package/scaffolds/local-models/fy.policy.json +5 -0
  316. package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
  317. package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
  318. package/scaffolds/local-models/pi/sandbox.json +15 -0
  319. package/scaffolds/local-models/pi/settings.json +15 -0
  320. package/skills/frameyard/README.md +1 -0
  321. package/skills/frameyard/SKILL.md +59 -59
  322. package/skills/frameyard/references/audio-assets.md +32 -3
  323. package/skills/frameyard/references/common-mistakes.md +81 -7
  324. package/skills/frameyard/references/controllers.md +5 -4
  325. package/skills/frameyard/references/deterministic-authoring.md +49 -37
  326. package/skills/frameyard/references/diagnostics.md +42 -4
  327. package/skills/frameyard/references/failure-recovery.md +56 -10
  328. package/skills/frameyard/references/fonts.md +132 -0
  329. package/skills/frameyard/references/json-results.md +165 -10
  330. package/skills/frameyard/references/quick-reference.md +222 -0
  331. package/skills/frameyard/references/timelines.md +109 -15
  332. /package/examples/demo/{video.config.js → fy.config.js} +0 -0
  333. /package/examples/video/{video.config.js → fy.config.js} +0 -0
  334. /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
@@ -12,7 +12,7 @@ The 1.8 TB USB HDD provides enough scratch capacity; a raw-frame cache would be
12
12
  ## Considered Options
13
13
 
14
14
  - **Live browser host (ADR 0011).** Rejected: deterministic and pixel-exact, but 138.8 ms per 1920×1080 frame with four layers versus 33.6 ms for a direct composition capture. It does not meet the long-timeline target.
15
- - **Skia + FFmpeg streaming compositor.** Rejected for 0.6: it avoids intermediates and preserves capture pixels, but adds a large compositor dependency and a custom frame pipeline. The FFmpeg + FFV1 path already meets the target without it. Skia remains an option if future effects or blend modes outgrow FFmpeg filters.
15
+ - **Skia + FFmpeg streaming compositor.** Rejected for 0.6: it avoids intermediates and preserves capture pixels, but adds a large compositor dependency and a custom frame pipeline. The FFmpeg + FFV1 path already meets the target without it. Skia remains an option if future effects outgrow FFmpeg filters; ADR 0014 records the static blend-mode path that stays within FFmpeg.
16
16
  - **FFmpeg streaming through pipes.** Rejected: avoids intermediates and dependencies, but requires a fragile cross-platform multi-pipe/filter-graph coordination for a variable set of active layers.
17
17
  - **Pre-render every clip for the whole timeline.** Rejected: for 60 minutes the intermediates can be tens to hundreds of gigabytes and the write/read time alone can exceed the render budget. Segmentation bounds peak scratch and lets workers run in parallel.
18
18
  - **Lossy intermediates.** Rejected as the default: an extra lossy encode breaks pixel parity at clip capture. FFV1 keeps the captured frames exact; only the final H.264 encode is lossy, as it already is for composition renders.
@@ -0,0 +1,38 @@
1
+ # Compose timeline clips with CSS blend modes
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-03
5
+ **Amended by:** [ADR-0018](0018-animate-timeline-clips-with-keyframes.md) (clip keyframes; blend modes remain static)
6
+
7
+ Visual timeline clips may declare a static `blend` mode. The mode is a CSS `mix-blend-mode` value and composites the clip against the accumulated backdrop of all lower layers and the timeline background. The clip's source alpha gates the blend: transparent pixels leave the backdrop unchanged. The supported modes are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`. `hue`, `saturation`, `color`, and `luminosity` require cross-channel math and are rejected at validation. `blend` is a top-level clip field, omitted means `normal`, and the timeline document stays at version 1.
8
+
9
+ Preview applies `mix-blend-mode` to the clip's DOM wrapper. Production applies FFmpeg's `blend` filter with the source alpha mask, then composites the blended result source-over. Chromium/CSS is the reference; FFmpeg is an approximation. The existing guarantee remains: parity is at clip capture, not at the final encoded pixel. Blend modes are static; there are no animated blend modes.
10
+
11
+ ## Context
12
+
13
+ Timeline rendering already supports static transforms and opacity. Blend modes are the natural next compositing feature, and the preview and production paths need the same semantics. FFmpeg's native `blend` is not alpha-aware: a transparent source pixel would replace the backdrop. The compositor therefore has to place the layer on a stage-sized transparent canvas, blend it with the backdrop, use the source alpha as a mask, and then composite normally. The alpha-correct recipe costs roughly 1.7× per blended 1080p layer in a micro-benchmark.
14
+
15
+ CSS and FFmpeg differ in a few places. FFmpeg's `soft-light` uses a legacy formula, so the W3C piecewise formula is supplied as a per-component expression. `color-dodge` and `color-burn` need the source as blend input #0; `overlay` and `hard-light` need the backdrop first. The separable modes otherwise match CSS within roughly 2/255 in 8-bit sRGB.
16
+
17
+ Preview layer order previously followed document/first-activation order, while production follows track order. Blend modes make that divergence visible, so preview now stacks by track and isolates the stage. Blending never crosses an iframe boundary; a nested timeline is its own document.
18
+
19
+ ## Considered Options
20
+
21
+ - **Preview-only blend modes.** Rejected: the preview would lie about the final output.
22
+ - **Blend the whole clip box as opaque.** Rejected: transparent compositions, `contain` letterboxing, and rotated edges would paint over the backdrop instead of leaving it intact.
23
+ - **Isolate each track into a group before blending.** Rejected: tracks are flat ordering, and track groups would change the existing compositing model. The backdrop is everything below the clip in the stage.
24
+ - **Inject a black backdrop into transparent nested timelines.** Rejected: a nested timeline with no declared background has no backdrop by definition. Authors who want internal blending against a color declare a background; authors who want the group to blend with the parent put `blend` on the nested clip.
25
+ - **Warn when a composition paints an opaque background.** Rejected: detecting the effective background is heuristic, and an opaque composition is a legitimate choice. The limitation is documented.
26
+ - **Implement the four non-separable modes with custom expressions.** Rejected for now: they need cross-channel math and would complicate the filter graph and validation for little gain.
27
+ - **Move compositing to Skia to preserve capture pixels.** Rejected for this change: ADR 0012 left Skia as a future option, but FFmpeg meets the blend requirement at an acceptable cost.
28
+
29
+ ## Consequences
30
+
31
+ - The FFmpeg recipe for one blended layer is: place the fitted/transformed layer on a transparent stage-sized canvas; `split` the canvas into blend and mask copies; `split` the backdrop into blend, mask, and final-composite copies; extract the backdrop alpha; `blend` the placed layer with the backdrop; `maskedmerge` the blended RGB with the source alpha; `overlay` the result source-over the original backdrop. The final alpha is the source alpha.
32
+ - Blend modes apply to all visual sources: composition, nested timeline, video, and image. A nested timeline blends as a flattened group; a blend inside it sees only that timeline's own backdrop.
33
+ - An opaque composition or nested background blocks lower layers, exactly as its alpha implies. A transparent composition or nested timeline lets the backdrop through.
34
+ - The preview stage gets `isolation: isolate`, and preview layers stack by track index. Blend modes never reach the preview chrome.
35
+ - `check` validates the mode statically. `still` and `render` share the same composite graph. A blend mode that is unsupported or applied to an audio clip is a `TIMELINE_CLIP_INVALID` error.
36
+ - Tests cover every supported mode against the CSS formula with a 3/255 tolerance, source alpha with transparent and semi-transparent regions, stacked blended layers, opacity and offset boxes, preview stacking and `mix-blend-mode`, preview-versus-still parity for screen and multiply, end-to-end composition, video, image, and nested-timeline layers, transparent-nested isolation, and composition layers across independent segments.
37
+ - The performance model gains a per-blended-layer cost. The alpha-correct recipe is used unconditionally; an opaque-backdrop fast path is deferred until the representative benchmark shows the 60-minute target needs it.
38
+ - Documentation lives in the design baseline, README, glossary, the official skill's timeline and common-mistakes references, and the scaffold guide. The timeline document version does not change.
@@ -0,0 +1,28 @@
1
+ # Inherit composition format from the project
2
+
3
+ **Status:** Accepted. Amends [ADR-0007](./0007-make-html-documents-the-composition-unit.md) and [ADR-0010](./0010-make-timelines-the-render-unit.md).
4
+ **Date:** 2026-11-03
5
+
6
+ `<fy-composition>` will keep `duration-in-frames` required but make `width`, `height`, and `fps` optional. A composition that omits any of them inherits the value from the single project `format` in `fy.config.js`; a declared value always wins. Declaring `width` without `height` (or the reverse) remains an error. A composition that declares its full format keeps working unchanged, including in composition-only projects that have no `format`.
7
+
8
+ The unit of reuse moves from "a file with fixed output pixels" to "a source that composes at whatever the project or timeline provides". The same composition can therefore appear in a 1080p timeline and a vertical timeline without editing the file or duplicating attributes that the project already fixes. Timelines do not gain their own format: `ResolvedTimeline.format` is already `project.format`, so timeline and standalone inheritance are the same resolution.
9
+
10
+ ## Considered Options
11
+
12
+ - **Keep all four attributes required.** Rejected because the project `format` and the composition root duplicate the same three values, and the copies can disagree. The only disagreement the renderer tolerates today is size; a stale `fps` is a hard error that adds no information.
13
+ - **Add a separate `defaultFormat` key.** Rejected because a second format invites divergence and adds precedence questions. `format` is already "the project format"; it now also fills omitted composition values.
14
+ - **Hardcode a framework default (1920×1080 at 30 fps).** Rejected because it is an implicit value that no project declared, and it would silently produce a different composition than the timeline that contains it.
15
+ - **Infer width and height from the browser viewport.** Rejected because frame pixels would depend on preview window size, breaking determinism and preview/render parity.
16
+ - **Make `duration-in-frames` optional too.** Rejected because duration is intrinsic to the composition and cannot be inherited from a project format; timelines trim a source range but do not define the source length.
17
+ - **Bake the resolved format into the composition HTML at build time.** Rejected because composition documents stay authored artifacts and the server already injects framework markup at request time.
18
+
19
+ ## Consequences
20
+
21
+ - Declared metadata is distinct from resolved metadata. `CompositionDescriptor.metadata` exposes `{ format: DeclaredFormat; durationInFrames }`, and `CompositionSourceInfo` carries the resolved `metadata` plus `declaredFormat`. `CompositionMetadata`, `window.__fy.metadata`, and render results keep exposing resolved numbers only.
22
+ - The preview server injects the project format as a `data-fy="format"` JSON block marked `data-fy-host`. The browser host reads it to resolve the same values the Node-side validator used; the existing static-vs-runtime metadata equality check therefore still holds.
23
+ - The composition route and `project.json` resolve metadata in the server. `project.json` keeps exposing resolved metadata, or `null` when the document is invalid or the format cannot be resolved.
24
+ - Rendering a composition that omits format values in a project without `format` fails with `COMPOSITION_FORMAT_UNRESOLVED`, a context error rather than a document diagnostic. The message names both fixes: declare the attributes or add `format` to `fy.config.js`.
25
+ - A declared `fps` that differs from the project format remains `TIMELINE_FPS_MISMATCH`. The check compares the declared fps, not the resolved one, so inheritance never hides a real mismatch. Width and height keep the existing fit behavior with no mismatch error.
26
+ - The resolved format is part of the immutable frame environment, alongside props and loaded assets. Layouts that use viewport units become format-dependent by design; preview and production use the same resolved values.
27
+ - The scaffold keeps declaring an explicit format as the safe teaching default. `examples/blend/compositions/backdrop.html` demonstrates inheritance.
28
+ - Static validation gains an optional `format` input. Composition-only projects without `format` remain valid as long as every composition declares its own width, height, and fps.
@@ -0,0 +1,32 @@
1
+ # Author compositions as HTML fragments
2
+
3
+ **Status:** Accepted. Amends [ADR-0007](./0007-make-html-documents-the-composition-unit.md) and [ADR-0008](./0008-share-the-composition-document-grammar.md).
4
+ **Date:** 2026-11-03
5
+
6
+ Frameyard 0.8 makes a composition a partial HTML fragment instead of a full HTML document. The author writes body content with one top-level `<fy-composition>` root, plus optional top-level props, controller, import maps, and styles. The server wraps that fragment in a fixed document shell at the composition URL, so preview and production keep the same relative-URL, stylesheet, and host semantics. This removes author-owned document boilerplate and makes the framework, not the author, responsible for the charset, title, import map, sizing style, format block, and host module.
7
+
8
+ ## Considered Options
9
+
10
+ - **Keep full HTML documents.** Rejected because every composition repeats a shell the framework already owns, and the head becomes a second place where import maps, metadata, and host markup can drift.
11
+ - **Accept both documents and fragments.** Rejected because two authoring shapes double validation, diagnostics, documentation, and agent examples for a pre-1.0 breaking release.
12
+ - **Inject a fragment into the preview container without a document.** Rejected for the reasons in ADR-0007: relative asset URLs, document styles, and head content would need rewriting. Serving a generated document at the real composition URL avoids that problem.
13
+ - **Generate full documents at build time.** Rejected because Frameyard has no build step and the preview server already renders composition entries per request.
14
+ - **Let projects configure the document head.** Rejected because the author goal is to stop managing the head; a configuration surface would recreate the problem in `fy.config.js`.
15
+
16
+ ## Decision
17
+
18
+ - A composition entry is a partial HTML fragment. It contains exactly one top-level `<fy-composition>` element. Props blocks, the controller, and import maps must be top-level siblings; `<style>` and `<link rel="stylesheet">` may be top-level or inside the root.
19
+ - The fragment must not contain `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`, and must not use the reserved `data-fy-host` attribute or `data-fy="format"` block.
20
+ - The server generates a fixed document: `<!doctype html>`, `<html lang="en">`, a head with charset and title, the format block, the framework import map, author import maps in source order, the host style, hoisted author styles and stylesheet links in source order, and the host module; then the remaining fragment nodes in the body.
21
+ - Top-level author import maps are allowed and are hoisted after the framework map. Their JSON, attributes, and specifiers are validated; `frameyard` and `frameyard/` are reserved. Multiple import maps merge in document order, and the framework entries resolve first.
22
+ - Static validation parses the fragment in a body context and detects legacy document shells with a separate document parse. The shared composition reader takes a `document` or `fragment` root scope; the browser host keeps document scope, while Node validation uses fragment scope.
23
+ - The authoring change is a clean break in 0.8. A legacy full document fails with `COMPOSITION_DOCUMENT_SHELL` and a suggestion to the migration guide. There is no migration command.
24
+
25
+ ## Consequences
26
+
27
+ - `src/server/composition-html.ts` replaces host injection into an author document with document-shell generation and head hoisting.
28
+ - `src/html/validate.ts` gains fragment structure checks, shell detection, import-map validation, and reserved-attribute checks. `COMPOSITION_DOCUMENT_SHELL` and `IMPORT_MAP_INVALID` join the stable diagnostic codes.
29
+ - The composition reader's root scope is explicit. ADR-0008's shared grammar still applies; only the container differs between Node validation and the browser host.
30
+ - `.html` stays the composition extension and `project.json` keeps `kind: "html"`; the change is documented in `docs/migration-0.8.md` rather than encoded in the JSON contract.
31
+ - `examples/`, the `init` scaffold, the agent skill, and the test fixtures migrate to fragments. A baseline still comparison guards the wrapper against visual regressions.
32
+ - The browser host still reads a full document, so `readCompositionDescriptor` keeps its document-mode rules and the served document remains inspectable.
@@ -0,0 +1,99 @@
1
+ # Render timeless compositions as still images
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-03
5
+
6
+ A composition that declares neither `duration-in-frames` nor `fps` is static. It
7
+ has no frame clock, no duration, and no audio or transcripts. Frameyard captures
8
+ it once in its initial state and publishes a PNG; inside a timeline the same
9
+ composition becomes an image layer that the clip's explicit duration controls.
10
+ A static composition is visual-only: `<img>`, CSS, SVG, and canvas are allowed;
11
+ `<video>`, `<audio>`, and `<fy-transcript>` are validation errors. Its controller
12
+ may `mount()` and validate props, but `update()` is not called and frame-state
13
+ variables are not set. CSS animations and transitions are paused at time 0 with
14
+ a warning. `data-fy-anim-*` is an error.
15
+
16
+ The metadata matrix is explicit. `duration-in-frames` plus `fps` is animated;
17
+ `duration-in-frames` without `fps` is animated with the project frame rate;
18
+ neither attribute is static; `fps` without `duration-in-frames` is
19
+ `COMPOSITION_INVALID`. `width` and `height` keep their current rules: declared
20
+ together or inherited from the project `format`. A static composition with no
21
+ dimensions in a project without `format` still fails with
22
+ `COMPOSITION_FORMAT_UNRESOLVED`.
23
+
24
+ `render <static-id>` writes `out/<id>.png` and accepts `--props`, `--overwrite`,
25
+ and `--json`. It rejects `--container`, `--codec`, `--encoder`, and
26
+ `--concurrency`. The render result is a discriminated union: `kind: "image"`
27
+ with `path`, `width`, `height`, `bytes`, `diagnostics`, and `elapsedMs`, or
28
+ `kind: "video"` with the existing video fields. `check` returns a matching
29
+ `kind: "static"` result with `framesChecked: 1` and no audio or transcript
30
+ fields. `still` treats a static composition as exactly frame 0; any other frame
31
+ is `FRAME_OUT_OF_RANGE`.
32
+
33
+ A timeline composition clip whose source is static must declare `duration` and
34
+ use `sourceIn: 0`, exactly like an image clip. The resolver gives it no intrinsic
35
+ duration and skips the frame-rate comparison. The renderer captures one
36
+ transparent PNG per composition/props pair and reuses it across segments and
37
+ workers through the existing image input path. The timeline still renders to
38
+ video; a fully static timeline is not converted to an image. Nested timelines
39
+ remain video layers in this version, although static compositions inside them
40
+ are still captured once. Timeline check inspects a static layer once and does
41
+ not seek it per frame.
42
+
43
+ ## Context
44
+
45
+ Composition rendering captured every frame of `duration-in-frames`, even when
46
+ the document never changed. Timeline rendering did the same for every active
47
+ composition layer in every segment. A long title card or a static background
48
+ therefore paid Chromium seeks, PNG captures, FFV1 encoding, and compositing for
49
+ identical frames. The existing `still` command already proved the single-capture
50
+ path, but it required the author to know the composition was static and to use a
51
+ different command.
52
+
53
+ The composition format had one required time fact, `duration-in-frames`, so
54
+ there was no way to express "this document has no time base". Treating missing
55
+ duration as an error protected authors from typos. This ADR makes the absence
56
+ meaningful and keeps the typo protection through the `fps`-without-duration
57
+ error, validation of time drivers, and a clear render output.
58
+
59
+ ## Considered options
60
+
61
+ - **An explicit `data-fy-static` marker with a frame sample.** Rejected: it keeps
62
+ `duration-in-frames` mandatory, so a still declares meaningless time metadata,
63
+ and it adds a second way to say what the missing attributes already say.
64
+ - **Runtime detection by comparing sampled frames.** Rejected: without a clock
65
+ there are no frames to sample, and with a clock the check is probabilistic
66
+ (an animation that only changes between samples escapes). The guarantee is
67
+ structural instead: a static composition has no time base and no time drivers.
68
+ - **Keep rendering static compositions as video.** Rejected: it preserves the
69
+ exact cost the change is meant to remove and forces a video container onto an
70
+ artifact that is one image.
71
+ - **Add `--video` to force the old path.** Rejected for now: a timeline already
72
+ produces video from an image clip, and the flag would make the output type
73
+ depend on a render flag rather than the composition contract.
74
+ - **Allow audio, transcripts, or embedded video in a static composition.**
75
+ Rejected: a PNG cannot carry them, and allowing them would make the same
76
+ source behave differently in a standalone render and in a timeline.
77
+ - **Export a fully static timeline as a PNG.** Rejected: a timeline is the
78
+ delivery unit and carries duration, audio, and sequencing even when its visual
79
+ content is constant.
80
+
81
+ ## Consequences
82
+
83
+ - `duration-in-frames` is no longer required on every composition. A composition
84
+ that declares neither time attribute is static; a composition that declares
85
+ only `fps` is invalid.
86
+ - Static compositions cannot use `data-fy-anim-*`, media, or transcripts. The
87
+ host reports `STATIC_ANIMATION_UNSUPPORTED`, `STATIC_MEDIA_UNSUPPORTED`, and
88
+ `STATIC_UPDATE_IGNORED`/`STATIC_ANIMATION_PAUSED` diagnostics.
89
+ - `render`, `check`, and `still` gain a static path. The video result keeps its
90
+ fields and adds `kind: "video"`; the image result and static check result are
91
+ new public shapes.
92
+ - Timeline composition clips are image-like when their source is static:
93
+ explicit duration, `sourceIn: 0`, no intrinsic source duration, and one
94
+ capture per props variant shared across segments.
95
+ - Preview loads a static composition once, disables its transport, and keeps a
96
+ static timeline layer loaded without seeking it.
97
+ - The design baseline, glossary, README, official skill, scaffold guide,
98
+ changelog, examples, and tests describe the new contract. Version 0.8.0 marks
99
+ the contract change.
@@ -0,0 +1,35 @@
1
+ # Animate timeline clips with keyframes
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-04
5
+
6
+ Visual timeline clips may animate `x`, `y`, `scale`, `rotation`, and `opacity` through a top-level `keyframes` object. Each channel is an ordered array of `{ frame, value, easing? }` entries in clip-local integer frames. Keys hold before the first and after the last key, never extend the clip, and use a CSS timing function on the interval after each key. A channel cannot also be an explicit scalar in `transform`, and `keyframes.scale` cannot be combined with a `transform.width`/`height` box. The timeline document version remains 1.
7
+
8
+ Preview evaluates the CSS timing function exactly on every seek. Production generates per-frame FFmpeg expressions from the same browser-safe evaluator; position, scale, and rotation use filter expressions, and animated opacity uses an alpha mask applied before blend-mode compositing. Easing curves in the filter graph approximate the CSS curve; parity remains at clip capture, not at the final encoded pixel. `check` evaluates every keyframe channel at every timeline frame. The read-only preview shows merged keyframe markers and seeks from them.
9
+
10
+ ## Context
11
+
12
+ Timelines had static transforms and static blend modes; keyframes were explicitly deferred by ADR 0010. The render unit is a segmented FFmpeg composite, and ADR 0012's performance model depends on not capturing every layer frame through Chromium. Animation therefore has to be expressed in the FFmpeg graph, not by adding a second per-frame compositor. At the same time, the preview is the authoring reference, so both paths need one definition of frame sampling and easing.
13
+
14
+ ## Considered Options
15
+
16
+ - **Whole-transform keyframe snapshots.** More ergonomic for coordinated moves, but every key needs a rule for omitted channels, and the rule is ambiguous. Per-channel tracks make "keyed or static" explicit and map directly to preview styles and filter parameters.
17
+ - **A separate animation document or named presets.** Adds a reference namespace, resolution, and cross-clip coupling. Inline tracks keep a clip self-contained; presets can be added later without breaking inline tracks.
18
+ - **Source-relative or absolute timeline frames.** Source-relative time preserves animation phase across slips but makes trims confusing. Absolute timeline frames decouple animation from clip placement. Clip-local frames keep the animation with the clip and leave `sourceIn` as a pure media-content selector.
19
+ - **Arbitrary expression values.** A formula language would add determinism, validation, and preview-parity risk. Values stay JSON numbers; complex motion belongs in a composition controller.
20
+ - **Composite keyframed clips in Chromium or software per frame.** Better preview parity, but a second production compositor and a large throughput cost. FFmpeg expressions keep one path and the existing segmentation model.
21
+ - **Allow animated blend modes or audio gain.** Blend modes are discrete and not interpolable; audio fades are a separate contract. Both remain deferred.
22
+ - **Constrain easing to `linear` and `hold` or to named curves only.** The composition runtime already speaks CSS easing, and preview can evaluate it exactly. The filter graph approximates any supported curve with an adaptive Hermite expression within a fixed error budget.
23
+
24
+ ## Consequences
25
+
26
+ - Clip keyframes are part of the timeline schema and resolved clips. Structural errors use `TIMELINE_CLIP_INVALID`; keys outside the resolved duration use `TIMELINE_CLIP_OUT_OF_RANGE`, with clip, track, channel, key index, frame, and JSON path details.
27
+ - `opacity` is clamped to `0..1` and `scale` to `>= 0` after easing. Position and rotation may overshoot. A zero-scale frame is invisible; the scale filter uses a zero-safe expression and the opacity mask gates it.
28
+ - Animated opacity and animated scale add a small alpha-mask pass at the fitted box size. The mask is generated from a 1×1 per-frame source and multiplied into the layer alpha, so blend modes see the animated source alpha. No stage-sized mask is added for normal layers.
29
+ - FFmpeg's rotate filter clips to a fixed output size. Static rotation uses the exact rotated bounding box; keyed rotation uses the diagonal of the fitted box, which is segment-stable and large enough for every angle. The filter fills the remaining area with transparent pixels, so rotation no longer cuts off corners.
30
+ - Segments are never split at keyframe boundaries. Values are evaluated against absolute timeline frames, so changing `maxSegmentFrames` or worker count does not change a frame's keyframe values.
31
+ - Production expressions are exact at keyframe frames and within 1/255 opacity, 0.5 px position, 0.1% scale, and 0.1° rotation of the shared evaluator between keys. Encoded output keeps the existing scaling, alpha, and color-conversion differences.
32
+ - `check` adds a per-frame keyframe sweep without capturing or encoding. `still` and `render` share the same composite path and `frameOffset`.
33
+ - The preview draws one marker per distinct keyframe frame across channels, with a channel/value tooltip and click-to-seek. It does not add an editing UI or keyframe manipulation.
34
+ - Timeline documents remain version 1; keyframes are additive. Documentation covers the contract, validation, preview markers, and the production approximation. The reference example is `examples/keyframes`.
35
+ - Performance is measured with a keyframed fixture; no hard CI ratio is enforced.
@@ -0,0 +1,36 @@
1
+ # Make same-track overlap the transition window
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-04
5
+ **Amended by:** [ADR 0020](./0020-crossfade-audio-clips-on-audio-tracks.md) adds the `crossfade` kind for audio tracks; the audio-track prohibition in this record is superseded.
6
+
7
+ A timeline may declare a `transitions` array. Each entry names a `from` clip, a `to` clip, and a `kind`. A dissolve requires the two clips to overlap on one video track; the overlap `[to.start, from.end)` is the transition window. Only the incoming clip carries animated alpha, `opacity × eased progress`, and both clips' contributed audio crossfades with equal power. The annotation is explicit: an overlap without a declared transition is still `TIMELINE_CLIP_OVERLAP`. The timeline document version remains 1.
8
+
9
+ ## Context
10
+
11
+ ADR 0010 rejected same-track overlap because transitions were deferred and overlap would make visual and audio precedence ambiguous. Crossfades used separate tracks plus opposing opacity keyframes, which is valid but doubles the track count and leaves the relationship implicit. The roadmap now needs transitions as a first-class authoring feature.
12
+
13
+ The renderer is segmented: every segment has a fixed layer set, and the segment plan builds layers from visual clips that fully cover the range. Preview activates clips pointwise. Both paths already evaluate presentation against absolute timeline frames, which is what keeps segments independent. A transition must not introduce a second timing model, retiming, or media handles.
14
+
15
+ ## Considered Options
16
+
17
+ - **Explicit overlap.** The clips visibly overlap by the transition length and the overlap is the window. Every clip's visible range stays exactly its declared range, so the planner, preview active set, `still`, and `check` all agree with the JSON. Authors move the incoming clip earlier by the transition duration. No handles are needed: the outgoing clip's declared duration already covers its side, and the incoming clip starts at its own `start`.
18
+ - **Cut-centered implicit extension.** Clips stay butt-joined and the renderer extends one or both past its declared bounds. This needs media handles that video and nested timelines may not have, and creates frames where a clip is visible outside `[start, end)`. `still`, `check`, and the preview would disagree with the document.
19
+ - **Transition inserts time and retimes downstream clips.** Timeline duration would depend on transition declarations, and every later clip would shift. Declarative JSON should not hide retiming.
20
+ - **Overlap alone implies a dissolve.** An accidental two-frame overlap would silently become a fade instead of failing. Explicit declaration keeps overlap an error unless intended.
21
+ - **FFmpeg `xfade` or a catalog of effects.** `xfade` is relative to a filter offset, not the absolute timeline frame, and most modes have no CSS equivalent. Preview parity would silently break.
22
+ - **Both clips fade (`1 - t` and `t`).** Background leaks through partially transparent clips and the formula depends on both clips' opacity. Overlaying the incoming clip at alpha `t` over a normally rendered outgoing clip is exact when the incoming clip is opaque and is the same alpha product the keyed-opacity path already uses.
23
+ - **Linear audio gain.** Equal-power (`sqrt`) keeps perceived loudness constant through the overlap; linear gain dips for uncorrelated material. `sqrt` is exact in both the browser and FFmpeg expressions.
24
+ - **Audio-track transitions in the same change.** Audio tracks have a separate overlap and gain contract. Deferred.
25
+
26
+ ## Consequences
27
+
28
+ - `transitions` entries are `{ from, to, kind, easing? }`. `kind` is required and must be `dissolve` (ADR 0020 extends the accepted kinds with `crossfade` for audio tracks); `easing` defaults to `linear` and uses the keyframe CSS timing-function grammar. There is no `duration`: the overlap is the single source of truth. A clip may be the `to` of one transition and the `from` of another, but not twice on the same side.
29
+ - Valid geometry is `from.start < to.start < from.end < to.end`. Anything else is `TIMELINE_TRANSITION_GEOMETRY` (ordering or containment) or `TIMELINE_TRANSITION_NOT_OVERLAPPING` (touching or later). Reference errors are `TIMELINE_TRANSITION_UNKNOWN_CLIP`, `TIMELINE_TRANSITION_WRONG_TRACK`, and `TIMELINE_TRANSITION_AUDIO_TRACK`; duplicate participation is `TIMELINE_TRANSITION_DUPLICATE`; an unsupported kind is `TIMELINE_TRANSITION_KIND`. Structural entry errors use `TIMELINE_TRANSITION_INVALID`.
30
+ - The pairwise overlap rule now allows only declared pairs on the same track. A third clip that overlaps either side is still an error, so at most two clips are active on a video track at any frame.
31
+ - During the window the incoming clip composites above the outgoing clip on the same track. Its alpha is `clipOpacity × t`, where `t = clamp(ease((frame - to.start) / (from.end - to.start)))`. Clip opacity may be static or keyframed and multiplies rather than being overridden. Blend modes compose with transition alpha because it is the same multiplier the keyed-opacity mask already applies.
32
+ - Production adds the transition factor to the existing `geq` alpha mask (`N + frameOffset` evaluates absolute frames) and does not split segments at transition edges: the edges are already clip boundaries. Segments chopped by `maxSegmentFrames` produce identical values.
33
+ - Contributed audio uses gain `sqrt(1 - t)` for the outgoing clip and `sqrt(t)` for the incoming clip, evaluated after easing. Envelopes are absolute frames and multiply through nested timelines, so a transition on a group clip fades its group audio. `audio.muted` still zeroes its side. Audio-track clips are untouched.
34
+ - Preview multiplies the incoming layer's CSS opacity by the same eased progress, stacks it above the outgoing clip via a per-track z-index pair, ramps `<audio>` volume from wall-clock playback, and draws a read-only hatched band on the strip. Production and preview agree at the composite stage; encoded pixels keep the existing scaling, alpha, and color-conversion differences.
35
+ - `check` sweeps every transition frame and exposes `transitions` in the timeline check result. `still` composites a frame inside the window through the same path as `render`.
36
+ - Timeline documents remain version 1; `transitions` is additive. Documentation and the `examples/transitions` reference project cover the contract. The documented 0.8 non-goal is removed.
@@ -0,0 +1,27 @@
1
+ # Crossfade audio clips on audio tracks
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-04
5
+
6
+ A timeline may declare a `crossfade` transition between two clips on one audio track. It uses the same overlap window and geometry as a dissolve, but applies only the equal-power audio gain ramp: the outgoing clip at `sqrt(1 - t)` and the incoming clip at `sqrt(t)`, after easing. A `dissolve` remains video-track only; `crossfade` is audio-track only. The timeline document version remains 1.
7
+
8
+ ## Context
9
+
10
+ ADR 0019 made same-track overlap the transition window but deferred audio-track transitions. Audio clips could already live directly on audio tracks, and the audio contributed by visual clips already crossfaded inside a dissolve, but two audio clips on one audio track could not overlap. A music or voice edit therefore needed two tracks and hand-tuned gain, and there was no gain envelope contract for arbitrary audio clips. The transition window, geometry, and equal-power curve already exist; only the track-kind gate blocks audio.
11
+
12
+ ## Considered Options
13
+
14
+ - **Reuse `dissolve` on audio tracks.** Shorter, but "dissolve" names a visual operation. Allowing it on a track with no picture makes the kind/track contract ambiguous and forecloses future visual-only kinds.
15
+ - **A dedicated `crossfade` kind.** Keeps the matrix explicit: `dissolve` is video, `crossfade` is audio. The schema already carries `kind`, so this is additive and validation is one comparison.
16
+ - **Audio fades inside the composition.** Moves the edit into a reusable asset, so the fade cannot follow clip placement and the same composition cannot be used with a different overlap.
17
+ - **Two audio tracks with opposing gain envelopes.** Needs a new per-clip audio-envelope contract and still leaves the relationship implicit. The transition already supplies the window and curve.
18
+ - **Linear gain.** Dips in perceived loudness for uncorrelated material. Equal power is already the contributed-audio behavior, so using a different curve for audio-track clips would be surprising.
19
+
20
+ ## Consequences
21
+
22
+ - `TRANSITION_KINDS` is `dissolve` and `crossfade`. Unknown or missing kinds use `TIMELINE_TRANSITION_KIND`; a dissolve on an audio track uses `TIMELINE_TRANSITION_AUDIO_TRACK`, and a crossfade on a video track uses `TIMELINE_TRANSITION_VIDEO_TRACK`. Both suggest the matching kind.
23
+ - The overlap relaxation covers audio tracks with the same pairwise rule: only a declared pair may overlap, a third clip is still an error, and geometry is `from.start < to.start < from.end < to.end`.
24
+ - `crossfade` changes no pixels and never reaches the segment planner or the composite filter graph. `collectTimelineAudio` already attaches absolute-frame gain envelopes to every clip with a transition, including audio-track clips, so the existing mix path applies the ramp. Clip and track gain still multiply the envelope, and `audio.muted` still zeroes a side.
25
+ - Preview drew the transition band from the resolved timeline already; audio tracks now produce it too. Playback ramps `<audio>` volume from the same envelope.
26
+ - `examples/transitions` gains an `audio-crossfade` timeline: the alpha and beta cards are reused with `audio.muted: true`, two audio clips sit on `a1`, and a `crossfade` shares the 15-frame overlap with the visual dissolve. This shows both the direct-audio placement and the kind/track split.
27
+ - Timeline documents remain version 1; `crossfade` is additive. The audio-track prohibition in ADR 0019 is superseded.
@@ -0,0 +1,32 @@
1
+ # Project timeline scenes once for production and preview
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-04
5
+
6
+ A timeline frame is produced from a **timeline scene**: the ordered active clips and their presentation at one timeline frame or over one render range, including transition roles and composite order. `src/timeline/scene.ts` is browser-safe and owns active-layer selection, transition pairing, stacking, capture-mode classification, and presentation defaults. `planTimelineRender` keeps segment splitting and projects each segment through the scene module; production capture, `check`, `still`, and preview consume the same projection. Capture sessions, FFmpeg, audio mapping, and both compositors stay outside the seam.
7
+
8
+ ## Context
9
+
10
+ The planner, the segment renderer, the still renderer, the audio mapper, and the preview host each derived the active clip set, the frame-to-source mapping, transition roles, stacking, and presentation defaults. Recent timeline features made the fan-out visible: dissolve, crossfade, and keyframes each added another copy. The preview bypassed `planTimelineRender` entirely and re-derived the scene pointwise. The copies already disagreed on static-composition caching and on what `TimelineCompositeLayer.kind` meant.
11
+
12
+ ADR 0018 already requires one keyframe evaluator shared by production and preview, and ADR 0019 requires both paths to agree at the composite stage. This decision applies the same rule to layer selection and presentation, without merging the compositors or the capture lifecycle.
13
+
14
+ ## Considered Options
15
+
16
+ - **A browser-safe scene projection (chosen).** One range-based module returns ordered scene layers with the resolved clip, normalized presentation, source start and size, capture mode, and transition window/role. A frame is a one-frame range. Production, `check`, `still`, and preview consume it; capture and audio mapping stay separate.
17
+ - **Projection plus capture/session ownership.** Fewer layers between resolve and pixels, but the module would absorb Chromium sessions, static-image caching, nested capture, and FFmpeg artifacts, making preview depend on production machinery.
18
+ - **Projection plus audio resolution.** One scene would drive both pixels and audio, but audio mapping would pull nested gain, mute, and media probing into the render-facing module before those concerns are ready to move.
19
+ - **Keep the per-consumer models and share only the transition map.** Smaller change, but frame mapping, defaults, capture mode, and stacking would still drift independently.
20
+ - **Precompute the preview scene on the server.** The preview needs arbitrary frames, so it would either ship one scene per frame or keep re-deriving the scene in the browser.
21
+
22
+ ## Consequences
23
+
24
+ - `src/timeline/scene.ts` defines `TimelineScene`, `TimelineSceneLayer`, the capture modes `frames | still | video | image | nested`, and one transition pairing used by the scene and by timeline audio envelopes. Layers carry the resolved clip and materialized `transform`, `keyframes`, `blend`, `sourceStart`, source size, and transition role; array order is composite order.
25
+ - `planTimelineRender` keeps segment splitting, removes `TimelineRenderLayer`, and projects each segment through the scene. `TimelineRenderPlan` shrinks to the segment list.
26
+ - `TimelineCompositeLayer` keeps its narrow FFmpeg shape but replaces the overloaded `kind` with an `input` mode (`sequence | image | video`). One mapper builds it from a scene layer plus the captured artifact.
27
+ - The preview payload becomes a serialized resolved timeline: clips keep the resolved shape, sources gain a `url`, and absolute paths are stripped. The browser host calls the scene projection on every seek. `TimelinePreviewClip` stops being a parallel model; the precomputed transitions array disappears.
28
+ - Nested timelines remain groups. The scene projects one timeline level; capture recurses into a nested source's own scene.
29
+ - Static-image caching keeps its current behavior during this change. Cache ownership moves with the capture pipeline, not with the scene projection.
30
+ - Projection errors are `FrameyardError`s carrying the clip or range context; the module does not return diagnostics because resolution already owns them.
31
+ - Tests target `test/timeline-scene.test.ts` for active layers, transition roles, stacking, frame-to-source mapping, capture modes, source sizes, and defaults. Plan tests keep segment splitting; render and preview tests keep pixels.
32
+ - No pixel behavior is intended to change. Preview and production still composite in different engines; parity remains at clip capture.
@@ -0,0 +1,35 @@
1
+ # Gate heavy operations with a workspace resource policy
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-05
5
+
6
+ `fy.policy.json` declares a workspace resource tier (`light`, `authoring`, or `full`), an optional `shared-cpu` profile, and stricter still/overwrite limits. The CLI reads the nearest file from the working directory upward, independent of `fy.config.js`; `FRAMEYARD_POLICY` replaces the file for a human override. `tts`, `transcribe`, and `render` are blocked outside `full`; `lint` and `--dry-run` provide a cheap authoring and handoff path. The policy is a guardrail for cooperative agents, not a security boundary.
7
+
8
+ ## Context
9
+
10
+ Local models, browsers, and FFmpeg often share one CPU and GPU. An agent that runs every validation pass through Chromium, or starts a render while the model is loaded, makes the machine unusable for the human. The existing skill says to confirm inference and avoid heavy work, but an instruction is not enforceable: a local model can ignore it or invoke the CLI directly.
11
+
12
+ `check` evaluates every frame through Chromium and is the correctness gate, but it is too expensive for a tight edit loop. `still` is bounded but still launches Chromium. `tts` and `transcribe` load local models, and `render` runs Chromium plus FFmpeg with up to sixteen workers. These operations need different treatment.
13
+
14
+ The Frameyard repository itself has no `fy.config.js`, so a policy tied to the project root would not protect commands that target `examples/*` with `--project`. The policy therefore belongs to the workspace, not the render target.
15
+
16
+ ## Considered Options
17
+
18
+ - **A workspace policy file with tiers and a shared profile (chosen).** One small JSON document controls operation gating, still limits, overwrite permission, worker caps, and process priority. It applies to direct CLI invocations, is visible in review, and does not require a host-specific integration.
19
+ - **Skill instructions only.** No new contract, but unenforceable against a model that ignores the skill or runs the binary directly.
20
+ - **A global user configuration.** Stronger against accidental edits, but it breaks the local-first project portability the agent workflows need.
21
+ - **A hosted control plane or MCP server.** Out of scope by design; it adds a second public surface for a problem the CLI can solve.
22
+ - **A daemon or job queue in v1.** It still consumes the same resources and adds state before the simple handoff is proven.
23
+
24
+ ## Consequences
25
+
26
+ - The policy is discovered from `cwd` upward. `--project` does not change discovery. `FRAMEYARD_POLICY=light|authoring|full` replaces the file, including its profile and tightened limits.
27
+ - `light` allows `lint`, `context`, `doctor`, `init`, `dev`, and one still frame; it blocks `check`, `tts`, `transcribe`, and `render`. `authoring` allows four still frames and `check` at low priority; it blocks the three heavy operations. `full` allows everything up to the existing 32-frame still cap.
28
+ - A policy file can only tighten the tier: `maxStillFrames` cannot exceed the tier default, and `allowOverwrite` cannot become true outside `full`. Unknown fields, unsupported versions, and invalid values fail with `POLICY_INVALID`.
29
+ - `frameyard lint <id>` runs static validation, timeline resolution, scene and keyframe projection for every timeline frame, controller determinism scanning, and media metadata probing without Chromium. It does not run `validateProps` or evaluate composition runtime; `check` remains the full gate.
30
+ - `tts`, `transcribe`, and `render` accept `--dry-run`. They validate inputs and return `{ dryRun: true, plan, commandLine, prerequisites, blockers }` without creating output directories, writing the manifest, or launching Chromium, FFmpeg, or a model. The handoff command includes `FRAMEYARD_POLICY=full` when the active policy is not `full`.
31
+ - `--profile shared-cpu` caps timeline workers at two and composition concurrency at one, lowers process priority, and leaves the encoder at `auto`. Explicit values above the cap fail with `RESOURCE_PROFILE_CONFLICT`; they are never clamped silently.
32
+ - `authoring` and `light` deny `--overwrite` and `--force`. `still` frames above the limit fail with `RESOURCE_POLICY_LIMIT`.
33
+ - `os.setPriority` is best effort. Linux and macOS use the low nice value; Windows uses below-normal. Unsupported platforms continue without failing.
34
+ - The policy file is editable by the agent, so it is a guardrail. Host permissions and the CLI environment are the second layer; a hostile process with shell access can still bypass it.
35
+ - Tests target `test/policy.test.ts` for parsing, precedence, discovery, tier gates, and profile caps; `test/cli-render.test.ts` for blocked commands, dry-run plans, still limits, and process non-spawn; `test/docs.test.ts`, `test/skill.test.ts`, and `test/scaffold.test.ts` for the contract text.
@@ -0,0 +1,98 @@
1
+ # Pin fonts as project assets
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-05
5
+
6
+ `fy.fonts.json` is a versioned project manifest of vendored font files.
7
+ Font files and their licenses live under `assets/fonts/<font-id>/`; the preview
8
+ server generates the `@font-face` sheet at `/__frameyard/fonts.css` and injects
9
+ it into every composition document before author styles. Fontsource is the
10
+ primary acquisition catalog; local files and pinned HTTPS URLs cover fonts that
11
+ are not in Fontsource. Fontshare, Google Fonts, and Bunny Fonts are not
12
+ providers in the first version.
13
+
14
+ ## Context
15
+
16
+ Compositions already load fonts through ordinary local `@font-face` rules or a
17
+ local `<link rel="stylesheet">`. The preview server serves WOFF2/WOFF/TTF/OTF
18
+ with the correct media type, and the host awaits `document.fonts.ready` after
19
+ `mount()`. What is missing is a project-level model: there is no inventory, no
20
+ way for an agent to search or install a font, no license record, no hash, and no
21
+ verification that a captured frame used the intended font. Remote Google Fonts
22
+ links work during the loading phase but make a render depend on the network and
23
+ on a mutable third-party response.
24
+
25
+ Fonts are not an FFmpeg concern: text is rasterized in Chromium during
26
+ composition capture, and FFmpeg only composites the resulting images. The font
27
+ files therefore need to be deterministic inputs to the browser session, not
28
+ dependencies of the encoder.
29
+
30
+ ## Considered Options
31
+
32
+ - **A project manifest with vendored files (chosen).** The CLI can update the
33
+ manifest atomically, record file hashes and licenses, and the server can
34
+ generate one stylesheet without scanning directories. The project remains
35
+ portable and renderable offline.
36
+ - **Declare fonts in `fy.config.js`.** Fewer files, but the CLI would need to
37
+ edit JavaScript to add a font, and hashes and licenses still need a sidecar.
38
+ - **Depend on `@fontsource/*` packages in `node_modules`.** Familiar for web
39
+ projects, but a Frameyard project is not necessarily an npm package, packages
40
+ include every subset and weight, and resolution depends on a package manager
41
+ and bundler.
42
+ - **Keep CSS-only fonts with no inventory.** Web-native, but gives the agent no
43
+ discovery, validation, license, or reproducibility surface.
44
+
45
+ ## Consequences
46
+
47
+ - `fy.fonts.json` has `version: 1` and a `fonts` array. Each family
48
+ records its CSS family, provider, optional provider version, license, variable
49
+ axes, and a non-empty `files` array. Each file records a project-relative path
50
+ below `assets/fonts/<font-id>/`, subset, style, weight or weight range, format,
51
+ optional unicode range, SHA-256, byte size, and optional upstream source.
52
+ Unknown fields, duplicate ids, duplicate CSS family names, duplicate faces,
53
+ paths outside the font directory, and invalid weights fail with
54
+ `FONT_MANIFEST_INVALID`.
55
+ - Font files and license files are committed with the project. Replacing a font
56
+ face or changing its provider requires `--force`; merging another subset,
57
+ style, or weight into an existing family does not.
58
+ - `frameyard font add --file` vendors a local `.woff2`, `.woff`, `.ttf`, or
59
+ `.otf` file with an explicit family, weight, style, optional subset, optional
60
+ unicode range, and optional license. A missing license produces a warning.
61
+ `font list`, `font verify`, and `font remove` manage the manifest;
62
+ `font remove` deletes the font directory unless `--keep-files` is supplied.
63
+ - `font search` reads the Fontsource catalog and filters by query, category,
64
+ subset, style, variable, and limit. `font add <id>` pins `npmVersion` from the
65
+ Fontsource API, downloads only the requested subset/style/weight, downloads
66
+ the package `LICENSE`, and records the exact CDN URL and SHA-256. The default
67
+ is the variable `wght` face when the family has one; static faces require
68
+ `--static` and a weight or weight list.
69
+ - HTTPS URL imports may include a pinned SHA-256. A URL import without a license
70
+ is recorded as `unknown`, requires the `full` policy, and produces warnings in
71
+ `list`, `verify`, and `doctor`. There is no shared font cache. Fontshare is
72
+ not a provider: its Free Font License prohibits redistribution, subsetting,
73
+ and format conversion for most of its catalog. Only OFL-licensed Fontshare
74
+ files are safe to vendor, and they use the generic URL import.
75
+ - The server generates the font sheet on every request from the manifest and
76
+ injects it after the host style and before author styles. Adding a font is
77
+ visible in `dev` on the next request without restarting the server. The
78
+ generated sheet uses `font-display: block` and one `@font-face` per manifest
79
+ file.
80
+ - The host awaits `document.fonts.ready`, then fails the load with
81
+ `FONT_LOAD_FAILED` if the browser reports a failed `FontFace`. Static
82
+ validation warns with `FONT_REMOTE` once per remote URL found in inline CSS,
83
+ local linked CSS, or a remote stylesheet link. Remote fonts remain compatible
84
+ but are not deterministic.
85
+ - `check`, `still`, and `render` verify manifest file sizes and SHA-256 values
86
+ before launching Chromium. `lint` verifies existence and size. `font verify`
87
+ also checks licenses and orphaned font directories, and `doctor` reports
88
+ manifest integrity. A changed file fails with `FONT_HASH_MISMATCH`.
89
+ - The policy operation is `font`. Search, list, and verify are allowed in every
90
+ tier; `font add` requires `authoring` or `full`; `font remove` and
91
+ `font add --force` require `full`.
92
+ - `context --json` reports installed families, provider, version, variable axes,
93
+ subsets, styles, license, file count, bytes, and status. The agent skill gains
94
+ a font reference; the scaffold continues to use `system-ui` and teaches
95
+ `font add` instead of bundling a default font.
96
+ - Typography roles (`--fy-font-display`, `--fy-font-body`), font pairs, a
97
+ preview font picker, and non-`wght` variable axes are out of scope for the
98
+ first version.
@@ -1,5 +1,7 @@
1
1
  # Migrating to Frameyard 0.4
2
2
 
3
+ > Historical guide. Frameyard 0.8 changed compositions from full HTML documents to partial fragments; see [migration-0.8](./migration-0.8.md) for the current authoring shape.
4
+
3
5
  0.4 replaces JavaScript composition modules with declarative HTML documents. The CLI commands, props files, audio and transcript asset formats, rendering pipeline, and determinism guarantees are unchanged. This guide maps the old authoring surface to the new one.
4
6
 
5
7
  ## 1. Change the config entry
@@ -153,7 +155,7 @@ After:
153
155
  <fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
154
156
  ```
155
157
 
156
- Audio, video, and `<fy-transcript>` `src` values resolve from the project root. The controller `src` and ordinary HTML assets (`img`, stylesheets) resolve relative to the composition document.
158
+ Audio, video, and `<fy-transcript>` `src` values resolve from the project root. The controller `src` and ordinary HTML assets (`img`, stylesheets) resolve relative to the composition URL.
157
159
 
158
160
  ## 7. Rename media attributes
159
161
 
@@ -257,11 +259,11 @@ export default {
257
259
 
258
260
  ## 11. Update preview and render expectations
259
261
 
260
- No command changes. Preview loads the composition in a same-origin iframe; `check`, `still`, and `render` navigate directly to the same document. The composition host is injected by the server, so keep composition entries listed in `video.config.js` and run through `frameyard dev` or `frameyard render`, not `file://`.
262
+ No command changes. Preview loads the composition in a same-origin iframe; `check`, `still`, and `render` navigate directly to the same document. The composition host is injected by the server, so keep composition entries listed in `fy.config.js` and run through `frameyard dev` or `frameyard render`, not `file://`.
261
263
 
262
264
  ## Migration checklist
263
265
 
264
- - [ ] Change `video.config.js` entries to `.html`.
266
+ - [ ] Change `fy.config.js` entries to `.html`.
265
267
  - [ ] Move metadata to `<fy-composition>`.
266
268
  - [ ] Move `defaultProps` to the `data-fy="props"` JSON block.
267
269
  - [ ] Move `mount()` markup into the document.