Experience tuning: mood-through-morphs fix, auto-advance, label wrap, binaural candidates #33

Merged
benstull merged 10 commits from feat/experience-tuning into main 2026-07-03 01:00:22 +00:00
2 changed files with 70 additions and 0 deletions
Showing only changes of commit 969236b3f7 - Show all commits
+47
View File
@@ -0,0 +1,47 @@
import { test, expect, Page } from "@playwright/test";
// Shareable deep-link: ?audio=none|soundtrack|music presets the Audio source on load
// (and ?vol=0..10 the master volume), so a link opens straight into a chosen sound mode.
async function ready(page: Page) {
await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 });
}
async function launch(page: Page) {
await page.locator("#welcome-launch").click();
await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 });
}
const srcVal = (page: Page) => page.locator("#audio-source").inputValue();
test("?audio=music preselects the Music source and survives launch", async ({ page }) => {
await page.goto("/?audio=music");
await ready(page);
expect(await srcVal(page)).toBe("music"); // set at init, before launch
await launch(page);
expect(await srcVal(page)).toBe("music");
});
test("?audio=soundtrack preselects Soundtrack", async ({ page }) => {
await page.goto("/?audio=soundtrack");
await ready(page);
expect(await srcVal(page)).toBe("soundtrack");
});
test("?audio=none preselects None and disables Volume", async ({ page }) => {
await page.goto("/?audio=none");
await ready(page);
expect(await srcVal(page)).toBe("none");
await expect(page.locator("#audio")).toBeDisabled();
});
test("an invalid ?audio value is ignored (stays default Soundtrack)", async ({ page }) => {
await page.goto("/?audio=banana");
await ready(page);
expect(await srcVal(page)).toBe("soundtrack");
});
test("?vol sets the master volume for the shared link", async ({ page }) => {
await page.goto("/?audio=music&vol=7");
await ready(page);
await launch(page);
expect(await page.locator("#audio").inputValue()).toBe("7");
});
+23
View File
@@ -1410,6 +1410,27 @@ function syncVolumeDisabled() {
if (slider) slider.disabled = audioSource() === "none";
}
// Shareable deep-link: ?audio=none|soundtrack|music presets the Audio source on load
// (and ?vol=0..10 the master volume), so a link opens straight into a chosen sound mode
// — e.g. share …/?audio=music with someone who'd prefer music over the soundtrack. The
// source is set on the (welcome-hidden) panel dropdown; the launch gesture then applies
// it. Invalid values are ignored (the default Soundtrack stands).
function applyUrlAudioParams() {
const p = new URLSearchParams(location.search);
const src = (p.get("audio") || "").toLowerCase();
if (src === "none" || src === "soundtrack" || src === "music") {
const el = $("audio-source"); if (el) el.value = src;
}
const vol = p.get("vol");
if (vol !== null && vol !== "" && Number.isFinite(+vol)) {
const v = String(Math.max(0, Math.min(10, Math.round(+vol))));
// Seed BOTH the welcome mirror (carried into the panel on launch) and the panel
// slider, so the shared level survives entry.
const w = $("welcome-audio"); if (w) w.value = v;
const a = $("audio"); if (a) a.value = v;
}
}
// The element currently carrying the most signal — for the diagnostic readout,
// which used to assume a single `aud`. With two crossfading elements either may be
// the audible one, so report on whichever is louder (ties → aud).
@@ -1682,7 +1703,9 @@ async function main() {
// Audio source dropdown: reconcile SYNCHRONOUSLY in this change gesture so
// switching to Soundtrack/Music unlocks + plays on Safari; None greys Volume.
$("audio-source").addEventListener("change", () => { syncVolumeDisabled(); applyAudio(); debounced(); });
applyUrlAudioParams(); // shareable ?audio=/?vol= deep-link presets
syncVolumeDisabled();
updateWelcomeAudioLabel();
// Video toggle (panel): the FIRST time video turns on without having entered via
// Launch, begin the experience too — couple audio in. (Normally Launch entered
// first, so videoEverOn is already true and this is a no-op beyond re-render.)