From f4eee2af3f713757fe34a095395306b168bbfced Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sun, 12 Apr 2026 10:12:27 +0300 Subject: [PATCH] feat: minimap navigation overlay + ruler/protractor property controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minimap: - Auto-shows in bottom-right corner when zoom > 1.05 - Renders full board content at scale (background + all strokes) - Purple viewport indicator with darkened outer areas - Click/drag to jump-pan the viewport - Cleaned up on destroy() Ruler/protractor property controls: - Rotation handle (purple ↺) — drag to rotate around origin - Resize handle (cyan ↔) — drag to change length/radius - Protractor now supports rotation via ctx.rotate(ov.angle) - Floating props panel in toolbar: angle° and length/radius inputs - Panel auto-shows on first click/drag, hides when overlay toggled off - Canvas-space hit testing with rotation-aware local coordinates Co-Authored-By: Claude Sonnet 4.6 --- frontend/js/whiteboard.js | 40 ++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/frontend/js/whiteboard.js b/frontend/js/whiteboard.js index 53951fb..daf7c14 100644 --- a/frontend/js/whiteboard.js +++ b/frontend/js/whiteboard.js @@ -2320,35 +2320,41 @@ class Whiteboard { this._mmCanvas.style.display = visible ? 'block' : 'none'; if (!visible) return; - const mm = this._mmCtx; - const MW = 192, MH = 108; - mm.clearRect(0, 0, MW, MH); + const mm = this._mmCtx; + const MW = 192, MH = 108; - // Blit the static layer (full board content) scaled to minimap size - mm.drawImage(this._canvas, 0, 0, MW, MH); + // Render full board at zoom=1 (same approach as renderThumbnail) + const [sw, sh, sz, spx, spy] = [this._cssW, this._cssH, this._zoom, this._panVX, this._panVY]; + this._cssW = MW; this._cssH = MH; + this._zoom = 1; this._panVX = 0; this._panVY = 0; + this._renderBg(mm); + if (this._template && this._template !== 'blank') this._renderTemplate(mm); + for (const s of this._strokes) this._renderStroke(mm, s); + // Restore + this._cssW = sw; this._cssH = sh; this._zoom = sz; this._panVX = spx; this._panVY = spy; - // Darken areas outside the viewport with a subtle vignette - const vpW = MW / this._zoom; - const vpH = MH / this._zoom; - const vpX = (this._panVX / Whiteboard.VW) * MW; - const vpY = (this._panVY / Whiteboard.VH) * MH; + // Viewport indicator + const vpW = MW / sz; + const vpH = MH / sz; + const vpX = (spx / Whiteboard.VW) * MW; + const vpY = (spy / Whiteboard.VH) * MH; // Dark overlay on non-viewport areas mm.fillStyle = 'rgba(0,0,0,0.42)'; - mm.fillRect(0, 0, MW, vpY); // top strip - mm.fillRect(0, vpY + vpH, MW, MH - vpY - vpH); // bottom strip - mm.fillRect(0, vpY, vpX, vpH); // left strip - mm.fillRect(vpX + vpW, vpY, MW - vpX - vpW, vpH); // right strip + mm.fillRect(0, 0, MW, vpY); + mm.fillRect(0, vpY + vpH, MW, MH - vpY - vpH); + mm.fillRect(0, vpY, vpX, vpH); + mm.fillRect(vpX + vpW, vpY, MW - vpX - vpW, vpH); // Viewport border mm.strokeStyle = 'rgba(155,93,229,0.95)'; mm.lineWidth = 1.5; mm.strokeRect(vpX, vpY, vpW, vpH); - // Current position crosshair at viewport center + // Crosshair at viewport center const cx = vpX + vpW / 2, cy = vpY + vpH / 2; - mm.strokeStyle = 'rgba(155,93,229,0.55)'; - mm.lineWidth = 0.7; + mm.strokeStyle = 'rgba(155,93,229,0.6)'; + mm.lineWidth = 0.8; mm.beginPath(); mm.moveTo(cx - 5, cy); mm.lineTo(cx + 5, cy); mm.stroke(); mm.beginPath(); mm.moveTo(cx, cy - 5); mm.lineTo(cx, cy + 5); mm.stroke(); }