From dfb032514745d705f1b676f7b4d3ce549a5f7575 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 27 Feb 2026 16:53:30 -0800 Subject: [PATCH] feat: gradual zoom, toolbar reorg, pinch-to-zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Zoom buttons 1.25x→1.1x, wheel 0.9/1.1→0.95/1.05 - Wheel + pinch zoom now center on cursor/pinch midpoint - Rename "Create" → "Note", remove quick-add "+" button - Redistribute 16 rApp embeds into thematic toolbar groups - Remove standalone "rApps" dropdown - Add pinch-to-zoom alongside two-finger pan on touch Co-Authored-By: Claude Opus 4.6 --- ...om-toolbar-reorganization-pinch-to-zoom.md | 42 ++++++ website/canvas.html | 120 +++++++++--------- 2 files changed, 100 insertions(+), 62 deletions(-) create mode 100644 backlog/tasks/task-71 - Gradual-zoom-toolbar-reorganization-pinch-to-zoom.md diff --git a/backlog/tasks/task-71 - Gradual-zoom-toolbar-reorganization-pinch-to-zoom.md b/backlog/tasks/task-71 - Gradual-zoom-toolbar-reorganization-pinch-to-zoom.md new file mode 100644 index 0000000..a0442ac --- /dev/null +++ b/backlog/tasks/task-71 - Gradual-zoom-toolbar-reorganization-pinch-to-zoom.md @@ -0,0 +1,42 @@ +--- +id: TASK-71 +title: 'Gradual zoom, toolbar reorganization, pinch-to-zoom' +status: Done +assignee: [] +created_date: '2026-02-28 00:53' +labels: + - canvas + - UX + - mobile +dependencies: [] +priority: medium +--- + +## Description + + +Three improvements to canvas UX: +1. **Gradual zoom** — button zoom 1.25x→1.1x, wheel zoom 0.9/1.1→0.95/1.05, cursor-centered wheel zoom +2. **Toolbar rename** — "📝 Create" → "📝 Note", removed quick-add "+" button +3. **Redistribute rApps** — removed standalone "📱 rApps" dropdown, moved 16 embed buttons into thematic groups (Note, Media, Embed, Decide, Creative) +4. **Pinch-to-zoom** — added two-finger pinch gesture with center-point zoom, alongside existing two-finger pan + + +## Acceptance Criteria + +- [ ] #1 Zoom buttons use 1.1x multiplier (10% steps) +- [ ] #2 Ctrl+wheel zoom uses 0.95/1.05 (5% steps) +- [ ] #3 Wheel and pinch zoom center on cursor/pinch midpoint +- [ ] #4 Toolbar shows '📝 Note' not '📝 Create' +- [ ] #5 No '📱 rApps' standalone dropdown exists +- [ ] #6 All 16 rApp embed buttons distributed into thematic groups +- [ ] #7 No quick-add '+' button in toolbar +- [ ] #8 Pinch-to-zoom works on touch devices alongside two-finger pan +- [ ] #9 All embed buttons still create folk-rapp shapes when clicked + + +## Final Summary + + +Modified `website/canvas.html` (58 insertions, 62 deletions). Zoom made more gradual across all input methods. Toolbar reorganized — rApp embeds distributed into thematic groups. Added pinch-to-zoom with center-point tracking. Cleaned up quick-add button and mobile menu rApps auto-open logic. + diff --git a/website/canvas.html b/website/canvas.html index a04dceb..039c452 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -702,8 +702,6 @@
- -
@@ -717,13 +715,17 @@
- +
+ + + +
@@ -737,6 +739,7 @@ +
@@ -746,6 +749,8 @@ + + @@ -757,6 +762,12 @@ + + + + + + @@ -786,28 +797,9 @@ - - - -
- -
- - - - - - - - - - - - -
@@ -2377,12 +2369,12 @@ } document.getElementById("zoom-in").addEventListener("click", () => { - scale = Math.min(scale * 1.25, maxScale); + scale = Math.min(scale * 1.1, maxScale); updateCanvasTransform(); }); document.getElementById("zoom-out").addEventListener("click", () => { - scale = Math.max(scale / 1.25, minScale); + scale = Math.max(scale / 1.1, minScale); updateCanvasTransform(); }); @@ -2398,22 +2390,9 @@ const toolbarEl = document.getElementById("toolbar"); mobileMenuBtn.addEventListener("click", () => { - // On mobile, first tap opens rApps group in the popout panel - const rAppsGroup = toolbarEl.querySelector(".toolbar-group:has(#embed-notes)") - || [...toolbarEl.querySelectorAll(".toolbar-group")].find(g => - g.querySelector(".toolbar-group-toggle")?.textContent.includes("rApps")); - if (rAppsGroup && !toolbarEl.classList.contains("mobile-open")) { - toolbarEl.classList.add("mobile-open"); - mobileMenuBtn.textContent = "✕"; - // Auto-open the rApps panel - setTimeout(() => { - if (typeof openToolbarPanel === "function") openToolbarPanel(rAppsGroup); - }, 50); - } else { - const isOpen = toolbarEl.classList.toggle("mobile-open"); - mobileMenuBtn.textContent = isOpen ? "✕" : "✚"; - if (!isOpen && typeof closeToolbarPanel === "function") closeToolbarPanel(); - } + const isOpen = toolbarEl.classList.toggle("mobile-open"); + mobileMenuBtn.textContent = isOpen ? "✕" : "✚"; + if (!isOpen && typeof closeToolbarPanel === "function") closeToolbarPanel(); }); // Auto-close toolbar after tapping a shape-creation button on mobile @@ -2495,21 +2474,6 @@ } }); - // Desktop quick-add button → opens the rApps popout panel - document.getElementById("quick-add")?.addEventListener("click", (e) => { - e.stopPropagation(); - const rAppsGroup = toolbarEl.querySelector(".toolbar-group:has(#embed-notes)") - || [...toolbarEl.querySelectorAll(".toolbar-group")].find(g => - g.querySelector(".toolbar-group-toggle")?.textContent.includes("rApps")); - if (rAppsGroup) { - if (activeToolbarGroup === rAppsGroup) { - closeToolbarPanel(); - } else { - openToolbarPanel(rAppsGroup); - } - } - }); - // Collapse/expand toolbar const collapseBtn = document.getElementById("toolbar-collapse"); collapseBtn.addEventListener("click", () => { @@ -2520,11 +2484,11 @@ // Mobile zoom controls (separate from toolbar) document.getElementById("mz-in").addEventListener("click", () => { - scale = Math.min(scale * 1.25, maxScale); + scale = Math.min(scale * 1.1, maxScale); updateCanvasTransform(); }); document.getElementById("mz-out").addEventListener("click", () => { - scale = Math.max(scale / 1.25, minScale); + scale = Math.max(scale / 1.1, minScale); updateCanvasTransform(); }); document.getElementById("mz-reset").addEventListener("click", () => { @@ -2534,8 +2498,9 @@ updateCanvasTransform(); }); - // Touch gesture handling for two-finger pan + // Touch gesture handling for two-finger pan + pinch-to-zoom let lastTouchCenter = null; + let lastTouchDist = null; function getTouchCenter(touches) { return { @@ -2544,6 +2509,12 @@ }; } + function getTouchDist(touches) { + const dx = touches[0].clientX - touches[1].clientX; + const dy = touches[0].clientY - touches[1].clientY; + return Math.hypot(dx, dy); + } + canvas.addEventListener("touchstart", (e) => { if (e.touches.length === 2) { e.preventDefault(); @@ -2552,6 +2523,7 @@ panPointerId = null; canvas.style.cursor = ""; lastTouchCenter = getTouchCenter(e.touches); + lastTouchDist = getTouchDist(e.touches); } }, { passive: false }); @@ -2559,13 +2531,30 @@ if (e.touches.length === 2) { e.preventDefault(); - // Two-finger pan (no zoom) const currentCenter = getTouchCenter(e.touches); + const currentDist = getTouchDist(e.touches); + if (lastTouchCenter) { + // Two-finger pan panX += currentCenter.x - lastTouchCenter.x; panY += currentCenter.y - lastTouchCenter.y; } + + if (lastTouchDist && lastTouchDist > 0) { + // Pinch-to-zoom around gesture center + const zoomDelta = currentDist / lastTouchDist; + const newScale = Math.min(Math.max(scale * zoomDelta, minScale), maxScale); + // Adjust pan so zoom centers on pinch midpoint + const rect = canvas.getBoundingClientRect(); + const cx = currentCenter.x - rect.left; + const cy = currentCenter.y - rect.top; + panX = cx - (cx - panX) * (newScale / scale); + panY = cy - (cy - panY) * (newScale / scale); + scale = newScale; + } + lastTouchCenter = currentCenter; + lastTouchDist = currentDist; updateCanvasTransform(); } @@ -2574,6 +2563,7 @@ canvas.addEventListener("touchend", (e) => { if (e.touches.length < 2) { lastTouchCenter = null; + lastTouchDist = null; } }); @@ -2581,9 +2571,15 @@ canvas.addEventListener("wheel", (e) => { e.preventDefault(); if (e.ctrlKey) { - // Ctrl+wheel (or trackpad pinch) = zoom - const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; - scale = Math.min(Math.max(scale * zoomFactor, minScale), maxScale); + // Ctrl+wheel (or trackpad pinch) = zoom centered on cursor + const zoomFactor = e.deltaY > 0 ? 0.95 : 1.05; + const newScale = Math.min(Math.max(scale * zoomFactor, minScale), maxScale); + const rect = canvas.getBoundingClientRect(); + const cx = e.clientX - rect.left; + const cy = e.clientY - rect.top; + panX = cx - (cx - panX) * (newScale / scale); + panY = cy - (cy - panY) * (newScale / scale); + scale = newScale; } else { // Regular wheel/two-finger scroll = pan panX -= e.deltaX;