Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-02-27 16:53:40 -08:00
commit f03d92d9b3
2 changed files with 100 additions and 62 deletions

View File

@ -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
<!-- SECTION:DESCRIPTION:BEGIN -->
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
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #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
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@ -702,8 +702,6 @@
</div>
<div id="toolbar">
<button id="quick-add" title="Add rApp" style="background:#14b8a6;color:white;font-size:18px;font-weight:700;padding:6px 10px;border:none;border-radius:8px;cursor:pointer;text-align:center;line-height:1;">+</button>
<span class="toolbar-sep"></span>
<div class="toolbar-group">
<button class="toolbar-group-toggle">✏️ Draw</button>
<div class="toolbar-dropdown">
@ -717,13 +715,17 @@
</div>
<div class="toolbar-group">
<button class="toolbar-group-toggle">📝 Create</button>
<button class="toolbar-group-toggle">📝 Note</button>
<div class="toolbar-dropdown">
<button id="new-markdown" title="New Note">📝 Note</button>
<button id="new-wrapper" title="New Card">🗂️ Card</button>
<button id="new-slide" title="New Slide">🎞️ Slide</button>
<button id="new-obs-note" title="New Rich Note">📓 Rich Note</button>
<button id="new-chat" title="New Chat">💬 Chat</button>
<button id="embed-notes" title="Embed rNotes">📝 rNotes</button>
<button id="embed-books" title="Embed rBooks">📚 rBooks</button>
<button id="embed-pubs" title="Embed rPubs">📖 rPubs</button>
<button id="embed-forum" title="Embed rForum">💬 rForum</button>
</div>
</div>
@ -737,6 +739,7 @@
<button id="new-drawfast" title="New Drawing">✏️ Drawfast</button>
<button id="new-freecad" title="New FreeCAD">📐 FreeCAD</button>
<button id="new-kicad" title="New KiCAD PCB">🔌 KiCAD PCB</button>
<button id="embed-swag" title="Embed rSwag">🎨 rSwag</button>
</div>
</div>
@ -746,6 +749,8 @@
<button id="new-transcription" title="New Transcription">🎤 Transcribe</button>
<button id="new-video-chat" title="New Video Call">📹 Video Call</button>
<button id="new-piano" title="New Piano">🎹 Piano</button>
<button id="embed-photos" title="Embed rPhotos">📸 rPhotos</button>
<button id="embed-tube" title="Embed rTube">🎬 rTube</button>
</div>
</div>
@ -757,6 +762,12 @@
<button id="new-calendar" title="New Calendar">📅 Calendar</button>
<button id="new-map" title="New Map">🗺️ Map</button>
<button id="new-social-post" title="New Post">📱 Social Post</button>
<button id="embed-files" title="Embed rFiles">📁 rFiles</button>
<button id="embed-work" title="Embed rWork">📋 rWork</button>
<button id="embed-inbox" title="Embed rInbox">📧 rInbox</button>
<button id="embed-cart" title="Embed rCart">🛒 rCart</button>
<button id="embed-data" title="Embed rData">📊 rData</button>
<button id="embed-network" title="Embed rNetwork">🌍 rNetwork</button>
</div>
</div>
@ -786,28 +797,9 @@
<button id="new-choice-rank" title="New Ranking">📊 Ranking</button>
<button id="new-choice-spider" title="New Scoring">🕸 Scoring</button>
<button id="new-token" title="New Token">🪙 Token</button>
</div>
</div>
<div class="toolbar-group">
<button class="toolbar-group-toggle">📱 rApps</button>
<div class="toolbar-dropdown">
<button id="embed-notes" title="Embed rNotes">📝 rNotes</button>
<button id="embed-photos" title="Embed rPhotos">📸 rPhotos</button>
<button id="embed-books" title="Embed rBooks">📚 rBooks</button>
<button id="embed-pubs" title="Embed rPubs">📖 rPubs</button>
<button id="embed-files" title="Embed rFiles">📁 rFiles</button>
<button id="embed-work" title="Embed rWork">📋 rWork</button>
<button id="embed-forum" title="Embed rForum">💬 rForum</button>
<button id="embed-inbox" title="Embed rInbox">📧 rInbox</button>
<button id="embed-tube" title="Embed rTube">🎬 rTube</button>
<button id="embed-funds" title="Embed rFunds">🌊 rFunds</button>
<button id="embed-wallet" title="Embed rWallet">💰 rWallet</button>
<button id="embed-vote" title="Embed rVote">🗳️ rVote</button>
<button id="embed-cart" title="Embed rCart">🛒 rCart</button>
<button id="embed-data" title="Embed rData">📊 rData</button>
<button id="embed-network" title="Embed rNetwork">🌍 rNetwork</button>
<button id="embed-swag" title="Embed rSwag">🎨 rSwag</button>
</div>
</div>
@ -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;