Skip to content

Latest commit

 

History

History
248 lines (193 loc) · 10.9 KB

File metadata and controls

248 lines (193 loc) · 10.9 KB

RUNNER PROTOCOL — art direction

The visual spec for device/runner.py. Every rule here is traceable to the reference board the project was built against rather than to taste. If a change fights this document, change the document first.


1. The look

Retro-futurist industrial sci-fi: the aesthetic of a security terminal that watches something biological. The lineage is the graphic-realism school — simplified, deconstructed forms at real-world proportions, strong flat colour statements, limited materials, accents doing the work of rendering: F1 livery sheets, Ghost in the Shell (1995), PS2-era Sony ads, Designers Republic on WipEout, Teenage Engineering hardware, Metahaven. Layout habits come from Japanese near-future graphic boards: bands and stripes, machine strings as texture, everything annotated.

Nothing is fetched or generated at runtime; everything the device renders is baked ahead of time into device/art/.


2. Palette

Defined in runner.py. Eight colours, and that is the whole budget.

Token Hex Role
VOID #060806 ground
VOID_2 #0C0F0B the other half of the dither pair
GRAPHITE #161A16 panel fill
GRAPHITE_2 #202620 unlit meter segments
RULE / RULE_HI #303828 / #4A5642 hairlines, frames, grid
ACID #C6FF1A the accent — data, fills, live state
ACID_D / ACID_XD #749812 / #3E520C dimmed acid, band tinting
BONE / BONE_D #E8ECE0 / #8C9484 prose, secondary readouts
CYAN / CYAN_D #3CDCEB / #1C747E one tertiary series (season trace)
ALERT / ALERT_D #FF3A26 ≥95%, blackout, rate limit

Rules

  1. Acid is load-bearing. It marks data, live state and the thing you should press. It is never used because a panel looked empty.
  2. Flat fills only. No gradients, no glows, no soft shadows, no rounded corners.
  3. Amber #FFB028 is the only colour not in the token list, used inline for the 75–90% warning step. It is deliberately not promoted to a token — it should stay hard to reach for.
  4. Two accents maximum in one panel: acid + cyan for series separation, acid + alert for danger. Never all three.

3. Typography

Maratype, baked to bitmaps at 10 / 14 / 20 / 32 / 56 px by tools/bake_font.py. Glyphs are stored as horizontal spans, so drawing is a few Screen.rect calls per character — no font library on device.

Size roles

px Use
56 the single most important number on screen (LOAD %), rank glyph
32 state name, secondary big numbers (season %, core %)
20 values in lists, tab labels, section headings
14 panel names, column headings, key/value rows — the floor for anything you must read
10 technical annotation only, meant to be small and mostly texture

The font's four defects, and what we do about them

These are properties of the typeface, not bugs in the baker. All are handled in bake_font.py; if the font is ever replaced, re-check every one.

  1. No % glyph at all. Synthesized on the digit box from a diagonal plus two counters. An earlier version reused the font's own /, which runs the full ascender — that produced a percent taller than the numbers next to it with the counters flung to the corners.
  2. X, V, K are squared forms whose distinguishing notches are one pixel below ~24px. XH, VU: "EXTRACT" read as "EHTRACT", "SALVAGE" as "SALUAGE", "BLACKOUT" as "BLACHOUT". All three are redrawn as true diagonals at the font's own stem weight and advance.
  3. No < or >. Synthesized as chevrons on the digit box.
  4. Caps-only — lowercase codepoints map to the same glyphs. So lowercase in the UI buys nothing; the one lowercase we use is the x in "x1.5", which gets a copy of the fixed uppercase.

Stem weight must be measured from a vertical stem (I, else L), never from the hyphen's height — at 14px the hyphen is 1px tall while stems are 2px wide, which made the synthesized diagonals a stroke too light.

Synthesized strokes are capped by the room available (min(stem, (w-1)//2, h//4)): full weight in a 5×7px box just fills the box.

Other rules

  • mono=True for anything tabular — percentages, credits, window numbers. Maratype is proportional (M=6px, I=2px at 10px) so columns will not line up otherwise.
  • track=1..2 on small caps text. The face is condensed and sets tight.
  • Prose is uppercase. The reference sets prose lowercase, but at 10–14px on a 3.5" panel the caps-only font is more legible, so legibility wins.

4. Layout

640×480, 8px margin, everything on a 32px background grid.

y 4    ┌ 150px ─────┬ 5 tabs x 72 ─────────────┬ clock ──┐   header, h 26
y 30   └────────────┴──────────────────────────┴─────────┘
y 36   ┌ body ───────────────────────────────────────────┐
y 446  └─────────────────────────────────────────────────┘
y 452  ticker lane ────────────────────────┊ control hints    footer

Panel anatomy (plate())

  • 1px RULE frame, GRAPHITE fill, no rounded corners.
  • Top-right corner clipped on a 10px diagonal, cut back to the background template — not to flat black — so the dither reads continuously through the notch.
  • Label sits in a solid acid tab overhanging the top edge by 2px. Panel content must therefore start at y+16 or lower.
  • invert_plate() is the acid slab with the label knocked out in void — the poster "LAUNCH" move. It takes over wherever it lands: one per screen, on the thing you press.

Meters (bar() / vbar())

Segmented blocks with gaps, never a smooth fill — the segmentation is what makes them read as instrumentation. Unlit segments are tinted by band (LOAD_ZONES) so the productive stretch is visible before you reach it, and the 95% blackout line is a bone marker.


5. Motion

Slow, mechanical, and mostly not moving. 20fps cap.

  • Never move anything horizontally as decoration. These panels have no vsync and horizontal drift reads as tearing.
  • Transitions are hard cuts plus a 3-frame band-displacement glitch (glitch()), never fades.
  • The dither shimmer on the hero (4 baked frames at slightly different thresholds) is the only continuous ambient motion.
  • Alarm chrome crawls; it is the one thing allowed to draw attention.
  • The extract stamp slams down over ~0.15s then holds.
  • The larvae undulate. A travelling sine along the body, and the rate is data: the live specimen's speed and amplitude both scale with load, so a hot window visibly writhes and a cold one barely moves. Older specimens in the tray settle to a slow idle.

6. Motif catalogue

The devices the style is built from, and where each lives.

Motif Where
Bayer dither field over near-black background template, built once, memcpy per frame
Halftone/dithered photographic figure hero portrait
Acid slab with knocked-out type invert_plate() — active tab, EXTRACT, rank
Segmented instrument meters bar(), vbar()
Diagonal hazard stripes hazard() — blackout, rate limit, alarm edges
Technical glyph marks (barcode, reticle, checker, X) glyph() / glyph_row(), plus the baked sigil strip
Terminal type-on boot draw_boot() — "INIT / CORE REVIEW", a poster device
Corner registration ticks background template, plate() notch, rank stamp
Coarse 32px grid background template
Machine strings as texture header identity cell, ticker
Seismic column plot TRACE tab
Annotated callouts with leader lines over imagery hero portrait, LINK specimen, attract mode
Vertical type running up an edge attract mode, RUN tab edge
Ribbed specimen larvae worm() — the DEBRIEF specimen tray and the live one on the RUN strip

6.1 The specimen larvae

A parallel row of ribbed acid-green grubs on a ribbed bed, glossy and translucent, each stencilled with its own serial (133083, 133084, …). They read as a specimen catalogue, which is what a run history is — so worm() is not decoration, it is the log. Form notes that matter for redrawing it:

  • roughly 8:1, fat through the middle, rounded at both ends. An early pass stretched one body across the full panel width and it became a flat ribbon with no readable taper.
  • ~13 ribs, 1px gap between them; the gap is what makes it read as segmented.
  • a brighter top third for the gloss. Body and gloss come from worm_tones(), at 55% and 100% of the base colour — dividing the base by three made them look like wires.
  • the substrate goes only under an occupied slot. Drawn across the whole panel it read as a barcode field rather than a bed.

6.2 The bioluminescent bloom

A green-blue organic growth on a dark plinth, slowly brightening over seconds with a reflection beneath it. The interesting part is purely the timing: a very slow luminance ramp with nothing else moving. Hosted by the boot sequence: three baked exposure steps (bloom_f0..f2.raw), stepped through at 0.2s / 1.0s / 1.7s. The ramp lives in dither density — the .raw format carries no brightness axis — and the reflection is part of the bake, not a runtime effect.


7. Do not

  • Add a ninth colour, a gradient, a glow, or a rounded corner.
  • Put text below 14px that the player actually has to read.
  • Animate anything horizontally for decoration.
  • Use acid on something that is not data, state, or a control.
  • Reach for ALERT below 90% load. It means this run is in danger, nothing else.
  • Add a second invert_plate() to a screen that already has one.

8. Rebuilding what's rebuildable

The art ships pre-baked in device/art/ — the .raw files are the artwork. The font and the sound effects rebuild from sources in the repo:

py tools\bake_font.py --preview   # -> device/art/maratype.fnt (+ tools/fontcheck.png)
py tools\bake_sfx.py              # -> device/art/sfx_*.wav

Preview any screen without hardware:

powershell -File pc\sim-runner.ps1 -Tab log -State inband -Runs 22
powershell -File pc\sim-runner.ps1 -Tab run -State critical

-Runs N fabricates N closed windows — use it to check anything whose layout depends on history depth (the specimen tray holds 18 and then reports +N OLDER). -State covers cold / inband / critical / blackout / extracted / extracting / nodata; -Boot plays the boot sequence.

Always judge a font change from the baked output (--preview re-renders through the device's own decoder), never from what PIL draws. Every glyph defect in section 3 was invisible in the source render and obvious in the bake.