Graphics

Canvas

Draw gradient fills, layered shapes, a colour palette and a progress bar with canvas-rendered text.

Open ↗
Canvas interaction recorded with the Blinc headless renderer
Hybrid · dark · native recording · 2×

Usage and properties

These four samples adapt the original canvas_demo application. On narrow screens the drawing gallery scrolls. Drawing is configured through DrawContext, so this page has no custom CSS editor.

Use the installation guide to create a Blinc application. This widget is available from blinc_layout; the demo uses the same Hybrid theme as the component library. Create the fixture bindings once, and return the demo from your UI builder. Values marked Bound accept Signal/Computed sources where the API supports them; structural options configure the component at construction.

PropertyTypeUpdate modelThis example
drawFn(&mut dyn DrawContext, CanvasBounds)ConstructionTheme-coloured circles and strokes
width / heightf32Constructioncontainer width / 200

Keyboard and interaction

Informational content; no activation or Tab stop is required.

Customizing it

Add application CSS after the default component layer. These selectors are the restyling surface for this component:

  • .demo-canvas-examples
  • .demo-canvas-sample
  • .demo-canvas-artwork

Theme and component variables: --primary, --text-primary, --text-secondary, --surface, --border, --radius-default, --duration-fast, --ease-state.

Built-in themes

The Live theme control installs Blinc's own theme bundles, including their colours, focus rings, corner shapes and motion tokens.

pub fn theme_bundle_for(name: &str) -> blinc_theme::ThemeBundle {
    match name {
        "restrained" => blinc_theme::RestrainedTheme::bundle(),
        "expressive" => blinc_theme::ExpressiveTheme::bundle(),
        _ => blinc_theme::HybridTheme::bundle(),
    }
    .with_css(blinc_cn::cn_styles::CN_STYLES)
}

Download the default widget and component stylesheets. Sample assets and licenses are available in the asset guide.

Documentation

From the canonical component guide.

The canvas() element provides direct GPU drawing access for custom graphics, charts, and procedural content.

Basic Usage

use blinc::core::{DrawContext, Rect, Brush, Color, CornerRadius};

canvas(|ctx: &mut dyn DrawContext, bounds| {
    // bounds contains the canvas size
    ctx.fill_rect(
        Rect::new(0.0, 0.0, bounds.width, bounds.height),
        CornerRadius::uniform(8.0),
        Brush::Solid(Color::RED),
    );
})
.w(200.0)
.h(100.0)

Drawing Primitives

Filled Rectangles

ctx.fill_rect(
    Rect::new(x, y, width, height),
    CornerRadius::uniform(8.0),  // Corner radius
    Brush::Solid(Color::BLUE),
);

// No corner radius
ctx.fill_rect(
    Rect::new(10.0, 10.0, 100.0, 50.0),
    CornerRadius::default(),
    Brush::Solid(Color::GREEN),
);

Stroked Rectangles

ctx.stroke_rect(
    Rect::new(x, y, width, height),
    CornerRadius::uniform(4.0),
    2.0,  // Stroke width
    Brush::Solid(Color::WHITE),
);

Circles

// Filled circle
ctx.fill_circle(
    Point::new(cx, cy),  // Center
    radius,
    Brush::Solid(Color::BLUE),
);

// Stroked circle
ctx.stroke_circle(
    Point::new(cx, cy),
    radius,
    2.0,  // Stroke width
    Brush::Solid(Color::WHITE),
);

Text

use blinc::core::TextStyle;

ctx.draw_text(
    "Hello, Canvas!",
    Point::new(x, y),
    &TextStyle::new(16.0).with_color(Color::WHITE),
);

Gradients

use blinc::core::{Gradient, GradientStop, Point};

// Linear gradient
let gradient = Brush::Gradient(Gradient::linear(
    Point::new(0.0, 0.0),      // Start
    Point::new(200.0, 0.0),    // End
    Color::rgba(0.9, 0.2, 0.5, 1.0),
    Color::rgba(0.2, 0.8, 0.6, 1.0),
));

ctx.fill_rect(
    Rect::new(0.0, 0.0, 200.0, 100.0),
    CornerRadius::default(),
    gradient,
);

// Multi-stop gradient
let gradient = Brush::Gradient(Gradient::linear_with_stops(
    Point::new(0.0, 0.0),
    Point::new(200.0, 0.0),
    vec![
        GradientStop::new(0.0, Color::RED),
        GradientStop::new(0.5, Color::YELLOW),
        GradientStop::new(1.0, Color::GREEN),
    ],
));

Transforms

use blinc::core::Transform;

// Push transform
ctx.push_transform(Transform::translate(50.0, 50.0));

// Draw in transformed space
ctx.fill_rect(/* ... */);

// Pop transform
ctx.pop_transform();

// Rotation
ctx.push_transform(Transform::rotate(angle_radians));
// ... draw ...
ctx.pop_transform();

// Scale
ctx.push_transform(Transform::scale(2.0, 2.0));
// ... draw ...
ctx.pop_transform();

Clipping

// Push clip region
ctx.push_clip(Rect::new(10.0, 10.0, 100.0, 100.0));

// Only content within clip region is visible
ctx.fill_rect(/* ... */);

// Pop clip
ctx.pop_clip();

Example: Animated Spinner

use std::f32::consts::PI;

fn spinner(ctx: &WindowedContext) -> impl ElementBuilder {
    let timeline = ctx.use_animated_timeline();

    let entry_id = timeline.lock().unwrap().configure(|t| {
        let id = t.add(0, 1000, 0.0, 360.0);
        t.set_loop(-1);
        t.start();
        id
    });

    let render_timeline = Arc::clone(&timeline);

    canvas(move |draw_ctx, bounds| {
        let angle_deg = render_timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);
        let angle_rad = angle_deg * PI / 180.0;

        let cx = bounds.width / 2.0;
        let cy = bounds.height / 2.0;
        let radius = 30.0;

        // Draw spinning segments
        for i in 0..8 {
            let segment_angle = angle_rad + (i as f32 * PI / 4.0);
            let alpha = 1.0 - (i as f32 * 0.1);

            let x = cx + segment_angle.cos() * radius;
            let y = cy + segment_angle.sin() * radius;

            draw_ctx.fill_circle(
                Point::new(x, y),
                4.0,
                Brush::Solid(Color::rgba(0.4, 0.6, 1.0, alpha)),
            );
        }
    })
    .w(80.0)
    .h(80.0)
}

Example: Progress Ring

fn progress_ring(progress: f32) -> impl ElementBuilder {
    canvas(move |ctx, bounds| {
        let cx = bounds.width / 2.0;
        let cy = bounds.height / 2.0;
        let radius = bounds.width.min(bounds.height) / 2.0 - 4.0;

        // Background ring
        ctx.stroke_circle(
            Point::new(cx, cy),
            radius,
            4.0,
            Brush::Solid(Color::rgba(0.2, 0.2, 0.25, 1.0)),
        );

        // Progress arc (simplified - actual arc drawing would need path API)
        // For now, draw segments
        let segments = 32;
        let filled = (segments as f32 * progress) as i32;

        for i in 0..filled {
            let angle = (i as f32 / segments as f32) * 2.0 * PI - PI / 2.0;
            let x = cx + angle.cos() * radius;
            let y = cy + angle.sin() * radius;

            ctx.fill_circle(
                Point::new(x, y),
                3.0,
                Brush::Solid(Color::rgba(0.4, 0.6, 1.0, 1.0)),
            );
        }

        // Center text
        ctx.draw_text(
            &format!("{}%", (progress * 100.0) as i32),
            Point::new(cx - 15.0, cy + 6.0),
            &TextStyle::new(16.0).with_color(Color::WHITE),
        );
    })
    .w(80.0)
    .h(80.0)
}

Example: Color Palette

fn color_palette() -> impl ElementBuilder {
    canvas(|ctx, bounds| {
        let cols = 8;
        let rows = 3;
        let cell_w = bounds.width / cols as f32;
        let cell_h = bounds.height / rows as f32;

        for row in 0..rows {
            for col in 0..cols {
                let hue = col as f32 / cols as f32;
                let sat = 1.0 - (row as f32 * 0.25);
                let color = hsv_to_rgb(hue, sat, 0.9);

                ctx.fill_rect(
                    Rect::new(
                        col as f32 * cell_w,
                        row as f32 * cell_h,
                        cell_w - 2.0,
                        cell_h - 2.0,
                    ),
                    CornerRadius::uniform(4.0),
                    Brush::Solid(color),
                );
            }
        }
    })
    .w(240.0)
    .h(90.0)
}

Best Practices

  1. Set explicit size - Canvas needs width and height to render.

  2. Use bounds parameter - Draw relative to bounds.width and bounds.height.

  3. Clone Arcs for closures - Animation values need Arc::clone() before the render closure.

  4. Push/pop transforms - Always pop what you push to avoid state leaks.

  5. Prefer elements when possible - Use div(), text() for standard UI; canvas for custom graphics.

Platforms and version

The component API is available in Blinc 0.6.0. The Live example runs in a dedicated worker and requires a compatible GPU surface. Preview and Code remain available when Live cannot start. The canvas has a descriptive label; a full screen-reader DOM mirror is not provided. The gallery integration is based on revision e2e55c24e078.

Recordings and live demos use the default Hybrid theme with bundled Noto Sans. The recording uses a 680 × 480 logical viewport at twice that resolution. Live follows its container and actual display density. Source base revision · Exact build source · Artifact versions and checksums · Font license.