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