diff --git a/Cargo.lock b/Cargo.lock index 849efd1..025c06a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1059,6 +1059,17 @@ dependencies = [ "kurbo", ] +[[package]] +name = "imaging_examples" +version = "0.0.1" +dependencies = [ + "image", + "imaging", + "imaging_vello_cpu", + "kurbo", + "peniko", +] + [[package]] name = "imaging_skia" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index a514b43..acc5d22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "imaging", "imaging_conformance", + "imaging_examples", "imaging_skia", "imaging_snapshot_tests", "imaging_tiny_skia", diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1ae5f4 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# imaging + +`imaging` is a small Rust command stream for 2D drawing. It is for code that wants to describe +drawing without choosing the final renderer too early. + +You can stream commands directly into a backend, or record them into a retained scene for +validation, replay, snapshots, and tests. + +This is not a finished graphics stack. It is the shared drawing vocabulary and recording layer used +by the backends in this workspace. + +## How To Read It + +- Use `Painter` to author drawing commands. +- Implement or use a `PaintSink` to receive those commands. +- Use `record::Scene` when you want an owned recording. +- Add a backend crate when you want pixels or renderer integration. + +If you are new to the project, start with the examples crate: + +```sh +cargo run -p imaging_examples --example hello_scene +``` + +To render a PNG through the Vello CPU backend: + +```sh +cargo run -p imaging_examples --example render_png_vello_cpu -- out.png +``` + +The longer path through the concepts is in the `imaging::guide` rustdoc pages: + +```sh +cargo doc -p imaging --open +``` + +## Where It Fits + +`imaging` is useful when application or toolkit code wants to describe drawing once and send it to +different targets: a retained scene, a CPU renderer, a GPU renderer, snapshot tests, diagnostics, or +adapter code for another vector format. + +It is especially relevant to people working on UI/toolkit rendering, SVG or Velato integration, +backend conformance, and Vello experiments. + +## Crates + +- `imaging`: `no_std` core `Painter`, `PaintSink`, `record::Scene`, validation, diagnostics, + and replay. +- `imaging_conformance`: shared backend conformance checks. +- `imaging_examples`: small examples for learning the core model. +- `imaging_skia`: Skia backend. +- `imaging_snapshot_tests`: image snapshot cases shared across backends. +- `imaging_tiny_skia`: CPU renderer based on `tiny-skia`. +- `imaging_vello`: Vello backend. +- `imaging_vello_cpu`: Vello CPU backend; start here if you want pixels quickly. +- `imaging_vello_hybrid`: Vello sparse/hybrid backend using `wgpu`. +- `imaging_wgpu`: shared texture-renderer traits and `wgpu` target glue. +- `imaging_wind_tunnel`: benchmark and measurement crate. +- `svg_imaging`: SVG lowering into `imaging`. +- `velato_imaging`: Velato lowering into `imaging`. + +## Current Shape + +The core crate is intentionally small and still experimental. The names and crate boundaries may +change while the project settles. + +What is useful today: + +- recording command streams with `record::Scene` +- validating and diagnosing recorded scenes +- replaying scenes into another sink +- rendering through backend crates for tests and experiments +- using snapshot tests to compare backend behavior + +What is still rough: + +- backend feature coverage differs +- GPU backends need platform/device setup +- this is not a full tutorial book or stable application API yet diff --git a/imaging/README.md b/imaging/README.md index 2d3874e..4ac011e 100644 --- a/imaging/README.md +++ b/imaging/README.md @@ -4,6 +4,9 @@ Backend-agnostic 2D imaging recording + streaming API. This crate is `no_std` by default (uses `alloc`); enable the `std` feature when needed. +If you are reading this on docs.rs, start with the `guide` module for the project-level learning +path, backend map, and mental model. + ## API shape `imaging` has two primary public layers: diff --git a/imaging/src/guide/backends.md b/imaging/src/guide/backends.md new file mode 100644 index 0000000..05d626e --- /dev/null +++ b/imaging/src/guide/backends.md @@ -0,0 +1,77 @@ +# Backend Map + +Start with `imaging_vello_cpu` if you are learning the crate and want pixels. It is CPU-based, has +fewer setup steps than the GPU paths, and still exercises the Vello-side rendering work. + +```sh +cargo run -p imaging_examples --example render_png_vello_cpu -- out.png +``` + +This page is a map, not a feature matrix. Backend coverage is still moving. + +## Core And Tools + +`imaging` + +The core command stream and retained scene crate. Use this for [`crate::Painter`], +[`crate::PaintSink`], [`crate::record::Scene`], validation, diagnostics, and replay. + +`imaging_examples` + +Runnable examples for learning the crate. These live outside the core crate so examples can pull in +renderer and image-writing dependencies without changing the core dependency surface. + +`imaging_conformance` + +Contract tests for backend behavior at the command-stream level. + +`imaging_snapshot_tests` + +Shared visual cases and backend snapshot tests. + +`imaging_wind_tunnel` + +Benchmark and measurement crate. + +## Renderers + +`imaging_vello_cpu` + +Vello CPU backend. This is the first backend to try for examples, tests, and local image output. + +`imaging_vello` + +Vello backend. Use this when working with the Vello renderer path. + +`imaging_vello_hybrid` + +Vello sparse/hybrid backend using `wgpu`. This needs a working GPU/device setup and is more +experimental than the CPU example path. + +`imaging_wgpu` + +Shared traits and target types for rendering into application-owned `wgpu` textures. + +`imaging_skia` + +Skia backend. Useful when you need Skia integration or want to compare behavior with a mature 2D +renderer. + +`imaging_tiny_skia` + +CPU renderer using `tiny-skia`. Useful for local rendering and backend comparisons. + +## Importers + +`svg_imaging` + +Adapter from SVG documents into `imaging` commands. + +`velato_imaging` + +Adapter from Velato/Lottie-style content into `imaging` commands. + +## Practical Rule + +Use [`crate::record::Scene`] while you are learning the model. Add a backend when you want pixels, +texture integration, or backend-specific behavior. diff --git a/imaging/src/guide/learning_path.md b/imaging/src/guide/learning_path.md new file mode 100644 index 0000000..95dcc97 --- /dev/null +++ b/imaging/src/guide/learning_path.md @@ -0,0 +1,85 @@ +# Learning Path + +This is the short path through `imaging`: run one example, understand where commands go, then look +at a renderer. It is not meant to be a book or a full API reference. + +The examples live in the workspace `imaging_examples` crate so the core crate does not need extra +example-only dependencies. + +## Draw Something Into A Scene + +Run: + +```sh +cargo run -p imaging_examples --example hello_scene +``` + +This creates a [`crate::record::Scene`], wraps it in a [`crate::Painter`], draws a rectangle, +validates the scene, and prints a few facts about the recording. + +The useful part is the shape of the code. Most callers should author drawing through +[`crate::Painter`]. The target can be a retained scene, a backend, a validator, or a small tool that +just observes commands. + +## Look At `PaintSink` + +Run: + +```sh +cargo run -p imaging_examples --example counting_sink +``` + +This example implements a small [`crate::PaintSink`] that counts commands instead of rendering +pixels. + +That is the core split in the crate: [`crate::Painter`] emits commands, and [`crate::PaintSink`] +receives them. A renderer can be a sink, but a sink does not have to be a renderer. + +## Keep A Recording + +Read: + +```text +imaging_examples/examples/hello_scene.rs +``` + +[`crate::record::Scene`] is just one sink. It stores commands so they can be validated, diagnosed, +replayed, compared in tests, or rendered later. + +That retained scene is backend-agnostic data. It is deliberately not a renderer. + +## Replay A Scene + +Run: + +```sh +cargo run -p imaging_examples --example replay_scene +``` + +This records one scene, validates it, replays it into another scene, and checks that the recordings +match. + +Replay is the bridge from retained data back into a sink. Once you can replay a scene, the same +recording can feed another retained scene, a backend, diagnostics, or a test harness. + +## Render A PNG + +Run: + +```sh +cargo run -p imaging_examples --example render_png_vello_cpu -- out.png +``` + +This renders a retained scene through `imaging_vello_cpu` and writes a PNG. + +Use this example when you want the quickest visible result. It keeps the rendering path CPU-based +while still exercising the Vello-side backend work. + +## Where To Go Next + +For visual comparison work, look at the `imaging_snapshot_tests` crate. It holds shared visual cases +and backend-specific snapshot tests. + +For source labels and diagnostic context, search the crate docs for [`crate::Painter::with_context`] +and [`crate::with_context!`]. Context is useful when a retained scene needs to explain where a +command came from in application code. diff --git a/imaging/src/guide/mental_model.md b/imaging/src/guide/mental_model.md new file mode 100644 index 0000000..a9472b8 --- /dev/null +++ b/imaging/src/guide/mental_model.md @@ -0,0 +1,71 @@ +# Mental Model + +`imaging` is a command stream for 2D drawing. The core crate gives names to the commands and defines +how they move through the system; backend crates decide how those commands become pixels or renderer +state. + +```text +Application code + | + v + Painter + | + v + PaintSink trait + | + +--> record::Scene + +--> Vello CPU backend + +--> Vello backend + +--> validation / diagnostics +``` + +[`crate::Painter`] is the authoring helper. It is the API most drawing code should use. + +[`crate::PaintSink`] is the receiving side. It accepts borrowed drawing commands. + +[`crate::record::Scene`] is an owned sink. It stores commands so they can be kept, checked, +replayed, or rendered later. + +Backends either receive commands as sinks or consume scenes through rendering traits. The exact +shape depends on the backend crate. + +## Streaming And Retained Paths + +The streaming path is: + +```text +Painter -> PaintSink +``` + +Use this when the caller can draw directly into the target. A sink might count commands, build a +backend-native scene, validate the stream, or render. + +The retained path is: + +```text +Painter -> record::Scene -> validate / diagnose / replay / render +``` + +Use this when the caller needs owned drawing data. That is useful for caching, tests, snapshots, +debugging, and backend-independent storage. + +## Terms + +- [`crate::Painter`]: helper for emitting drawing commands. +- [`crate::PaintSink`]: trait that receives borrowed drawing commands. +- [`crate::record::Scene`]: owned retained command stream. +- Validation: structural checks for balanced clips, groups, contexts, and referenced data. +- Diagnostics: warnings about suspicious but valid drawing. +- Replay: sending a retained scene into another sink. + +## A Few Edges To Know + +Backend support is uneven. A scene can be valid and still use a feature a particular backend does +not support yet. + +[`crate::record::Scene::validate`] checks structure, not whether the result is visually what you +intended. + +Diagnostics are advisory. They are meant to catch likely mistakes without rejecting valid scenes. + +The core crate is pre-1.0 and intentionally small, so names and boundaries may still change. diff --git a/imaging/src/lib.rs b/imaging/src/lib.rs index 277f3e5..2b9981b 100644 --- a/imaging/src/lib.rs +++ b/imaging/src/lib.rs @@ -21,58 +21,27 @@ //! retained payloads. //! //! ```rust -//! use imaging::{ -//! BlurredRoundedRect, ClipRef, FillRef, GlyphRunRef, GroupRef, PaintSink, Painter, StrokeRef, -//! }; +//! use imaging::{record::Scene, Painter}; //! use kurbo::Rect; //! use peniko::Color; //! -//! #[derive(Default)] -//! struct CountingSink { -//! fills: usize, -//! clips: usize, -//! } -//! -//! impl PaintSink for CountingSink { -//! fn push_clip(&mut self, _clip: ClipRef<'_>) { -//! self.clips += 1; -//! } -//! -//! fn pop_clip(&mut self) {} -//! -//! fn push_group(&mut self, _group: GroupRef<'_>) {} -//! -//! fn pop_group(&mut self) {} -//! -//! fn fill(&mut self, _draw: FillRef<'_>) { -//! self.fills += 1; -//! } -//! -//! fn stroke(&mut self, _draw: StrokeRef<'_>) {} -//! -//! fn glyph_run( -//! &mut self, -//! _draw: GlyphRunRef<'_>, -//! _glyphs: &mut dyn Iterator, -//! ) {} -//! -//! fn blurred_rounded_rect(&mut self, _draw: BlurredRoundedRect) {} -//! } -//! -//! let mut sink = CountingSink::default(); +//! let mut scene = Scene::new(); //! //! { -//! let mut painter = Painter::new(&mut sink); +//! let mut painter = Painter::new(&mut scene); //! painter.fill_rect(Rect::new(0.0, 0.0, 64.0, 64.0), Color::from_rgb8(0x2a, 0x6f, 0xdb)); //! painter.with_fill_clip(Rect::new(8.0, 8.0, 56.0, 56.0), |p| { //! p.fill_rect(Rect::new(16.0, 16.0, 48.0, 48.0), Color::from_rgb8(0x2a, 0x6f, 0xdb)); //! }); //! } //! -//! assert_eq!(sink.fills, 2); -//! assert_eq!(sink.clips, 1); +//! scene.validate()?; +//! # Ok::<(), imaging::record::ValidateError>(()) //! ``` //! +//! For a complete custom [`PaintSink`] implementation, see +//! `imaging_examples/examples/counting_sink.rs`. +//! //! # Recording //! //! Use [`record::Scene`] when you want an owned, backend-agnostic recording you can retain, @@ -103,6 +72,8 @@ //! Low-level retained payloads like [`record::Draw`], [`record::Clip`], and [`record::Group`] are //! also public under [`record`] when you need exact control over the recorded representation. //! +//! If you are new to the crate, start with [`guide::learning_path`]. +//! //! The API is intentionally small and experimental; expect breaking changes while we iterate. #![no_std] @@ -114,6 +85,22 @@ use kurbo::{Affine, Rect}; use peniko::BlendMode; pub mod diagnostics; +#[cfg(doc)] +pub mod guide { + //! Short guides for learning the crate. + //! + //! These pages are built for rustdoc and docs.rs. They keep the conceptual material close to + //! the public API without adding anything to normal builds. + + #[doc = include_str!("guide/learning_path.md")] + pub mod learning_path {} + + #[doc = include_str!("guide/mental_model.md")] + pub mod mental_model {} + + #[doc = include_str!("guide/backends.md")] + pub mod backends {} +} mod paint; mod painter; pub mod record; diff --git a/imaging_examples/Cargo.toml b/imaging_examples/Cargo.toml new file mode 100644 index 0000000..4c918ac --- /dev/null +++ b/imaging_examples/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "imaging_examples" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Small learning examples for the imaging command stream." +publish = false + +[dependencies] +image = { features = ["png"], workspace = true } +imaging = { features = ["std"], workspace = true } +imaging_vello_cpu = { features = ["std"], workspace = true } +kurbo = { default-features = true, workspace = true } +peniko = { default-features = true, workspace = true } + +[lints] +workspace = true diff --git a/imaging_examples/examples/counting_sink.rs b/imaging_examples/examples/counting_sink.rs new file mode 100644 index 0000000..6f7dbaf --- /dev/null +++ b/imaging_examples/examples/counting_sink.rs @@ -0,0 +1,71 @@ +// Copyright 2026 the Imaging Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Count commands to show that `PaintSink` is only a command target. + +use imaging::{ + BlurredRoundedRect, ClipRef, FillRef, GlyphRunRef, GroupRef, PaintSink, Painter, StrokeRef, +}; +use kurbo::Rect; +use peniko::Color; + +#[derive(Default, Debug)] +struct CountingSink { + clips: usize, + fills: usize, + groups: usize, + glyph_runs: usize, + strokes: usize, + blurred_rounded_rects: usize, +} + +impl PaintSink for CountingSink { + fn push_clip(&mut self, _clip: ClipRef<'_>) { + self.clips += 1; + } + + fn pop_clip(&mut self) {} + + fn push_group(&mut self, _group: GroupRef<'_>) { + self.groups += 1; + } + + fn pop_group(&mut self) {} + + fn fill(&mut self, _draw: FillRef<'_>) { + self.fills += 1; + } + + fn stroke(&mut self, _draw: StrokeRef<'_>) { + self.strokes += 1; + } + + fn glyph_run( + &mut self, + _draw: GlyphRunRef<'_>, + _glyphs: &mut dyn Iterator, + ) { + self.glyph_runs += 1; + } + + fn blurred_rounded_rect(&mut self, _draw: BlurredRoundedRect) { + self.blurred_rounded_rects += 1; + } +} + +fn main() { + let mut sink = CountingSink::default(); + + { + let mut painter = Painter::new(&mut sink); + painter.fill_rect(Rect::new(0.0, 0.0, 128.0, 80.0), Color::WHITE); + painter.with_fill_clip(Rect::new(16.0, 16.0, 112.0, 64.0), |painter| { + painter.fill_rect( + Rect::new(28.0, 24.0, 100.0, 56.0), + Color::from_rgb8(0x2a, 0x6f, 0xdb), + ); + }); + } + + println!("{sink:#?}"); +} diff --git a/imaging_examples/examples/hello_scene.rs b/imaging_examples/examples/hello_scene.rs new file mode 100644 index 0000000..5b2c984 --- /dev/null +++ b/imaging_examples/examples/hello_scene.rs @@ -0,0 +1,34 @@ +// Copyright 2026 the Imaging Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Build the smallest useful retained scene. + +use imaging::{Painter, record::Scene}; +use kurbo::{Rect, RoundedRect}; +use peniko::Color; + +fn main() -> Result<(), Box> { + let mut scene = Scene::new(); + + { + let mut painter = Painter::new(&mut scene); + painter.fill_rect(Rect::new(0.0, 0.0, 160.0, 96.0), Color::WHITE); + painter + .fill( + RoundedRect::new(24.0, 20.0, 136.0, 76.0, 10.0), + Color::from_rgb8(0x2a, 0x6f, 0xdb), + ) + .draw(); + } + + scene.validate()?; + let diagnostics = scene.diagnose(); + + println!("commands: {}", scene.commands().len()); + println!("diagnostics: {}", diagnostics.len()); + if !diagnostics.is_empty() { + println!("{diagnostics:#?}"); + } + + Ok(()) +} diff --git a/imaging_examples/examples/render_png_vello_cpu.rs b/imaging_examples/examples/render_png_vello_cpu.rs new file mode 100644 index 0000000..47eb27a --- /dev/null +++ b/imaging_examples/examples/render_png_vello_cpu.rs @@ -0,0 +1,59 @@ +// Copyright 2026 the Imaging Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Render a retained scene to a PNG with the Vello CPU backend. + +use std::{fs::File, path::PathBuf}; + +use image::{ColorType, ImageEncoder, codecs::png::PngEncoder}; +use imaging::{Painter, record::Scene}; +use imaging_vello_cpu::VelloCpuRenderer; +use kurbo::{Rect, RoundedRect}; +use peniko::Color; + +const WIDTH: u16 = 320; +const HEIGHT: u16 = 180; + +fn main() -> Result<(), Box> { + let output = std::env::args_os() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("out.png")); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.fill_rect( + Rect::new(0.0, 0.0, f64::from(WIDTH), f64::from(HEIGHT)), + Color::from_rgb8(0xf6, 0xf7, 0xfb), + ); + painter + .fill( + RoundedRect::new(48.0, 42.0, 272.0, 138.0, 18.0), + Color::from_rgb8(0x1d, 0x4e, 0x89), + ) + .draw(); + painter + .fill( + RoundedRect::new(72.0, 66.0, 248.0, 114.0, 10.0), + Color::from_rgba8(0xff, 0xff, 0xff, 0x60), + ) + .draw(); + } + + scene.validate()?; + + let mut renderer = VelloCpuRenderer::new(WIDTH, HEIGHT); + let image = renderer.render_scene(&scene, WIDTH, HEIGHT)?; + + let file = File::create(&output)?; + PngEncoder::new(file).write_image( + &image.data, + image.width, + image.height, + ColorType::Rgba8.into(), + )?; + println!("wrote {}", output.display()); + + Ok(()) +} diff --git a/imaging_examples/examples/replay_scene.rs b/imaging_examples/examples/replay_scene.rs new file mode 100644 index 0000000..b8f1e72 --- /dev/null +++ b/imaging_examples/examples/replay_scene.rs @@ -0,0 +1,36 @@ +// Copyright 2026 the Imaging Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Replay a retained scene into another retained scene. + +use imaging::{Painter, record}; +use kurbo::Rect; +use peniko::Color; + +fn main() -> Result<(), Box> { + let mut source = record::Scene::new(); + + { + let mut painter = Painter::new(&mut source); + painter.fill_rect(Rect::new(0.0, 0.0, 96.0, 96.0), Color::WHITE); + painter.with_context("replay-demo/card", None, |painter| { + painter.fill_rect( + Rect::new(16.0, 16.0, 80.0, 80.0), + Color::from_rgb8(0xd9, 0x77, 0x06), + ); + }); + } + + source.validate()?; + + let mut replayed = record::Scene::new(); + record::replay(&source, &mut replayed); + + assert_eq!( + source, replayed, + "replay should preserve the recorded scene" + ); + println!("replayed {} commands", replayed.commands().len()); + + Ok(()) +} diff --git a/imaging_examples/src/lib.rs b/imaging_examples/src/lib.rs new file mode 100644 index 0000000..3cfeb11 --- /dev/null +++ b/imaging_examples/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2026 the Imaging Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Small runnable examples for learning the `imaging` command stream.