🔧 fix: pinch-zoom trap over menus + cable blips ride the data's own rate
Two live-play fixes: - Zoom: trackpad pinch (ctrl+wheel) over a DOM overlay (right-click menu, panels) used to hit the BROWSER's page zoom, which the canvas then made un-undoable because it preventDefaults every wheel — zoomed-in with no way out except re-opening a menu. Now a capture-phase window listener catches pinch everywhere and drives the app's own view zoom (shared zoomAt()); Safari's gesture* events are blocked too. Plain scrolling in menus and panels is untouched; canvas behavior unchanged (verified single-fire). - Cables: dash/blip speed was uniform (activity-only). Each source now carries flowDisp — an EMA of |Δnorm|/s with fast attack + slow release — and every cable's dashPhase advances at 6 + flow·240 + activity·40, with the pulse dot riding the same phase. A ticking feed streams, a glacial feed crawls (venus.dist ~0 flow vs mercury.phase saturated, measured); event sources surge on fire via firePulse. Verified: pinch in/out over overlay drives view.z and is defaultPrevented, plain scroll not hijacked, canvas single-fire, flows measured per-source via temp hook (removed, grep-verified 0), zero console errors, T O B G S + ⌘Z regression green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9fbba05426
commit
7c6d44b010
@ -5458,6 +5458,14 @@
|
||||
for (const n of sources.values()) {
|
||||
n.normDisp = approach(n.normDisp, n.norm, 0.16, dt);
|
||||
n.rawDisp = approach(n.rawDisp, n.raw, 0.16, dt);
|
||||
// the data's own pulse: |Δnorm|/s, fast attack + slow release, so a ticking
|
||||
// feed holds a flow level ∝ how often (and how hard) it actually moves —
|
||||
// this is what the cables' blip speed rides (a glacial feed crawls, BTC streams)
|
||||
const pv = n._flowPrev == null ? n.norm : n._flowPrev;
|
||||
const fv = clamp(Math.sqrt(Math.abs(n.norm - pv) / Math.max(dt, 1e-3) * 8), 0, 1);
|
||||
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 (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);
|
||||
}
|
||||
@ -5468,7 +5476,9 @@
|
||||
}
|
||||
for (const r of routesArr) {
|
||||
r.activeDisp = approach(r.activeDisp, r.active, 0.14, dt);
|
||||
r.dashPhase -= dt * (18 + r.activeDisp * 90);
|
||||
const sn = sources.get(r.source); // the cable flows at the DATA's rate:
|
||||
const flow = sn && sn.flowDisp ? sn.flowDisp : 0; // lively source = streaming blips,
|
||||
r.dashPhase -= dt * (6 + flow * 240 + r.activeDisp * 40); // glacial source = a slow crawl
|
||||
}
|
||||
|
||||
if (chakraMode) updateChakra(t, dt); // positions sources on the subtle body
|
||||
@ -5560,8 +5570,8 @@
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// travelling pulse dot
|
||||
const pt = ((t * (0.15 + a * 0.6) + r.dashPhase * 0.002) % 1 + 1) % 1;
|
||||
// travelling pulse dot — rides dashPhase, so its speed IS the data's rate
|
||||
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);
|
||||
@ -7762,13 +7772,27 @@
|
||||
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;
|
||||
function zoomAt(clientX, clientY, deltaY) { // zoom toward the cursor (shared by canvas + overlay pinch)
|
||||
const cx = (clientX - view.x) / view.z, cy = (clientY - view.y) / view.z; // content point under cursor
|
||||
view.z = clamp(view.z * Math.exp(-deltaY * 0.0016), 1, 6);
|
||||
view.x = clientX - cx * view.z; view.y = clientY - cy * view.z;
|
||||
applyView();
|
||||
}
|
||||
canvas.addEventListener("wheel", (ev) => { // scroll or trackpad-pinch → zoom
|
||||
ev.preventDefault();
|
||||
zoomAt(ev.clientX, ev.clientY, ev.deltaY);
|
||||
}, { passive: false });
|
||||
// Trackpad pinch arrives as ctrl+wheel. Over a DOM overlay (the right-click menu,
|
||||
// a panel) it used to hit the BROWSER's page zoom — which the canvas then made
|
||||
// un-undoable, because it preventDefaults every wheel. Catch pinch everywhere:
|
||||
// it always drives OUR zoom, never the page's. Plain scrolling in panels is untouched.
|
||||
window.addEventListener("wheel", (ev) => {
|
||||
if (!ev.ctrlKey || ev.target === canvas) return; // not a pinch / canvas already handles it
|
||||
ev.preventDefault();
|
||||
zoomAt(ev.clientX, ev.clientY, ev.deltaY);
|
||||
}, { passive: false, capture: true });
|
||||
for (const ge of ["gesturestart", "gesturechange", "gestureend"]) // Safari's proprietary pinch path
|
||||
window.addEventListener(ge, (ev) => ev.preventDefault(), { 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user