Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

Canvas

Canvas(w:, h:) · Draw.Pixels(buffer, w, h, dx, dy[, dw, dh]) · Draw.Clear(color) · Draw.Rect(x, y, w, h, color) · Draw.Line(x1, y1, x2, y2, color[, width]) · Draw.Circle(x, y, radius, color) · Draw.Text(text, x, y, color, size) · Draw.Image(texture|url, [sx, sy, sw, sh,] dx, dy, dw, dh)

A drawing surface, and the verbs that paint on it. Put a `Canvas` in a render block, call `Draw.*` from an `on frame` body, and the picture is redrawn every frame — a game, a visualisation, a custom chart, anything the box-shaped render vocabulary cannot express. The drawing code is ordinary Osy#: ordinary loops, ordinary helper methods, ordinary state.

preview2 examples compiled by CIuicanvasdrawinggames

Summary#

Everything else the render vocabulary draws is a box. That reaches further than it sounds — a Tetris well, a bar chart and even a textured raycaster are all boxes — but a box cannot vary within itself, so lighting that falls off down a wall, a sprite clipped by the thing in front of it, or a textured floor have no spelling.

Canvas is the surface for those. It is immediate mode: there is no scene, no retained objects and nothing to keep in sync. Each frame you clear it and draw what should be there now.

The drawing happens in Osy#, in your on frame body — not in a callback handed to a JavaScript library. That is the point of the design: a game's inner loop stays in the same language as the rest of the app.

Signature#

Canvas(w: 640, h: 400)              // the surface. `w`/`h` are the drawing BUFFER, in canvas pixels

Draw.Clear(color)                   // fill the whole canvas
Draw.Rect(x, y, w, h, color)        // a filled rectangle, from its top-left corner
Draw.Line(x1, y1, x2, y2, color)    // a 1px stroked segment
Draw.Line(x1, y1, x2, y2, color, width)
Draw.Circle(x, y, radius, color)    // a filled disc, from its CENTRE
Draw.Text(text, x, y, color, size)  // a string, from its TOP-left; `size` in canvas pixels
Draw.Image(wall, dx, dy, dw, dh)    // a TEXTURE the app ships, into a destination rectangle
Draw.Image(wall, sx, sy, sw, sh, dx, dy, dw, dh)  // a SOURCE rectangle of it, into a destination one
Draw.Image(url, …)                  // the same two forms over a runtime url — see [textures](/reference/ui/textures/)
Draw.Pixels(buffer, w, h, dx, dy)   // a w-by-h buffer of packed 0xRRGGBB colours, one canvas pixel each
Draw.Pixels(buffer, w, h, dx, dy, dw, dh)   // …STRETCHED into a destination rectangle

Description#

What goes in a frame body — clear, then draw#

A canvas keeps what you drew last time, so a frame normally starts by clearing it and then draws everything that should be visible now. State lives in the component, exactly as it does without a canvas.

[Page("/bounce")]
[AllowAnonymous]
component Bounce() {
  double x = 60;
  double y = 60;
  double vx = 210;
  double vy = 160;

  on frame (double dt) {
    x = x + vx * dt;
    y = y + vy * dt;
    if (x < 24) { x = 24; vx = 0 - vx; }
    if (x > 296) { x = 296; vx = 0 - vx; }
    if (y < 24) { y = 24; vy = 0 - vy; }
    if (y > 176) { y = 176; vy = 0 - vy; }

    Draw.Clear("#0B0616");
    Draw.Circle(x, y, 22, "#22E5FF");
  }

  render { Canvas(w: 320, h: 200); }
}

Every position is scaled by dt, so the disc crosses the surface in the same wall-clock time whatever frame rate the display runs at. See on mount / on unmount — that is a property of on frame, not of the canvas.

Coordinates are canvas pixels#

x grows right, y grows down, and the origin is the top-left corner — the convention every 2D drawing API uses. The unit is the buffer declared by Canvas(w:, h:), not CSS pixels and not a normalised space, so a loop index is an x directly:

[Page("/columns")]
[AllowAnonymous]
component Columns() {
  double t = 0;

  on frame (double dt) {
    t = t + dt;
    Draw.Clear("#0B0616");
    for (var i = 0; i < 160; i = i + 1) {
      var h = 20 + i;
      Draw.Rect(i * 2, 200 - h, 2, h, i % 2 == 0 ? "#2A1B3D" : "#3A2551");
    }
  }

  render { Canvas(w: 320, h: 200); }
}

w: and h: are the resolution, and they are required#

They set the drawing buffer — how many pixels there are to draw on — as well as the element's CSS size, so one canvas pixel is one CSS pixel by default. To show a fixed resolution larger or smaller, style the element: Canvas(w: 320, h: 200, maxW: "100%").

Both are required, and both must be written as whole-number literals. A canvas with no declared size silently gets 300×150 for its buffer while the element stretches to whatever CSS says, which renders everything blurred and in the wrong place with nothing to indicate why — so it is refused instead. And because assigning a canvas's size clears it, a size that varied with state would blank the surface at a moment nothing in the code names; a fixed resolution scaled by CSS is what you want anyway.

Text draws in the canvas's own font#

Draw.Text uses the font-family the Canvas element itself has, so Canvas(w: 640, h: 400, fontFamily: Font.Display) makes drawn text match the Text(…) beside it. y is the text's top, like every other verb's y — not its baseline.

Images load on first use#

Draw.Image starts loading a url the first time it sees one and draws nothing until it has arrived, so the first frame that names a new image skips it and every later frame has it. Cache-warm it by drawing it off-screen if the first frame matters.

The nine-argument form takes a source rectangle and a destination rectangle, which is how a sprite sheet is cut up — and how a textured wall is drawn, by stretching a one-pixel-wide slice of a texture to the wall's height.

Drawing needs a canvas in the same component#

Draw.* paints the Canvas declared by the component whose body is running. A canvas inside a nested component belongs to that component, and drawing on it from the outside is refused with a message saying so — it would otherwise appear to work until the child re-rendered.

Can a screen reader read a canvas? No#

Nothing drawn on a canvas is text, an element, or reachable by a screen reader or a keyboard. Use it for what is genuinely a picture, and put anything a reader must be able to read or press in ordinary atoms around it — a score, a legend, the controls. A canvas that is decorative needs no description; one that carries information needs the same information available another way.

How much can a frame draw?#

Measured in a real browser: roughly 30,000 draw commands per frame while holding 60fps, and around 45,000 plain arithmetic operations. That is far past a per-column raycaster and well into per-tile and per-sprite work. If a frame body gets slower than the display, the frame rate drops rather than frames queueing up.

See also#

Related

Canvas 3D

A lit, shadowed 3D scene on the same `Canvas` the 2D verbs paint. Build meshes once into fields, then each frame place…

textures

Drop `.png`, `.jpg` or `.webp` files into `model/textures/` and blit them onto a canvas with `Draw.Image(wall, …)`. The…

on mount / on unmount

`on mount { … }` runs a block ONCE, the first time a component appears — before its first paint; `on unmount { … }`…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

animation — looping motion with no destination state

An `animation` block declares reusable, looping motion — a shimmer, a pulse, an indeterminate progress hint. Its…