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
@@ -16,9 +16,9 @@ export default {
16
16
  };
17
17
  ```
18
18
 
19
- - `format` fixes width, height, and fps for every timeline. It is required when the project lists timelines.
19
+ - `format` fixes width, height, and fps for every timeline and fills those values for compositions that omit them; a declared value always wins. It is required when the project lists timelines.
20
20
  - Composition and timeline IDs share one namespace and must be unique.
21
- - A composition used in a timeline must match the project fps exactly. A different size is fitted by the clip.
21
+ - An animated composition that declares an `fps` must match the project fps exactly; an undeclared `fps` inherits it. A different size is fitted by the clip. A static composition has no fps and skips the frame-rate comparison.
22
22
  - Composition-only projects remain valid without `format` or `timelines`.
23
23
 
24
24
  ## Timeline document
@@ -41,7 +41,14 @@ export default {
41
41
  "duration": 150,
42
42
  "sourceIn": 0,
43
43
  "props": { "title": "Hello" },
44
- "transform": { "fit": "contain", "x": 0, "y": 0, "scale": 1, "rotation": 0, "opacity": 1 },
44
+ "blend": "screen",
45
+ "transform": { "fit": "contain", "x": 0, "y": 0, "scale": 1, "rotation": 0 },
46
+ "keyframes": {
47
+ "opacity": [
48
+ { "frame": 0, "value": 0 },
49
+ { "frame": 20, "value": 1, "easing": "ease-out" }
50
+ ]
51
+ },
45
52
  "audio": { "muted": false, "gain": 1 }
46
53
  },
47
54
  {
@@ -68,13 +75,13 @@ export default {
68
75
  - `tracks` and `clips` are required. Track order is bottom-to-top for video and irrelevant for audio.
69
76
  - `background` is an optional color; the final video composites over black.
70
77
  - `durationInFrames` is optional. The duration is derived from the latest clip end unless declared; a declared duration may extend but not cut clips.
71
- - Unknown fields are errors. No transitions, effects, keyframes, or track solo/visibility in 0.6.
78
+ - Unknown fields are errors. Blend modes are static per clip; there are no animated blend modes, other effects, or track solo/visibility. Clip transitions are the one same-track overlap (see [Transitions](#transitions)).
72
79
 
73
80
  ## Sources
74
81
 
75
82
  | `source.kind` | Fields | Notes |
76
83
  | --- | --- | --- |
77
- | `composition` | `id` | Project composition ID. The clip may set `props`. |
84
+ | `composition` | `id` | Project composition ID. The clip may set `props`. A static composition behaves as an image source. |
78
85
  | `timeline` | `id` | Nested timeline ID. Rendered as a group. |
79
86
  | `video` | `src` | Local MP4/WebM, project-relative. |
80
87
  | `image` | `src` | Local PNG/JPEG/WebP. `duration` is required. |
@@ -85,10 +92,11 @@ Media paths resolve from the project root. No remote URLs.
85
92
  ## Time and trims
86
93
 
87
94
  - `start` is the timeline frame where the clip begins.
88
- - `sourceIn` is the first source frame. It defaults to `0` and must be `0` for images.
89
- - `duration` is the number of timeline frames. It is optional for compositions, nested timelines, video, and audio; it defaults to the rest of the source. Images require it.
90
- - A clip that exceeds its source is an error.
91
- - Clips on one track cannot overlap. Touching endpoints are allowed. Use separate tracks for overlays.
95
+ - `sourceIn` is the first source frame. It defaults to `0` and must be `0` for images and static compositions.
96
+ - `duration` is the number of timeline frames. It is optional for animated compositions, nested timelines, video, and audio; it defaults to the rest of the source. Images and static compositions require it.
97
+ - A clip that exceeds its source is an error. Static compositions have no intrinsic duration, so the clip duration is the only bound.
98
+ - A static composition clip is captured once per props variant as a transparent PNG and reused across segments. A clip whose source is static and whose `audio` settings are non-default receives an `AUDIO_WARNING` because there is no source audio.
99
+ - Clips on one track cannot overlap unless a timeline-level `transitions` entry pairs them. Touching endpoints are allowed but are not a transition. Use separate tracks for overlays that are not transitions.
92
100
 
93
101
  ## Presentation
94
102
 
@@ -100,12 +108,96 @@ Visual clips accept a static transform:
100
108
  - `scale`: multiplier after fitting to the project frame; an alternative to `width`/`height`.
101
109
  - `rotation`: degrees around the center.
102
110
  - `opacity`: 0–1.
111
+ - `blend`: CSS `mix-blend-mode`, defaulting to `normal`. Supported values are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`. `hue`, `saturation`, `color`, and `luminosity` are rejected.
103
112
 
104
- No keyframes or animated transforms in 0.6.
113
+ A blend mode composites the clip against everything below it in the stage, including the timeline background, and only where the clip's source alpha is non-zero. A composition or nested timeline that paints an opaque background blocks the lower layers; source alpha is what lets a blend reach them. A top-level timeline with no declared background renders over black; a nested or embedded timeline stays transparent. Blend modes are static; there are no animated blend modes.
114
+
115
+ Rotation keeps the full rotated bounds. Production expands the rotate filter's output to a fixed bound: static rotation uses its exact rotated bounding box, while keyed rotation uses the diagonal of the fitted box so every segment uses the same size. The filter fills the padding with transparent pixels, so rotated corners are visible instead of being cut off.
116
+
117
+ ## Keyframes
118
+
119
+ Visual clips may animate `x`, `y`, `scale`, `rotation`, and `opacity` with a top-level `keyframes` object keyed by channel:
120
+
121
+ ```json
122
+ "keyframes": {
123
+ "opacity": [
124
+ { "frame": 0, "value": 0 },
125
+ { "frame": 24, "value": 1, "easing": "ease-out" },
126
+ { "frame": 120, "value": 1 },
127
+ { "frame": 150, "value": 0, "easing": "ease-in" }
128
+ ],
129
+ "x": [
130
+ { "frame": 0, "value": -80, "easing": "ease-out" },
131
+ { "frame": 24, "value": 0 }
132
+ ]
133
+ }
134
+ ```
135
+
136
+ - `frame` is clip-local. `0` is the clip's start boundary and `duration` is its end boundary. Moving `start` moves the animation with the clip; `sourceIn` does not shift it.
137
+ - Keys are integers in `0..duration`, strictly increasing, with at least two per channel and a required `value`.
138
+ - A channel holds its first value before the first key and its last value after the last key. Keyframes never extend a clip's duration.
139
+ - `easing` applies to the interval after that key. It defaults to `linear`; the final key's easing has no effect.
140
+ - Supported easings are `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`, `step-start`, `step-end`, `hold`, `cubic-bezier(x1, y1, x2, y2)`, and `steps(n, jump-start|jump-end|jump-none|jump-both)`.
141
+ - A channel cannot also be an explicitly declared scalar in `transform`. Defaults do not conflict; unkeyed channels keep their `transform` value.
142
+ - `opacity` values are `0..1` and `scale` values are `>= 0`. Interpolated opacity and scale are clamped after easing; `x`, `y`, and `rotation` may overshoot.
143
+ - `keyframes.scale` cannot be combined with a `transform.width`/`height` box.
144
+ - `keyframes` is only valid on visual clips. A static composition is captured once as an image and then animated like any other visual layer.
145
+
146
+ Preview evaluates keyframes on every seek with the exact CSS timing function. Production generates per-frame FFmpeg expressions from the same evaluator and uses an alpha mask for animated opacity. Floating-point easing in the filter graph approximates the CSS curve; parity remains at clip capture, not at the final encoded pixel. Fractional `x`/`y` values may differ by a subpixel between preview and production.
147
+
148
+ The read-only timeline preview draws a marker for each distinct keyframe frame on a clip. Hovering a marker lists the channels at that frame; clicking it seeks the playhead there.
149
+
150
+ ## Transitions
151
+
152
+ A timeline may declare a `transitions` array. Each entry pairs two clips on one track across their overlap:
153
+
154
+ ```json
155
+ "clips": [
156
+ { "id": "alpha", "track": "main", "source": { "kind": "composition", "id": "alpha" }, "start": 0, "duration": 60 },
157
+ { "id": "beta", "track": "main", "source": { "kind": "composition", "id": "beta" }, "start": 45, "duration": 60 }
158
+ ],
159
+ "transitions": [
160
+ { "from": "alpha", "to": "beta", "kind": "dissolve", "easing": "ease-in-out" }
161
+ ]
162
+ ```
163
+
164
+ - `from` and `to` must be clip IDs on the same track. A transition never spans tracks.
165
+ - `kind` is required. `dissolve` is a visual fade on a video track and also crossfades the audio the two clips contribute. `crossfade` is an audio gain ramp on an audio track. Using the wrong kind for the track is an error.
166
+ - `easing` defaults to `linear` and accepts the same CSS timing functions as keyframes, including `hold` and `steps(...)`.
167
+ - There is no `duration`. The window is the overlap `to.start .. from.end`. Move `to.start` earlier to lengthen the transition; the outgoing clip's declared duration already covers its side, so no media handles are needed.
168
+ - Valid geometry is `from.start < to.start < from.end < to.end`. The incoming clip must start inside the outgoing clip and end after it. Reversed order, a contained clip, a touching or later start, and cross-track pairs are errors.
169
+ - A clip may be the `to` of one transition and the `from` of the next, so transitions chain. A clip may not participate twice on the same side, and the same pair may not be declared twice.
170
+ - Overlap without a declared transition is still `TIMELINE_CLIP_OVERLAP`. A third clip that overlaps either side is an error, so at most two clips are active on a track.
171
+ - During a dissolve the incoming clip composites directly above the outgoing clip with alpha `clipOpacity × eased progress`. Its static or keyframed `opacity` multiplies the transition. `blend` works normally because transition alpha is part of the source alpha.
172
+ - Both kinds use equal power: the outgoing clip ramps as `sqrt(1 - t)` and the incoming as `sqrt(t)`, after easing. For a dissolve this applies to contributed audio; for a crossfade it applies to the two audio clips. `audio.muted` still zeroes its side. Transitions on a nested-timeline clip fade that group's audio.
173
+ - Preview stacks the incoming visual layer above the outgoing layer, applies the same alpha, ramps `<audio>` volume during playback, and draws a read-only hatched band over the overlap on either track.
174
+
175
+ `check` exposes the resolved transition count and validates every window. `still` and `render` share the composite path, so a still captured inside a dissolve matches the rendered frame. A crossfade changes no pixels. See `examples/transitions`.
176
+
177
+ ### Audio on the timeline
178
+
179
+ Audio can live directly on an audio track and still crossfade:
180
+
181
+ ```json
182
+ "tracks": [
183
+ { "id": "v1", "kind": "video" },
184
+ { "id": "a1", "kind": "audio" }
185
+ ],
186
+ "clips": [
187
+ { "id": "bed", "track": "v1", "source": { "kind": "composition", "id": "bed" }, "start": 0, "duration": 105 },
188
+ { "id": "chord-a", "track": "a1", "source": { "kind": "audio", "src": "assets/a.wav" }, "start": 0, "duration": 60 },
189
+ { "id": "chord-b", "track": "a1", "source": { "kind": "audio", "src": "assets/b.wav" }, "start": 45, "duration": 60 }
190
+ ],
191
+ "transitions": [
192
+ { "from": "chord-a", "to": "chord-b", "kind": "crossfade", "easing": "linear" }
193
+ ]
194
+ ```
195
+
196
+ Mute composition audio with `audio.muted: true` when the sound comes from audio-track clips, so it does not play twice. `examples/transitions` has an `audio-crossfade` timeline that pairs a visual dissolve with an audio crossfade over the same 15 frames.
105
197
 
106
198
  ## Audio
107
199
 
108
- - Composition, video, and nested-timeline clips contribute audio automatically unless `audio.muted` is true.
200
+ - Animated composition, video, and nested-timeline clips contribute audio automatically unless `audio.muted` is true. Static compositions contribute no audio; put audio on a timeline track.
109
201
  - Timeline audio clips live on audio tracks. Audio tracks support `gain` and `muted`.
110
202
  - `audio.gain` on a clip is 0–1 and multiplies the source audio.
111
203
  - There are no linked A/V pairs. For a J/L cut, mute the visual clip and add an audio clip with a different `sourceIn`.
@@ -113,7 +205,7 @@ No keyframes or animated transforms in 0.6.
113
205
 
114
206
  ## Nested timelines
115
207
 
116
- A nested timeline clip behaves as one layer: its internal tracks are composited first, then the parent clip's opacity and transform apply to the whole group. Nested audio is included. Cycles are rejected statically. Timeline clips do not override props of compositions inside a nested timeline in 0.6.
208
+ A nested timeline clip behaves as one layer: its internal tracks are composited first, then the parent clip's opacity, transform, and blend mode apply to the whole group. A blend mode inside the nested timeline sees only that timeline's own backdrop; it never sees the parent's layers. A declared nested background is part of the group, so a transparent nested timeline has no backdrop to blend against beyond its own layers. Nested audio is included. Cycles are rejected statically. Timeline clips do not override props of compositions inside a nested timeline.
117
209
 
118
210
  ## Commands
119
211
 
@@ -130,14 +222,16 @@ npx frameyard render main --props props/main.json --workers 8 --scratch /path/to
130
222
  - `--container <mp4|mov|mkv|webm>` selects the delivery container (default `mp4`); `--codec <h264|hevc|vp9>` selects the codec, and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded. MP4 and MOV hold H.264/HEVC with AAC; MKV holds all three; WebM holds VP9 with Opus and defaults to VP9. `auto` is the default and probes NVENC, then Quick Sync, then software; `software` forces `libx264`, `libx265`, or `libvpx-vp9`. FFV1 intermediates stay lossless and software-encoded, HEVC keeps the `hvc1` tag in MP4/MOV, and the output extension must match the container.
131
223
  - `--concurrency` is composition-only.
132
224
  - `render` defaults to `out/<timeline-id>.mp4`.
133
- - `check` evaluates every timeline frame and composition frame. `still` captures one or more requested frames through the same clip capture and composite path; a multi-frame still resolves the timeline, starts one server, and reuses one scratch directory. The `--frame` spec and 32-frame cap are the same as for compositions.
225
+ - `check` evaluates every timeline frame and every animated composition frame; a static composition layer is inspected once and not seeked per frame. It also evaluates every keyframe channel at every timeline frame. `still` captures one or more requested frames through the same clip capture and composite path; a multi-frame still resolves the timeline, starts one server, and reuses one scratch directory. The `--frame` spec and 32-frame cap are the same as for compositions.
134
226
  - `npx frameyard dev` lists timelines in the preview; the timeline view has a program monitor, read-only tracks and clips, playhead, playback, audio controls, and diagnostics.
135
227
 
136
228
  ## Determinism and performance
137
229
 
138
- - A timeline frame derives only from its active clips, their composition frames, props, assets, and the render plan.
230
+ - A timeline frame derives only from its active clips, their composition frames, keyframe values, props, assets, and the render plan.
139
231
  - Composition frames are captured with Chromium; transparent compositions preserve alpha.
140
232
  - Timeline production captures active composition and nested-timeline clips to lossless FFV1, composites segments with FFmpeg, concatenates the ordered segments, and mixes audio once.
141
- - Preview composites in Chromium; production composites with FFmpeg. Small scaling, alpha, and video color differences are expected. Parity is guaranteed at clip capture, not at final encoded pixels.
233
+ - Keyframe and transition values are evaluated against absolute timeline frames, so segment boundaries do not change them. Position, scale, and rotation use per-frame filter expressions; animated opacity and incoming transition alpha use an alpha mask. Animated opacity, animated scale, and a dissolve add a mask/scale pass per affected layer.
234
+ - Preview composites in Chromium with CSS `mix-blend-mode` and CSS `opacity`; production composites with FFmpeg's `blend`, `maskedmerge`, and an alpha mask. CSS is the reference; small scaling, alpha, color, blend-math, and subpixel-position differences are expected. Easing curves in the filter graph approximate the CSS curve; parity is guaranteed at clip capture, not at final encoded pixels.
235
+ - Alpha-correct blending costs roughly 1.7× per blended 1080p layer in the production composite. Budget for it when a timeline stacks several blended layers.
142
236
  - Final output bytes are not a reproducibility guarantee.
143
237
  - Long timelines are segmented and bounded. The reference target and measured worst cases are documented in `docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md`.
File without changes