Replace the single ethereal loop with the operator's chosen set — Music for
Manatees, Dewdrop Fantasy, Eastminster, Overheat, Concentration (Kevin MacLeod,
CC BY 4.0, drumless). The Music source now plays one track and every 5 min
crossfades to a random OTHER track (pure HEFScrub.pickNextIndex, never an
immediate repeat), altitude-independent, reusing the two audio elements; both
primed in-gesture for Safari. Intervals injectable via window.__hefMusicRotateMs
/ __hefMusicXfadeMs.
Media loudness-normalized + transcoded under the 25MB LFS limit (build_audio_media
MUSIC), committed via LFS; ethereal loop retired. CC-BY attribution added to
credits.html. Docs updated (music-candidate-pool = final set; audio-candidate-pool
ethereal section superseded).
+2 node unit (pickNextIndex), +2 e2e (music-playlist), audio-source spec updated.
40 unit + 49 e2e green; live probe confirms playback + rotation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the single 0–10 'Audio' slider into two controls:
- Audio — a dropdown selecting the source: None / Soundtrack / Music
- Volume — the renamed 0–10 slider (master gain for the active source)
Music is one ethereal, relaxing ambient loop played the same at every
altitude (no per-altitude crossfade), per the innerflo.me brief. Sourced
CC0: deadrobotmusic 'Ambient F Sharp Minor Ethereal Choir Pad' (freesound
#808032); added to build_audio_media.py, the candidate-pool doc, and the
public credits colophon. Media stays gitignored; run build_audio_media.py.
Routing lives in a pure, node-tested HEFScrub.audioPlan(source, level, frac);
app.js's restAudio/blendAudio consult it. None greys out Volume. The welcome
screen's mirror level slider is relabeled Volume too. i18n for all 4 langs.
Spec: docs/superpowers/specs/2026-07-01-audio-music-option-design.md
Tests: +5 node unit (audioPlan), +4 e2e (audio-source.spec.ts); 32 related
e2e green, 38 node unit green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real-browser eyeball: below-0x reverse jittered because the base loop
clips aren't all-intra (the flagged risk). Narrow the slider to 0x–4x
forward (still continuous, step=any); 0x still freezes; OS reduced-motion
behavior unchanged. Removes the manual rAF reverse loop and the
HEFScrub.reverseStep helper + its unit tests.
Tests: node unit 10/10; a11y e2e 12/12 (Task 5 now 3 slider tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Continuous -2x..2x slider drives ONLY the resting altitude loop video
(#vid-loop); the morph/transition video is scrub-driven and untouched.
Forward = native playbackRate; ~0x = freeze; below 0x = a manual rAF
reverse loop (native negative playbackRate is unreliable). Reverse math
is the pure, node-tested HEFScrub.reverseStep helper.
Reduced-motion accessibility is preserved but now derives live from the
OS prefers-reduced-motion setting (no visible toggle): when active the
loop stays frozen and the slider is disabled. Retires hef.reduceMotion.
i18n: speed.label (en/es/fr/ja); warn.body reworded to point at the OS
setting.
Tests: +4 node unit cases for reverseStep (14/14 green); a11y e2e Task 5
rewritten for the slider (13/13 green); static-build e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate the universe on one clip per altitude + connecting morphs (~126 MB, ~10-20s),
then grow the pool in the background. The random pick (pickPoolClip/pickRandomMember)
is restricted to fully-loaded clips — base + connecting morphs both ways cached
(HEFPreload.eligibleDestinations) — so a transition never stalls on un-downloaded
media and new clips become reachable only once fully loaded. Pure planner in
preload.js (+ node tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integrates the fully-static Cloudflare publish line (live at
benstull.art/human-experience-simulator) into main: the static build (Pages + R2,
manifest-only media tree, baked API JSON), the "Run simulation" start button, the
public CC-BY/BY-SA credits page with the "License & reuse" CC BY-SA 4.0 derivative
declaration (Option A + D1), the repeatable Cloudflare deploy script, and assorted
load/audio fixes (progressive boot, scaleAudioUrl via mediaBase).
Clean auto-merge with main's per-clip affect/feelings + i18n work — only app.js and
altitude-lock.spec.ts overlapped, both auto-resolved. Verified green on the merged
tree: node 29, pytest 315 passed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
s0031 removed "About this work" to avoid duplicating it on the separate About
page, but that section WAS the D1 compliance — it declares the altered work is a
derivative offered under CC BY-SA 4.0, which is what satisfies the ShareAlike
obligation for the BY-SA source clips. Without it the live site credited the
BY-SA authors but never declared the derivative share-alike.
Restore the declaration on the credits page, under the heading "License & reuse"
instead of "About this work" so it isn't confused with the separate About page
(which covers the project's intent). Tests assert the declaration is present and
that the heading is NOT "About this work".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CC BY-SA work-license declaration ("About this work") was on the credits
page, but the About page (built on the parallel feat/accessibility-pass) will
own that content; keeping it in both places would duplicate it. Remove the
section from credits.html and make the footage note self-contained (it still
states the footage is altered — the per-clip "indicate modifications" cue stays
with the attributions). Tests updated to assert the declaration is intentionally
no longer on the credits page. The .declaration CSS rule is left in place for the
About page to reuse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CC BY / CC BY-SA footage needs its author + license shown to the viewer, and the
installation alters every frame (a derivative). Per-clip license/source already
shipped in clips.json but only surfaced in the dev panel and internal review
pages — the public experience showed nothing, so the static benstull.art site
was not attribution-compliant.
Option A — add a manifest-driven credits/colophon (credits.html + credits.js):
the page fetches the same clips the experience uses (baked clips.json in static,
the live API otherwise) and renders one attribution block per clip (title,
license, source), HTML-escaped, sorted, never silently dropping a clip. An
"ⓘ Credits & licenses" link in the header (localised en/es/fr/ja) makes it
reachable from the work — what CC's "reasonable to the medium" calls for. Audio
is all PD/CC0, credited as a courtesy block.
Option D1 — the colophon declares the altered work itself is offered under
CC BY-SA 4.0 and states the footage is modified, satisfying the ShareAlike +
"indicate changes" obligations for the BY-SA sources. Non-commercial.
Ships in the static build (added to PUBLIC_ASSETS). Verified: 41/41 clips
credited incl. all 7 CC-BY/BY-SA clips, 0 missing a license. Tests: node units
for creditEntries/creditsListHtml + escaping + page structure/declaration;
pytest build-output assertion; Playwright e2e (page lists attributions, header
links through).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relative asset paths (work at dev-root and under the /human-experience-simulator
deploy prefix), config.js gate, devLiveReload no-ops in static mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ascending the altitude dial and landing on the higher-altitude clip jumped ~3
seconds. A baked morph `transitions/{h}__{l}.mp4` spans src@0 (frac 0) ->
dst@~LOOP_TAIL_S (frac 1) and is scrubbed bidirectionally, but the steady loop
element always seeked LOOP_TAIL_S (3.0s) on landing. Descending lands on dst at
its tail (matches the seek — the s0026 loop-from-tail fix); ascending lands on
src at the morph's HEAD (src@0), so seeking the tail was a 3s jump.
Make the loop landing frame direction-aware: pure `HEFScrub.loopLandFrame(dir,
loopTailS)` returns 0 when ascending, loopTailS otherwise. `loadLoop` takes a
`landFrame`, stores it as `dataset.loopStart`, and the timeupdate/ended wrap
handlers honor it (the base clips are already seamless crossfade-loops, so a
reverse landing wraps [0,dur] cleanly). The settle path lands explicitly on the
direction's frame so a fast ascent never jumps even if the heading clip wasn't
preloaded in time. App-side only — no 154-morph LFS re-bake.
Verify: node unit (loopLandFrame) + a new reverse-landing e2e asserting the loop
anchors at the head (loopStart "0"), and the existing forward "loops from tail"
e2e still green (12/12 altitude-lock).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per plan Task 2. Adds <audio id=aud-b>; blendAudio(lo,frac) mixes the two
adjacent scale soundtracks by knob fraction (element reuse across crossings so
only the genuinely-new scale reloads); restAudio settles a single scale at rest.
Diagnostic readout now reports whichever element is audible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per plan Task 1. position->segment, frac->currentTime, audio gains, and
integer-crossing detection; 9 node --test cases green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>