A 3D engine, built by hand.
Every frame on the record.

Halcyon renders 3D in software on Canvas 2D: its own matrices, culling, clipping and z-buffer, six shading modes, 34 generated primitives — and a console that reports what every stage of every frame costs.

FLATPERSPECTIVE—SOLIDSKYFLOORGRIDWIRECULLSPHERE_01 | 0 tris drawn | drag orbit · scroll zoomRUNNING
SHAPERENDERWORLD
SHADING MODEPOINTSWIREFLATGOURAUDDEPTHNORMALSPIPELINEWIREFRAMEONOFFBACKFACE CULLINGONOFFZ-BUFFERONOFFDITHERINGONOFFEDGE ANTIALIASONOFF
frame time1.00 ms
TRANSFORM0.00CLIP-CULL0.00RASTERIZE0.00PRESENT0.00FILL RATE0 px/f
geometryscene total
VERTICES0EDGES0TRIANGLES0CULLED0DRAW CALLS1POLY BUDGET0 / 8192
depth distributionnear → far
neardepthfar
The console

Every stage of the frame. On the record.

Each card runs a real piece of the pipeline, drawn in this browser by the same kind of software renderer Halcyon is.

POINTSWIREFLATGOURAUDDEPTHNORMALS
POINTSWIREFLATGOURAUDDEPTHNORMALS
Six shading modesPoints, wire, flat, Gouraud, depth and normals — switched live, the same mesh under each.
framerate90 frames
MIN0AVG0MAX0
frame time1.00 ms
TRANSFORM0.00CLIP-CULL0.00RASTERIZE0.00PRESENT0.00FILL RATE0 px/f
framerate90 frames
MIN0AVG0MAX0
Live telemetryFrame rate, frame time split into transform, clip-cull, rasterise and present, and the fill rate per frame.
BACKFACE CULLINGONOFFTRIANGLES0CULLED0tris drawn0
Backface cullingEvery triangle tested for its winding; the ones facing away are dropped and counted.
DEPTH · 1/d per pixel
A 1/d z-bufferDepth kept per pixel in a Float32Array, so shapes that pass through each other meet exactly.
5 bits · 4×4 BayerDITHERINGONOFF
Ordered dither32 levels a channel through a 4×4 Bayer matrix, for smooth light at low bit depth.
UNCATEGORIZEDSphere143 pts · 240 tris01 of 34 primitives
UNCATEGORIZEDSphere143 pts · 240 tris01 of 34 primitives
34 generated primitivesPolyhedra, knots, a Menger sponge and eight molecules, every one built in code.
12345678MIDIFADER 1PITCH 0FADER 2YAW 150FADER 3ROLL 3FADER 4SPIN 139FADER 5SCALE 119CC 0 · 64 · FADER 1
12345678
Played from a MIDI controllerA KORG nanoKONTROL2 over Web MIDI: 21 bindings, every fader centred on its default.
01 — Rasterise

Two backends, one frame

Switch the z-buffer off and Halcyon sorts whole triangles and paints them on Canvas 2D, with textures mapped by subdivided affine patches. Switch it on and every triangle is walked pixel by pixel with Pineda edge functions, its depth kept as 1/d in a Float32Array.

  • Near-plane clipping before projection
  • Edge antialiasing that feathers outward
  • Colour and depth in typed arrays
Z-BUFFERONOFF
Whole triangles sorted: the weave breaks
Painter's algorithm · Canvas 2DFRAME—FILL RATE—tris drawn—
Z-BUFFERONOFF
Depth settled per pixel: each bar over the next
Depth-buffered · 1/d per pixelFRAME—FILL RATE—tris drawn—
02 — Inspect

Every switch answers on the next frame

The inspector's RENDER tab sets the shading mode and the pipeline — wireframe, backface culling, z-buffer, dithering, edge antialias — and the telemetry under the viewport shows what each change costs: triangles culled, draw calls, frame time by stage, fill rate.

  • Six shading modes
  • Five pipeline switches
  • Frame time split into four stages
geometryscene total
VERTICES0EDGES0TRIANGLES0CULLED0DRAW CALLS1POLY BUDGET0 / 8192
framerate90 frames
MIN0AVG0MAX0
SHAPERENDERWORLD
SHADING MODEPOINTSWIREFLATGOURAUDDEPTHNORMALSPIPELINEWIREFRAMEONOFFBACKFACE CULLINGONOFFZ-BUFFERONOFFDITHERINGONOFFEDGE ANTIALIASONOFF
03 — Play

A hardware controller, bound to the console

Plug in a KORG nanoKONTROL2 and Web MIDI puts the shape under your hands: faders on pitch, yaw, roll, spin and scale, knobs on the camera orbit and the fog, buttons on the camera presets, the shape list and theatre mode.

  • 21 bindings, checked at compile time
  • Every fader centred on its row's default
  • The traffic printed as it arrives
12345678
Value curvePITCH
MIDInanoKONTROL2 · CONNECTEDFADER 1PITCH 0FADER 2YAW 150FADER 3ROLL 3FADER 4SPIN 139FADER 5SCALE 119KNOB 1AZIMKNOB 2ELEVKNOB 3FOGSOLO 1VIEW FRNTSOLO 5VIEW ISOTRACK ▶SHAPE +1RECTOGGLE THEATRECC 0 · 64 · FADER 1
Preview

The whole console, live.

The console with each inspector tab open — shape, render, world — drawn and measured in your browser as you watch.

Live software rendering · Canvas 2D · no WebGL
And the rest

Built to be read.

A triangle that crosses the near plane is cut in camera space by Sutherland–Hodgman — into nothing, a triangle, or a quad fanned into two — its texture coordinates carried along the cut.

One key light: ambient plus diffuse quantised to 64 steps, and a Blinn-Phong highlight, exponent 32, in 16. Azimuth, elevation, ambient and specular each have a slider.

Water, methane, ammonia, carbon dioxide, benzene, caffeine, aspirin and glucose, built as balls and sticks, each with its formula, its molar mass and its references.

Space pauses, W draws the wireframe, C toggles culling, S and F the sky and the floor, G the grid, R resets, 1–9 pick a shape — and Escape leaves theatre mode.

Engine

Built from first principles.
Open to the last line.

Halcyon is plain TypeScript on one Canvas 2D context — about 41,000 lines, MIT-licensed, every stage of the pipeline written and tested in the open.

Zero runtime dependenciesThe package ships dev tools only: TypeScript, Vite, Vitest, Biome, Playwright. The renderer is its own.
Typed arrays all the way downColour in a Uint8ClampedArray, depth as 1/d in a Float32Array, uploaded once a frame with putImageData.
53 test filesVitest on the pure modules in Node: projection, the rasteriser, the camera, the shapes, the MIDI router.
Atomic releasesEach build goes up by rsync into its own timestamped release, switched in by a symlink, rolled back in one command.

A 3D engine, built by hand.
Open it and read every frame.

Six shading modes, two backends, 34 primitives — in the browser, on Canvas 2D.