01 / CONCEPT
A museum that feels like standing in front of a cabinet.
CABINET is about Bellwether, Iowa: a fictional town where homemade machines moved between the Dairy Dip, Lark Lanes, the VFW, and a one-screen theatre. The copy avoids the usual nostalgia fog. It names the routes, repairs, workers, and awkward local constraints that made those objects particular.
The layout moves like an archive tour. A loud cabinet pulls you in; the paper-white origin story slows the pace; the collection becomes a row of machine cards; the town turns into a route map; and the oral histories are filed as statements. Heavy borders, offset shadows, and solid color planes borrow the visual language of screen-printed arcade side art without pretending to be a generic neon arcade.
02 / SIGNATURE TECHNIQUE
The cabinet is a real low-resolution game, not a picture of one.
The screen is a 160 × 120 canvas—the resolution is intentionally small enough that every route, wall, ticket, and character is an authored pixel decision. CSS enlarges that canvas with image-rendering: pixelated, preserving the hard edges instead of smoothing them into a retro-looking blur.
A tiny offscreen sprite sheet holds four walking frames. Each 12fps tick draws one 8 × 8 cell from it, moves the player, advances the night manager, tests the eight tickets, and paints the scene. The curved CRT housing sits above that truthful canvas: its glass overlay adds scanlines, RGB aperture-grille stripes, a dark edge falloff, and a subtle perspective tilt without changing the underlying pixel data.
const sheet = document.createElement('canvas');
sheet.width = 64; sheet.height = 16;
const sctx = sheet.getContext('2d');
function buildSheet() {
sprites.forEach((sprite, frame) =>
sprite.forEach((row, y) => [...row].forEach((bit, x) => {
if (bit === '1') {
sctx.fillStyle = C.gold;
sctx.fillRect(frame * 16 + x, y, 1, 1);
}
}))
);
}
ctx.imageSmoothingEnabled = false;
ctx.drawImage(sheet, (ticks % 4) * 16, 0, 8, 8,
player.x, player.y, 8, 8);
function stopTimer() {
if (timer) { clearInterval(timer); timer = null; }
}
function sync() {
const shouldRun = visible && !document.hidden &&
!motionQuery.matches && state === 'play';
if (shouldRun && !timer) timer = window.setInterval(tick, 1000 / 12);
if (!shouldRun) stopTimer();
}
function updateVisibility() {
const bounds = canvas.getBoundingClientRect();
visible = bounds.bottom > 0 && bounds.top < window.innerHeight &&
bounds.right > 0 && bounds.left < window.innerWidth;
sync();
}EXACT IMPLEMENTATION EXCERPT / THE TIMER ONLY RUNS WHILE THE SCREEN IS VISIBLE
03 / PALETTE + TYPE
A closed, sixteen-color parts bin.
All UI, illustration, game pixels, and CRT treatment pull from the same fixed 16-color palette. The five anchor colors are deliberately NES-adjacent: near-black ink, hot red, warm gold, leaf green, and electric blue. The remaining shades support screens, printed paper, cabinet wood, and route-map contrast without sneaking in fashionable gradients.
#0F0F1BRED
#E43B44GOLD
#FFCD75GREEN
#38B764BLUE
#41A6F6CREAM
#FFF4D8VIOLET
#B86DF7TEAL
#73EFF7PINK
#FF77A8BROWN
#7A4B2DUMBER
#4D2B32SLATE
#314359PAPER
#D9D9C1BRICK
#B52A3APINE
#266B43SKY
#A3D9FF
ZERO ROM DUMPS
04 / ITERATION PASSES
What changed while tuning the cabinet.
- Pass 1 — correctness: guarded the canvas and offscreen sprite-sheet contexts so an unsupported display reports a clear status instead of throwing an exception.
- Started the 12fps loop only after a viewport check; it now pauses when the screen is hidden, the canvas leaves view, the game ends, or reduced motion is requested. A scroll/resize fallback covers browsers without
IntersectionObserver; the unrelated decorative loops were removed. - Restricted keyboard capture to the focused game, cleared held directions on blur, and made the touch controls and restart buttons 44 × 44px with keyboard activation for reduced-motion stepping.
- Corrected contrast on the marquee, screen caption, visit label, and dark-red palette swatch; added context-specific focus rings and semantic labels for the game, control groups, and map.
- Hardened 390px layouts by removing undersized controls, keeping code-scroll containment, and enlarging the small map labels. The game intentionally stays at a fixed 160 × 120 backing resolution, so device pixel ratio never inflates canvas work.
Pass 2 — design depth
- Weakest moment: the collection read as six copies of one card. Each record now has its own low-resolution canvas exhibit and a distinct desktop composition: a full-bleed feature screen, a recessed portrait screen, a vertical service tag, a compact score panel, a split-screen record, and a wide static test.
- Weakest moment: display type had scale but not a decisive rhythm. The hero now breaks into three intentional beats, display tracking and leading are tuned for the pixel face, major headings balance their lines, and a named spacing scale gives the archive sections different, purposeful vertical weight.
- Weakest moment: the town route stopped cleanly at the map edge. A stamped “1 TOWN / 4 DOORS” marker now straddles the seam on desktop and relocates safely over the map on small screens; the collection’s oversized service-record typography also lets the archive bleed beyond its safe column.
Pass 3 — complexify
- Weakest moment: exploration ended after the hero game. The archive now hides a working Static Valley service bench below the oral histories. Three wrapping, accessible dials affect a second truthful pixel canvas; stable axes expose more of the attract screen, and the full combination restores it.
- Weakest moment: the static effect was only decoration. The new test screen composites a hand-drawn
96 × 64field-test image through horizontal and vertical drift, then adds deterministic pixel weather at a density tied to fine tune. It is a repair task with a real no-carrier state, not a looping noise texture. - Weakest moment: a recovered signal disappeared when visitors looked elsewhere. Locking the bench changes the archive-power readout and top-line signal color for the rest of the page. Resetting returns the board to its documented unstable state. The final pass also added social metadata, tightened a misleading collection caption, and re-checked the small-screen containment and motion behavior.