Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne 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) · Draw.Push() · Draw.Translate(x, y) · Draw.Rotate(radians) · Draw.Scale(sx, sy) · Draw.Pop() · Draw.Reset() · Draw.Surface(w, h) · Draw.Into(surface) · Draw.Screen()

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

Draw.Push()                         // save the current transform
Draw.Pop()                          // put back the one the matching Push saved
Draw.Translate(x, y)                // move the origin
Draw.Rotate(radians)                // turn everything drawn after it, about the origin
Draw.Scale(sx, sy)                  // resize it; a NEGATIVE factor mirrors
Draw.Reset()                        // back to plain canvas coordinates, stack and all

Gradient g = Draw.Gradient(x0, y0, x1, y1, colorA, colorB)   // a LINEAR fill, running between two points
Gradient r = Draw.Radial(x, y, radius, colorA, colorB)      // a RADIAL fill, running out from a centre
Draw.Rect(0, 0, 640, 420, g)        // a gradient goes wherever a colour goes

var sky = Draw.Surface(640, 420)    // an OFFSCREEN surface — hold it in a field, build it once
Draw.Into(sky)                      // …the verbs above now paint on it
Draw.Screen()                       // …and back to the component's Canvas
Draw.Image(sky, dx, dy, dw, dh)     // blit it, with the same two forms a texture takes

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); }
}

Rotating, moving and scaling what you draw#

Every shape above is drawn axis-aligned: Draw.Rect takes a rectangle and Draw.Image takes a destination rectangle, so neither can be turned. The transform verbs are how anything points somewhere.

The idea is that you draw a thing once, in its own coordinates — at the origin, facing along +x — and then say where it is and which way it faces:

Draw.Push();                 // remember where we were
Draw.Translate(ship.X, ship.Y);
Draw.Rotate(ship.Heading);   // RADIANS, like Math.Cos / Math.Sin
Draw.Line(14, 0, -10, -9, "#22E5FF", 2);     // the ship, at the origin, pointing along +x
Draw.Line(14, 0, -10, 9, "#22E5FF", 2);
Draw.Line(-10, -9, -10, 9, "#22E5FF", 2);
Draw.Pop();                  // and back

Draw.Scale(sx, sy) resizes the same way, which lets one unit-sized shape serve every size of a thing; a negative factor mirrors, so Draw.Scale(-1, 1) is how a sprite faces left.

The verbs compose: a translate inside a translate is relative to the outer one, which is what lets a turret sit on a tank and turn independently, with each drawn in its own frame.

⚠️ Angles are radians. Same as Draw.Mesh's rotations, and same as Math.Sin / Math.Cos — which is what you are usually holding anyway. From degrees: deg * Math.Pi / 180.0.

Every frame starts clean

The transform is reset before each frame body runs. That is worth knowing for two reasons:

  • A Draw.Push() you forget to Draw.Pop() is wrong for that frame only. Without the reset the world would drift a little further every frame, and the symptom — a slow rotation over half a minute — looks like a bug in your own maths rather than a missing line. (You are also told, once, when a frame body leaves one unpopped.)
  • A camera can be a single line. Draw.Translate(-camX, -camY) at the top of the body, with no push and no pop, is the whole of it — because the next frame does not inherit it.

Draw.Reset() does the same thing mid-frame: it drops every transform and every push, which is how a HUD gets nailed to the screen over a world that is moving.

Two verbs that do not follow the transform

  • Draw.Clear always clears the whole canvas, whatever the transform is. Clearing "the visible rectangle, moved" is never what anyone means, and the leftovers would read as smearing.
  • Draw.Pixels, in its unscaled form, lands in plain canvas coordinates — the browser's pixel blit ignores transforms by specification. The stretched form (dw, dh) goes through the image path and does follow them. You are told once if you blit under a transform.
  void PaintShip() {
    Draw.Push();
    Draw.Translate(sx, sy);
    Draw.Rotate(heading);
    Draw.Line(14, 0, -10, -9, "#22E5FF", 2);
    Draw.Line(14, 0, -10, 9, "#22E5FF", 2);
    Draw.Line(-10, -9, -10, 9, "#22E5FF", 2);
    // The exhaust flickers, and it is drawn in the SAME frame as the hull — so it stays glued to the tail however
    // the ship is turning, with no second angle to keep in step.
    if (thrusting) {
      var flame = 8.0 + Math.Sin(spin * 40.0) * 4.0;
      Draw.Line(-10, -4, -10 - flame, 0, "#FF2D95", 2);
      Draw.Line(-10, 4, -10 - flame, 0, "#FF2D95", 2);
    }
    Draw.Pop();
  }

Gradients are values, not verbs#

Draw.Gradient and Draw.Radial do not draw anything. They answer a Gradient — a fill you then pass wherever a colour string goes:

Gradient sky;

on frame (double dt) {
  if (sky == null) {
    sky = Draw.Gradient(0, 0, 0, 430, "#4d8fd6", "#a9d2f2", "#ffeccd");
  }
  Draw.Rect(0, 0, 1280, 430, sky);
}

That is why there is no Draw.GradientRect. A gradient is accepted by Draw.Clear, Draw.Rect, Draw.Circle, Draw.Line and Draw.Text alike, and by anything added later — because the fill is a value rather than a second version of every verb.

Two or more colours, spaced evenly along the run. Draw.Gradient runs between the two points you give, so (0, 0, 0, 430) is vertical and (0, 0, 640, 0) is horizontal; Draw.Radial runs from a centre outward, which is what a sun, a glow or a vignette wants. A colour with alpha ("rgba(255,238,190,0)") is how a glow fades into what is behind it.

Build it once. A gradient is fixed in canvas pixels, so hold it in a Gradient field — as above, or in on mount — rather than rebuilding it every frame.

Painting something once — offscreen surfaces#

A canvas keeps nothing between frames, so everything on screen is redrawn every frame — including the parts that never change. A starfield of 160 stars is 160 draw commands, which is 9,600 a second for a picture that is identical each time.

A surface is an offscreen canvas you paint once and then blit:

var sky = Draw.Surface(640, 420);     // a field: built once, used every frame
bool painted = false;

on frame (double dt) {
  if (!painted) {
    Draw.Into(sky);                   // every verb from here paints on `sky`…
    Draw.Clear("#0B0616");            // …including Clear, which clears the SURFACE
    PaintStars();
    Draw.Screen();                    // …and back to the canvas
    painted = true;
  }
  Draw.Image(sky, 0, 0, 640, 420);    // one command instead of a hundred and sixtythe moving things…
}

Draw.Image takes a surface exactly as it takes a texture, in both the whole-image and source-rectangle forms — so switching a background from shipped art to something you rendered is a change of value, not of call site.

The other thing surfaces buy is a pre-rendered sprite: rotating a shape costs the same every frame, and rotating it once into a surface costs one blit thereafter.

⚠️ Build a surface once. It is a real canvas — allocating one per frame is the same mistake as loading an image per frame. A field, or the first frame, is where one belongs. The largest edge is 4096 pixels.

Every frame starts on the screen

As with the transform, the target is reset before each frame body runs. A Draw.Into you forget to close would otherwise send every following frame into a buffer, and the screen would simply stop changing — no error, no blank canvas, a frozen picture that reads as a hung game. Instead it costs you the rest of one frame, and you are told once.

What a surface cannot do

The 3D verbs (Draw.Camera / Light / Fog / Mesh) always render on the component's own canvas and refuse to paint into a surface — call Draw.Screen() first. Each surface keeps its own transform and its own Draw.Push stack, so a Draw.Pop inside one can never restore the screen's; entering a surface gives you a clean coordinate system.

    if (!starsPainted) {
      Draw.Into(stars);
      Draw.Clear("#0B0616");
      // ⚠ A JITTERED LATTICE, NOT `Roll()` FOR THE POSITIONS. `Roll()` is the small LCG `/blockfall` uses
      //    (`* 75 % 65537`, chosen because Osy# integer arithmetic is checked and a textbook multiplier overflows),
      //    and its period is short enough that 160 draws landed on 17 distinct pixels — measured. Coprime strides
      //    give 160 distinct positions by construction; `Roll()` still picks the brightness, where a short cycle is
      //    invisible.
      for (var i = 0; i < 160; i = i + 1) {
        var sxp = (i * 271) % 640;
        var syp = (i * 157) % 420;
        var mag = Roll();
        Draw.Rect(sxp, syp, mag > 0.86 ? 2 : 1, mag > 0.86 ? 2 : 1,
                  mag > 0.86 ? "#FFFFFF" : (mag > 0.5 ? "#8B79A8" : "#3A2A55"));
      }
      Draw.Screen();
      starsPainted = true;
    }

    Draw.Image(stars, 0, 0, W, H);

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?#

Re-measured in a real browser, 2026-09-05: roughly 75,000 draw commands per frame while holding 60fps (90,000 drops to 30), and more than 300,000 arithmetic steps — the top of the rig, so that ceiling was not found. 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.

⚠️ The draw budget is the browser's, not the language's. The same measurement with the JS compilation switched off is identical — 75,000 either way — because what a Draw.* call costs is the canvas, and Osy#'s own overhead around it is already small beside that. The arithmetic budget is where compilation shows, and there the ceiling is past anything a 2D game needs.

⚑ These are one machine on one day, as the previous figures were; treat them as an order of magnitude rather than a specification, and measure your own frame if you are close to the edge.

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…