diff --git a/viz/index.html b/viz/index.html index b751040..8ad5b28 100644 --- a/viz/index.html +++ b/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 + ""; + 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(/" + k + ""; + 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 = '' + meta.emoji + " " + gk + " ยท " + node.members.size + + '