Merge branch 'dev'
This commit is contained in:
commit
f03d92d9b3
|
|
@ -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 -->
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue