A place rather than a character: banded tower on a headland, keeper’s shed
beside it, open water running off to the left, and a lens that turns. Three
loops — a clear-night sweep, a gale with rain off the right and spray on the
near rock, and a fog where the beam goes short and fat and the horn takes over.
The demo is the part worth clicking. A lighthouse is not identified by its
shape — at four miles on a black night you cannot see its shape — it is
identified by its light characteristic: the exact pattern of on and off it
repeats forever, written on a chart as Fl(2) W 10s or Oc W 4s or Iso W 6s.
Two towers can be the same height, the same colour, the same everything, and
still be told apart because one of them goes quiet for eight seconds and the
other for one. The demo drives the lamp from six of those patterns in real time
with a timing ribbon underneath, so you can watch a light say its own name; the
weather row runs any of them through a gale or a fog bank.
The lens only sweeps for the flashing ones. Fixed, occulting and isophase lights aren’t rotating at all — they show all round at once, which is a different silhouette and the whole reason those characteristics exist: you can read them from any bearing without waiting for the beam to come round to you.
Reuse
export/light-sheet.png— 864×40 strip, eighteen 48×40 frames inFRAME_ORDER:sweep-0…7,gale-0…5,fog-0…3.light-{sweep,gale,fog}-*.png— the individual frames.light@6x.png— prescaled 6× hero shot for docs/previews.
Render at integer scales with nearest-neighbor filtering or the edges smear.
sweep plays at 4 fps, gale at 6, fog at 1.6 (ANIMATIONS carries all
three).
The module is worth importing rather than the PNGs if you want the light to do
anything: paintScene(ctx, spec, opts) takes a frame spec directly, so the
beam angle, the on/off, the light colour and the weather layers are all
parameters rather than baked pixels. characteristicAt(spec, t) evaluates one
of the CHARACTERISTICS entries at a time in seconds and hands back { on, progress } — progress is what turns a flash into a visible sweep instead of a
hard blink.
Gotchas
- The beam paints only over sky and water pixels, so the tower and the rock occlude it for free. Add anything to the scene that should cast a shadow and it works automatically; add anything the beam should pass through and it won’t.
- Fog goes on before the beam. Paint the bank over the wedge instead and you get the one night where the light does nothing, which is not the night you meant to draw.
- Rain and fog use a deterministic hash rather than
Math.random, so a re-render produces byte-identical PNGs. Swap in real randomness and every run is a diff.
Source
No .aseprite — the canonical source is source/harbor-light.mjs: a
framework-free ES module holding the scene as a character map (one char per
pixel, palette at the top), with the moving parts painted procedurally over it.
Edit and regenerate everything with:
node source/render.mjs
(needs site/node_modules installed — the script resolves Playwright from
there.) That one command rewrites export/, media/, thumb.png, and the
demo’s bundled copy of the module, so nothing drifts.