🔍 pinch/scroll zoom — get right into the orbits
The whole visual now zooms: scroll or trackpad-pinch to zoom the canvas toward the cursor (1×–6×), drag empty space to pan when zoomed, double-click to snap home. Implemented as a CSS transform on #stage (smooth, no draw-loop surgery); all four hit-test chokepoints (mousedown/move/up/contextmenu) invert the zoom so clicking/dragging/right-clicking nodes still lands exactly — provably correct (the inverse of the transform). Pan clamps so the canvas always covers the view. Grimoire + manual note the gesture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d1fa247ad8
commit
2db0d5945f
@ -518,6 +518,8 @@ These four — element, waxing, declination, fertility — gather in the **spiri
|
||||
|
||||
The console is not a picture of the instrument. It *is* the instrument. Every sphere, every cable, every dial answers to the hand. Nothing here writes to `config.json` — the edits ride the same websocket the data rides (*add_route · remove_route · set_route · make_group*), so the modulation matrix rewires beneath your fingers in real time, live, on stage, mid-note.
|
||||
|
||||
**Zoom in.** Scroll (or pinch on a trackpad) to zoom the whole sky toward your cursor — get right up into the orbits, the cables, the chakra wheels. Once you're in, **drag empty space to pan** around, and **double-click** to snap back out to the full view.
|
||||
|
||||
Learn these seven gestures and the whole planet is under your hands:
|
||||
|
||||
- **Click any node — toggle it on or off.** A source sphere (*the sun, an earthquake, Delhi's air, bitcoin's volatility*) or a voice (*lead, bass, pad, drone, perc*): one click mutes it, and the node takes a red slash. Silence the sun and the master filter stops breathing with the solar wind; silence *perc* and BTC's chaos stops driving the hats. Click again — it returns.
|
||||
@ -1001,3 +1003,5 @@ Every feed the world sends is one of eight temperaments; the shape dictates how
|
||||
- **🌊 Juno-106 chorus** — an always-on subtle BBD-style stereo chorus on the synth's master bus (two short delays warbled by slow LFOs, panned wide) that warms and widens the dry oscillators.
|
||||
- **⬗ Parameter locks (p-locks)** — Pocket-Operator / Elektron style: a texture voice (filter, glitch, reverb, drive) gets a per-step `lock` lane that pins its value to a fixed level on that step (blocky, disjointed), overriding the world; drag a step below the lane to unlock it and hand that step back to the data. Applied in `computeDests` via `applyGroove`; travels with the patch.
|
||||
- **🌊 Juno-106 chorus** — an always-on subtle BBD-style stereo chorus on the synth's master bus (two short delays warbled by slow LFOs, panned wide) that warms and widens the dry oscillators.
|
||||
- **⬗ Parameter locks (p-locks)** — Pocket-Operator / Elektron style: a texture voice (filter, glitch, reverb, drive) gets a per-step `lock` lane that pins its value to a fixed level on that step (blocky, disjointed), overriding the world; drag a step below the lane to unlock it and hand that step back to the data. Applied in `computeDests` via `applyGroove`; travels with the patch.
|
||||
- **🌊 Juno-106 chorus** — an always-on subtle BBD-style stereo chorus on the synth's master bus (two short delays warbled by slow LFOs, panned wide) that warms and widens the dry oscillators.
|
||||
|
||||
@ -971,6 +971,8 @@
|
||||
|
||||
<p class="lead">The console is not a picture of the instrument. It <i>is</i> the instrument. Every sphere, every cable, every dial answers to the hand. Nothing here writes to <kbd>config.json</kbd> — the edits ride the same websocket the data rides (<i>add_route · remove_route · set_route · make_group</i>), so the modulation matrix rewires beneath your fingers in real time, live, on stage, mid-note.</p>
|
||||
|
||||
<p><b>Zoom in.</b> Scroll (or pinch on a trackpad) to zoom the whole sky toward your cursor — get right up into the orbits, the cables, the chakra wheels. Once you're in, <b>drag empty space to pan</b> around, and <b>double-click</b> to snap back out to the full view.</p>
|
||||
|
||||
<p>Learn these seven gestures and the whole planet is under your hands:</p>
|
||||
|
||||
<ul>
|
||||
@ -1062,6 +1064,21 @@
|
||||
const ctx = canvas.getContext("2d");
|
||||
let W = 0, H = 0, DPR = 1;
|
||||
|
||||
// ---- pinch / scroll zoom of the whole visual (CSS transform on the canvas) ----
|
||||
const view = { z: 1, x: 0, y: 0 }; // zoom + pan (screen px); #stage is fixed at 0,0
|
||||
let panDrag = null;
|
||||
canvas.style.transformOrigin = "0 0";
|
||||
function clampView() {
|
||||
view.x = clamp(view.x, window.innerWidth * (1 - view.z), 0); // keep the canvas covering the viewport
|
||||
view.y = clamp(view.y, window.innerHeight * (1 - view.z), 0);
|
||||
}
|
||||
function applyView() {
|
||||
if (view.z <= 1.002) { view.z = 1; view.x = 0; view.y = 0; } // snap home when fully out
|
||||
clampView();
|
||||
canvas.style.transform = (view.z === 1 && !view.x && !view.y) ? "none"
|
||||
: "translate(" + view.x.toFixed(1) + "px," + view.y.toFixed(1) + "px) scale(" + view.z.toFixed(4) + ")";
|
||||
}
|
||||
|
||||
// ---- graphics level ----------------------------------------------------
|
||||
// "auto" starts full and drops to lite if the frame rate drowns (ultrawides,
|
||||
// older boxes). lite = no shadow blur, flat node cores, DPR capped at 1 —
|
||||
@ -5132,10 +5149,18 @@
|
||||
return hit;
|
||||
}
|
||||
|
||||
canvas.addEventListener("wheel", (ev) => { // scroll or trackpad-pinch → zoom toward the cursor
|
||||
ev.preventDefault();
|
||||
const cx = (ev.clientX - view.x) / view.z, cy = (ev.clientY - view.y) / view.z; // content point under cursor
|
||||
view.z = clamp(view.z * Math.exp(-ev.deltaY * 0.0016), 1, 6);
|
||||
view.x = ev.clientX - cx * view.z; view.y = ev.clientY - cy * view.z;
|
||||
applyView();
|
||||
}, { passive: false });
|
||||
canvas.addEventListener("dblclick", () => { if (view.z !== 1) { view.z = 1; view.x = 0; view.y = 0; applyView(); } });
|
||||
canvas.addEventListener("mousedown", (ev) => {
|
||||
if (ev.button !== 0) return; // right button belongs to the menu
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const mx = ev.clientX - r.left, my = ev.clientY - r.top;
|
||||
const mx = (ev.clientX - r.left) / view.z, my = (ev.clientY - r.top) / view.z; // screen → zoomed content coords
|
||||
if (inGodBand(mx, my)) { godDrag = { x0: mx, bpm0: godtime.bpm, moved: false }; return; }
|
||||
const lp = loupeNodeAt(mx, my); // a magnified row in the hover zoom
|
||||
if (lp) loupe.frozen = true; // hold the panel still through the press
|
||||
@ -5160,12 +5185,18 @@
|
||||
}
|
||||
chakraSpin.vel = 0; // empty space = grab / look around
|
||||
chakraDrag = { x0: mx, y0: my, rot0: chakraSpin.rot, tilt0: chakraSpin.tilt, lastX: mx };
|
||||
} else if (view.z > 1) { // zoomed in, empty space → pan the view
|
||||
panDrag = { sx: ev.clientX, sy: ev.clientY, x0: view.x, y0: view.y };
|
||||
}
|
||||
});
|
||||
window.addEventListener("mousemove", (ev) => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const mx = ev.clientX - r.left, my = ev.clientY - r.top;
|
||||
if (chakraDrag) { // spin / tilt the subtle body
|
||||
const mx = (ev.clientX - r.left) / view.z, my = (ev.clientY - r.top) / view.z;
|
||||
if (panDrag) { // drag the zoomed view around
|
||||
view.x = panDrag.x0 + (ev.clientX - panDrag.sx);
|
||||
view.y = panDrag.y0 + (ev.clientY - panDrag.sy);
|
||||
applyView(); canvas.style.cursor = "grabbing";
|
||||
} else if (chakraDrag) { // spin / tilt the subtle body
|
||||
chakraSpin.rot = chakraDrag.rot0 + (mx - chakraDrag.x0) * 0.008;
|
||||
chakraSpin.tilt = clamp(chakraDrag.tilt0 + (my - chakraDrag.y0) * 0.004, 0.06, 0.95);
|
||||
chakraSpin.vel = (mx - chakraDrag.lastX) * 0.48; // flick momentum, rad/s-ish
|
||||
@ -5194,6 +5225,7 @@
|
||||
});
|
||||
window.addEventListener("mouseup", (ev) => {
|
||||
loupe.frozen = false; // the hover zoom follows the cursor again
|
||||
if (panDrag) { panDrag = null; canvas.style.cursor = "default"; return; }
|
||||
if (chakraDrag) { chakraDrag = null; return; } // let go — momentum carries it
|
||||
if (gainDrag) { // release the level ride
|
||||
const gd = gainDrag; gainDrag = null;
|
||||
@ -5210,7 +5242,7 @@
|
||||
}
|
||||
if (!drag) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const mx = ev.clientX - r.left, my = ev.clientY - r.top;
|
||||
const mx = (ev.clientX - r.left) / view.z, my = (ev.clientY - r.top) / view.z;
|
||||
const d = drag; drag = null; hoverDest = null;
|
||||
const isSrc = d.node.kind === "source";
|
||||
if (d.moved) { // drag source -> dest = new cable
|
||||
@ -5520,7 +5552,7 @@
|
||||
canvas.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const mx = ev.clientX - r.left, my = ev.clientY - r.top;
|
||||
const mx = (ev.clientX - r.left) / view.z, my = (ev.clientY - r.top) / view.z; // zoomed content coords
|
||||
openCtxMenu(nodeAt(mx, my) || destAt(mx, my), ev.clientX, ev.clientY);
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user