Summary#
Canvas paints flat shapes. A game that wants depth — hills that recede, a bird lit from one side, a shadow under it, haze at the horizon — cannot get it from rectangles at any frame rate, so the canvas carries a second layer: a scene of meshes, drawn with the same immediate-mode discipline as everything else. Nothing is retained between frames except the meshes themselves; each frame you say where the camera is, where the light comes from, and where every mesh sits now.
The look is deliberately low-poly and flat-shaded: every face is one flat colour, lit by a sun with a soft shadow, an ambient sky, filmic tone mapping and distance fog. That is the look a few hundred well-placed boxes, cones and spheres produce best, and it is what runs at 60fps on an ordinary laptop.
Signature#
Mesh ground = Mesh.Plane(200, 200); // a field — build a mesh ONCE, in `on mount` or an initializer
Mesh trunk = Mesh.Cylinder(0.3, 0.4, 2, 8); // (radiusTop, radiusBottom, height, segments), along y
Mesh crown = Mesh.Cone(1.4, 3, 8); // (radius, height, segments), point on top
Mesh rock = Mesh.Sphere(1, 1); // (radius, detail 0..4) — an icosphere, 20 × 4^detail faces
Mesh crate = Mesh.Box(1, 1, 1); // (width, height, depth), centred on the origin
Mesh hill = Mesh.From(vertices, indices); // x, y, z per vertex; three vertex numbers per triangle
Draw.Camera(ex, ey, ez, tx, ty, tz, fovDeg); // eye position, the point it looks at, vertical field of view
Draw.Light(dx, dy, dz, sun, ambient); // the direction the sun SHINES ALONG, its colour, the sky colour
Draw.Fog(color, near, far); // haze from `near` to fully `color` at `far`, in scene units
Draw.Mesh(crate, x, y, z, color); // placed
Draw.Mesh(crate, x, y, z, rx, ry, rz, scale, color); // rotated (radians) and scaled uniformly
Draw.Mesh(crate, x, y, z, rx, ry, rz, sx, sy, sz, color); // …or per axisDescription#
A scene is drawn each frame, like everything else on a canvas#
The meshes live in fields because building one costs a buffer upload; everything else is said again every frame.
The 2D verbs and the 3D verbs share the frame body: the sky is a Draw.Rect, the scene is drawn over it, and the
score is a Draw.Text over that — in the order they are written.
[Page("/tree")]
[AllowAnonymous]
component Tree() {
Mesh ground;
Mesh trunk;
Mesh crown;
double t = 0;
on mount {
ground = Mesh.Plane(60, 60);
trunk = Mesh.Cylinder(0.25, 0.35, 1.6, 8);
crown = Mesh.Cone(1.3, 3.2, 8);
}
on frame (double dt) {
t += dt;
Draw.Rect(0, 0, 640, 360, "#bfe3ff"); // the sky, in 2D, BEHIND the scene
Draw.Camera(0, 3, 12, 0, 1.5, 0, 45);
Draw.Light(-0.5, -1, -0.4, "#fff4d6", "#9ec5ff");
Draw.Fog("#bfe3ff", 20, 60);
Draw.Mesh(ground, 0, 0, 0, "#7bbf5a");
Draw.Mesh(trunk, 0, 0.8, 0, "#8a5a3c");
Draw.Mesh(crown, 0, 3.2, 0, 0, t * 0.3, 0, 1, "#3f8f4a"); // slowly turning about y
Draw.Text("a tree", 12, 12, "#204020", 20); // the HUD, in 2D, OVER the scene
}
render { Canvas(w: 640, h: 360); }
}Coordinates and units#
The scene is right-handed with y up: +x is screen-right for a camera looking along −z, and +y is up.
Units are whatever you choose — the builders, the camera and the fog all speak the same ones. Rotations are in
radians, like Math.Sin, and apply x, then y, then z — pitch a bird, then yaw it to its heading.
A mesh's origin is its centre (a Plane is centred at y = 0, a Cylinder/Cone runs from −height/2 to
+height/2), so a tree of height 3 standing on the ground is drawn at y = 1.5.
The light is a direction, and the second colour is the sky#
Draw.Light(dx, dy, dz, sun, ambient) takes the direction the sun shines along — (−0.5, −1, −0.4) is a sun
high and to the right, casting shadows down and to the left. Faces turned toward it get sun; faces turned away
get the ambient colour, stronger on faces that look up (the sky) than on faces that look down. Shadows are
cast by every mesh onto every mesh, softened at the edge, within the range the fog reaches — a shadow far beyond
the fog would never be seen, and the shadow map's resolution is spent where it shows.
Fog is what makes distance read#
Draw.Fog(color, near, far) blends every surface toward color from near (no fog) to far (only fog). Give
it the sky's colour and a far hill dissolves into the horizon the way it does outdoors; it also sets how far the
shadow map reaches, so the two are tuned together.
Mesh.From builds anything the builders cannot#
A heightfield, a bird's body, a rock: give it every vertex as x, y, z and every triangle as three vertex
numbers. Faces are flat-shaded from their own winding, so wind each triangle counter-clockwise seen from
the outside — a face wound the other way is culled as a back face and simply is not there. An index outside
the vertex list, or a list whose length is not a multiple of three, is an error that names the position.
[Page("/ridge")]
[AllowAnonymous]
component Ridge() {
Mesh ridge;
on mount {
var verts = new List<double>();
var idx = new List<int>();
var cols = 24;
var rows = 6;
for (var r = 0; r <= rows; r++) {
for (var c = 0; c <= cols; c++) {
var x = (c - cols / 2.0) * 2;
var z = (r - rows / 2.0) * 2;
var y = Math.Sin(c * 0.5) * 1.5 + Math.Cos(r * 0.9) * 0.6 + 2;
verts.Add(x); verts.Add(y); verts.Add(z);
}
}
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
var a = r * (cols + 1) + c;
var b = a + 1;
var d = a + cols + 1;
var e = d + 1;
idx.Add(a); idx.Add(d); idx.Add(b); // counter-clockwise seen from above (+y)
idx.Add(b); idx.Add(d); idx.Add(e);
}
}
ridge = Mesh.From(verts, idx);
}
on frame (double dt) {
Draw.Rect(0, 0, 640, 360, "#cfe8ff");
Draw.Camera(0, 8, 22, 0, 2, 0, 40);
Draw.Light(-0.4, -1, -0.6, "#fff3d0", "#a9cbff");
Draw.Fog("#cfe8ff", 25, 70);
Draw.Mesh(ridge, 0, 0, 0, "#6faf58");
}
render { Canvas(w: 640, h: 360); }
}Colours are the 2D verbs' colours#
#rgb, #rrggbb, rgb(…) and rgba(…) — the alpha is ignored, a mesh is opaque. Anything else draws
magenta, the colour every renderer uses to mean "this is not a colour", so a typo is loud rather than dark.
It lowers to a kernel like the rest of the frame body#
Every 3D verb and every builder is part of the host contract a frame body is lowered against, so a body that uses
them still runs as a compiled JavaScript kernel — the hot loop of a game pays no interpreter cost for being 3D.
A mesh in a field is a plain value to the kernel; build it in on mount, read it in on frame.
Where WebGL is missing, the scene is missing#
The scene is rendered by the browser's GPU. In an environment with no WebGL the 3D verbs log one error and draw nothing, while the 2D verbs keep working — so a HUD and a backdrop still appear over a blank scene rather than the page failing. Real browsers all have it; the headless DOM the unit tests run in does not, which is why the pixels are proven by the visual harness and not by a unit test.
What a frame can afford#
The cost is per mesh drawn and per face — not per pixel — plus one shadow pass over the same meshes. A few
hundred Draw.Mesh calls over meshes of tens to low hundreds of faces each is comfortably 60fps; a Sphere at
detail 4 is 5,120 faces and is the wrong choice for anything smaller than a planet.
See also#
- Canvas — the surface itself and the 2D verbs, which draw under and over the scene
- on mount / on unmount —
on frame (double dt), the clock that drives it - component — component state, where the meshes and the world live