# Adi Dassler — native HTML5 restoration

Built from the supplied `adi_dassler.zip` archive. Open `/adi-dassler/index.html`
on the portfolio's development server, or serve this directory independently:

```sh
python3 -m http.server 4173 --directory public/adi-dassler --bind 127.0.0.1
```

Then open http://127.0.0.1:4173. An HTTP server is required for the asset manifest.
No build step, npm dependencies, Flash plugin, or SWF emulator is required.

## Recovered original material

- The 972 × 460 workshop and night backgrounds, 972 × 96 header, and footer.
- Embedded Rockwell fonts, original music and ambient sound.
- Adi's daylight movement frames and the original light texture.
- Card backgrounds, portraits, label texture, and story facts from the English XML.
- Four product rotation sequences: 74, 62, 74, and 73 frames respectively.
- The intro, main film, Gonzales film, and four spoken stories, converted from FLV.
- The original Mac and PC screensaver ZIP downloads, retained unchanged as historical files.

## Native interactions

Enter or skip the intro; select the workshop objects; browse four story cards;
flip cards; slide out the story-information slip; rotate products by dragging, touch, or arrow keys; play/pause narration;
watch the main film or the Gonzales film; return to the workshop; mute sound;
and reveal the original screensaver downloads.

Tab moves between controls. Escape returns from a film or story. Left/right arrows
browse stories, or rotate the object while its canvas is focused. Media pauses
when the tab is hidden. Reduced-motion preferences disable ambient and automatic
rotation animation. The original stage scales to fit the available width.

## Fidelity boundaries

This is a working reconstruction using the original assets, not a frame-exact port
of the Flash runtime. Card layout, object interaction timing, hotspot shapes, dust, and light behavior
are rebuilt in CSS/JavaScript. The card choreography now follows the recovered
`CardHandler.as`: three independently animated layers, a two-second curved entrance
with 250 ms staggering, a 900 ms back-to-front shuffle after a 100 ms delay,
a 500 ms information-slip reveal, a 700 ms landscape turn for the Gonzales film,
and a reversed departure. Pointer tilt is limited to eight degrees. Cubic easing
and the original operation order are retained, while the Papervision3D world
coordinates are adapted to CSS perspective rather than rendered by the original
camera. The five randomized Adi gestures are not fully reproduced; one recovered
daylight movement sequence is used. A click-to-enter
screen accommodates modern browser audio policies. The original header/footer were
static GIFs in the supplied HTML and remain static here. No historical tracking is run.
The legacy screensavers have not been executed or tested on modern operating systems.
No subtitles are invented for the archived English files' empty subtitle entries.

## Recovery pipeline

JPEXS Free Flash Decompiler 26.4.0 extracted images, fonts, sounds, ActionScript,
and product sprites. A temporary Java 21 runtime was used for recovery only;
neither Java nor JPEXS is required to run the restored site.

The repeatable asset packaging script is `scripts/adi-dassler/prepare-assets.py`
in the project root. It accepts the JPEXS export directory and unzipped archive
directory. It uses Pillow to encode original images as WebP.

For the main SWF, export `image,script`, plus `sound,font`. For each of
`assets/product_1.swf` through `product_4.swf`, export `image,script`, then export
the sprite ID identified by `RotationObject.as` to `rotation_N`.

Video conversion (replace NAME with introMovie, mainMovie, or gonz):

```sh
ffmpeg -i flv/NAME.flv -map 0:v:0 -map 0:a:0 \
  -vf 'pad=ceil(iw/2)*2:ceil(ih/2)*2' -c:v libx264 -crf 20 \
  -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart NAME.mp4
```

Narration conversion (bern, superstar, gonzales, nastase):

```sh
ffmpeg -i flv/audio_NAME.flv -vn -c:a aac -b:a 128k NAME.m4a
```

The full original archive and decompiled source remain separate from the public
site. `product_5.swf`, placeholder media, and historical tracking configuration are
not used because the original English navigation and card-media mapping expose
four stories.

## Verification

JavaScript syntax checked with `node --check`. Browser checks cover workshop entry,
story navigation, flipping, keyboard rotation, narration, converted video playback,
and returning from films. Asset integrity checks verify all rotation frames, image
decoding, referenced local files, and media stream metadata. Browser and historical
runtime differences above remain documented rather than claimed as pixel-perfect.

Animation regression checks: `node --test scripts/adi-dassler/tests/card-motion.test.cjs`.
They cover entrance staggering, closing the slip before a shuffle, cancellation
cleanup, exit-frame persistence, reduced motion, and preventing delayed playback
after navigation.

## Mobile workshop

At widths up to 700 px, the room fills the available height and can be panned
horizontally. The original artwork and all hotspots share one camera transform.
Drag to explore; tap a plus marker to open a story. Edge buttons provide an
alternative to dragging. Panning is bounded and includes gentle momentum, disabled
with reduced motion. A 6 px gesture threshold prevents drags from activating
hotspots, while vertical gestures remain available for scrolling. Returning from
a story retains the camera position.

Cards use an unscaled portrait layout with 44 px controls. Swipe the front card
or use arrows to change stories. Information opens over the card instead of outside
the screen. Product rotation remains a separate drag gesture. The original films
use native video controls. Desktop layout and its card choreography are retained.

Mobile verification uses 390 × 844 and 320 × 740 browser viewports, plus camera
regression tests for drag-versus-tap, vertical scroll intent, bounds, position
retention, and keyboard activation. Run all tests with:
`node --test scripts/adi-dassler/tests/*.test.cjs`.

Mobile drag refinement: pointer movement is rendered once per animation frame using
a composited transform. Release speed is estimated from the recent 80 ms of movement,
with frame-rate-independent exponential deceleration (240 ms time constant). Touching
a coast stops it without opening a hotspot. Adi movement and dust animation pause
while the camera moves. Tests include release-only swipes, burst events, 60/120 Hz
consistency, stationary release, and reduced motion.

## Hotspot audit — 27 October 2026

Compared the restored buttons with the archive's `WorkshopController.as`,
`WorkshopButton.as`, `CardHandler.as`, placed SWF symbols, and
`xml/cards/cardmedia.xml`. The live Flash workshop has four story buttons:

| Workshop object | Original instance | Destination | Narration |
| --- | --- | --- | --- |
| Shelf sneaker | product_3 | Mark Gonzales | audio_gonzales |
| Tennis racket | product_4 | Ilie Nastase | audio_nastase |
| Workbench boots | product_1 | Miracle of Bern | audio_bern |
| Basketball | product_2 | Superstar | audio_superstar |
| Television | Restoration shortcut | Main film | Main film soundtrack |

The four restored mappings are correct. Only Gonzales has the additional `gonz`
film. The archive's English workshop tooltip XML gives inconsistent product names;
the running controller, card content and media assignments are the mapping authority.
A main-film button symbol exists in the SWF library but has no placement reference;
the original controller exposes the film through workshop navigation. The television
shortcut is therefore an explicit restoration addition, not a recovered fifth story.

The original transparent hit areas are broad, slanted regions extending away from
the visible objects. This version keeps separate object-sized targets, avoiding
ambiguous overlap with the added television shortcut. The racket target was tightened
and moved onto the object (x792, y82, 52 × 113 in the 972 × 460 artwork).
All targets now retain a minimum 44 × 44 CSS-pixel hit area after camera scaling.
Mobile room framing now covers the entire stage with the artwork behind floating
controls. The bottom navigation has no opaque bar and its non-button area allows
pointer events through to the room; hotspot centers clear the controls on short
portrait screens. This supersedes the initial reserved top/bottom control strips.
Artwork and buttons remain in the same transformed room, so they pan together.

Browser verification: clicked every hotspot at 1280 × 800 and 320 × 568; all four
story titles matched and both TV checks started `mainMovie.mp4`. At the small
portrait size, all objects were reachable using the pan controls and targets measured
at least 44 CSS pixels. Checked the right-side composition at 390 × 844. Keyboard Tab
revealed the off-screen racket, and Enter opened Ilie Nastase. Returning from each
story preserved the camera position. Existing drag/animation regression coverage plus
the new short-screen geometry check totals 14 passing tests. The geometry test reads
the actual CSS target bounds rather than a duplicate list of coordinates.

These are browser viewport and automated interaction checks, not physical iPhone or
Android hardware measurements. Actual touch latency and browser chrome behavior
still warrant a device pass before release.

Touch capture follow-up: dragging from a hotspot transfers implicit pointer capture
from the button to the room. A child's bubbling `lostpointercapture` event previously
cancelled the gesture. The handler now cancels only when the room itself loses capture
for the active pointer. A regression test covers child capture transfer, unrelated
pointer IDs, and actual room capture loss (15 tests passing). This addresses a touch
event path relevant to Chrome device emulation; it has not yet been verified in an
attached Chrome DevTools touch session. Viewport resizing alone is not touch emulation.


## Native header and footer

The original full-width GIF chrome is replaced with semantic HTML navigation,
live text links and CSS stripes. The adidas logo is a local SVG containing vector paths for both the trefoil and
wordmark, so no header elements depend on the original raster screenshot. Products opens the four restored stories; Ads &
downloads opens the film and original screensaver downloads. Corporate links lead
to current adidas domain roots rather than inventing missing archived pages.

Below 701 px the header uses a 44 px hamburger toggle with expanded-state labeling.
The menu closes on selection, Escape, outside pointer input, focus leaving the header,
and breakpoint changes. Submenus use native details/summary controls. The mobile
footer wraps live links rather than cropping the original footer image. Browser
checks covered mobile expansion, story selection, Escape dismissal and desktop layout;
all 15 existing motion/camera tests still pass.

Logo vector source: [Original Adidas logo.svg, Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Original_Adidas_logo.svg), credited there to adidas. Geometry retained; fill changed to white and unused export metadata removed. Stored locally as `images/adidas-originals.svg` with no external resources or font dependencies.

## Larger desktop framing

Above 700 px, the site is fluid up to 4320 CSS pixels wide. Its width also respects
the available viewport height, reserving 168 pixels for the header, footer and note.
The existing 972 × 460 stage scales as one unit, preserving hotspot alignment and
card choreography; video grows with that stage. Surrounding space uses the room's
dark background. Mobile framing is unchanged. Browser checks verified a 1920 × 909
stage at a 1920 × 1080 viewport, story selection and video playback. The browser's
large-viewport check rendered at 4096 pixels wide when 4320 was requested. Original
raster assets and video remain at their recovered resolution; this is responsive
display scaling, not a claim of new high-resolution source detail.


## Film audio and timeline controls

The original intro FLV and converted MP4 both have a near-silent opening through
about 10.34 seconds, with sound around 10.34–13.08 seconds. No soundtrack was lost
in conversion. A separate IntroMusic asset exists in the SWF library, but the
recovered code does not show it being played; it has not been layered onto the film.

The intro and main film now use a shared playback bar for play/pause, seeking, time,
mute, volume and fullscreen. Skip/Explore occupies its own grid cell; portrait
layouts reserve rows with 44 px touch controls. Native controls remain the fallback
if the custom-control script does not load. Gonzales's in-card player retains its
native controls. Controller tests cover seek updates, unknown duration, play/pause
and volume/mute synchronization. All 17 tests pass. Browser checks confirmed seeking
with byte-range support; Python's basic http.server preview does not support those
requests, so seeking there is unreliable even though the timeline logic is correct.


## Narration on card expansion

Opening story information calls narration.play() synchronously from the user's
click/tap, before awaiting card animation. Global mute is respected. Closing the
panel, switching stories, opening video, or hiding the document stops narration.
The existing Listen/Pause button remains available when browser playback is blocked.

Each waveform contains 64 RMS amplitude bins extracted from that story's original
converted recording. Elapsed bars and timestamps follow the HTML audio media clock;
there is no invented animation, microphone access or Web Audio routing dependency.
If waveform loading fails, narration and the manual control remain functional.
Information panels scroll when needed so waveform controls cannot overlap facts.
The test suite now has 19 passing tests, including immediate gesture playback,
waveform progress, blocked playback fallback and stale navigation errors.


Waveform seeking uses a native range input over a 44 px waveform target. Click/tap
and drag preserve the current play/pause state. Arrow keys seek five seconds,
Page Up/Down ten seconds, and Home/End jump to the boundaries; these keys do not
propagate to story navigation. Dragging temporarily shows the full recording
waveform. Unknown durations disable seeking. Desktop and portrait browser smoke
checks verified position changes and resume, with 22 tests passing.


## Conservative 2× derivatives

`hd/` contains deterministic Lanczos 2× upscales made directly from the recovered
JPEG/PNG and FLV sources by `scripts/adi-dassler/upscale-assets.py`. The original
web assets remain untouched at their original URLs. No generated imagery is used.
Opaque stills receive a mild unsharp mask (radius 1, 20%, threshold 3); transparent
sprites are resampled without sharpening to preserve composite edges. All scene
coordinates, silhouettes, colors and timings are retained. The rotating-object
canvas backing resolution is doubled with the frames, while its CSS size is unchanged.

Films use Lanczos 2× scaling, mild luma-only sharpening, H.264 CRF 21 with a
3.2 Mbps video ceiling, and byte-for-byte copied existing AAC audio. Desktop
playback selects these derivatives when rendered pixel width exceeds 1100;
portrait mobile and Save-Data connections retain the smaller original conversions.
Source selection happens before playback, never during playback or resizing.
These derivatives improve resampling and edge clarity, not genuine captured detail.

Validation: 318 still/frame derivatives have exactly double their original dimensions
and retain their alpha mode. All three films decode successfully, retain their frame
counts, and have identical AAC stream hashes to the existing conversions. The Gonzales
FLV is 313×222, so its derivative is 626×444 (the earlier conversion rounded width
to 314). The 22 interaction tests pass. Browser smoke checks covered all four cards,
object rotation, 1920px desktop HD playback, and 390px portrait standard playback.

Mobile playback controls now occupy one 44px-touch-target row directly below the
film: play/pause, seek, mute, fullscreen and Skip/Explore. The separate volume
slider and visible time readout are desktop-only; mobile uses device volume, and
the seek control retains its accessible elapsed/duration label. Checked at 320px
and 390px widths with no wrapping or overlapping controls.
