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 rectangleDescription#
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#
- Canvas 3D — the lit, shadowed 3D scene the same canvas can carry:
Draw.Camera,Draw.Light,Draw.Mesh - on mount / on unmount —
on frame (double dt), the clock that drives a canvas - component — component state, which is where a drawing's state lives
- animation — looping motion with no destination state — declarative motion for ordinary elements, which needs no canvas