Skip to content

Instantly share code, notes, and snippets.

@bellinitte
Last active September 23, 2020 19:35
Show Gist options
  • Save bellinitte/3bdd04332901810b7c0d83ea9747e0a6 to your computer and use it in GitHub Desktop.
Save bellinitte/3bdd04332901810b7c0d83ea9747e0a6 to your computer and use it in GitHub Desktop.
Utility functions for handling geometrically correct panning and zooming of a canvas
class Viewport {
constructor(canvas) {
this.canvas = canvas;
this.camera = {
x: 0,
y: 0,
z: 1,
};
this.pivot = null;
}
startPan(screenPoint) {
this.pivot = {
x: this.camera.x + (screenPoint.x - this.canvas.width / 2) * this.camera.z,
y: this.camera.y + (screenPoint.y - this.canvas.height / 2) * this.camera.z,
};
}
pan(screenPoint) {
if (this.pivot) {
this.camera.x = this.pivot.x - (screenPoint.x - this.canvas.width / 2) * this.camera.z;
this.camera.y = this.pivot.y - (screenPoint.y - this.canvas.height / 2) * this.camera.z;
}
}
endPan() {
this.pivot = null;
}
zoom(screenPivotPoint, factor) {
const difference = this.camera.z * (1 - factor);
this.camera.x += (screenPivotPoint.x - this.canvas.width / 2) * difference;
this.camera.y += (screenPivotPoint.y - this.canvas.height / 2) * difference;
this.camera.z *= factor;
}
projectToCanvas(screenPoint) {
return {
x: (screenPoint.x - this.canvas.width / 2) * this.camera.z + this.camera.x,
y: (screenPoint.y - this.canvas.height / 2) * this.camera.z + this.camera.y,
};
}
projectToScreen(canvasPoint) {
return {
x: this.canvas.width / 2 + (canvasPoint.x - this.camera.x) / this.camera.z,
y: this.canvas.height / 2 + (canvasPoint.y - this.camera.y) / this.camera.z,
};
}
scaleToCanvas(screenScalar) {
return screenScalar * this.camera.z;
}
scaleToScreen(canvasScalar) {
return canvasScalar / this.camera.z;
}
}
const canvas = document.getElementsByTagName('canvas')[0];
const viewport = new Viewport(canvas);
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
window.addEventListener('mousedown', e => {
viewport.startPan({x: e.x, y: e.y});
});
window.addEventListener('mousemove', e => {
viewport.pan({x: e.x, y: e.y});
});
window.addEventListener('mouseup', e => {
viewport.endPan();
});
window.addEventListener("wheel", e => {
const factor = e.deltaY < 0 ? 0.9 : (1 / 0.9);
viewport.zoom({x: e.x, y: e.y}, factor);
});
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw a grid of points
for (let y = -2; y <= 2; y++) {
for (let x = -2; x <= 2; x++) {
ctx.beginPath();
const point = viewport.projectToScreen({x: x * 100, y: y * 100});
const radius = 10;
ctx.arc(point.x, point.y, viewport.scaleToScreen(radius), 0, Math.PI * 2, false);
ctx.fill();
}
}
requestAnimationFrame(animate);
}
animate();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment