Spaces:
Running on Zero
Running on Zero
Add a top-right mute button that silences all audio (persisted)
Browse filesA fixed glass button in the top-right toggles mute for every audio element (tapes + lobby) via querySelectorAll('audio').muted - independent of the volume/fade logic, so unmuting restores it. The choice persists in localStorage ('lofinity:muted') and is restored on load. z-index 50 keeps it above the modals so it works while a tape plays. Verified in preview: renders, mutes all 3 audio elements, persists across reload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- frontend/index.html +17 -0
- frontend/style.css +44 -0
- frontend/ui.js +29 -0
frontend/index.html
CHANGED
|
@@ -72,6 +72,23 @@
|
|
| 72 |
</div>
|
| 73 |
<button id="start-btn" type="button">▶ click to start</button>
|
| 74 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
<div id="vending-label" class="hover-label"><span>♪ vend a vibe</span></div>
|
| 76 |
<div id="collection-label" class="hover-label">
|
| 77 |
<span>♪ cassette collection</span>
|
|
|
|
| 72 |
</div>
|
| 73 |
<button id="start-btn" type="button">▶ click to start</button>
|
| 74 |
</div>
|
| 75 |
+
<button
|
| 76 |
+
id="mute-btn"
|
| 77 |
+
class="mute-btn"
|
| 78 |
+
type="button"
|
| 79 |
+
aria-label="mute all sound"
|
| 80 |
+
aria-pressed="false"
|
| 81 |
+
title="mute"
|
| 82 |
+
>
|
| 83 |
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 84 |
+
<path d="M4 9 L7.5 9 L13 5 L13 19 L7.5 15 L4 15 Z" fill="currentColor" stroke="none" />
|
| 85 |
+
<g class="mute-waves">
|
| 86 |
+
<path d="M16 9.5a4 4 0 0 1 0 5" />
|
| 87 |
+
<path d="M18.6 7a8 8 0 0 1 0 10" />
|
| 88 |
+
</g>
|
| 89 |
+
<line class="mute-slash" x1="3.5" y1="3.5" x2="20.5" y2="20.5" />
|
| 90 |
+
</svg>
|
| 91 |
+
</button>
|
| 92 |
<div id="vending-label" class="hover-label"><span>♪ vend a vibe</span></div>
|
| 93 |
<div id="collection-label" class="hover-label">
|
| 94 |
<span>♪ cassette collection</span>
|
frontend/style.css
CHANGED
|
@@ -159,6 +159,50 @@ body {
|
|
| 159 |
}
|
| 160 |
}
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
@keyframes title-bob {
|
| 163 |
from {
|
| 164 |
transform: translateY(0);
|
|
|
|
| 159 |
}
|
| 160 |
}
|
| 161 |
|
| 162 |
+
/* Global mute toggle, top-right. z-index above the modals (max 20) so it stays
|
| 163 |
+
clickable while a tape plays. No transform on this backdrop-filter element. */
|
| 164 |
+
.mute-btn {
|
| 165 |
+
position: fixed;
|
| 166 |
+
top: 1rem;
|
| 167 |
+
right: 1rem;
|
| 168 |
+
z-index: 50;
|
| 169 |
+
width: 2.6rem;
|
| 170 |
+
height: 2.6rem;
|
| 171 |
+
display: flex;
|
| 172 |
+
align-items: center;
|
| 173 |
+
justify-content: center;
|
| 174 |
+
border-radius: 999px;
|
| 175 |
+
color: #ffffff;
|
| 176 |
+
background: rgba(18, 28, 58, 0.34);
|
| 177 |
+
border: 1px solid rgba(255, 255, 255, 0.32);
|
| 178 |
+
backdrop-filter: blur(6px);
|
| 179 |
+
-webkit-backdrop-filter: blur(6px);
|
| 180 |
+
cursor: pointer;
|
| 181 |
+
transition:
|
| 182 |
+
background 0.2s ease,
|
| 183 |
+
border-color 0.2s ease,
|
| 184 |
+
color 0.2s ease;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.mute-btn:hover {
|
| 188 |
+
background: rgba(18, 28, 58, 0.55);
|
| 189 |
+
border-color: rgba(255, 255, 255, 0.6);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.mute-btn.muted {
|
| 193 |
+
color: rgba(255, 255, 255, 0.55);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.mute-btn .mute-slash {
|
| 197 |
+
display: none;
|
| 198 |
+
}
|
| 199 |
+
.mute-btn.muted .mute-waves {
|
| 200 |
+
display: none;
|
| 201 |
+
}
|
| 202 |
+
.mute-btn.muted .mute-slash {
|
| 203 |
+
display: inline;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
@keyframes title-bob {
|
| 207 |
from {
|
| 208 |
transform: translateY(0);
|
frontend/ui.js
CHANGED
|
@@ -157,6 +157,35 @@ export function initUI({
|
|
| 157 |
window.addEventListener("pointerdown", primeLobby);
|
| 158 |
window.addEventListener("keydown", primeLobby);
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
function setStage(stage) {
|
| 161 |
controlsRow.classList.toggle("hidden", stage !== "prompt");
|
| 162 |
generating.classList.toggle("hidden", stage !== "generating");
|
|
|
|
| 157 |
window.addEventListener("pointerdown", primeLobby);
|
| 158 |
window.addEventListener("keydown", primeLobby);
|
| 159 |
|
| 160 |
+
// --- mute -------------------------------------------------------------------
|
| 161 |
+
// Top-right toggle that silences ALL audio (tapes + lobby). Persisted in
|
| 162 |
+
// localStorage so the choice survives reloads. Uses each element's `muted`
|
| 163 |
+
// flag, which is independent of the volume/fade logic — unmuting restores it.
|
| 164 |
+
const muteBtn = $("mute-btn");
|
| 165 |
+
const MUTE_KEY = "lofinity:muted";
|
| 166 |
+
let muted = false;
|
| 167 |
+
try {
|
| 168 |
+
muted = localStorage.getItem(MUTE_KEY) === "1";
|
| 169 |
+
} catch {
|
| 170 |
+
/* private mode — default to unmuted */
|
| 171 |
+
}
|
| 172 |
+
function applyMute() {
|
| 173 |
+
document.querySelectorAll("audio").forEach((el) => (el.muted = muted));
|
| 174 |
+
muteBtn.classList.toggle("muted", muted);
|
| 175 |
+
muteBtn.setAttribute("aria-pressed", String(muted));
|
| 176 |
+
muteBtn.title = muted ? "unmute" : "mute";
|
| 177 |
+
}
|
| 178 |
+
applyMute(); // restore the persisted choice on load
|
| 179 |
+
muteBtn.addEventListener("click", () => {
|
| 180 |
+
muted = !muted;
|
| 181 |
+
try {
|
| 182 |
+
localStorage.setItem(MUTE_KEY, muted ? "1" : "0");
|
| 183 |
+
} catch {
|
| 184 |
+
/* ignore */
|
| 185 |
+
}
|
| 186 |
+
applyMute();
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
function setStage(stage) {
|
| 190 |
controlsRow.classList.toggle("hidden", stage !== "prompt");
|
| 191 |
generating.classList.toggle("hidden", stage !== "generating");
|