Introduzione
Canvas HTML5 è uno strumento potente per la grafica 2D, ma il suo vero valore emerge quando lo usi per costruire interfacce interattive. In questo tutorial vedremo come realizzare un editor di disegno 2D con due funzioni fondamentali e molto pratiche: zoom e pan (spostamento della vista).
Questo sotto-argomento è utile in tanti contesti reali: mappe, diagrammi, schizzi tecnici, lavagne digitali, editor vettoriali semplificati e strumenti di annotazione. L’idea è semplice: l’utente deve poter esplorare una superficie più grande del canvas visibile, ingrandire i dettagli e spostarsi liberamente senza perdere il controllo del disegno.
Per ottenere questo risultato useremo il contesto 2D del canvas e una trasformazione della matrice grafica tramite translate e scale. In pratica, non muoveremo gli oggetti uno a uno: muoveremo la “camera” che osserva il mondo disegnato.
Codice completo
Il seguente esempio mostra un canvas con una griglia, alcuni oggetti geometrici e controlli per zoomare con la rotellina del mouse e trascinare la scena con il tasto sinistro.
<canvas id="board" width="900" height="600"></canvas>
<script>
const canvas = document.getElementById("board");
const ctx = canvas.getContext("2d");
// Stato della vista
let view = {
x: 0, // spostamento orizzontale della camera
y: 0, // spostamento verticale della camera
scale: 1 // livello di zoom
};
// Stato del drag
let isPanning = false;
let startPan = { x: 0, y: 0 };
let startView = { x: 0, y: 0 };
// Oggetti del mondo da disegnare
const shapes = [
{ type: "rect", x: 100, y: 80, w: 180, h: 120, color: "#4f46e5" },
{ type: "circle", x: 420, y: 170, r: 60, color: "#16a34a" },
{ type: "rect", x: 620, y: 100, w: 140, h: 160, color: "#dc2626" },
{ type: "circle", x: 260, y: 360, r: 90, color: "#f59e0b" }
];
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function drawGrid() {
const gridSize = 50;
const startX = Math.floor((-view.x) / (gridSize * view.scale)) - 2;
const endX = Math.ceil((canvas.width - view.x) / (gridSize * view.scale)) + 2;
const startY = Math.floor((-view.y) / (gridSize * view.scale)) - 2;
const endY = Math.ceil((canvas.height - view.y) / (gridSize * view.scale)) + 2;
ctx.save();
ctx.strokeStyle = "#e5e7eb";
ctx.lineWidth = 1;
for (let x = startX; x <= endX; x++) {
const worldX = x * gridSize;
const screenX = worldX * view.scale + view.x;
ctx.beginPath();
ctx.moveTo(screenX, 0);
ctx.lineTo(screenX, canvas.height);
ctx.stroke();
}
for (let y = startY; y <= endY; y++) {
const worldY = y * gridSize;
const screenY = worldY * view.scale + view.y;
ctx.beginPath();
ctx.moveTo(0, screenY);
ctx.lineTo(canvas.width, screenY);
ctx.stroke();
}
ctx.restore();
}
function drawShapes() {
ctx.save();
// Trasformazione della vista: prima spostiamo, poi zoomiamo
ctx.translate(view.x, view.y);
ctx.scale(view.scale, view.scale);
for (const shape of shapes) {
ctx.fillStyle = shape.color;
if (shape.type === "rect") {
ctx.fillRect(shape.x, shape.y, shape.w, shape.h);
}
if (shape.type === "circle") {
ctx.beginPath();
ctx.arc(shape.x, shape.y, shape.r, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
function drawHUD() {
ctx.save();
ctx.fillStyle = "#111827";
ctx.font = "14px sans-serif";
ctx.fillText(`Zoom: ${view.scale.toFixed(2)}x`, 16, 24);
ctx.fillText("Mouse wheel: zoom | Drag: pan", 16, 44);
ctx.restore();
}
function render() {
clearCanvas();
drawGrid();
drawShapes();
drawHUD();
}
function screenToWorld(screenX, screenY) {
return {
x: (screenX - view.x) / view.scale,
y: (screenY - view.y) / view.scale
};
}
// Zoom con rotellina, centrato sul puntatore
canvas.addEventListener("wheel", (e) => {
e.preventDefault();
const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
const mouse = screenToWorld(e.offsetX, e.offsetY);
const newScale = Math.min(3, Math.max(0.4, view.scale * zoomFactor));
// Manteniamo fisso il punto sotto il mouse
view.x = e.offsetX - mouse.x * newScale;
view.y = e.offsetY - mouse.y * newScale;
view.scale = newScale;
render();
});
// Pan con drag
canvas.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
isPanning = true;
startPan.x = e.clientX;
startPan.y = e.clientY;
startView.x = view.x;
startView.y = view.y;
});
window.addEventListener("mousemove", (e) => {
if (!isPanning) return;
const dx = e.clientX - startPan.x;
const dy = e.clientY - startPan.y;
view.x = startView.x + dx;
view.y = startView.y + dy;
render();
});
window.addEventListener("mouseup", () => {
isPanning = false;
});
// Ridisegno iniziale
render();
</script> Spiegazione
Il cuore di questo esempio è la distinzione tra mondo e schermo. Il mondo contiene gli oggetti da disegnare: rettangoli, cerchi, griglie o qualunque altra forma. Lo schermo è l’area visibile del canvas. La vista agisce come una telecamera che decide quale porzione del mondo mostrare e con quale ingrandimento.
1. La variabile view
La struttura view contiene tre valori:
- x e y: lo spostamento della camera sul canvas.
- scale: il livello di zoom.
Quando scale aumenta, tutto appare più grande. Quando x e y cambiano, la scena si sposta sullo schermo.
2. Disegnare con translate e scale
Nel metodo drawShapes() usiamo:
- ctx.translate(view.x, view.y) per spostare il sistema di coordinate.
- ctx.scale(view.scale, view.scale) per applicare lo zoom.
Questo approccio è molto più pulito rispetto al ricalcolo manuale delle coordinate di ogni oggetto. Disegniamo tutti gli elementi nel loro sistema naturale, e lasciamo al canvas il compito di trasformarli.
3. La griglia adattiva
La griglia viene disegnata tenendo conto della vista corrente. Per evitare di tracciare linee inutili fuori dall’area visibile, calcoliamo il range di linee da mostrare in base a view.x, view.y e view.scale. Questo è importante per mantenere buone prestazioni quando la scena cresce.
4. Zoom centrato sul puntatore
Uno degli aspetti più interessanti è lo zoom centrato sul mouse. Se ingrandissimo il canvas senza correggere la posizione della vista, il punto osservato si sposterebbe in modo fastidioso. Per evitarlo:
- convertiamo la posizione del mouse da coordinate schermo a coordinate mondo con screenToWorld();
- calcoliamo il nuovo zoom;
- ricalcoliamo view.x e view.y per mantenere fermo il punto sotto il cursore.
5. Pan con il drag
Il pan funziona memorizzando la posizione iniziale del mouse e della vista al momento del click. Durante il movimento, calcoliamo la differenza dx e dy e aggiorniamo la camera. Il risultato è uno spostamento fluido e intuitivo.
Best practice
- Separare sempre lo stato del mondo dallo stato della vista: gli oggetti devono avere coordinate proprie, indipendenti dallo zoom.
- Usare ctx.save() e ctx.restore(): così eviti effetti collaterali tra griglia, oggetti e interfaccia.
- Limitare lo zoom minimo e massimo: valori troppo estremi rendono l’interfaccia difficile da usare e possono degradare le prestazioni.
- Convertire correttamente le coordinate: quando l’utente interagisce con il canvas, ricordati sempre di distinguere tra coordinate schermo e coordinate mondo.
- Ridisegnare in modo controllato: in applicazioni più grandi, valuta requestAnimationFrame invece di chiamare render in ogni evento, soprattutto se ci sono animazioni continue.
- Ottimizzare la griglia: disegna solo le linee visibili. Su scene grandi, questo fa una differenza notevole.
Riepilogo
In questo tutorial hai visto come creare un editor di disegno 2D con Canvas HTML5 capace di gestire zoom e pan. Hai imparato a usare una camera virtuale, a trasformare le coordinate con translate e scale, e a mantenere il punto sotto il mouse durante l’ingrandimento.
Questa tecnica è una base molto solida per sviluppare strumenti più avanzati: mappe interattive, lavagne condivise, editor di diagrammi, planner visuali o applicazioni CAD semplificate. Una volta compreso il rapporto tra mondo e schermo, Canvas diventa molto più flessibile e professionale.
Approfondisci con risorse ufficiali
- MDN Web Docs - Canvas API: documentazione completa sulle funzionalità 2D del canvas.
- MDN Web Docs - Transformations: approfondimento su translate, scale e rotate.
- MDN Web Docs - MouseEvent: utile per gestire input da mouse e coordinate del puntatore.
- WHATWG HTML Living Standard: riferimento ufficiale per l’elemento <canvas>.
