Introduzione
HTML5 Canvas è utile non solo per disegnare forme, ma anche per costruire interfacce grafiche dinamiche. In questo tutorial realizzeremo un diagramma di rete 2D: ogni nodo rappresenterà un elemento, mentre le linee mostreranno le relazioni tra gli elementi. L’utente potrà trascinare i nodi con il mouse e osservare il diagramma aggiornarsi in tempo reale.
Questo tipo di componente può essere utilizzato per visualizzare architetture software, mappe concettuali, relazioni tra utenti, dipendenze tra moduli o flussi di lavoro. L’esempio utilizzerà esclusivamente Canvas, JavaScript e un piccolo pannello HTML per spiegare come separare dati, rendering e interazione.
Il canvas possiede una propria superficie grafica, ma non crea automaticamente elementi DOM per le forme disegnate. Di conseguenza, per capire quale nodo è stato selezionato dobbiamo calcolare manualmente la distanza tra il punto del mouse e il centro di ciascun nodo.
Codice completo
<div class="toolbar">
<button id="resetButton">Ripristina posizione</button>
<button id="addButton">Aggiungi nodo</button>
</div>
<canvas id="networkCanvas" width="900" height="520">
Il browser non supporta Canvas.
</canvas>
<p id="status">Seleziona un nodo per iniziare.</p>
<script>
const canvas = document.querySelector("#networkCanvas");
const ctx = canvas.getContext("2d");
const status = document.querySelector("#status");
const resetButton = document.querySelector("#resetButton");
const addButton = document.querySelector("#addButton");
const initialNodes = [
{ id: "browser", label: "Browser", x: 170, y: 180, color: "#2563eb" },
{ id: "api", label: "API", x: 450, y: 120, color: "#16a34a" },
{ id: "database", label: "Database", x: 720, y: 220, color: "#dc2626" },
{ id: "cache", label: "Cache", x: 450, y: 360, color: "#ca8a04" }
];
const links = [
["browser", "api"],
["api", "database"],
["api", "cache"],
["cache", "database"]
];
let nodes = cloneInitialNodes();
let selectedNode = null;
let isDragging = false;
let pointerOffset = { x: 0, y: 0 };
let newNodeNumber = 1;
function cloneInitialNodes() {
return initialNodes.map(node => ({ ...node }));
}
// Converte le coordinate della pagina nelle coordinate interne al canvas.
function getCanvasPosition(event) {
const rectangle = canvas.getBoundingClientRect();
return {
x: (event.clientX - rectangle.left) * canvas.width / rectangle.width,
y: (event.clientY - rectangle.top) * canvas.height / rectangle.height
};
}
function findNodeAt(position) {
// I nodi vengono controllati al contrario per simulare
// una priorità all´ultimo elemento disegnato.
for (let index = nodes.length - 1; index >= 0; index--) {
const node = nodes[index];
const distance = Math.hypot(position.x - node.x, position.y - node.y);
if (distance <= 34) {
return node;
}
}
return null;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Sfondo leggermente colorato.
ctx.fillStyle = "#f8fafc";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawLinks();
nodes.forEach(node => {
drawNode(node);
});
requestAnimationFrame(draw);
}
function drawLinks() {
ctx.lineWidth = 2;
ctx.strokeStyle = "#94a3b8";
links.forEach(([fromId, toId]) => {
const from = nodes.find(node => node.id === fromId);
const to = nodes.find(node => node.id === toId);
// Un collegamento può essere ignorato se uno dei nodi non esiste.
if (!from || !to) return;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
// Disegna un piccolo punto centrale sulla relazione.
const centerX = (from.x + to.x) / 2;
const centerY = (from.y + to.y) / 2;
ctx.fillStyle = "#64748b";
ctx.beginPath();
ctx.arc(centerX, centerY, 4, 0, Math.PI * 2);
ctx.fill();
});
}
function drawNode(node) {
const isSelected = selectedNode === node;
ctx.beginPath();
ctx.arc(node.x, node.y, isSelected ? 39 : 34, 0, Math.PI * 2);
ctx.fillStyle = node.color;
ctx.fill();
ctx.lineWidth = isSelected ? 5 : 2;
ctx.strokeStyle = isSelected ? "#111827" : "#ffffff";
ctx.stroke();
ctx.fillStyle = "#ffffff";
ctx.font = "bold 14px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(node.label, node.x, node.y);
}
canvas.addEventListener("pointerdown", event => {
const position = getCanvasPosition(event);
const node = findNodeAt(position);
if (!node) return;
selectedNode = node;
isDragging = true;
pointerOffset.x = position.x - node.x;
pointerOffset.y = position.y - node.y;
canvas.setPointerCapture(event.pointerId);
status.textContent = `Nodo selezionato: ${node.label}`;
});
canvas.addEventListener("pointermove", event => {
if (!isDragging || !selectedNode) return;
const position = getCanvasPosition(event);
selectedNode.x = Math.max(
40,
Math.min(canvas.width - 40, position.x - pointerOffset.x)
);
selectedNode.y = Math.max(
40,
Math.min(canvas.height - 40, position.y - pointerOffset.y)
);
});
canvas.addEventListener("pointerup", event => {
isDragging = false;
selectedNode = null;
canvas.releasePointerCapture(event.pointerId);
status.textContent = "Trascina un nodo per spostarlo.";
});
canvas.addEventListener("pointercancel", () => {
isDragging = false;
selectedNode = null;
});
resetButton.addEventListener("click", () => {
nodes = cloneInitialNodes();
status.textContent = "Posizioni ripristinate.";
});
addButton.addEventListener("click", () => {
const newNode = {
id: `extra-${newNodeNumber}`,
label: `Nodo ${newNodeNumber}`,
x: 150 + Math.random() * 600,
y: 80 + Math.random() * 350,
color: "#7c3aed"
};
nodes.push(newNode);
newNodeNumber++;
status.textContent = `${newNode.label} aggiunto al diagramma.`;
});
draw();
</script> Spiegazione
Modello dei dati
Ogni nodo è rappresentato da un oggetto con un identificatore, un’etichetta, una posizione e un colore. L’array links contiene coppie di identificatori: in questo modo i collegamenti non dipendono direttamente dall’ordine dei nodi e possono essere ricostruiti facilmente.
Questa separazione è importante. I dati descrivono il diagramma, mentre le funzioni di disegno si occupano esclusivamente della sua visualizzazione. In un’applicazione reale, l’array dei nodi potrebbe arrivare da un file JSON o da una richiesta HTTP.
Rendering continuo
La funzione draw() cancella il contenuto precedente con clearRect(), ridisegna lo sfondo, i collegamenti e infine i nodi. La chiamata a requestAnimationFrame() programma il rendering successivo in modo sincronizzato con il browser.
Anche se il diagramma non si muove autonomamente, un ciclo di rendering continuo rende l’aggiornamento fluido durante il trascinamento. Per progetti molto grandi è possibile ridisegnare soltanto quando cambiano i dati, evitando lavoro inutile.
Coordinate e trascinamento
getCanvasPosition() converte le coordinate del puntatore in coordinate interne al canvas. La conversione tiene conto delle dimensioni effettive visualizzate, quindi continua a funzionare anche quando il canvas viene ridimensionato tramite CSS.
Quando l’utente preme su un nodo, memorizziamo la differenza tra il punto cliccato e il centro del nodo. Questo evita uno spostamento improvviso: il nodo mantiene la stessa posizione relativa rispetto al puntatore durante il drag.
Il metodo setPointerCapture() permette al canvas di continuare a ricevere gli eventi anche se il puntatore esce temporaneamente dai suoi bordi. Gli eventi Pointer sono preferibili ai soli eventi mouse perché supportano anche touch screen e penne digitali.
Best practice
- Usa identificatori univoci per i nodi e non affidarti all’indice dell’array.
- Limita le coordinate dei nodi ai bordi del canvas, così gli elementi rimangono sempre visibili.
- Usa getBoundingClientRect() quando il canvas può avere dimensioni CSS diverse dalla risoluzione interna.
- Per supportare display ad alta densità, considera il rapporto tra devicePixelRatio e dimensioni CSS.
- Mantieni separati dati, rilevamento dell’interazione e funzioni di rendering.
- Per molti nodi, evita di controllare ogni elemento se non è vicino all’area modificata; puoi introdurre una griglia spaziale o un quadtree.
- Prevedi un’alternativa accessibile: Canvas non espone automaticamente i nodi come elementi navigabili da tastiera.
- Usa colori con contrasto sufficiente e non affidarti esclusivamente al colore per comunicare una relazione.
Riepilogo
In questo esempio abbiamo costruito un diagramma di rete interattivo con Canvas HTML5. Abbiamo definito nodi e collegamenti tramite oggetti JavaScript, disegnato cerchi e linee con l’API 2D, rilevato il nodo sotto il puntatore e implementato il trascinamento con gli eventi Pointer.
La stessa struttura può essere estesa con frecce direzionali, etichette esterne, cancellazione dei nodi, salvataggio in formato JSON, zoom, ricerca o layout automatici. Il concetto centrale rimane invariato: Canvas disegna i pixel, mentre il codice JavaScript conserva lo stato e gestisce le interazioni.
Approfondisci con risorse ufficiali
- MDN Canvas API: documentazione completa per contesto 2D, tracciati, testo e trasformazioni.
- MDN CanvasRenderingContext2D: riferimento per metodi come arc(), lineTo(), fill() e stroke().
- MDN Pointer Events: guida agli eventi compatibili con mouse, touch e penna.
- WHATWG HTML Living Standard: specifica ufficiale dell’elemento Canvas e del suo contesto grafico.
