Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107// Pointer interactions on the canvas: pan-drag, mouse-anchored wheel zoom,// double-click recenter, hover tracking, click-to-select-post.
const MIN_ZOOM = 0.2;const MAX_ZOOM = 4;const ZOOM_FACTOR = 1.1;
let dragging = false;let dragStart = null;let dragDistance = 0; // tracked between mousedown and the following clickconst DRAG_PX_THRESHOLD = 6;
canvas.addEventListener("mousedown", (e) => { dragging = true; dragDistance = 0; dragStart = { x: e.clientX, y: e.clientY, cx: camera.x, cy: camera.y };});window.addEventListener("mouseup", () => { dragging = false; dragStart = null; });
canvas.addEventListener("mousemove", (e) => { if (dragging && dragStart) { const dx = e.clientX - dragStart.x; const dy = e.clientY - dragStart.y; dragDistance = Math.hypot(dx, dy); camera.x = dragStart.cx - dx; camera.y = dragStart.cy - dy; draw(); return; } const rect = canvas.getBoundingClientRect(); const sx = e.clientX - rect.left - rect.width / 2; const sy = e.clientY - rect.top - rect.height / 2; const t = screenToTile(sx, sy); if (!hoverTile || hoverTile.gx !== t.gx || hoverTile.gy !== t.gy) { hoverTile = t; showHoverInfo(t); // Hover repaint goes ONLY to the overlay canvas — the main tile layer // is untouched. This is what makes mousemove cheap regardless of how // many tiles are in the snapshot. drawHoverOverlay(); }});
canvas.addEventListener("mouseleave", () => { if (hoverTile) { hoverTile = null; drawHoverOverlay(); }});
canvas.addEventListener("wheel", (e) => { e.preventDefault(); const factor = e.deltaY < 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR; const newZoom = clamp(camera.zoom * factor, MIN_ZOOM, MAX_ZOOM); if (newZoom !== camera.zoom) { // Anchor the zoom to the mouse position: solve for new camera offset // so the world point under the cursor stays in the same screen pixel. // mx,my are the cursor's offset from canvas center (the origin our // isoProject works against). const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left - rect.width / 2; const my = e.clientY - rect.top - rect.height / 2; const ratio = newZoom / camera.zoom; camera.x = (mx + camera.x) * ratio - mx; camera.y = (my + camera.y) * ratio - my; camera.zoom = newZoom; } draw();}, { passive: false });
canvas.addEventListener("dblclick", () => { camera = { x: 0, y: 0, zoom: 1.0 }; draw();});
// Click: look up the tile's interned URI, fetch the post, render in the// sidebar's post-detail panel. Drag-vs-click is gated on dragDistance from// the preceding mousedown — anything past DRAG_PX_THRESHOLD was a pan, so// we leave the selection alone. (Browsers usually suppress click after a// big drag on their own, but explicit tracking makes it consistent across// trackpads / touch / different drag thresholds.)canvas.addEventListener("click", (e) => { const wasDrag = dragDistance > DRAG_PX_THRESHOLD; dragDistance = 0; if (wasDrag) return; if (!filmstrip || !filmstrip.snapshots.length) return; const rect = canvas.getBoundingClientRect(); const sx = e.clientX - rect.left - rect.width / 2; const sy = e.clientY - rect.top - rect.height / 2; const t = screenToTile(sx, sy); const found = findTileAtGrid(t.gx, t.gy); if (!found || !found.row) { clearSelectedPost(); return; } const uriIdx = found.row[5]; const uri = (uriIdx && filmstrip.uriTable) ? filmstrip.uriTable[uriIdx] : ""; selectedTile = { gx: t.gx, gy: t.gy }; drawHoverOverlay(); if (!uri) { openPostDetail(); renderPostDetail(null, null, "this tile isn't tied to a specific post"); return; } selectPost(uri, found.owningFarmDid);});