Animation — the two complementary styles
How to make the KN-86 screen move. There are exactly two sanctioned animation styles, and they are complements, not rivals — pick by what the effect needs (see the decision table below). Both are driven by the same idle-timer substrate; neither blocks the event loop.
Authority: ADR-0052 (the decision + the soft-glyph plane), ADR-0036 (the renderer), GWP-644 (the idle-timer substrate). Worked reference: runtime/programs/animlab/animlab.lsp — PAD-1..PAD-5 select the soft-glyph motion style, PAD-6/PAD-7 the glyph-cycle field style (the named-style tables below); record it with tools/record-demo.sh --screen animlab.
Inspired by COMPUTE!‘s First Book of Atari Graphics: the era’s lesson was animate a lookup, not the pixels. The KN-86 has no font-rescan hardware, but the philosophy gives us a cheap primitive (soft-glyph) beside the obvious-but-costly one (glyph-cycle).
Pick a style
Section titled “Pick a style”| Soft-glyph | Glyph-cycle | |
|---|---|---|
| What | Redefine ONE mutable 8×8 slot per frame, tile it across a field | Choose a Code-Page glyph PER CELL per frame |
| Primitive (system tier) | render/soft-define + render/soft-fill | render/glyph in a per-cell loop |
| Primitive (cart tier) | soft-define + soft-fill | cell-set in a per-cell loop |
| Caller cost / frame | O(1) — 8 bytes + 1 fill call, any field size | O(cells) — one call + one decision per cell |
| Motion | sub-cell (the 8×8 pattern itself moves) | whole-cell (a cell swaps which glyph it shows) |
| Spatial variation | none — every cell is the SAME slot (uniform tiled texture) | full — every cell can differ |
| Reach for it for | backgrounds, scanlines, barber-poles, water, dither, loaders, ambient phosphor texture | fire, dissolve, heatmaps, wipes, per-cell reveals |
One-line rule of thumb: uniform texture → soft-glyph; spatially-varying field → glyph-cycle. A surface may use both (the animlab reference shows them side by side).
Both tiers, same functionality
Section titled “Both tiers, same functionality”Every technique on this page is available to a cartridge as well as to a system screen or program. That was not always true: the soft plane and the vector line primitive shipped system-tier only, so carts had the expensive animation style and no way to draw a line. They are now at parity.
The two tiers differ only in spelling and in what they can reach:
| System tier | Cart tier | |
|---|---|---|
| Soft slots | render/soft-define / -fill / -glyph, slots 0–7 | soft-define / soft-fill / soft-set, slots 0–7 (cart-local) |
| Vectors | render/half-block-line over 128×150 | half-block-line over 128×146 |
| Canvas | all 75 rows | Rows 1–73 (Row 0 and Row 74 are firmware chrome) |
The renderer’s bank is 16 slots split down the middle: 0–7 system, 8–15 cart. A cart numbers its own slots 0–7 and the runtime maps them into the upper half, so a cart physically cannot redefine a glyph the status bar or action bar is drawing with. Naming slot 8 or above from a cart is out of range and ignored.
Cart-tier drawing is chrome-gated the same way cell-set is: a soft fill
spanning Row 0 drops that row and counts a violation. Vectors cannot address
chrome at all, since the half-block canvas only covers Rows 1–73.
Use the drawing layer, not the raw calls
Section titled “Use the drawing layer, not the raw calls”ui/draw.lsp is the semantic layer over both families, written against the cart
tier so it loads in either context. Prefer it:
; a 20x6 field of scrolling barber-pole, animated at O(1) per frame(draw/textured-field 4 10 20 6 0 (draw/tex-bars 0)) ; once(draw/retexture! 0 (draw/tex-bars frame)) ; each frame
; vectors in CELL coordinates; the sub-pixel seam is handled for you(draw/line 10 20 40 30)(draw/box-hb 20 20 40 24)(draw/circle-hb 50 60 12)(draw/polygon-hb (list (cons 40 40) (cons 48 40) (cons 44 48)))Named textures (draw/tex-solid, -empty, -dither-25 / -50 / -75,
-scanline, -grid, -bars, -diagonal, -static) exist so nobody
hand-assembles eight row bytes. The frame-varying ones take a frame number.
draw/retexture! is the point of the whole plane: the cells already carry the
slot, so advancing the animation is eight bytes of work no matter how large the
field is.
Soft-glyph (the cheap, uniform one)
Section titled “Soft-glyph (the cheap, uniform one)”A small bank of mutable 8×8 glyph slots in their own address space (render/soft-slots → 16; not Code Page codepoints). Rewrite a slot’s 8 bytes, then tile it:
; one slot's 8 rows (MSB-first bytes, 0..255 — the same layout as a font glyph)(render/soft-define slot b0 b1 b2 b3 b4 b5 b6 b7)
; tile slot across a cols x rows cell block at cell (col,row), scale, fg, bg.; ONE call paints the whole field — the per-frame cost does not grow with size.(render/soft-fill col row cols rows scale slot fg bg)
; or place a single soft glyph (pixel-coord mirror of render/glyph):(render/soft-glyph col row slot scale fg bg)Animate by regenerating the 8 bytes from your frame counter each render and re-filling. The whole field of any size is two Lisp calls per frame:
(defn paint (f) (apply render/soft-define (cons 0 (my-bytes f))) ; redefine slot 0 (render/soft-fill 4 8 28 36 1 0 UI-LIT UI-BLACK)) ; tile it — O(1)The catch: every tile is the same slot, so soft-glyph makes a uniform
texture — it cannot make cell (0,0) differ from cell (5,3) with one slot. For a
handful of distinct regions, use a handful of slots (still O(slots), not
O(cells)).
Dither — apparent extra brightness on one phosphor
Section titled “Dither — apparent extra brightness on one phosphor”The KN-86 is single-foreground amber; you cannot get more colors. You can get apparent mid-tones by animating a dither pattern in a soft slot (spatial checkerboards, or temporal flicker between two patterns across frames on the persistent phosphor). This is the monochrome analog of the Atari book’s NTSC artifacting, and it is the natural job for a soft slot.
The named motion styles
Section titled “The named motion styles”Five sanctioned soft-glyph motions ship as the design-system library
(selection-passed 2026-07-27). Every one is the same recipe (redefine 8 bytes,
tile once, O(1)/frame); they differ only in the (row, frame) -> byte
function. The on-glass reference is
animlab: PAD-1..PAD-5
select the left field’s motion, and each style has a committed capture
(runtime/tools/kn86rec/demos/animlab-<style>.{rec,gif}, cropped to the field).
| # | Style | Bytes | Reach for it for |
|---|---|---|---|
| 1 | BARBER-POLE | 0xF0 rotated, row-indexed by (row + frame) | diagonal drift, loaders, progress texture |
| 2 | WATER | 38 E0 E0 38 0E 07 0E 38 hex, scrolled by (row + frame) | shimmer, liquid backdrops |
| 3 | SCANLINE | 0xFF on row frame mod 8, else 0 | raster sweeps, CRT dress |
| 4 | BREATHE | centred span, widths 2..8..2 with a dwell at each end | pulse, alert glow; the one style whose motion is density, no drift |
| 5 | RAIN | two 1-px drops per tile at opposite phases | sparse precipitation, drip |
Two rules paid for on-glass ride with the library:
- BREATHE never hits width 0. A zero-width beat blanks the whole field for one frame and reads as a flash.
- SCANLINE and RAIN carry 1-px features, so their captures must be recorded native (see the capture rule under Rules).
Glyph-cycle (the spatially-varying one)
Section titled “Glyph-cycle (the spatially-varying one)”Each cell independently picks a Code-Page glyph from your field state. This is
what the fireplace does
(a heat field → the shade ramp ░▒▓█):
(let y 0)(while (< y H) (let x 0) (while (< x W) (let g (cell->glyph (field-at x y) f)) ; per-cell decision (when (> g 0) (render/glyph (+ X0 x) (+ Y0 y) g 1 UI-LIT UI-BLACK)) (set x (+ x 1))) (set y (+ y 1)))Use it when cells must differ across the field. It costs one render/glyph
call (and one decision) per cell per frame — fine for modest fields, but do not
reach for it to paint a uniform moving texture; that is the soft-glyph job.
The named field styles
Section titled “The named field styles”Two sanctioned glyph-cycle fields ship with the library, both on
animlab’s right panel
(PAD-6/PAD-7; captures at
runtime/tools/kn86rec/demos/animlab-{wave,matrix}.{rec,gif}):
| # | Style | Recipe |
|---|---|---|
| 6 | SHADE-WAVE | shade ramp indexed by (x + y + frame) mod 8: a diagonal density band |
| 7 | MATRIX-RAIN | falling character streams: each column hashes its own speed (1..2 rows/frame), phase, and trail (14..24 rows over a 44..54 period); TWO wrapping streams half a period apart keep ~3/4 of the column lit; the head is a fresh printable glyph every frame, the body mutates every 5 frames, the tail fades ▒ then ░ |
Worked composition — ABYSSAL SWEEP (the Depth Charge sonar scope)
Section titled “Worked composition — ABYSSAL SWEEP (the Depth Charge sonar scope)”The full-screen glyph-cycle instrument: the algo-art piece
design/art/abyssal-sweep-philosophy.md
ported on-glass at runtime/tools/kn86rec/demos/abyssal.{lsp,rec,gif}. A polar
transform over the text grid (cell aspect compensated so the disk reads round);
an 8 s probe sweeps clockwise and every cell decays behind it through the
density ramp (block -> dark -> medium -> light shade -> dots -> black,
tau 1.6 s); three range rings render as marching-squares double-line arcs;
cardinal axes surface only where the trail has gone dark; seeded contacts
brighten in the wake, a ghost fraction gating between revolutions. Fully
deterministic off the tick clock; geometry precomputed to flat vectors at load.
This is not a selectable animlab style: it is the sanctioned SONAR SCOPE for Depth Charge, whose spec names the Abyssal Sweep as the primary sensory view. A cart that needs a PPI/sonar surface starts from this composition; the piece also demonstrates the composition rules at scale (precompute geometry, derive every frame from one clock, carry brightness as glyph density).
MATRIX-RAIN is the canonical demonstration of why this style exists: per-column desync and per-cell glyph choice are exactly what a tiled soft slot cannot express. Two implementation notes from its build: hash the column with a quadratic term (a linear key gives consecutive columns constant phase steps, which reads as diagonal staircases), and keep all hash intermediates under 2^24 (Fe numbers are single-precision floats).
Driving the motion (both styles)
Section titled “Driving the motion (both styles)”Neither style spins or blocks. Both ride the GWP-644 idle-timer substrate: arm a repeating timer that advances a state counter; the render handler paints the current state. Arm lazily on first render (so a dormant screen does not churn) and guard on the substrate so a timer-less host degrades to a static frame:
(defn arm! () (when (and (provided? 'editor/timer) (not *armed*)) (run-with-timer PERIOD PERIOD advance! 0) ; advance! ticks the counter (set *armed* t)))The host pumps the timer registry (screen_router_pump_timers; the recorder
pumps a virtual clock). Keep the timer thunk pure state-advance; do the drawing
in the render handler. See fireplace.lsp / animlab.lsp for the full shape.
Sequences: composing phases into a narrative (the BOOTSEQ pattern)
Section titled “Sequences: composing phases into a narrative (the BOOTSEQ pattern)”A sequence stages several animation phases on one frame counter and
resolves into live deck furniture. The canonical example is the power-on,
bootseq
(selection-passed 2026-07-28; capture at
runtime/tools/kn86rec/demos/bootseq.{rec,gif}): BOOT (the road floor
rolls toward the viewer while the KN-86 wordmark TYPES IN as exact system
font behind a blinking block cursor, then an energise bar sweeps the mark)
then DOCK (the mark zooms into the header slot in integer scale steps),
resting on the rule-aligned masthead over the rolling road.
The pattern’s rules, each paid for on-glass:
- One clock. Every phase keys off the same frame counter through
threshold constants (
BS-LOGO-START,BS-DOCK-START, …). No nested timers and no per-frame RNG: the sequence is deterministic, so the recorder replays it exactly and it never flickers across a virtual clock. - One source per value. Layout derives from a handful of constants (the rule’s ends, the band midline, text lengths, scales); a phase is a different read of the same data, never a second copy of it.
- Move in the platform’s units. Positions lerp continuously; glyph scale steps through the integers (16/12/8/4/3), because no fractional scaling exists. The mark is genuine deck text at every stage: no traced stand-ins (a bitmap font cannot be beam-traced honestly), so what docks in the header IS the text the deck renders everywhere else.
- The rest state is the deck. The final phase holds a working surface (the rolling road + the masthead), so the sequence sits as an attract loop with no cliff at the end.
Phases draw with whatever the scene needs (bootseq is vector redraw on
render/half-block-line + render/text); the styles above and this pattern
compose freely. Timing and arming follow “Driving the motion”; loops stay
flat per the Fe GC-stack rule.
Reveals: animating appearance (the shipped primitive)
Section titled “Reveals: animating appearance (the shipped primitive)”Text and ASCII surfaces do not hand-roll their arrival flourish. The
(reveal ...) NoshAPI family (ADR-0033;
contract at reveal-styles.md)
is the sanctioned mechanism: an async C engine snapshots the text surface,
animates it (:char-flicker is the Sneakers / no-more-secrets decrypt —
clean-room, per-cell hashed settle deadlines; :radial is the ripple), and
completes back to event-driven idle. One verb, multiple styles, every surface.
On-glass reference: runtime/tools/kn86rec/demos/decrypt.{lsp,rec,gif} — a
classified transmission resolving from full scramble via the inspiration
entry’s example call, (reveal nil :style :char-flicker :rate 30 :duration 1.0).
The recorder pumps the reveal clock on HOLD beats and captures the text tier,
so any reveal use is capturable the same way.
Choose by what moves: a FIELD that never finishes wants a style from the
tables above; an APPEARANCE that finishes and idles wants (reveal ...).
Worked example — animating IN-MODE (Keyring)
Section titled “Worked example — animating IN-MODE (Keyring)”Both styles work the same way inside a knEmacs major-mode as on a system attract screen — a mode re-renders its own cells on the idle-timer, no drop to a separate surface (ADR-0046 §“Amendment Log” 2026-06-28). Keyring’s crack is the worked consumer of the two-style choreography:
- RUN = soft-glyph. The crack’s progress loader redefines one soft slot per
tick from the countdown and
render/soft-fills the crack panel — a uniform dither-ramp (sparse → dense) atO(1)/frame. - RESOLVE = glyph-cycle. On success the recovered key’s fingerprint materializes cell-by-cell (each cell cycles shade glyphs to its final hex digit) — the spatially-varying case.
Both are idle-timer-driven and guarded on (provided? 'editor/timer) (a
substrate-less host paints the final frame instantly and never raises). Input
stays grammar (completing-read + EVAL); only the output moves. The point: a
data/nav mode can hold a timed, moving operation without becoming a
real-time surface. See software/programs/keyring.md
§“Scope-line” + the design record
plans/2026-06-27-keyring-crack-design.md (deleted 2026-07-29; recover from git history).
(bzbx’s capture-waterfall is the other in-mode soft-glyph candidate —
software/programs/bzbx.md §“Scope-line”.)
- Rows 0 and 74 are firmware-owned. Animate only within Rows 1–73 (see
screen-design-rules.md). - Amber on black is canonical (
UI-LIT/UI-BLACK); the renderer maps to the active phosphor scheme. Do not hardcode hues. - Carry the cost in mind. A full-screen uniform effect via glyph-cycle is ~9,000 calls/frame; via soft-glyph it is two. Choose accordingly.
- CIPHER stays OLED-exclusive — animation on the main grid never renders CIPHER (the Null cartridge is the only exception; see ADR-0015).
- A tiled slot is spatially periodic at 8 px. Any static pattern in one soft slot reads as a lattice in a still frame: a slope-1 diagonal reads as scanlines, scattered dots read as a dot grid. For an atmosphere plane, go aperiodic in TIME instead: hash-reposition sparse specks every few frames (
whirl’s backdrop is the worked example). Multiple slots buy distinct regions, never aperiodicity within a fill. - Features under ~4 px must be captured native. A 1-px pattern sits at the Nyquist limit of the standard 2:1 demo downscale, and any resample (encoder or browser) turns its drift into blinking bands: nearest-neighbour strobes the brightness, area averaging flickers the contrast while the mean reads steady. Record such surfaces with
tools/record-demo.sh --scale -1(its--scale-flagscomment carries the mechanics). - Select from the named library; extend it before bypassing it. A screen picks a named style from the tables above. A motion the library lacks follows the same road as a missing UI pattern (see
ascii-design-system.md): judged in the workbench, landed as a namedanimlabstyle + a table row here + a committed capture, and only then consumed by the screen. Per-screen bespoke motion is the thing this library exists to prevent.