diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index df6b357b5f..16ed16787e 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -71,11 +71,12 @@ /* Scoring from a DAW is anonymous dummy play, so the account door is shut and the corner it occupied is free for the midi lamp. */ body.midi-out #logout { display: none; } - #midi-out { position: fixed; top: max(14px, env(safe-area-inset-top)); - right: max(14px, env(safe-area-inset-right)); z-index: 6; display: none; - padding: 6px 8px; border: 2px solid #6e768d; border-radius: 8px; - background: #171b28; box-shadow: 2px 3px 0 rgba(20, 25, 38, .35); - line-height: 0; } + /* Placed from script against the canvas, since the HUD's title-safe frame + and the round QR live in game units rather than CSS ones. */ + #midi-out { position: fixed; top: 14px; right: 14px; z-index: 6; + display: none; padding: 6px 8px; border: 2px solid #6e768d; + border-radius: 8px; background: #171b28; + box-shadow: 2px 3px 0 rgba(20, 25, 38, .35); line-height: 0; } body.midi-out #midi-out { display: block; } body.social-preview #midi-out { display: none; } #midi-out .key-white { fill: #b0b8ca; transition: fill .18s ease-out; } @@ -798,6 +799,26 @@ // The lamp answers every note that leaves, so the corner doubles as proof // the bridge is sending even when a pad is still silent. const midiLamp = document.querySelector("#midi-out"); + + // The round QR owns the top-right corner of the HUD-safe frame whenever it + // is up (`hello.js` spectatorQrBox), so the lamp keeps the same corner and + // the same five-percent inset but sits under the band the QR reserves. + // These numbers mirror hudSafeInset/spectatorQrBox; the canvas keeps a + // stable 1080-unit height, which is what converts them into CSS pixels. + function layoutMidiLamp() { + const box = canvas.getBoundingClientRect(); + if (!box.height) return; + const unit = box.height / 1080; + const compact = canvas.width < 1500; + const inset = Math.min(72, Math.max(compact ? 24 : 48, + Math.round(Math.min(canvas.width, 1080) * .05))); + const qrBand = (compact ? 108 : 158) + 12; + midiLamp.style.top = `${box.top + (inset + qrBand) * unit}px`; + midiLamp.style.right = `${innerWidth - box.right + inset * unit}px`; + } + addEventListener("resize", layoutMidiLamp); + addEventListener("orientationchange", layoutMidiLamp); + let lampTimer = null; const flashMidiLamp = () => { midiLamp.classList.add("lit"); @@ -806,18 +827,19 @@ }; const oskiewarMidi = createOskiewarMidi({ onSignal: flashMidiLamp }); globalThis.__oskiewarMidi = oskiewarMidi; + // The address is the whole state, and nothing about it survives a refresh: + // oskiewar.com is the game, `?midi` is this visit, midi.oskiewar.com is the + // way to keep it. Remembering the choice meant one visit to `?midi` left the + // procedural bank muted on the plain site forever, with nothing on screen + // saying why — so the old flag is cleared rather than read. + try { localStorage.removeItem("oskiewar-midi"); } catch {} const midiParam = new URLSearchParams(location.search).get("midi"); - let midiMode = midiParam; - if (midiParam !== null) { - try { localStorage.setItem("oskiewar-midi", midiParam || "1"); } catch {} - } else if (location.hostname.startsWith("midi.")) { - midiMode = "1"; // the subdomain is the ask; no query string to remember - } else { - try { midiMode = localStorage.getItem("oskiewar-midi"); } catch {} - } + const midiMode = midiParam !== null ? midiParam + : location.hostname.startsWith("midi.") ? "1" : null; if (midiMode !== null && midiMode !== "0" && midiMode !== "off") { if (await oskiewarMidi.enable()) { document.body.classList.add("midi-out"); + layoutMidiLamp(); oskiewarMidi.connectSignals(globalThis); const layered = midiMode === "both"; if (!layered) oskiewarSfx.mute(true); diff --git a/xbox/live/social/manifest.json b/xbox/live/social/manifest.json index c1996d52eb..7dbfbc0cc0 100644 --- a/xbox/live/social/manifest.json +++ b/xbox/live/social/manifest.json @@ -1,7 +1,7 @@ { "format": "ac.oskiewar.social-preview", "version": 2, - "build": "508d577023a8bbda", + "build": "06b89a9d0989f02d", "theme": "light", "imageWidth": 1200, "imageHeight": 630, diff --git a/xbox/live/tests/oskiewar-midi.test.mjs b/xbox/live/tests/oskiewar-midi.test.mjs index 7366b28e3e..59b13d718b 100644 --- a/xbox/live/tests/oskiewar-midi.test.mjs +++ b/xbox/live/tests/oskiewar-midi.test.mjs @@ -213,9 +213,24 @@ test("midi mode shuts the account door and lights the lamp", () => { assert.match(webShell, /classList\.add\("midi-out"\)/); }); +test("midi mode never outlives the address that asked for it", () => { + assert.doesNotMatch(webShell, /localStorage\.getItem\("oskiewar-midi"\)/, + "a remembered flag would mute the plain site's bank with nothing saying why"); + assert.doesNotMatch(webShell, /localStorage\.setItem\("oskiewar-midi"/); + assert.match(webShell, /localStorage\.removeItem\("oskiewar-midi"\)/, + "the flag anyone already stored has to be cleared, not merely ignored"); +}); + +test("the lamp keeps the HUD's safe frame and clears the QR's corner", () => { + assert.match(webShell, /function layoutMidiLamp\(\)/); + assert.match(webShell, /canvas\.getBoundingClientRect\(\)/); + assert.match(webShell, /box\.height \/ 1080/, "game units convert via the canvas"); + assert.match(webShell, /compact \? 108 : 158/, "the QR band must be reserved"); + assert.match(webShell, /addEventListener\("resize", layoutMidiLamp\)/); +}); + test("the web shell keeps midi opt-in so it cannot silence the bank by surprise", () => { assert.match(webShell, /import createOskiewarMidi from "\/oskiewar-midi\.mjs"/); - assert.match(webShell, /localStorage\.getItem\("oskiewar-midi"\)/); assert.match(webShell, /oskiewarMidi\.connectSignals\(globalThis\)/); assert.match(webShell, /oskiewarSfx\.mute\(true\)/, "the bank is muted unless layered"); assert.match(webShell, /pagehide[\s\S]{0,60}panic\(\)/, "held notes are released on hide");