From 7c6d44b0106dde2eb399f6a24a0dcd4ab1dbe782 Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Sat, 25 Jul 2026 10:32:28 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix:=20pinch-zoom=20trap=20over?= =?UTF-8?q?=20menus=20+=20cable=20blips=20ride=20the=20data's=20own=20rate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- viz/index.html | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/viz/index.html b/viz/index.html index fab35d3..298857d 100644 --- a/viz/index.html +++ b/viz/index.html @@ -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