Introduzione
Canvas HTML5 è uno strumento perfetto per disegnare elementi grafici dinamici direttamente nel browser. A differenza di SVG, Canvas lavora in modo pixel-based: tu definisci cosa disegnare e il browser “dipinge” il risultato su una superficie 2D. Questo lo rende ideale per componenti visivi personalizzati, mini-editor, dashboard e visualizzazioni dati.
In questo tutorial vedremo un sotto-argomento molto utile e pratico: creare una linea del tempo interattiva con Canvas 2D. L’obiettivo è rappresentare una sequenza di eventi, permettere all’utente di selezionare i punti con il mouse e visualizzare un dettaglio dell’evento scelto.
È un caso reale molto comune in applicazioni web: cronologie di attività, workflow, roadmap di progetto, log visuali o percorsi narrativi. L’esempio sarà semplice ma completo, così potrai riutilizzare la logica in progetti più complessi.
Codice completo
// Canvas HTML5: linea del tempo interattiva
const canvas = document.getElementById(´timelineCanvas´);
const ctx = canvas.getContext(´2d´);
const events = [
{ time: ´09:00´, title: ´Apertura´, detail: ´Il sistema viene avviato e inizializzato.´ },
{ time: ´10:30´, title: ´Primo accesso´, detail: ´L’utente entra nella dashboard e consulta i dati.´ },
{ time: ´12:00´, title: ´Aggiornamento´, detail: ´Vengono sincronizzati i nuovi record dal server.´ },
{ time: ´15:45´, title: ´Controllo finale´, detail: ´Verifica dello stato generale e dei log.´ }
];
let selectedIndex = 0;
let hoverIndex = -1;
// Dimensioni e layout
const paddingX = 60;
const lineY = 140;
const pointRadius = 10;
const activeRadius = 16;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Calcola la posizione orizzontale dei punti
function getEventX(index) {
const usableWidth = canvasWidth - paddingX * 2;
const step = usableWidth / (events.length - 1);
return paddingX + step * index;
}
// Disegna il testo del dettaglio
function drawDetailPanel(event) {
ctx.fillStyle = ´#f4f7fb´;
ctx.fillRect(40, 210, canvasWidth - 80, 120);
ctx.strokeStyle = ´#d7e0ea´;
ctx.strokeRect(40, 210, canvasWidth - 80, 120);
ctx.fillStyle = ´#1f2d3d´;
ctx.font = ´bold 20px sans-serif´;
ctx.fillText(event.title, 60, 245);
ctx.fillStyle = ´#52616b´;
ctx.font = ´16px sans-serif´;
ctx.fillText(´Ora: ´ + event.time, 60, 275);
wrapText(ctx, event.detail, 60, 305, canvasWidth - 120, 22);
}
// Funzione per andare a capo automaticamente
function wrapText(context, text, x, y, maxWidth, lineHeight) {
const words = text.split(´ ´);
let line = ´´;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ´ ´;
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line, x, y);
line = words[n] + ´ ´;
y += lineHeight;
} else {
line = testLine;
}
}
context.fillText(line, x, y);
}
// Disegna l´intera timeline
function drawTimeline() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Sfondo
ctx.fillStyle = ´#ffffff´;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// Titolo
ctx.fillStyle = ´#1f2d3d´;
ctx.font = ´bold 24px sans-serif´;
ctx.fillText(´Linea del tempo interattiva´, 40, 40);
ctx.fillStyle = ´#52616b´;
ctx.font = ´16px sans-serif´;
ctx.fillText(´Passa sopra i punti e clicca per selezionare un evento.´, 40, 65);
// Linea orizzontale
ctx.beginPath();
ctx.moveTo(paddingX, lineY);
ctx.lineTo(canvasWidth - paddingX, lineY);
ctx.strokeStyle = ´#b8c4d1´;
ctx.lineWidth = 4;
ctx.stroke();
// Punti e etichette
events.forEach((event, index) => {
const x = getEventX(index);
const isSelected = index === selectedIndex;
const isHover = index === hoverIndex;
// Etichetta oraria
ctx.fillStyle = ´#334155´;
ctx.font = ´14px sans-serif´;
ctx.textAlign = ´center´;
ctx.fillText(event.time, x, lineY - 22);
// Cerchio del punto
ctx.beginPath();
ctx.arc(x, lineY, isSelected ? activeRadius : pointRadius, 0, Math.PI * 2);
if (isSelected) {
ctx.fillStyle = ´#2563eb´;
} else if (isHover) {
ctx.fillStyle = ´#60a5fa´;
} else {
ctx.fillStyle = ´#94a3b8´;
}
ctx.fill();
// Bordo
ctx.lineWidth = 3;
ctx.strokeStyle = ´#ffffff´;
ctx.stroke();
// Titolo sotto il punto
ctx.fillStyle = ´#1f2d3d´;
ctx.font = ´15px sans-serif´;
ctx.fillText(event.title, x, lineY + 35);
});
// Pannello dettagli
drawDetailPanel(events[selectedIndex]);
}
// Rileva il punto sotto il mouse
function getHoveredIndex(mouseX, mouseY) {
for (let i = 0; i < events.length; i++) {
const x = getEventX(i);
const dx = mouseX - x;
const dy = mouseY - lineY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= activeRadius) {
return i;
}
}
return -1;
}
// Converte coordinate mouse rispetto al canvas
function getMousePos(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
// Eventi mouse
canvas.addEventListener(´mousemove´, (event) => {
const pos = getMousePos(event);
hoverIndex = getHoveredIndex(pos.x, pos.y);
canvas.style.cursor = hoverIndex !== -1 ? ´pointer´ : ´default´;
drawTimeline();
});
canvas.addEventListener(´click´, (event) => {
const pos = getMousePos(event);
const index = getHoveredIndex(pos.x, pos.y);
if (index !== -1) {
selectedIndex = index;
drawTimeline();
}
});
// Primo render
drawTimeline(); Per testare il codice, ti basta avere un elemento <canvas> con id timelineCanvas e dimensioni adeguate, ad esempio 900x380. Il codice si occupa di tutto il rendering e dell’interazione.
Spiegazione
Il cuore di questo esempio è la combinazione tra disegno statico e interazione. Canvas non conserva automaticamente gli oggetti disegnati: ogni volta che qualcosa cambia, devi cancellare e ridisegnare la scena. Per questo la funzione drawTimeline() esegue sempre il rendering completo.
1. Dati dell’interfaccia
La timeline è costruita a partire da un array di eventi. Ogni evento contiene:
- time: l’orario o riferimento temporale
- title: un’etichetta breve
- detail: una descrizione più estesa
Questa struttura è semplice ma già molto utile, perché separa i dati dalla grafica. Se in futuro volessi caricare gli eventi da un’API, basterebbe sostituire l’array statico con dati dinamici.
2. Calcolo delle posizioni
La funzione getEventX(index) distribuisce i punti in modo uniforme lungo la linea. Invece di fissare coordinate manuali, calcola lo spazio disponibile e divide la larghezza utile per il numero di intervalli. Questo approccio rende il layout più pulito e adattabile.
3. Rendering del canvas
La funzione drawTimeline() segue una sequenza precisa:
- pulisce il canvas con
clearRect() - disegna lo sfondo
- scrive titolo e istruzioni
- disegna la linea orizzontale
- disegna i punti con stati diversi: normale, hover, selezionato
- mostra il pannello di dettaglio dell’evento selezionato
La distinzione tra hover e selected è importante perché migliora l’usabilità. L’hover comunica che il punto è cliccabile, mentre la selezione indica quale evento è attivo.
4. Rilevamento del mouse
Per rendere la timeline interattiva, il codice usa due funzioni:
getMousePos()converte le coordinate del mouse in coordinate relative al canvasgetHoveredIndex()verifica se il mouse si trova vicino a uno dei punti
Questo secondo passaggio usa la distanza euclidea tra il mouse e il centro del punto. Se la distanza è minore o uguale al raggio attivo, consideriamo l’evento “sotto il cursore”.
5. Pannello dettagli
Il pannello inferiore mostra il contenuto dell’evento selezionato. Qui viene usata anche una piccola utility, wrapText(), per andare a capo automaticamente. È una funzione molto utile in Canvas, perché il testo non si adatta da solo alla larghezza disponibile.
Questo dettaglio rende l’interfaccia più leggibile e professionale, soprattutto quando le descrizioni sono lunghe o provengono da dati reali.
Best practice
- Separare dati e rendering: tieni gli eventi in una struttura dedicata e concentra il disegno in una sola funzione.
- Ridisegnare solo quando necessario: in questo esempio il canvas viene ridisegnato su hover e click, ma in progetti più grandi conviene ottimizzare gli aggiornamenti.
- Usare coordinate relative: calcola la posizione del mouse con
getBoundingClientRect(), così il canvas funziona bene anche se viene spostato nella pagina. - Curare gli stati visivi: differenzia chiaramente normale, hover e selezione con colori e dimensioni diverse.
- Gestire testi lunghi: Canvas non fa wrapping automatico, quindi crea utility dedicate come
wrapText(). - Preparare il layout per la responsività: se il canvas cambia dimensione, ricalcola posizioni e proporzioni prima del rendering.
Un altro consiglio importante: se vuoi supportare schermi ad alta densità, come i display Retina, conviene gestire il device pixel ratio per evitare testi sfocati. È un passo in più, ma migliora molto la qualità visiva.
Riepilogo
In questo tutorial hai visto come usare Canvas HTML5 per creare una linea del tempo interattiva in 2D. L’esempio ha mostrato i concetti fondamentali della grafica Canvas:
- disegno di forme base con
arc(),moveTo()elineTo() - gestione del testo con
fillText() - interazione con mouse tramite eventi DOM
- ricalcolo e ridisegno dell’interfaccia in risposta alle azioni dell’utente
Questa tecnica è molto versatile. Puoi adattarla per creare roadmap di progetto, cronologie di eventi, step di onboarding, percorsi di spedizione o registri attività. Il punto forte di Canvas è che ti consente di costruire UI grafiche molto personalizzate, mantenendo il pieno controllo su ogni pixel.
Approfondisci con risorse ufficiali
- MDN Web Docs - Canvas API: documentazione completa sulle API 2D del canvas.
- MDN Web Docs - Using canvas: guida introduttiva con esempi pratici.
- WHATWG HTML Standard: specifica ufficiale del tag
<canvas>e del contesto 2D. - MDN Web Docs - CanvasRenderingContext2D: riferimento dei metodi come
fillRect,strokeText,arc,measureText.
Se vuoi fare un passo successivo, prova a trasformare questa timeline in un componente con zoom, drag orizzontale o caricamento dati da JSON. È un ottimo esercizio per consolidare sia Canvas sia la gestione dell’interazione nel browser.
