1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
//! # Wavyte guide (v0.2.1)
//!
//! This module is a standalone, end-to-end walkthrough of Wavyte’s architecture and public API.
//! It is intentionally detailed so future phases (and external integrations) can build on a shared
//! mental model of what “a render” means in this codebase.
//!
//! If you are looking for copy/paste commands, start with the repository `README.md`.
//! If you are implementing new features, start here.
//!
//! ---
//!
//! ## Core concepts
//!
//! - [`Composition`](crate::Composition): the timeline (assets + tracks + clips) and global settings
//! - [`FrameIndex`](crate::FrameIndex): a 0-based frame index within the composition’s duration
//! - [`Evaluator`](crate::Evaluator): resolves what is visible at a frame and in what order
//! - [`RenderPlan`](crate::RenderPlan): backend-agnostic render IR for a single frame
//! - [`RenderBackend`](crate::RenderBackend): executes a plan into pixels
//! - [`FrameRGBA`](crate::FrameRGBA): the output pixels (RGBA8, premultiplied alpha)
//! - [`PreparedAssetStore`](crate::PreparedAssetStore): immutable prepared assets loaded up front
//!
//! The rendering pipeline is explicitly staged:
//!
//! 1. Evaluate timeline visibility: [`Evaluator::eval_frame`](crate::Evaluator::eval_frame)
//! 2. Compile into passes: [`compile_frame`](crate::compile_frame)
//! 3. Execute passes: [`RenderBackend::render_plan`](crate::RenderBackend::render_plan)
//!
//! Convenience wrappers for step (1)+(2)+(3) live in:
//! - [`render_frame`](crate::render_frame)
//! - [`render_frames`](crate::render_frames)
//! - [`render_to_mp4`](crate::render_to_mp4)
//!
//! ---
//!
//! ## “No IO in the renderer” (and why)
//!
//! Wavyte wants evaluation/compilation/rendering to be deterministic, testable, and portable.
//! To do that, renderer code never reaches into the filesystem (or network).
//! Instead:
//!
//! - IO and decoding happen through [`PreparedAssetStore::prepare`](crate::PreparedAssetStore::prepare)
//! - Renderers consume **prepared** assets:
//! - [`PreparedImage`](crate::PreparedImage) (premultiplied RGBA8)
//! - [`PreparedSvg`](crate::PreparedSvg) (`usvg::Tree`)
//! - [`PreparedText`](crate::PreparedText) (Parley layout + font bytes)
//!
//! Assets are loaded from a root directory via [`PreparedAssetStore::prepare`](crate::PreparedAssetStore::prepare)
//! and shared immutably across compile/render steps.
//!
//! This design makes it straightforward to add a future asset cache that loads from:
//! - an in-memory store
//! - a content-addressed CAS
//! - a remote object store
//! without changing renderer logic.
//!
//! ---
//!
//! ## Premultiplied alpha (Wavyte’s pixel contract)
//!
//! Wavyte’s internal and output pixel convention is **premultiplied RGBA8**:
//!
//! - decoded images are premultiplied at ingest
//! - render backends output premultiplied pixels in [`FrameRGBA`](crate::FrameRGBA)
//! - CPU compositing and effects assume premultiplied alpha
//! - MP4 encoding may optionally flatten alpha over a background color
//!
//! If you integrate Wavyte with external compositors, this is the most important contract to
//! preserve. Treat `FrameRGBA.data` as premultiplied unless explicitly stated otherwise by the API.
//!
//! ---
//!
//! ## Building a composition (Rust DSL)
//!
//! JSON is supported via Serde, but the JSON representation is necessarily verbose because many
//! timeline fields are modeled as animated values ([`Anim<T>`](crate::Anim)).
//! For programmatic usage, prefer the builder DSL.
//!
//! The following example builds a minimal composition containing a single inlined path asset
//! (no external IO needed), then renders one frame on the CPU backend.
//!
//! ```rust,no_run
//! use wavyte::{
//! Anim, Asset, BackendKind, Canvas, ClipBuilder, CompositionBuilder, Fps, FrameIndex,
//! FrameRange, PathAsset, PreparedAssetStore, RenderSettings, TrackBuilder, Transform2D, Vec2,
//! create_backend, render_frame,
//! };
//!
//! # fn main() -> wavyte::WavyteResult<()> {
//! let comp = CompositionBuilder::new(
//! Fps::new(30, 1)?,
//! Canvas { width: 512, height: 512 },
//! FrameIndex(60),
//! )
//! .seed(1)
//! .asset(
//! "rect",
//! Asset::Path(PathAsset {
//! svg_path_d: "M0,0 L120,0 L120,120 L0,120 Z".to_string(),
//! }),
//! )?
//! .track(
//! TrackBuilder::new("main")
//! .clip(
//! ClipBuilder::new(
//! "c0",
//! "rect",
//! FrameRange::new(FrameIndex(0), FrameIndex(60))?,
//! )
//! .transform(Anim::constant(Transform2D {
//! translate: Vec2::new(180.0, 180.0),
//! scale: Vec2::new(2.5, 2.5),
//! ..Transform2D::default()
//! }))
//! .build()?,
//! )
//! .build()?,
//! )
//! .build()?;
//!
//! let settings = RenderSettings {
//! clear_rgba: Some([18, 20, 28, 255]),
//! };
//! let mut backend = create_backend(BackendKind::Cpu, &settings)?;
//! let assets = PreparedAssetStore::prepare(&comp, ".")?;
//!
//! let frame = render_frame(&comp, FrameIndex(0), backend.as_mut(), &assets)?;
//! assert_eq!(frame.width, 512);
//! assert_eq!(frame.height, 512);
//! assert!(frame.premultiplied);
//! assert_eq!(frame.data.len(), 512 * 512 * 4);
//! # Ok(())
//! # }
//! ```
//!
//! Notes:
//!
//! - [`Composition::validate`](crate::Composition::validate) is called by the builder.
//! - The example uses [`Anim::constant`](crate::Anim::constant) to avoid verbose keyframes.
//!
//! ---
//!
//! ## Asset paths and validation
//!
//! For assets that reference external files (`Image`, `Svg`, `Text` font sources), v0.2.1 enforces:
//!
//! - **relative** paths (no leading `/`)
//! - OS-agnostic separators (`\` normalized to `/`)
//! - no `..` components
//!
//! These checks happen during composition validation.
//!
//! Important: validation checks that the path is well-formed, but does not require that the file
//! exists. IO errors are surfaced when the prepared store is built.
//!
//! ---
//!
//! ## Evaluation: from timeline to visible nodes
//!
//! [`Evaluator`](crate::Evaluator) converts a `Composition` into an [`EvaluatedGraph`](crate::EvaluatedGraph)
//! at a given frame index:
//!
//! - clips are filtered by their [`FrameRange`](crate::FrameRange)
//! - animated properties are sampled ([`Anim::sample`](crate::Anim::sample))
//! - transforms are resolved into a `kurbo::Affine` (re-exported as [`Affine`](crate::Affine))
//! - opacities are clamped into `[0, 1]`
//! - transitions are resolved into typed transition instances with progress
//!
//! The evaluated graph is painter’s-order: later nodes are drawn “on top”.
//!
//! ---
//!
//! ## Compilation: from nodes to `RenderPlan`
//!
//! The compiler takes visible nodes and produces a backend-agnostic plan:
//!
//! - explicit surfaces with known sizes/formats
//! - an ordered list of passes
//! - draw operations and composites expressed in terms of stable asset IDs
//!
//! In v0.2.1, the plan uses these pass types:
//!
//! - [`ScenePass`](crate::ScenePass)
//! - [`OffscreenPass`](crate::OffscreenPass) (currently used for blur)
//! - [`CompositePass`](crate::CompositePass) (`Over`, `Crossfade`, `Wipe`)
//!
//! ### Draw operations and coordinate conventions
//!
//! Each [`DrawOp`](crate::DrawOp) carries:
//!
//! - a [`Affine`](crate::Affine) transform
//! - an opacity factor in `[0, 1]`
//! - a blend mode (currently only [`BlendMode::Normal`](crate::BlendMode))
//! - an integer `z` used for ordering within a pass
//!
//! v0.2.1 draw ops:
//!
//! - `FillPath`:
//! - the local coordinate space is the SVG path coordinates parsed into a [`BezPath`](crate::BezPath)
//! - the op’s `transform` maps local path space into canvas space
//! - `Image`:
//! - the local coordinate space is `[0,0]..[width,height]` in image pixels
//! - the `transform` maps pixel space into canvas space
//! - `Svg`:
//! - the asset is a `usvg::Tree` (vector)
//! - v0.2.1: rasterized via `resvg` into a pixmap (premultiplied RGBA8), then drawn as an image
//! - note: we intentionally rasterize SVG via `resvg` for predictable SVG `<text>` correctness.
//! - `Text`:
//! - the asset is a prepared Parley layout
//! - glyph positioning originates in the Parley layout coordinate space; the op `transform`
//! positions it in canvas space
//!
//! ### Effects and transitions (v0.2.1)
//!
//! v0.2.1 supports a small set of effects and transitions, chosen specifically to validate the
//! multi-pass architecture.
//!
//! - Effects:
//! - inline effects (folded into op transform/opacity at compile time)
//! - pass effects (implemented as an [`OffscreenPass`](crate::OffscreenPass))
//! - currently implemented pass effect: blur
//! - Transitions:
//! - crossfade
//! - wipe (direction + soft edge)
//!
//! Transitions are compiled into [`CompositePass`](crate::CompositePass) operations rather than
//! being “baked” into draw op opacity, because they conceptually combine multiple rendered layers.
//!
//! Why this exists:
//!
//! - render backends can share the same compilation logic
//! - tests can validate compiler output without involving a renderer
//! - future effects/transitions can be expressed once in the IR
//!
//! ---
//!
//! ## Rendering: backends and `RenderBackend`
//!
//! A renderer implements [`RenderBackend`](crate::RenderBackend), which extends
//! [`PassBackend`](crate::PassBackend) (the trait that knows how to execute individual passes).
//!
//! Backend construction is done via:
//!
//! - [`BackendKind`](crate::BackendKind): `Cpu`
//! - [`create_backend`](crate::create_backend): returns `Box<dyn RenderBackend>`
//!
//! CPU backend (always available):
//!
//! - powered by `vello_cpu`
//! - SVG is supported by rasterizing `usvg::Tree` via `resvg` into an RGBA pixmap
//!
//! Wavyte v0.2.1 focuses on CPU rendering only.
//!
//! Parallel rendering is available via [`RenderThreading`](crate::RenderThreading), including
//! optional static-frame elision using [`FrameFingerprint`](crate::FrameFingerprint) to skip
//! duplicate frame graphs in chunked parallel runs.
//!
//! ---
//!
//! ## MP4 encoding: `ffmpeg` as a runtime prerequisite
//!
//! Wavyte intentionally does not ship a built-in MP4 encoder. Instead, it wraps the system `ffmpeg`
//! binary:
//!
//! - [`FfmpegEncoder`](crate::FfmpegEncoder) spawns `ffmpeg` and streams raw frames to stdin
//! - [`render_to_mp4`](crate::render_to_mp4) orchestrates frame rendering and feeding the encoder
//!
//! `ffmpeg` must be installed and on `PATH`. If it is not available, encoding returns a structured
//! error; there is no silent fallback.
//!
//! The encoder configuration surface is:
//!
//! - [`EncodeConfig`](crate::EncodeConfig)
//! - [`default_mp4_config`](crate::default_mp4_config)
//!