From 12a296f284b1b8d981b9d25fb907176be5ec8375 Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Sat, 25 Jul 2026 12:00:05 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AA=20gig=20layer:=20pulse=20cables,?= =?UTF-8?q?=20speed-at-a-glance,=20sleeper=20fold,=20no=20ghost=20cables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four hands-on changes from live-play feedback (Jono's "what speed are those things going at?"): - ghost cables: a patched cable whose source is silent no longer vanishes — alpha/width floor (va = max(activeDisp, 0.10)) on the idle line, dashes and dot; glow still reserved for active cables. - speed at a glance: flowLabel() speaks each source's rate in bars of the current godtime ("streaming · ≈ 1/8 bar · ≈ every 5 bars · asleep, still 12m · glacial"); shown in the node right-click live readout and as a second line in every loupe source row. Sources track _lastMoveT (last real data). - ⚡ PULSE: per-cable clocked throb — r.pulse ∈ {16,8,4,1} pulses/bar; the shaped value rides a raised-cosine synced to seqPhase, so a glacial feed (debt, declination) becomes a ≥1/16 groove whose DEPTH is the slow value. Cycled via the ⚡ chip on each cable row (off → ÷16 → ÷8 → ÷4 → bar), serialized with the patch, undo-marked, whitelisted on load. - 💤 sleeper fold: cableless sources with no signal (norm ≤ 0.02) and no movement for 45s fold out of the field; they wake THEMSELVES when their feed speaks (lastMove refresh). Bottom-left chip shows the count and toggles the fold (localStorage gs_fold). Field view only — skins keep their full cosmologies, zero stays zero, disconnected dev view unaffected. Verified live (static serve + temp hooks, removed & grep-verified 0): pulse active == norm·window to 0 error over 12 same-frame samples and cycles ⚡÷16→÷8→÷4→bar→off; ghost cable visible on a dead venus.dist route; labels discriminate (mercury "≈ every 5 bars", neptune "glacial", earth.beat honestly "waiting…" — it never moves); fold hides dead feeds with chip "💤 2 quiet feeds folded", chip toggle restores, woken feed rejoins field; zero console errors; T O B G S + ⌘Z regression green; grimoire + manual regenerated. Co-Authored-By: Claude Fable 5 --- viz/index.html | 122 ++++++++++++++++++++++++++++++++++++++++-------- viz/manual.html | 7 +++ 2 files changed, 109 insertions(+), 20 deletions(-) diff --git a/viz/index.html b/viz/index.html index 298857d..294d33f 100644 --- a/viz/index.html +++ b/viz/index.html @@ -664,6 +664,11 @@ @media (max-width: 520px){ #grimoire .spec .row { grid-template-columns: 1fr; gap: 3px; } } 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 */ + #sleepchip { position: fixed; left: 14px; bottom: 52px; z-index: 12; display: none; cursor: pointer; + background: rgba(20,28,44,0.82); color: rgba(160,180,215,0.75); border: 1px solid rgba(120,150,200,0.22); + border-radius: 9px; padding: 4px 10px; font-size: 11px; -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); } + #sleepchip:hover { color: #cfe0ff; border-color: rgba(140,170,230,0.5); }
@@ -1235,6 +1240,8 @@
  • 🕊 PSALM — sing, and the instrument answers. Right-click the sky → 🕊 PSALM, allow the microphone, and sing. The pitch is heard, folded to the scale you're in (so you're never out of key), and played as notes — a 🕊 chip shows the note leaving your throat. And the duet with ⚡ GODSPEED: with the arp running, each note you sing latches into the held chord, one at a time — hum four notes and the machine arpeggiates the chord you sang. It listens locally: the analysis happens in the page, in its own silence, and nothing ever leaves the machine — no recording, no upload, just your voice becoming the instrument's.
  • ✦ CANON — the shelf of worked examples. Open ⚙ → DIMENSIONS and at the top of the list, above your own saved worlds, sit four ✦ factory dimensions that ship with the instrument — the heartbeat (a boom-bap kit with the swing and the kick's duck), the wheel within the wheel (WHEELS, made audible), the squeeze (the market pumping GODSQUASH), and quiet arrival (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 read-only, the same for everyone, a place to start rather than a thing to overwrite.
  • 🪐 spheres — the music of the spheres. Press S (or right-click the sky → 🪐 spheres) and turn it on: the eight planets join the field as modulation sources, the slowest LFOs there are. Each planet offers three signals — phase (its heliocentric longitude, a sawtooth one full turn per orbit), dist (its eccentric breathing, in and out between perihelion and aphelion — Mercury and Mars swing hard, Venus barely), and beat (its synodic dance against Earth — Venus and Earth clasp every 583.9 days, the pentagram cycle). Route any of them like any source, or just say "mars drive filter." One knob rules time: the rate, from 1 to 3650 days of sky per bar — 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 ☸ orbital ratios button sets the drum lanes' wheels to the true resonances — 4 : 8 : 16 (Io, Europa and Ganymede, locked at 1 : 2 : 4) and 8 : 13 (Venus to Earth, the pentagram in wheel form), so the polymeter you hear is one the solar system actually keeps. It is all computed here, at the instrument's own clock — 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.
  • +
  • ⚡ Pulse — make the slow world playable. 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 chip on any cable to cycle ÷16 · ÷8 · ÷4 · bar: the cable now throbs on the godtime clock, and the slow value becomes the depth 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.
  • +
  • 💤 The sleeper fold — and speed at a glance. The field folds away quiet feeds — sources with no cables, no signal and nothing to say for a minute — behind a small 💤 chip (bottom-left) that shows the count and brings them back with one click. The moment a folded feed speaks, it wakes itself and rejoins the field: the world breathes in and out of view. And every cable now moves at the data's own rate — streaming feeds race, glacial ones crawl — with the words to match: hover a column and each source row reads its speed (streaming · ≈ every 3 bars · asleep, still 12m), and a node's right-click readout says the same. What speed is that thing going at? Now it just tells you.
  • 📽 The stage — send the visual to any screen. Right-click the sky → the stage 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 double-click for fullscreen. The stage has a camera: the whole view by default (whatever you're in — a skin, the wheel, mind mode's POV), or right-click any node → project this node and the camera glides after that one sphere through its orbits, or project its station 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.
  • 🕸 The commune — play together, properly. Right-click the sky → the commune. This is for signed-in players: your session rides the connection, so the hub knows exactly who everyone is — no impostors, no setup, no Bluetooth. One of you hosts: name the room, then ✉ invite the others by their username (they must be on the site — invitations reach the present). The invited see the invitation arrive live and join or decline; anyone may leave whenever they wish, and if the host leaves, the room closes with them. Then the sections: members claim voices (right-click any voice, or the panel's picker), or the host allocates them — this bass to her, those pads to him, the godtime itself to whoever conducts — and the hub enforces it: only the section's holder can speak for it. From that moment your levels, mutes, grooves and arranger lanes for what you hold follow your hands on every screen, same living room or opposite sides of the earth. The world — the Source — stays identical for everyone, because it always was. Three people, one planet, one instrument.
  • @@ -3592,8 +3599,11 @@ } r.active = Math.abs(amt) * shaped; continue; } - destVals[r.dest] = (destVals[r.dest] || 0) + amt * shaped; - r.active = Math.abs(amt * shaped); + let sh = shaped; + if (r.pulse) // ⚡ PULSE — a glacial value rides a clocked wave: slow data = depth, godtime = motion + sh *= 0.5 - 0.5 * Math.cos(((seqPhase * r.pulse / 16) % 1) * Math.PI * 2); + destVals[r.dest] = (destVals[r.dest] || 0) + amt * sh; + r.active = Math.abs(amt * sh); } for (const k in destVals) destVals[k] = clamp(destVals[k], 0, 1); for (const k in notes) destVals[k] = notes[k]; @@ -3779,7 +3789,7 @@ function serializePatch() { return { routes: routesArr.map(r => ({ source: r.source, dest: r.dest, amount: r.amount, - curve: r.curve || "lin", gate: r.gate, quantize: r.quantize, label: r.label })), + curve: r.curve || "lin", gate: r.gate, quantize: r.quantize, pulse: r.pulse || undefined, label: r.label })), tweaks: JSON.parse(JSON.stringify(tweaks)), seqs: JSON.parse(JSON.stringify(seqs)), // your grooves travel with the patch arr: { grid: JSON.parse(JSON.stringify(arr.grid)), banks: JSON.parse(JSON.stringify(arr.banks)), @@ -4849,8 +4859,8 @@ const botPad = FOOTER_H + 20; const usableH = Math.max(120, H - topPad - botPad); - // group sources by family for nicer grouping order - const srcList = Array.from(sources.values()); + // group sources by family for nicer grouping order (💤 sleepers sit out of the field) + const srcList = Array.from(sources.values()).filter(n => !n.hidden); const famOrder = ["body","planet","sky","markets","human","other"]; srcList.sort((a, b) => { const fa = famOrder.indexOf(a.family), fb = famOrder.indexOf(b.family); @@ -4883,6 +4893,41 @@ const loupe = { on: true, active: false, side: null, y: 0, rows: [], frozen: false, panel: null }; try { loupe.on = localStorage.getItem("gs_loupe") !== "0"; } catch (e) {} + // ---- 💤 the sleeper fold — a calm first load ------------------------------ + // A hundred sources where thirty are near-silent is the deep-god view answering + // a question nobody asked yet. Cableless sources with no signal (norm ≈ 0) and + // no recent movement fold out of the field; they wake THEMSELVES the moment + // their feed speaks (lastMove refreshes), and the 💤 chip is the door to all of + // them. Field view only — the skins keep their full cosmologies, zero is zero. + let sleepFold = true; + try { sleepFold = localStorage.getItem("gs_fold") !== "0"; } catch (e) {} + let sleepChipEl = null; + function sleepChip(nAsleep) { + if (!sleepChipEl) { + sleepChipEl = document.createElement("div"); sleepChipEl.id = "sleepchip"; + sleepChipEl.onclick = () => { sleepFold = !sleepFold; + try { localStorage.setItem("gs_fold", sleepFold ? "1" : "0"); } catch (e) {} + layoutDirty = true; }; + document.body.appendChild(sleepChipEl); + } + const txt = nAsleep ? (sleepFold ? "💤 " + nAsleep + " quiet feeds folded — show" : "💤 fold " + nAsleep + " quiet feeds") : ""; + if (sleepChipEl._t !== txt) { sleepChipEl._t = txt; sleepChipEl.textContent = txt; sleepChipEl.style.display = txt ? "block" : "none"; } + } + function sleepFoldPass(t) { + const canFold = !chakraMode && !zeroMode && connected; + const cabled = new Set(); + for (const r of routesArr) cabled.add(r.source); + let slept = 0; + for (const n of sources.values()) { + const asleep = canFold && !cabled.has(n.key) && !selection.has(n.key) && soloKey !== n.key && + (n.norm || 0) <= 0.02 && (t - (n._lastMoveT != null ? n._lastMoveT : -1e9)) > 45; + if (asleep) slept++; + const hide = asleep && sleepFold; + if (hide !== !!n.hidden) { n.hidden = hide; layoutDirty = true; } + } + sleepChip(canFold ? slept : 0); + } + function updateLoupe(mx, my) { if (!loupe.on || chakraMode || performMode || zenMode || !columnInfo || inspectEl.classList.contains("open") || arr.open) { loupe.active = false; return; } @@ -4950,7 +4995,14 @@ ctx.fillText(truncate(n.label, 22), panelX + 30, ry + ROWH / 2); ctx.textAlign = "right"; ctx.font = "500 10px 'Helvetica Neue', Arial, sans-serif"; ctx.fillStyle = "rgba(150,170,200,0.7)"; - ctx.fillText(n.isNote ? midiToName(n.raw) : fmtRaw(n.rawDisp != null ? n.rawDisp : n.raw), panelX + panelW - 8, ry + ROWH / 2); + if (n.kind === "source") { // sources answer Jono's question right in the row: value, then speed + ctx.fillText(fmtRaw(n.rawDisp != null ? n.rawDisp : n.raw), panelX + panelW - 8, ry + 8); + ctx.font = "500 8px 'Helvetica Neue', Arial, sans-serif"; + ctx.fillStyle = "rgba(150,170,200,0.55)"; + ctx.fillText(flowLabel(n), panelX + panelW - 8, ry + ROWH - 6); + } else { + ctx.fillText(n.isNote ? midiToName(n.raw) : fmtRaw(n.rawDisp != null ? n.rawDisp : n.raw), panelX + panelW - 8, ry + ROWH / 2); + } rows.push({ node: n, x0: panelX, y0: ry, x1: panelX + panelW, y1: ry + ROWH }); } ctx.strokeStyle = "rgba(150,180,230,0.3)"; ctx.lineWidth = 1; @@ -5274,6 +5326,7 @@ curve: r.curve || "lin", gate: (r.gate != null ? r.gate : undefined), quantize: r.quantize || undefined, + pulse: ([16, 8, 4, 1].indexOf(r.pulse) >= 0 ? r.pulse : 0), // ⚡ PULSE — throb this cable on the clock (pulses per bar; 0 = off) label: r.label || "", dashPhase: Math.random() * 40 }; @@ -5449,6 +5502,7 @@ } } + sleepFoldPass(t); // 💤 quiet feeds fold; they wake themselves when the world speaks if (layoutDirty) doLayout(); spheresFrame(); // 🪐 RESONANCE — refresh the sphere sources on the great-year clock if (spheresMeterEls) spheresPanelTick(); // live meters while the panel is open @@ -5466,6 +5520,7 @@ n._flowPrev = n.norm; const ft = Math.max(fv, n.firePulse || 0); // an event firing IS data arriving n.flowDisp = approach(n.flowDisp || 0, ft, ft > (n.flowDisp || 0) ? 0.5 : 0.05, dt); + if (ft > 0.03) n._lastMoveT = t; // remember when this feed last actually spoke if (n.y !== n.targetY && n.targetY != null) n.y = approach(n.y, n.targetY, 0.12, dt); if (n.firePulse > 0) n.firePulse = Math.max(0, n.firePulse - dt * 1.8); } @@ -5493,7 +5548,7 @@ drawShockwaves(t, dt); drawParticles(dt); if (chakraMode) drawChakraView(t); - else for (const n of sources.values()) drawSourceNode(n, t); + else for (const n of sources.values()) { if (!n.hidden) drawSourceNode(n, t); } // 💤 sleepers sit out 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 @@ -5535,6 +5590,7 @@ const d = dests.get(r.dest); if (!s || !d) continue; const a = r.activeDisp; + const va = Math.max(a, 0.10); // a patched cable NEVER vanishes: a silent source still shows its wire const x1 = s.x + NODE_R + 6, y1 = s.y; const x2 = d.x - NODE_R - 6, y2 = d.y; @@ -5549,21 +5605,21 @@ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.bezierCurveTo(cx1, y1, cx2, y2, x2, y2); - ctx.lineWidth = 1 + a * 3.5; - ctx.strokeStyle = rgba(col, 0.08 + a * 0.30); + ctx.lineWidth = 1 + va * 3.5; + ctx.strokeStyle = rgba(col, 0.08 + va * 0.30); ctx.setLineDash([]); ctx.stroke(); - if (a > 0.02) { - // glowing flowing dashes + { + // glowing flowing dashes — drawn for EVERY cable; a dead source just crawls ctx.beginPath(); ctx.moveTo(x1, y1); ctx.bezierCurveTo(cx1, y1, cx2, y2, x2, y2); - ctx.lineWidth = 1.4 + a * 4.5; - ctx.strokeStyle = rgba(glow, 0.35 + a * 0.5); - ctx.setLineDash([2 + a * 8, 10 - a * 4]); + ctx.lineWidth = 1.4 + va * 4.5; + ctx.strokeStyle = rgba(glow, 0.30 + va * 0.5); + ctx.setLineDash([2 + va * 8, 10 - va * 4]); ctx.lineDashOffset = r.dashPhase; - if (fxLevel > 1) { // shadow blur on 50+ beziers is the #1 cost + if (fxLevel > 1 && a > 0.02) { // shadow blur on 50+ beziers is the #1 cost; no glow on silent wires ctx.shadowColor = rgba(col, 0.6); ctx.shadowBlur = 6 + a * 14; } @@ -5574,9 +5630,9 @@ const pt = ((-r.dashPhase * 0.004) % 1 + 1) % 1; const bp = bezierPoint(x1, y1, cx1, y1, cx2, y2, x2, y2, pt); ctx.beginPath(); - ctx.arc(bp.x, bp.y, 1.5 + a * 3, 0, Math.PI * 2); - ctx.fillStyle = rgba(rgbMix(col, 0.5), 0.6 + a * 0.4); - if (fxLevel > 1) { + ctx.arc(bp.x, bp.y, 1.5 + va * 3, 0, Math.PI * 2); + ctx.fillStyle = rgba(rgbMix(col, 0.5), 0.5 + va * 0.4); + if (fxLevel > 1 && a > 0.02) { ctx.shadowColor = rgba(col, 0.9); ctx.shadowBlur = 8 + a * 10; } @@ -7712,6 +7768,7 @@ function nodeAt(mx, my) { let hit = null, best = (NODE_R + 12) * (NODE_R + 12); for (const n of sources.values()) { + if (n.hidden) continue; // 💤 a folded sleeper is not a click target const dx = mx - n.x, dy = my - n.y, d2 = dx * dx + dy * dy; if (d2 < best) { best = d2; hit = n; } } @@ -7988,6 +8045,23 @@ if (a >= 10) return v.toFixed(1); return v.toFixed(3); } + // Jono's question, answered in words: "what speed is that thing going at?" + // Inverts the flow curve (flow = √(rate·8)) into ≈seconds per full 0..1 swing, + // then speaks it in bars of the CURRENT godtime — the musician's unit. + const fmtAgo = (s) => s < 90 ? Math.round(s) + "s" : s < 5400 ? Math.round(s / 60) + "m" : Math.round(s / 3600) + "h"; + function flowLabel(n) { + const f = n.flowDisp || 0; + if (f >= 0.85) return "streaming"; + if (f <= 0.02) { + const still = lastFrame - (n._lastMoveT != null ? n._lastMoveT : lastFrame); + return still > 60 ? "asleep · still " + fmtAgo(still) : "waiting…"; + } + const bars = (8 / (f * f)) / (60 / godtime.bpm * 4); + if (bars < 0.7) return "≈ 1/" + clamp(Math.round(1 / bars), 2, 32) + " bar"; + if (bars < 1.5) return "≈ every bar"; + if (bars < 300) return "≈ every " + Math.round(bars) + " bars"; + return "glacial"; + } let tickerPins = new Set(); // your watchlist — live values riding the ticker try { tickerPins = new Set(JSON.parse(localStorage.getItem("gs_pins") || "[]")); } catch (e) {} function savePins() { try { localStorage.setItem("gs_pins", JSON.stringify([...tickerPins])); } catch (e) {} } @@ -8043,7 +8117,8 @@ const sc = spark.getContext("2d"); const drawLive = () => { lv.textContent = "live: " + fmtVal(node.raw) + - (typeof node.norm === "number" ? " · " + node.norm.toFixed(2) + " into the matrix" : ""); + (typeof node.norm === "number" ? " · " + node.norm.toFixed(2) + " into the matrix" : "") + + " · " + flowLabel(node); const h = node.hist || []; sc.clearRect(0, 0, spark.width, spark.height); if (h.length > 1) { @@ -8092,9 +8167,16 @@ sl.type = "range"; sl.min = "0"; sl.max = "1"; sl.step = "0.01"; sl.value = String(rt.amount != null ? rt.amount : 0.5); sl.title = "amount"; sl.oninput = () => setRouteAmountLocal(rt.source, rt.dest, parseFloat(sl.value)); + const pu = document.createElement("span"); pu.className = "x"; // ⚡ PULSE — cycle off → ÷16 → ÷8 → ÷4 → bar + pu.title = "⚡ pulse — throb this cable on the clock (slow data becomes depth on a synced wave)"; + const PULSE_STEPS = [0, 16, 8, 4, 1]; + const paintPu = () => { pu.textContent = rt.pulse ? "⚡" + (rt.pulse === 1 ? "bar" : "÷" + rt.pulse) : "⚡"; pu.style.opacity = rt.pulse ? "1" : "0.4"; }; + paintPu(); + pu.onclick = () => { ungodlyMark("pulse"); + rt.pulse = PULSE_STEPS[(PULSE_STEPS.indexOf(rt.pulse || 0) + 1) % PULSE_STEPS.length]; paintPu(); }; const x = document.createElement("span"); x.className = "x"; x.textContent = "×"; x.title = "unpatch"; x.onclick = () => { ungodlyMark("unpatch"); removeRouteLocal(rt.source, rt.dest); row.remove(); }; - row.appendChild(lbl); row.appendChild(sl); row.appendChild(x); + row.appendChild(lbl); row.appendChild(sl); row.appendChild(pu); row.appendChild(x); cb.appendChild(row); } if (rel.length) ctxItem("💾 save these as a vibe", () => { // this node's cables → a shareable vibe (Z3) diff --git a/viz/manual.html b/viz/manual.html index d6bb0eb..e542449 100644 --- a/viz/manual.html +++ b/viz/manual.html @@ -668,6 +668,11 @@ @media (max-width: 520px){ #grimoire .spec .row { grid-template-columns: 1fr; gap: 3px; } } 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 */ + #sleepchip { position: fixed; left: 14px; bottom: 52px; z-index: 12; display: none; cursor: pointer; + background: rgba(20,28,44,0.82); color: rgba(160,180,215,0.75); border: 1px solid rgba(120,150,200,0.22); + border-radius: 9px; padding: 4px 10px; font-size: 11px; -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); } + #sleepchip:hover { color: #cfe0ff; border-color: rgba(140,170,230,0.5); }