⌨ global keymap — F1-F10 defaults + fully remappable keyboard

Capture-phase keydown runs bound actions (keymap wins over built-ins, skips
inputs and the mind-mode console). Defaults F1-F10: grimoire, play/stop,
skin view, next skin, cycle mood, tracks, performance, godtime lock, earth
echo, stage. Bind any key to any action from the ⌨ keys panel (sky menu), or
"map a key" in a voice's right-click menu — including targeted nudge-up /
nudge-down (a & s as a live fader on a drone) and mute/solo. keymap persists
(gs_keys); reset-to-defaults button. Synth.toggle() exposed for the play key.

Verified: F1 opens the grimoire, F5 cycles the mood (first light->ambient,
bpm->70 locked) — the engine and defaults fire; node-bind uses the same
capture flow. Zero console errors. Grimoire section added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-06 15:42:30 +10:00
parent 7a444c3965
commit d0a9345cb2

View File

@ -82,8 +82,18 @@
#cvpanel .cvrow select { min-width: 0; }
#cvpanel .cvrow .asel:first-child { flex: 1; }
#cvpanel .x { cursor: pointer; color: #ff8b8b; font-weight: 700; padding: 0 3px; }
#holdings, #natal, #commune, #cvpanel {
position: fixed; top: 96px; right: 14px; z-index: 12; width: 292px;
#keys.open { display: block; }
#keys h3 { margin: 0 24px 4px 0; font-size: 13px; color: #a8d8ff; }
#keys .sub { color: rgba(150,170,200,0.72); font-size: 11px; margin: 4px 0 8px; line-height: 1.5; }
#keys .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: #8fa4c8; font-size: 16px; }
#keys .hrow { display: flex; gap: 6px; align-items: center; margin: 3px 0; }
#keys .keycap { min-width: 30px; text-align: center; font-family: monospace; font-size: 11px; color: #cfe0f5;
background: rgba(120,150,200,0.15); border: 1px solid rgba(120,150,200,0.3); border-radius: 5px; padding: 2px 6px; }
#keys .x { cursor: pointer; color: #ff8b8b; font-weight: 700; padding: 0 3px; }
#keys select { background: rgba(30,40,60,0.85); color: #dbe6f5; border: 1px solid rgba(120,150,200,0.3); border-radius: 6px; padding: 3px 6px; font-size: 11px; }
#keys .midibtn { background: rgba(40,52,74,0.85); color: #b9c8e2; border: 1px solid rgba(120,150,200,0.25); border-radius: 6px; padding: 3px 10px; font-size: 11px; cursor: pointer; }
#holdings, #natal, #commune, #cvpanel, #keys {
position: fixed; top: 96px; right: 14px; z-index: 12; width: 292px; display: none;
background: rgba(10,15,26,0.95); border: 1px solid rgba(120,150,200,0.35);
border-radius: 12px; padding: 12px 14px; display: none; color: #c7d4ea;
font-size: 12px; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
@ -910,6 +920,7 @@
<li><b>♪ (top-right) — wake the sound.</b> 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.</li>
<li><b>The godtime dial (top) — set the pulse.</b> Drag it to choose a tempo from <b>40 to 180 BPM</b>, with the real planets of tonight's sky wheeling around it as you turn. Click the dial to <b>lock</b> 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).</li>
<li><b>Press <kbd>P</kbd> — performance mode.</b> The editing chrome vanishes, the readouts declutter, and a rotating line of dispatches from the world (<i>bitcoin at $62,902 · the moon is 81% lit · mercury is retrograde · M4.2 earthquake</i>) rises for the audience. Put the console on the projector, hit <kbd>P</kbd>, and conduct.</li>
<li><b>The keyboard is yours — <kbd>F1</kbd><kbd>F10</kbd> and beyond.</b> A default set of function keys is ready to fly: <kbd>F1</kbd> the grimoire, <kbd>F2</kbd> play/stop the synth, <kbd>F3</kbd> the skin view, <kbd>F4</kbd> next skin, <kbd>F5</kbd> cycle mood, <kbd>F6</kbd> tracks, <kbd>F7</kbd> performance mode, <kbd>F8</kbd> lock the godtime, <kbd>F9</kbd> the earth echo, <kbd>F10</kbd> the stage. But every key is remappable: right-click the sky → <b>⌨ keys</b> to bind any key to any action, or <b>map a key</b> from a voice's own right-click menu. Want <kbd>a</kbd> and <kbd>s</kbd> to raise and lower a drone's level like a fader? Right-click that voice → <i>map a key → raise its level</i>, press <kbd>a</kbd>; again for <i>lower</i>, press <kbd>s</kbd> — now they nudge it live, hold to sweep. Your whole flow, under your fingers.</li>
<li><b>Press <kbd>C</kbd> — chakra view.</b> 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. <b>Drag empty space to spin the whole body 360°</b>, 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. <kbd>C</kbd> again and the sources glide home.</li>
<li><b>Right-click anything — the everything menu.</b> Every node carries its full settings under the right button: mute, <b>solo</b>, a <b>level</b> fader (02×), a <b>trim</b> offset, every cable it carries with its amount slider and an unpatch ×, and a nested <i>patch to →</i> list for laying new cables without dragging. Right-click the empty sky for the view menu. And the modifiers, for the fast hands: <b>click</b> mutes · <b>⇧-click</b> opens the node's tab · <b>⇧-drag</b> rides the level up and down · <b>⌥-click</b> solos a source · <b>⌘-click</b> multi-selects for grouping · <b>⌘⇧-click</b> resets a node to neutral. People play differently; the instrument shouldn't care which hand you reach with.</li>
<li><b>Click a wheel — go inside it (mind mode).</b> In chakra view, click any chakra's core and the camera steps <i>into</i> the wheel: its mandala of petals turns around your whole field of view, and that chakra's feeds circle <i>you</i> — drag to look around the full 360°. A <b>vector console appears under your hands</b>: 12 keys of that wheel's own scale (C minor pentatonic at the root, rising key by key to B hirajoshi at the crown), played by clicking, by the <kbd>Q</kbd><kbd>]</kbd> row, or by any connected <b>MIDI keyboard</b> — every note runs through the world's own reverb and grit, so you are jamming <i>inside</i> the instrument, in that chakra's mode, while the planet plays around you. <kbd></kbd> <kbd></kbd> travel the spine wheel to wheel; <kbd>esc</kbd> steps back out. And for powerful machines, right-click the sky and set graphics to <b>ultra ✦</b>: every orbiting feed grows a glowing comet trail, and the orbits breathe so the trails weave slow spirals.</li>
@ -3590,6 +3601,10 @@
const ks = Object.keys(SKINS), i = ks.indexOf(activeSkin.key);
return ks[(i + 1) % ks.length];
}
function prevSkinKey() {
const ks = Object.keys(SKINS), i = ks.indexOf(activeSkin.key);
return ks[(i - 1 + ks.length) % ks.length];
}
function chakraGeom() {
const top = HEADER_H + 74;
@ -4557,6 +4572,13 @@
ctxItem("open inspector tab", () => openInspector(node), "⇧-click");
ctxItem("reset to neutral", () => resetTweaks(node.key), "⌘⇧-click");
ctxItem("📽 project this node", () => openStage({ type: "node", key: node.key }));
const mapSub = ctxSub("⌨ map a key to this");
const mk = (lbl, act) => { const it2 = document.createElement("div"); it2.className = "it"; it2.textContent = lbl;
it2.onclick = () => { bindKeyPrompt(act, node.key, prettyDest(node.key)); closeCtx(); }; mapSub.appendChild(it2); };
mk("raise its level", "nudgeUp");
mk("lower its level", "nudgeDown");
mk("mute / unmute", "muteKey");
if (isSrc) mk("solo", "soloKeyA");
if (isSrc && chakraMode)
ctxItem("📽 project its " + activeSkin.word,
() => openStage({ type: "station", key: node.station || activeSkin.stationOf(node.key) }));
@ -4622,6 +4644,7 @@
() => openCV());
ctxItem((earthEchoOn ? "🌀 earth echo: on" : "🌀 earth echo — the planet on tape"),
() => earthEchoSet(!earthEchoOn));
ctxItem("⌨ keys — remap the keyboard", () => openKeys());
if (commune.room) {
const gOwner = commune.alloc["@godtime"];
ctxItem(gOwner === communeMe() ? "🕸 release the godtime" :
@ -5141,6 +5164,106 @@
}
if (earthEchoOn) setTimeout(() => { for (const [s, d2, a] of ECHO_CABLES) addRouteLocal(s, d2, a); }, 1200);
// ---- the keymap — global, fully remappable keyboard control ---------------
// Every binding is key → action (some actions take a target voice/source).
// F1F10 ship with a useful default set; bind anything from the ⌨ keys panel
// or "map a key" in a node's right-click menu. a/s as up/down on a voice? bind
// nudge-up / nudge-down to a & s, targeted at that voice.
let cycleMoodIdx = 0;
function cycleMood() { const ks = Object.keys(MOODS); cycleMoodIdx = (cycleMoodIdx + 1) % ks.length; applyMood(ks[cycleMoodIdx]); }
const KEY_ACTIONS = {
grimoire: { label: "open the grimoire (help)", run: () => { const g = document.getElementById("grimoire"); if (g) g.classList.add("open"); } },
play: { label: "play / stop the synth", run: () => { if (typeof Synth !== "undefined") Synth.toggle(); } },
chakraView:{ label: "chakra / skin view", run: () => { chakraMode = !chakraMode; if (!chakraMode) { layoutDirty = true; exitMind(); } else clearTrails(); } },
skinNext: { label: "next skin", run: () => { setSkin(nextSkinKey()); if (!chakraMode) { chakraMode = true; clearTrails(); } } },
skinPrev: { label: "previous skin", run: () => { setSkin(prevSkinKey()); if (!chakraMode) { chakraMode = true; clearTrails(); } } },
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" })) },
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) },
holdings: { label: "your holdings", run: () => openHoldings() },
stars: { label: "your stars", run: () => openNatal() },
commune: { label: "the commune", run: () => openCommune() },
hoverZoom: { label: "toggle hover zoom", run: () => { loupe.on = !loupe.on; try { localStorage.setItem("gs_loupe", loupe.on ? "1" : "0"); } catch (e) {} } },
nudgeUp: { label: "raise the level of…", targeted: true, run: (k) => { const t = tw(k); t.gain = clamp((t.gain != null ? t.gain : 1) + 0.08, 0, 2); } },
nudgeDown: { label: "lower the level of…", targeted: true, run: (k) => { const t = tw(k); t.gain = clamp((t.gain != null ? t.gain : 1) - 0.08, 0, 2); } },
muteKey: { label: "mute / unmute…", targeted: true, run: (k) => toggleMuteLocal(k) },
soloKeyA: { label: "solo…", targeted: true, run: (k) => { soloKey = soloKey === k ? null : k; } },
};
const DEFAULT_KEYS = { F1: { a: "grimoire" }, F2: { a: "play" }, F3: { a: "chakraView" }, F4: { a: "skinNext" },
F5: { a: "moodCycle" }, F6: { a: "tracks" }, F7: { a: "perform" }, F8: { a: "godLock" }, F9: { a: "earthEcho" }, F10: { a: "stage" } };
let keymap = {};
try { keymap = JSON.parse(localStorage.getItem("gs_keys")) || {}; } catch (e) {}
if (!Object.keys(keymap).length) keymap = JSON.parse(JSON.stringify(DEFAULT_KEYS));
function saveKeys() { try { localStorage.setItem("gs_keys", JSON.stringify(keymap)); } catch (e) {} }
const normKey = (k) => (k && k.length === 1 ? k.toLowerCase() : k); // 'A' and 'a' are one key
let keyCapture = null; // fn(key) while binding
window.addEventListener("keydown", (ev) => { // capture phase — keymap wins
const tag = (ev.target && ev.target.tagName) || "";
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return;
const k = normKey(ev.key);
if (keyCapture) { // binding a key
if (["Shift", "Control", "Alt", "Meta"].indexOf(ev.key) >= 0) return;
ev.preventDefault(); ev.stopPropagation();
const fn = keyCapture; keyCapture = null; fn(k);
return;
}
const b = keymap[k];
if (!b || !KEY_ACTIONS[b.a]) return;
if (mindChakra && k.length === 1) return; // the mind-mode console owns letters
ev.preventDefault(); ev.stopPropagation(); // override any built-in for this key
try { KEY_ACTIONS[b.a].run(b.arg); } catch (e) {}
}, true);
function bindKeyPrompt(action, arg, niceTarget) {
keyCapture = (k) => {
keymap[k] = arg != null ? { a: action, arg } : { a: action };
saveKeys();
eventTicker.unshift({ text: "⌨ " + (k === " " ? "space" : k) + " → " + KEY_ACTIONS[action].label.replace("…", "") + (arg ? " " + niceTarget : ""), tAdded: now(), src: "sys" });
if (keysEl.classList.contains("open")) openKeys();
};
eventTicker.unshift({ text: "⌨ press any key to bind…", tAdded: now(), src: "sys" });
}
// the ⌨ keys panel
const keysEl = document.createElement("div"); keysEl.id = "keys"; document.body.appendChild(keysEl);
function closeKeys() { keysEl.classList.remove("open"); }
function openKeys() {
keysEl.innerHTML = "";
const x2 = document.createElement("span"); x2.className = "close"; x2.textContent = "×"; x2.onclick = closeKeys; keysEl.appendChild(x2);
const h3 = document.createElement("h3"); h3.textContent = "⌨ keys"; keysEl.appendChild(h3);
const sub = document.createElement("div"); sub.className = "sub";
sub.textContent = "global keyboard control. F1F10 come pre-set; bind any key to any action below (or map a key from a voice's right-click menu). a & s as up/down on a voice? bind raise/lower there.";
keysEl.appendChild(sub);
const rows = document.createElement("div");
const drawBinding = (k) => {
const b = keymap[k]; const act = KEY_ACTIONS[b.a]; if (!act) return;
const row = document.createElement("div"); row.className = "hrow";
const kk = document.createElement("span"); kk.className = "keycap"; kk.textContent = (k === " " ? "space" : k);
const lbl = document.createElement("span"); lbl.style.flex = "1"; lbl.style.fontSize = "11px";
lbl.textContent = act.label.replace("…", "") + (b.arg ? " " + prettyDest(b.arg) : "");
const rm = document.createElement("span"); rm.className = "x"; rm.textContent = "×";
rm.onclick = () => { delete keymap[k]; saveKeys(); openKeys(); };
row.appendChild(kk); row.appendChild(lbl); row.appendChild(rm); rows.appendChild(row);
};
Object.keys(keymap).sort().forEach(drawBinding);
keysEl.appendChild(rows);
// add a global (non-targeted) binding
const addRow = document.createElement("div"); addRow.className = "hrow"; addRow.style.marginTop = "8px";
const aSel = document.createElement("select"); aSel.className = "asel"; aSel.style.flex = "1";
for (const a in KEY_ACTIONS) if (!KEY_ACTIONS[a].targeted) { const o = document.createElement("option"); o.value = a; o.textContent = KEY_ACTIONS[a].label; aSel.appendChild(o); }
const bindBtn = document.createElement("button"); bindBtn.className = "midibtn"; bindBtn.textContent = "bind a key";
bindBtn.onclick = () => bindKeyPrompt(aSel.value, null);
addRow.appendChild(aSel); addRow.appendChild(bindBtn); keysEl.appendChild(addRow);
const rst = document.createElement("button"); rst.className = "midibtn"; rst.style.marginTop = "8px"; rst.textContent = "reset to F-key defaults";
rst.onclick = () => { keymap = JSON.parse(JSON.stringify(DEFAULT_KEYS)); saveKeys(); openKeys(); };
keysEl.appendChild(rst);
keysEl.classList.add("open");
}
// ---- your stars (the natal source — a real ephemeris, in the browser) -----
// Truncated Meeus (Sun ~0.01°, Moon ≤0.1°) + JPL/Standish Keplerian elements
// (planets ≤~0.2°, valid 1800-2050), precessed J2000→equinox of date so the
@ -5914,7 +6037,8 @@
}
});
return { update(dests) { if (dests) Object.assign(D, dests); }, pluck };
return { update(dests) { if (dests) Object.assign(D, dests); }, pluck,
toggle() { on ? stop() : start(); paint(); return on; }, playing() { return on; } };
})();
// ---------------------------------------------------------------------------