Skip to content

canvas

stable

2D software rasterizer providing a Canvas class with an RGBA pixel buffer, drawing primitives (lines, rects, circles, ellipses, triangles, beziers, bitmap text), flood fill, transforms, and PPM export.

use plugin canvas::{Canvas.new, fill, clear, …}
29 functions Graphics
/ filter jk navigate Esc clear
Functions (29)
  1. Canvas.new Create a new RGBA canvas of the given size
  2. fill Fill the entire canvas with a solid color
  3. clear Reset every pixel to transparent black
  4. set_pixel Set a single pixel to an RGBA color
  5. get_pixel Read the RGBA color of a pixel
  6. blend_pixel Alpha-blend a color onto a pixel
  7. fill_rect Fill a solid rectangle
  8. draw_rect Draw a rectangle outline
  9. draw_line Draw a line between two points
  10. draw_circle Draw a circle outline
  11. fill_circle Fill a solid circle
  12. draw_ellipse Draw an ellipse outline
  13. draw_triangle Draw a triangle outline
  14. fill_triangle Fill a solid triangle
  15. draw_bezier Draw a cubic Bezier curve
  16. draw_text_5x7 Render text with a built-in 5x7 bitmap font
  17. flood_fill Flood-fill a connected region with a color
  18. copy_region Copy a rectangular region to another position
  19. resize Return a nearest-neighbor scaled copy
  20. flip_horizontal Mirror the canvas left-to-right in place
  21. flip_vertical Mirror the canvas top-to-bottom in place
  22. rotate_90 Return a copy rotated 90° clockwise
  23. rotate_180 Return a copy rotated 180°
  24. rotate_270 Return a copy rotated 270° clockwise
  25. width Get the canvas width in pixels
  26. height Get the canvas height in pixels
  27. to_bytes Get the raw RGBA pixel buffer
  28. to_ppm Encode the canvas as binary PPM bytes
  29. save_ppm Write the canvas to a PPM image file

Overview

canvas is a pure-software 2D rasterizer: it draws into an in-memory RGBA pixel buffer with no GPU, windowing system, or external image library. Everything centers on the Canvas class — a stateful handle wrapping a width * height * 4 byte framebuffer that you create with Canvas.new(w, h) and then mutate with drawing calls. Colors are always four integer channels r, g, b, a in the 0-255 range, and coordinates are pixel positions with (0, 0) at the top-left.

Most methods draw onto the canvas in place and return nil; a handful (resize, rotate_90, rotate_180, rotate_270) leave the original untouched and return a brand-new Canvas. Once you have rendered a scene, export it with to_bytes (raw RGBA), to_ppm (binary PPM bytes), or save_ppm (write a PPM file). Reach for this plugin when you need deterministic, dependency-free image generation — procedural art, charts, sprites, or test fixtures — in either the native runtime or the browser playground.

Common patterns

Build a scene with a background, primitives, and a text label, then save it to a file:

use plugin canvas::{Canvas}

let cv = Canvas.new(256, 256)
cv.fill(16, 18, 32, 255)                       // dark background
cv.fill_circle(128, 120, 70, 255, 200, 0, 255) // a sun
cv.draw_rect(8, 8, 240, 240, 80, 80, 120, 255) // border frame
cv.draw_text_5x7(96, 230, "SUNSET", 230, 230, 240, 255)
cv.save_ppm("scene.ppm")
print("rendered {cv.width()}x{cv.height()} -> scene.ppm")

Outline a shape, then flood-fill its interior with a second color:

use plugin canvas::{Canvas}

let cv = Canvas.new(128, 128)
cv.fill(0, 0, 0, 255)
cv.draw_circle(64, 64, 40, 255, 255, 255, 255) // white outline
cv.flood_fill(64, 64, 220, 40, 60, 255)        // fill the inside red
let center = cv.get_pixel(64, 64)
print("center is now rgba({center["r"]}, {center["g"]}, {center["b"]}, {center["a"]})")

Draw once, then derive transformed copies without mutating the source:

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 100)
cv.fill_rect(0, 0, 100, 100, 40, 160, 220, 255)
let tall = cv.rotate_90()    // 100x200, original untouched
let thumb = cv.resize(50, 25) // nearest-neighbor downscale
print("rotated {tall.width()}x{tall.height()}, thumb {thumb.width()}x{thumb.height()}")

Playground

canvas is a built-in in the browser playground — no install or plugin directory needed. Select Canvas Graphics from the example menu (or write your own use plugin canvas::{Canvas} program) and run it; the rendered framebuffer appears in the Canvas pane beside the output console.

use plugin canvas::{Canvas}

let cv = Canvas.new(320, 240)
cv.fill(16, 18, 32, 255)
cv.draw_text_5x7(8, 9, "HELLO", 0, 220, 180, 255)
print("canvas {cv.width()}x{cv.height()}")

Create a new RGBA canvas of the given size

Creates a new canvas of width x height pixels backed by an RGBA buffer. All pixels start as transparent black (0, 0, 0, 0). Colors throughout the API are passed as four integer channels r, g, b, a in the 0-255 range.

use plugin canvas::{Canvas}

let cv = Canvas.new(320, 240)
cv.fill(20, 20, 40, 255)
print("canvas {cv.width()}x{cv.height()}")

Fill the entire canvas with a solid color

Fills the entire canvas with a single RGBA color, overwriting every pixel. Useful as a background pass before drawing.

use plugin canvas::{Canvas}

let cv = Canvas.new(100, 100)
cv.fill(255, 255, 255, 255)  // solid white background

Reset every pixel to transparent black

Resets every pixel to transparent black (0, 0, 0, 0).

Set a single pixel to an RGBA color

Sets the pixel at (x, y) to the given RGBA color, replacing whatever was there. Coordinates outside the canvas are silently ignored.

use plugin canvas::{Canvas}

let cv = Canvas.new(64, 64)
cv.set_pixel(32, 32, 255, 0, 0, 255)  // single red dot in the center

Read the RGBA color of a pixel

Reads the color at (x, y) and returns a table with r, g, b, a integer fields. Out-of-bounds coordinates return all zeros.

use plugin canvas::{Canvas}

let cv = Canvas.new(64, 64)
cv.set_pixel(10, 10, 200, 100, 50, 255)
let px = cv.get_pixel(10, 10)
print("rgba({px["r"]}, {px["g"]}, {px["b"]}, {px["a"]})")

Alpha-blend a color onto a pixel

Alpha-blends the given color onto the existing pixel at (x, y) using standard source-over compositing, instead of overwriting it like set_pixel. Use semi-transparent alpha values to layer colors.

use plugin canvas::{Canvas}

let cv = Canvas.new(64, 64)
cv.fill(0, 0, 255, 255)
cv.blend_pixel(5, 5, 255, 0, 0, 128)  // 50% red over blue

Layering several translucent dabs accumulates toward opacity:

use plugin canvas::{Canvas}

let cv = Canvas.new(32, 32)
let x = 0
while x < 5 {
  cv.blend_pixel(16, 16, 255, 255, 255, 60)  // build up brightness
  x = x + 1
}
let px = cv.get_pixel(16, 16)
print("accumulated alpha: {px["a"]}")

Fill a solid rectangle

Fills a solid w x h rectangle whose top-left corner is at (x, y). Pixels falling outside the canvas are clipped.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 200)
cv.fill_rect(20, 20, 160, 100, 30, 144, 255, 255)

Draw a rectangle outline

Draws a one-pixel rectangle outline with top-left corner at (x, y) and size w x h.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 200)
cv.draw_rect(10, 10, 180, 180, 255, 255, 0, 255)

Draw a line between two points

Draws a one-pixel line from (x1, y1) to (x2, y2) using Bresenham's algorithm.

use plugin canvas::{Canvas}

let cv = Canvas.new(100, 100)
cv.draw_line(0, 0, 99, 99, 255, 0, 0, 255)
cv.draw_line(99, 0, 0, 99, 0, 255, 0, 255)

Lines are the building block for a quick line chart:

use plugin canvas::{Canvas}

let cv = Canvas.new(120, 80)
cv.fill(245, 245, 245, 255)
let data = [10, 35, 20, 60, 45, 70]
let i = 0
while i < data.len() - 1 {
  let x1 = i * 20
  let x2 = (i + 1) * 20
  cv.draw_line(x1, 79 - data[i], x2, 79 - data[i + 1], 30, 90, 200, 255)
  i = i + 1
}

Draw a circle outline

Draws a one-pixel circle outline centered at (cx, cy) with the given radius, using the midpoint circle algorithm.

use plugin canvas::{Canvas}

let cv = Canvas.new(128, 128)
cv.draw_circle(64, 64, 50, 255, 0, 255, 255)

Fill a solid circle

Fills a solid circle centered at (cx, cy) with the given radius using horizontal scanlines.

use plugin canvas::{Canvas}

let cv = Canvas.new(128, 128)
cv.fill_circle(64, 64, 40, 255, 165, 0, 255)

Draw an ellipse outline

Draws an axis-aligned ellipse outline centered at (cx, cy) with horizontal radius rx and vertical radius ry, using the midpoint ellipse algorithm.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 120)
cv.draw_ellipse(100, 60, 80, 40, 0, 200, 200, 255)

Draw a triangle outline

Draws the outline of a triangle by connecting the three vertices with lines.

use plugin canvas::{Canvas}

let cv = Canvas.new(120, 120)
cv.draw_triangle(60, 10, 10, 110, 110, 110, 255, 255, 255, 255)

Fill a solid triangle

Fills a solid triangle defined by three vertices using scanline rasterization. Vertex order does not matter.

use plugin canvas::{Canvas}

let cv = Canvas.new(120, 120)
cv.fill_triangle(60, 10, 10, 110, 110, 110, 0, 128, 0, 255)

Draw a cubic Bezier curve

Draws a cubic Bezier curve from (x0, y0) to (x3, y3) with control points (x1, y1) and (x2, y2). The curve is approximated with 200 line segments; coordinates accept floats.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 100)
cv.draw_bezier(10.0, 90.0, 60.0, 10.0, 140.0, 10.0, 190.0, 90.0, 255, 0, 0, 255)

Render text with a built-in 5x7 bitmap font

Renders text starting at (x, y) using a built-in 5x7 pixel bitmap font, advancing 6 pixels per character. The font covers ASCII 32-90 (space, punctuation, digits, uppercase letters); lowercase letters are rendered as uppercase and unsupported characters are skipped.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 40)
cv.fill(0, 0, 0, 255)
cv.draw_text_5x7(10, 15, "HELLO ZOLO", 0, 255, 0, 255)

Because each glyph advances a fixed 6 pixels, you can compute label widths and stack lines:

use plugin canvas::{Canvas}

let cv = Canvas.new(160, 60)
cv.fill(10, 10, 20, 255)
let lines = ["SCORE: 42", "LEVEL 3"]
let row = 0
while row < lines.len() {
  cv.draw_text_5x7(6, 8 + row * 12, lines[row], 255, 220, 0, 255)
  row = row + 1
}

Flood-fill a connected region with a color

Flood-fills the 4-connected region of pixels matching the color found at the seed point (x, y), replacing them with the given color. Does nothing if the seed already has the target color or lies outside the canvas.

use plugin canvas::{Canvas}

let cv = Canvas.new(100, 100)
cv.draw_circle(50, 50, 30, 255, 255, 255, 255)
cv.flood_fill(50, 50, 255, 0, 0, 255)  // paint the inside red

Copy a rectangular region to another position

Copies the sw x sh rectangle whose top-left corner is at (sx, sy) to the destination position (dx, dy) on the same canvas. The source is buffered first, so overlapping regions copy correctly.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 100)
cv.fill_circle(40, 50, 30, 255, 0, 0, 255)
cv.copy_region(10, 20, 60, 60, 120, 20)  // duplicate the circle on the right

Return a nearest-neighbor scaled copy

Returns a new canvas of new_w x new_h pixels containing a nearest-neighbor scaled copy of this canvas. The original is left unchanged.

use plugin canvas::{Canvas}

let cv = Canvas.new(100, 100)
cv.fill(255, 0, 0, 255)
let big = cv.resize(400, 400)
print("scaled to {big.width()}x{big.height()}")

Downscaling produces a thumbnail while the full-resolution source stays available:

use plugin canvas::{Canvas}

let cv = Canvas.new(256, 256)
cv.fill(20, 20, 40, 255)
cv.fill_circle(128, 128, 90, 0, 200, 255, 255)
let thumb = cv.resize(32, 32)
thumb.save_ppm("thumb.ppm")
print("original {cv.width()}px, thumb {thumb.width()}px")

Mirror the canvas left-to-right in place

Mirrors the canvas left-to-right in place.

Mirror the canvas top-to-bottom in place

Mirrors the canvas top-to-bottom in place.

Return a copy rotated 90° clockwise

Returns a new canvas rotated 90 degrees clockwise. The original is unchanged; width and height are swapped in the result.

use plugin canvas::{Canvas}

let cv = Canvas.new(200, 100)
let rotated = cv.rotate_90()
print("{rotated.width()}x{rotated.height()}")  // 100x200

Return a copy rotated 180°

Returns a new canvas rotated 180 degrees. The original is unchanged.

Return a copy rotated 270° clockwise

Returns a new canvas rotated 270 degrees clockwise (90 degrees counter-clockwise). The original is unchanged; width and height are swapped in the result.

Get the canvas width in pixels

Returns the canvas width in pixels.

Get the canvas height in pixels

Returns the canvas height in pixels.

Get the raw RGBA pixel buffer

Returns a copy of the raw pixel buffer as bytes in row-major RGBA order (4 bytes per pixel, width * height * 4 total).

use plugin canvas::{Canvas}

let cv = Canvas.new(2, 2)
cv.fill(255, 0, 0, 255)
let raw = cv.to_bytes()
print("buffer size: {raw.len()}")  // 16

Encode the canvas as binary PPM bytes

Encodes the canvas as a binary PPM (P6) image and returns the bytes. The alpha channel is dropped, since PPM only stores RGB.

use plugin canvas::{Canvas}

let cv = Canvas.new(64, 64)
cv.fill_circle(32, 32, 20, 0, 0, 255, 255)
let ppm = cv.to_ppm()

Write the canvas to a PPM image file

Encodes the canvas as a binary PPM (P6) image and writes it to path. Errors if the file cannot be written. The alpha channel is dropped.

use plugin canvas::{Canvas}

let cv = Canvas.new(256, 256)
cv.fill(20, 20, 40, 255)
cv.fill_circle(128, 128, 80, 255, 200, 0, 255)
cv.draw_text_5x7(100, 240, "SUN", 255, 255, 255, 255)
cv.save_ppm("sun.ppm")
print("saved sun.ppm")
enespt-br