Grafo is a GPU-accelerated vector graphics library for Rust.
- Path rendering with cached tessellation
- Hierarchical path clipping
- Per-instance 3D and perspective transforms
- Solid fills and linear, radial, and conic gradients
- Custom WGSL shader effects on shape masks, groups, and backdrops
- Geometry-based antialiasing and MSAA
Install Grafo from crates.io or read the API documentation.
Add the following to your Cargo.toml:
[dependencies]
grafo = "0.20"
winit = "0.30"
futures = "0.3"
env_logger = "0.11"Create a shape, set its fill and transform, then render it. For a complete window, run
cargo run --example basic. The same example appears in the crate documentation.
use grafo::{Color, Shape, ShapeDrawCommandOptions};
// Set the fill when queueing the shape.
let rect = Shape::rect([(0.0, 0.0), (200.0, 100.0)]);
renderer
.add_shape(
rect,
None,
None,
ShapeDrawCommandOptions::new()
.color(Color::rgb(0, 128, 255))
.transform(grafo::TransformInstance::translation(100.0, 100.0)),
)
.unwrap();
// Call this on RedrawRequested in a winit event loop
renderer.render(&mut surface).unwrap();
renderer.clear_draw_queue();Create a surface and renderer from the same context:
use grafo::{Renderer, RendererContext, Surface};
let context = RendererContext::new().await;
let mut surface = Surface::new(&context, platform_target, size, true, false)?;
let mut renderer = Renderer::new_with_context(context, size, scale_factor, 1);
renderer.render(&mut surface)?;Pass a window or an object that provides native display and surface handles to Surface::new.
Use surface.resize(size) to resize it and surface.set_vsync(enabled) to change vsync.
render borrows the target and uses its dimensions.
For memory output, Pixmap owns the buffer and PixmapMut borrows a slice:
use grafo::{PixelFormat, PixelLayout, Pixmap, PixmapMut};
let size = (640, 480);
let mut image = Pixmap::new(size, PixelFormat::Bgra8)?;
renderer.render(&mut image)?;
let bytes = image.pixels();
let mut pixels = vec![0_u32; 640 * 480];
renderer.render(PixmapMut::argb32(&mut pixels, size)?)?;
// RGBA rows with 32 bytes of padding.
let layout = PixelLayout::new(size, PixelFormat::Rgba8, 640 * 4 + 32)?;
let mut storage = vec![0; layout.byte_len()];
let mut borrowed = PixmapMut::new(&mut storage, layout)?;
renderer.render(&mut borrowed)?;Pixmap::resize reuses buffer capacity when it can. A borrowed slice must fit its layout.
Rendering leaves row padding and trailing bytes untouched.
When rendering to memory, WGPU waits for readback. Errors leave the buffer unchanged. Rendering to a surface submits and presents the frame.
BGRA8 and RGBA8 store channels in that byte order. ARGB32 stores native-endian 0xAARRGGBB words.
RGB is premultiplied in linear space, then encoded as sRGB. Alpha stays linear.
Reuse a RendererContext to share the WGPU device, queue, and textures across windows:
use futures::executor::block_on;
use grafo::{Renderer, RendererContext, Surface};
let context = block_on(RendererContext::new());
let first_surface = Surface::new(&context, first_window, first_size, true, false)?;
let second_surface = Surface::new(&context, second_window, second_size, true, false)?;
let first_renderer = Renderer::new_with_context(
context.clone(), first_size, first_scale_factor, 1,
);
let second_renderer = Renderer::new_with_context(
context, second_size, second_scale_factor, 1,
);Create a renderer when you open a window and drop it when you close the window. Draw calls added to one renderer never appear in another renderer's draw queue.
Loaded shapes are also shared by the context. A cache_key passed to load_shape is scoped to
the RendererContext, so another renderer can reuse that shape through
add_cached_shape. Use a content-derived key when the same geometry should
be shared; loading a different shape with the same key replaces the shared entry, and
remove_shape removes it for every renderer using the context.
The second argument to add_shape and add_clipping_rect is the optional
parent_shape_id. A child is drawn inside that parent in the draw tree.
By default, parents clip their children:
use grafo::{Color, Shape, ShapeDrawCommandOptions};
let clipping_parent_id = renderer
.add_shape(
Shape::rect([(0.0, 0.0), (120.0, 80.0)]),
None,
None,
ShapeDrawCommandOptions::new().color(Color::rgb(220, 220, 220)),
)
.unwrap();
renderer
.add_shape(
Shape::rect([(80.0, 20.0), (160.0, 60.0)]),
Some(clipping_parent_id),
None,
ShapeDrawCommandOptions::new().color(Color::rgb(220, 80, 80)),
)
.unwrap();
// Let children render outside `overflow_parent_id`, while still respecting any ancestor clip.
let overflow_parent_id = renderer
.add_shape(
Shape::rect([(0.0, 0.0), (120.0, 80.0)]),
None,
None,
ShapeDrawCommandOptions::new()
.color(Color::rgb(220, 220, 220))
.clips_children(false),
)
.unwrap();
renderer
.add_shape(
Shape::rect([(80.0, 20.0), (160.0, 60.0)]),
Some(overflow_parent_id),
None,
ShapeDrawCommandOptions::new().color(Color::rgb(220, 80, 80)),
)
.unwrap();
// The same call works for ids returned by `add_clipping_rect`, so clip rectangles
// can also be used as non-clipping containers.basic.rsdraws shapes using winit 0.30'sApplicationHandler.transforms.rscovers instance transforms, color, perspective, and hit-testing.benches/visual_regression.rsbenchmarks the visual regression scene with Criterion.
Run the visual-regression benchmark in release mode:
cargo bench --bench visual_regressionThe examples directory includes hierarchical clipping, texture layers, transforms, and shader effects.
Shapes composite up to two texture layers over the instance color using premultiplied alpha.
Set the layers with ShapeDrawCommandOptions::background_texture and
ShapeDrawCommandOptions::foreground_texture. Each accepts ShapeTextureOptions
with a texture ID and fit mode. Use background_texture_id and foreground_texture_id
to set just the IDs.
Composition from bottom to top:
final = foreground + (background + color * (1 - background.a)) * (1 - foreground.a)
API:
use grafo::{Color, Renderer, Shape, ShapeDrawCommandOptions};
// After allocating textures via renderer.texture_manager()
renderer
.add_shape(
Shape::rect([(0.0, 0.0), (300.0, 200.0)]),
None,
None,
ShapeDrawCommandOptions::new()
.color(Color::rgb(40, 40, 40))
.background_texture_id(bg_tex_id)
.foreground_texture_id(fg_tex_id),
)
.unwrap();
// Transparent parts of the background texture reveal the white fill.
renderer
.add_shape(
Shape::rect([(0.0, 0.0), (300.0, 200.0)]),
None,
None,
ShapeDrawCommandOptions::new()
.color(Color::WHITE)
.background_texture_id(bg_tex_id),
)
.unwrap();See examples/multi_texture.rs for procedural background and foreground textures.
Use per-shape transforms to position shapes. Common helpers:
- Translate:
TransformInstance::translation(tx, ty) - Scale:
TransformInstance::scale(sx, sy) - Rotate around Z:
TransformInstance::rotation_z_deg(deg) - Compose:
a.multiply(&b)anda.then(&b)applyafirst, thenb.
Example:
use grafo::ShapeDrawCommandOptions;
let r = grafo::TransformInstance::rotation_z_deg(15.0);
let t = grafo::TransformInstance::translation(150.0, 80.0);
// Rotate first, then translate
renderer
.add_shape(
my_shape,
None,
None,
ShapeDrawCommandOptions::new().transform(r.then(&t)),
)
.unwrap();Documentation is available on docs.rs.
Read CONTRIBUTING.md for coding conventions and required checks.
This project is licensed under the MIT License - see the LICENSE.md file for details