# Amiga Juggler — realtime WebGL2

Een zelfstandige browserhommage aan Eric Grahams Amiga Juggler (1986). De scène
wordt iedere frame analytisch geraytracet — geen video, vooraf gerenderde
frames of 3D-meshes. Scène, camera, licht, timing en geluid zijn gekalibreerd
op de 24 originele HAM-frames; de figuur gebruikt een stabielere,
13h-geïnspireerde 3D-anatomie voor de vrij beweegbare camera.

## Starten

Er is geen buildstap en de demo heeft geen externe runtime-dependencies.
Serveer de map via HTTP en open de getoonde URL in een moderne desktopbrowser:

```fish
npm run serve
```

De pagina is Nederlandstalig; `en/` is de Engelse versie van dezelfde demo
(beide pagina's delen `styles.css`, `src/` en de teksten in `src/messages.js`).

Klik op **Start demo** of **Start fullscreen**. Met **Sluiten** keer je op ieder
apparaat terug naar het startscherm; op een desktop werkt `Esc` ook. WebGL2 is
vereist. Web Audio is optioneel: zonder (of met een geblokkeerde audiocontext)
draait de demo zonder geluid en meldt de bedieningsbalk dat. Alleen voor de
fullscreenoptie is toestemming van de browser nodig; browsers zonder
fullscreen-API tonen alleen **Start demo**. De snelle unittests draaien zonder
installatie met:

```fish
npm test
```

De lifecycle rond focus, pauze, audio en fullscreen, de bediening en de
gouden beelden van de capturemodus worden daarnaast in een echte
Chromium-browser getest. Installeer die testomgeving eenmalig en draai daarna
de browsertests of beide suites samen. Hiervoor is Node.js 20 of nieuwer
nodig:

```fish
npm install
npx playwright install --no-shell chromium
npm run test:browser
npm run test:all
```

Op een kale Ubuntu-installatie kan Playwright de benodigde systeempakketten
mee installeren met `npx playwright install --with-deps --no-shell chromium`.
De gouden beelden staan in `tests/browser/capture.spec.js-snapshots/`; ververs
ze alleen bewust, na een gewilde beeldwijziging, met
`npx playwright test tests/browser/capture.spec.js --update-snapshots`.

Tijdens de demo:

- de demo start in de **1986-modus**: 320×200 met dikke pixels in een
  4:3-kader (pixelverhouding 5:6 zoals op een monitor destijds), 4 bits per
  kleurkanaal zoals HAM, en de animatie stapt in de 24 discrete frames van het
  origineel (30 fps); kies **Modern** voor de vloeiend geïnterpoleerde weergave
  op volle resolutie en **1986** om terug te schakelen;
- de bedieningsbalk verdwijnt tijdens het afspelen na drie seconden rust en
  komt terug bij muisbeweging, aanraking, een toets of toetsenbordfocus; in
  pauze, met een open cameramenu of met de muis op de balk blijft hij staan;
- gebruik **Pauze** en **Speel** (of de spatiebalk) om de demo zonder tijdsprong
  stil te zetten en te hervatten; de camera blijft ook tijdens de pauze
  bedienbaar;
- stap met `,` en `.` één beeld terug of vooruit op het 24-beeldenraster van
  het origineel; stappen pauzeert de animatie en de balk toont het beeldnummer
  (`7/24`) zolang de 1986-modus actief is;
- sleep met muis of touch om rond de Juggler te draaien;
- gebruik Shift-/rechter-/middelsleep om het kader te verschuiven; met twee
  vingers worden pannen en knijpzoomen gecombineerd;
- gebruik het muiswiel of een knijpbeweging om in te zoomen;
- gebruik de pijltjestoetsen en `+`/`-` als toetsenbordalternatief;
- klik **Herstel camera** of druk `0` om de originele kadrering terug te zetten;
- schakel **Drone shot** in om de camera doorlopend rond de Juggler te laten cirkelen;
- `Esc` sluit eerst een open cameramenu en pas daarna de venstermodus;
- regel onderaan de animatiesnelheid van `0,01×` tot `2×`; de demo start op de
  originele snelheid `1×` (0,8 s per lus). De schaal is tot `1×` logaritmisch
  (elke 0,4 slider is een factor 10) en daarboven lineair, met `1×` precies op
  het streepje bij 80%. Net als bij het langzamer of sneller afspelen van een
  band verandert het geluid mee in toonhoogte en lengte.

## Reconstructie en 3D-rig

De scène combineert de teruggevonden broncode en de 24 originele frames met
een camera-onafhankelijke, gearticuleerde lichaamsbouw:

- **Model**: 70 bollen met Grahams materialen en kleurwaarden uit `robot.dat`.
  De lichaamsbouw volgt de slankere verhoudingen van Ben Hankes
  camera-onafhankelijke 3D-pose: romp 0,69→0,55, hoofd r 0,46 en vaste,
  links/rechts gelijke arm- en beenlengtes. Two-bone IK houdt handen en voeten
  op hun gemeten animatiedoelen zonder vervorming vanuit vrije camerahoeken.
- **Scène**: oneindige vloer met Grahams `gingham()`-checker (tegels van 3
  eenheden, gespiegeld om beide assen — de dubbele band bij de oorsprong is in
  het origineel zichtbaar en wordt exact gereproduceerd); lucht als
  sin²(elevatie)-mengsel ⟨.7,.7,1⟩ → ⟨.1,.1,1⟩; één verre lamp, positie en
  sterkte gefit op de schaduwrichting en het helderheidsverloop van de vloer.
- **Shading**: het model uit `rt1.c` — wrap-ambient (n·ẑ+1,5)·0,4 × 0,25,
  cos/r²-lamp met vaste belichtingsfactor, binaire zuiver witte glimpunten
  boven cos² > 0,95, spiegels als recursieve reflectie × 0,9, harde
  schaduwen, en géén gammacorrectie: alle kleuren staan in weergaveruimte,
  zoals het origineel ze naar het HAM-scherm schreef.
- **Camera**: gefit op de framedata — oog exact op (−6, 3,32, −6), blik 45°
  over de damborddiagonaal, vrijwel waterpas (horizon op rij 97 van 200),
  brandpunt 215 px met vierkante beeldpixels (beeldhoek ≈ 50° verticaal); de
  figuur staat op de wereldoorsprong.
- **Beweging**: de originele bewegingsdata is verloren; balcentra, hoofd,
  handen, voeten en de buigrichting van de gewrichten zijn daarom opnieuw uit
  alle 24 originele 320×200-frames gemeten. De 1986-modus gebruikt 24 discrete
  rigposes; de moderne modus interpoleert ze als een gesloten kromme.
  De schermbaan van de ballen wordt op een licht gekanteld 3D-jongleervlak
  gelegd; de handen bewegen op een parallel vlak er vlak achter. Daardoor
  blijven alle gemeten schermposities behouden, raken de handen de ballen ook
  werkelijk en gaan de ballen bij een vrije camera niet door hoofd of romp.
- **Geluid**: één "ding" per lus op het vangmoment (lusfase 0), zoals het
  origineel: een vrijwel zuivere sinus van 1 kHz met de gemeten trapsgewijze
  envelope in stappen van 50 ms (~0,5 s), nagemeten aan Ernie Wrights opname
  van een echte Amiga (`juggler.wav`). De sample wordt als tape afgespeeld:
  snelheid `r` geeft toonhoogte `r×` en duur `1/r×`, ook bij een live wijziging.

## Techniek

- 70 analytische bollen plus het oneindige gingham-vlak
- harde slagschaduwen en maximaal twee spiegelreflecties
- 1986-modus: raytracing op exact 320×200, 4-bit-kwantisatie
  (`⌊16·kleur+0,5⌋`, zoals `rt2.c`), nearest-neighbour naar een 4:3-kader
- moderne modus: native canvasresolutie op basis van de device-pixel-ratio,
  met FXAA als lichte tweede beeldpass; brede schermen tonen meer opzij
- framebudget in vier trappen: twee reflecties op volle resolutie, één
  reflectie, en in de moderne modus daarna renderschaal 0,75 en 0,5. Het
  budget meet eerst het scherminterval zonder renderwerk (mediaan van 24 lege
  animatieframes, begrensd op 60 fps), zodat een 30 Hz-scherm of een
  gedrosselde browser niet als trage GPU telt. Na rustige meetvensters
  probeert het budget weer een trap hoger; mislukt die proef, dan wacht de
  volgende twee keer zo lang (maximaal 16 vensters). Een nieuwe start, een
  andere beeldmodus of een ander canvasformaat begint op volle kwaliteit
- het canvas volgt `clientWidth`/`clientHeight`, zodat de korte
  schaal-animatie bij een moduswissel het renderformaat niet beïnvloedt
- een schermvergrendeling (`navigator.wakeLock`) houdt het scherm aan zolang
  de demo speelt; zonder ondersteuning verandert er niets
- gebufferd Web Audio-geluid met tape-snelheid, herstel na een verloren
  WebGL-context en pauzeren zonder tijdsprong
- laden en cachen: `index.html` laadt alle modules met `modulepreload` en de
  drie shaders met `preload`, zodat de modulegraaf in één ronde binnenkomt.
  Alle modules, shaders en `styles.css` dragen dezelfde `?v=`-versie (de
  shaders erven die van de module-URL) en worden een week gecachet; verhoog
  de versie in beide HTML-pagina's en alle imports bij iedere wijziging in
  `src/` of `styles.css`. De unittest `asset-version` bewaakt dat in een
  git-werkboom: gewijzigde assets zonder nieuwe versie laten `npm test`
  falen. `.htaccess` geeft `.glsl` een tekst-MIME-type (dus gzip) en houdt
  tests, ontwikkelconfiguratie en `VERVOLG.md` buiten de publieke site

Bewuste omissies: HAM-kleurfringing (het per-scanlijn-foutgedrag van de
encoder) wordt niet geëmuleerd, en de audio-envelope houdt de harde
50 ms-stappen van de originele speler.

## Historische bronnen

- De teruggevonden [Raytracer 1.0-bron van Eric Graham](https://github.com/AlphaPixel/Eric-Graham-1987-Juggler-Raytracer-1.0)
  (`robot.dat`, `rt1.c`–`rt3.c`) voor materialen, vloer en shadingmodel.
- De [technische analyse en frame-/audiocaptures van Ernie Wright](http://www.etwright.org/cghist/juggler.html)
  (24 PNG-frames en `juggler.wav`) als kalibratie- en verificatiemateriaal.
- De [wiskundige reconstructie van Meatfighter](https://meatfighter.com/juggler/)
  en [Ben Hankes realtime WebGL-versie](http://13h.org/juggler/) als aanvullende
  vergelijking voor de hoge/lage showerbaan en een camera-onafhankelijke 3D-pose.
- De oorspronkelijke Juggler werd in november 1986 uitgebracht als 24
  HAM-frames op 30 fps (0,8 s per lus) met één "ding" per lus; camera- en
  bewegingsdata overleefden niet en zijn uit de frames teruggemeten.
