|>
_|_ how this is built
____/___\____ ~~~~~~~~~~~~~~~~~~~~~
\ o o o / .:-=+# one ray per character
~~~\_________/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
How this harbor is built
- Renderer. Each rendered frame casts one ray per character cell from your eye, except cells still covered by intro noise.
Buildings, the dock and the hull are convex solids made of planes; the hull leaves its deck open over the hatch to the gun deck.
Masts, barrels, tree trunks and the lighthouse are true cones with smooth normals, and so are the gun barrels and carriage wheels, on horizontal axes.
Rocks and tree canopies are ellipsoids.
Sails and the flag are curved cloth.
Inside the cloth's box, a ray samples the gap between the ray and the cloth.
Near-parallel rays subdivide intervals that can contain crossings and select the nearest crossing inside the cloth's outline.
Each bracketed crossing uses a halving and straight-line steps.
Ship rigging, the masthead pennants and the cabin stair hand rails use projected lines; tall grass uses projected tufts.
Both check the depth buffer instead of ray intersections.
The sea is a height field of travelling waves, ray marched per cell; the ship and the boats ride it.
Low, dark hills rise from the sea on part of the horizon, just lighter than the sky.
- Light. Exterior lamps, lit windows, the ship's lanterns and the beacon light nearby surfaces, with brightness that decreases with distance.
Exterior solids cast shadows when shadow rays are enabled, except the thin raised cabin trim.
A shadow ray skips the solid being lit, because a convex solid cannot shade its own lit face.
Only hatch parts above the deck form the grouped shadow caster, with one bounding box.
The hatch boards and ladder receive light without shadow rays.
The hatch lantern emits light without shadow rays.
The lighthouse beam sweeps; the moon gives a dim fill.
Ship sails and the flag bypass lamp and shadow calculations; sails are brightest where they belly out farthest, and fog still dims them.
The black flag draws only its outline, its folds and its pale skull and crossbones.
The house's lamp and fire, the cabin's hanging lantern and the gun deck lanterns light nearby surfaces without shadow rays.
The cabin's wall lantern glows like the lit windows but lights no other surface.
The sea shows Fresnel reflection, moon and lamp highlights and foam.
Fog and contact shadows add depth.
- Clouds. Three cloud layers, with bases at 260, 480 and 900 m, drift downwind; the nearer layers move faster as you walk.
Each cloud is a long band of rounded bumps on a flat base, drawn on a billboard that faces you, and haze shrinks distant clouds.
Bump edges that face the moon are pale, and so is the flat base when the moon is below it.
Cloud density picks glyphs from
.:-=+*#; thick cloud hides the stars and the moon, and thin cloud dims the moon.
A buffer of 1° sky texels keeps the cloud density and rim of each direction.
Without reduced motion, a texel refreshes about ten times a second when a sky cell reads it.
The elevation rows take turns to spread refresh work across frames.
Turning reuses fresh texels but computes unseen or expired texels.
With reduced motion, each render refreshes the texels it reads, including after movement or a change to the motion setting.
- Characters. Brightness usually picks a glyph from the density ramp
.`',:;~-=+*#%@▒▓█ and one of eight colour levels per material.
Grass uses blade glyphs.
The renderer caps each tuft's projected height at 1/14 of the screen height before rounding blades to whole cells.
Blades use | ( ) \ / with pale moonlit tips.
Far tufts are dimmer.
Pale paving marks and foam keep their texture glyphs with a minimum brightness that decreases in fog.
Fountain water uses animated texture glyphs instead of the density ramp.
Flames in the house use fixed glyphs from an orange base to pale tips, so the idle house needs no redraws.
The moonlit sea seen through the gun ports uses still wave glyphs.
An edge glyph (| - / \ _) is drawn wherever one surface meets a farther one.
- Frame loop.
requestAnimationFrame moves you, the sea, the ship and its sails, and redraws the grid on a canvas.
A ray tests solids whose screen box reaches its tile of cells, nearest first.
It stops when no remaining solid can be nearer than its hit.
After the intro, the canvas keeps unchanged rows.
Devices without (pointer: coarse) draw rows with changed cells again from their full text-run positions.
Touch devices redraw only each run of changed cells and its neighbouring cells; runs fewer than five cells apart share one redraw.
Grass wind animation uses a 12 Hz clock; other scene changes can repaint grass cells between wind updates.
The mini map draws again each frame, even when its cells do not change.
The pop-up sign also draws again each frame; the cells it covered draw again when it moves or closes.
Devices with (pointer: coarse) draw one glyph per fillText call to limit WebKit's text cache.
Their canvas uses at most 2 device pixels per CSS pixel.
Other devices draw one run of one colour per call.
The backing-store area stays at or below 4096 * 4096 device pixels; this does not limit each dimension to 4096.
Large high-DPR windows use a lower rendering ratio, which can fall below 1.
Grid measurement clears the complete canvas, including padding, even when the backing-store size does not change.
The intro overlay uses CSS dimensions to cover the transformed canvas at any rendering ratio.
The first grid measurement waits for fonts; if the requested font fails to load, the scene uses a fallback font.
For a fixed exterior viewport, the glyph size stays fixed; slow frames can disable shadows without changing the view.
Resizes wait until the viewport settles, then update the canvas immediately before the next draw.
Slow rendering first disables exterior shadow rays, if enabled, then uses larger room glyphs with fewer cells.
Fast room rendering restores a finer grid.
Inside a room, missed frames also reduce detail relative to the measured display refresh rate.
Before counting slower callbacks as painting load, the renderer skips one repaint to measure the display period without that load.
This sample also detects display refresh changes during continuous input.
Each door or hatch crossing requests the same unloaded sample, so timing from the previous scene does not hide room painting load.
A room redraws only after movement, looking, resizing, or another change that requires a new frame.
Reduced motion also stops exterior animation.
- Intro rendering. The sweep shifts a single cached 8x8 glyph tile over cells that still hide the scene.
It uses the same canvas without a cached scene.
Each intro frame ends with a black canvas fill at opacity
1 - progress * progress.
Desktop drawing keeps the colour runs; touch drawing sends each glyph directly to the existing single-glyph painter.
The mini map, signs, and touch pad stay hidden until the intro ends.
- Controls. WASD or arrows walk and turn, R and F look up and down, the mouse looks around, Enter opens what you point at.
On a phone, the bottom-left pad walks; drag on the right to look around.
The pad and map controls respect safe-area insets.
See the harbor README for sign links and keyboard access.
- Mini map. Outside, M picks a point on the text map in the corner.
Press M again to fill the screen with the map.
Use the arrows to choose a point.
Enter or a tap selects a destination.
See the map travel guidance for routes and reduced motion.
See the house, cabin and gun deck guidance for door and hatch crossings and controls inside.
- Content.
harbor/build.py builds the points of interest with the README's Features list and More docs row, and the changelog.
- Deploy. See the deployment guidance.
The page has no runtime dependencies; nothing in
harbor/ reaches a Crewship host or worker image.
The source on GitHub · Back to the harbor