diff --git a/viz/index.html b/viz/index.html
index 7421762..68a04dd 100644
--- a/viz/index.html
+++ b/viz/index.html
@@ -398,6 +398,8 @@
#grimoire code { font-family: ui-monospace, Menlo, monospace; font-size: 12.5px; color: #b8e0c8; background: rgba(120,255,178,0.08); padding: 0 4px; border-radius: 4px; }
#grimoire .hr { height: 1px; background: linear-gradient(90deg, transparent, rgba(140,170,230,0.25), transparent); margin: 30px 0; }
#grimoire .foot { text-align: center; color: rgba(150,170,205,0.55); font-size: 12px; margin-top: 40px; font-style: italic; }
+ 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 */
⚙
@@ -921,7 +923,9 @@
⌘/Ctrl-click several sources, then group them — mint your own vibe. Multi-select spheres by feeling, not category — a shadow of debt and inflation and quakes and the death rate, a bright of births and moonlight and fertility — and group them into a new named macro you can then mute, freeze, or gain as one hand-sized thing. The factory ships fourteen such vibes (planet, cosmos, human, market, heavens, dark, light, and the seven elemental + seasonal blends); yours join them.
♪ (top-right) — wake the sound. Browsers hold audio until a human asks. This is the ask. One click and the in-browser synth spins up, and the planet — already flowing as data — becomes audible.
The godtime dial (top) — set the pulse. Drag it to choose a tempo from 40 to 180 BPM, with the real planets of tonight's sky wheeling around it as you turn. Click the dial to lock the tempo where you left it — otherwise the data itself keeps nudging it (bitcoin's velocity, the dealgod market, inflation, each with a small hand on the clock).
-Press P — performance mode. The editing chrome vanishes, the readouts declutter, and a rotating line of dispatches from the world (bitcoin at $62,902 · the moon is 81% lit · mercury is retrograde · M4.2 earthquake) rises for the audience. Put the console on the projector, hit P, and conduct.
+Press P — performance mode. Everything that is not the world vanishes: the wordmark, the header, the counts, the godtime readout, the ticker, every button and chip — pure visuals, with only a rotating line of dispatches from the world (bitcoin at $62,902 · the moon is 81% lit · mercury is retrograde · M4.2 earthquake) rising for the audience. Put the console on the projector, hit P, and conduct. (Want even the labels and dispatches gone? Z is zen — nothing but the sky.)
+Press W — the sky wheel. The zodiac rises behind the whole instrument, huge and faint: the twelve signs in their element colours, the living planets on the rim exactly where they stand tonight (the Sun in Cancer in early July, wherever the Moon has wandered to), and the sky's aspects strung as chords across the wheel — red for the hard angles, green for the soft, gold for the meetings. Cast a chart (✨ your stars) and the layer deepens: your whole-sign houses divide the wheel, the ASC and MC stand in gold, your natal planets sit as gold ghosts on the inner ring, and transit chords strike them as the sky moves. The same wheel the zodiac skin draws, now living under the main view. W again to put it away; it remembers.
+Press Z — zen. Every word vanishes: labels, readouts, panels, the header, the ticker, the dial — all of it — and only the visuals remain, full screen. The spheres, the cables, the shockwaves, the sky wheel if it's up, still moving to the world; nothing left to read, everything left to watch. Z or esc brings the words back.
The keyboard is yours — F1–F10 and beyond. A default set of function keys is ready to fly: F1 the grimoire, F2 play/stop the synth, F3 the skin view, F4 next skin, F5 cycle mood, F6 tracks, F7 performance mode, F8 lock the godtime, F9 the earth echo, F10 the stage. But every key is remappable: right-click the sky → ⌨ keys to bind any key to any action, or map a key from a voice's own right-click menu. Want a and s to raise and lower a drone's level like a fader? Right-click that voice → map a key → raise its level, press a; again for lower, press s — now they nudge it live, hold to sweep. Your whole flow, under your fingers.
Press C — chakra view. The sources leave their column and take up their stations on the subtle body: seven wheels on a spine of light, crown to root, each feed orbiting the chakra it belongs to — the sky's machinery at the crown, the omens at the third eye, the species talking at the throat, weather and air at the heart, fire and money at the solar plexus, birth and water at the sacral, quakes and debt and the mortal weight at the root. Each wheel glows with the live average of its members and spins its traditional petals, counter-rotating wheel to wheel. Drag empty space to spin the whole body 360°, drag up/down to tilt, flick and let momentum carry it. Everything still works in there: click a node to mute it, drag it onto a voice to patch, shift-click for its tab. C again and the sources glide home.
Right-click anything — the everything menu. Every node carries its full settings under the right button: mute, solo, a level fader (0–2×), a trim offset, every cable it carries with its amount slider and an unpatch ×, and a nested patch to → list for laying new cables without dragging. Right-click the empty sky for the view menu. And the modifiers, for the fast hands: click mutes · ⇧-click opens the node's tab · ⇧-drag rides the level up and down · ⌥-click solos a source · ⌘-click multi-selects for grouping · ⌘⇧-click resets a node to neutral. People play differently; the instrument shouldn't care which hand you reach with.
@@ -2140,7 +2144,7 @@
try { loupe.on = localStorage.getItem("gs_loupe") !== "0"; } catch (e) {}
function updateLoupe(mx, my) {
- if (!loupe.on || chakraMode || performMode || !columnInfo
+ if (!loupe.on || chakraMode || performMode || zenMode || !columnInfo
|| inspectEl.classList.contains("open") || arr.open) { loupe.active = false; return; }
if (loupe.frozen) return; // held still through a press
// once open, keep it open while the cursor is over the magnified panel, so
@@ -2571,6 +2575,10 @@
if (chakraMode) updateChakra(t, dt); // positions sources on the subtle body
drawBackground(t);
+ if (skyWheel && !chakraMode && connected) { // the sky wheel, huge and faint behind everything (press W)
+ const r = Math.min(W, H) * 0.47;
+ drawWheelOverlay(t, null, { x0: W / 2 - r, x1: W / 2 + r, y0: H / 2 - r, y1: H / 2 + r });
+ }
drawCables(t);
drawShockwaves(t, dt);
drawParticles(dt);
@@ -2580,10 +2588,10 @@
for (const n of dests.values()) drawDestNode(n, t);
if (!chakraMode) drawLoupe(t); // the hover zoom over a dense column
drawEditing(t);
- if (performMode) drawPerform(t, dt);
- drawHeader(t);
- if (connected) drawGodtime(t);
- drawFooter(t);
+ if (performMode && !zenMode) drawPerform(t, dt);
+ if (!zenMode && !performMode) drawHeader(t); // performance: no wordmark, no counts
+ if (connected && !zenMode && !performMode) drawGodtime(t);
+ if (!zenMode && !performMode) drawFooter(t); // performance: no ticker
if (!connected) drawConnecting(t);
requestAnimationFrame(frame);
@@ -3310,8 +3318,8 @@
if (n && typeof n.raw === "number") n.station = SIGNS[Math.floor((((n.raw % 360) + 360) % 360) / 30)].key;
}
}
- function drawWheelOverlay(t, g) {
- const box = mapBox(g);
+ function drawWheelOverlay(t, g, layerBox) { // layerBox: drawn as a main-screen layer (press W)
+ const box = layerBox || mapBox(g);
const cx = (box.x0 + box.x1) / 2, cy = (box.y0 + box.y1) / 2;
const R = Math.min(box.x1 - box.x0, box.y1 - box.y0) * 0.5 - 4;
const lonXY = (lon, r) => {
@@ -3319,11 +3327,24 @@
return [cx + r * Math.cos(a2), cy - r * Math.sin(a2)];
};
ctx.save();
+ glyphPass = true; // the wheel's glyphs survive zen
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.strokeStyle = "rgba(200,212,240,0.20)"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.arc(cx, cy, R * 0.55, 0, Math.PI * 2); ctx.stroke();
+ // as a layer the skin's station nodes aren't there, so the wheel carries
+ // its own signs: boundary ticks and the twelve glyphs, in element colour
+ if (layerBox) for (let i2 = 0; i2 < 12; i2++) {
+ const t1 = lonXY(i2 * 30, R - 6), t2 = lonXY(i2 * 30, R + 5);
+ ctx.beginPath(); ctx.moveTo(t1[0], t1[1]); ctx.lineTo(t2[0], t2[1]);
+ ctx.strokeStyle = "rgba(200,212,240,0.14)"; ctx.lineWidth = 1; ctx.stroke();
+ const gp = lonXY(i2 * 30 + 15, R + 17);
+ ctx.fillStyle = rgba(SIGNS[i2].col, 0.55);
+ ctx.font = "13px 'Helvetica Neue', Arial, sans-serif";
+ ctx.fillText(SIGNS[i2].label.split(" ")[0] + "︎", gp[0], gp[1]); // ︎: text glyph, not emoji
+ }
+
// whole-sign houses + the ascendant, when a chart with a birthplace is cast
if (natalActive && natalActive.asc != null) {
const ascSign = Math.floor(natalActive.asc / 30);
@@ -3417,6 +3438,7 @@
ctx.fillStyle = "rgba(224,234,255,0.95)";
ctx.fillText(lv.glyph, pp[0], pp[1]);
}
+ glyphPass = false;
ctx.restore();
}
@@ -4547,6 +4569,18 @@
let learnMode = false, learnTarget = null;
let patchList = [], templateSelect = null;
let performMode = false, dispatchIdx = 0, dispatchT = 0;
+
+ // the sky wheel as a main-screen layer (W) and zen — nothing but the sky (Z)
+ let zenMode = false, glyphPass = false;
+ let skyWheel = false;
+ try { skyWheel = localStorage.getItem("gs_wheel") === "1"; } catch (e) {}
+ function toggleWheel() {
+ skyWheel = !skyWheel;
+ try { localStorage.setItem("gs_wheel", skyWheel ? "1" : "0"); } catch (e) {}
+ }
+ function setZen(on) { zenMode = on; document.body.classList.toggle("zen", on); }
+ const _fillText = ctx.fillText.bind(ctx); // zen silences every canvas label…
+ ctx.fillText = (s, x, y, m) => { if (!zenMode || glyphPass) _fillText(s, x, y, m); }; // …except the wheel's glyphs
const midiBindings = loadBindings(); // "cc:17" / "note:36" -> {target,param,mode}
const lastCCSent = {}, lastNoteFor = {};
let outSel = null, inSel = null, midiStatusEl = null, learnBtn = null, bindCountEl = null;
@@ -4708,19 +4742,17 @@
}
}
if (ev.key === "p" || ev.key === "P") {
- performMode = !performMode;
- if (performMode) {
- gearEl.style.display = "none"; panelEl.style.display = "none";
- inspectEl.style.display = "none";
- } else {
- gearEl.style.display = ""; panelEl.style.display = ""; inspectEl.style.display = "";
- }
- }
+ performMode = !performMode; // pure visuals: the CSS hides every
+ document.body.classList.toggle("perform", performMode); // DOM chrome, the frame loop
+ } // skips header/godtime/footer
if (ev.key === "c" || ev.key === "C") {
chakraMode = !chakraMode;
if (!chakraMode) { layoutDirty = true; exitMind(); } // sources glide home
else clearTrails();
}
+ if (ev.key === "w" || ev.key === "W") toggleWheel(); // the sky wheel layer
+ if (ev.key === "z" || ev.key === "Z") setZen(!zenMode); // zen — nothing but the sky
+ if (ev.key === "Escape" && zenMode) setZen(false);
if (ev.key === "t" || ev.key === "T") {
arr.open ? closeArranger() : openArranger(); // tracks, for the casuals
}
@@ -4913,6 +4945,8 @@
}
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");
const moodSub = ctxSub("🌗 mood — how much world");
@@ -5485,6 +5519,8 @@
moodCycle: { label: "cycle mood (first light / drift / storm)", run: () => cycleMood() },
tracks: { label: "tracks / arranger", run: () => { arr.open ? closeArranger() : openArranger(); } },
perform: { label: "performance mode", run: () => window.dispatchEvent(new KeyboardEvent("keydown", { key: "p" })) },
+ skyWheel: { label: "the sky wheel (zodiac layer)", run: () => toggleWheel() },
+ zen: { label: "zen — only the visuals", run: () => setZen(!zenMode) },
godLock: { label: "lock / unlock godtime", run: () => { godtime.locked = !godtime.locked; } },
stage: { label: "the stage (project the view)", run: () => { stageWin ? closeStage() : openStage(null); } },
earthEcho: { label: "toggle the earth echo", run: () => earthEchoSet(!earthEchoOn) },