☰ the launcher · ☄ the omens · 🌍 shared worlds — the review's build list

The audit's verdict was that Godstrument is short on REACH, not features:
everything hid behind an invisible right-click, and everything you made
died in one tab. Four builds against exactly that.

☰ EVERYTHING — a visible, grouped index of the whole instrument (view /
play / your world / together / out / start here). Labels lead with the
plain verb and wear the god-name behind: "arpeggiator · GODSPEED",
"record to a file · TESTAMENT". Critically it is ONE data-driven list
with TWO renderers — the ☰ panel and the right-click sky menu both
render skyGroups(), so a feature can never appear in one and not the
other. (The sky menu's 90 hand-written lines became a 17-line loop.)
Submenus expand in place, so touch works.

☄ THE OMENS — the instrument always knew when the world was doing
something rare and never said so. Reads the live feeds and ranks what's
remarkable: a full moon, a harmonious sky, satellites overhead, a quake
that just landed, your solar return. One line whispers into the ticker
on arrival; the full reading is a tap away. Verified against the real
planet — it read a 91%-lit moon, an unusually harmonious sky and a
convulsing bitcoin, all true at the time.

🌍 SHARED WORLDS — the whole instrument (cables, grooves, beat, kit,
scale, tempo) folded into one link, gzipped by the browser's own
CompressionStream: ~10KB of patch becomes a ~620-character URL. It
rides the fragment, so nothing is uploaded, no account or gallery is
needed, and there is nothing to take down. The receiver is always
asked, never overwritten, and ⌘Z restores theirs. Round-trip verified:
a 12-wheel hat and 137bpm came back exactly. Deliberately NOT gated on
the hub handshake — a shared link must open even when the hub is down.

📱 THE PHONE FORK — a touch device is offered lifestrument once
(remembered, never forced), and the top bar folds to fit a narrow
screen either way.

Plus: the expression-lane drag was written out byte-identically in
three places; it is now one shared helper that speaks pointer events,
so velocity/chance/ratchet/p-lock lanes work under a thumb too.
Grimoire documents all four; manual regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-26 20:12:05 +10:00
parent 510ddaaf8c
commit 0ef7c68371
2 changed files with 536 additions and 102 deletions

View File

@ -305,6 +305,47 @@
border: 1px solid rgba(120,150,200,0.3); cursor: pointer; font-size: 12px;
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); user-select: none;
}
/* ☰ the launcher — the visible front door. Everything used to hide behind a
right-click on empty sky: no cue, and impossible to reach on a touch screen. */
#launchbtn {
position: fixed; right: 14px; bottom: 164px; z-index: 12; padding: 6px 12px;
border-radius: 9px; background: rgba(24,34,54,0.92); color: #cfe0ff; font-weight: 600;
border: 1px solid rgba(150,190,245,0.45); cursor: pointer; font-size: 12px;
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); user-select: none;
}
#launchbtn:hover { color: #fff; border-color: rgba(180,215,255,0.8); }
#launchbtn.hidden { display: none; }
#launcher {
position: fixed; right: 14px; bottom: 200px; z-index: 26; width: 320px;
max-height: min(72vh, 620px); overflow-y: auto; display: none;
background: rgba(10,15,26,0.96); border: 1px solid rgba(120,150,200,0.3);
border-radius: 12px; padding: 8px 0 10px; color: #c7d4ea; font-size: 12px;
-webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
#launcher.open { display: block; }
#launcher .lhd { display: flex; align-items: baseline; gap: 8px; padding: 4px 14px 8px; }
#launcher .lhd b { color: #eaf1ff; font-size: 13px; letter-spacing: .4px; }
#launcher .lhd span { color: rgba(150,170,200,0.6); font-size: 10px; flex: 1; }
#launcher .lx { cursor: pointer; color: #8fa4c8; font-size: 16px; flex: none !important; }
#launcher .lgrp { color: rgba(160,190,235,0.75); font-size: 10px; letter-spacing: 1.4px;
text-transform: uppercase; padding: 10px 14px 4px; border-top: 1px solid rgba(120,150,200,0.14); }
#launcher .lit { padding: 7px 14px; cursor: pointer; border-left: 2px solid transparent; }
#launcher .lit:hover { background: rgba(90,130,200,0.16); }
#launcher .lit.on { border-left-color: #7cffb2; }
#launcher .lit.on .ll { color: #bff0c9; }
#launcher .ll { color: #dbe8ff; display: flex; align-items: center; gap: 6px; }
#launcher .lk { margin-left: auto; color: rgba(150,170,200,0.55); font-size: 10px; }
#launcher .lh { color: rgba(150,170,200,0.62); font-size: 10.5px; margin-top: 1px; }
#launcher .lsub { background: rgba(0,0,0,0.25); padding: 2px 0; }
#launcher .lsi { padding: 6px 14px 6px 26px; cursor: pointer; color: #b9c8e2; }
#launcher .lsi:hover { background: rgba(90,130,200,0.2); color: #eaf1ff; }
#ctxmenu .ctxgrp { color: rgba(160,190,235,0.7); font-size: 9.5px; letter-spacing: 1.3px;
text-transform: uppercase; padding: 8px 12px 3px; border-top: 1px solid rgba(120,150,200,0.14); }
@media (max-width: 560px){
#launcher { right: 8px; left: 8px; width: auto; bottom: 196px; max-height: 62vh; }
#launchbtn { right: 10px; }
}
#beatbtn:hover { color: #dbe8ff; border-color: rgba(150,180,230,0.6); }
#beatbtn.on { color: #cfe0ff; border-color: rgba(150,200,255,0.7); background: rgba(30,42,66,0.9); }
#beatbtn.hidden { display: none; }
@ -1257,6 +1298,10 @@
<li><b>✦ CANON — the shelf of worked examples.</b> Open <b>⚙ → DIMENSIONS</b> and at the top of the list, above your own saved worlds, sit four <b>✦ factory dimensions</b> that ship with the instrument — <i>the heartbeat</i> (a boom-bap kit with the swing and the kick's duck), <i>the wheel within the wheel</i> (WHEELS, made audible), <i>the squeeze</i> (the market pumping GODSQUASH), and <i>quiet arrival</i> (the earth echo, slow and sparse). Load one to hear a feature in its element, then save your own version under a new name — the ✦ ones are <b>read-only</b>, the same for everyone, a place to start rather than a thing to overwrite.</li>
<li><b>🪐 spheres — the music of the spheres.</b> Press <kbd>S</kbd> (or right-click the sky → <b>🪐 spheres</b>) and turn it <b>on</b>: the eight planets join the field as <b>modulation sources</b>, the slowest LFOs there are. Each planet offers three signals — <b>phase</b> (its heliocentric longitude, a sawtooth one full turn per orbit), <b>dist</b> (its eccentric breathing, in and out between perihelion and aphelion — Mercury and Mars swing hard, Venus barely), and <b>beat</b> (its <i>synodic</i> dance against Earth — Venus and Earth clasp every 583.9 days, the pentagram cycle). Route any of them like any source, or just say <i>"mars drive filter."</i> One knob rules time: the <b>rate</b>, from <b>1 to 3650 days of sky per bar</b> — at the default thirty, Venus' year is about seven and a half bars and Jupiter's a hundred and forty-four, real orbits compressed into slow musical arcs. And the <b>☸ orbital ratios</b> button sets the drum lanes' wheels to the true resonances — <b>4 : 8 : 16</b> (Io, Europa and Ganymede, locked at 1 : 2 : 4) and <b>8 : 13</b> (Venus to Earth, the pentagram in wheel form), so the polymeter you hear is one the solar system actually keeps. It is all <b>computed here, at the instrument's own clock</b> — the same JPL ephemeris SOLARGOD uses, vendored in, no network and no server, so the sky is deterministic: the same bar always shows the same heaven, and a saved patch reloads the identical sky.</li>
<li><b>🚪 The doors — how the world opens.</b> The front door is a skin like any other. On your first visit a card offers <b>six doors</b>, and the one you walk is <b>remembered</b> — every arrival after that opens the same way (right-click the sky → <b>🚪 the doors</b> to choose again, or say <i>"doors"</i>). The classics: <b>🌱 start from zero</b> (an empty field, build it yourself), <b>🌅 first light</b> (a gentle arrangement), <b>⛈ the full storm</b> (every hand at once). And the gig doors: <b>🎪 the rig</b> — you walk on stage with the <b>beat panel and GODSPEED already open</b> and the field folded to the eight liveliest hands, everything within reach; <b>🃏 the deal</b> — the world <b>deals you five cables</b> from its liveliest feeds onto five voices, <i>one card always pulsing</i>, with a <b>🃏 deal again</b> chip (and <kbd>⌘Z</kbd> takes any hand back) — redeal until it sings, and every arrival deals fresh; <b>🌒 the dawn</b> — the players <b>take the stage one at a time</b>, liveliest first, the field assembling itself over half a minute like an orchestra walking on. Same instrument behind every door — only the meeting changes.</li>
<li><b>☰ Everything — the index of the instrument.</b> The bottom-right <b>☰ everything</b> button opens the whole instrument as one <i>grouped</i> list — <b>the view</b> (skins, sky wheel, spheres, zen, performance), <b>play</b> (the beat, tracks, the arpeggiator, singing, the samplers, undo), <b>your world</b> (your heart, your holdings, your stars, a tab fed in), <b>together</b> (the commune), <b>out</b> (recording, the stage, control voltage, the keyboard) and <b>start here</b>. Every line leads with what it <i>does</i> and wears its god-name behind — <i>arpeggiator · GODSPEED</i>, <i>record to a file · TESTAMENT</i> — because you shouldn't need the lore to find the tool. Right-clicking the empty sky shows the very same list, grouped the same way, for hands that already know the road: one list, two doors, so nothing can hide in only one of them.</li>
<li><b>☄ What's remarkable now — the omens.</b> The instrument always knew when the world was doing something rare, and never said so. Open <b>☰ everything → what's remarkable now</b> and it reads the living feeds and tells you plainly: <i>the moon is full tonight</i> · <i>the sky is unusually harmonious — trines and sextiles everywhere</i> · <i>six satellites are over your head right now</i> · <i>a magnitude 5.4 earthquake just moved the ground</i> · <i>your solar return is upon you</i>. Nothing is invented — every omen is a feed you can point at. And when you arrive, if something genuinely rare is happening, a single line whispers it into the ticker. A reason to come back that isn't a notification: <b>the world itself is the news</b>.</li>
<li><b>🌍 Share a world — a link that plays.</b> A vibe is a handful of cables; a <b>world</b> is the whole instrument — every cable, groove, beat, kit, scale and tempo. <b>☰ everything → share this world</b> folds all of it into a single link and copies it. Paste that link to anyone: they open it, the instrument asks <i>“someone shared a world — listen to it?”</i>, and one tap puts your exact wiring under their hands, playing the live planet as it stands for them. The whole world rides in the <b>URL's fragment</b>, gzipped by the browser itself — so it is never uploaded anywhere, needs no account and no gallery, and there is nothing to take down. Your patch is replaced, never merged, and <kbd>⌘Z</kbd> gives you yours back.</li>
<li><b>📱 On a phone.</b> This console wants a mouse and a keyboard — drag-to-patch, the hover loupe, a keyboard of shortcuts. So a phone arriving here is <i>offered</i> (once, and remembered) the client built for a thumb: <b>🌱 lifestrument</b>, the same living planet as eight vibes and two knobs. Stay in the full console if you like — the offer never forces, and the top bar folds itself to fit a narrow screen either way.</li>
<li><b>⚡ Pulse — make the slow world playable.</b> Some of the planet's truest signals barely move — the national debt, the Moon's declination, a glacier of a currency. Right-click a node, open its cables, and tap the <b></b> chip on any cable to cycle <b>÷16 · ÷8 · ÷4 · bar</b>: the cable now <i>throbs on the godtime clock</i>, and the slow value becomes the <b>depth</b> of that throb. The debt stops being a frozen offset and becomes a sixteenth-note pressure whose weight is the debt itself — slow data as character, the clock as motion. Off by default; the world's own tempo is still the truth unless you ask for the heartbeat.</li>
<li><b>💤 The sleeper fold — and speed at a glance.</b> The field folds away <b>quiet feeds</b> — sources with no cables, no signal and nothing to say for a minute — behind a small <b>💤 chip</b> (bottom-left) that shows the count and brings them back with one click. The moment a folded feed <i>speaks</i>, it wakes itself and rejoins the field: the world breathes in and out of view. And every cable now moves at <b>the data's own rate</b> — streaming feeds race, glacial ones crawl — with the words to match: hover a column and each source row reads its speed (<i>streaming · ≈ every 3 bars · asleep, still 12m</i>), and a node's right-click readout says the same. What speed is that thing going at? Now it just tells you.</li>
<li><b>📽 The stage — send the visual to any screen.</b> Right-click the sky → <b>the stage</b> and a clean black mirror window opens, fed live from the canvas. Drag it to a projector, an AirPlay display, a second monitor — macOS treats it like any window — and <b>double-click for fullscreen</b>. The stage has a <b>camera</b>: the whole view by default (whatever you're in — a skin, the wheel, mind mode's POV), or right-click any node → <b>project this node</b> and the camera glides after that one sphere through its orbits, or <b>project its station</b> to frame a whole wheel and its satellites. Change cameras live from the menus while the projector runs; the audience sees the camera breathe from subject to subject.</li>
@ -1815,6 +1860,22 @@
// the grid edits — passed as a getter `getSteps()` so overlays always read the CURRENT
// array (the arranger reassigns a voice's sq.steps per bar when a clip plays); `lenArr`
// is the voice seq's len[16]. Returns renderNotes() — call it after every steps edit (paint()).
// Drag a value out of a lane cell — the one gesture shared by every expression
// lane (velocity / chance / ratchet in the arranger AND the beat panel, and the
// p-lock lane). It was written out three times, byte for byte; now it is written
// once, and it speaks pointer events, so the lanes work under a thumb as well as
// a mouse. Listeners live on window so a drag that leaves the cell still tracks.
function dragValue(cell, setY) {
cell.addEventListener("pointerdown", (e0) => {
e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
const up = () => { window.removeEventListener("pointermove", mv); window.removeEventListener("pointerup", up);
window.removeEventListener("pointercancel", up); };
window.addEventListener("pointermove", mv); window.addEventListener("pointerup", up);
window.addEventListener("pointercancel", up);
});
}
function attachNoteLengths(grid, cells, getSteps, lenArr, rows, onDrop) {
for (const c of cells) { // explicit placement so overlays can span columns
c.el.style.gridColumn = String(c.stp + 1);
@ -2387,10 +2448,7 @@
const setY = (cy) => { const r = cell.getBoundingClientRect();
const frac = clamp(1 - (cy - r.top) / r.height, 0, 1);
ungodlyGesture("groove expr"); arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); };
cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
const up = () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); };
window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); });
dragValue(cell, setY); // shared pointer drag — mouse and touch
bars.push({ fill, stp });
lane.appendChild(cell);
}
@ -2423,10 +2481,7 @@
const val = 1 - (cy - r.top) / r.height;
ungodlyGesture("groove expr"); sqx.lock[stp] = val < 0 ? -1 : clamp(val, 0, 1); // dragged below the lane = unlock
paintLock(); };
cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
const up = () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); };
window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); });
dragValue(cell, setY); // shared pointer drag — mouse and touch
bars.push({ fill, stp });
lane.appendChild(cell);
}
@ -2830,6 +2885,13 @@
omniBtn.onclick = () => OMNI.toggle();
document.body.appendChild(omniBtn);
const launchBtn = document.createElement("div"); // ☰ everything — the visible index (the sky menu stays as the shortcut)
launchBtn.id = "launchbtn";
launchBtn.textContent = "☰ everything";
launchBtn.title = "every instrument, view and output — grouped (right-click the sky is the same list, faster)";
launchBtn.onclick = () => toggleLauncher();
document.body.appendChild(launchBtn);
const beatBtn = document.createElement("div"); // 🥁 beat — the GODRUM panel (B), sibling of 🎹 OMNI
beatBtn.id = "beatbtn";
beatBtn.textContent = "🥁 beat";
@ -3051,10 +3113,7 @@
const setY = (cy) => { const r = cell.getBoundingClientRect();
const frac = clamp(1 - (cy - r.top) / r.height, 0, 1);
ungodlyGesture("beat expr"); arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); };
cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
const up = () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); };
window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); });
dragValue(cell, setY); // shared pointer drag — mouse and touch
bars.push({ fill, stp }); exlane.appendChild(cell);
}
paintExpr();
@ -4112,6 +4171,43 @@
// The doors card — first-ever visit, and any time from the sky menu.
// First visit: esc/backdrop = first light (the gentle default). Re-choosing: esc just closes.
// 📱 the phone fork — this console is built for a desktop (drag-to-patch, hover
// loupe, a keyboard of shortcuts). lifestrument is the same planet made for a
// thumb. Offer, never force: a tablet user may genuinely want the full rig, and
// the answer is remembered either way.
function phoneOffer() {
let asked = false;
try { asked = !!localStorage.getItem("gs_phonefork"); } catch (e) {}
const touch = (navigator.maxTouchPoints || 0) > 1 || "ontouchstart" in window;
if (asked || !touch || Math.min(innerWidth, innerHeight) > 560) return false;
const wrap = document.createElement("div"); wrap.id = "phonefork";
Object.assign(wrap.style, { position: "fixed", inset: "0", zIndex: "72", display: "flex",
alignItems: "center", justifyContent: "center", padding: "24px", textAlign: "center",
background: "rgba(6,10,20,0.86)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" });
const card = document.createElement("div");
Object.assign(card.style, { maxWidth: "420px", color: "#d6e2f4" });
card.innerHTML = "<div style='font-weight:800;font-size:26px;letter-spacing:1px;margin-bottom:8px'>you're on a phone</div>" +
"<div style='color:rgba(165,185,218,0.8);font-style:italic;font-size:14px;line-height:1.6;margin-bottom:22px'>" +
"this console wants a mouse and a keyboard. <b>lifestrument</b> is the same living planet, built for a thumb — " +
"eight vibes, two knobs, no cables.</div>";
const go = document.createElement("button");
go.textContent = "🌱 open lifestrument";
Object.assign(go.style, { display: "block", width: "100%", minHeight: "54px", fontSize: "17px", fontWeight: "700",
borderRadius: "14px", border: "none", cursor: "pointer", marginBottom: "10px",
background: "linear-gradient(90deg,#ffd3a6,#ff9ab0)", color: "#14131a", fontFamily: "inherit" });
const stay = document.createElement("button");
stay.textContent = "stay in the full console";
Object.assign(stay.style, { display: "block", width: "100%", minHeight: "46px", fontSize: "14px",
borderRadius: "12px", border: "1px solid rgba(120,150,200,0.35)", cursor: "pointer",
background: "transparent", color: "rgba(190,210,240,0.9)", fontFamily: "inherit" });
const remember = (v) => { try { localStorage.setItem("gs_phonefork", v); } catch (e) {} };
go.onclick = () => { remember("life"); location.href = "/life.html"; };
stay.onclick = () => { remember("full"); wrap.remove(); };
card.appendChild(go); card.appendChild(stay); wrap.appendChild(card);
document.body.appendChild(wrap);
return true;
}
function showDoorsCard(again) {
if (document.getElementById("firstcard")) return;
const wrap = document.createElement("div");
@ -4744,6 +4840,106 @@
vibeStore.save(vibe); // saved either way; applying is opt-in
}
// ---- 🌍 share a world — a link that plays -------------------------------
// A vibe is a handful of cables; a WORLD is the whole instrument — every cable,
// groove, beat, kit, scale and tempo — folded into one link. Everything you make
// here otherwise dies in this tab. Gzipped through the browser's own
// CompressionStream so the link stays pasteable (≈10 KB of patch → ≈2 KB of URL);
// falls back to plain base64 where that API is missing. The patch rides the URL
// FRAGMENT, so it never touches a server — no upload, no account, no gallery to
// moderate. The receiver is always ASKED; a shared world never overwrites yours.
async function worldToCode() {
const json = JSON.stringify({ v: 1, t: Date.now(), p: serializePatch() });
if (window.CompressionStream) {
try {
const cs = new CompressionStream("gzip");
const buf = new Uint8Array(await new Response(new Blob([json]).stream().pipeThrough(cs)).arrayBuffer());
let bin = ""; for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]);
return "GSW1z." + btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
} catch (e) { /* fall through to plain */ }
}
return "GSW1." + b64urlEncode(json);
}
async function codeToWorld(code) {
if (typeof code !== "string") return null;
const m = /GSW1(z?)\.([A-Za-z0-9_\-]+)/.exec(code.trim());
if (!m) return null;
try {
let json;
if (m[1] === "z") {
let s = m[2].replace(/-/g, "+").replace(/_/g, "/"); while (s.length % 4) s += "=";
const bin = atob(s), buf = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
const ds = new DecompressionStream("gzip");
json = await new Response(new Blob([buf]).stream().pipeThrough(ds)).text();
} else json = b64urlDecode(m[2]);
const o = JSON.parse(json);
return (o && o.p && typeof o.p === "object") ? o : null;
} catch (e) { return null; }
}
async function shareWorld() {
const code = await worldToCode();
const url = location.origin + location.pathname + "#w=" + code;
const nRoutes = routesArr.length, nSeqs = Object.keys(seqs).filter(k => seqs[k] && seqs[k].on).length;
const done = (ok) => eventTicker.unshift({
text: ok ? "🌍 world copied — paste that link and it plays exactly this: " + nRoutes + " cables, " + nSeqs + " lanes, " + Math.round(godtime.bpm) + " bpm"
: "🌍 couldn't reach the clipboard — the link is in the console", tAdded: now(), src: "sys" });
if (url.length > 60000) { // absurdly large patch — warn rather than hand over a broken link
eventTicker.unshift({ text: "🌍 this world is too big to fit in a link — save it as a dimension instead (⚙ → DIMENSIONS)", tAdded: now(), src: "sys" });
return;
}
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(url).then(() => done(true), () => { console.log("world link:", url); done(false); });
else { console.log("world link:", url); done(false); }
}
function offerWorldImport(w) { // #w=GSW1… — always asked, never auto-applied
const p = w.p, nR = Array.isArray(p.routes) ? p.routes.length : 0;
const nS = p.seqs ? Object.keys(p.seqs).length : 0;
const when = w.t ? new Date(w.t) : null;
const wrap = document.createElement("div"); wrap.id = "worldoffer";
Object.assign(wrap.style, { position: "fixed", inset: "0", zIndex: "97", display: "flex", alignItems: "center",
justifyContent: "center", background: "rgba(6,10,20,.74)", backdropFilter: "blur(7px)", WebkitBackdropFilter: "blur(7px)" });
const card = document.createElement("div");
Object.assign(card.style, { background: "rgba(14,20,34,.98)", border: "1px solid rgba(120,150,200,.3)",
borderRadius: "16px", padding: "24px", width: "400px", maxWidth: "92%", textAlign: "center",
color: "#dbe6f8", boxShadow: "0 12px 44px rgba(0,0,0,.5)" });
card.innerHTML = '<div style="font-size:32px;margin-bottom:8px">🌍</div>'
+ '<div style="font-weight:700;color:#eaf1ff;font-size:16px">someone shared a world</div>'
+ '<div style="color:#cdd9f0;font-size:13px;margin-top:6px">' + nR + ' cable' + (nR === 1 ? "" : "s")
+ ' · ' + nS + ' lane' + (nS === 1 ? "" : "s") + ' · ' + Math.round(p.bpm || 90) + ' bpm</div>'
+ (when ? '<div style="color:rgba(165,185,218,.55);font-size:11px;margin-top:3px">built ' + when.toLocaleDateString() + '</div>' : "")
+ '<div style="color:rgba(165,185,218,.65);font-size:11.5px;font-style:italic;margin-top:10px;line-height:1.5">'
+ 'the world\'s data is live as always — this is how they wired it. loading replaces your current patch (⌘Z takes it back).</div>';
const row = document.createElement("div"); row.style.cssText = "display:flex;gap:10px;justify-content:center;margin-top:18px";
const go = document.createElement("button"); go.textContent = "listen to it"; go.className = "midibtn";
const no = document.createElement("button"); no.textContent = "not now"; no.className = "midibtn"; no.style.opacity = ".7";
go.onclick = () => {
ungodlyMark("loaded a shared world"); // ⏪ so ⌘Z restores what they had
loadPatch(p); wrap.remove();
eventTicker.unshift({ text: "🌍 their world is yours — press ♪ to hear it (⌘Z puts yours back)", tAdded: now(), src: "sys" });
};
no.onclick = () => wrap.remove();
row.appendChild(go); row.appendChild(no); card.appendChild(row);
wrap.appendChild(card); document.body.appendChild(wrap);
wrap.onclick = (e) => { if (e.target === wrap) wrap.remove(); };
}
// A shared world is pure client state, so it must NOT wait on the hub handshake
// the way the mood/doors boot does — a link handed to someone while the hub is
// slow or down still has to open. Fires on its own clock, once, at load.
setTimeout(() => {
try {
const mw = /[#&]w=([^&]+)/.exec(location.hash || "");
if (!mw) return;
const raw = decodeURIComponent(mw[1]);
history.replaceState(null, "", location.pathname + location.search); // clean the bar before we ask
codeToWorld(raw).then(w => {
if (w) offerWorldImport(w);
else eventTicker.unshift({ text: "🌍 that shared-world link didn't survive the trip — ask for a fresh one", tAdded: now(), src: "sys" });
});
} catch (e) {}
}, 1200);
// ---- OMNI (Z5) — the synth panel: a NICE view over dests + tweaks -----------
// NO new audio paths. Each knob's ARC is the dest's live value (n.normDisp — the
// same number the field meters); dragging rides that dest's tweak OFFSET
@ -5365,6 +5561,8 @@
try { saved = localStorage.getItem("gs_mood") || "first light"; } catch (e) {}
try { door = localStorage.getItem("gs_door"); } catch (e) {}
setTimeout(() => {
phoneOffer(); // 📱 a thumb gets offered the client built for it (once, remembered)
setTimeout(omenLine, 6000); // ☄ once the feeds have landed, whisper the rarest thing happening
if (firstEver) {
try { localStorage.setItem("gs_seen", "1"); } catch (e) {}
showDoorsCard(false); // 🚪 the doors: zero / light / storm / rig / deal / dawn
@ -8242,6 +8440,259 @@
}
function ctxSep() { const s = document.createElement("div"); s.className = "sep"; ctxEl.appendChild(s); }
// ---- ☄ the omens — why tonight is worth hearing ------------------------
// The instrument always knew when the world was doing something remarkable — a
// full moon, a harmonious sky, five satellites overhead, the market convulsing —
// and never said so. This reads the feeds already flowing and ranks what's rare
// right now. One line lands in the ticker on arrival; the full reading is one tap
// away. Nothing is invented: every omen names the feed it came from.
function omens() {
const S = (k) => { const n = sources.get(k); return n && n.hist && n.hist.length ? n : null; };
const nv = (k, d) => { const n = S(k); return n && typeof n.norm === "number" ? n.norm : d; };
const rw = (k, d) => { const n = S(k); return n && typeof n.raw === "number" ? n.raw : d; };
const out = [];
const add = (rank, icon, text) => out.push({ rank, icon, text });
const moon = nv("astro.moon", null);
if (moon != null) {
if (moon > 0.965) add(0.95, "🌕", "the moon is full tonight");
else if (moon < 0.035) add(0.8, "🌑", "the moon is dark tonight a new one begins");
else if (moon > 0.9) add(0.55, "🌔", "the moon is all but full — " + Math.round(moon * 100) + "% lit");
}
const elev = rw("sky.elev", null);
if (elev != null && Math.abs(elev) < 5)
add(0.85, elev >= 0 ? "🌅" : "🌇", "the sun is on the horizon where you are, right now");
const harm = nv("astro.harmony", null), tens = nv("astro.tension", null);
if (harm != null && harm > 0.82) add(0.7, "✨", "the sky is unusually harmonious — trines and sextiles everywhere");
if (tens != null && tens > 0.82) add(0.7, "⚡", "the sky is under strain — squares and oppositions stacked up");
if (nv("astro.mercury_retro", 0) > 0.5) add(0.6, "☿", "mercury is retrograde");
const retro = rw("astro.retro", 0);
if (retro >= 3) add(0.5, "♆", Math.round(retro) + " planets are retrograde at once");
const sats = rw("sats.overhead_count", null);
if (sats != null && sats >= 6) add(0.65, "🛰", Math.round(sats) + " satellites are over your head right now");
const iss = nv("sats.best_elev", null);
if (iss != null && iss > 0.86) add(0.75, "🛰", "a satellite is passing almost directly overhead");
if (nv("crypto.vel", 0) > 0.85) add(0.7, "📈", "the market is convulsing — bitcoin is having a moment");
if (nv("crypto.cross_up", 0) > 0.4) add(0.6, "📈", "a golden cross just formed on bitcoin");
if (nv("crypto.cross_down", 0) > 0.4) add(0.6, "📉", "a death cross just formed on bitcoin");
const qd = rw("quake.event", 0);
if (qd > 4.5) add(0.98, "🌎", "a magnitude " + qd.toFixed(1) + " earthquake just moved the ground");
if (nv("fire.count", 0) > 0.86) add(0.55, "🔥", Math.round(rw("fire.count", 0)) + " wildfires are burning worldwide");
if (nv("natal.birthday", 0) > 0.75) add(1.0, "🎂", "your solar return is upon you — the sun is returning to where it stood when you were born");
if (nv("sun.speed", 0) > 0.82) add(0.7, "🌌", "the solar wind is running hard — " + Math.round(rw("sun.speed", 0)) + " km/s off the sun");
if (nv("almanac.fertile", 0) > 0.9) add(0.4, "🌱", "the almanac calls this a fertile day");
if (nv("sport.inplay", 0) > 0.8) add(0.4, "🏟", Math.round(rw("sport.inplay", 0)) + " matches are being played at once");
out.sort((a, b) => b.rank - a.rank);
return out;
}
let omensShown = false;
function omenLine() { // the arrival whisper — one line, the rarest thing
if (omensShown) return; omensShown = true;
const o = omens();
if (!o.length || o[0].rank < 0.6) return; // nothing rare enough to interrupt for
eventTicker.unshift({ text: o[0].icon + " " + o[0].text + " — ☰ everything → what's remarkable", tAdded: now(), src: "sys" });
}
function showOmens() {
const o = omens();
const wrap = document.createElement("div"); wrap.id = "omencard";
Object.assign(wrap.style, { position: "fixed", inset: "0", zIndex: "71", display: "flex",
alignItems: "center", justifyContent: "center", padding: "24px",
background: "rgba(6,10,20,0.78)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" });
wrap.onclick = (e) => { if (e.target === wrap) wrap.remove(); };
const card = document.createElement("div");
Object.assign(card.style, { maxWidth: "560px", width: "100%", color: "#d6e2f4", textAlign: "center" });
const h = document.createElement("div");
h.textContent = o.length ? "the world, right now" : "a quiet hour on earth";
Object.assign(h.style, { fontWeight: "800", fontSize: "clamp(20px,3.4vw,28px)", letterSpacing: "1.5px", marginBottom: "18px" });
card.appendChild(h);
if (!o.length) {
const q = document.createElement("div");
q.textContent = "nothing rare is happening — the moon is ordinary, the ground is still, the markets are asleep. Which is its own kind of remarkable.";
Object.assign(q.style, { color: "rgba(165,185,218,0.8)", fontStyle: "italic", lineHeight: "1.7", fontSize: "14px" });
card.appendChild(q);
}
for (const it of o.slice(0, 6)) {
const row = document.createElement("div");
Object.assign(row.style, { display: "flex", alignItems: "center", gap: "12px", textAlign: "left",
padding: "11px 14px", marginBottom: "8px", borderRadius: "12px",
background: "rgba(255,255,255,0.05)", border: "1px solid rgba(120,150,200,0.22)" });
const ic = document.createElement("div"); ic.textContent = it.icon;
Object.assign(ic.style, { fontSize: "22px", flex: "none" });
const tx = document.createElement("div"); tx.textContent = it.text;
Object.assign(tx.style, { fontSize: "14.5px", lineHeight: "1.45" });
row.appendChild(ic); row.appendChild(tx); card.appendChild(row);
}
const close = document.createElement("button"); close.textContent = "listen";
Object.assign(close.style, { marginTop: "14px", minHeight: "46px", padding: "0 28px", fontSize: "15px",
fontWeight: "700", borderRadius: "12px", border: "none", cursor: "pointer", fontFamily: "inherit",
background: "linear-gradient(90deg,#9fd0ff,#f6c8ff)", color: "#14131a" });
close.onclick = () => wrap.remove();
card.appendChild(close); wrap.appendChild(card); document.body.appendChild(wrap);
}
// ---- ☰ the launcher — one grouped index of everything the instrument does ----
// Everything used to live behind right-click-the-empty-sky: invisible, ungrouped,
// and impossible on a touch screen. This is ONE list with TWO renderers — the ☰
// panel (grouped, visible, tappable) and the sky menu (flat, fast, for hands that
// already know the way) — so a feature can never appear in one and not the other.
// Labels lead with the plain verb; the god-name rides behind as flavour, because
// a musician scanning for "arpeggiator" should not have to know it's GODSPEED.
function skyGroups() {
const fxLabel = fxMode === "auto" ? "auto (" + (fxLevel > 1 ? "full" : "lite") + ")"
: fxMode === "ultra" ? "ultra ✦ trails" : fxMode;
const gOwner = commune.room ? commune.alloc["@godtime"] : null;
const heartOn = !!hrDevice;
return [
{ name: "the view", items: [
{ label: chakraMode ? "leave the spirit view" : "spirit view · the skins", kbd: "C", on: chakraMode,
hint: "the field re-read as a cosmology",
run: () => { if (zeroMode) return; chakraMode = !chakraMode; if (!chakraMode) layoutDirty = true; } },
{ label: "skin · " + activeSkin.label, hint: "fifteen cosmologies, one planet",
sub: (add) => { for (const sk of Object.keys(SKINS))
add((SKINS[sk].key === activeSkin.key ? "✓ " : "") + SKINS[sk].label,
() => { setSkin(sk); if (!chakraMode && !zeroMode) { chakraMode = true; clearTrails(); } }); } },
{ label: "sky wheel · signs, houses & planets", kbd: "W", on: skyWheel,
hint: "the zodiac behind everything", run: () => toggleWheel() },
{ label: spheres.on ? "spheres · playing" : "spheres · the planets as modulators", kbd: "S", on: spheres.on,
hint: "orbits become oscillators", run: () => openSpheres() },
{ label: zenMode ? "leave zen" : "zen · only the visuals", kbd: "Z", on: zenMode,
hint: "every word vanishes", run: () => setZen(!zenMode) },
{ label: performMode ? "leave performance mode" : "performance mode", kbd: "P", on: performMode,
hint: "clean the stage for an audience",
run: () => window.dispatchEvent(new KeyboardEvent("keydown", { key: "p" })) },
{ label: "graphics: " + fxLabel, hint: "how hard the visuals work",
run: () => setFxMode(fxMode === "auto" ? "full" : fxMode === "full" ? "ultra"
: fxMode === "ultra" ? "lite" : "auto") },
{ label: "hover zoom: " + (loupe.on ? "on" : "off"), on: loupe.on, hint: "a loupe over the field",
run: () => { loupe.on = !loupe.on; try { localStorage.setItem("gs_loupe", loupe.on ? "1" : "0"); } catch (e) {} } },
{ if: !!mindChakra, label: "step out of " + (mindChakra ? mindChakra.label : ""), kbd: "esc",
hint: "back to the field", run: () => exitMind() },
{ if: !!soloKey, label: "clear solo (" + soloKey + ")", hint: "let the rest speak again",
run: () => { ungodlyMark("solo"); soloKey = null; } },
] },
{ name: "play", items: [
{ label: beat.open ? "close the beat" : "make a beat · GODRUM", kbd: "B", on: beat.open,
hint: "five drums on the godtime clock", run: () => { beat.open ? closeBeat() : openBeat(); } },
{ label: arr.open ? "close tracks" : "arrange clips · tracks", kbd: "T", on: arr.open,
hint: "clips, follow-actions, a song", run: () => { arr.open ? closeArranger() : openArranger(); } },
{ label: "arpeggiator · GODSPEED", on: arp.on, hint: "hold a chord, the machine runs it",
run: () => openGodspeed() },
{ label: psalm.on ? "listening… · PSALM (stop)" : "sing to play · PSALM", on: psalm.on,
hint: "your voice, folded to the scale", run: () => togglePsalm() },
{ label: "sample into the kit · AMPLER", hint: "chop the world onto the drums",
run: () => openAmpler() },
{ label: Synth.sampling() ? "resampling · GODSONIQ (re-sample)" : "resample the world · GODSONIQ",
on: Synth.sampling(), hint: "every pluck replays the planet",
run: () => { const ok = Synth.godsoniq(3);
eventTicker.unshift({ text: ok ? "🎛 GODSONIQ — sampling the world for 3s… then every pluck replays it, pitched across the keys (🎸 string voice to exit)"
: "🎛 GODSONIQ needs the audio worklet — press ♪ first, then try again", tAdded: now(), src: "sys" }); } },
{ label: Synth.stringVoice() ? "string voice · on" : "string voice · Karplus pluck",
on: Synth.stringVoice(), hint: "swap the FM tine for a string",
run: () => Synth.stringVoice(!Synth.stringVoice()) },
{ label: earthEchoOn ? "earth echo · on" : "earth echo · the planet on tape", on: earthEchoOn,
hint: "a Space Echo the world drives", run: () => earthEchoSet(!earthEchoOn) },
{ label: "undo · UNGODLY" + (ungodly.past.length ? " (" + ungodly.past.length + ")" : ""), kbd: "⌘Z",
hint: "take back the last thing you did", run: () => ungodlyUndo() },
] },
{ name: "your world", items: [
{ if: !heartOn, label: "connect your heart · bluetooth", hint: "your pulse becomes a source",
run: () => connectHeart() },
{ if: heartOn, label: "heart · " + (heartBpm() ? Math.round(heartBpm()) + " bpm" : "listening…") + " (disconnect)",
on: true, hint: "your pulse is in the choir", run: () => disconnectHeart() },
{ if: heartOn, label: "godtime follows your heart", on: heartDrivesGodtime,
hint: "the planet plays at your pulse", run: () => { heartDrivesGodtime = !heartDrivesGodtime; } },
{ label: "your holdings" + (holdings.length ? " (" + holdings.length + ")" : ""),
hint: "your own money, as a feed", run: () => openHoldings() },
{ label: "your stars" + (natalActive ? " · " + natalActive.name : ""),
hint: "your natal chart, cast live", run: () => openNatal() },
{ label: Synth.feeding() ? "tab feed · live (stop)" : "feed a tab into the world", on: Synth.feeding(),
hint: "a YouTube tab becomes weather",
run: async () => { const r = await Synth.feedTab();
const msg = r.stopped ? "🌐 tab feed stopped"
: r.ok ? "🌐 tab feed is live — it's in the world now: route crush for lo-fi, or 🎛 GODSONIQ to resample it pitched"
: r.reason === "unsupported" ? "🌐 tab-audio capture needs Chrome or Edge (Safari & Firefox can't). On a Mac it grabs one TAB's sound, not all-system"
: r.reason === "no-audio" ? "🌐 pick the tab AND tick “Share tab audio” in the picker — no audio came through"
: "🌐 tab feed cancelled";
eventTicker.unshift({ text: msg, tAdded: now(), src: "sys" }); } },
] },
{ name: "together", items: [
{ label: "the commune" + (commune.room ? " · " + commune.room : " · play together"), on: !!commune.room,
hint: "one planet, several pairs of hands", run: () => openCommune() },
{ if: !!commune.room, label: gOwner === communeMe() ? "release the godtime"
: (gOwner ? "godtime held by " + gOwner : "claim the godtime (tempo)"),
hint: "who conducts", run: () => { gOwner === communeMe() ? communeRelease("@godtime") : communeClaim("@godtime"); } },
] },
{ name: "out", items: [
{ label: (Synth.testamentState && Synth.testamentState().on) ? "recording… · TESTAMENT (stop & save)" : "record to a file · TESTAMENT",
on: !!(Synth.testamentState && Synth.testamentState().on),
hint: "the true master mix, to your disk", run: () => { if (Synth.testament) Synth.testament(); } },
{ label: stageWin ? "the stage · projecting" : "the stage · project this view", on: !!stageWin,
hint: "send the visual to another screen", run: () => openStage(null) },
{ if: !!stageWin, label: "close the stage", hint: "bring it home", run: () => closeStage() },
{ label: "control voltage" + (cv.on ? " · live" : " · drive a modular"), on: cv.on,
hint: "true DC volts out of the browser", run: () => openCV() },
{ label: "remap the keyboard · keys", hint: "bind any key to anything", run: () => openKeys() },
] },
{ name: "start here", items: [
{ label: "what's remarkable now · the omens", hint: (() => { const o = omens();
return o.length ? o[0].icon + " " + o[0].text : "a quiet hour on earth"; })(),
run: () => showOmens() },
{ label: "share this world · a link that plays", hint: "the whole instrument, folded into a URL",
run: () => shareWorld() },
{ label: "the doors · how the world opens", hint: "six ways in; it remembers yours",
run: () => showDoorsCard(true) },
{ label: "mood · how much world", hint: "from a single breath to the full storm",
sub: (add) => {
add(zeroMode ? "0 zero — you're building it (a mood leaves)" : "0 zero — build from nothing", () => enterZero());
for (const mn of Object.keys(MOODS)) add(mn, () => { zeroMode ? exitZeroToMood(mn) : applyMood(mn); });
} },
{ label: "the grimoire · read the manual", hint: "every feed, every voice, every trick",
run: () => { const g = document.getElementById("grimoire"); if (g) g.classList.add("open"); } },
{ label: "lifestrument · the gentle one", hint: "the same planet, for a phone",
run: () => window.open("/life.html", "_blank") },
] },
];
}
// the ☰ panel — the visible front door to the list above
let launchEl = null;
function closeLauncher() { if (launchEl) launchEl.classList.remove("open"); }
function launcherOpen() { return !!(launchEl && launchEl.classList.contains("open")); }
function openLauncher() {
if (!launchEl) { launchEl = document.createElement("div"); launchEl.id = "launcher"; document.body.appendChild(launchEl); }
launchEl.innerHTML = "";
const hd = document.createElement("div"); hd.className = "lhd";
const t = document.createElement("b"); t.textContent = "everything";
const sub = document.createElement("span"); sub.textContent = "right-click the sky for this list, fast";
const x = document.createElement("span"); x.className = "lx"; x.textContent = "×"; x.onclick = closeLauncher;
hd.appendChild(t); hd.appendChild(sub); hd.appendChild(x); launchEl.appendChild(hd);
for (const g of skyGroups()) {
const items = g.items.filter(it => it.if !== false);
if (!items.length) continue;
const gh = document.createElement("div"); gh.className = "lgrp"; gh.textContent = g.name;
launchEl.appendChild(gh);
for (const it of items) {
const row = document.createElement("div"); row.className = "lit" + (it.on ? " on" : "");
const l1 = document.createElement("div"); l1.className = "ll"; l1.textContent = it.label;
if (it.kbd) { const k = document.createElement("span"); k.className = "lk"; k.textContent = it.kbd; l1.appendChild(k); }
row.appendChild(l1);
if (it.hint) { const l2 = document.createElement("div"); l2.className = "lh"; l2.textContent = it.hint; row.appendChild(l2); }
if (it.sub) { // submenus expand in place — no hover, so touch works
row.onclick = () => {
const nx = row.nextSibling;
if (nx && nx.classList && nx.classList.contains("lsub")) { nx.remove(); return; }
const box = document.createElement("div"); box.className = "lsub";
it.sub((label, fn) => { const d = document.createElement("div"); d.className = "lsi"; d.textContent = label;
d.onclick = (e) => { e.stopPropagation(); fn(); openLauncher(); }; box.appendChild(d); });
row.parentNode.insertBefore(box, row.nextSibling);
};
} else row.onclick = () => { it.run(); if (launcherOpen()) openLauncher(); };
launchEl.appendChild(row);
}
}
launchEl.classList.add("open");
}
function toggleLauncher() { launcherOpen() ? closeLauncher() : openLauncher(); }
function openCtxMenu(node, px, py) {
ctxEl.innerHTML = "";
if (ctxLive) { clearInterval(ctxLive); ctxLive = null; }
@ -8383,96 +8834,23 @@
const b = document.createElement("b"); b.textContent = "godstrument";
const sp = document.createElement("span"); sp.textContent = "view";
hd.appendChild(b); hd.appendChild(sp); ctxEl.appendChild(hd);
ctxItem(chakraMode ? "leave chakra view" : "chakra view",
() => { if (zeroMode) return; chakraMode = !chakraMode; if (!chakraMode) layoutDirty = true; }, "C");
const skinSub = ctxSub("skin · " + activeSkin.label);
for (const sk of Object.keys(SKINS)) {
const it2 = document.createElement("div"); it2.className = "it";
it2.textContent = (SKINS[sk].key === activeSkin.key ? "✓ " : "") + SKINS[sk].label;
it2.onclick = () => { setSkin(sk); if (!chakraMode && !zeroMode) { chakraMode = true; clearTrails(); } closeCtx(); };
skinSub.appendChild(it2);
// the same list the ☰ launcher shows — grouped here too, so the two can
// never drift. Power users keep the speed; everyone else gets the ☰ button.
for (const g of skyGroups()) {
const items = g.items.filter(it => it.if !== false);
if (!items.length) continue;
const gh = document.createElement("div"); gh.className = "ctxgrp"; gh.textContent = g.name;
ctxEl.appendChild(gh);
for (const it of items) {
if (it.sub) {
const body = ctxSub(it.label);
it.sub((label, fn) => {
const d = document.createElement("div"); d.className = "it"; d.textContent = label;
d.onclick = () => { fn(); closeCtx(); }; body.appendChild(d);
});
} else ctxItem((it.on ? "✓ " : "") + it.label, it.run, it.kbd);
}
}
ctxItem(performMode ? "leave performance mode" : "performance mode",
() => window.dispatchEvent(new KeyboardEvent("keydown", { key: "p" })), "P");
ctxItem((skyWheel ? "✓ " : "") + "☉ sky wheel — signs, houses & planets", () => toggleWheel(), "W");
ctxItem(zenMode ? "leave zen" : "☾ zen — only the visuals", () => setZen(!zenMode), "Z");
ctxItem(arr.open ? "close tracks" : "🎛 tracks — arrange clips",
() => { arr.open ? closeArranger() : openArranger(); }, "T");
ctxItem(beat.open ? "close beat" : "🥁 beat — make a beat",
() => { beat.open ? closeBeat() : openBeat(); }, "B");
ctxItem("⏪ UNGODLY — take it back" + (ungodly.past.length ? " (" + ungodly.past.length + ")" : ""),
() => ungodlyUndo(), "⌘Z");
ctxItem("🚪 the doors — how the world opens", () => showDoorsCard(true));
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 = () => { zeroMode ? exitZeroToMood(mn) : applyMood(mn); closeCtx(); };
moodSub.appendChild(it2);
}
if (mindChakra) ctxItem("step out of " + mindChakra.label, () => exitMind(), "esc");
const fxLabel = fxMode === "auto" ? "auto (" + (fxLevel > 1 ? "full" : "lite") + ")"
: fxMode === "ultra" ? "ultra ✦ trails" : fxMode;
ctxItem("graphics: " + fxLabel,
() => setFxMode(fxMode === "auto" ? "full" : fxMode === "full" ? "ultra"
: fxMode === "ultra" ? "lite" : "auto"));
ctxItem("hover zoom: " + (loupe.on ? "on" : "off"),
() => { loupe.on = !loupe.on; try { localStorage.setItem("gs_loupe", loupe.on ? "1" : "0"); } catch (e) {} });
if (soloKey) ctxItem("clear solo (" + soloKey + ")", () => { ungodlyMark("solo"); soloKey = null; });
ctxSep();
if (!hrDevice) {
ctxItem("♥ connect your heart (bluetooth)", () => connectHeart());
} else {
ctxItem("♥ " + (heartBpm() ? Math.round(heartBpm()) + " bpm" : "listening…") + " · disconnect",
() => disconnectHeart());
ctxItem((heartDrivesGodtime ? "✓ " : "") + "godtime follows your heart",
() => { heartDrivesGodtime = !heartDrivesGodtime; });
}
ctxItem("💰 your holdings…" + (holdings.length ? " (" + holdings.length + ")" : ""),
() => openHoldings());
ctxItem("✨ your stars…" + (natalActive ? " (" + natalActive.name + ")" : ""),
() => openNatal());
ctxItem("🕸 the commune…" + (commune.room ? " (" + commune.room + ")" : ""),
() => openCommune());
ctxItem("⚡ control voltage…" + (cv.on ? " (live)" : ""),
() => openCV());
ctxItem((earthEchoOn ? "🌀 earth echo: on" : "🌀 earth echo — the planet on tape"),
() => earthEchoSet(!earthEchoOn));
ctxItem("🎸 string voice" + (Synth.stringVoice() ? " (on — plucks ring like a string)" : " — swap the FM tine for a Karplus string"),
() => Synth.stringVoice(!Synth.stringVoice()));
ctxItem("🎛 GODSONIQ" + (Synth.sampling() ? " (on — plucks replay your capture · tap to re-sample)" : " — resample the world onto the keys"),
() => { const ok = Synth.godsoniq(3);
eventTicker.unshift({ text: ok ? "🎛 GODSONIQ — sampling the world for 3s… then every pluck replays it, pitched across the keys (🎸 string voice to exit)"
: "🎛 GODSONIQ needs the audio worklet — press ♪ first, then try again", tAdded: now(), src: "sys" }); });
ctxItem("🌾 AMPLER — there is ample (sample the world into the kit)", () => openAmpler());
ctxItem((Synth.testamentState && Synth.testamentState().on) ? "📼 TESTAMENT — stop & save the take" : "📼 TESTAMENT — record what god does",
() => { if (Synth.testament) Synth.testament(); });
ctxItem("⚡ GODSPEED — hold a chord, the machine runs", () => openGodspeed());
ctxItem((spheres.on ? "🪐 spheres — music of the spheres (on)" : "🪐 spheres — the planets as modulators"), () => openSpheres(), "S");
ctxItem((psalm.on ? "🕊 PSALM — listening… (click to stop)" : "🕊 PSALM — sing, and the instrument answers"), () => togglePsalm());
ctxItem("🌐 " + (Synth.feeding() ? "tab feed: live — click to stop" : "feed a tab (YouTube, a stream…) into the world"),
async () => { const r = await Synth.feedTab();
const msg = r.stopped ? "🌐 tab feed stopped"
: r.ok ? "🌐 tab feed is live — it's in the world now: route crush for lo-fi, or 🎛 GODSONIQ to resample it pitched"
: r.reason === "unsupported" ? "🌐 tab-audio capture needs Chrome or Edge (Safari & Firefox can't). On a Mac it grabs one TAB's sound, not all-system"
: r.reason === "no-audio" ? "🌐 pick the tab AND tick “Share tab audio” in the picker — no audio came through"
: "🌐 tab feed cancelled";
eventTicker.unshift({ text: msg, tAdded: now(), src: "sys" }); });
ctxItem("⌨ keys — remap the keyboard", () => openKeys());
ctxItem("🌱 lifestrument — the gentle one (new tab)", () => window.open("/life.html", "_blank"));
if (commune.room) {
const gOwner = commune.alloc["@godtime"];
ctxItem(gOwner === communeMe() ? "🕸 release the godtime" :
(gOwner ? "🕸 godtime held by " + gOwner : "🕸 claim the godtime (tempo)"),
() => { gOwner === communeMe() ? communeRelease("@godtime") : communeClaim("@godtime"); });
}
ctxSep();
ctxItem(stageWin ? "📽 stage: whole view" : "📽 the stage — project this view",
() => openStage(null));
if (stageWin) ctxItem("📽 close the stage", () => closeStage());
}
ctxEl.classList.add("open");
ctxEl.style.left = Math.min(px, window.innerWidth - 252) + "px";

View File

@ -309,6 +309,47 @@
border: 1px solid rgba(120,150,200,0.3); cursor: pointer; font-size: 12px;
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); user-select: none;
}
/* ☰ the launcher — the visible front door. Everything used to hide behind a
right-click on empty sky: no cue, and impossible to reach on a touch screen. */
#launchbtn {
position: fixed; right: 14px; bottom: 164px; z-index: 12; padding: 6px 12px;
border-radius: 9px; background: rgba(24,34,54,0.92); color: #cfe0ff; font-weight: 600;
border: 1px solid rgba(150,190,245,0.45); cursor: pointer; font-size: 12px;
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); user-select: none;
}
#launchbtn:hover { color: #fff; border-color: rgba(180,215,255,0.8); }
#launchbtn.hidden { display: none; }
#launcher {
position: fixed; right: 14px; bottom: 200px; z-index: 26; width: 320px;
max-height: min(72vh, 620px); overflow-y: auto; display: none;
background: rgba(10,15,26,0.96); border: 1px solid rgba(120,150,200,0.3);
border-radius: 12px; padding: 8px 0 10px; color: #c7d4ea; font-size: 12px;
-webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
#launcher.open { display: block; }
#launcher .lhd { display: flex; align-items: baseline; gap: 8px; padding: 4px 14px 8px; }
#launcher .lhd b { color: #eaf1ff; font-size: 13px; letter-spacing: .4px; }
#launcher .lhd span { color: rgba(150,170,200,0.6); font-size: 10px; flex: 1; }
#launcher .lx { cursor: pointer; color: #8fa4c8; font-size: 16px; flex: none !important; }
#launcher .lgrp { color: rgba(160,190,235,0.75); font-size: 10px; letter-spacing: 1.4px;
text-transform: uppercase; padding: 10px 14px 4px; border-top: 1px solid rgba(120,150,200,0.14); }
#launcher .lit { padding: 7px 14px; cursor: pointer; border-left: 2px solid transparent; }
#launcher .lit:hover { background: rgba(90,130,200,0.16); }
#launcher .lit.on { border-left-color: #7cffb2; }
#launcher .lit.on .ll { color: #bff0c9; }
#launcher .ll { color: #dbe8ff; display: flex; align-items: center; gap: 6px; }
#launcher .lk { margin-left: auto; color: rgba(150,170,200,0.55); font-size: 10px; }
#launcher .lh { color: rgba(150,170,200,0.62); font-size: 10.5px; margin-top: 1px; }
#launcher .lsub { background: rgba(0,0,0,0.25); padding: 2px 0; }
#launcher .lsi { padding: 6px 14px 6px 26px; cursor: pointer; color: #b9c8e2; }
#launcher .lsi:hover { background: rgba(90,130,200,0.2); color: #eaf1ff; }
#ctxmenu .ctxgrp { color: rgba(160,190,235,0.7); font-size: 9.5px; letter-spacing: 1.3px;
text-transform: uppercase; padding: 8px 12px 3px; border-top: 1px solid rgba(120,150,200,0.14); }
@media (max-width: 560px){
#launcher { right: 8px; left: 8px; width: auto; bottom: 196px; max-height: 62vh; }
#launchbtn { right: 10px; }
}
#beatbtn:hover { color: #dbe8ff; border-color: rgba(150,180,230,0.6); }
#beatbtn.on { color: #cfe0ff; border-color: rgba(150,200,255,0.7); background: rgba(30,42,66,0.9); }
#beatbtn.hidden { display: none; }
@ -526,7 +567,6 @@
#account .fbitem b { color: #9fd0ff; }
#account .ghost { background: rgba(255,255,255,0.05); color: #b9c8e2; border: 1px solid rgba(120,150,200,0.2);
border-radius: 7px; padding: 6px 10px; cursor: pointer; font-size: 11px; }
#account .code { font-family: monospace; color: #7cffb2; margin-left: 8px; font-size: 12px; }
#account .codes { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 2px; }
#account .codechip { font-family: monospace; color: #7cffb2; font-size: 12px; cursor: pointer;
background: rgba(124,255,178,0.08); border: 1px solid rgba(124,255,178,0.3); border-radius: 6px;
@ -666,6 +706,18 @@
#grimoire ol.steps li { padding-left: 4px; margin: 6px 0; font-size: 13.5px; line-height: 1.65; }
#grimoire ol.steps li::before { content: none; }
@media (max-width: 520px){ #grimoire .spec .row { grid-template-columns: 1fr; gap: 3px; } }
/* ---- narrow screens: the top bar is five separately-pinned chips (gear 14, ♪ 52,
account 90, 🗣 92, ⏺ 132) sized for a desktop. On a phone they collide into each
other and the canvas source-counter. Lay them out as one right-aligned strip and
drop the account chip to a second row so nothing overlaps. ---- */
@media (max-width: 560px){
#gear { right: 10px; }
#sound { right: 44px !important; }
#testament { right: 78px !important; }
#godspeakrec { right: 122px; }
#authchip { top: 48px; right: 10px; max-width: 46vw; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; }
}
body.zen > :not(#stage):not(#ctxmenu) { display: none !important; } /* zen — nothing but the sky */
body.perform > :not(#stage):not(#ctxmenu) { display: none !important; } /* performance — pure visuals, no chrome */
/* 💤 the sleeper fold — quiet feeds tuck out of the field; this chip is the door back */
@ -1226,6 +1278,10 @@
<li><b>✦ CANON — the shelf of worked examples.</b> Open <b>⚙ → DIMENSIONS</b> and at the top of the list, above your own saved worlds, sit four <b>✦ factory dimensions</b> that ship with the instrument — <i>the heartbeat</i> (a boom-bap kit with the swing and the kick's duck), <i>the wheel within the wheel</i> (WHEELS, made audible), <i>the squeeze</i> (the market pumping GODSQUASH), and <i>quiet arrival</i> (the earth echo, slow and sparse). Load one to hear a feature in its element, then save your own version under a new name — the ✦ ones are <b>read-only</b>, the same for everyone, a place to start rather than a thing to overwrite.</li>
<li><b>🪐 spheres — the music of the spheres.</b> Press <kbd>S</kbd> (or right-click the sky → <b>🪐 spheres</b>) and turn it <b>on</b>: the eight planets join the field as <b>modulation sources</b>, the slowest LFOs there are. Each planet offers three signals — <b>phase</b> (its heliocentric longitude, a sawtooth one full turn per orbit), <b>dist</b> (its eccentric breathing, in and out between perihelion and aphelion — Mercury and Mars swing hard, Venus barely), and <b>beat</b> (its <i>synodic</i> dance against Earth — Venus and Earth clasp every 583.9 days, the pentagram cycle). Route any of them like any source, or just say <i>"mars drive filter."</i> One knob rules time: the <b>rate</b>, from <b>1 to 3650 days of sky per bar</b> — at the default thirty, Venus' year is about seven and a half bars and Jupiter's a hundred and forty-four, real orbits compressed into slow musical arcs. And the <b>☸ orbital ratios</b> button sets the drum lanes' wheels to the true resonances — <b>4 : 8 : 16</b> (Io, Europa and Ganymede, locked at 1 : 2 : 4) and <b>8 : 13</b> (Venus to Earth, the pentagram in wheel form), so the polymeter you hear is one the solar system actually keeps. It is all <b>computed here, at the instrument's own clock</b> — the same JPL ephemeris SOLARGOD uses, vendored in, no network and no server, so the sky is deterministic: the same bar always shows the same heaven, and a saved patch reloads the identical sky.</li>
<li><b>🚪 The doors — how the world opens.</b> The front door is a skin like any other. On your first visit a card offers <b>six doors</b>, and the one you walk is <b>remembered</b> — every arrival after that opens the same way (right-click the sky → <b>🚪 the doors</b> to choose again, or say <i>"doors"</i>). The classics: <b>🌱 start from zero</b> (an empty field, build it yourself), <b>🌅 first light</b> (a gentle arrangement), <b>⛈ the full storm</b> (every hand at once). And the gig doors: <b>🎪 the rig</b> — you walk on stage with the <b>beat panel and GODSPEED already open</b> and the field folded to the eight liveliest hands, everything within reach; <b>🃏 the deal</b> — the world <b>deals you five cables</b> from its liveliest feeds onto five voices, <i>one card always pulsing</i>, with a <b>🃏 deal again</b> chip (and <kbd>⌘Z</kbd> takes any hand back) — redeal until it sings, and every arrival deals fresh; <b>🌒 the dawn</b> — the players <b>take the stage one at a time</b>, liveliest first, the field assembling itself over half a minute like an orchestra walking on. Same instrument behind every door — only the meeting changes.</li>
<li><b>☰ Everything — the index of the instrument.</b> The bottom-right <b>☰ everything</b> button opens the whole instrument as one <i>grouped</i> list — <b>the view</b> (skins, sky wheel, spheres, zen, performance), <b>play</b> (the beat, tracks, the arpeggiator, singing, the samplers, undo), <b>your world</b> (your heart, your holdings, your stars, a tab fed in), <b>together</b> (the commune), <b>out</b> (recording, the stage, control voltage, the keyboard) and <b>start here</b>. Every line leads with what it <i>does</i> and wears its god-name behind — <i>arpeggiator · GODSPEED</i>, <i>record to a file · TESTAMENT</i> — because you shouldn't need the lore to find the tool. Right-clicking the empty sky shows the very same list, grouped the same way, for hands that already know the road: one list, two doors, so nothing can hide in only one of them.</li>
<li><b>☄ What's remarkable now — the omens.</b> The instrument always knew when the world was doing something rare, and never said so. Open <b>☰ everything → what's remarkable now</b> and it reads the living feeds and tells you plainly: <i>the moon is full tonight</i> · <i>the sky is unusually harmonious — trines and sextiles everywhere</i> · <i>six satellites are over your head right now</i> · <i>a magnitude 5.4 earthquake just moved the ground</i> · <i>your solar return is upon you</i>. Nothing is invented — every omen is a feed you can point at. And when you arrive, if something genuinely rare is happening, a single line whispers it into the ticker. A reason to come back that isn't a notification: <b>the world itself is the news</b>.</li>
<li><b>🌍 Share a world — a link that plays.</b> A vibe is a handful of cables; a <b>world</b> is the whole instrument — every cable, groove, beat, kit, scale and tempo. <b>☰ everything → share this world</b> folds all of it into a single link and copies it. Paste that link to anyone: they open it, the instrument asks <i>“someone shared a world — listen to it?”</i>, and one tap puts your exact wiring under their hands, playing the live planet as it stands for them. The whole world rides in the <b>URL's fragment</b>, gzipped by the browser itself — so it is never uploaded anywhere, needs no account and no gallery, and there is nothing to take down. Your patch is replaced, never merged, and <kbd>⌘Z</kbd> gives you yours back.</li>
<li><b>📱 On a phone.</b> This console wants a mouse and a keyboard — drag-to-patch, the hover loupe, a keyboard of shortcuts. So a phone arriving here is <i>offered</i> (once, and remembered) the client built for a thumb: <b>🌱 lifestrument</b>, the same living planet as eight vibes and two knobs. Stay in the full console if you like — the offer never forces, and the top bar folds itself to fit a narrow screen either way.</li>
<li><b>⚡ Pulse — make the slow world playable.</b> Some of the planet's truest signals barely move — the national debt, the Moon's declination, a glacier of a currency. Right-click a node, open its cables, and tap the <b></b> chip on any cable to cycle <b>÷16 · ÷8 · ÷4 · bar</b>: the cable now <i>throbs on the godtime clock</i>, and the slow value becomes the <b>depth</b> of that throb. The debt stops being a frozen offset and becomes a sixteenth-note pressure whose weight is the debt itself — slow data as character, the clock as motion. Off by default; the world's own tempo is still the truth unless you ask for the heartbeat.</li>
<li><b>💤 The sleeper fold — and speed at a glance.</b> The field folds away <b>quiet feeds</b> — sources with no cables, no signal and nothing to say for a minute — behind a small <b>💤 chip</b> (bottom-left) that shows the count and brings them back with one click. The moment a folded feed <i>speaks</i>, it wakes itself and rejoins the field: the world breathes in and out of view. And every cable now moves at <b>the data's own rate</b> — streaming feeds race, glacial ones crawl — with the words to match: hover a column and each source row reads its speed (<i>streaming · ≈ every 3 bars · asleep, still 12m</i>), and a node's right-click readout says the same. What speed is that thing going at? Now it just tells you.</li>
<li><b>📽 The stage — send the visual to any screen.</b> Right-click the sky → <b>the stage</b> and a clean black mirror window opens, fed live from the canvas. Drag it to a projector, an AirPlay display, a second monitor — macOS treats it like any window — and <b>double-click for fullscreen</b>. The stage has a <b>camera</b>: the whole view by default (whatever you're in — a skin, the wheel, mind mode's POV), or right-click any node → <b>project this node</b> and the camera glides after that one sphere through its orbits, or <b>project its station</b> to frame a whole wheel and its satellites. Change cameras live from the menus while the projector runs; the audience sees the camera breathe from subject to subject.</li>