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 @@
@@ -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;