canvas
stable2D 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, …} Functions (29)
- Canvas.new Create a new RGBA canvas of the given size
- fill Fill the entire canvas with a solid color
- clear Reset every pixel to transparent black
- set_pixel Set a single pixel to an RGBA color
- get_pixel Read the RGBA color of a pixel
- blend_pixel Alpha-blend a color onto a pixel
- fill_rect Fill a solid rectangle
- draw_rect Draw a rectangle outline
- draw_line Draw a line between two points
- draw_circle Draw a circle outline
- fill_circle Fill a solid circle
- draw_ellipse Draw an ellipse outline
- draw_triangle Draw a triangle outline
- fill_triangle Fill a solid triangle
- draw_bezier Draw a cubic Bezier curve
- draw_text_5x7 Render text with a built-in 5x7 bitmap font
- flood_fill Flood-fill a connected region with a color
- copy_region Copy a rectangular region to another position
- resize Return a nearest-neighbor scaled copy
- flip_horizontal Mirror the canvas left-to-right in place
- flip_vertical Mirror the canvas top-to-bottom in place
- rotate_90 Return a copy rotated 90° clockwise
- rotate_180 Return a copy rotated 180°
- rotate_270 Return a copy rotated 270° clockwise
- width Get the canvas width in pixels
- height Get the canvas height in pixels
- to_bytes Get the raw RGBA pixel buffer
- to_ppm Encode the canvas as binary PPM bytes
- 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")