view.js (2.5 KB)
1 let dpr = window.devicePixelRatio || 1; 2 let cssW = 0, cssH = 0; 3 4 const ZOOM_STOPS = [2, 3, 4, 5, 6, 8, 11, 15, 21, 30]; 5 const MIN_ZOOM = ZOOM_STOPS[0]; 6 const ZOOM_SENSITIVITY = 3.5; // fractional levels per unit log(pinch factor) 7 let zoomIdx = 0; 8 let zoom = MIN_ZOOM; 9 10 let camX = 0, camY = 0; 11 12 let curX = 0, curY = 0; 13 // used to keep the brush pinned under the real cursor while panning, since 14 // the OS does not emit pointermove events when only the camera moves. 15 let curClientX = null, curClientY = null; 16 let mouseInside = false; 17 let cursorAlpha = 0; 18 let cursorFadeFrom = 0; 19 let cursorFadeTo = 0; 20 let cursorFadeStart = 0; 21 const CURSOR_FADE_IN_MS = 100; 22 const CURSOR_FADE_OUT_MS = 400; 23 function setCursorFadeTarget(target) { 24 if (target === cursorFadeTo) return; 25 cursorFadeFrom = cursorAlpha; 26 cursorFadeTo = target; 27 cursorFadeStart = performance.now(); 28 requestDraw(); 29 } 30 function showCursor() { 31 mouseInside = true; 32 setCursorFadeTarget(1); 33 } 34 function hideCursorNow() { 35 mouseInside = false; 36 cursorAlpha = 0; 37 cursorFadeFrom = 0; 38 cursorFadeTo = 0; 39 } 40 41 function resize() { 42 const firstResize = cssW === 0; 43 dpr = window.devicePixelRatio || 1; 44 cssW = window.innerWidth; 45 cssH = window.innerHeight; 46 view.style.width = cssW + 'px'; 47 view.style.height = cssH + 'px'; 48 view.width = Math.floor(cssW * dpr); 49 view.height = Math.floor(cssH * dpr); 50 if (firstResize) { 51 curX = Math.floor(cssW / (2 * zoom)); 52 curY = Math.floor(cssH / (2 * zoom)); 53 } 54 requestDraw(); 55 } 56 57 function clientToWorld(clientX, clientY) { 58 const lx = Math.floor(clientX / zoom + camX); 59 const ly = Math.floor(clientY / zoom + camY); 60 return { x: lx, y: ly }; 61 } 62 63 function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } 64 65 function applyZoom(factor, ax, ay) { 66 const worldAtAnchorX = camX + ax / zoom; 67 const worldAtAnchorY = camY + ay / zoom; 68 zoomIdx = clamp(zoomIdx + Math.log(factor) * ZOOM_SENSITIVITY, 0, ZOOM_STOPS.length - 1); 69 const newZoom = ZOOM_STOPS[Math.round(zoomIdx)]; 70 if (newZoom !== zoom) { 71 zoom = newZoom; 72 camX = worldAtAnchorX - ax / zoom; 73 camY = worldAtAnchorY - ay / zoom; 74 } 75 } 76 77 function zoomTo(newZoom) { 78 if (newZoom === zoom) return; 79 const ax = cssW / 2, ay = cssH / 2; 80 const worldAtAnchorX = camX + ax / zoom; 81 const worldAtAnchorY = camY + ay / zoom; 82 zoom = newZoom; 83 zoomIdx = ZOOM_STOPS.indexOf(zoom); 84 camX = worldAtAnchorX - ax / zoom; 85 camY = worldAtAnchorY - ay / zoom; 86 requestDraw(); 87 } 88 89 function stepZoom(dir) { 90 const i = ZOOM_STOPS.indexOf(zoom); 91 zoomTo(ZOOM_STOPS[clamp(i + dir, 0, ZOOM_STOPS.length - 1)]); 92 } 93 function resetZoom() { zoomTo(MIN_ZOOM); }