jarvising · entry 001 jarvising.com

e·nig·ma

/ɪˈnɪɡ.mə/ noun; here, a teardown in scroll Entry 001

A scroll-driven teardown of the Enigma I on a night watch in Hut 6, from oak lid to reflector, ending in a faithful machine you can type on.

The Enigma I lifted apart into six layers on a desk in a wartime hut, lit by a single pendant lamp, a blackout window and a notice board behind it. Open the teardown → WebGL · about two minutes
sound optional · keyboard works

Source: github.com/StovBuilds/jarvising · src/enigma/

What it does

You scroll, and the machine comes apart. Nine chapters run from the hero orbit through a timeline of Scherbius's patent, then the camera leans in to read the card inside the lid, the oak case lifts into six labelled layers, the middle rotor slides offstage and explodes into its own stack, one keypress is traced as glowing current through plug board, rotors and reflector and back, and the rotors step like an odometer.

Then the room takes over. The machine sits on a desk in Hut 6 at Bletchley Park, on the night watch: one pendant lamp, a blackout window, a notice board with the day's crib pinned to it. Two chapters tell the story of the people who broke it, from Rejewski's paper reconstruction in 1932 and the Pyry hand-over of July 1939 to Turing and Welchman's bombes and the thirty-year silence that followed.

The last beat is a working Enigma I: rotors I·II·III, reflector B, ten plug pairs, the double-step anomaly included. Type on your keyboard or tap the keys; the lamps answer and a paper tape keeps both alphabets. "Copy secret link" bundles the start position and ciphertext into a URL, and because Enigma is reciprocal, whoever opens it watches the machine type the plaintext back out.

Sound is opt-in and synthesised live: key clacks, lamp thunks, rotor ratchets, a lid creak, and faint Morse under the hero. No audio files either.

How it was made

Everything is generated at runtime. The oak grain, the crinkle-paint chassis, the alphabet rings, the key caps, the painted hut boards, the floorboards, the notice board and the card inside the lid are all 2D canvases drawn on the fly and used as textures. The machine is built from primitives as named parts, each carrying a resting position, a full explode vector and a small lag, so one scalar can pull the whole thing apart in a staggered order. The room is a second set of primitives around it, lit by one shadow-casting spotlight where the pendant bulb hangs.

One number drives the page. Scroll position becomes a 0–1 progress value, smoothed per frame. Every effect is a smoothstep window on that value: the explode envelope, the lid hinge, the rotor solo, the two current tubes revealed by advancing their draw range, the camera path sampled from ten orbit keyframes, the chapter copy, the callout fade-ins. Nothing is on a timer, so scrubbing backwards just works.

The labels are 3D points. Each callout is an invisible object parented to the actual mesh it describes, so it rides along through the explode, the lid hinge and the rotor spin. Every frame it is projected through the camera, placed to the near side, clamped inside the viewport and away from the copy, pushed clear of any label it would overlap, and drawn as HTML with an SVG leader line back to the point.

The machine's parts come from Blender now. The case with its rim moulding, corner plates, hinges and latch; the lid with its inner frame; the riveted deck and plates; a dished, ringed key cap; a knurled lamp bezel; a twin-jack plug socket; the serrated thumb wheel, alphabet ring, pinned core and contact plate of each rotor. Each is one mesh in a small glTF parts library, authored by a Python script in the same local frame as the primitive it replaces, so the page keeps every position, hinge, explode vector and callout anchor and simply swaps geometry in. If the file does not arrive within a few seconds, the primitives build as before.

The bombe is the other modelled asset. Chapter 008 needed a machine to look at, and primitives in the browser were never going to make 108 drums read. It is built the same way, welded and quantised to under a megabyte, and loaded lazily once you are past the rotor chapter. Drum colours follow the period convention of one colour per rotor type.

The cipher is separate from the picture. An 80-line class holds the historical wirings and does the encryption; the scene only asks it which lamp lit. The live current trace reuses the same path factory the authored chapter uses, evaluated at an explode value of zero.

Built with Claude Code from a one-line brief, in the same session that shipped it as experiment 008 on jstov.uk's lab. This is now the primary version: the hut, the lid card, the anchored callouts and the two Bletchley chapters were added here first.

How to make your own

  1. Write the mechanism first, with no graphics. A pure function you can test against a known vector. For Enigma: at AAA with no plugs, pressing A five times gives BDZGO.
  2. Model in named parts, not one mesh. Give every part a base position, an explode direction and a lag. The teardown is then base + dir × clamp(e × (1 + lag) − lag) for one scalar e.
  3. Derive everything from one progress value. Read scroll, normalise to 0–1, ease it a little each frame, and express each beat as a smoothstep between two progress values. Never start a timer.
  4. Keyframe the camera in spherical coordinates. A short list of {p, theta, phi, r, target} entries, interpolated by progress, reads far better than any free-fly control.
  5. Put labels in 3D and project them. Anchor to a part, call project(), position an HTML element, draw the leader line in one SVG you rewrite per frame.
  6. Keep two timelines. Scroll position maps to a "beat" value through one function, so new chapters can be inserted by remapping instead of retuning every window. The Bletchley chapters hold the machine on its stepping beat while the camera pulls back.
  7. Add a debug parameter that pins progress. ?p=0.42 lets you screenshot any beat on a machine with software GL, where smoothed values never settle.
  8. Reach for Blender when primitives stop reading. Keep it scripted so the asset is reproducible, keep the scene scale identical, and load it lazily so the first paint stays light. Skip Draco and meshopt if your page ships a strict content-security policy; quantisation alone halves the file and needs no decoder.
  9. Replace geometry, not structure. Ship a parts library named after your scene graph, authored centred on the same local origins, and keep your positions and animation in code. One catch: if you pull quantised geometry out of its node, convert positions to float first, or anything larger than a unit gets clamped flat.
  10. Make sound opt-in and synthesised. Create the AudioContext on the user's click, and build clacks and ratchets from filtered noise bursts. Zero downloads, and no autoplay policy to fight.

The whole cipher, for the taking (the full piece is in src/enigma/ on GitHub):

// Enigma I: rotors I·II·III, reflector B. Historical wirings, public domain.
const WIRINGS = ["EKMFLGDQVZNTOWYHXUSPAIBRCJ", "AJDKSIRUXBLHWTMCQGZNPYFVOE", "BDFHJLCPRTXVZNYEIWGAKMUSQO"];
const NOTCHES = [16, 4, 21];           // Q, E, V
const REFLECTOR = "YRUHQSLDPXNGOKMIEBFZCWVJAT";
const mod = (n) => ((n % 26) + 26) % 26;

const fwd = WIRINGS.map((w) => [...w].map((c) => c.charCodeAt(0) - 65));
const rev = fwd.map((f) => { const r = []; f.forEach((v, i) => (r[v] = i)); return r; });
const ref = [...REFLECTOR].map((c) => c.charCodeAt(0) - 65);
let pos = [0, 0, 0];                  // left → right, 0 = 'A' in the window

function step() {                     // includes the double-step anomaly
  if (pos[1] === NOTCHES[1]) { pos[0] = mod(pos[0] + 1); pos[1] = mod(pos[1] + 1); }
  else if (pos[2] === NOTCHES[2]) pos[1] = mod(pos[1] + 1);
  pos[2] = mod(pos[2] + 1);
}

function press(letter, plug = new Map()) {
  let x = letter.toUpperCase().charCodeAt(0) - 65;
  step();
  x = plug.get(x) ?? x;
  for (let i = 2; i >= 0; i--) x = mod(fwd[i][mod(x + pos[i])] - pos[i]);
  x = ref[x];
  for (let i = 0; i <= 2; i++) x = mod(rev[i][mod(x + pos[i])] - pos[i]);
  x = plug.get(x) ?? x;
  return String.fromCharCode(65 + x);
}

Plug pairs go in the map both ways. Reset pos to the same start position to decrypt.

See also

Entry 002 is in progress.

← all entries