Skip to content

Architecture

Randy Zwitch edited this page Sep 15, 2026 · 3 revisions

canvas_mojo architecture

Where to start, which of the package's drawing paths fits what you are building, and the rendering model underneath them. For what was built and why, see Changelog; for what was deliberately not, see Backlog.

flowchart LR
    Start(["canvas"]) --> Pick{"Pixels, markup or pages?"}

    Pick -->|"raster"| Canvas["Canvas(width, height, fill)"]
    Pick -->|"vector"| Svg["SvgCanvas(width, height)"]
    Pick -->|"document"| Pdf["PdfCanvas(width, height)"]

    Canvas --> Draw
    Svg --> Draw
    Pdf --> Draw

    subgraph Draw["Draw — the same DrawTarget methods on any backend"]
        direction TB
        Prim["Shape primitives<br/>fill_rect, fill_circle_aa, fill_arc_aa,<br/>draw_line_aa, fill_circles_aa, fill_mesh …"]
        PathAPI["Path<br/>move_to → line_to / curve_to / arc_to<br/>→ fill_path_aa / stroke_path_aa"]
        Text["draw_text / stroke_text / draw_text_on_path<br/>(native font discovery → native TTF/CFF parser<br/>→ glyph rasterization)"]
        Grad["Gradients and patterns<br/>(paint source for either)"]
        State["save / restore, transforms, clips,<br/>blend modes, color space, begin_batch"]

        Text --> PathAPI
        Grad -.->|"paint"| Prim
        Grad -.->|"paint"| PathAPI
    end

    Img["read_png / read_bmp / read_jpeg<br/>→ a Canvas to compose or place"] --> Canvas
    Canvas --> Png["write_png / write_bmp"]
    Svg --> Str["to_string() / write_svg"]
    Pdf --> File["write_pdf<br/>→ pages, fonts and images embedded"]
Loading

Three backends, one interface

Canvas (canvas/buffer.mojo) rasterizes into an in-memory RGBA buffer with straight alpha. SvgCanvas (canvas/vector/svg.mojo) accumulates SVG markup, and PdfCanvas (canvas/vector/pdf.mojo) accumulates PDF page content with embedded font subsets and images; neither does any anti-aliasing of its own, since a viewer rasterizes at whatever resolution it displays. All three implement DrawTarget (canvas/vector/draw_target.mojo). A fourth conformer, BoundsTarget (canvas/bounds.mojo), draws nothing and keeps the union of what it was asked to draw, so a scene can be measured before a real target is sized to it: render once into it, read ink_pixels(), render again translated by minus the box's corner.

The trait carries the drawing primitives a chart needs: rectangles, lines, circles, ellipses, arcs and ring sectors; bulk markers (fill_circles_aa, fill_ellipses_aa, fill_arcs_aa); meshes (fill_mesh, fill_mesh_shaded); path fill and stroke with the full stroke style; draw_image for a raster block; rectangle clips; annotated groups; begin_batch/end_batch; and the state model, which is transforms with save/restore, the blend mode and the color space. Text is deliberately not on the trait, because its representation differs by backend: Canvas rasterizes glyphs, SvgCanvas emits text elements, PdfCanvas embeds font subsets. A caller draws text through the concrete backend.

Pick Canvas for pixels, a PNG to embed or a notebook preview; SvgCanvas when the viewer should own resolution and zoom; PdfCanvas for print, multiple pages, or a document whose text must stay extractable.

Ways to put ink on any of them

Shape primitives (canvas/shapes/): one function per shape, in a hard-edged and an anti-aliased form. lines.mojo holds lines, polylines and polygon outlines with dashes, caps and joins; rects.mojo, circles.mojo, ellipses.mojo and arcs.mojo the filled and outlined shapes; polygon_fill.mojo the polygon fills; mesh.mojo the seam-free mesh. See examples/circles.mojo, examples/arc.mojo, examples/mesh.mojo.

Path (canvas/path.mojo): arbitrary geometry from move_to, line_to, quad_curve_to, cubic_curve_to, arc_to and close, then fill_path_aa under either fill rule, stroke_path_aa, or the gradient and pattern fills. Paths can also be transformed, measured, and hit-tested with in_fill and in_stroke. See examples/path.mojo and examples/fill_rule.mojo.

Text (canvas/text/): draw_text, stroke_text, draw_text_on_path, measure_text, measure_text_block, and the prepared-layout API (prepare_text, measure_layout, draw_layout). font_discovery.mojo resolves a family, slant and weight to a font file by reading the installed fonts' own tables; ttf.mojo and cff.mojo parse TrueType and OpenType outlines, metrics and CBDT color bitmaps; bidi.mojo and joining.mojo handle bidirectional runs and Arabic joining; font_cache.mojo is the caller-owned cache for resolved paths, parsed faces and rasterized glyph masks. A glyph outline is a Path, so text is a consumer of the path rasterizer rather than a separate drawing mechanism. See examples/text.mojo.

Paint sources (canvas/gradient.mojo, canvas/pattern.mojo): linear, radial and conic gradients and raster or hatch patterns, which the rectangle and path fills accept in place of a flat color. See examples/gradient.mojo and examples/patterns.mojo.

Compositing and effects (canvas/compose.mojo, blend.mojo, mask.mojo, blur.mojo, resize.mojo): place one canvas on another with an offset, a transform or a mask; the Porter-Duff operators and the separable and non-separable blend modes; 8-bit coverage masks; Gaussian blur and drop shadows; integer downsampling and general resizing. See examples/layers.mojo and examples/shadows.mojo.

The rendering model on Canvas

Every pixel is written once per primitive. Each rasterizer resolves coverage for a pixel and calls set_pixel once, so a translucent fill never blends over its own interior seams. Pixel (x, y) is the square centered at (x, y), and every rasterizer shares that convention, so a hard-edged and an anti-aliased fill of the same shape agree on the boundary.

Two anti-aliasing rasterizers. aa_area.mojo computes exact signed-area coverage per pixel and is what nonzero fills, strokes and paths use; aa_crossing.mojo samples a sub-pixel grid and serves the even-odd rule and the shapes whose coverage has no closed form. Disks and ellipses under a size limit use closed-form coverage instead of either. A mesh rasterizes its faces hard-edged at sixteen sub-samples per pixel with a fixed rule for which face owns a sub-sample on a shared edge, which is what makes shared edges seam-free.

Row bands across cores. A large pass splits the canvas into horizontal bands, one task each, through std.runtime.asyncrt. Bands write disjoint rows, which is the whole safety argument. Whether a pass bands at all is decided per pass from what it does per pixel: a pass that only stores switches on bytes against the cache slice canvas/machine.mojo probes from the operating system, and a pass that reads and computes bands regardless of size. Every threshold in canvas/workers.mojo and the primitives was measured on the machine that set it, and the reasoning is in the Changelog.

Batches. begin_batch records every subsequent primitive as a compact op with ranges into shared side lists rather than drawing it; end_batch builds the geometry in parallel and replays the ops band by band (canvas/batch.mojo). A scene of thousands of small shapes renders across cores as one pass instead of thousands of serial calls. Output is byte-identical to drawing the same calls at once.

Supersampled regions. begin_supersampled(factor) records, and end_supersampled replays one output band at a time into a scratch holding only that band's enlarged rows, downsampling each into the canvas. An 800x600 canvas at factor 3 never holds the 16.5 MB intermediate the two-step recipe needs, and the result is byte-identical to that recipe. A primitive with no recorded form falls back to materializing the buffer, with the output unchanged.

Straight alpha, sRGB by default. Color holds four bytes with straight alpha so get_pixel returns the color a caller recognizes. set_color_space(ColorSpace.LINEAR) moves source-over blends, gradient interpolation and mesh shading into linear light on the raster backend.

Getting the work out

Canvas writes through write_png and write_bmp (canvas/io/), and read_png, read_bmp and read_jpeg (baseline and progressive) read a file into a fresh Canvas for composing or placing. SvgCanvas returns its markup from to_string() or writes it with write_svg. PdfCanvas writes one or more pages with write_pdf; new_page() finishes the current page and starts another.

Where things live

Directory Contents
canvas/ buffer.mojo (Canvas), path.mojo, batch.mojo, gradient.mojo, pattern.mojo, compose.mojo, blend.mojo, mask.mojo, blur.mojo, resize.mojo, color.mojo, geometry.mojo, machine.mojo, workers.mojo, the two AA rasterizers
canvas/shapes/ one module per shape family, plus mesh.mojo and dash.mojo
canvas/text/ discovery, parsing, layout, bidi, the font cache and rendering
canvas/vector/ draw_target.mojo, svg.mojo, pdf.mojo, pdf_font.mojo
canvas/io/ PNG, BMP, JPEG and the DEFLATE codec
tests/ one file per module, plus golden images and byte-identity suites
examples/ one runnable program per feature, rendered into the docs site
benchmarks/ the survey, the micro-benchmarks, the recorded reference and digests

CONTRIBUTING.md in the repository covers the conventions a change is expected to hold to, and AGENTS.md the operational rules for working on the code.

Clone this wiki locally