frameyard 0.8.0 → 0.10.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 (327) hide show
  1. package/CHANGELOG.md +177 -14
  2. package/GLOSSARY.md +112 -2
  3. package/README.md +302 -265
  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 +1685 -93
  21. package/dist/cli.js.map +1 -1
  22. package/dist/composition/format.d.ts +1 -1
  23. package/dist/composition/format.d.ts.map +1 -1
  24. package/dist/composition/format.js +1 -1
  25. package/dist/composition/format.js.map +1 -1
  26. package/dist/context.d.ts +82 -0
  27. package/dist/context.d.ts.map +1 -0
  28. package/dist/context.js +277 -0
  29. package/dist/context.js.map +1 -0
  30. package/dist/contracts.d.ts +25 -0
  31. package/dist/contracts.d.ts.map +1 -1
  32. package/dist/contracts.js.map +1 -1
  33. package/dist/doctor.d.ts +14 -1
  34. package/dist/doctor.d.ts.map +1 -1
  35. package/dist/doctor.js +166 -7
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/errors.d.ts +1 -1
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/errors.js.map +1 -1
  40. package/dist/fonts/add.d.ts +41 -0
  41. package/dist/fonts/add.d.ts.map +1 -0
  42. package/dist/fonts/add.js +259 -0
  43. package/dist/fonts/add.js.map +1 -0
  44. package/dist/fonts/binary.d.ts +4 -0
  45. package/dist/fonts/binary.d.ts.map +1 -0
  46. package/dist/fonts/binary.js +21 -0
  47. package/dist/fonts/binary.js.map +1 -0
  48. package/dist/fonts/css.d.ts +4 -0
  49. package/dist/fonts/css.d.ts.map +1 -0
  50. package/dist/fonts/css.js +50 -0
  51. package/dist/fonts/css.js.map +1 -0
  52. package/dist/fonts/fontsource.d.ts +107 -0
  53. package/dist/fonts/fontsource.d.ts.map +1 -0
  54. package/dist/fonts/fontsource.js +565 -0
  55. package/dist/fonts/fontsource.js.map +1 -0
  56. package/dist/fonts/index.d.ts +11 -0
  57. package/dist/fonts/index.d.ts.map +1 -0
  58. package/dist/fonts/index.js +11 -0
  59. package/dist/fonts/index.js.map +1 -0
  60. package/dist/fonts/install.d.ts +24 -0
  61. package/dist/fonts/install.d.ts.map +1 -0
  62. package/dist/fonts/install.js +145 -0
  63. package/dist/fonts/install.js.map +1 -0
  64. package/dist/fonts/manifest.d.ts +21 -0
  65. package/dist/fonts/manifest.d.ts.map +1 -0
  66. package/dist/fonts/manifest.js +96 -0
  67. package/dist/fonts/manifest.js.map +1 -0
  68. package/dist/fonts/options.d.ts +9 -0
  69. package/dist/fonts/options.d.ts.map +1 -0
  70. package/dist/fonts/options.js +66 -0
  71. package/dist/fonts/options.js.map +1 -0
  72. package/dist/fonts/schema.d.ts +4 -0
  73. package/dist/fonts/schema.d.ts.map +1 -0
  74. package/dist/fonts/schema.js +316 -0
  75. package/dist/fonts/schema.js.map +1 -0
  76. package/dist/fonts/summary.d.ts +16 -0
  77. package/dist/fonts/summary.d.ts.map +1 -0
  78. package/dist/fonts/summary.js +22 -0
  79. package/dist/fonts/summary.js.map +1 -0
  80. package/dist/fonts/types.d.ts +56 -0
  81. package/dist/fonts/types.d.ts.map +1 -0
  82. package/dist/fonts/types.js +8 -0
  83. package/dist/fonts/types.js.map +1 -0
  84. package/dist/fonts/verify.d.ts +33 -0
  85. package/dist/fonts/verify.d.ts.map +1 -0
  86. package/dist/fonts/verify.js +200 -0
  87. package/dist/fonts/verify.js.map +1 -0
  88. package/dist/html/validate.d.ts.map +1 -1
  89. package/dist/html/validate.js +59 -0
  90. package/dist/html/validate.js.map +1 -1
  91. package/dist/index.d.ts +3 -3
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +1 -1
  94. package/dist/index.js.map +1 -1
  95. package/dist/inspect.d.ts +61 -0
  96. package/dist/inspect.d.ts.map +1 -0
  97. package/dist/inspect.js +325 -0
  98. package/dist/inspect.js.map +1 -0
  99. package/dist/lint.d.ts +63 -0
  100. package/dist/lint.d.ts.map +1 -0
  101. package/dist/lint.js +265 -0
  102. package/dist/lint.js.map +1 -0
  103. package/dist/policy.d.ts +69 -0
  104. package/dist/policy.d.ts.map +1 -0
  105. package/dist/policy.js +361 -0
  106. package/dist/policy.js.map +1 -0
  107. package/dist/preview/fonts/LICENSE-figtree.txt +93 -0
  108. package/dist/preview/fonts/LICENSE-space-mono.txt +93 -0
  109. package/dist/preview/fonts/README.md +23 -0
  110. package/dist/preview/fonts/figtree-latin-wght-normal.woff2 +0 -0
  111. package/dist/preview/fonts/space-mono-latin-400-normal.woff2 +0 -0
  112. package/dist/preview/fonts/space-mono-latin-700-normal.woff2 +0 -0
  113. package/dist/preview/main.js +5 -2
  114. package/dist/preview/main.js.map +1 -1
  115. package/dist/preview/timeline-data.d.ts +17 -69
  116. package/dist/preview/timeline-data.d.ts.map +1 -1
  117. package/dist/preview/timeline-host.d.ts.map +1 -1
  118. package/dist/preview/timeline-host.js +70 -45
  119. package/dist/preview/timeline-host.js.map +1 -1
  120. package/dist/preview/timeline-main.d.ts.map +1 -1
  121. package/dist/preview/timeline-main.js +88 -27
  122. package/dist/preview/timeline-main.js.map +1 -1
  123. package/dist/project.d.ts +10 -3
  124. package/dist/project.d.ts.map +1 -1
  125. package/dist/project.js +20 -17
  126. package/dist/project.js.map +1 -1
  127. package/dist/renderer/audio.d.ts +6 -0
  128. package/dist/renderer/audio.d.ts.map +1 -1
  129. package/dist/renderer/audio.js +3 -1
  130. package/dist/renderer/audio.js.map +1 -1
  131. package/dist/renderer/browser.d.ts.map +1 -1
  132. package/dist/renderer/browser.js +13 -0
  133. package/dist/renderer/browser.js.map +1 -1
  134. package/dist/renderer/concurrency.d.ts +6 -0
  135. package/dist/renderer/concurrency.d.ts.map +1 -0
  136. package/dist/renderer/concurrency.js +27 -0
  137. package/dist/renderer/concurrency.js.map +1 -0
  138. package/dist/renderer/ffmpeg.d.ts +104 -5
  139. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  140. package/dist/renderer/ffmpeg.js +580 -67
  141. package/dist/renderer/ffmpeg.js.map +1 -1
  142. package/dist/renderer/index.js +1 -1
  143. package/dist/renderer/index.js.map +1 -1
  144. package/dist/renderer/keyframe-expressions.d.ts +39 -0
  145. package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
  146. package/dist/renderer/keyframe-expressions.js +180 -0
  147. package/dist/renderer/keyframe-expressions.js.map +1 -0
  148. package/dist/renderer/session-pool.d.ts +28 -0
  149. package/dist/renderer/session-pool.d.ts.map +1 -0
  150. package/dist/renderer/session-pool.js +91 -0
  151. package/dist/renderer/session-pool.js.map +1 -0
  152. package/dist/renderer/timeline.d.ts +2 -0
  153. package/dist/renderer/timeline.d.ts.map +1 -1
  154. package/dist/renderer/timeline.js +600 -147
  155. package/dist/renderer/timeline.js.map +1 -1
  156. package/dist/renderer/timings.d.ts +13 -0
  157. package/dist/renderer/timings.d.ts.map +1 -0
  158. package/dist/renderer/timings.js +29 -0
  159. package/dist/renderer/timings.js.map +1 -0
  160. package/dist/runtime/host.d.ts.map +1 -1
  161. package/dist/runtime/host.js +4 -0
  162. package/dist/runtime/host.js.map +1 -1
  163. package/dist/scaffold.d.ts +3 -0
  164. package/dist/scaffold.d.ts.map +1 -1
  165. package/dist/scaffold.js +48 -320
  166. package/dist/scaffold.js.map +1 -1
  167. package/dist/server/composition-html.d.ts +2 -0
  168. package/dist/server/composition-html.d.ts.map +1 -1
  169. package/dist/server/composition-html.js +7 -0
  170. package/dist/server/composition-html.js.map +1 -1
  171. package/dist/server/preview-html.d.ts.map +1 -1
  172. package/dist/server/preview-html.js +74 -188
  173. package/dist/server/preview-html.js.map +1 -1
  174. package/dist/server/preview-style.d.ts +11 -0
  175. package/dist/server/preview-style.d.ts.map +1 -0
  176. package/dist/server/preview-style.js +298 -0
  177. package/dist/server/preview-style.js.map +1 -0
  178. package/dist/server/server.d.ts.map +1 -1
  179. package/dist/server/server.js +31 -1
  180. package/dist/server/server.js.map +1 -1
  181. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  182. package/dist/server/timeline-preview-data.js +12 -34
  183. package/dist/server/timeline-preview-data.js.map +1 -1
  184. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  185. package/dist/server/timeline-preview-html.js +140 -148
  186. package/dist/server/timeline-preview-html.js.map +1 -1
  187. package/dist/templates.d.ts +16 -0
  188. package/dist/templates.d.ts.map +1 -0
  189. package/dist/templates.js +186 -0
  190. package/dist/templates.js.map +1 -0
  191. package/dist/timeline/easing.d.ts +36 -0
  192. package/dist/timeline/easing.d.ts.map +1 -0
  193. package/dist/timeline/easing.js +225 -0
  194. package/dist/timeline/easing.js.map +1 -0
  195. package/dist/timeline/keyframes.d.ts +23 -0
  196. package/dist/timeline/keyframes.d.ts.map +1 -0
  197. package/dist/timeline/keyframes.js +95 -0
  198. package/dist/timeline/keyframes.js.map +1 -0
  199. package/dist/timeline/plan.d.ts +6 -12
  200. package/dist/timeline/plan.d.ts.map +1 -1
  201. package/dist/timeline/plan.js +17 -30
  202. package/dist/timeline/plan.js.map +1 -1
  203. package/dist/timeline/resolve.d.ts +12 -0
  204. package/dist/timeline/resolve.d.ts.map +1 -1
  205. package/dist/timeline/resolve.js +105 -9
  206. package/dist/timeline/resolve.js.map +1 -1
  207. package/dist/timeline/scene.d.ts +126 -0
  208. package/dist/timeline/scene.d.ts.map +1 -0
  209. package/dist/timeline/scene.js +109 -0
  210. package/dist/timeline/scene.js.map +1 -0
  211. package/dist/timeline/schema.d.ts +31 -0
  212. package/dist/timeline/schema.d.ts.map +1 -1
  213. package/dist/timeline/schema.js +240 -9
  214. package/dist/timeline/schema.js.map +1 -1
  215. package/dist/timeline/transitions.d.ts +21 -0
  216. package/dist/timeline/transitions.d.ts.map +1 -0
  217. package/dist/timeline/transitions.js +23 -0
  218. package/dist/timeline/transitions.js.map +1 -0
  219. package/dist/version.d.ts +1 -1
  220. package/dist/version.d.ts.map +1 -1
  221. package/dist/version.js +1 -1
  222. package/dist/version.js.map +1 -1
  223. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  224. package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
  225. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  226. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +2 -1
  227. package/docs/adr/0015-inherit-composition-format-from-the-project.md +2 -2
  228. package/docs/adr/0016-author-compositions-as-html-fragments.md +1 -1
  229. package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
  230. package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
  231. package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
  232. package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
  233. package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
  234. package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
  235. package/docs/adr/0024-stream-composition-captures-and-skip-trivial-composites.md +86 -0
  236. package/docs/adr/0025-capture-timeline-layers-in-parallel-with-a-global-limit.md +67 -0
  237. package/docs/adr/0026-reuse-loaded-composition-sessions-across-segments.md +70 -0
  238. package/docs/adr/0027-encode-two-full-frame-layers-through-dual-pipes.md +76 -0
  239. package/docs/adr/0028-encode-full-frame-video-segments-directly.md +65 -0
  240. package/docs/migration-0.4.md +2 -2
  241. package/docs/preview-testing.md +27 -6
  242. package/docs/{v0.1-design.md → v0.x-design.md} +57 -32
  243. package/examples/blend/README.md +3 -3
  244. package/examples/demo/README.md +2 -2
  245. package/examples/intro/README.md +71 -0
  246. package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
  247. package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
  248. package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
  249. package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
  250. package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
  251. package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
  252. package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
  253. package/examples/intro/assets/grain.png +0 -0
  254. package/examples/intro/assets/score.ogg +0 -0
  255. package/examples/intro/compositions/brand.html +412 -0
  256. package/examples/intro/compositions/chrome.controller.js +63 -0
  257. package/examples/intro/compositions/chrome.html +256 -0
  258. package/examples/intro/compositions/lattice.controller.js +148 -0
  259. package/examples/intro/compositions/lattice.html +308 -0
  260. package/examples/intro/compositions/opening.html +504 -0
  261. package/examples/intro/compositions/studio.html +930 -0
  262. package/examples/intro/compositions/theme.css +218 -0
  263. package/examples/intro/fy.config.js +13 -0
  264. package/examples/intro/fy.fonts.json +100 -0
  265. package/examples/intro/scripts/make-grain.mjs +79 -0
  266. package/examples/intro/scripts/make-score.mjs +702 -0
  267. package/examples/intro/timelines/intro.timeline.json +60 -0
  268. package/examples/keyframes/README.md +76 -0
  269. package/examples/keyframes/compositions/backdrop.html +60 -0
  270. package/examples/keyframes/compositions/card.html +99 -0
  271. package/examples/keyframes/fy.config.js +10 -0
  272. package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
  273. package/examples/nested/README.md +112 -0
  274. package/examples/nested/assets/pulse.wav +0 -0
  275. package/examples/nested/compositions/backdrop.html +84 -0
  276. package/examples/nested/compositions/card.html +275 -0
  277. package/examples/nested/compositions/overlay.html +307 -0
  278. package/examples/nested/fy.config.js +13 -0
  279. package/examples/nested/scripts/make-pulse.sh +20 -0
  280. package/examples/nested/timelines/chapter-a.timeline.json +39 -0
  281. package/examples/nested/timelines/chapter-b.timeline.json +39 -0
  282. package/examples/nested/timelines/nested-reel.timeline.json +52 -0
  283. package/examples/static/README.md +2 -2
  284. package/examples/transitions/README.md +128 -0
  285. package/examples/transitions/assets/alpha.wav +0 -0
  286. package/examples/transitions/assets/beta.wav +0 -0
  287. package/examples/transitions/assets/gamma.wav +0 -0
  288. package/examples/transitions/compositions/alpha.html +56 -0
  289. package/examples/transitions/compositions/beta.html +56 -0
  290. package/examples/transitions/compositions/gamma.html +56 -0
  291. package/examples/transitions/fy.config.js +12 -0
  292. package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
  293. package/examples/transitions/timelines/transitions.timeline.json +31 -0
  294. package/examples/video/README.md +4 -4
  295. package/examples/webgl/README.md +1 -1
  296. package/package.json +13 -6
  297. package/scaffolds/init/AGENTS.md +131 -0
  298. package/scaffolds/init/README.md +56 -0
  299. package/scaffolds/init/compositions/hello.html +79 -0
  300. package/scaffolds/init/fy.config.js +9 -0
  301. package/scaffolds/init/gitignore.template +1 -0
  302. package/scaffolds/init/package.json +11 -0
  303. package/scaffolds/init/props/hello.json +4 -0
  304. package/scaffolds/init/props/main.json +6 -0
  305. package/scaffolds/init/timelines/main.timeline.json +20 -0
  306. package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
  307. package/scaffolds/local-models/fy.policy.json +5 -0
  308. package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
  309. package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
  310. package/scaffolds/local-models/pi/sandbox.json +15 -0
  311. package/scaffolds/local-models/pi/settings.json +15 -0
  312. package/skills/frameyard/README.md +1 -0
  313. package/skills/frameyard/SKILL.md +59 -61
  314. package/skills/frameyard/references/audio-assets.md +32 -3
  315. package/skills/frameyard/references/common-mistakes.md +53 -2
  316. package/skills/frameyard/references/deterministic-authoring.md +10 -3
  317. package/skills/frameyard/references/diagnostics.md +31 -1
  318. package/skills/frameyard/references/failure-recovery.md +32 -6
  319. package/skills/frameyard/references/fonts.md +132 -0
  320. package/skills/frameyard/references/json-results.md +119 -6
  321. package/skills/frameyard/references/quick-reference.md +222 -0
  322. package/skills/frameyard/references/timelines.md +97 -7
  323. /package/examples/blend/{video.config.js → fy.config.js} +0 -0
  324. /package/examples/demo/{video.config.js → fy.config.js} +0 -0
  325. /package/examples/static/{video.config.js → fy.config.js} +0 -0
  326. /package/examples/video/{video.config.js → fy.config.js} +0 -0
  327. /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
@@ -42,7 +42,13 @@ export default {
42
42
  "sourceIn": 0,
43
43
  "props": { "title": "Hello" },
44
44
  "blend": "screen",
45
- "transform": { "fit": "contain", "x": 0, "y": 0, "scale": 1, "rotation": 0, "opacity": 1 },
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
+ },
46
52
  "audio": { "muted": false, "gain": 1 }
47
53
  },
48
54
  {
@@ -69,7 +75,7 @@ export default {
69
75
  - `tracks` and `clips` are required. Track order is bottom-to-top for video and irrelevant for audio.
70
76
  - `background` is an optional color; the final video composites over black.
71
77
  - `durationInFrames` is optional. The duration is derived from the latest clip end unless declared; a declared duration may extend but not cut clips.
72
- - Unknown fields are errors. Blend modes are static per clip; no transitions, keyframes, other effects, or track solo/visibility.
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)).
73
79
 
74
80
  ## Sources
75
81
 
@@ -90,7 +96,7 @@ Media paths resolve from the project root. No remote URLs.
90
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.
91
97
  - A clip that exceeds its source is an error. Static compositions have no intrinsic duration, so the clip duration is the only bound.
92
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.
93
- - Clips on one track cannot overlap. Touching endpoints are allowed. Use separate tracks for overlays.
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.
94
100
 
95
101
  ## Presentation
96
102
 
@@ -104,7 +110,90 @@ Visual clips accept a static transform:
104
110
  - `opacity`: 0–1.
105
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.
106
112
 
107
- 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 keyframes or animated transforms.
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.
108
197
 
109
198
  ## Audio
110
199
 
@@ -133,15 +222,16 @@ npx frameyard render main --props props/main.json --workers 8 --scratch /path/to
133
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.
134
223
  - `--concurrency` is composition-only.
135
224
  - `render` defaults to `out/<timeline-id>.mp4`.
136
- - `check` evaluates every timeline frame and every animated composition frame; a static composition layer is inspected once and not seeked per 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.
137
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.
138
227
 
139
228
  ## Determinism and performance
140
229
 
141
- - 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.
142
231
  - Composition frames are captured with Chromium; transparent compositions preserve alpha.
143
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.
144
- - Preview composites in Chromium with CSS `mix-blend-mode`; production composites with FFmpeg's `blend` and `maskedmerge`. CSS is the reference; small scaling, alpha, color, and blend-math 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.
145
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.
146
236
  - Final output bytes are not a reproducibility guarantee.
147
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