Introduzione
HTML5 Canvas non serve soltanto per disegnare forme o animazioni: può diventare anche un componente grafico interattivo. In questo tutorial realizzeremo un selettore colori 2D, simile a quelli presenti nei programmi di grafica. L’utente potrà scegliere una tonalità su una barra orizzontale e modificare saturazione e luminosità all’interno di un’area quadrata.
Il progetto utilizza esclusivamente HTML, JavaScript e l’API Canvas 2D. Imparerai a:
- disegnare gradienti complessi con
createLinearGradient(); - convertire le coordinate del mouse in coordinate relative al canvas;
- gestire gli eventi
pointerdownepointermove; - convertire un colore dal formato HSL al formato RGB;
- mostrare il colore selezionato in formato HEX.
Questo esempio è utile in editor grafici, pannelli di personalizzazione, strumenti per il web design e interfacce per la scelta di temi cromatici.
Codice completo
<div id="color-picker">
<h2>Selettore colori</h2>
<canvas id="saturationCanvas" width="320" height="220">
Il browser non supporta Canvas.
</canvas>
<canvas id="hueCanvas" width="320" height="28"></canvas>
<div id="preview"
style="width:80px; height:50px; margin-top:12px;
border:1px solid #222; border-radius:6px;">
</div>
<p>
Colore selezionato:
<strong id="hexValue">#ff0000</strong>
</p>
</div>
<script>
const saturationCanvas =
document.getElementById("saturationCanvas");
const hueCanvas =
document.getElementById("hueCanvas");
const saturationContext =
saturationCanvas.getContext("2d");
const hueContext =
hueCanvas.getContext("2d");
const preview =
document.getElementById("preview");
const hexValue =
document.getElementById("hexValue");
let hue = 0;
let saturation = 100;
let lightness = 50;
function drawHueBar() {
const gradient = hueContext.createLinearGradient(
0, 0,
hueCanvas.width, 0
);
// La barra contiene l´intero spettro delle tonalità HSL.
for (let i = 0; i <= 360; i += 60) {
gradient.addColorStop(
i / 360,
`hsl(${i}, 100%, 50%)`
);
}
hueContext.fillStyle = gradient;
hueContext.fillRect(
0, 0,
hueCanvas.width,
hueCanvas.height
);
}
function drawSaturationArea() {
// Primo gradiente: da bianco al colore puro.
const saturationGradient =
saturationContext.createLinearGradient(
0, 0,
saturationCanvas.width, 0
);
saturationGradient.addColorStop(
0,
"hsl(" + hue + ", 0%, 100%)"
);
saturationGradient.addColorStop(
1,
"hsl(" + hue + ", 100%, 50%)"
);
saturationContext.fillStyle = saturationGradient;
saturationContext.fillRect(
0, 0,
saturationCanvas.width,
saturationCanvas.height
);
// Secondo gradiente: trasparente in alto,
// nero progressivo verso il basso.
const lightnessGradient =
saturationContext.createLinearGradient(
0, 0,
0, saturationCanvas.height
);
lightnessGradient.addColorStop(
0,
"rgba(0, 0, 0, 0)"
);
lightnessGradient.addColorStop(
1,
"rgba(0, 0, 0, 1)"
);
saturationContext.fillStyle = lightnessGradient;
saturationContext.fillRect(
0, 0,
saturationCanvas.width,
saturationCanvas.height
);
}
function hslToRgb(h, s, l) {
s /= 100;
l /= 100;
const chroma = (1 - Math.abs(2 * l - 1)) * s;
const second = chroma * (1 - Math.abs((h / 60) % 2 - 1));
const match = l - chroma / 2;
let red = 0;
let green = 0;
let blue = 0;
if (h < 60) {
red = chroma;
green = second;
} else if (h < 120) {
red = second;
green = chroma;
} else if (h < 180) {
green = chroma;
blue = second;
} else if (h < 240) {
green = second;
blue = chroma;
} else if (h < 300) {
red = second;
blue = chroma;
} else {
red = chroma;
blue = second;
}
return [
Math.round((red + match) * 255),
Math.round((green + match) * 255),
Math.round((blue + match) * 255)
];
}
function rgbToHex(red, green, blue) {
return "#" + [red, green, blue]
.map(value => value.toString(16).padStart(2, "0"))
.join("");
}
function updateColor() {
const [red, green, blue] =
hslToRgb(hue, saturation, lightness);
const hex = rgbToHex(red, green, blue);
preview.style.backgroundColor = hex;
hexValue.textContent = hex;
}
function selectHue(event) {
const rectangle =
hueCanvas.getBoundingClientRect();
const x = event.clientX - rectangle.left;
hue = Math.max(
0,
Math.min(360, (x / rectangle.width) * 360)
);
drawSaturationArea();
updateColor();
}
function selectSaturation(event) {
const rectangle =
saturationCanvas.getBoundingClientRect();
const x = event.clientX - rectangle.left;
const y = event.clientY - rectangle.top;
saturation = Math.max(
0,
Math.min(100, (x / rectangle.width) * 100)
);
lightness = Math.max(
0,
Math.min(100, 100 - (y / rectangle.height) * 100)
);
updateColor();
}
hueCanvas.addEventListener(
"pointerdown",
selectHue
);
saturationCanvas.addEventListener(
"pointerdown",
selectSaturation
);
drawHueBar();
drawSaturationArea();
updateColor();
</script> Spiegazione
La barra delle tonalità
La funzione drawHueBar() crea un gradiente orizzontale. Il ciclo inserisce diversi punti di colore usando la funzione CSS hsl(). Il primo valore, chiamato hue, rappresenta la tonalità e varia da 0 a 360 gradi.
Il metodo addColorStop() stabilisce quale colore deve comparire in una determinata posizione del gradiente. Per esempio, la posizione 0 corrisponde all’inizio della barra, mentre 1 corrisponde alla fine.
L’area di saturazione e luminosità
L’area principale è ottenuta sovrapponendo due gradienti. Il primo va dal bianco al colore puro e controlla la saturazione. Il secondo è verticale e applica una quantità crescente di nero, modificando la luminosità.
Il metodo fillRect() disegna ciascun gradiente sull’intera superficie del canvas. Poiché il secondo gradiente contiene trasparenza, il colore sottostante rimane visibile nella parte superiore.
Coordinate del puntatore
getBoundingClientRect() restituisce la posizione del canvas nella pagina. Sottraendo rectangle.left e rectangle.top dalle coordinate del puntatore otteniamo la posizione interna al canvas.
La coordinata orizzontale viene trasformata in una percentuale per calcolare la saturazione. La coordinata verticale viene invertita per ottenere una luminosità maggiore nella parte alta dell’area.
Conversione HSL-RGB
HSL è comodo per costruire il selettore, mentre RGB è utile per rappresentare il colore in JavaScript. La funzione hslToRgb() esegue la conversione matematica e restituisce tre valori compresi tra 0 e 255.
Successivamente, rgbToHex() converte i valori RGB in una stringa esadecimale, come #3366cc. Il colore viene quindi applicato al riquadro di anteprima tramite style.backgroundColor.
Best practice
- Usa
pointerdowninvece di gestire separatamente mouse e touchscreen. - Controlla sempre i valori con
Math.max()eMath.min()per evitare coordinate fuori dal canvas. - Se il selettore deve funzionare anche durante il trascinamento, aggiungi gli eventi
pointermovee verifica se il pulsante del puntatore è attivo. - Per canvas ridimensionabili, considera il rapporto tra la dimensione CSS e la dimensione interna del canvas.
- Evita di ricreare elementi HTML inutilmente: aggiorna soltanto il testo e il colore che sono cambiati.
- Fornisci sempre un’alternativa accessibile, ad esempio un campo
input type="color"o un valore HEX modificabile.
Riepilogo
Canvas permette di creare componenti grafici personalizzati senza dipendere da librerie esterne. In questo progetto abbiamo combinato gradienti, eventi del puntatore, coordinate relative e conversioni cromatiche per realizzare un selettore colori completo.
Il concetto può essere esteso aggiungendo un cursore circolare, campi per RGB e HEX, copia negli appunti, salvataggio dei colori preferiti e supporto al trascinamento continuo. La stessa tecnica è applicabile a palette, strumenti di disegno e pannelli di configurazione visuale.
Approfondisci con risorse ufficiali
- MDN Canvas API: documentazione completa del contesto di disegno 2D.
- MDN CanvasRenderingContext2D: metodi per gradienti, rettangoli, percorsi e trasformazioni.
- MDN Pointer Events: guida alla gestione unificata di mouse, penne digitali e touchscreen.
- W3C Canvas 2D Context: specifica tecnica dello standard Canvas.
- MDN CSS Color: approfondimento sui formati RGB, HSL, HEX e sulle funzioni colore CSS.
