03Techniques
The signature: parametric helix winding
The whole site is one React Three Fiber scene, loaded with no build step via an ES-module importmap (react 18.3.1, @react-three/fiber 8.17.10, @react-three/drei 9.114.3, three 0.160.0, htm 3.1.1 — markup as htm tagged templates with explicit React.Fragment, since the fragment shorthand crashes R3F's reconciler in a no-JSX build). If the stack fails to resolve or WebGL is absent, a static SVG fallback ships instead of a dead canvas.
The strip is two things. The leather is a single ribbon BufferGeometry — 541 sample pairs, six per letter — whose vertices are re-marched every frame the geometry changes: wound samples follow the analytic helix (radius R = L·0.36 / 2π for circumference L in letters, axial pitch 0.325 per turn), and unwound samples switch to a numeric march with a gravity steer, so the loose tail dives off the staff, hits the case floor, and pools into a flat coil whose curvature decays as it feeds — the coil visibly un-pools as the intro winds. The ninety letters are one InstancedMesh: a canvas-drawn glyph atlas (27 cells, 96×128, drawn in Marcellus — no raster files) sampled by a per-instance cell attribute in a small ShaderMaterial, with a per-instance angle attribute driving a raking-light term so the row facing the read-line brightens. Each instance's basis is set so glyphs stand upright along the staff axis — which is precisely why the decoded rows read horizontally only when turns stack.
Everything the HUD says is measured, not scripted: shear is recovered from the instanced angles (the fold of the angular slip between letter 0 and letter round(L), converted to letters per turn), the read panel picks the physically nearest letter per turn along each axial line (idx = round(a + n·L)), and "letters read true" counts character matches against the dispatch. A wrong integer staff — four or six letters round — stacks its columns perfectly and still reads garbage, so the HUD gets its own state for it: COLUMNS TRUE · TEXT FALSE. The honest failure is driven, not narrated: CUT THE STRIP leaves 24 letters — 4.8 turns on the true staff, rows of four or five letters — and every wrong diameter produces fragments no worse, which is the real cryptographic point.
- R3F + drei — Canvas with DPR capped at [1,2]; OrbitControls (damped, pan/zoom off, ±0.55 rad azimuth) so the case can be peered around but never lost.
- State probe — window.__scytale exposes lettersPerTurn, measured shear, turns, decoded rows, match count and status; all QA gates on state, never wall-clock.
- Rest state — the strip is already mid-wind at 5.2 letters/turn when the page settles: half-legible rows, the tail still coiled on the floor, dust in the case light. Reduced motion snaps the wind complete and switches the frameloop to demand.
- Hygiene — context-loss fallback, IntersectionObserver pause when scrolled past, rAF throttled when hidden, clamped frame delta on resume.
04Honest numbers
Measured from the running case
90 = 5 × 18letters on the strap — five rows of eighteen at the true circumference of 5 letters/turn
3.2 – 7.5slider range, letters per turn, 0.05 steps; radius spans 0.18–0.43 world units
+0.20 → 21/90at the resting 5.2 letters/turn the columns shear a fifth of a letter per wrap; 21 of 90 letters still read true
24 → 4.8 turnsthe cut strip on the true staff — rows recover 5,5,5,5,4 letters; the key becomes unprovable
The historical claims are hedged where the literature hedges: Plutarch (five centuries after Lysander) is the fullest source for the scytale as a cipher, and some modern scholarship reads the earlier mentions as a plain message-baton. The dispatch itself is our reconstruction — the battle of Aigospotamoi, 405 BC, is real; the strap's text is not a surviving artefact and the page says so.