Sketches
A sketch is a struct that owns its own animation state plus a draw() method called every frame. Implement the Sketch trait on your struct, then mount it into a Div tree with sketch(key, impl).
The Sketch trait
#![allow(unused)]
fn main() {
use blinc_canvas_kit::prelude::*;
use blinc_core::layer::Color;
struct Bouncer {
x: f32,
vx: f32,
}
impl Sketch for Bouncer {
fn draw(&mut self, ctx: &mut SketchContext<'_>, _t: f32, dt: f32) {
self.x += self.vx * dt;
if self.x < 0.0 || self.x + 40.0 > ctx.width {
self.vx = -self.vx;
}
let mut p = ctx.painter();
p.fill(Color::WHITE).no_stroke();
p.rect(self.x, 100.0, 40.0, 40.0);
}
}
}
The trait has two methods:
| Method | Called | Purpose |
|---|---|---|
setup(&mut self, ctx) | Once before the first draw | Asset preload, GPU upload, one-shot layout. Default: no-op. |
draw(&mut self, ctx, t, dt) | Every frame | Mutate state; emit draw calls. t = seconds since the sketch started; dt = seconds since the previous frame. |
Sketches must be Send + 'static — their state lives behind an Arc<Mutex<...>> in Blinc’s persistent state bag.
Mounting: sketch()
#![allow(unused)]
fn main() {
fn build_ui() -> impl ElementBuilder {
div()
.w(600)
.h(400)
.child(sketch("bouncer", Bouncer { x: 0.0, vx: 200.0 }))
}
}
The key identifies the sketch for state persistence. Every sketch("bouncer", ...) with the same key reuses the same persisted state across rebuilds — hot reload, layout changes, route transitions all preserve counters, particle systems, and elapsed time. Pick unique keys per instance.
Wrap the returned Div in a sized container (.w(...), .h(...), .aspect_ratio(...), or a flex parent) to control bounds. The sketch fills its parent.
SketchContext
The per-frame context exposes the canvas size, a frame counter, and three drawing entry points:
#![allow(unused)]
fn main() {
pub struct SketchContext<'a> {
pub width: f32, // Canvas width in layout units
pub height: f32, // Canvas height in layout units
pub frame_count: u64, // Frames drawn since setup()
// ...
}
}
| Method | Returns | Use for |
|---|---|---|
ctx.painter() | Painter2D<'_> | Stateful immediate-mode drawing (Processing-style) |
ctx.draw_context() | &mut dyn DrawContext | Full GPU access: gradients, glass, clips, 3D, images, text |
ctx.play(&mut player, rect, t) | () | Forward to a Player |
painter() and draw_context() each mutably borrow the underlying DrawContext — drop one before calling the other.
Painter2D
The painter holds a current fill, stroke, and transform stack so you don’t repeat those arguments on every primitive call.
Fill & stroke state
#![allow(unused)]
fn main() {
let mut p = ctx.painter();
p.fill(Color::RED).no_stroke(); // Red fill, no outline
p.rect(10.0, 10.0, 100.0, 50.0);
p.stroke(Color::BLACK, 2.0); // Add a 2px black stroke
p.circle(200.0, 200.0, 40.0);
p.no_fill().stroke(Color::BLUE, 1.0);
p.line(0.0, 0.0, 300.0, 300.0);
}
Transform stack
push() / pop() bracket grouped transforms. A single pop() undoes every transform pushed since its matching push():
#![allow(unused)]
fn main() {
p.push();
p.translate(100.0, 100.0);
p.rotate(std::f32::consts::FRAC_PI_4);
p.scale(2.0, 2.0);
p.rect(-10.0, -10.0, 20.0, 20.0); // All three transforms active
p.pop(); // All three transforms undone
}
Calling translate / rotate / scale without a surrounding push() still pushes onto the underlying stack, but pop() can’t undo them. Always use the bracketed pattern for scoped transforms.
When Painter2D’s operations aren’t enough — gradients, glass, clips, 3D, images, text — drop the painter and reach for ctx.draw_context() directly. See Canvas Drawing for the full DrawContext surface.