🌱 Z1.5 — stage nodes are spinning orbs (John's vision)

Each built group becomes a spinning orb on the stage, not a flat chip — a glowing
core in the group's hue with its picked feeds as petals orbiting around it (CSS
zspin/zpulse). They accumulate as you build: pick from fire → one orb; add cosmos
→ two; and on. Orbs breathe live — the core glows by the group's average value,
each petal brightens with its own feed. More feeds widen the orbit ring.

Verified live: fire (3 petals) + cosmos (2 petals) render as two spinning glowing
orbs; console clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-10 23:33:43 +10:00
parent d30cea469c
commit 4e44533cdd

View File

@ -2697,10 +2697,16 @@
+ ".zdot{width:40px;height:40px;border-radius:50%;flex:0 0 auto;position:relative}" + ".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{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}" + ".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{position:absolute;left:210px;right:210px;top:84px;bottom:150px;pointer-events:auto;display:flex;flex-wrap:wrap;align-content:center;justify-content:center;gap:22px;padding:14px;overflow:auto}"
+ ".zstage .zhint{margin:auto;color:rgba(150,170,205,.5);font-size:13px;font-style:italic;text-align:center}" + ".zstage .zhint{margin:auto;color:rgba(150,170,205,.5);font-size:13px;font-style:italic;text-align:center;max-width:280px}"
+ ".zchip{border-radius:13px;padding:11px 13px;min-width:130px;background:rgba(20,28,44,.72);border:1px solid;font-size:12px;color:#dbe6f8}" + ".zorb{position:relative;width:132px;height:150px;flex:0 0 auto;cursor:grab}"
+ ".zchip b{color:#eaf1ff}.zchip .mem{color:rgba(180,195,225,.8);font-size:11px;margin-top:5px;line-height:1.5}" + ".zorb .core{position:absolute;left:50%;top:44%;width:44px;height:44px;margin:-22px;border-radius:50%;animation:zpulse 3.2s ease-in-out infinite}"
+ ".zorb .orbit{position:absolute;left:0;right:0;top:0;height:132px;animation:zspin 15s linear infinite}"
+ ".zorb .pet{position:absolute;left:50%;top:50%;width:13px;height:13px;margin:-6.5px;border-radius:50%;box-shadow:0 0 6px currentColor}"
+ ".zorb .cap{position:absolute;left:0;right:0;bottom:0;text-align:center;font-size:11px;color:#dbe6f8}"
+ ".zorb .cap small{color:rgba(170,190,220,.6)}"
+ "@keyframes zspin{to{transform:rotate(360deg)}}"
+ "@keyframes zpulse{0%,100%{transform:scale(1);opacity:.9}50%{transform:scale(1.12);opacity:1}}"
+ ".zrack{position:absolute;left:210px;right:210px;bottom:20px;height:120px;pointer-events:auto;overflow:auto}" + ".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}" + ".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}" + ".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}"
@ -2791,21 +2797,31 @@
}, },
renderStage() { renderStage() {
if (!this.stage) return; if (!this.stage) return;
this.stage.innerHTML = ""; this.stage.innerHTML = ""; this.orbs = [];
const keys = Object.keys(this.nodes); const keys = Object.keys(this.nodes);
if (!keys.length) { if (!keys.length) {
const hint = document.createElement("div"); hint.className = "zhint"; 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)."; hint.textContent = "click a ring on either side → pick a feed → it becomes a spinning orb here. gather more and the orbs multiply. (drag to a voice — soon.)";
this.stage.appendChild(hint); return; this.stage.appendChild(hint); return;
} }
for (const gk of keys) { for (const gk of keys) { // each built group → a spinning orb; they accumulate
const meta = GROUP_META[gk], node = this.nodes[gk]; const meta = GROUP_META[gk], mem = [...this.nodes[gk].members];
const chip = document.createElement("div"); chip.className = "zchip"; chip.style.borderColor = meta.hue; const orb = document.createElement("div"); orb.className = "zorb"; orb.title = mem.map(k => (sources.get(k) || {}).label || k).join(" · ");
const names = [...node.members].map(k => { const n = sources.get(k); return n ? n.label : k; }); const orbit = document.createElement("div"); orbit.className = "orbit";
chip.innerHTML = '<b style="color:' + meta.hue + '">' + meta.emoji + " " + gk + "</b> · " + node.members.size const R = 44 + Math.min(mem.length, 8) * 1.5; // more feeds → a slightly wider ring
+ '<div class="mem">' + names.map(x => x.replace(/</g, "&lt;")).join(" · ") + "</div>"; mem.forEach((k, i) => {
chip._group = gk; const a = (i / mem.length) * Math.PI * 2;
this.stage.appendChild(chip); const pet = document.createElement("div"); pet.className = "pet";
pet.style.color = meta.hue; pet.style.background = meta.hue;
pet.style.transform = "translate(" + (Math.cos(a) * R).toFixed(1) + "px," + (Math.sin(a) * R).toFixed(1) + "px)";
pet._src = k; orbit.appendChild(pet);
});
const core = document.createElement("div"); core.className = "core"; core.style.background = meta.hue;
const cap = document.createElement("div"); cap.className = "cap";
cap.innerHTML = meta.emoji + " " + gk + " <small>· " + mem.length + "</small>";
orb.appendChild(orbit); orb.appendChild(core); orb.appendChild(cap);
orb._group = gk; orb._core = core;
this.stage.appendChild(orb); this.orbs.push(orb);
} }
}, },
update() { update() {
@ -2823,6 +2839,14 @@
row._fill.style.width = Math.round((n ? (n.normDisp || n.norm || 0) : 0) * 100) + "%"; row._fill.style.width = Math.round((n ? (n.normDisp || n.norm || 0) : 0) * 100) + "%";
} }
} }
if (this.orbs) for (const orb of this.orbs) { // orbs breathe: core glows by group avg, petals by feed
const v = _grpAvg(orb._group);
orb._core.style.boxShadow = "0 0 " + (8 + v * 24).toFixed(0) + "px " + (1 + v * 3).toFixed(1) + "px " + GROUP_META[orb._group].hue;
for (const pet of orb.querySelectorAll(".pet")) {
const n = sources.get(pet._src);
pet.style.opacity = (0.32 + (n ? (n.normDisp || n.norm || 0) : 0) * 0.68).toFixed(2);
}
}
}, },
}; };