🌱 Z1 — zero mode: the calm 7+7 field (rings, picker drawer, stage)
The overlay that replaces the sensory flood. In zero mode the node field is not drawn (frame() skips drawCables/drawSourceNode/drawDestNode); a DOM overlay takes its place over the starfield: - 7 vibe-rings down each side — the 14 config groups (left = elements/feelings: light·dark·fire·water·earth·air·spirit; right = domains: planet·cosmos·heavens· human·market·wealth·summer). Each: a hued dot (GROUP_META — taste consts, retune freely), label, live feed count, and a breathing glow scaled by the group's live average value. Members come from groupsInfo (the hub's config). - Picker drawer: click a ring → it slides in from that side listing the group's feeds (name, key, live value bar, ⊕ that toggles in place). Pick 1+ → a stage node (chip, colored by group) lands on the center stage. - Output rack along the bottom: OMNI voices (lead·bass·pad·drone·perc) + space & grit (filter·reverb·delay·…) as labeled sockets. Visual only — routing is Z2. Veil (z-8) sits below the ♪/tracks/grimoire controls (z10-13) so they stay usable. Verified live: rings render with hues + live glow, heavens drawer opens with its 5 feeds + value bars, picking lands stage chips, ⊕ toggles in place, console clean. Routing to the rack + right-click assign = Z2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2a705e3a35
commit
d30cea469c
208
viz/index.html
208
viz/index.html
@ -2581,12 +2581,14 @@
|
||||
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" });
|
||||
eventTicker.unshift({ text: "🌱 zero — the field is empty. pick a feed from a ring, or right-click a source → patch to → a voice, then press ♪", tAdded: now(), src: "sys" });
|
||||
zeroUI.open();
|
||||
layoutDirty = true;
|
||||
}
|
||||
function exitZeroToMood(name) { // leave zero back into the full world, then a mood
|
||||
if (zeroMode) {
|
||||
zeroMode = false;
|
||||
zeroUI.close();
|
||||
try { const pz = localStorage.getItem("gs_prezero"); if (pz) loadPatch(JSON.parse(pz)); } catch (e) {}
|
||||
}
|
||||
applyMood(name);
|
||||
@ -2643,6 +2645,187 @@
|
||||
document.body.appendChild(wrap);
|
||||
}
|
||||
|
||||
// ---- ZERO UI (Z1) — the calm 7+7 field --------------------------------------
|
||||
// A DOM overlay shown only in zero mode: 7 source-vibe rings down each side (the
|
||||
// 14 config groups), a center stage for the nodes you build, an output rack of the
|
||||
// voices/FX. Same matrix underneath — this is a calmer face on it. Hues/side/order
|
||||
// are TASTE CONSTS (retune freely). Members come live from groupsInfo (the hub's config).
|
||||
const GROUP_META = {
|
||||
// left column — the elements & feelings
|
||||
light: { emoji: "☀", hue: "#ffd67a", side: "L", ord: 0 },
|
||||
dark: { emoji: "🌑", hue: "#7a6cff", side: "L", ord: 1 },
|
||||
fire: { emoji: "🔥", hue: "#ff5a4d", side: "L", ord: 2 },
|
||||
water: { emoji: "💧", hue: "#4db8ff", side: "L", ord: 3 },
|
||||
earth: { emoji: "⛰", hue: "#b0854a", side: "L", ord: 4 },
|
||||
air: { emoji: "🌬", hue: "#bfe6ff", side: "L", ord: 5 },
|
||||
spirit: { emoji: "✦", hue: "#d18aff", side: "L", ord: 6 },
|
||||
// right column — the domains
|
||||
planet: { emoji: "🌍", hue: "#57c98c", side: "R", ord: 0 },
|
||||
cosmos: { emoji: "🛰", hue: "#8090ff", side: "R", ord: 1 },
|
||||
heavens:{ emoji: "🪐", hue: "#ffd24d", side: "R", ord: 2 },
|
||||
human: { emoji: "👤", hue: "#ff9060", side: "R", ord: 3 },
|
||||
market: { emoji: "📈", hue: "#4dd6c2", side: "R", ord: 4 },
|
||||
wealth: { emoji: "💰", hue: "#ffcf3a", side: "R", ord: 5 },
|
||||
summer: { emoji: "🌻", hue: "#ff9e3d", side: "R", ord: 6 },
|
||||
};
|
||||
const ZERO_RACK = [ // the outputs, plain-language (routing is Z2)
|
||||
{ grp: "voices", items: [["lead.note", "lead — sings"], ["bass.note", "bass — roots"],
|
||||
["pad.brightness", "pad — glows"], ["drone.voices", "drone — hums"], ["perc.density", "perc — taps"]] },
|
||||
{ grp: "space & grit", items: [["filter.cutoff", "filter"], ["reverb.size", "reverb"],
|
||||
["delay.feedback", "delay"], ["saturation", "saturation"], ["glitch", "glitch"],
|
||||
["granular.density", "granular"], ["crush", "crush"], ["wavetable.morph", "morph"],
|
||||
["lfo.rate", "lfo"], ["master.space", "space"]] },
|
||||
];
|
||||
function _grpMembers(key) { const gi = groupsInfo[key]; return (gi && gi.members) || []; }
|
||||
function _grpAvg(key) {
|
||||
const m = _grpMembers(key); if (!m.length) return 0;
|
||||
let s = 0, c = 0; for (const k of m) { const n = sources.get(k); if (n) { s += n.normDisp || n.norm || 0; c++; } }
|
||||
return c ? s / c : 0;
|
||||
}
|
||||
const zeroUI = {
|
||||
root: null, drawer: null, stage: null, rings: {}, nodes: {}, openGroup: null,
|
||||
build() {
|
||||
if (this.root) return;
|
||||
const st = document.createElement("style");
|
||||
st.textContent = "#zerowrap{position:fixed;inset:0;z-index:8;display:none;pointer-events:auto;font-family:inherit}"
|
||||
+ "#zerowrap.on{display:block;background:radial-gradient(ellipse at 50% 45%,rgba(8,12,24,.35),rgba(4,6,14,.62))}"
|
||||
+ ".zcol{position:absolute;top:76px;bottom:20px;width:168px;display:flex;flex-direction:column;justify-content:space-between;pointer-events:auto}"
|
||||
+ ".zcol.left{left:22px}.zcol.right{right:22px}"
|
||||
+ ".zring{display:flex;align-items:center;gap:11px;cursor:pointer;opacity:.86;transition:opacity .15s}"
|
||||
+ ".zcol.right .zring{flex-direction:row-reverse;text-align:right}.zring:hover{opacity:1}"
|
||||
+ ".zring.sel .zlbl{color:#fff}"
|
||||
+ ".zdot{width:40px;height:40px;border-radius:50%;flex:0 0 auto;position:relative}"
|
||||
+ ".zlbl{font-size:12.5px;color:#c6d4ec;letter-spacing:.3px;line-height:1.2}"
|
||||
+ ".zlbl small{display:block;color:rgba(150,170,205,.6);font-size:10px}"
|
||||
+ ".zstage{position:absolute;left:210px;right:210px;top:84px;bottom:150px;pointer-events:auto;display:flex;flex-wrap:wrap;align-content:flex-start;gap:12px;padding:14px;overflow:auto}"
|
||||
+ ".zstage .zhint{margin:auto;color:rgba(150,170,205,.5);font-size:13px;font-style:italic;text-align:center}"
|
||||
+ ".zchip{border-radius:13px;padding:11px 13px;min-width:130px;background:rgba(20,28,44,.72);border:1px solid;font-size:12px;color:#dbe6f8}"
|
||||
+ ".zchip b{color:#eaf1ff}.zchip .mem{color:rgba(180,195,225,.8);font-size:11px;margin-top:5px;line-height:1.5}"
|
||||
+ ".zrack{position:absolute;left:210px;right:210px;bottom:20px;height:120px;pointer-events:auto;overflow:auto}"
|
||||
+ ".zrack .grp{font-size:10px;letter-spacing:1px;color:rgba(150,170,205,.55);text-transform:uppercase;margin:2px 0 4px}"
|
||||
+ ".zsock{display:inline-block;margin:0 7px 7px 0;padding:6px 10px;border-radius:9px;background:rgba(18,26,42,.7);border:1px solid rgba(120,150,200,.2);font-size:11.5px;color:#aab8d4}"
|
||||
+ ".zdrawer{position:absolute;top:76px;bottom:20px;left:0;width:288px;background:rgba(12,18,32,.95);border-radius:0 16px 16px 0;border:1px solid rgba(120,150,200,.25);box-shadow:0 8px 40px rgba(0,0,0,.5);opacity:0;transform:translateX(-16px);transition:opacity .18s,transform .18s;pointer-events:none;overflow:auto;padding:16px}"
|
||||
+ ".zdrawer.right{left:auto;right:0;border-radius:16px 0 0 16px;transform:translateX(16px)}"
|
||||
+ ".zdrawer.open{opacity:1;transform:translateX(0);pointer-events:auto}"
|
||||
+ ".zdrawer h4{margin:0 0 4px;color:#eaf1ff;font-size:15px;letter-spacing:.5px}"
|
||||
+ ".zdrawer .dsub{color:rgba(150,170,205,.6);font-size:11px;margin-bottom:12px}"
|
||||
+ ".zfeed{display:flex;align-items:center;gap:8px;padding:7px 4px;border-top:1px solid rgba(140,170,230,.08)}"
|
||||
+ ".zfeed .fn{flex:1;font-size:12px;color:#cdd9f0}.zfeed .fn small{display:block;color:rgba(150,170,205,.55);font-size:10px}"
|
||||
+ ".zfeed .bar{width:44px;height:4px;border-radius:2px;background:rgba(120,150,200,.15);overflow:hidden}"
|
||||
+ ".zfeed .bar i{display:block;height:100%;border-radius:2px}"
|
||||
+ ".zadd{width:22px;height:22px;border-radius:6px;border:1px solid rgba(150,180,255,.4);color:#9fd0ff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;background:rgba(120,150,255,.12)}"
|
||||
+ ".zadd.on{background:rgba(120,255,178,.18);border-color:rgba(120,255,178,.5);color:#7cffb2}";
|
||||
document.head.appendChild(st);
|
||||
const w = document.createElement("div"); w.id = "zerowrap"; this.root = w;
|
||||
const colL = document.createElement("div"); colL.className = "zcol left";
|
||||
const colR = document.createElement("div"); colR.className = "zcol right";
|
||||
const order = Object.keys(GROUP_META).sort((a, b) => GROUP_META[a].ord - GROUP_META[b].ord);
|
||||
for (const key of order) {
|
||||
const meta = GROUP_META[key];
|
||||
const ring = document.createElement("div"); ring.className = "zring";
|
||||
const dot = document.createElement("div"); dot.className = "zdot"; dot.style.background = meta.hue;
|
||||
const lbl = document.createElement("div"); lbl.className = "zlbl";
|
||||
lbl.innerHTML = meta.emoji + " " + key + "<small></small>";
|
||||
ring.appendChild(dot); ring.appendChild(lbl);
|
||||
ring.onclick = () => this.openDrawer(key);
|
||||
(meta.side === "L" ? colL : colR).appendChild(ring);
|
||||
this.rings[key] = { ring, dot, lbl };
|
||||
}
|
||||
const stage = document.createElement("div"); stage.className = "zstage"; this.stage = stage;
|
||||
const rack = document.createElement("div"); rack.className = "zrack";
|
||||
for (const sect of ZERO_RACK) {
|
||||
const g = document.createElement("div"); g.className = "grp"; g.textContent = sect.grp; rack.appendChild(g);
|
||||
const box = document.createElement("div");
|
||||
for (const [key, label] of sect.items) {
|
||||
const s = document.createElement("span"); s.className = "zsock"; s.textContent = label; s.dataset.dest = key;
|
||||
box.appendChild(s);
|
||||
}
|
||||
rack.appendChild(box);
|
||||
}
|
||||
const drawer = document.createElement("div"); drawer.className = "zdrawer"; this.drawer = drawer;
|
||||
w.appendChild(colL); w.appendChild(colR); w.appendChild(stage); w.appendChild(rack); w.appendChild(drawer);
|
||||
document.body.appendChild(w);
|
||||
this.renderStage();
|
||||
},
|
||||
open() { this.build(); this.root.classList.add("on"); this.renderStage(); },
|
||||
close() { if (this.root) { this.root.classList.remove("on"); this.closeDrawer(); } },
|
||||
openDrawer(key) {
|
||||
this.build();
|
||||
const meta = GROUP_META[key], d = this.drawer;
|
||||
this.openGroup = key;
|
||||
d.className = "zdrawer" + (meta.side === "R" ? " right" : "");
|
||||
const gi = groupsInfo[key] || {};
|
||||
d.innerHTML = "";
|
||||
const h = document.createElement("h4"); h.textContent = meta.emoji + " " + key; h.style.color = meta.hue;
|
||||
const sub = document.createElement("div"); sub.className = "dsub"; sub.textContent = (gi.label || "") + " — pick the feeds you want on the stage";
|
||||
d.appendChild(h); d.appendChild(sub);
|
||||
const picked = this.nodes[key] ? this.nodes[key].members : new Set();
|
||||
for (const k of _grpMembers(key)) {
|
||||
const n = sources.get(k);
|
||||
const row = document.createElement("div"); row.className = "zfeed";
|
||||
const fn = document.createElement("div"); fn.className = "fn";
|
||||
fn.innerHTML = (n ? n.label : k).replace(/</g, "<") + "<small>" + k + "</small>";
|
||||
const bar = document.createElement("div"); bar.className = "bar";
|
||||
const fill = document.createElement("i"); fill.style.background = meta.hue; fill.style.width = "0%"; bar.appendChild(fill);
|
||||
const add = document.createElement("div"); add.className = "zadd" + (picked.has(k) ? " on" : "");
|
||||
add.textContent = picked.has(k) ? "✓" : "+";
|
||||
add.onclick = (e) => { e.stopPropagation(); this.pick(key, k); // toggle in place (no drawer re-flicker)
|
||||
const on = !!(this.nodes[key] && this.nodes[key].members.has(k));
|
||||
add.className = "zadd" + (on ? " on" : ""); add.textContent = on ? "✓" : "+"; };
|
||||
row.appendChild(fn); row.appendChild(bar); row.appendChild(add);
|
||||
row._srcKey = k; row._fill = fill;
|
||||
d.appendChild(row);
|
||||
}
|
||||
void d.offsetWidth; // force reflow, then transition in reliably
|
||||
d.classList.add("open");
|
||||
for (const r of this.rings ? Object.entries(this.rings) : []) r[1].ring.classList.toggle("sel", r[0] === key);
|
||||
},
|
||||
closeDrawer() { if (this.drawer) this.drawer.classList.remove("open"); this.openGroup = null;
|
||||
for (const k in this.rings) this.rings[k].ring.classList.remove("sel"); },
|
||||
pick(groupKey, srcKey) {
|
||||
let node = this.nodes[groupKey];
|
||||
if (!node) node = this.nodes[groupKey] = { members: new Set() };
|
||||
if (node.members.has(srcKey)) node.members.delete(srcKey); else node.members.add(srcKey);
|
||||
if (!node.members.size) delete this.nodes[groupKey];
|
||||
this.renderStage();
|
||||
},
|
||||
renderStage() {
|
||||
if (!this.stage) return;
|
||||
this.stage.innerHTML = "";
|
||||
const keys = Object.keys(this.nodes);
|
||||
if (!keys.length) {
|
||||
const hint = document.createElement("div"); hint.className = "zhint";
|
||||
hint.textContent = "click a ring on either side → pick a feed → it lands here. then drag it to a voice (soon).";
|
||||
this.stage.appendChild(hint); return;
|
||||
}
|
||||
for (const gk of keys) {
|
||||
const meta = GROUP_META[gk], node = this.nodes[gk];
|
||||
const chip = document.createElement("div"); chip.className = "zchip"; chip.style.borderColor = meta.hue;
|
||||
const names = [...node.members].map(k => { const n = sources.get(k); return n ? n.label : k; });
|
||||
chip.innerHTML = '<b style="color:' + meta.hue + '">' + meta.emoji + " " + gk + "</b> · " + node.members.size
|
||||
+ '<div class="mem">' + names.map(x => x.replace(/</g, "<")).join(" · ") + "</div>";
|
||||
chip._group = gk;
|
||||
this.stage.appendChild(chip);
|
||||
}
|
||||
},
|
||||
update() {
|
||||
if (!this.root || !this.root.classList.contains("on")) return;
|
||||
for (const key in this.rings) { // breathing glow = the group's live average
|
||||
const v = _grpAvg(key), meta = GROUP_META[key];
|
||||
const dot = this.rings[key].dot;
|
||||
dot.style.boxShadow = "0 0 " + (6 + v * 26).toFixed(0) + "px " + (1 + v * 4).toFixed(1) + "px " + meta.hue;
|
||||
const sm = this.rings[key].lbl.querySelector("small");
|
||||
if (sm) sm.textContent = _grpMembers(key).length + " feeds";
|
||||
}
|
||||
if (this.openGroup && this.drawer.classList.contains("open")) { // live bars in the open drawer
|
||||
for (const row of this.drawer.querySelectorAll(".zfeed")) {
|
||||
const n = sources.get(row._srcKey);
|
||||
row._fill.style.width = Math.round((n ? (n.normDisp || n.norm || 0) : 0) * 100) + "%";
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function makeGroupLocal(name, members) {
|
||||
if (!name || !members || !members.length) return;
|
||||
groupsInfo[name] = { label: name, members: members.slice() };
|
||||
@ -3129,6 +3312,9 @@
|
||||
if (dt > 0.1) dt = 0.1;
|
||||
lastFrame = t;
|
||||
pulseClock += dt;
|
||||
if (zeroMode) zeroUI.update(); // Z1: breathe the vibe rings + drawer bars
|
||||
|
||||
|
||||
|
||||
// fps governor — a machine that can't hold the frame rate sheds eye-candy
|
||||
fpsEma += (1 / Math.max(dt, 1 / 240) - fpsEma) * 0.05;
|
||||
@ -3210,15 +3396,17 @@
|
||||
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);
|
||||
if (chakraMode) drawChakraView(t);
|
||||
else for (const n of sources.values()) drawSourceNode(n, t);
|
||||
drawAspectCables(t); // the sky's geometry, drawn between its spheres
|
||||
for (const n of dests.values()) drawDestNode(n, t);
|
||||
if (!chakraMode) drawLoupe(t); // the hover zoom over a dense column
|
||||
drawEditing(t);
|
||||
if (!zeroMode) { // zero mode hides the node field — the ring UI replaces it
|
||||
drawCables(t);
|
||||
drawShockwaves(t, dt);
|
||||
drawParticles(dt);
|
||||
if (chakraMode) drawChakraView(t);
|
||||
else for (const n of sources.values()) drawSourceNode(n, t);
|
||||
drawAspectCables(t); // the sky's geometry, drawn between its spheres
|
||||
for (const n of dests.values()) drawDestNode(n, t);
|
||||
if (!chakraMode) drawLoupe(t); // the hover zoom over a dense column
|
||||
drawEditing(t);
|
||||
}
|
||||
if (performMode && !zenMode) drawPerform(t, dt);
|
||||
if (!zenMode && !performMode) drawHeader(t); // performance: no wordmark, no counts
|
||||
if (connected && !zenMode && !performMode) drawGodtime(t);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user