diff --git a/viz/index.html b/viz/index.html index ad7130d..b751040 100644 --- a/viz/index.html +++ b/viz/index.html @@ -2566,6 +2566,83 @@ if (typeof p.bpm === "number") godtime.bpm = clamp(p.bpm, 40, 180); layoutDirty = true; } + + // ---- zero mode β€” dimension zero ------------------------------------------- + // The calm front door: not a muted world (moods do that) but an EMPTY one β€” no + // cables, nothing sounding β€” that you build by hand. It is the SAME matrix + // (routesArr/tweaks/synth), just started from nothing; leaving zero restores + // the world you had. Client-local, like moods β€” the hub/other users untouched. + let zeroMode = false; + function enterZero() { + if (!zeroMode) { // snapshot what you had, so exiting can restore it + try { localStorage.setItem("gs_prezero", JSON.stringify(serializePatch())); } catch (e) {} + } + clientMatrix = true; // own the matrix so the hub can't re-populate the cables + loadPatch({ routes: [], tweaks: {}, seqs: {} }); // empty the field + zeroMode = true; + try { localStorage.setItem("gs_mood", "0 zero"); } catch (e) {} + eventTicker.unshift({ text: "🌱 zero β€” the field is empty. build your own instrument: right-click a source β†’ patch to β†’ a voice, then press β™ͺ", tAdded: now(), src: "sys" }); + layoutDirty = true; + } + function exitZeroToMood(name) { // leave zero back into the full world, then a mood + if (zeroMode) { + zeroMode = false; + try { const pz = localStorage.getItem("gs_prezero"); if (pz) loadPatch(JSON.parse(pz)); } catch (e) {} + } + applyMood(name); + } + + // The three doors, shown once on a first-ever visit (replaces the silent drop + // into first light). Backdrop/esc = first light, the gentle default. + function showFirstVisitCard() { + if (document.getElementById("firstcard")) return; + const wrap = document.createElement("div"); + wrap.id = "firstcard"; + Object.assign(wrap.style, { position: "fixed", inset: "0", zIndex: "70", display: "flex", + alignItems: "center", justifyContent: "center", background: "rgba(6,10,20,0.72)", + backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" }); + const card = document.createElement("div"); + Object.assign(card.style, { maxWidth: "640px", width: "88%", textAlign: "center", + color: "#d6e2f4", fontFamily: "inherit" }); + const h = document.createElement("div"); + h.textContent = "one world, three doors"; + Object.assign(h.style, { fontWeight: "800", fontSize: "clamp(22px,4vw,32px)", letterSpacing: "2px", marginBottom: "6px" }); + const sub = document.createElement("div"); + sub.textContent = "the planet is already playing. choose how you meet it."; + Object.assign(sub.style, { color: "rgba(165,185,218,0.75)", fontStyle: "italic", marginBottom: "26px", fontSize: "14px" }); + card.appendChild(h); card.appendChild(sub); + const row = document.createElement("div"); + Object.assign(row.style, { display: "flex", gap: "14px", flexWrap: "wrap", justifyContent: "center" }); + const doors = [ + ["🌱", "start from zero", "an empty field. build the instrument yourself, one cable at a time.", () => enterZero()], + ["πŸŒ…", "first light", "a gentle arrangement β€” a bass line, soft hats, a small melody. ease in.", () => applyMood("first light")], + ["β›ˆ", "the full storm", "every one of the world's hands on it at once. the factory instrument.", () => applyMood("the full storm")], + ]; + for (const [emoji, title, desc, act] of doors) { + const b = document.createElement("div"); + Object.assign(b.style, { flex: "1 1 170px", minWidth: "160px", maxWidth: "200px", cursor: "pointer", + padding: "20px 16px", borderRadius: "16px", background: "rgba(20,28,44,0.7)", + border: "1px solid rgba(120,150,200,0.28)", transition: "transform .12s, border-color .12s" }); + b.onmouseenter = () => { b.style.transform = "translateY(-3px)"; b.style.borderColor = "rgba(150,200,255,0.7)"; }; + b.onmouseleave = () => { b.style.transform = ""; b.style.borderColor = "rgba(120,150,200,0.28)"; }; + b.innerHTML = '
' + emoji + '
' + + '
' + title + '
' + + '
' + desc + '
'; + b.onclick = () => { wrap.remove(); document.removeEventListener("keydown", onEsc, true); act(); }; + row.appendChild(b); + } + card.appendChild(row); + const foot = document.createElement("div"); + foot.textContent = "you can switch any time β€” right-click the sky β†’ πŸŒ— mood"; + Object.assign(foot.style, { marginTop: "22px", fontSize: "11px", color: "rgba(150,170,205,0.55)" }); + card.appendChild(foot); + wrap.appendChild(card); + const onEsc = (e) => { if (e.key === "Escape") { wrap.remove(); document.removeEventListener("keydown", onEsc, true); applyMood("first light"); } }; + wrap.onclick = (e) => { if (e.target === wrap) { wrap.remove(); document.removeEventListener("keydown", onEsc, true); applyMood("first light"); } }; + document.addEventListener("keydown", onEsc, true); + document.body.appendChild(wrap); + } + function makeGroupLocal(name, members) { if (!name || !members || !members.length) return; groupsInfo[name] = { label: name, members: members.slice() }; @@ -2841,10 +2918,13 @@ try { firstEver = !localStorage.getItem("gs_seen"); } catch (e) {} try { saved = localStorage.getItem("gs_mood") || "first light"; } catch (e) {} setTimeout(() => { - applyMood(saved); if (firstEver) { try { localStorage.setItem("gs_seen", "1"); } catch (e) {} - eventTicker.unshift({ text: "πŸŒ… first light β€” a gentle arrangement to start you off. right-click the sky β†’ πŸŒ— mood for more (or the full storm when you're ready)", tAdded: now(), src: "sys" }); + showFirstVisitCard(); // three doors: zero / first light / storm + } else if (saved === "0 zero") { + enterZero(); // a returning zero-builder comes back to zero + } else { + applyMood(saved); } }, 900); } @@ -5580,9 +5660,13 @@ ctxItem(arr.open ? "close tracks" : "πŸŽ› tracks β€” arrange clips", () => { arr.open ? closeArranger() : openArranger(); }, "T"); const moodSub = ctxSub("πŸŒ— mood β€” how much world"); + const z0 = document.createElement("div"); z0.className = "it"; + z0.textContent = zeroMode ? "0 zero β€” you're building it (a mood leaves)" : "0 zero β€” build from nothing"; + z0.onclick = () => { enterZero(); closeCtx(); }; + moodSub.appendChild(z0); for (const mn of Object.keys(MOODS)) { const it2 = document.createElement("div"); it2.className = "it"; it2.textContent = mn; - it2.onclick = () => { applyMood(mn); closeCtx(); }; + it2.onclick = () => { zeroMode ? exitZeroToMood(mn) : applyMood(mn); closeCtx(); }; moodSub.appendChild(it2); } if (mindChakra) ctxItem("step out of " + mindChakra.label, () => exitMind(), "esc"); @@ -6174,9 +6258,10 @@ nudgeDown: { label: "lower the level of…", targeted: true, run: (k) => { const t = tw(k); t.gain = clamp((t.gain != null ? t.gain : 1) - 0.08, 0, 2); } }, muteKey: { label: "mute / unmute…", targeted: true, run: (k) => toggleMuteLocal(k) }, soloKeyA: { label: "solo…", targeted: true, run: (k) => { soloKey = soloKey === k ? null : k; } }, + zero: { label: "zero β€” build from nothing", run: () => { zeroMode ? exitZeroToMood("first light") : enterZero(); } }, }; const DEFAULT_KEYS = { F1: { a: "grimoire" }, F2: { a: "play" }, F3: { a: "chakraView" }, F4: { a: "skinNext" }, - F5: { a: "moodCycle" }, F6: { a: "tracks" }, F7: { a: "perform" }, F8: { a: "godLock" }, F9: { a: "earthEcho" }, F10: { a: "stage" } }; + F5: { a: "moodCycle" }, F6: { a: "tracks" }, F7: { a: "perform" }, F8: { a: "godLock" }, F9: { a: "earthEcho" }, F10: { a: "stage" }, F11: { a: "zero" } }; let keymap = {}; try { keymap = JSON.parse(localStorage.getItem("gs_keys")) || {}; } catch (e) {} if (!Object.keys(keymap).length) keymap = JSON.parse(JSON.stringify(DEFAULT_KEYS));