diff --git a/viz/index.html b/viz/index.html
index 6d31521..0d99d97 100644
--- a/viz/index.html
+++ b/viz/index.html
@@ -1993,11 +1993,11 @@
scRow.appendChild(scL);
const rootSel = document.createElement("select"); rootSel.className = "asel";
NOTE_NAMES.forEach((nm, i) => { const o = document.createElement("option"); o.value = String(i); o.textContent = nm; if (i === gScale.root) o.selected = true; rootSel.appendChild(o); });
- rootSel.onchange = () => { gScale.root = +rootSel.value; openArranger(); };
+ rootSel.onchange = () => { ungodlyMark("scale"); gScale.root = +rootSel.value; openArranger(); };
scRow.appendChild(rootSel);
const scaleSel = document.createElement("select"); scaleSel.className = "asel";
Object.keys(SCALES).forEach(nm => { const o = document.createElement("option"); o.value = nm; o.textContent = nm.replace("_", " "); if (nm === gScale.name) o.selected = true; scaleSel.appendChild(o); });
- scaleSel.onchange = () => { gScale.name = scaleSel.value; SEQ_ROWS = scaleRows(gScale.name); openArranger(); };
+ scaleSel.onchange = () => { ungodlyMark("scale"); gScale.name = scaleSel.value; SEQ_ROWS = scaleRows(gScale.name); openArranger(); };
scRow.appendChild(scaleSel);
const scHint = document.createElement("span"); scHint.className = "albl";
scHint.textContent = "every groove is in this key — change it and your patterns re-harmonise (the roll is always folded to the scale)";
@@ -2013,7 +2013,7 @@
const vv = document.createElement("span"); vv.className = "gval";
const show = () => vv.textContent = Math.round(get() / max * 100) + "%";
sl.value = String(Math.round(get() / max * 1000)); show();
- sl.oninput = () => { set(clamp(+sl.value / 1000 * max, 0, max)); show(); };
+ sl.oninput = () => { ungodlyGesture("groove"); set(clamp(+sl.value / 1000 * max, 0, max)); show(); };
w.appendChild(lb); w.appendChild(sl); w.appendChild(vv);
return w;
};
@@ -2024,7 +2024,7 @@
for (const pn of Object.keys(GROOVE_PRESETS)) {
const pb = document.createElement("button"); pb.className = "midibtn gpreset"; pb.textContent = pn;
pb.classList.toggle("on", groove.preset === pn);
- pb.onclick = () => { Object.assign(groove, GROOVE_PRESETS[pn]); groove.preset = pn; openArranger(); };
+ pb.onclick = () => { ungodlyMark("groove preset"); Object.assign(groove, GROOVE_PRESETS[pn]); groove.preset = pn; openArranger(); };
gr.appendChild(pb);
}
body.appendChild(gr);
@@ -2039,7 +2039,7 @@
const fsel = document.createElement("select"); fsel.className = "asel";
[["", "— stay"], ["again", "play again"], ["next", "next clip"], ["any", "any clip"], ["other", "any other"], ["stop", "stop"]]
.forEach(([v, t]) => { const o = document.createElement("option"); o.value = v; o.textContent = t; if (v === fa.act) o.selected = true; fsel.appendChild(o); });
- fsel.onchange = () => { setFA({ act: fsel.value }); openArranger(); };
+ fsel.onchange = () => { ungodlyMark("follow"); setFA({ act: fsel.value }); openArranger(); };
fr.appendChild(flbl); fr.appendChild(fsel);
if (fa.act) {
const cwrap = document.createElement("span"); cwrap.className = "gctl";
@@ -2047,7 +2047,7 @@
const csl = document.createElement("input"); csl.type = "range"; csl.min = "0"; csl.max = "100";
csl.value = String(Math.round((fa.chance != null ? fa.chance : 1) * 100));
const cval = document.createElement("span"); cval.className = "gval"; cval.textContent = csl.value + "%";
- csl.oninput = () => { setFA({ chance: clamp(+csl.value / 100, 0, 1) }); cval.textContent = csl.value + "%"; };
+ csl.oninput = () => { ungodlyGesture("follow"); setFA({ chance: clamp(+csl.value / 100, 0, 1) }); cval.textContent = csl.value + "%"; };
cwrap.appendChild(clab); cwrap.appendChild(csl); cwrap.appendChild(cval);
fr.appendChild(cwrap);
}
@@ -2066,7 +2066,7 @@
const l = document.createElement("span"); l.className = "glbl"; l.style.minWidth = "auto"; l.textContent = label;
const n = document.createElement("input"); n.type = "number"; n.min = String(min); n.max = String(max);
n.value = String(get()); n.className = "eunum";
- n.oninput = () => { set(clamp(Math.round(+n.value || 0), min, max));
+ n.oninput = () => { ungodlyGesture("euclid"); set(clamp(Math.round(+n.value || 0), min, max));
euclidFill(steps, euclidP.pulses, euclidP.len, euclidP.rot, nSel.isNote); paintG(); };
w.appendChild(l); w.appendChild(n);
return w;
@@ -2075,7 +2075,7 @@
euRow.appendChild(mkEuNum("steps", () => euclidP.len, v => euclidP.len = v, 2, 16));
euRow.appendChild(mkEuNum("rotate", () => euclidP.rot, v => euclidP.rot = v, 0, 15));
const euBtn = document.createElement("button"); euBtn.className = "midibtn"; euBtn.textContent = "fill";
- euBtn.onclick = () => { euclidFill(steps, euclidP.pulses, euclidP.len, euclidP.rot, nSel.isNote); paintG(); };
+ euBtn.onclick = () => { ungodlyMark("euclid"); euclidFill(steps, euclidP.pulses, euclidP.len, euclidP.rot, nSel.isNote); paintG(); };
euRow.appendChild(euBtn);
const euHint = document.createElement("span"); euHint.className = "albl";
euHint.textContent = "spread the pulses evenly — E(3,8) tresillo · E(5,8) cinquillo · E(4,16) four-on-floor";
@@ -2094,10 +2094,10 @@
if (stp % 4 === 0) cell.classList.add("bar");
if (nSel.isNote) {
cell.title = midiToName(seqRootFor(arrSelKey) + SEQ_ROWS[rw]) + " · step " + (stp + 1);
- cell.onclick = () => { stepToggle(steps, stp, rw); paintG(); };
+ cell.onclick = () => { ungodlyGesture("groove edit"); stepToggle(steps, stp, rw); paintG(); };
} else {
cell.title = "step " + (stp + 1);
- cell.onclick = () => { steps[stp] = steps[stp] ? 0 : 1; paintG(); };
+ cell.onclick = () => { ungodlyGesture("groove edit"); steps[stp] = steps[stp] ? 0 : 1; paintG(); };
}
cells.push({ el: cell, rw, stp });
grid.appendChild(cell);
@@ -2151,7 +2151,7 @@
cell.appendChild(fill);
const setY = (cy) => { const r = cell.getBoundingClientRect();
const frac = clamp(1 - (cy - r.top) / r.height, 0, 1);
- arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); };
+ ungodlyGesture("groove expr"); arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); };
cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
const up = () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); };
@@ -2186,7 +2186,7 @@
cell.appendChild(fill);
const setY = (cy) => { const r = cell.getBoundingClientRect();
const val = 1 - (cy - r.top) / r.height;
- sqx.lock[stp] = val < 0 ? -1 : clamp(val, 0, 1); // dragged below the lane = unlock
+ ungodlyGesture("groove expr"); sqx.lock[stp] = val < 0 ? -1 : clamp(val, 0, 1); // dragged below the lane = unlock
paintLock(); };
cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
@@ -2234,7 +2234,7 @@
if (arr.playing && b === arr.bar) cell.classList.add("acur");
};
cell.onclick = () => {
- lane[b] = CYCLE[(CYCLE.indexOf(lane[b]) + 1) % CYCLE.length];
+ ungodlyGesture("clip"); lane[b] = CYCLE[(CYCLE.indexOf(lane[b]) + 1) % CYCLE.length];
if (arr.playing && b === arr.bar) applyArrColumn();
paint();
};
@@ -2553,6 +2553,7 @@
function applyMood(name) {
const m = MOODS[name];
if (!m) return;
+ ungodlyMark("mood: " + name); // ⏪ before a mood rewires the mix
try { localStorage.setItem("gs_mood", name); } catch (e) {} // your vibe survives a reload
for (const k of MOOD_KEYS) { const t2 = tw(k); t2.mute = 0; t2.gain = 1; t2.offset = 0; }
if (m.storm) {
@@ -2670,7 +2671,7 @@
for (const pn of Object.keys(GROOVE_PRESETS)) {
const pb = document.createElement("button"); pb.className = "midibtn gpreset"; pb.textContent = pn;
pb.classList.toggle("on", groove.preset === pn);
- pb.onclick = () => { Object.assign(groove, GROOVE_PRESETS[pn]); groove.preset = pn; openBeat(); };
+ pb.onclick = () => { ungodlyMark("groove preset"); Object.assign(groove, GROOVE_PRESETS[pn]); groove.preset = pn; openBeat(); };
gr.appendChild(pb); presetBtns.push({ pb, pn });
}
const paintPresets = () => presetBtns.forEach(({ pb, pn }) => pb.classList.toggle("on", groove.preset === pn));
@@ -2681,7 +2682,7 @@
const vv = document.createElement("span"); vv.className = "gval";
const show = () => vv.textContent = Math.round(get() / max * 100) + "%";
sl.value = String(Math.round(get() / max * 1000)); show();
- sl.oninput = () => { set(clamp(+sl.value / 1000 * max, 0, max)); show(); paintPresets(); }; // a drag drops the preset to "custom" — relight the buttons to match
+ sl.oninput = () => { ungodlyGesture("groove"); set(clamp(+sl.value / 1000 * max, 0, max)); show(); paintPresets(); }; // a drag drops the preset to "custom" — relight the buttons to match
w.appendChild(l); w.appendChild(sl); w.appendChild(vv); return w;
};
gr.appendChild(mkGS("swing", () => groove.swing, v => { groove.swing = v; groove.preset = "custom"; }, 1));
@@ -2699,6 +2700,7 @@
if (!parts.length || !Number.isFinite(parts[0])) return; // junk / empty confirm → no-op; an explicit "0" still clears
const pulses = clamp(Math.round(parts[0]), 0, 16),
rot = clamp(Math.round(Number.isFinite(parts[1]) ? parts[1] : 0), 0, 15);
+ ungodlyMark("euclid");
euclidFill(sq.steps, pulses, 16, rot, false);
};
const lanes = [];
@@ -2712,7 +2714,7 @@
mute._on = on; mute.classList.toggle("on", on); mute.textContent = on ? "●" : "○"; };
mute._on = null; paintMute();
mute.title = "mute / unmute this lane";
- mute.onclick = () => { sq.on = !sq.on; paintMute(); paintPlay(); };
+ mute.onclick = () => { ungodlyMark("beat mute"); sq.on = !sq.on; paintMute(); paintPlay(); };
const eu = document.createElement("button"); eu.className = "midibtn bmini"; eu.textContent = "⬢";
eu.title = "euclid — spread N pulses evenly across the 16 steps";
eu.onclick = () => { euclidPrompt(sq, v); paintGrid(); };
@@ -2724,7 +2726,7 @@
const cell = document.createElement("div"); cell.className = "seqcell";
if (stp % 4 === 0) cell.classList.add("bar"); // beats 1 / 5 / 9 / 13
cell.title = v + " · step " + (stp + 1);
- cell.onclick = () => { sq.steps[stp] = sq.steps[stp] ? 0 : 1; paintGrid(); };
+ cell.onclick = () => { ungodlyGesture("beat edit"); sq.steps[stp] = sq.steps[stp] ? 0 : 1; paintGrid(); };
cells.push(cell); steps.appendChild(cell);
}
lane.appendChild(steps); body.appendChild(lane);
@@ -2749,6 +2751,7 @@
play.onclick = () => { // master ▶ — flip all five lanes at once
const turnOn = !anyOn();
+ ungodlyMark("beat play");
lanes.forEach(L => { L.sq.on = turnOn; L.paintMute(); });
paintGrid();
};
@@ -2791,7 +2794,7 @@
const fill = document.createElement("div"); fill.className = "exprbar"; cell.appendChild(fill);
const setY = (cy) => { const r = cell.getBoundingClientRect();
const frac = clamp(1 - (cy - r.top) / r.height, 0, 1);
- arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); };
+ ungodlyGesture("beat expr"); arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); };
cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY);
const mv = (e2) => setY(e2.clientY);
const up = () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); };
@@ -2817,7 +2820,7 @@
const l = document.createElement("span"); l.className = "glbl"; l.style.minWidth = "44px"; l.textContent = p.label;
const sl = document.createElement("input"); sl.type = "range"; sl.min = "0"; sl.max = "1000";
sl.value = String(Math.round(clamp(Synth.drumParam(v, p.key), 0, 1) * 1000));
- sl.oninput = () => Synth.drumParam(v, p.key, clamp(+sl.value / 1000, 0, 1));
+ sl.oninput = () => { ungodlyGesture("kit"); Synth.drumParam(v, p.key, clamp(+sl.value / 1000, 0, 1)); };
w.appendChild(l); w.appendChild(sl); box.appendChild(w);
}
const au = document.createElement("button"); au.className = "midibtn bmini"; au.textContent = "▷ hear";
@@ -3186,7 +3189,7 @@
}
// ---- local edits (mutate your patch instead of the read-only hub) ----------
- function toggleMuteLocal(key) { const t = tw(key); t.mute = t.mute >= 0.5 ? 0 : 1; }
+ function toggleMuteLocal(key) { ungodlyMark("mute"); const t = tw(key); t.mute = t.mute >= 0.5 ? 0 : 1; }
function setParamLocal(target, param, value) { tw(target)[param] = value; }
function addRouteLocal(source, dest, amount) {
if (!source || !dest) return;
@@ -3312,6 +3315,43 @@
layoutDirty = true;
}
+ // ---- ⏪ UNGODLY — undo as time-travel over the patch (un-doing what god did) ----
+ // loadPatch() already restores everything (routes, tweaks, seqs incl. drums, arr, bpm,
+ // groove, scale, kit) AND rebuilds open panels — so undo is just a ring of
+ // serializePatch() snapshots. Memory-only in v1: a reload starts history fresh.
+ const ungodly = { past: [], future: [], muted: false, MAX: 32, lastT: 0, lastLabel: "" };
+ function ungodlyMark(label) { // call BEFORE a mutation applies — the snapshot is the world as it WAS
+ if (ungodly.muted) return;
+ const s = JSON.stringify(serializePatch());
+ if (ungodly.past.length && ungodly.past[ungodly.past.length - 1].s === s) return; // no empty entries
+ ungodly.past.push({ s, label });
+ if (ungodly.past.length > ungodly.MAX) ungodly.past.shift(); // ring of 32
+ ungodly.future.length = 0; // a fresh act forks the timeline — no redo across it
+ }
+ function ungodlyGesture(label) { // continuous drags → ONE mark per burst (same label within 800ms coalesces)
+ const tNow = performance.now();
+ if (label === ungodly.lastLabel && tNow - ungodly.lastT < 800) { ungodly.lastT = tNow; return; }
+ ungodly.lastLabel = label; ungodly.lastT = tNow; ungodlyMark(label);
+ }
+ function ungodlyApply(s) { // restore a snapshot without recording it as a new act
+ ungodly.muted = true; loadPatch(JSON.parse(s)); ungodly.muted = false;
+ ungodly.lastLabel = ""; ungodly.lastT = 0; // a drag after undo mustn't coalesce with one before it
+ }
+ function ungodlyUndo() {
+ if (!ungodly.past.length) { eventTicker.unshift({ text: "⏪ nothing to take back — god has done nothing yet", tAdded: now(), src: "sys" }); return; }
+ const p = ungodly.past.pop();
+ ungodly.future.push({ s: JSON.stringify(serializePatch()), label: p.label }); // current state → redo stack
+ ungodlyApply(p.s);
+ eventTicker.unshift({ text: "⏪ ungodly: " + p.label, tAdded: now(), src: "sys" });
+ }
+ function ungodlyRedo() {
+ if (!ungodly.future.length) { eventTicker.unshift({ text: "⏩ nothing to redo", tAdded: now(), src: "sys" }); return; }
+ const f = ungodly.future.pop();
+ ungodly.past.push({ s: JSON.stringify(serializePatch()), label: f.label });
+ ungodlyApply(f.s);
+ eventTicker.unshift({ text: "⏩ ungodly: " + f.label, tAdded: now(), src: "sys" });
+ }
+
// ---- zero mode — dimension zero -------------------------------------------
// The calm front door: not a muted world (moods do that) but an EMPTY one — no
// cables, nothing sounding — that you build by hand. It is the SAME matrix
@@ -3319,6 +3359,7 @@
// the world you had. Client-local, like moods — the hub/other users untouched.
let zeroMode = false;
function enterZero(restore) { // restore=true only on a reload back into an existing zero build
+ if (!restore) ungodlyMark("enter zero"); // ⏪ (a reload-restore is not a user act — don't record it)
if (!zeroMode && !restore) { // fresh enter: snapshot what you had, so exiting can restore it
try { localStorage.setItem("gs_prezero", JSON.stringify(serializePatch())); } catch (e) {}
}
@@ -3357,6 +3398,7 @@
return false;
}
function finishZeroExit(mode, name) { // mode: keep | old | weave (C1)
+ ungodlyMark("exit zero"); // ⏪ the zero build, before we leave it (the inner applyMood marks the mood step)
const build = serializePatch(); // the zero-built patch (routes + tweaks + seqs), captured before we touch anything
zeroMode = false; zeroUI.close();
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); localStorage.removeItem("gs_zerovibes"); } catch (e) {}
@@ -3912,6 +3954,7 @@
const activeVibes = new Set(); // names currently applied — drives the panel toggles
function applyVibe(vibe) {
const v = sanitizeVibe(vibe); if (!v) return 0;
+ ungodlyMark("loaded " + (v.name || "vibe")); // ⏪ before a vibe lays its cables on
let n = 0;
for (const r of v.routes) {
const id = r.source + "|" + r.dest;
@@ -4187,10 +4230,10 @@
if (!isNote) {
let dragging = false, sy = 0, so = 0;
wrap.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; dragging = true; sy = e.clientY; so = ((tweaks[key] && tweaks[key].offset) || 0); try { wrap.setPointerCapture(e.pointerId); } catch (x) {} e.preventDefault(); });
- wrap.addEventListener("pointermove", (e) => { if (!dragging) return; setParamLocal(key, "offset", clamp(so + (sy - e.clientY) / 150, -1, 1)); });
+ wrap.addEventListener("pointermove", (e) => { if (!dragging) return; ungodlyGesture("trim"); setParamLocal(key, "offset", clamp(so + (sy - e.clientY) / 150, -1, 1)); });
const end = (e) => { dragging = false; try { wrap.releasePointerCapture(e.pointerId); } catch (x) {} if (zeroMode) zeroUI.persist(); }; // a knob shape is part of the zero build
wrap.addEventListener("pointerup", end); wrap.addEventListener("pointercancel", end);
- wrap.ondblclick = () => { setParamLocal(key, "offset", 0); if (zeroMode) zeroUI.persist(); };
+ wrap.ondblclick = () => { ungodlyMark("trim"); setParamLocal(key, "offset", 0); if (zeroMode) zeroUI.persist(); };
}
wrap.oncontextmenu = (e) => { e.preventDefault(); const n = dests.get(key); if (n) openCtxMenu(n, e.clientX, e.clientY); };
this.knobs.push({ key, val, dot, isNote });
@@ -4496,7 +4539,8 @@
} else if (saved === "0 zero") {
enterZero(true); // a returning zero-builder comes back to zero, build intact (C2)
} else {
- applyMood(saved);
+ const _um0 = ungodly.muted; ungodly.muted = true; // ⏪ the reload-restore mood is not a user act
+ applyMood(saved); ungodly.muted = _um0;
}
try { // #vibe=GSV1… — a shared vibe in the URL; offer, never auto-apply (Z3)
const m = /[#&]vibe=([^&]+)/.exec(location.hash || "");
@@ -6926,7 +6970,7 @@
} else if (gainDrag) { // ⇧-drag: up louder, down quieter
const dy = gainDrag.y0 - my;
if (Math.abs(dy) > 3) gainDrag.moved = true;
- tw(gainDrag.node.key).gain = clamp(gainDrag.gain0 + dy * 0.01, 0, 2);
+ ungodlyGesture("level"); tw(gainDrag.node.key).gain = clamp(gainDrag.gain0 + dy * 0.01, 0, 2);
canvas.style.cursor = "ns-resize";
} else if (godDrag) {
const dx = mx - godDrag.x0;
@@ -6967,13 +7011,13 @@
const d = drag; drag = null; hoverDest = null;
const isSrc = d.node.kind === "source";
if (d.moved) { // drag source -> dest = new cable
- if (isSrc) { const dest = destAt(mx, my); if (dest) addRouteLocal(d.node.key, dest.key, 0.5); }
+ if (isSrc) { const dest = destAt(mx, my); if (dest) { ungodlyMark("patch cable"); addRouteLocal(d.node.key, dest.key, 0.5); } }
} else if (ev.shiftKey) { // shift-click -> the node's tab
openInspector(d.node);
} else if (isSrc && (ev.metaKey || ev.ctrlKey)) { // cmd/ctrl-click -> multi-select (group)
toggleSelect(d.node.key);
} else if (ev.altKey && isSrc) { // ⌥-click -> solo this source
- soloKey = soloKey === d.node.key ? null : d.node.key;
+ ungodlyMark("solo"); soloKey = soloKey === d.node.key ? null : d.node.key;
} else if (isSrc && armLearn({ target: d.node.key, param: "mute", mode: "toggle" })) {
/* armed for MIDI learn */
} else { // plain click -> toggle on/off
@@ -6987,6 +7031,7 @@
}
function resetTweaks(key) { // back to neutral: level 1×, no trim, unmuted
+ ungodlyMark("reset");
delete tweaks[key];
if (soloKey === key) soloKey = null;
}
@@ -7009,26 +7054,29 @@
return;
}
}
- if (ev.key === "p" || ev.key === "P") {
+ if ((ev.metaKey || ev.ctrlKey) && (ev.key === "z" || ev.key === "Z")) { // ⏪ UNGODLY — ⌘Z undo · ⌘⇧Z redo
+ ev.preventDefault(); ev.shiftKey ? ungodlyRedo() : ungodlyUndo(); return;
+ } // (⌘/⌃ + a letter no longer trips a plain-key shortcut — see guards below)
+ if ((ev.key === "p" || ev.key === "P") && !ev.metaKey && !ev.ctrlKey) {
performMode = !performMode; // pure visuals: the CSS hides every
document.body.classList.toggle("perform", performMode); // DOM chrome, the frame loop
} // skips header/godtime/footer
- if ((ev.key === "c" || ev.key === "C") && !zeroMode) {
+ if ((ev.key === "c" || ev.key === "C") && !zeroMode && !ev.metaKey && !ev.ctrlKey) {
chakraMode = !chakraMode;
if (!chakraMode) { layoutDirty = true; exitMind(); } // sources glide home
else clearTrails();
}
- if (ev.key === "o" || ev.key === "O") OMNI.toggle(); // Z5: the OMNI synth panel
- if (ev.key === "w" || ev.key === "W") toggleWheel(); // the sky wheel layer
- if (ev.key === "z" || ev.key === "Z") setZen(!zenMode); // zen — nothing but the sky
+ if ((ev.key === "o" || ev.key === "O") && !ev.metaKey && !ev.ctrlKey) OMNI.toggle(); // Z5: the OMNI synth panel
+ if ((ev.key === "w" || ev.key === "W") && !ev.metaKey && !ev.ctrlKey) toggleWheel(); // the sky wheel layer
+ if ((ev.key === "z" || ev.key === "Z") && !ev.metaKey && !ev.ctrlKey) setZen(!zenMode); // zen — nothing but the sky
if (ev.key === "Escape" && zenMode) setZen(false);
- if (ev.key === "t" || ev.key === "T") {
+ if ((ev.key === "t" || ev.key === "T") && !ev.metaKey && !ev.ctrlKey) {
arr.open ? closeArranger() : openArranger(); // tracks, for the casuals
}
- if (ev.key === "b" || ev.key === "B") {
+ if ((ev.key === "b" || ev.key === "B") && !ev.metaKey && !ev.ctrlKey) {
beat.open ? closeBeat() : openBeat(); // 🥁 GODRUM — make a beat
}
- if (ev.key === "g" || ev.key === "G") toggleGodspeed(); // ⚡ GODSPEED — the arp panel
+ if ((ev.key === "g" || ev.key === "G") && !ev.metaKey && !ev.ctrlKey) toggleGodspeed(); // ⚡ GODSPEED — the arp panel
if (beat.open && !ev.repeat && !ev.metaKey && !ev.ctrlKey && !ev.altKey) { // finger-drum the kit (1–5)
const byCode = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5"].indexOf(ev.code); // ev.code keeps ⇧+digit mapped
const di = byCode >= 0 ? byCode : "12345".indexOf(ev.key); // …with an ev.key fallback for code-less envs
@@ -7038,7 +7086,7 @@
if (beat.rec) { // record: quantize the tap to the nearest 16th, write the step
const step = Math.round(seqPhase) % 16; // frac > .5 rounds to the upcoming 16th (raw grid; swing rides at playback)
const sq = drumSeqFor("drum." + name);
- sq.steps[step] = 1; sq.vel[step] = vel;
+ ungodlyGesture("beat rec"); sq.steps[step] = 1; sq.vel[step] = vel;
if (beatRefresh) beatRefresh();
}
return;
@@ -7149,10 +7197,10 @@
ctxItem(node.muted ? "unmute" : "mute", () => toggleMuteLocal(node.key), "click");
if (isSrc) ctxItem(soloKey === node.key ? "unsolo" : "solo",
- () => { soloKey = soloKey === node.key ? null : node.key; }, "⌥-click");
+ () => { ungodlyMark("solo"); soloKey = soloKey === node.key ? null : node.key; }, "⌥-click");
if (!node.isNote) { // level/trim don't apply to a pitch — no dead sliders
- ctxSlider("level", t0.gain, 0, 2, x => { t0.gain = x; }, x => x.toFixed(2) + "×");
- ctxSlider("trim", t0.offset, -1, 1, x => { t0.offset = x; });
+ ctxSlider("level", t0.gain, 0, 2, x => { ungodlyGesture("level"); t0.gain = x; }, x => x.toFixed(2) + "×");
+ ctxSlider("trim", t0.offset, -1, 1, x => { ungodlyGesture("trim"); t0.offset = x; });
}
ctxSep();
@@ -7172,7 +7220,7 @@
sl.value = String(rt.amount != null ? rt.amount : 0.5); sl.title = "amount";
sl.oninput = () => setRouteAmountLocal(rt.source, rt.dest, parseFloat(sl.value));
const x = document.createElement("span"); x.className = "x"; x.textContent = "×"; x.title = "unpatch";
- x.onclick = () => { removeRouteLocal(rt.source, rt.dest); row.remove(); };
+ x.onclick = () => { ungodlyMark("unpatch"); removeRouteLocal(rt.source, rt.dest); row.remove(); };
row.appendChild(lbl); row.appendChild(sl); row.appendChild(x);
cb.appendChild(row);
}
@@ -7194,6 +7242,7 @@
const it = document.createElement("div"); it.className = "it";
it.textContent = other.label || other.key;
it.onclick = () => {
+ ungodlyMark("patch to");
if (isSrc) addRouteLocal(node.key, other.key, 0.5);
else addRouteLocal(other.key, node.key, 0.5);
closeCtx();
@@ -7250,6 +7299,8 @@
() => { arr.open ? closeArranger() : openArranger(); }, "T");
ctxItem(beat.open ? "close beat" : "🥁 beat — make a beat",
() => { beat.open ? closeBeat() : openBeat(); }, "B");
+ ctxItem("⏪ UNGODLY — take it back" + (ungodly.past.length ? " (" + ungodly.past.length + ")" : ""),
+ () => ungodlyUndo(), "⌘Z");
const moodSub = ctxSub("🌗 mood — how much world");
const z0 = document.createElement("div"); z0.className = "it";
z0.textContent = zeroMode ? "0 zero — you're building it (a mood leaves)" : "0 zero — build from nothing";
@@ -7268,7 +7319,7 @@
: fxMode === "ultra" ? "lite" : "auto"));
ctxItem("hover zoom: " + (loupe.on ? "on" : "off"),
() => { loupe.on = !loupe.on; try { localStorage.setItem("gs_loupe", loupe.on ? "1" : "0"); } catch (e) {} });
- if (soloKey) ctxItem("clear solo (" + soloKey + ")", () => { soloKey = null; });
+ if (soloKey) ctxItem("clear solo (" + soloKey + ")", () => { ungodlyMark("solo"); soloKey = null; });
ctxSep();
if (!hrDevice) {
ctxItem("♥ connect your heart (bluetooth)", () => connectHeart());
@@ -7425,7 +7476,7 @@
sl.value = String(rt.amount != null ? rt.amount : 0.5); sl.title = "amount";
sl.oninput = () => setRouteAmountLocal(rt.source, rt.dest, parseFloat(sl.value));
const x = document.createElement("span"); x.className = "x"; x.textContent = "×"; x.title = "unpatch";
- x.onclick = () => { removeRouteLocal(rt.source, rt.dest); setTimeout(() => openInspector(node), 120); };
+ x.onclick = () => { ungodlyMark("unpatch"); removeRouteLocal(rt.source, rt.dest); setTimeout(() => openInspector(node), 120); };
row.appendChild(sl); row.appendChild(x);
inspectEl.appendChild(row);
}
@@ -8377,15 +8428,15 @@
const nm = document.createElement("span"); nm.className = "nm"; nm.textContent = g.label || gname;
const btns = document.createElement("span"); btns.className = "btns";
const mute = document.createElement("button"); mute.textContent = "mute";
- mute.onclick = () => { if (armLearn({ target: "@" + gname, param: "mute", mode: "toggle" })) return; const t = tw("@" + gname); t.mute = t.mute >= 0.5 ? 0 : 1; mute.classList.toggle("on"); };
+ mute.onclick = () => { if (armLearn({ target: "@" + gname, param: "mute", mode: "toggle" })) return; ungodlyMark("mute"); const t = tw("@" + gname); t.mute = t.mute >= 0.5 ? 0 : 1; mute.classList.toggle("on"); };
const frz = document.createElement("button"); frz.textContent = "freeze"; frz.className = "frz";
- frz.onclick = () => { if (armLearn({ target: "@" + gname, param: "freeze", mode: "toggle" })) return; const t = tw("@" + gname); t.freeze = t.freeze >= 0.5 ? 0 : 1; frz.classList.toggle("on"); };
+ frz.onclick = () => { if (armLearn({ target: "@" + gname, param: "freeze", mode: "toggle" })) return; ungodlyMark("freeze"); const t = tw("@" + gname); t.freeze = t.freeze >= 0.5 ? 0 : 1; frz.classList.toggle("on"); };
btns.appendChild(mute); btns.appendChild(frz);
row.appendChild(nm); row.appendChild(btns); wrap.appendChild(row);
const slider = document.createElement("input");
slider.type = "range"; slider.min = "0"; slider.max = "1.5"; slider.step = "0.01"; slider.value = "1"; slider.title = "gain";
slider.addEventListener("pointerdown", () => armLearn({ target: "@" + gname, param: "gain", mode: "cc" }));
- slider.oninput = () => { if (!learnMode) setParamLocal("@" + gname, "gain", parseFloat(slider.value)); };
+ slider.oninput = () => { if (!learnMode) { ungodlyGesture("level"); setParamLocal("@" + gname, "gain", parseFloat(slider.value)); } };
wrap.appendChild(slider);
panelEl.appendChild(wrap);
}
@@ -8442,7 +8493,7 @@
const n = templateSelect.value; if (!n) return;
if (n === "__zero__") { enterZero(); pMsg.textContent = "entered dimension zero — the empty field"; return; }
const r = await gsFetch("/api/presets/" + encodeURIComponent(n), "GET");
- if (r.ok && r.data.data) { loadPatch(r.data.data); pMsg.textContent = "loaded “" + n + "”"; }
+ if (r.ok && r.data.data) { ungodlyMark("loaded " + n); loadPatch(r.data.data); pMsg.textContent = "loaded “" + n + "”"; }
else pMsg.textContent = "load failed";
};
loadRow.appendChild(templateSelect); loadRow.appendChild(loadBtn); panelEl.appendChild(loadRow);