CANAL MONSTERSTUDIOS
GOWANUS --:--
All notes

FIELD NOTE · GAME

How the Gowanus on this site works

· 4 min read · #pixel art#canvas#typescript#astro

Everything behind this page is drawn in code: the tile factory, the sign, the viaduct, Lower Manhattan, the canal and its monster. There isn't a single image file in the scene.

The world is four layers

The scene is split by distance into far (Lower Manhattan), mid (Brooklyn rooftops), viaduct (the Smith–9th Sts F/G line) and near (the canal bank and the tile factory). Each layer is generated once per screen size from a seeded random number generator, so a given width always produces the same skyline, and drawn pixel by pixel into an offscreen buffer.

Moving things like the trains, clouds, egret and monster are drawn on top every frame, between the layers they belong to. Trains pass behind the factory because they're drawn after the viaduct layer and before the near one.

Two colours per pixel

Day turns into night because each layer stores two colours for every pixel:

  • an albedo, which is what it looks like in plain daylight
  • an optional emissive colour, plus a threshold byte for how dark it has to get before that pixel switches on

Windows are emissive. So are street lamps, the station sign and the stained glass in the church. Every window gets its own random threshold, so the buildings light up a few windows at a time as you scroll into the evening.

Relighting a layer is one pass over its pixels:

for (let i = start; i < end; i += 4) {
  if (!alb[i + 3]) { out[i + 3] = 0; continue; }       // empty
  const th = emi[i + 3];
  if (th && nightByte >= th) {                          // lit window
    r = emi[i]; g = emi[i + 1]; b = emi[i + 2];
  } else {                                              // daylight × ambient
    r = alb[i] * mr; g = alb[i + 1] * mg; b = alb[i + 2] * mb;
  }
  // aerial perspective: distant layers fade toward the horizon colour
  r += (hazeR - r) * haze; g += (hazeG - g) * haze; b += (hazeB - b) * haze;
  out[i] = r; out[i + 1] = g; out[i + 2] = b; out[i + 3] = 255;
}

The ambient multiplier, horizon haze colour and "night level" come from a handful of keyframes (day, golden hour, sunset, blue hour and night), blended by scroll position. The relight only reruns when the quantised time of day changes, so scrolling costs a few milliseconds and standing still costs nothing.

Pixels that stay square

Pixel art falls apart the moment a pixel is 2.5 screen pixels wide. The scene picks a scale based on the viewport, then rounds it so one art pixel is always a whole number of device pixels, taking devicePixelRatio into account. The canvas is rendered at that tiny internal resolution (about 480×300 on a laptop) and scaled up with image-rendering: pixelated.

Gradients, the 16-bit way

Smooth gradients look wrong next to hard-edged pixel art. The sky and the canal are drawn as flat colour bands, and only the seam between two bands is stippled with a 4×4 Bayer dither. You get the banding of an old console with soft-ish transitions.

Water

The canal is a gradient with a mirrored copy of the bank above it, drawn one row at a time with a small sideways wobble and every fifth row dimmed to suggest ripples. The oil sheen is the Gowanus's signature: a few drifting ellipses of dithered pixels whose hue shifts with position and time. At night the lamps and the sign get long reflections of their own.

Bits of the real world

  • The moon is in its actual phase, worked out from the length of the lunar month and a known new moon.
  • The season follows the date in New York. Autumn leaves fall into the canal, winter brings snow, spring has blossom and summer nights have fireflies. You can preview them with ?season=winter.
  • The Empire State Building goes orange in October.
  • The clock in the corner and the one on the studio wall both run on Gowanus time.

Keeping it light

The loop runs at 30 frames a second, which suits pixel art and halves the work. It stops when the tab is hidden, and the studio interior only animates while it's on screen. With prefers-reduced-motion set, the world renders a still frame for each scroll position.

The content column has pointer-events: none and the cards turn it back on, so clicks in the gaps fall through to the canvas. That's how you can catch a train or poke whatever's in the water. Everything is hit-tested in art pixels.

The stack

Astro builds the pages from Markdown, and TypeScript with the Canvas 2D API handles everything that moves. It's all static files on GitHub Pages, with no framework running in the browser.

PAUSED

ESC TO RESUME

ACHIEVEMENTS 0/10