From bc6f5873f17ae10f42f87825ac64b136cfee3d72 Mon Sep 17 00:00:00 2001 From: sanine Date: Sun, 29 May 2022 17:11:48 -0500 Subject: add zooming and panning --- src/Canvas.js | 95 +++++++++++++++++++++++++++++++++++++++++++++-------- src/main.js | 17 +++++++--- src/modules/Util.js | 6 +++- 3 files changed, 98 insertions(+), 20 deletions(-) diff --git a/src/Canvas.js b/src/Canvas.js index 590034a..bd39f47 100644 --- a/src/Canvas.js +++ b/src/Canvas.js @@ -1,46 +1,113 @@ +import { clamp } from './modules/Util.js'; + class Canvas { constructor(rootId) { const root = document.getElementById(rootId); this.element = document.createElement('canvas'); this.context = this.element.getContext('2d'); - this.fillWindow(); - window.addEventListener('resize', () => this.fillWindow()); - root.appendChild(this.element); + /* state variables */ + this.scale = 1; + this.zoom = 1; + const ZOOM_SPEED = 1.2; + + this.mousePos = { x: 0, y: 0 }; + this.pan = { x: 0, y: 0 }; + this.panning = false; - this.ondraw = null; + /* callbacks */ + this.onDraw = null; + this.onMouseMove = null; - /* automatically compute drawing-space mouse coordinates */ - this.mousePos = { x: 0, y: 0 }; - this.onmousemove = null; + /* register event listeners */ + + /* mouse movement */ this.element.addEventListener('mousemove', e => { const matrix = this.context.getTransform(); matrix.invertSelf(); - const screenX = e.offsetX; const screenY = e.offsetY; - this.mousePos.x = matrix.a*screenX + matrix.b*screenY; - this.mousePos.y = matrix.c*screenX + matrix.d*screenY; - if (this.onmousemove) this.onmousemove(this.mousePos); + /* compute drawing-space position */ + const x = matrix.a*screenX + matrix.b*screenY + matrix.e; + const y = matrix.c*screenX + matrix.d*screenY + matrix.f; + + /* compute movement */ + const dx = e.movementX / (this.zoom * this.scale); + const dy = e.movementY / (this.zoom * this.scale); + + /* pan? */ + if (this.panning) { + this.pan.x += dx; + this.pan.y += dy; + this.setTransform(); + } + + this.mousePos.x = x; + this.mousePos.y = y; + + if (this.onMouseMove) this.onMouseMove(this.mousePos); }); + /* clicking */ + this.element.addEventListener('mousedown', e => { + if (e.button === 1) this.panning = true; + }); + this.element.addEventListener('mouseup', e => { + if (e.button === 1) this.panning = false; + }); + + /* mouse leave */ + this.element.addEventListener('mouseleave', e => { + this.panning = false; + }); + + /* mouse wheel */ + this.element.addEventListener('wheel', e => { + if (this.panning) return; // don't zoom and pan simultaneously + const delta = e.deltaY < 0 ? ZOOM_SPEED : 1/ZOOM_SPEED; + this.zoom *= delta; + this.zoom = clamp(this.zoom, 1, Infinity); + this.setTransform(); + this.draw(); + }); + + /* finalize setup */ + this.fillWindow(); + window.addEventListener('resize', () => this.fillWindow()); + root.appendChild(this.element); + } + + setTransform() { + /* clamp pan */ + console.log('---'); + console.log(this.pan); + const xMax = this.element.width / (this.zoom * this.scale) - 1; + const yMax = this.element.height / (this.zoom * this.scale) - 1; + this.pan.x = clamp(this.pan.x, xMax, 0); + this.pan.y = clamp(this.pan.y, yMax, 0); + console.log(xMax, yMax); + console.log(this.pan); + + this.context.setTransform(1, 0, 0, 1, 0, 0); + this.context.scale(this.zoom * this.scale, this.zoom * this.scale); + this.context.translate(this.pan.x, this.pan.y); } fillWindow() { const width = window.innerWidth; const height = window.innerHeight; - const scale = Math.max(width, height); + this.scale = Math.max(width, height); this.element.width = width; this.element.height = height; - this.context.scale(scale, scale); + this.setTransform(); this.draw(); } draw() { this.context.clearRect(0, 0, 1, 1); - if (this.ondraw) this.ondraw(this.context); + if (this.onDraw) this.onDraw(this.context); } } diff --git a/src/main.js b/src/main.js index 40c3f83..5c9b085 100644 --- a/src/main.js +++ b/src/main.js @@ -6,15 +6,22 @@ window.onload = () => { const canvas = new Canvas('root'); const pos = canvas.mousePos; - canvas.onmousemove = () => canvas.draw(); + canvas.onMouseMove = () => canvas.draw(); - canvas.ondraw = ct => { - ct.fillRect(0, 0, 0.5, 0.5); + canvas.onDraw = ct => { + ct.fillStyle = '#f00'; + ct.fillRect(0, 0, 1/3, 1/3); + ct.fillStyle = '#0f0'; + ct.fillRect(2/3, 0, 1/3, 1/3); + ct.fillStyle = '#ff0'; + ct.fillRect(0, 2/3, 1/3, 1/3); + ct.fillStyle = '#00f'; + ct.fillRect(2/3, 2/3, 1/3, 1/3); ct.strokeStyle = '#fff'; - ct.lineWidth = 0.001; + ct.lineWidth = 1/(canvas.zoom * canvas.scale); ct.beginPath(); - ct.arc(pos.x, pos.y, 0.1, 0, 2*Math.PI); + ct.arc(pos.x, pos.y, 30 /(canvas.zoom * canvas.scale), 0, 2*Math.PI); ct.closePath(); ct.stroke(); }; diff --git a/src/modules/Util.js b/src/modules/Util.js index cbe466d..2375f10 100644 --- a/src/modules/Util.js +++ b/src/modules/Util.js @@ -10,4 +10,8 @@ function useAverage() { return [() => avg, append]; } -export { useAverage }; +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} + +export { useAverage, clamp }; -- cgit v1.2.1