Corso · 10 lezioni da 2 ore
Da zero a una mano che ti imita
Un corso di TypeScript per chi ha visto poco codice. Si parte dalla prima variabile e si arriva a un'applicazione vera: la webcam guarda la tua mano e il computer la ridisegna sullo schermo, dito per dito.
Come è fatto questo corso
Ogni lezione dura 120 minuti ed è sempre divisa allo stesso modo: si spiega poco e si prova molto. Nessuna lezione è solo teoria, e nessun esercizio chiede cose non ancora spiegate.
Il filo conduttore è uno solo: ogni costrutto del linguaggio si impara perché serve alla mano. Le variabili servono a tenere una coordinata, gli array a tenere 21 punti, le interfacce a dire com'è fatto un punto, le funzioni asincrone ad accendere la webcam. Nessun esercizio astratto senza un perché.
docs/COME-FUNZIONA.md. Servono come soluzione di riferimento e come traguardo
da mostrare agli studenti il primo giorno: vedere dove si va a parare cambia la motivazione
di tutte le venti ore.
Dove scrivono gli studenti
Le prime cinque lezioni non richiedono di installare niente: si lavora in ambienti che si aprono nel browser e funzionano senza registrazione. Dalla lezione 6 si passa al computer, perché la webcam e il progetto vero vogliono un progetto locale.
| Ambiente | Quando | Cosa offre | Account |
|---|---|---|---|
TypeScript Playgroundtypescriptlang.org/play |
Lezioni 1-4 | Il playground ufficiale del linguaggio: scrivi TypeScript a sinistra, vedi gli errori mentre scrivi, il JavaScript prodotto e i console.log a destra. Il link contiene il codice, quindi il docente può distribuire un esercizio incollando un indirizzo. |
No |
| vite.new/vanilla-ts (StackBlitz) |
Lezioni 5-6 | Apre un progetto Vite + TypeScript completo dentro il browser, con terminale e anteprima della pagina: è identico a quello che poi si installerà in locale, quindi il passaggio non spiazza nessuno. | No (serve solo per salvare) |
| JSFiddle | Lezione 5, prove rapide | Quattro riquadri (HTML, CSS, JS, risultato) e via: comodo per un esperimento di due minuti sul canvas senza creare un progetto. | No |
| VS Code + Node.js | Dalla lezione 6 | L'ambiente vero: editor, terminale, npm, il compilatore TypeScript. Si installa insieme in aula nella lezione 6. |
No |
vite.new/vanilla-ts installa le dipendenze e avvia il server da solo in
una decina di secondi. CodePen non è adatto: senza account risponde con un errore di accesso.
Mappa: cosa si impara e dove serve
Questa tabella è anche il modo più rapido, per il docente, di rispondere alla domanda «a cosa mi servirà?» — che con i principianti arriva sempre, ed è legittima.
| Lezione | Concetti di TypeScript | A cosa serve nella mano |
|---|---|---|
| 1 | let/const, tipi string, number, boolean, annotazioni e inferenza, console.log | Tenere una coordinata, un contatore di fotogrammi, il nome di un gesto |
| 2 | if/else, operatori, cicli for e for…of, array tipizzati, tipi unione | Scorrere i 21 punti della mano; decidere se una mano c'è o no |
| 3 | Funzioni, parametri e tipo di ritorno, void, funzioni freccia, map e filter | Calcolare la distanza fra due dita, convertire coordinate |
| 4 | Oggetti, interface, proprietà opzionali, null e undefined, ?. e ?? | Il tipo Punto {x, y, z}: il dato su cui gira tutto il progetto |
| 5 | Il DOM tipizzato, as, eventi, il canvas 2D | Disegnare punti e ossa sullo schermo |
| 6 | import/export, tsconfig.json, npm, la compilazione | Dividere il progetto in file come quello vero |
| 7 | async/await, Promise, try/catch, requestAnimationFrame | Accendere la webcam e girare 30 volte al secondo |
| 8 | Librerie esterne, tipi generici (leggeri), restringimento dei tipi | Chiedere a MediaPipe dove sono le dita e disegnarle |
| 9 | class, this, campi privati, tipi unione come stato | Filtro anti-tremolio, gesti, pulsanti accendi/spegni |
| 10 | Ripasso, cenni al 3D, come si mette online | La mano in tre dimensioni e il progetto presentato |
Il programma, lezione per lezione
- 01Il primo programma: variabili e tipiPlayground
- 02Decidere e ripetere:
if, cicli, arrayPlayground - 03Funzioni: dare un nome a un pezzo di lavoroPlayground
- 04Oggetti e interfacce: il tipo
PuntoPlayground - 05La pagina e il canvas: disegnare una mano fintaStackBlitz
- 06Sul proprio computer: Node, Vite, moduliLocale
- 07La webcam:
async, permessi, fotogrammiLocale - 08MediaPipe: i 21 punti della mano veraLocale
- 09Rifinire: classi, stato, gesti, tremolioLocale
- 10La terza dimensione, la pubblicazione, le presentazioniLocale
Il primo programma: variabili e tipi
Cosa succede davvero quando un programma «gira», come si tiene da parte un valore, e perché TypeScript ti sgrida prima di eseguire il codice invece che dopo.
Teoria
Un programma è un elenco di istruzioni che il computer esegue una dopo l'altra, dall'alto verso il basso. L'istruzione più utile all'inizio è quella che scrive un messaggio:
console.log("ciao");
console.log("mondo");
Due righe, due messaggi, in quest'ordine. Non c'è magia: se inverti le righe, si inverte il risultato.
Una variabile è un nome dato a un valore, per poterlo riusare. In TypeScript se ne dichiarano di due tipi:
let punteggio = 0; // potrà cambiare
const nome = "Giulia"; // non cambierà mai
punteggio = punteggio + 10; // ok
nome = "Marco"; // ERRORE: const non si può riassegnare
const sempre, e si passa a let solo quando ci si accorge
che il valore deve cambiare. Non è pignoleria: leggendo il codice, const dice
«questo qui non ti sorprenderà».
Ogni valore ha un tipo. I tre che servono oggi:
string (testo, fra virgolette), number (numeri, interi o con la
virgola — che in programmazione è un punto) e boolean (solo
true o false).
const nome: string = "Giulia";
const eta: number = 17;
const maggiorenne: boolean = false;
Quel : string è l'annotazione di tipo: è la parte che in
JavaScript non esiste ed è il motivo per cui questo linguaggio si chiama
TypeScript. Spesso si può omettere, perché il compilatore capisce il tipo da solo
guardando il valore (si dice che lo inferisce):
const nome = "Giulia"; // TypeScript sa già che è una string
nome.toUpperCase(); // infatti qui ti suggerisce i metodi del testo
E a cosa serve tutto questo? A far comparire gli errori mentre scrivi, non mentre il programma gira davanti agli utenti:
let eta = 17;
eta = "diciassette";
// Errore: Type 'string' is not assignable to type 'number'.
// Tradotto: "avevi detto che qui ci va un numero, e questo è testo".
Ultimo pezzo: unire testo e valori. Con gli apici inclinati ` (backtick) si
scrivono stringhe in cui si possono infilare valori con ${…}:
const dita = 5;
console.log(`la mano ha ${dita} dita`);
console.log(`e ${dita * 4} falangi... circa`);
Demo del docente (15')
- Apri la mano finita e muovi la mano davanti alla webcam. Dì: «fra dieci lezioni lo scrivete voi».
- Apri il Playground e scrivi tre righe con
console.log. - Dichiara
const punta = 8;e spiega che quel numero, nel progetto finale, sarà la punta dell'indice. - Sbaglia apposta un tipo e leggi l'errore insieme alla classe.
Laboratorio (60')
Esercizio 1.1 — Il primo messaggio base
Apri typescriptlang.org/play, cancella tutto quello che c'è e fai scrivere
al programma il tuo nome e la tua età, su due righe separate.
Soluzione
console.log("Giulia");
console.log(17);
Va bene anche con le variabili — anzi, meglio: è il passo successivo.
Esercizio 1.2 — Tre variabili base
Crea tre variabili: il tuo nome (testo), quanti anni hai (numero), se ti piace la pizza (booleano). Poi stampa una frase sola che le contenga tutte e tre, usando i backtick.
Soluzione
const nome = "Giulia";
const eta = 17;
const amaLaPizza = true;
console.log(`${nome} ha ${eta} anni e la pizza le piace: ${amaLaPizza}`);
Esercizio 1.3 — Rompilo apposta base
Prendi l'esercizio precedente e prova a fare queste tre cose, una alla volta, leggendo ogni volta l'errore che compare:
- assegna un testo alla variabile dell'età;
- riassegna un valore a una variabile dichiarata con
const; - scrivi
console.log(nomeSbagliato), con un nome che non esiste.
Scrivi su un foglio, con parole tue, cosa dice il computer nei tre casi.
Cosa devono aver capito
- Type 'string' is not assignable to type 'number' → il tipo non corrisponde a quello dichiarato o dedotto.
- Cannot assign to 'nome' because it is a constant →
constnon si riassegna. - Cannot find name 'nomeSbagliato' → quella variabile non esiste: quasi sempre è un errore di battitura.
Il punto dell'esercizio non è il codice: è imparare che l'errore dice dove e cosa, e che leggerlo costa meno che indovinare.
Esercizio 1.4 — Un po' di conti medio
La webcam del progetto finale lavora a 640 pixel di larghezza e 480 di altezza, e produce 30 fotogrammi al secondo. Dichiara le tre variabili con il tipo giusto e stampa:
- quanti pixel ha un fotogramma;
- quanti fotogrammi arrivano in un minuto;
- quante volte al secondo il programma dovrà disegnare (uguale ai fotogrammi).
Soluzione
const larghezza: number = 640;
const altezza: number = 480;
const fotogrammiAlSecondo: number = 30;
console.log(`un fotogramma ha ${larghezza * altezza} pixel`);
console.log(`in un minuto arrivano ${fotogrammiAlSecondo * 60} fotogrammi`);
console.log(`il disegno gira ${fotogrammiAlSecondo} volte al secondo`);
307.200 pixel per fotogramma, 1800 fotogrammi al minuto: numeri utili da avere in testa quando, alla lezione 7, si parlerà di velocità.
Esercizio 1.5 — Un punto sullo schermo sfida
Nel progetto finale ogni punto della mano ha due coordinate fra 0 e 1: 0 è
il bordo sinistro (o alto) dell'immagine, 1 il bordo destro (o basso).
Dichiara x = 0.5 e y = 0.25 e stampa a quale pixel
corrispondono su un'immagine di 640×480.
Soluzione
const x = 0.5;
const y = 0.25;
const larghezza = 640;
const altezza = 480;
console.log(`pixel x: ${x * larghezza}`); // 320
console.log(`pixel y: ${y * altezza}`); // 120
È esattamente il calcolo che farà il programma vero, moltissime volte al secondo, per ogni punto della mano. Chi finisce presto: stampare anche il punto al centro esatto dell'immagine.
Errori tipici in aula
- Virgolette sbagliate: il correttore del sistema operativo trasforma
"in“ ”. Nel Playground non succede, ma succede se qualcuno scrive il codice in un documento di testo e lo incolla. - Backtick introvabile: su tastiera italiana è Alt Gr + '. Vale la pena mostrarlo alla lavagna: senza, l'esercizio 1.2 si blocca.
- Virgola al posto del punto nei decimali:
0,5non è un numero,0.5sì. - «Non vedo niente»: il pannello dei messaggi del Playground è sulla destra, scheda Logs. Da mostrare una volta per tutte all'inizio.
Verifica veloce (5 domande)
Domande e risposte
- Qual è la differenza fra
leteconst? →constnon si può riassegnare. - Che tipo ha
"17"? →string: è fra virgolette, quindi è testo. - Cosa stampa
console.log(2 + 3)? Econsole.log("2" + "3")? →5e23: il+su testo unisce. - Perché scrivere
: numberse TypeScript lo capisce da solo? → Per dichiarare l'intenzione e far scoprire l'errore a chi scriverà dopo di noi. - Quando compare un errore di tipo: prima o dopo aver eseguito il programma? → Prima, mentre si scrive.
Compito per casa (20-30 minuti)
Scrivi un programma che descriva la tua mano: quante dita, il nome del dito più lungo,
se sei mancino. Deve usare almeno un const, un let, i tre tipi
visti e una frase con i backtick. Poi condividi il link del Playground con il docente
(pulsante Share: il codice viaggia dentro l'indirizzo, non serve un account).
Decidere e ripetere: if, cicli, array
Finora il programma andava dritto dall'alto in basso. Oggi impara a scegliere una strada invece di un'altra e a ripetere lo stesso lavoro su molti valori. Serve perché la mano non è un numero: sono ventuno punti, e vanno guardati tutti.
if · i tre modi di ripetere · array · tipi unioneTeoria
A cosa serve. Un programma che fa sempre le stesse cose nello stesso ordine non è molto utile. La mano davanti alla webcam a volte c'è e a volte no, le dita a volte sono alzate e a volte chiuse: il programma deve decidere. E siccome i punti della mano sono ventuno, scrivere ventuno volte la stessa riga non è una strada percorribile: il programma deve ripetere.
Si comincia dal confronto. Confrontare due valori produce sempre un
boolean, cioè true o false: è lo stesso tipo visto
la volta scorsa, ma questa volta non lo scriviamo a mano, lo calcola il computer.
const dita = 5;
console.log(dita === 5); // true "è uguale a"
console.log(dita !== 5); // false "è diverso da"
console.log(dita > 3); // true "maggiore di"
console.log(dita < 3); // false "minore di"
console.log(dita >= 5); // true "maggiore o uguale"
console.log(dita <= 4); // false "minore o uguale"
== sia ===, e non fanno la
stessa cosa. Il doppio uguale, prima di confrontare, converte i valori: per lui
"5" == 5 è true, anche se uno è testo e l'altro è un numero.
Il triplo uguale non converte niente: "5" === 5 è false, perché
sono di tipo diverso. Regola del corso, senza eccezioni: si usa sempre
=== e !==. Il doppio uguale produce bug che si trovano
solo a programma finito.
I confronti si combinano con gli operatori logici. Sono tre e si leggono ad alta voce come in italiano:
const x = 0.4;
console.log(x >= 0 && x <= 1); // true "e" → devono essere vere entrambe
console.log(x < 0 || x > 1); // false "o" → basta che ne sia vera una
console.log(!(x < 0)); // true "non" → capovolge il risultato
Con un valore boolean in mano si può finalmente scegliere. L'istruzione
if esegue un blocco di codice solo se la condizione è vera:
const dita = 5;
if (dita === 5) {
console.log("mano aperta");
}
Le parentesi graffe racchiudono il blocco, cioè le righe che appartengono a quel
if. Con else si dice cosa fare quando la condizione è falsa, e
con else if si mettono in fila più domande. Il computer le prova
dall'alto in basso e si ferma alla prima vera:
const dita = 2;
if (dita === 0) {
console.log("pugno");
} else if (dita === 5) {
console.log("mano aperta");
} else if (dita === 2) {
console.log("vittoria");
} else {
console.log("gesto non riconosciuto");
}
// stampa: vittoria
"None", e l'applicazione mostra una mano generica.
Passiamo al ripetere. Il modo più classico è il ciclo for.
Scritto tutto insieme fa paura, quindi si legge un pezzo alla volta:
for (let i = 0; i < 5; i++) {
console.log(i);
}
// stampa: 0 1 2 3 4
Dentro le tonde ci sono tre cose separate da punto e virgola, e ognuna ha un compito:
let i = 0— il punto di partenza. Si crea una variabile, di solito chiamatai(da indice), che vale zero.i < 5— la condizione per continuare. Prima di ogni giro il computer la controlla: se è vera fa il giro, se è falsa esce dal ciclo.i++— cosa fare alla fine di ogni giro.i++è la scrittura breve dii = i + 1.
Quindi il computer esegue: i vale 0, 0 è minore di 5, stampa, i
diventa 1… finché i vale 5, che non è minore di 5, e il ciclo finisce.
Il corpo è stato eseguito cinque volte, con i che valeva 0, 1, 2, 3, 4.
i++ — il ciclo non finisce più e la scheda del browser si blocca. Non è
grave: si chiude la scheda e si riapre il Playground, che conserva il codice
nell'indirizzo. Ma succederà a qualcuno, quindi è bene dirlo prima.
Adesso serve qualcosa su cui ripetere. Un array è un elenco ordinato di
valori, tutti dello stesso tipo. Si scrive fra parentesi quadre, e il suo tipo è il tipo
degli elementi seguito da []:
const numeri: number[] = [10, 20, 30, 40];
const nomi: string[] = ["pollice", "indice", "medio"];
console.log(numeri.length); // 4 → quanti elementi ci sono
console.log(numeri[0]); // 10 → il PRIMO elemento
console.log(numeri[3]); // 40 → l'ultimo
console.log(numeri[4]); // undefined → non esiste
Due cose vanno imparate subito, perché sono la fonte di metà degli errori dei
principianti. Primo: gli indici partono da zero. Un array di quattro
elementi ha gli indici 0, 1, 2, 3. Secondo: di conseguenza l'ultimo indice è
length - 1, mai length.
undefined
Se chiedi un elemento che non c'è, il programma non si arrabbia: restituisce
undefined, che è il valore speciale «qui non c'è niente». Nel Playground
TypeScript ti dice che numeri[4] è un number e non protesta:
si fida del fatto che tu sappia quanto è lungo l'array. Poi, all'esecuzione, salta fuori
undefined e i conti danno NaN (Not a Number, «non è un
numero»). Se vedi NaN stampato a schermo, nove volte su dieci hai letto oltre
la fine di un array.
Mettendo insieme ciclo e array si ottiene la riga più usata di tutto il corso:
const numeri: number[] = [10, 20, 30, 40];
for (let i = 0; i < numeri.length; i++) {
console.log(`l'elemento numero ${i} vale ${numeri[i]}`);
}
Si scrive i < numeri.length e non i < 4: così il codice
continua a funzionare se domani l'array cambia lunghezza. E nel progetto finale cambia
eccome, perché un array di punti può contenerne 21 (una mano) o 33 (un corpo intero).
Quando l'indice non ti serve, e ti interessa solo il valore, esiste una forma più corta e
più leggibile, for…of:
const numeri: number[] = [10, 20, 30, 40];
for (const valore of numeri) {
console.log(valore);
}
Si legge letteralmente: «per ogni valore di numeri».
Si usa const perché a ogni giro nasce una variabile nuova.
Quando usare quale: se ti basta il valore, for…of; se ti
serve anche sapere in che posizione sei — e per trovare il dito più alto serve —
allora il for classico con l'indice.
Esiste un terzo modo, while, che ripete finché una condizione resta vera
senza contatore automatico. Si usa quando non sai in partenza quanti giri serviranno:
let rimaste = 3;
while (rimaste > 0) {
console.log(`mancano ${rimaste} prove`);
rimaste = rimaste - 1;
}
Nel progetto della mano non compare mai: quando si lavora su elenchi di punti si sa sempre quanti sono. È utile conoscerlo, non è utile usarlo oggi.
Ultimo concetto, ed è quello che fa di TypeScript qualcosa di più di JavaScript: i tipi unione. A cosa servono: spesso una variabile non può contenere qualunque testo, ma solo due o tre parole precise. Dirlo al compilatore evita una intera famiglia di errori.
type Stato = "spenta" | "accesa";
let camera: Stato = "spenta";
camera = "accesa"; // ok
camera = "acesa"; // ERRORE: Type '"acesa"' is not assignable to type 'Stato'.
La barra verticale | si legge «oppure». Senza il tipo unione avresti scritto
let camera: string, e quel "acesa" con una c sola
sarebbe passato liscio: il programma avrebbe controllato camera === "accesa",
avrebbe trovato false e non avrebbe acceso niente, senza un solo messaggio
d'errore. Un pomeriggio buttato via, per una lettera.
main.ts dell'applicazione c'è esattamente questa riga:
type State = "idle" | "loading" | "running" | "error". Sono i quattro stati
possibili della pagina: spenta, in avvio, in funzione, in errore. Niente altro è ammesso,
e il compilatore lo fa rispettare.
Chiudiamo con una trappola che merita i suoi trenta secondi. const su un
array non rende l'array immutabile: impedisce solo di sostituirlo con un
altro array.
const numeri: number[] = [1, 2, 3];
numeri.push(4); // PERMESSO: l'array ora è [1, 2, 3, 4]
console.log(numeri);
numeri = [9, 9, 9]; // ERRORE: Cannot assign to 'numeri' because it is a constant.
Il motivo: const protegge il nome, non il contenuto. La variabile
numeri continuerà a indicare quell'elenco lì; cosa ci sia dentro l'elenco è
un altro discorso. push aggiunge un elemento in fondo ed è il modo normale di
costruire un array poco per volta.
Demo del docente (15')
- Scrivi un array di cinque numeri e stampalo con un
forclassico, leggendo ad alta voce i tre pezzi delle tonde. - Riscrivi lo stesso ciclo con
for…ofe chiedi alla classe cosa si è guadagnato e cosa si è perso (si è persa la posizione). - Togli
i++e mostra la scheda che si blocca. Poi rimettilo. Serve a togliere la paura: è successo, si chiude, si riapre. - Dichiara un array di 21 numeri fra 0 e 1 — «sono le posizioni orizzontali dei ventuno punti della mano» — e conta con un
ifdentro il ciclo quanti stanno sotto 0.5. - Scrivi
type Gesto = "pugno" | "mano aperta", sbaglia una lettera e mostra l'errore. È il momento in cui i tipi unione si capiscono.
Laboratorio (60')
Esercizio 2.1 — Somma e media base
Dato l'array qui sotto, calcola e stampa la somma di tutti i numeri e la loro media. Copia il codice di partenza e completa.
const numeri: number[] = [12, 7, 30, 4, 22];
let somma = 0;
// scrivi qui il ciclo che aggiunge ogni numero a `somma`
console.log(`somma: ${somma}`);
Soluzione
const numeri: number[] = [12, 7, 30, 4, 22];
let somma = 0;
for (const valore of numeri) {
somma = somma + valore;
}
const media = somma / numeri.length;
console.log(`somma: ${somma}`); // 75
console.log(`media: ${media}`); // 15
Perché così. La variabile somma è dichiarata
fuori dal ciclo e con let: dentro il ciclo nascerebbe e morirebbe
a ogni giro, e alla fine non ci sarebbe più niente da stampare. Per la media si divide
per numeri.length e non per 5: se domani aggiungi un numero, il codice è
già giusto.
Esercizio 2.2 — La coordinata è dentro l'immagine? base
Nel progetto finale ogni punto della mano arriva con due coordinate comprese fra 0 e 1:
0 è il bordo sinistro (o alto) dell'immagine, 1 il bordo destro
(o basso). Ogni tanto, quando la mano esce dall'inquadratura, arriva un valore fuori da
quell'intervallo.
Dichiara const x = 0.42; e scrivi un if che stampi
"punto dentro l'immagine" se x sta fra 0 e 1 (estremi
compresi) e "punto fuori dall'immagine" altrimenti. Poi prova a cambiare
x in 1.3 e in -0.2 e controlla che risponda bene
in tutti e tre i casi.
Soluzione
const x = 0.42;
if (x >= 0 && x <= 1) {
console.log("punto dentro l'immagine");
} else {
console.log("punto fuori dall'immagine");
}
Perché così. Servono due confronti uniti da &&,
perché devono essere vere entrambe le condizioni. Attenzione all'errore classico:
0 <= x <= 1 in italiano si dice, ma nel codice non funziona —
il computer calcola prima 0 <= x, ottiene un booleano, e poi confronta
un booleano con 1. Risultato senza senso, e il compilatore infatti protesta.
Esercizio 2.3 — Quante dita sono alzate medio
Il progetto finale, per riconoscere un gesto, parte da cinque risposte sì/no: pollice
alzato? indice alzato? e così via. Le teniamo in un array di cinque boolean,
in ordine: pollice, indice, medio, anulare, mignolo.
Conta quante dita sono alzate e stampa il numero. Poi, con un if,
aggiungi una frase: 0 dita è un pugno, 5 dita è una mano aperta, qualunque altro numero
è «altro».
const alzate: boolean[] = [false, true, true, false, false];
Soluzione
const alzate: boolean[] = [false, true, true, false, false];
let quante = 0;
for (const dito of alzate) {
if (dito === true) {
quante = quante + 1;
}
}
console.log(`dita alzate: ${quante}`); // 2
if (quante === 0) {
console.log("pugno");
} else if (quante === 5) {
console.log("mano aperta");
} else {
console.log("altro");
}
Perché così. if (dito === true) si può scrivere anche
solo if (dito), perché dito è già un booleano: la forma corta
è quella che troverai nel codice vero. Qui si è scelta quella lunga perché all'inizio
è più chiara — l'importante è sapere che sono la stessa cosa.
Esercizio 2.4 — Il massimo e la sua posizione medio
Trova il valore più grande dell'array e anche a quale indice si trova. Stampa tutti e due.
const numeri: number[] = [12, 7, 30, 4, 22];
for con l'indice e non for…of.
Soluzione
const numeri: number[] = [12, 7, 30, 4, 22];
let massimo = numeri[0];
let indiceDelMassimo = 0;
for (let i = 1; i < numeri.length; i++) {
if (numeri[i] > massimo) {
massimo = numeri[i];
indiceDelMassimo = i;
}
}
console.log(`il massimo è ${massimo}, alla posizione ${indiceDelMassimo}`);
// il massimo è 30, alla posizione 2
Perché così. Si parte dal primo elemento come campione da battere e
il ciclo comincia da i = 1: confrontare il primo con se stesso sarebbe
inutile. Le due variabili si aggiornano sempre insieme, dentro lo stesso
if: se si aggiornasse solo il valore, l'indice resterebbe quello di prima
e la risposta sarebbe sbagliata in modo silenzioso.
Un errore molto comune è partire da let massimo = 0. Funziona con questi
numeri, ma se l'array contenesse solo numeri negativi la risposta sarebbe 0, che
nell'array non c'è nemmeno. Partire dal primo elemento è sempre corretto.
Esercizio 2.5 — I punti nella metà sinistra sfida
Ecco le ventuno posizioni orizzontali dei punti di una mano, come le produrrebbe il riconoscitore: numeri fra 0 (bordo sinistro dell'immagine) e 1 (bordo destro).
const x: number[] = [
0.50, 0.42, 0.36, 0.33, 0.30,
0.44, 0.42, 0.41, 0.40,
0.51, 0.51, 0.51, 0.51,
0.57, 0.58, 0.59, 0.60,
0.63, 0.66, 0.68, 0.70,
];
Scrivi un programma che stampi:
- quanti punti stanno nella metà sinistra dell'immagine (cioè hanno
xminore di 0.5); - quanti nella metà destra;
- una frase che dica dove sta la mano: «più a sinistra», «più a destra» o «al centro» se i due conteggi sono uguali.
Controlla anche che i due conteggi sommati facciano 21: è un modo rapido di accorgersi di aver saltato un elemento.
Soluzione
const x: number[] = [
0.50, 0.42, 0.36, 0.33, 0.30,
0.44, 0.42, 0.41, 0.40,
0.51, 0.51, 0.51, 0.51,
0.57, 0.58, 0.59, 0.60,
0.63, 0.66, 0.68, 0.70,
];
let sinistra = 0;
let destra = 0;
for (const valore of x) {
if (valore < 0.5) {
sinistra = sinistra + 1;
} else {
destra = destra + 1;
}
}
console.log(`punti totali: ${x.length}`); // 21
console.log(`a sinistra: ${sinistra}`); // 8
console.log(`a destra: ${destra}`); // 13
console.log(`controllo: ${sinistra + destra}`); // 21
if (sinistra > destra) {
console.log("la mano sta più a sinistra");
} else if (destra > sinistra) {
console.log("la mano sta più a destra");
} else {
console.log("la mano sta al centro");
}
Perché così. Usando if/else ogni punto
finisce in uno e un solo conteggio: per questo la somma deve fare per forza 21.
Il valore esattamente uguale a 0.5 (il polso) finisce a destra, perché la condizione è
< 0.5: è una scelta, va bene, ma va fatta apposta e non per caso.
Chi finisce presto: stampa anche la posizione orizzontale media dei ventuno punti, riusando il metodo dell'esercizio 2.1. È il modo più semplice di dire «dove sta il centro della mano», e nella lezione 4 lo si riscriverà per davvero.
Errori tipici in aula
- Confondere
=e===:if (dita = 5)non confronta, assegna. Il compilatore in questo caso aiuta e segnala l'errore, ma il messaggio non è intuitivo: vale la pena mostrarlo prima che lo incontrino da soli. - Indice che parte da 1:
for (let i = 1; i <= numeri.length; i++)salta il primo elemento e legge uno che non esiste. Doppio danno in una riga sola. - La variabile dichiarata dentro il ciclo:
let somma = 0scritto dentro le graffe si azzera a ogni giro, e alla fine stampa solo l'ultimo valore. - Graffe dimenticate:
if (x > 0) console.log("a"); console.log("b");stampa sempre"b". Regola del corso: le graffe si mettono sempre, anche per una riga sola. NaNstampato a schermo: quasi sempre è unnumeri[i]letto oltre la fine dell'array, oppure una divisione per unlengthche vale zero.- Ciclo infinito: la scheda si blocca. Si chiude, si riapre il Playground, il codice è nell'indirizzo. Nessun dramma.
Verifica veloce (5 domande)
Domande e risposte
- Quanti giri fa
for (let i = 0; i < 3; i++)? → Tre: coniuguale a 0, 1 e 2. - Qual è l'ultimo indice valido di un array di 21 elementi? → 20, cioè
length - 1. - Perché si scrive
===e non==? → Perché il doppio uguale converte i tipi prima di confrontare, e"5" == 5risulta vero. Il triplo uguale confronta anche il tipo. - Cosa stampa
console.log([1, 2, 3][5])? →undefined: quell'elemento non esiste. - Si può fare
pushsu un array dichiarato conconst? → Sì.constimpedisce di sostituire l'array, non di modificarne il contenuto.
Compito per casa (30 minuti)
Scrivi un programma che, partendo dall'array x dell'esercizio 2.5, stampi
il punto più a destra (valore e posizione) e il punto più a
sinistra, e dica se la mano è larga o stretta: chiama «larga» una mano in cui la
differenza fra il massimo e il minimo supera 0.35, «stretta» altrimenti.
Dichiara la risposta in una variabile con il tipo type Larghezza = "larga" |
"stretta". Condividi il link del Playground con il docente.
Funzioni: dare un nome a un pezzo di lavoro
Nella lezione scorsa hai scritto tre volte quasi lo stesso ciclo. Oggi impari a scriverlo una volta sola, dargli un nome e richiamarlo quando serve. È il passo che trasforma un elenco di istruzioni in un programma che si può leggere.
void · freccia · map e filterdistanza() e la usa per misurare quanto è grande una manoTeoria
A cosa serve. Due motivi, in ordine di importanza. Il primo è che dare
un nome a un pezzo di lavoro lo rende comprensibile: distanza(a, b) si capisce
al volo, quattro righe di radici quadrate no. Il secondo è che una cosa scritta in un
punto solo si corregge in un punto solo. Se la stessa formula è copiata in cinque punti,
prima o poi quattro sono aggiornati e uno no.
Una funzione è un blocco di codice con un nome, che riceve dei valori in ingresso e ne restituisce uno in uscita. Si dichiara così:
function raddoppia(numero: number): number {
return numero * 2;
}
console.log(raddoppia(5)); // 10
console.log(raddoppia(21)); // 42
Pezzo per pezzo, perché ogni parte ha un nome che poi il docente userà:
function— la parola che annuncia «sto per dichiarare una funzione».raddoppia— il nome. Si sceglie un verbo, perché una funzione fa qualcosa.numero: number— il parametro, con il suo tipo. È una variabile che esiste solo dentro la funzione.: numberdopo le tonde — il tipo di ritorno: cosa esce.return— l'istruzione che consegna il risultato e termina subito la funzione.
raddoppia(5) è la chiamata: il 5 entra, prende il posto di
numero, e la funzione restituisce 10. Il valore restituito si può stampare,
mettere in una variabile, o darlo in pasto a un'altra funzione.
return chiude la porta
Tutto quello che sta dopo un return eseguito non viene mai eseguito. Non è un
difetto, è la cosa utile: if (dimensione === 0) return 0; all'inizio di una
funzione è il modo standard di sbrigare subito i casi strani e poi scrivere il caso normale
senza annidare cinque if uno dentro l'altro.
I parametri possono essere più di uno, separati da virgola, ognuno con il proprio tipo:
function area(larghezza: number, altezza: number): number {
return larghezza * altezza;
}
console.log(area(640, 480)); // 307200
L'ordine conta. area(640, 480) e area(480, 640)
qui danno lo stesso risultato per caso, ma in generale no: il primo valore finisce nel
primo parametro, punto.
Alcune funzioni non restituiscono niente: stampano, disegnano, accendono. Il loro tipo di
ritorno si scrive void, che significa «vuoto»:
function saluta(nome: string): void {
console.log(`ciao ${nome}`);
}
saluta("Giulia"); // stampa: ciao Giulia
const risultato = saluta("Marco");
console.log(risultato); // undefined: non c'era niente da restituire
void. Nel progetto finale stop(), che spegne tutto, è
void; realSide(), che risponde «è la destra o la sinistra»,
restituisce un valore.
Esiste una seconda scrittura, più breve, chiamata funzione freccia. Fa esattamente la stessa cosa:
const raddoppia = (numero: number): number => {
return numero * 2;
};
// e quando il corpo è una sola espressione, si può togliere anche return e graffe:
const triplica = (numero: number): number => numero * 3;
console.log(raddoppia(5)); // 10
console.log(triplica(5)); // 15
La funzione viene messa in una const come fosse un valore qualsiasi — perché
lo è. Questa forma serve soprattutto quando una funzione va passata a qualcun
altro, che è il prossimo argomento.
I parametri possono avere un valore predefinito, usato quando chi chiama non lo passa:
function aPixel(valore: number, dimensione: number = 640): number {
return valore * dimensione;
}
console.log(aPixel(0.5)); // 320 → usa 640
console.log(aPixel(0.5, 480)); // 240 → usa quello che gli passi
Oppure possono essere opzionali, con un punto interrogativo. In quel caso,
se non vengono passati, valgono undefined, e la funzione deve prevederlo:
function etichetta(nome: string, gesto?: string): string {
if (gesto === undefined) {
return nome;
}
return `${nome}: ${gesto}`;
}
console.log(etichetta("Destra")); // Destra
console.log(etichetta("Destra", "pugno")); // Destra: pugno
Le funzioni possono chiamarne altre, e questo è il modo normale di costruire programmi: si parte da pezzi piccoli e sicuri e si compongono.
function distanza(x1: number, y1: number, x2: number, y2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
function dimensioneMano(xPolso: number, yPolso: number, xNocca: number, yNocca: number): number {
return distanza(xPolso, yPolso, xNocca, yNocca);
}
console.log(dimensioneMano(0.50, 0.88, 0.51, 0.58)); // circa 0.3002
Math è un insieme di strumenti matematici già pronti che il linguaggio mette
a disposizione: Math.sqrt è la radice quadrata, Math.abs il
valore assoluto, Math.round l'arrotondamento, Math.min e
Math.max il più piccolo e il più grande fra due numeri.
Ultimo pezzo di oggi: map e filter. Sono due
strumenti degli array che prendono una funzione come argomento. Sembra strano la
prima volta, quindi conviene leggerli come frasi in italiano:
const numeri: number[] = [1, 2, 3, 4];
// map: "fammi un array NUOVO, applicando questa funzione a ogni elemento"
const doppi = numeri.map((n) => n * 2); // [2, 4, 6, 8]
// filter: "fammi un array NUOVO con i soli elementi per cui questa funzione dice true"
const grandi = numeri.filter((n) => n > 2); // [3, 4]
console.log(doppi);
console.log(grandi);
console.log(numeri); // [1, 2, 3, 4] — l'originale non è stato toccato
Entrambi non modificano l'array di partenza: ne restituiscono uno nuovo.
Sono la scrittura breve di un ciclo for che costruisce un array con
push, e nel codice vero si usano quasi sempre al posto di quel ciclo perché
dicono in una riga quale sia l'intenzione.
Chiudiamo con un'idea, non con una sintassi. Una funzione si dice pura
quando, dati gli stessi ingressi, restituisce sempre la stessa uscita e non tocca niente
fuori da sé. distanza è pura: con quei quattro numeri risponderà sempre
uguale, oggi e fra un mese. saluta non lo è, perché stampa — cambia qualcosa
nel mondo esterno. Le funzioni pure sono più facili da provare e non riservano sorprese:
quando puoi scriverne una, scrivila.
Demo del docente (15')
- Prendi il codice dell'esercizio 2.1 (somma e media) e trasformalo in una funzione
mediaDi. Chiamala due volte su due array diversi: è il momento in cui si vede il guadagno. - Scrivi
distanzadal vivo, conMath.sqrt, spiegando che è il teorema di Pitagora e nient'altro. - Mostra
Math.hypot(dx, dy), che fa lo stesso in una riga, e di' che nel progetto vero si usa quella. - Calcola la distanza fra polso
(0.50, 0.88)e nocca del medio(0.51, 0.58): «questo numero, nell'applicazione vera, decide quanto sono grosse le dita disegnate sullo schermo». Mostra la demo della mano e avvicina la mano alla camera. - Dimentica apposta il
returne fai vedere che la funzione stampaundefined.
Laboratorio (60')
Esercizio 3.1 — La distanza fra due punti base
Scrivi la funzione distanza(x1, y1, x2, y2) che restituisce la distanza fra
due punti, usando Math.sqrt. Provala con (0, 0) e
(3, 4): deve dare esattamente 5.
Poi riscrivila con Math.hypot, che calcola direttamente la radice della
somma dei quadrati, e controlla che il risultato sia identico.
Soluzione
function distanza(x1: number, y1: number, x2: number, y2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
function distanzaBreve(x1: number, y1: number, x2: number, y2: number): number {
return Math.hypot(x2 - x1, y2 - y1);
}
console.log(distanza(0, 0, 3, 4)); // 5
console.log(distanzaBreve(0, 0, 3, 4)); // 5
Perché così. dx e dy sono i due cateti del
triangolo; la distanza è l'ipotenusa. Mettere i due cateti in due const
con un nome, invece di scrivere tutto in una riga, rende la formula leggibile e evita
di calcolare due volte la stessa sottrazione.
Nel progetto vero questa è la riga che misura quanto è grande la mano inquadrata: la distanza fra il polso e la nocca del medio. Da quel numero dipende lo spessore delle ossa e il raggio delle articolazioni disegnate — così la mano resta credibile sia vicina alla camera (grande) sia lontana (piccola).
Esercizio 3.2 — Da coordinata a pixel base
Le coordinate del riconoscitore vanno da 0 a 1; lo schermo invece ragiona in pixel.
Scrivi aPixel(valore: number, dimensione: number): number che fa la
conversione, e provala con tutti questi casi:
aPixel(0.5, 640)deve dare 320;aPixel(0, 640)deve dare 0;aPixel(1, 480)deve dare 480.
Poi aggiungi un valore predefinito di 640 per la dimensione e riprova senza passarla.
Soluzione
function aPixel(valore: number, dimensione: number = 640): number {
return valore * dimensione;
}
console.log(aPixel(0.5, 640)); // 320
console.log(aPixel(0, 640)); // 0
console.log(aPixel(1, 480)); // 480
console.log(aPixel(0.25)); // 160 — usa il predefinito
Perché così. È una moltiplicazione sola, e proprio per questo merita una funzione: le darai un nome una volta e poi la userai decine di volte senza più chiederti se andava moltiplicato o diviso. Nella lezione 5 la userai per davvero, per disegnare i punti sul canvas.
Esercizio 3.3 — La media, ma come funzione base
Scrivi mediaDi(numeri: number[]): number, che restituisce la media degli
elementi. Deve funzionare con array di lunghezza qualsiasi; se l'array è vuoto deve
restituire 0 invece di NaN.
Soluzione
function mediaDi(numeri: number[]): number {
if (numeri.length === 0) {
return 0;
}
let somma = 0;
for (const valore of numeri) {
somma = somma + valore;
}
return somma / numeri.length;
}
console.log(mediaDi([12, 7, 30, 4, 22])); // 15
console.log(mediaDi([0.4, 0.6])); // 0.5
console.log(mediaDi([])); // 0
Perché così. Il controllo sull'array vuoto sta in cima e chiude subito
con un return: è il caso strano tolto di mezzo prima di iniziare.
Senza, la divisione sarebbe 0 / 0, cioè NaN, e quel
NaN si propagherebbe in tutti i conti successivi senza dire da dove viene.
Esercizio 3.4 — Tutte le coordinate in pixel, con map medio
Ecco le posizioni orizzontali di alcuni punti della mano, fra 0 e 1. Convertile tutte in
pixel per un'immagine larga 640, usando map e la funzione
aPixel dell'esercizio 3.2.
const x: number[] = [0.50, 0.42, 0.36, 0.44, 0.63, 0.70];
Poi, con filter, ricava un secondo array con i soli punti che stanno nella
metà sinistra dell'immagine (meno di 320 pixel) e stampa quanti sono.
Soluzione
function aPixel(valore: number, dimensione: number): number {
return valore * dimensione;
}
const x: number[] = [0.50, 0.42, 0.36, 0.44, 0.63, 0.70];
const inPixel = x.map((valore) => aPixel(valore, 640));
console.log(inPixel); // [320, 268.8, 230.4, 281.6, 403.2, 448] — circa
const aSinistra = inPixel.filter((p) => p < 320);
console.log(`a sinistra ce ne sono ${aSinistra.length}`); // 3
Perché così. map riceve una funzione e la applica a ogni
elemento: qui la funzione freccia (valore) => aPixel(valore, 640) serve
solo a fissare il secondo argomento. Il risultato è un array nuovo, della stessa
lunghezza; filter invece restituisce un array più corto, con i soli
elementi che superano la prova.
I numeri con la virgola non sono un errore: i pixel interi si otterrebbero con
Math.round. Per disegnare non serve — il canvas accetta benissimo le
posizioni frazionarie, anzi risultano più morbide. Se vedi stampato
230.39999999999998 invece di 230.4 non hai sbagliato niente:
è il modo in cui il computer conserva i numeri con la virgola, e nel disegno non si
nota di un pixel.
Esercizio 3.5 — Dal conteggio al nome del gesto sfida
Due funzioni, una che usa l'altra.
ditaAlzate(stati: boolean[]): number— riceve i cinque sì/no (pollice, indice, medio, anulare, mignolo) e restituisce quante dita sono alzate.nomeGesto(stati: boolean[]): "pugno" | "mano aperta" | "altro"— usa la prima funzione e restituisce il nome del gesto: 0 dita alzate è un pugno, 5 è una mano aperta, tutto il resto è «altro».
Provale con almeno tre array diversi.
Soluzione
type Gesto = "pugno" | "mano aperta" | "altro";
function ditaAlzate(stati: boolean[]): number {
let quante = 0;
for (const dito of stati) {
if (dito) {
quante = quante + 1;
}
}
return quante;
}
function nomeGesto(stati: boolean[]): Gesto {
const quante = ditaAlzate(stati);
if (quante === 0) {
return "pugno";
}
if (quante === 5) {
return "mano aperta";
}
return "altro";
}
console.log(nomeGesto([false, false, false, false, false])); // pugno
console.log(nomeGesto([true, true, true, true, true])); // mano aperta
console.log(nomeGesto([false, true, true, false, false])); // altro
Perché così. Il tipo di ritorno è un tipo unione, quindi il
compilatore controlla ogni return: se scrivessi
return "mano aperta!" con un punto esclamativo di troppo, l'errore
comparirebbe subito invece che in aula durante la presentazione.
Notare anche che non serve else: ogni return chiude la
funzione, quindi se il codice arriva all'ultima riga significa che le due condizioni
precedenti erano false. Si legge meglio così.
Chi finisce presto: aggiungi il gesto "vittoria" quando le dita
alzate sono esattamente indice e medio. Serve guardare le singole posizioni
dell'array, non solo il totale: stati[1] e stati[2] veri e
gli altri tre falsi.
Errori tipici in aula
returndimenticato: la funzione stampa ma non restituisce, e chi la chiama riceveundefined. Se TypeScript conosce il tipo di ritorno lo segnala subito — ottimo motivo per scriverlo sempre.- Dichiarare e non chiamare: scrivere
function distanza(…)e aspettarsi che succeda qualcosa. Dichiarare è come scrivere una ricetta; chiamare è cucinare. - Chiamare senza le tonde:
console.log(mediaDi)stampa il testo della funzione, non il risultato. ServemediaDi(numeri). - Parametri invertiti:
aPixel(640, 0.5)dà 320 comeaPixel(0.5, 640)— la moltiplicazione perdona — ma condistanzal'ordine sbagliato produce risultati insensati. Meglio abituarsi subito a leggere la firma. - Usare una variabile dichiarata dentro un'altra funzione: i parametri e le
constdichiarate dentro una funzione esistono solo lì dentro. Il messaggio è Cannot find name. - Il punto e virgola dopo la freccia:
const f = () => { … };vuole il punto e virgola finale perché è una dichiarazione di variabile;function f() { … }no. Nessuno lo ricorda, e va benissimo così: il formattatore lo sistema.
Verifica veloce (5 domande)
Domande e risposte
- Che differenza c'è fra dichiarare e chiamare una funzione? → Dichiarare la definisce, chiamare la esegue. Una funzione mai chiamata non fa niente.
- Cosa significa
: void? → Che la funzione non restituisce nessun valore: fa un'azione. - Cosa restituisce
[1, 2, 3].map((n) => n + 1)? → Un array nuovo,[2, 3, 4]. L'originale resta[1, 2, 3]. - Quando si mette
?su un parametro? → Quando è facoltativo; dentro la funzione può valereundefinede va previsto. - Cos'è una funzione pura? → Una che, con gli stessi ingressi, dà sempre lo stesso risultato e non cambia niente fuori da sé.
Compito per casa (30 minuti)
Scrivi tre funzioni e provale: massimoDi(numeri: number[]): number,
minimoDi(numeri: number[]): number e
larghezzaDi(numeri: number[]): number, che restituisce la differenza fra
massimo e minimo riusando le prime due. Provale sull'array delle ventuno
posizioni orizzontali dell'esercizio 2.5. Nessuna delle tre deve contenere un
console.log: devono restituire, non stampare — la stampa la fai fuori.
Oggetti e interfacce: il tipo Punto
Fin qui una coordinata erano due variabili separate, x e y, che
bisognava ricordarsi di tenere insieme. Oggi diventano una cosa sola con un nome e un
tipo. È il dato su cui gira tutto il progetto finale: dalla prossima lezione non
scriveremo quasi più altro.
interface · array di oggetti · readonly · ?. e ??Punto e la funzione che trova il centro della manoTeoria
A cosa serve. Prova a immaginare di scrivere il progetto finale con quello che sai adesso: ventuno valori orizzontali in un array, ventuno verticali in un altro, e la promessa di non sbagliare mai l'accoppiamento. Basta un ordinamento su un array solo e la mano si smonta. Un oggetto tiene insieme i valori che descrivono la stessa cosa, e li tiene insieme per costruzione.
const polso = { x: 0.50, y: 0.88 };
console.log(polso.x); // 0.5
console.log(polso.y); // 0.88
console.log(polso); // { x: 0.5, y: 0.88 }
Fra graffe si elencano le proprietà: un nome, due punti, un valore,
separate da virgola. Si leggono con il punto: polso.x. È tutto qui.
Se la variabile è let, o se la proprietà non è protetta, si possono anche
modificare:
const polso = { x: 0.50, y: 0.88 };
polso.x = 0.55; // PERMESSO, anche con const (vale la stessa regola degli array)
console.log(polso.x); // 0.55
polso.z = 0.1; // ERRORE: Property 'z' does not exist on type '{ x: number; y: number; }'.
Quell'ultimo errore è già TypeScript che lavora: ha dedotto da solo la forma dell'oggetto
e non accetta proprietà inventate. Però quella forma è scritta in un punto solo, e se
domani ci servisse in dieci funzioni diverse dovremmo ricopiarla dieci volte. Per questo
esiste l'interface: dà un nome a una forma.
interface Punto {
x: number;
y: number;
z?: number; // il punto interrogativo: facoltativa
}
const polso: Punto = { x: 0.50, y: 0.88 };
const puntaIndice: Punto = { x: 0.40, y: 0.37, z: -0.02 };
console.log(polso.x, puntaIndice.z);
Dentro l'interfaccia le proprietà si separano con punto e virgola (o a capo) e
non con la virgola come negli oggetti: è la confusione numero uno della
lezione. Da qui in poi Punto è un tipo a tutti gli effetti, e si usa come
number o string: come tipo di una variabile, di un parametro, di
un valore di ritorno, e dentro un array.
const punti: Punto[] = [
{ x: 0.50, y: 0.88 },
{ x: 0.40, y: 0.37 },
{ x: 0.51, y: 0.58 },
];
console.log(punti.length); // 3
console.log(punti[1].x); // 0.4 → l'elemento 1, e di quello la x
for (const p of punti) {
console.log(`(${p.x}, ${p.y})`);
}
punti[1].x si legge da sinistra a destra: prendi l'elemento di indice 1,
e di quell'oggetto prendi la proprietà x. Questa riga, con
punti[8].x, comparirà nel progetto vero decine di volte: 8 è la punta
dell'indice.
interface Punto {
x: number;
y: number;
z?: number;
}
const mano: Punto[] = [
{ x: 0.50, y: 0.88 }, // 0 polso
{ x: 0.42, y: 0.84 }, // 1 base del pollice
{ x: 0.30, y: 0.64 }, // 2 punta del pollice (nel modello vero è il punto 4)
{ x: 0.44, y: 0.60 }, // 3 nocca dell'indice (nel modello vero è il 5)
{ x: 0.40, y: 0.37 }, // 4 punta dell'indice (nel modello vero è l'8)
{ x: 0.51, y: 0.58 }, // 5 nocca del medio (nel modello vero è il 9)
{ x: 0.51, y: 0.33 }, // 6 punta del medio (nel modello vero è il 12)
{ x: 0.63, y: 0.63 }, // 7 nocca del mignolo (nel modello vero è il 17)
{ x: 0.70, y: 0.45 }, // 8 punta del mignolo (nel modello vero è il 20)
];
Una proprietà può essere dichiarata readonly: si legge, non
si riscrive. Serve a dire «questo dato, una volta creato, non si tocca».
interface PuntoFisso {
readonly x: number;
readonly y: number;
}
const origine: PuntoFisso = { x: 0, y: 0 };
origine.x = 5; // ERRORE: Cannot assign to 'x' because it is a read-only property.
Adesso il pezzo più importante della lezione, e quello che separa chi scriverà programmi solidi da chi passerà le serate a cercare bug: i valori che potrebbero non esserci. Ce ne sono due, e significano cose leggermente diverse:
undefined— «qui non è mai stato messo niente». È quello che ottieni da una proprietà facoltativa non valorizzata, da un parametro opzionale non passato, o da un elemento oltre la fine di un array.null— «qui, di proposito, non c'è niente». Lo scrive un programmatore per dire «questo posto è vuoto, e lo so».
Nel progetto finale entrambi compaiono: let stream: MediaStream | null = null
significa «la webcam adesso è spenta, e non è una dimenticanza».
Il problema è che leggere una proprietà di un valore che non c'è fa esplodere il
programma:
interface Punto { x: number; y: number }
let punta: Punto | undefined = undefined;
console.log(punta.x); // ERRORE del compilatore: 'punta' is possibly 'undefined'.
// e se lo eseguissi davvero: TypeError, il programma si ferma
Ci sono tre modi di gestirlo, in ordine di eleganza. Il primo è l'accesso
sicuro ?.: prova a leggere, e se davanti non c'è niente restituisce
undefined invece di esplodere.
let punta: Punto | undefined = undefined;
console.log(punta?.x); // undefined — nessun errore
Il secondo è il valore di riserva ??, che si legge
«altrimenti»: se quello che sta a sinistra è undefined o null,
prende quello a destra. I due si usano quasi sempre insieme:
let punta: Punto | undefined = undefined;
const x = punta?.x ?? 0;
console.log(x); // 0
punta = { x: 0.40, y: 0.37 };
console.log(punta?.x ?? 0); // 0.4
?? non è ||
Sembrano intercambiabili e non lo sono. || scatta su qualunque valore
«vuoto», e per lui anche lo zero è vuoto: 0 || 0.5 dà
0.5. Con le coordinate della mano è un disastro, perché 0 è il bordo sinistro
dell'immagine, un valore legittimo. 0 ?? 0.5 invece dà 0, che è
la risposta giusta. Regola: per i valori mancanti si usa ??.
Il terzo modo è il restringimento del tipo: un if che
controlla, e dentro il quale il compilatore sa che il valore c'è. È il più chiaro quando
devi usare il valore più di una volta:
function stampaPunto(punto: Punto | undefined): void {
if (punto === undefined) {
console.log("nessun punto");
return;
}
// da qui in poi TypeScript sa che `punto` esiste: niente ?. e niente errori
console.log(`x vale ${punto.x} e y vale ${punto.y}`);
}
stampaPunto(undefined); // nessun punto
stampaPunto({ x: 0.4, y: 0.37 }); // x vale 0.4 e y vale 0.37
Il compilatore segue il ragionamento: dopo quel return, l'unico modo
per essere ancora lì è che punto non sia undefined. Non è magia,
è la stessa cosa che avresti pensato tu leggendo il codice — solo che la controlla lui.
interface o type?
Si può scrivere anche type Punto = { x: number; y: number }, e per i casi di
questo corso le due forme sono equivalenti. Convenzione del corso, per non doverci più
pensare: interface per descrivere la forma di un oggetto,
type per gli alias e per i tipi unione come type Gesto = "pugno" |
"mano aperta", che con interface non si possono nemmeno scrivere.
Demo del docente (15')
- Scrivi
interface Puntoalla lavagna e poi nel Playground. Crea polso e punta dell'indice con i valori realistici visti sopra. - Metti il
ze togli ilz, mostrando che conz?entrambe le versioni sono valide. - Scrivi
centro(punti: Punto[]): Puntodal vivo, riusando l'idea della media della lezione 3. Falla restituire un oggetto: è la prima funzione del corso che restituisce qualcosa di composto. - Apri la demo della mano, mostra i puntini bianchi sull'anteprima e di': «ognuno di quei puntini è un oggetto come quello che abbiamo appena scritto; sono ventuno, e arrivano trenta volte al secondo».
- Scrivi
punti[99].xe fai vedere l'esplosione in esecuzione. Poi sistemala conpunti[99]?.x ?? 0.
Laboratorio (60')
Esercizio 4.1 — Il tipo Punto base
Definisci l'interfaccia Punto con x e y
obbligatori e z facoltativo. Poi crea due variabili: il polso in
(0.50, 0.88) e la punta dell'indice in (0.40, 0.37), quest'ultima
con anche z uguale a -0.02. Stampa una frase che le descriva
entrambe.
Soluzione
interface Punto {
x: number;
y: number;
z?: number;
}
const polso: Punto = { x: 0.50, y: 0.88 };
const puntaIndice: Punto = { x: 0.40, y: 0.37, z: -0.02 };
console.log(`il polso sta in (${polso.x}, ${polso.y})`);
console.log(`la punta dell'indice sta in (${puntaIndice.x}, ${puntaIndice.y})`);
console.log(`profondità della punta: ${puntaIndice.z ?? 0}`);
Perché così. Sul polso non abbiamo messo z ed è lecito,
perché è facoltativa. Proprio per questo, quando la si legge, si usa
?? 0: polso.z sarebbe undefined, e stampare
«profondità: undefined» non aiuta nessuno.
Nel progetto vero la z c'è sempre ed è la profondità rispetto al
polso: negativa significa più vicino alla camera. È la coordinata meno precisa delle
tre, perché una sola telecamera non vede davvero in profondità: la stima.
Esercizio 4.2 — Il centro della mano medio
Scrivi centro(punti: Punto[]): Punto, che restituisce il punto medio di un
elenco: la x del risultato è la media delle x, la
y è la media delle y. Provala sull'array mano
dato nella teoria.
Soluzione
function centro(punti: Punto[]): Punto {
if (punti.length === 0) {
return { x: 0, y: 0 };
}
let sommaX = 0;
let sommaY = 0;
for (const p of punti) {
sommaX = sommaX + p.x;
sommaY = sommaY + p.y;
}
return { x: sommaX / punti.length, y: sommaY / punti.length };
}
const c = centro(mano);
console.log(`il centro della mano sta in (${c.x}, ${c.y})`);
// il centro della mano sta in (0.49, 0.5911111111111111)
Perché così. Si scorre l'array una volta sola accumulando due somme:
fare due cicli separati funzionerebbe ma sarebbe lavoro doppio. Il return
costruisce un oggetto nuovo sul momento — non serve dichiararlo prima in una
variabile.
Nel progetto vero il centro serve a decidere dove piazzare la mano nella scena, e a capire se si sta spostando: se il centro si muove molto da un fotogramma all'altro, la mano è in movimento veloce, e il filtro anti-tremolio della lezione 9 si comporterà di conseguenza.
Esercizio 4.3 — Il punto più in alto medio
Scrivi piuAlto(punti: Punto[]): Punto, che restituisce il punto più in alto
dell'elenco. Provala sull'array mano: deve restituire la punta del medio,
cioè { x: 0.51, y: 0.33 }.
y = 0 è il bordo superiore, y = 1 è quello
inferiore. Quindi «più in alto» vuol dire «y più piccola», e la
funzione va scritta cercando il minimo, non il massimo. A scuola il piano
cartesiano andava all'incontrario; qui no. Nel progetto vero questa inversione si paga
di nuovo nel passaggio alla scena tridimensionale, dove invece la y torna a crescere
verso l'alto e va rovesciata un'altra volta.
Soluzione
function piuAlto(punti: Punto[]): Punto {
let migliore = punti[0];
for (const p of punti) {
if (p.y < migliore.y) { // y PIÙ PICCOLA = più in alto nell'immagine
migliore = p;
}
}
return migliore;
}
const alto = piuAlto(mano);
console.log(`il punto più in alto è (${alto.x}, ${alto.y})`);
// il punto più in alto è (0.51, 0.33) → la punta del medio
Perché così. È il massimo dell'esercizio 2.4 con il confronto
girato e, soprattutto, con gli oggetti al posto dei numeri: teniamo da parte
l'intero punto, non la sola coordinata, così chi chiama la funzione ottiene
anche la x senza doverla ricercare. Il commento sulla riga del confronto
non è decorativo: fra sei mesi sarà l'unica cosa che ti eviterà di «correggere» quel
< in >.
Chi finisce presto: fai restituire anche l'indice, come nella lezione 2.
Dovrai usare il for con il contatore e restituire un oggetto con due
proprietà, per esempio { punto: …, indice: … }.
Esercizio 4.4 — Un punto che potrebbe non esserci medio
Il riconoscitore, quando la mano esce dall'inquadratura, non restituisce ventuno punti: non ne restituisce nessuno. Simuliamo la cosa.
Scrivi puntoOppureNiente(punti: Punto[], indice: number): Punto | undefined,
che restituisce il punto richiesto oppure undefined se l'indice è fuori
dall'elenco. Poi scrivi descrivi(punto: Punto | undefined): string che
restituisce "x: 0.4" se il punto c'è, e "punto assente" se non
c'è. Provale con un indice valido e con l'indice 99.
Soluzione
function puntoOppureNiente(punti: Punto[], indice: number): Punto | undefined {
if (indice < 0 || indice >= punti.length) {
return undefined;
}
return punti[indice];
}
function descrivi(punto: Punto | undefined): string {
if (punto === undefined) {
return "punto assente";
}
return `x: ${punto.x}`;
}
console.log(descrivi(puntoOppureNiente(mano, 4))); // x: 0.4
console.log(descrivi(puntoOppureNiente(mano, 99))); // punto assente
// la versione corta, quando serve solo un numero:
const x = puntoOppureNiente(mano, 99)?.x ?? 0;
console.log(x); // 0
Perché così. Il controllo dell'indice usa || perché basta
che una delle due condizioni sia vera per essere fuori. Dentro descrivi
si è usato il restringimento con if invece di ?.: quando il
punto va usato più volte è più leggibile, e si evita una fila di punti interrogativi.
Le ultime due righe mostrano la forma compatta: ?. per non esplodere e
?? per il valore di riserva. È esattamente la scrittura che troverai nel
progetto vero, dove una riga come
result.gestures[i]?.[0]?.categoryName ?? "None" significa «il nome del
gesto, se c'è; altrimenti nessuno».
Esercizio 4.5 — L'oggetto Mano sfida
Un oggetto può contenere un array, e le sue proprietà possono avere tipi unione. Definisci:
interface Mano {
lato: "destra" | "sinistra";
punti: Punto[];
gesto?: string;
}
Crea una mano destra con i punti dell'array mano della teoria e senza gesto,
e una mano sinistra con due soli punti e gesto "pugno".
Poi scrivi riassumi(m: Mano): string che produce una riga come:
mano destra · 9 punti · centro (0.490, 0.591) · gesto: nessuno
Per arrotondare a tre decimali usa .toFixed(3) su un numero. Riusa la
funzione centro dell'esercizio 4.2.
Soluzione
interface Mano {
lato: "destra" | "sinistra";
punti: Punto[];
gesto?: string;
}
function riassumi(m: Mano): string {
const c = centro(m.punti);
const nomeGesto = m.gesto ?? "nessuno";
return `mano ${m.lato} · ${m.punti.length} punti · centro (${c.x.toFixed(3)}, ${c.y.toFixed(3)}) · gesto: ${nomeGesto}`;
}
const destra: Mano = { lato: "destra", punti: mano };
const sinistra: Mano = {
lato: "sinistra",
punti: [
{ x: 0.20, y: 0.70 },
{ x: 0.24, y: 0.62 },
],
gesto: "pugno",
};
console.log(riassumi(destra));
// mano destra · 9 punti · centro (0.490, 0.591) · gesto: nessuno
console.log(riassumi(sinistra));
// mano sinistra · 2 punti · centro (0.220, 0.660) · gesto: pugno
Perché così. m.gesto ?? "nessuno" in una riga risolve la
proprietà facoltativa. Il tipo unione su lato impedisce di scrivere
"Destra" con la maiuscola o "dx": con un string
generico passerebbero entrambi, e il confronto m.lato === "destra" più
avanti fallirebbe in silenzio.
Nel progetto vero questa è quasi esattamente la struttura che arriva dal riconoscitore: per ogni mano inquadrata ci sono il lato, i ventuno punti e, facoltativo, il nome del gesto riconosciuto fra quelli noti. E il lato, nel codice vero, è un tipo unione identico a questo.
Errori tipici in aula
- Virgola dentro l'
interface: si scrivex: number;con il punto e virgola (o solo a capo). Con la virgola il compilatore protesta e il messaggio non è chiarissimo. - Due punti al posto dell'uguale:
const polso = { x: 0.5 }dentro l'oggetto vuole i due punti, ma la variabile si crea con l'uguale. Confondere le due cose è normalissimo il primo giorno. - Parentesi graffe nel
return:return { x: 1, y: 2 };restituisce un oggetto. Le graffe qui non sono un blocco di codice, sono l'oggetto: capita di cercare un errore inesistente per questo motivo. - Cercare il massimo per trovare il punto più in alto: nelle immagini la y cresce in giù. Se la mano disegnata risulta capovolta, il problema è quasi sempre qui.
- Usare
||invece di??: funziona finché non arriva uno zero legittimo, e allora si rompe in modo difficilissimo da rintracciare. - Confrontare oggetti con
===:{ x: 1 } === { x: 1 }èfalse. Il confronto guarda se sono lo stesso oggetto, non se hanno lo stesso contenuto. Per confrontare il contenuto si confrontano le proprietà una per una.
Verifica veloce (5 domande)
Domande e risposte
- Cosa significa
z?: numberdentro un'interfaccia? → Che la proprietà è facoltativa: se manca valeundefined. - Cosa restituisce
punti[99]?.xse l'array ha 21 elementi? →undefined, senza far esplodere il programma. - Nell'immagine, qual è il punto più in alto: quello con y 0.2 o quello con y 0.8? → Quello con y 0.2. La y cresce verso il basso.
- Perché
0 ?? 5dà 0 mentre0 || 5dà 5? → Perché??scatta solo sunulleundefined,||su qualunque valore considerato «vuoto», zero compreso. - Si può modificare una proprietà di un oggetto dichiarato con
const? → Sì, a meno che non siareadonly.constprotegge il nome, non il contenuto.
Compito per casa (30-40 minuti)
Scrivi dimensioneMano(punti: Punto[]): number, che restituisce la distanza fra
il primo punto dell'array (il polso) e quello di indice 5 (la nocca del medio, nel nostro
array ridotto), riusando la funzione distanza della lezione 3 — adattala però
perché riceva due Punto invece di quattro numeri.
Poi scrivi aperta(punti: Punto[]): boolean, che risponde true se
la punta del medio sta più in alto della nocca del medio di almeno un quarto della
dimensione della mano. Provala sull'array mano e su una versione modificata in
cui hai abbassato la punta del medio a y: 0.56.
La pagina e il canvas: disegnare una mano finta
Fin qui il programma parlava solo attraverso console.log. Oggi si passa a una
pagina vera e a una tela su cui disegnare, e alla fine delle due ore sullo schermo
comparirà una mano. Finta, ferma, disegnata da te: ma è la stessa mano che fra
quattro lezioni si muoverà seguendo la tua.
Teoria
A cosa serve. Un programma che stampa numeri in una console non lo guarda nessuno. Il progetto finale è una pagina web: c'è un pulsante, c'è un riquadro con la webcam e c'è una tela su cui compare la mano. Oggi impariamo i due meccanismi che tengono insieme il codice e quello che si vede: prendere un elemento della pagina e disegnare dentro di esso.
Cominciamo dalla pagina. Una pagina HTML è fatta di tag, che si aprono e si chiudono e contengono altri tag. Questa è la più piccola che ci serva:
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<title>La mia mano</title>
</head>
<body>
<canvas id="tela" width="480" height="640"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<head> contiene le informazioni sulla pagina, <body>
quello che si vede. Il <canvas> è la tela: un
rettangolo vuoto, qui largo 480 pixel e alto 640, dentro cui il codice può disegnare.
L'id è il nome con cui il codice lo cercherà: deve essere unico nella pagina.
L'ultima riga carica il nostro programma.
index.html, di cui sostituirai il
contenuto del <body>, e src/main.ts, che puoi svuotare
completamente prima di iniziare. È lo stesso identico strumento che si installerà sul
proprio computer nella lezione 6: non si butta via niente.
Adesso il codice deve mettere le mani su quel <canvas>. Lo si chiede
alla pagina per nome:
const tela = document.getElementById("tela");
document è la pagina, vista dal codice. getElementById cerca
l'elemento con quell'id. Se però provi a usarlo, TypeScript si mette di
traverso:
const tela = document.getElementById("tela");
console.log(tela.width);
// ERRORE: 'tela' is possibly 'null'.
// ERRORE: Property 'width' does not exist on type 'HTMLElement'.
Due proteste, e hanno tutte e due ragione:
-
Potrebbe non esserci. Il tipo di ritorno è
HTMLElement | null: quel tipo unione dice «un elemento, oppure niente». Se hai scritto male l'id, o se il codice parte prima che la pagina sia pronta, la risposta ènull. È lo stessonulldella lezione 4. -
Non sa che tipo di elemento è. Sa che è un elemento della pagina,
HTMLElement, e basta. Ma un titolo non ha una larghezza di disegno, un canvas sì: finché il compilatore non sa che è un canvas, non ti fa usare le cose da canvas.
Il secondo problema si risolve con as, che si legge «trattalo come»: è una
promessa che fai tu al compilatore.
const tela = document.getElementById("tela") as HTMLCanvasElement | null;
if (!tela) {
throw new Error("canvas non trovato: controlla l'id nell'HTML");
}
console.log(tela.width); // 480 — adesso va
if (!tela) si legge «se tela NON c'è»: è il punto esclamativo della lezione 2,
quello che capovolge. Dopo quel blocco il compilatore sa che tela esiste — è
lo stesso restringimento della lezione 4.
as è una promessa, non un controllo
Scrivere as HTMLCanvasElement non verifica niente: convince il compilatore e
basta. Se nell'HTML quell'id è su un <div>, il programma si
romperà lo stesso, solo più tardi e con un messaggio più oscuro. Si usa as
quando sai com'è fatta la pagina perché l'hai scritta tu — ed è esattamente il caso
del nostro progetto, dove gli id sono scritti a mano e non cambiano mai.
Sulla tela non si disegna direttamente: si chiede il contesto, cioè l'oggetto che possiede tutti gli attrezzi da disegno. Ne esistono di diversi; a noi serve quello bidimensionale.
const ctx = tela.getContext("2d");
if (!ctx) {
throw new Error("contesto 2d non disponibile");
}
Anche questo può restituire null (per esempio se alla stessa tela fosse già
stato chiesto un contesto di tipo diverso), quindi si controlla come prima. Da qui in poi
ctx è l'oggetto con cui si disegna tutto.
Le coordinate. Prima di disegnare bisogna sapere dove finiranno le cose.
Il canvas ha l'origine in alto a sinistra: la x cresce verso
destra, la y cresce verso il basso. È la stessa convenzione
delle immagini vista nella lezione 4 — e per una volta questo è un vantaggio: le
coordinate del riconoscitore, moltiplicate per la dimensione della tela, finiscono al
posto giusto senza rovesciare niente.
// su una tela 480 × 640:
// (0, 0) angolo in alto a sinistra
// (480, 0) angolo in alto a destra
// (0, 640) angolo in basso a sinistra
// (240, 320) centro esatto
function aPixel(valore: number, dimensione: number): number {
return valore * dimensione;
}
console.log(aPixel(0.5, 480)); // 240 → metà della larghezza
Disegnare un cerchio. Serve per le articolazioni della mano. Sono tre righe, sempre le stesse:
ctx.beginPath(); // 1. inizia un disegno nuovo
ctx.arc(240, 320, 8, 0, Math.PI * 2); // 2. descrivi un cerchio
ctx.fillStyle = "#38bdf8"; // 3. scegli il colore di riempimento
ctx.fill(); // 4. riempilo
Gli argomenti di arc, in ordine: centro x, centro y, raggio, angolo di partenza, angolo di arrivo. 0 e Math.PI * 2 sono l'inizio e la fine del giro completo: è la ricetta per «cerchio intero», e si copia così com'è.
beginPath non è facoltativo
Senza, ogni nuova figura si aggiunge a quelle precedenti nello stesso disegno: cambiare
colore le ricolora tutte, e i cerchi risultano collegati da linee misteriose. Se disegnando
i ventuno punti della mano ottieni una ragnatela, la causa è questa. Regola: una figura, un
beginPath.
Disegnare una linea. Serve per le ossa. Stesso schema:
ctx.beginPath();
ctx.moveTo(100, 100); // porta la penna qui, senza disegnare
ctx.lineTo(300, 400); // traccia fino a qui
ctx.strokeStyle = "#f472b6"; // colore della linea
ctx.lineWidth = 4; // spessore in pixel
ctx.stroke(); // disegna davvero
La differenza fra fill e stroke è la stessa fra riempire e
contornare: fillStyle/fill per il dentro,
strokeStyle/stroke per il bordo. I colori si scrivono in
esadecimale ("#38bdf8") o per nome ("white").
E niente viene disegnato davvero finché non chiami fill o stroke:
le istruzioni prima si limitano a descrivere la forma.
Cancellare. Quando la mano si muove non si disegna sopra alla precedente: si pulisce tutto e si ridisegna da capo, trenta volte al secondo.
ctx.clearRect(0, 0, tela.width, tela.height); // svuota tutta la tela
Rispondere a un clic. Ultimo attrezzo di oggi. Si prende il pulsante come si è preso il canvas, e gli si dice cosa fare quando viene premuto:
const pulsante = document.getElementById("cambia") as HTMLButtonElement | null;
if (pulsante) {
pulsante.addEventListener("click", () => {
console.log("premuto!");
});
}
addEventListener vuole due cose: il nome dell'evento ("click") e
una funzione da eseguire quando succede. Quella funzione è una funzione freccia senza
parametri, della lezione 3. Nessuno la chiama esplicitamente: la chiama il browser, quando
e se l'utente preme. Nel progetto finale è esattamente così che parte la webcam, dal clic
sul pulsante «Accendi la camera».
Demo del docente (15')
- Apri
vite.new/vanilla-tsdavanti alla classe: far vedere che in dieci secondi c'è un progetto funzionante toglie l'ansia dell'installazione. - Svuota
src/main.tse il<body>diindex.html, metti il<canvas>, salva: la pagina resta bianca. «La tela c'è, è vuota.» - Scrivi
getElementByIdsenzaase mostra i due errori rossi. Aggiungiase il controllo, e commentali: sono i due problemi che il compilatore ha appena evitato. - Disegna un cerchio. È il momento «funziona!» della lezione: aspetta due secondi prima di continuare.
- Disegna tre cerchi dimenticando
beginPathe fai vedere la ragnatela. Poi aggiungilo. - Collega un pulsante che cambia il colore del cerchio e ridisegna.
Laboratorio (60')
Esercizio 5.1 — Il primo cerchio e la prima linea base
Su vite.new/vanilla-ts: metti nel <body> di
index.html un canvas 480 × 640 con id="tela", svuota
src/main.ts e disegna un cerchio azzurro al centro della tela e una linea
rosa dall'angolo in alto a sinistra al centro.
<body>
<canvas id="tela" width="480" height="640"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
Soluzione
const tela = document.getElementById("tela") as HTMLCanvasElement | null;
if (!tela) {
throw new Error("canvas non trovato");
}
const ctx = tela.getContext("2d");
if (!ctx) {
throw new Error("contesto 2d non disponibile");
}
// il cerchio azzurro al centro
ctx.beginPath();
ctx.arc(240, 320, 20, 0, Math.PI * 2);
ctx.fillStyle = "#38bdf8";
ctx.fill();
// la linea rosa dall'angolo al centro
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(240, 320);
ctx.strokeStyle = "#f472b6";
ctx.lineWidth = 4;
ctx.stroke();
Perché così. I due controlli in cima non sono burocrazia: sono la
differenza fra un messaggio che ti dice cosa è andato storto e una pagina bianca
silenziosa. Il centro è (240, 320) perché la tela è 480 × 640: metà e
metà.
Esercizio 5.2 — I ventuno punti di una mano finta medio
Ecco una mano vera, in coordinate da 0 a 1: sono i ventuno punti di una destra aperta con le dita verso l'alto, nell'ordine esatto usato dal progetto finale. Copiali.
interface Punto {
x: number;
y: number;
}
const MANO_APERTA: Punto[] = [
{ x: 0.50, y: 0.88 }, // 0 polso
{ x: 0.42, y: 0.84 }, // 1 pollice, base
{ x: 0.36, y: 0.77 }, // 2 pollice
{ x: 0.33, y: 0.70 }, // 3 pollice
{ x: 0.30, y: 0.64 }, // 4 pollice, PUNTA
{ x: 0.44, y: 0.60 }, // 5 indice, nocca
{ x: 0.42, y: 0.50 }, // 6 indice
{ x: 0.41, y: 0.43 }, // 7 indice
{ x: 0.40, y: 0.37 }, // 8 indice, PUNTA
{ x: 0.51, y: 0.58 }, // 9 medio, nocca
{ x: 0.51, y: 0.47 }, // 10 medio
{ x: 0.51, y: 0.40 }, // 11 medio
{ x: 0.51, y: 0.33 }, // 12 medio, PUNTA
{ x: 0.57, y: 0.60 }, // 13 anulare, nocca
{ x: 0.58, y: 0.49 }, // 14 anulare
{ x: 0.59, y: 0.43 }, // 15 anulare
{ x: 0.60, y: 0.37 }, // 16 anulare, PUNTA
{ x: 0.63, y: 0.63 }, // 17 mignolo, nocca
{ x: 0.66, y: 0.55 }, // 18 mignolo
{ x: 0.68, y: 0.50 }, // 19 mignolo
{ x: 0.70, y: 0.45 }, // 20 mignolo, PUNTA
];
Scrivi aPixel (è quella della lezione 3) e disegna un cerchio azzurro di
raggio 6 per ognuno dei ventuno punti. Usa tela.width e
tela.height, non i numeri 480 e 640 scritti a mano.
Soluzione
function aPixel(valore: number, dimensione: number): number {
return valore * dimensione;
}
ctx.clearRect(0, 0, tela.width, tela.height);
for (const p of MANO_APERTA) {
const px = aPixel(p.x, tela.width);
const py = aPixel(p.y, tela.height);
ctx.beginPath();
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.fillStyle = "#38bdf8";
ctx.fill();
}
Perché così. for…of perché qui serve solo il valore, non
la posizione. beginPath sta dentro il ciclo: uno per cerchio,
altrimenti i ventuno punti si collegano fra loro. E usare tela.width
invece di 480 significa che cambiando la dimensione nell'HTML il disegno si adatta da
solo, senza toccare il codice.
Dovresti già intravedere la forma di una mano. Non è ancora una mano: mancano le ossa.
Esercizio 5.3 — Le ossa medio
Un dito è fatto di punti collegati fra loro. Quali punti vadano collegati a quali è una informazione fissa, che nel progetto vero arriva dalla libreria di riconoscimento: queste sono le ventuno connessioni, identiche a quelle usate nell'applicazione.
const CONNESSIONI: number[][] = [
[0, 1], [1, 2], [2, 3], [3, 4], // pollice
[0, 5], [5, 6], [6, 7], [7, 8], // indice
[5, 9], [9, 10], [10, 11], [11, 12], // medio
[9, 13], [13, 14], [14, 15], [15, 16], // anulare
[13, 17], [17, 18], [18, 19], [19, 20], // mignolo
[0, 17], // il lato del palmo
];
Ogni elemento è un array di due numeri: gli indici dei due punti da unire con una linea. Disegna tutte le ossa in azzurro con spessore 4, prima dei cerchi, così i punti restano sopra.
Soluzione
ctx.clearRect(0, 0, tela.width, tela.height);
// 1) le ossa, sotto
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 4;
for (const osso of CONNESSIONI) {
const a = MANO_APERTA[osso[0]];
const b = MANO_APERTA[osso[1]];
ctx.beginPath();
ctx.moveTo(aPixel(a.x, tela.width), aPixel(a.y, tela.height));
ctx.lineTo(aPixel(b.x, tela.width), aPixel(b.y, tela.height));
ctx.stroke();
}
// 2) le articolazioni, sopra
for (const p of MANO_APERTA) {
ctx.beginPath();
ctx.arc(aPixel(p.x, tela.width), aPixel(p.y, tela.height), 6, 0, Math.PI * 2);
ctx.fillStyle = "#38bdf8";
ctx.fill();
}
Perché così. osso[0] e osso[1] sono i due
indici; usarli per pescare dentro MANO_APERTA è la riga chiave
dell'esercizio. L'ordine conta: il canvas disegna in ordine di istruzione, quindi
quello che viene dopo copre quello di prima. Ossa prima, articolazioni dopo.
strokeStyle e lineWidth sono impostati una volta fuori dal
ciclo: valgono finché non vengono cambiati, non c'è bisogno di ripeterli ventuno volte.
Adesso sullo schermo c'è una mano. L'hai disegnata tu: è la stessa figura che nella lezione 8 comincerà a muoversi.
Esercizio 5.4 — Punte bianche medio
Nel progetto vero le cinque punte delle dita — gli indici 4, 8, 12, 16 e 20 — sono bianche e un po' più grandi delle altre articolazioni: servono a leggere il gesto a colpo d'occhio. Fai la stessa cosa.
Ti servirà il for con l'indice, non for…of: devi sapere in che
posizione sei. Suggerimento: const PUNTE = [4, 8, 12, 16, 20]; e poi
PUNTE.includes(i), che risponde true se il numero è
nell'elenco.
Soluzione
const PUNTE: number[] = [4, 8, 12, 16, 20];
for (let i = 0; i < MANO_APERTA.length; i++) {
const p = MANO_APERTA[i];
const punta = PUNTE.includes(i);
ctx.beginPath();
ctx.arc(aPixel(p.x, tela.width), aPixel(p.y, tela.height), punta ? 9 : 6, 0, Math.PI * 2);
ctx.fillStyle = punta ? "#ffffff" : "#38bdf8";
ctx.fill();
}
Perché così. Qui serve l'indice perché l'informazione «è una punta» sta nella posizione, non nel valore: è lo stesso motivo dell'esercizio 2.4.
punta ? 9 : 6 è una scrittura nuova, l'operatore
condizionale: si legge «se punta è vero allora 9, altrimenti 6».
È un if/else che sta dentro un'espressione e restituisce un
valore. Si usa solo per scelte piccolissime come questa; per qualunque cosa più
complessa resta più leggibile un if normale.
Esercizio 5.5 — Il pulsante che cambia posa sfida
Aggiungi un pulsante alla pagina. A ogni clic la mano passa da aperta a pugno e viceversa, ridisegnando tutto da capo.
<body>
<button id="cambia">Cambia posa</button>
<canvas id="tela" width="480" height="640"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
Ecco la seconda posa, con le dita chiuse verso il palmo:
const PUGNO: Punto[] = [
{ x: 0.50, y: 0.88 }, { x: 0.42, y: 0.84 }, { x: 0.37, y: 0.78 },
{ x: 0.36, y: 0.72 }, { x: 0.40, y: 0.68 },
{ x: 0.44, y: 0.62 }, { x: 0.43, y: 0.54 }, { x: 0.46, y: 0.56 },
{ x: 0.48, y: 0.60 },
{ x: 0.51, y: 0.60 }, { x: 0.51, y: 0.52 }, { x: 0.53, y: 0.55 },
{ x: 0.54, y: 0.60 },
{ x: 0.57, y: 0.61 }, { x: 0.58, y: 0.53 }, { x: 0.59, y: 0.57 },
{ x: 0.59, y: 0.62 },
{ x: 0.63, y: 0.64 }, { x: 0.65, y: 0.58 }, { x: 0.65, y: 0.62 },
{ x: 0.64, y: 0.66 },
];
Il pezzo difficile è organizzare il codice: metti tutto il disegno dentro una funzione
disegna(punti: Punto[]): void, così puoi richiamarla con una posa o con
l'altra.
Soluzione
function disegna(punti: Punto[]): void {
ctx.clearRect(0, 0, tela.width, tela.height);
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 4;
for (const osso of CONNESSIONI) {
const a = punti[osso[0]];
const b = punti[osso[1]];
ctx.beginPath();
ctx.moveTo(aPixel(a.x, tela.width), aPixel(a.y, tela.height));
ctx.lineTo(aPixel(b.x, tela.width), aPixel(b.y, tela.height));
ctx.stroke();
}
for (let i = 0; i < punti.length; i++) {
const p = punti[i];
const punta = PUNTE.includes(i);
ctx.beginPath();
ctx.arc(aPixel(p.x, tela.width), aPixel(p.y, tela.height), punta ? 9 : 6, 0, Math.PI * 2);
ctx.fillStyle = punta ? "#ffffff" : "#38bdf8";
ctx.fill();
}
}
let aperta = true;
disegna(MANO_APERTA);
const pulsante = document.getElementById("cambia") as HTMLButtonElement | null;
if (pulsante) {
pulsante.addEventListener("click", () => {
aperta = !aperta;
disegna(aperta ? MANO_APERTA : PUGNO);
});
}
Perché così. La funzione disegna riceve i punti come
parametro invece di leggere una variabile globale: così la stessa funzione serve per
tutte e due le pose, e domani servirà anche per i punti veri della webcam senza
cambiare una riga. È il motivo per cui esistono i parametri.
aperta = !aperta capovolge il booleano a ogni clic: è la scrittura
standard per un interruttore. E il clearRect in cima a
disegna è indispensabile: senza, le due pose si accumulerebbero sulla tela
e dopo tre clic si vedrebbe un groviglio.
Chi finisce presto: disegna il palmo. È un poligono che unisce i punti 0, 1,
5, 9, 13 e 17: si fa con moveTo sul primo, lineTo su tutti
gli altri, ctx.closePath() e ctx.fill(). È esattamente quello
che fa il progetto vero, dove il palmo è un poligono semitrasparente.
Errori tipici in aula
- Pagina bianca e nessun errore: quasi sempre l'
idnell'HTML e quello nel codice non coincidono, oppure manca la riga<script type="module" src="/src/main.ts">. Primo posto dove guardare: la console del browser. - La ragnatela:
beginPathdimenticato dentro il ciclo. Tutti i cerchi risultano uniti da linee. - Niente si vede ma nessun errore:
fillostrokedimenticati. Il disegno è stato descritto ma non eseguito. - La mano capovolta: qualcuno ha «corretto» la y pensando che 0 fosse in basso. Nel canvas la y cresce verso il basso, come nell'immagine: va lasciata com'è.
- Il canvas dimensionato con il CSS: mettere larghezza e altezza fuori dagli attributi
widtheheightstira il disegno invece di ingrandirlo. Per oggi: solo attributi nell'HTML. - Mano minuscola in un angolo: si è usato
aPixel(p.x, 1)o ci si è dimenticati di moltiplicare. Le coordinate 0…1 senza moltiplicazione stanno tutte nel primo pixel. - Il pulsante non risponde: l'ascoltatore è stato registrato su un elemento
nulldentro unifmai vero, oppure l'idè sbagliato.
Verifica veloce (5 domande)
Domande e risposte
- Perché
document.getElementByIdrestituisceHTMLElement | null? → Perché quell'idpotrebbe non esistere nella pagina: il tipo lo dice, e obbliga a controllare. - Cosa fa
as HTMLCanvasElement? → Convince il compilatore che quell'elemento è un canvas. Non verifica niente: è una promessa di chi scrive. - A cosa serve
beginPath? → A iniziare una figura nuova, staccata dalla precedente. Senza, le figure si uniscono. - Dove si trova il punto
(0, 0)di un canvas? → In alto a sinistra. La y cresce verso il basso. - Che differenza c'è fra
fillestroke? →fillriempie l'interno della figura,strokene traccia il contorno.
Compito per casa (40 minuti)
Parti dalla soluzione dell'esercizio 5.5 e aggiungi una terza posa a tua scelta — il gesto
della vittoria, il pollice in su, la mano che saluta — inventando tu le ventuno coordinate.
Il pulsante deve ruotare fra le tre pose invece di alternarne due: tieni un
let posa = 0, portalo avanti a ogni clic e riportalo a zero quando arriva a
tre. Mettendo le tre pose in un array di array — const POSE: Punto[][] = [MANO_APERTA,
PUGNO, LA_TUA] — il codice diventa di tre righe. Porta il link di StackBlitz alla
prossima lezione: è l'ultima volta che si lavora nel browser, perché la sesta lezione si
apre installando gli strumenti veri sul proprio computer.
Sul proprio computer: Node, Vite, moduli
Fino a ieri il codice girava dentro una pagina web altrui. Da oggi il progetto è tuo: sta in una cartella sul tuo computer, si avvia con un comando e si divide in più file. È il passaggio che rende possibile tutto il resto del corso.
Teoria
Nelle prime cinque lezioni hai scritto codice dentro una pagina che qualcun altro teneva aperta per te. Ha funzionato benissimo per imparare, ma adesso c'è un ostacolo preciso: la webcam non si accende dentro l'anteprima degli ambienti online. Il riquadro che mostra il risultato è una pagina incorniciata dentro un'altra, che arriva da un sito diverso, e i browser negano camera e microfono a quei riquadri. Non è un difetto dell'ambiente: è una regola pensata per impedire che un sito qualunque accenda la tua telecamera nascondendosi dentro una cornice.
Dalla prossima lezione ti serve la webcam. Quindi oggi ti serve un progetto vero.
Node.js: JavaScript fuori dal browser
Node.js è un programma che esegue JavaScript sul tuo computer, senza browser. A te non serve per far girare la mano — quella girerà nel browser come sempre — ma serve agli strumenti: il compilatore di TypeScript, il server di sviluppo, il programma che mette insieme i file. Sono tutti scritti in JavaScript e girano su Node.
Insieme a Node si installa npm (node package
manager), che fa due cose:
- scarica librerie scritte da altri — alla lezione 8 scaricherai con un comando il riconoscitore di mani di Google;
- esegue i comandi del progetto, quelli scritti in
package.json:npm run dev,npm run build.
document e window non esistono. Il browser è il contrario.
Il tuo codice della mano vive nel browser; Node lo prepara e glielo serve.
Vite: il progetto già pronto
Creare a mano la cartella giusta, il file di configurazione giusto e il server giusto è noioso e non insegna niente. Vite lo fa per te con un comando:
npm create vite@latest mano-corso
Ti fa due domande. Alla prima (Select a framework) scegli Vanilla,
che vuol dire «niente framework, solo la pagina e il codice». Alla seconda (Select a
variant) scegli TypeScript. In breve si dice «il modello
vanilla-ts», e lo puoi anche chiedere direttamente:
npm create vite@latest mano-corso -- --template vanilla-ts
Dentro la cartella appena creata trovi questo:
mano-corso/
├── index.html ← la pagina: il titolo, i pulsanti, il canvas
├── package.json ← il "documento d'identità" del progetto
├── tsconfig.json ← le regole del compilatore TypeScript
├── public/ ← file serviti così come sono (immagini, icone)
└── src/
├── main.ts ← il codice che parte all'apertura della pagina
└── style.css ← l'aspetto
In fondo a index.html c'è una riga sola che collega la pagina al codice:
<script type="module" src="/src/main.ts"></script>
Poi due comandi, sempre gli stessi, per tutto il resto del corso:
cd mano-corso # entra nella cartella
npm install # scarica gli strumenti: crea node_modules/ (pesante, non si tocca)
npm run dev # avvia il server di sviluppo
npm run dev stampa un indirizzo, di solito
http://localhost:5173. Lo apri nel browser e vedi la tua pagina.
Il server resta acceso: lascia quella finestra del terminale aperta e
scrivi il codice nell'altra finestra. Ogni volta che salvi, la pagina si aggiorna da sola
in una frazione di secondo. Per fermarlo: Ctrl + C.
localhost vuol dire «questo computer». Nessuno, da fuori, vede quella
pagina. Ed è anche un indirizzo di cui il browser si fida: come vedrai la prossima
lezione, la webcam si accende solo su https://… o su
localhost, e questo è il motivo per cui il progetto locale funziona subito.
I due file di configurazione
package.json è un file di testo che descrive il progetto: il nome, le
librerie che usa e i comandi disponibili.
{
"name": "mano-corso",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.9.0",
"vite": "^7.0.0"
}
}
Quello che sta in scripts sono scorciatoie: npm run dev esegue
quello che c'è scritto accanto a "dev". Quello che sta in
devDependencies sono gli strumenti scaricati da npm install.
Il numero accanto al nome è la versione, e conta: alla lezione 8 vedrai
cosa succede quando una libreria cambia sotto i piedi di un progetto.
tsconfig.json dice al compilatore quanto essere severo. La riga che ci
interessa oggi è una:
{
"compilerOptions": {
"target": "ES2022",
"strict": true
},
"include": ["src"]
}
Con "strict": true TypeScript smette di lasciar correre. I due controlli che
noterai subito:
-
niente
anyper caso: un parametro senza tipo diventa un errore, perché «non lo so» non è una risposta accettabile; -
niente
nulldi nascosto: se una cosa può non esistere — comedocument.getElementById("tela"), che restituisce l'elemento oppurenull— devi dirlo tu cosa fare in quel caso.
// con strict acceso
const tela = document.getElementById("tela");
tela.textContent = "ciao";
// Errore: 'tela' is possibly 'null'.
// due modi onesti per risolverlo
const tela = document.getElementById("tela") as HTMLCanvasElement; // "fidati, esiste"
// oppure, meglio:
const tela = document.getElementById("tela");
if (!tela) throw new Error("manca l'elemento #tela nella pagina");
strict ti fa comparire adesso è un errore che non comparirà
alla lezione 8, mentre muovi la mano davanti alla webcam e non capisci perché lo schermo
resta nero. Il momento giusto per scoprire che una cosa può essere null è
quando la scrivi.
Dividere il codice in file: export e import
Il progetto vero della mano — quello che vedi online — è diviso in cinque file, ognuno con un compito: accendere la camera, riconoscere la mano, smorzare il tremolio, disegnare, mettere tutto in fila. Non è pignoleria: è l'unico modo per ritrovare le cose.
Un file diventa un modulo nel momento in cui exporta
qualcosa. Quello che non è esportato resta privato di quel file.
// src/disegna.ts
export interface Punto {
x: number;
y: number;
z: number;
}
const COLORE_OSSA = "#38bdf8"; // niente export: la usa solo questo file
export function disegnaPunti(ctx: CanvasRenderingContext2D, punti: Punto[]): void {
for (const p of punti) {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
// src/main.ts
import { disegnaPunti, type Punto } from "./disegna";
const punti: Punto[] = [{ x: 100, y: 50, z: 0 }];
// disegnaPunti(ctx, punti);
Tre cose da notare, e sono sempre le stesse tre:
-
fra graffe si scrivono gli stessi nomi usati nell'
export: se lì èdisegnaPunti, qui non può essereDisegnaPunti; -
il percorso comincia con
./(stessa cartella) o../(cartella sopra), e l'estensione.tsnon si scrive; -
per importare un tipo si scrive
typedavanti al nome: dice al compilatore «questo serve solo a te, non finirà nel codice eseguito».
Cosa fa il compilatore, davvero
Il browser non sa cos'è TypeScript. Non ha mai saputo cosa sono i tipi e non li imparerà. Quello che succede è più semplice di quanto sembri: i tipi vengono cancellati.
// quello che scrivi tu (TypeScript)
function distanza(a: Punto, b: Punto): number {
const dx: number = a.x - b.x;
const dy: number = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
// quello che arriva al browser (JavaScript)
function distanza(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
Identico, meno le annotazioni. Da qui discendono due conseguenze che vale la pena fissare adesso:
- i tipi non costano niente a chi esegue il programma: a mezzogiorno sono già spariti;
-
i tipi non proteggono mentre il programma gira. Se alla lezione 8 il
riconoscitore restituisce zero mani, nessun tipo te lo impedisce: dovrai controllarlo
tu con un
if.
In sviluppo questa trasformazione la fa Vite, al volo, a ogni salvataggio. Il controllo
dei tipi invece lo fa tsc, che gira quando lanci npm run build
e in continuazione dentro VS Code, che è il motivo per cui le sottolineature rosse
compaiono mentre scrivi.
Demo del docente (15')
- Terminale vuoto:
node -venpm -v, per mostrare che ci sono. npm create vite@latest demo-aula -- --template vanilla-ts, poinpm installenpm run dev: dal niente alla pagina aperta, in meno di due minuti.- Cambia una riga in
src/main.ts, salva e fai vedere la pagina che si aggiorna da sola mentre nessuno la ricarica. - Crea
src/saluta.tscon dentroexport function saluta(nome: string) { … }e importala inmain.ts. Poi sbaglia apposta: togli l'exporte leggi l'errore insieme alla classe. - Apri
tsconfig.json, metti"strict": truee mostra la riga che diventa rossa.
Laboratorio (60')
vanilla-ts ma dentro il browser, con
terminale e anteprima. Gli esercizi da 6.3 in poi si fanno identici, riga per riga. Per
la lezione 7 servirà la webcam: chi è rimasto online lavora in coppia
con un compagno che ha il progetto in locale, o prova il codice sul filmato registrato
dal docente. L'anteprima di StackBlitz in una scheda separata accende la camera, ma è
l'ultima possibilità e va detto perché: quella pagina arriva dai server di StackBlitz e
non ha la CSP del nostro progetto. Prendi nota di chi è in questa situazione:
da lì in poi si lavora a coppie fisse, non improvvisate ogni volta.
Esercizio 6.1 — Installare gli strumenti base
Tre installazioni, in quest'ordine. Se una macchina ce le ha già, salta e passa all'esercizio 6.2.
- Node.js, versione LTS (quella consigliata, non la «Current»): nodejs.org. Avanti, avanti, fine: non cambiare nessuna opzione.
- Visual Studio Code: code.visualstudio.com.
- In VS Code, due estensioni utili (icona dei quadratini nella barra a sinistra): Italian Language Pack se preferisci i menu in italiano, e Prettier, che rimette in ordine il codice da solo quando salvi. Il supporto a TypeScript è già dentro VS Code: non c'è niente da installare per quello.
Poi apri il terminale di VS Code (menu Terminale → Nuovo terminale) e scrivi:
node -v
npm -v
Cosa devi vedere
v22.20.0 # un numero pari ≥ 20, il tuo può essere diverso
10.9.3 # qualunque numero ≥ 9
Se invece compare «node non è riconosciuto come comando», quasi sempre basta chiudere e riaprire VS Code: il terminale aveva già in memoria l'elenco dei programmi di prima dell'installazione. Se dopo la riapertura l'errore resta, passa al piano B e riprova a casa: non perdere l'ora di laboratorio.
Esercizio 6.2 — Il tuo progetto base
Crea un progetto chiamato mano-corso, installa gli strumenti e avvialo.
Poi apri l'indirizzo che compare e fai vedere la pagina al tuo vicino di banco.
Soluzione
npm create vite@latest mano-corso -- --template vanilla-ts
cd mano-corso
npm install
npm run dev
Il terminale scrive Local: http://localhost:5173/. In molti terminali si
apre con Ctrl + clic sull'indirizzo. La finestra con npm run
dev resta aperta per tutta la lezione: se la chiudi, la pagina smette di
rispondere.
Esercizio 6.3 — Fai piazza pulita base
Il progetto appena creato contiene un esempio con i loghi di Vite. Non serve. Riduci
index.html alla pagina che ti servirà davvero — un titolo, un pulsante e
un canvas — e svuota src/main.ts lasciando solo l'importazione del foglio
di stile e un console.log. Controlla nella console del browser
(F12) che il messaggio compaia.
Soluzione
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>La mia mano</title>
</head>
<body>
<h1>La mia mano</h1>
<button id="disegna">Disegna</button>
<canvas id="tela" width="640" height="480"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
// src/main.ts
import "./style.css";
console.log("il progetto è vivo");
Cancella pure src/counter.ts e src/typescript.svg: se poi
la pagina si lamenta di non trovarli, vuol dire che era rimasta un'importazione da
togliere. È un ottimo primo errore da leggere.
Esercizio 6.4 — Il trasloco della lezione 5 medio
Riprendi il codice della lezione 5 — la mano finta disegnata sul canvas — e portalo qui dentro, diviso in due file:
src/disegna.ts: l'interfacciaPunto, la funzione che disegna i punti e quella che disegna le ossa. Tutte esportate.src/main.ts: prende il canvas dalla pagina, importa le funzioni e le chiama.
Se nella lezione 5 avevi dato nomi diversi alle funzioni, tienili: l'esercizio è il trasloco, non la riscrittura.
Soluzione
// src/disegna.ts
export interface Punto {
x: number;
y: number;
z: number;
}
/** Le 21 ossa della mano: ogni coppia dice quali due punti collegare. */
export const OSSA: number[][] = [
[0, 1], [1, 2], [2, 3], [3, 4],
[0, 5], [5, 6], [6, 7], [7, 8],
[5, 9], [9, 10], [10, 11], [11, 12],
[9, 13], [13, 14], [14, 15], [15, 16],
[13, 17], [17, 18], [18, 19], [19, 20],
[0, 17],
];
export function disegnaPunti(ctx: CanvasRenderingContext2D, punti: Punto[]): void {
ctx.fillStyle = "#ffffff";
for (const p of punti) {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
export function disegnaOssa(ctx: CanvasRenderingContext2D, punti: Punto[]): void {
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 3;
for (const osso of OSSA) {
const a = punti[osso[0]];
const b = punti[osso[1]];
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
// src/main.ts
import "./style.css";
import { disegnaOssa, disegnaPunti, type Punto } from "./disegna";
import { MANO_FINTA } from "./punti";
const tela = document.getElementById("tela") as HTMLCanvasElement;
const ctx = tela.getContext("2d") as CanvasRenderingContext2D;
const punti: Punto[] = MANO_FINTA;
ctx.clearRect(0, 0, tela.width, tela.height);
disegnaOssa(ctx, punti);
disegnaPunti(ctx, punti);
Le funzioni sono rimaste identiche a quelle della lezione 5: l'unica cosa nuova sono
le parole export e import. È esattamente così che è fatto il
progetto vero.
Esercizio 6.5 — Il secondo modulo: i dati medio
Il file disegna.ts sa come si disegna; non deve sapere
cosa. Crea allora src/punti.ts con dentro due cose esportate:
MANO_FINTA: i 21 punti inventati della lezione 5, ma con le coordinate fra 0 e 1, come arriveranno dalla webcam alla lezione 8;aPixel(p, larghezza, altezza): la funzione della lezione 3 che converte un punto normalizzato in pixel.
Poi in main.ts converti i punti prima di disegnarli.
Soluzione
// src/punti.ts
import type { Punto } from "./disegna";
/** Una mano aperta inventata: coordinate fra 0 e 1, come quelle vere. */
export const MANO_FINTA: Punto[] = [
{ x: 0.50, y: 0.90, z: 0 }, // 0 polso
{ x: 0.40, y: 0.84, z: 0 }, // 1 pollice
{ x: 0.33, y: 0.76, z: 0 }, // 2
{ x: 0.28, y: 0.68, z: 0 }, // 3
{ x: 0.24, y: 0.61, z: 0 }, // 4 punta del pollice
{ x: 0.44, y: 0.62, z: 0 }, // 5 indice
{ x: 0.43, y: 0.50, z: 0 }, // 6
{ x: 0.42, y: 0.42, z: 0 }, // 7
{ x: 0.42, y: 0.35, z: 0 }, // 8 punta dell'indice
{ x: 0.52, y: 0.60, z: 0 }, // 9 medio
{ x: 0.52, y: 0.47, z: 0 }, // 10
{ x: 0.52, y: 0.38, z: 0 }, // 11
{ x: 0.52, y: 0.31, z: 0 }, // 12 punta del medio
{ x: 0.59, y: 0.62, z: 0 }, // 13 anulare
{ x: 0.60, y: 0.50, z: 0 }, // 14
{ x: 0.61, y: 0.42, z: 0 }, // 15
{ x: 0.61, y: 0.35, z: 0 }, // 16 punta dell'anulare
{ x: 0.65, y: 0.67, z: 0 }, // 17 mignolo
{ x: 0.68, y: 0.58, z: 0 }, // 18
{ x: 0.70, y: 0.52, z: 0 }, // 19
{ x: 0.72, y: 0.46, z: 0 }, // 20 punta del mignolo
];
/** Da coordinate 0…1 a pixel di una tela larga `larghezza` e alta `altezza`. */
export function aPixel(p: Punto, larghezza: number, altezza: number): Punto {
return { x: p.x * larghezza, y: p.y * altezza, z: p.z };
}
// src/main.ts — la parte che cambia
import { MANO_FINTA, aPixel } from "./punti";
const punti = MANO_FINTA.map((p) => aPixel(p, tela.width, tela.height));
disegnaOssa(ctx, punti);
disegnaPunti(ctx, punti);
Adesso la divisione dei compiti è netta: punti.ts tiene i dati,
disegna.ts il disegno, main.ts mette in fila le due cose.
Alla lezione 8 cambierà solo la prima riga: i punti finti
diventeranno quelli veri della tua mano, e il disegno non se ne accorgerà nemmeno.
Esercizio 6.6 — Accendi strict sfida
Apri tsconfig.json, assicurati che ci sia "strict": true
dentro compilerOptions, salva e guarda cosa si colora di rosso. Correggi
tutto, poi lancia npx tsc --noEmit per avere la conferma dal compilatore
invece che dall'editor.
Soluzione
I due errori che compaiono quasi sempre, e come si sistemano:
// 1) 'const ctx' is possibly 'null'
// getContext("2d") restituisce null se il canvas non supporta il 2D.
const ctx = tela.getContext("2d");
if (!ctx) throw new Error("questo browser non sa disegnare in 2D");
// 2) Parameter 'p' implicitly has an 'any' type
// dentro map il tipo di solito è dedotto; se non lo è, si scrive.
const punti = MANO_FINTA.map((p: Punto) => aPixel(p, tela.width, tela.height));
npx tsc --noEmit significa «controlla i tipi e non scrivere niente»:
è lo stesso controllo che il progetto vero fa prima di ogni pubblicazione. Se non
stampa niente, va tutto bene — i programmi di controllo, quando sono contenti,
tacciono.
node -vstampa un numero;npm -vstampa un numero;- nella cartella del progetto,
npm run devparte e stampa un indirizzo; http://localhost:5173mostra la tua pagina, con la mano finta disegnata;- salvando un file, la pagina si aggiorna da sola.
Errori tipici in aula
- Comando lanciato nella cartella sbagliata:
npm run devrisponde «Could not read package.json». Manca ilcd mano-corso. Il terminale mostra sempre in quale cartella sei: si legge prima di scrivere. - Spazi o accenti nel percorso: una cartella chiamata
Documenti scuola/progèttoprima o poi dà problemi. Usa nomi corti, minuscoli, senza spazi. - Il server è già acceso: «Port 5173 is in use». Vite ne sceglie un'altra e lo scrive — oppure si ferma il primo con Ctrl + C.
- La pagina è bianca: quasi sempre un errore in
main.ts. Si apre la console del browser con F12 e si legge. La console è il primo posto dove guardare, non l'ultimo. - «L'ho corretto ma l'errore resta»: il file non era salvato. Ctrl + S. Il pallino accanto al nome del file, in alto, significa «non salvato».
- Percorso di
importsenza./:import { … } from "disegna"fa cercare una libreria di nome «disegna» dentronode_modules. Ci vuole"./disegna". node_modules«occupa troppo spazio»: sì, centinaia di megabyte, ed è normale. Non si tocca, non si copia, non si mette in una chiavetta: si ricrea in trenta secondi connpm install.
Verifica veloce (5 domande)
Domande e risposte
- Perché non possiamo restare sugli ambienti online? → Perché i browser negano la webcam alle anteprime incorniciate che arrivano da un altro sito, e dalla lezione 7 la webcam serve.
- Cosa fa
npm install? → Leggepackage.jsone scarica innode_modulesle librerie e gli strumenti elencati lì. - Cosa succede ai tipi quando il codice arriva al browser? → Vengono cancellati: il browser esegue JavaScript normale.
- Cosa serve per usare in
main.tsuna funzione scritta indisegna.ts? →exportdavanti alla funzione eimport { … } from "./disegna"in cima al file. - Cosa cambia accendendo
strict? → Il compilatore non accetta più tipi impliciti e obbliga a gestire i casi in cui un valore può esserenull.
Compito per casa (30-40 minuti)
Sistema il progetto e rendilo tuo: metti la mano finta al centro della tela qualunque sia
la dimensione del canvas, aggiungi un terzo modulo src/colori.ts che esporta
i colori usati dal disegno (così cambiando un file cambia tutto l'aspetto), e verifica
che npx tsc --noEmit non dica niente. Porta la prossima volta il progetto che
parte: la lezione 7 comincia con npm run dev già acceso.
La webcam: async, permessi, fotogrammi
Oggi la tua pagina guarda. Per riuscirci servono tre cose nuove: chiedere qualcosa che
richiede tempo (async e await), reagire quando va storto
(try/catch) e ridisegnare trenta volte al secondo senza
inceppare il browser.
async/await · errori · permessi · il ciclo dei fotogrammicamera.ts: accendi, spegni, e la spia della webcam che si spegne davveroTeoria
Alcune cose non sono pronte subito
Finora ogni riga di codice dava il suo risultato immediatamente. Accendere la webcam no: il browser deve chiedere il permesso all'utente, che magari ci pensa cinque secondi, poi deve accendere una telecamera fisica. Nel frattempo il programma non può fermarsi: se si fermasse, la pagina si bloccherebbe e nemmeno il pulsante «Consenti» funzionerebbe.
La soluzione di JavaScript si chiama Promise: non il
risultato, ma la promessa di un risultato. Una funzione come
getUserMedia ti restituisce subito una promessa, e quella promessa prima o
poi si mantiene (arriva il flusso video) oppure si rompe (l'utente ha detto no).
Per lavorare con le promesse ci sono due parole:
async function accendi() {
const flusso = await navigator.mediaDevices.getUserMedia({ video: true });
console.log("acceso!", flusso);
}
-
awaitvuol dire «aspetta che questa promessa si mantenga, poi dammi il valore». Mentre aspetta, il resto della pagina continua a funzionare: i pulsanti rispondono, le animazioni girano. -
asyncdavanti afunctionè il permesso di usareawaitlì dentro. Se lo dimentichi, il compilatore te lo dice subito.
async restituisce sempre una promessa, anche
quando dentro scrivi return 5. Per questo il suo tipo di ritorno si scrive
Promise<number>, e quando non restituisce niente
Promise<void>. Quelle parentesi angolari dicono «promessa di
che cosa».
async function accendiCamera(video: HTMLVideoElement): Promise<MediaStream> {
const flusso = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
video.srcObject = flusso;
await video.play();
return flusso;
}
Quando va storto: try / catch
Una promessa può rompersi, e con la webcam si rompe spesso: permesso negato, nessuna telecamera, telecamera già usata da un altro programma. Se non fai niente, l'utente vede una pagina che non reagisce e non capisce perché.
try {
flusso = await accendiCamera(video);
// qui ci arrivi solo se è andata bene
} catch (e) {
// qui ci arrivi solo se è andata male
console.log("non ha funzionato:", e);
}
Il browser non ti manda una frase, ti manda un errore con un nome. I tre che vedrai in aula oggi:
| Nome | Cosa è successo | Cosa dire all'utente |
|---|---|---|
NotAllowedError | L'utente ha negato il permesso, o l'aveva già negato in passato | «Consenti l'uso della camera dalle impostazioni del browser e riprova» |
NotFoundError | Non c'è nessuna webcam collegata | «Nessuna webcam trovata su questo dispositivo» |
NotReadableError | C'è, ma la sta usando un altro programma | «La webcam è occupata da un'altra applicazione» |
Tradurli è banale e cambia tutto: è la differenza fra un'applicazione e un compito.
Il contesto sicuro
La webcam si accende solo se la pagina arriva da https://… oppure da
localhost. Su un http:// normale,
navigator.mediaDevices non esiste proprio: il codice
fallisce con un errore oscuro.
Il motivo è serio. Su http:// chiunque stia in mezzo alla rete — il gestore
del wi-fi, per dire — può cambiare la pagina mentre viaggia, e quindi far accendere la
telecamera a una pagina che non lo aveva chiesto. https lo impedisce.
localhost è ammesso perché non viaggia da nessuna parte: è il tuo computer.
if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) {
throw new Error("La camera funziona solo su una pagina HTTPS o su localhost.");
}
È esattamente il controllo che fa il progetto vero, prima di ogni altra cosa. E ti spiega anche perché alla lezione 10, quando parleremo di mettere il sito online, HTTPS non sarà un dettaglio facoltativo.
Dove finisce il video
Il flusso della webcam non si guarda da solo: va agganciato a un elemento
<video>.
<video id="video" autoplay muted playsinline></video>
<canvas id="tela" width="640" height="480"></canvas>
Tre attributi, tre motivi:
autoplay— parte da solo, senza un secondo clic;muted— senza questo i browser bloccano la riproduzione automatica; e comunque a noi l'audio non serve, infatti chiediamoaudio: false;playsinline— sui telefoni impedisce che il video si apra a tutto schermo mangiandosi la pagina.
Il <video> però è solo una finestrella. Per disegnarci sopra i punti
della mano ti serve il canvas, e per prima cosa devi copiarci dentro l'immagine:
ctx.drawImage(video, 0, 0, tela.width, tela.height);
Una riga, e nel canvas c'è il fotogramma corrente. Da lì in poi disegni quello che vuoi sopra: alla lezione 8 saranno 21 puntini.
Il ciclo dei fotogrammi
Disegnare una volta sola non basta: l'immagine cambia in continuazione. Serve un ciclo che riparta a ogni fotogramma, e lo fornisce il browser:
function ciclo(): void {
requestAnimationFrame(ciclo); // prenota il prossimo giro
ctx.drawImage(video, 0, 0, tela.width, tela.height);
}
ciclo(); // il primo giro lo chiami tu
requestAnimationFrame — «chiedi un fotogramma di animazione» — dice al
browser: richiamami un istante prima del prossimo ridisegno dello schermo. Di
solito sono 60 volte al secondo.
Perché non un timer, cioè setInterval(ciclo, 16)? Tre motivi concreti:
- il timer non sa quando lo schermo si ridisegna, quindi ogni tanto disegni due volte fra un ridisegno e l'altro (lavoro buttato) o zero volte (scatto visibile);
-
se il computer è lento, i timer si accumulano e la pagina si ingolfa;
requestAnimationFrameinvece salta semplicemente un giro; -
quando cambi scheda, il browser sospende
requestAnimationFrame: niente batteria consumata per disegnare una pagina che nessuno guarda. Il timer continuerebbe imperterrito.
Il ciclo si ferma con cancelAnimationFrame, e per farlo bisogna essersi
tenuti il numero che requestAnimationFrame restituisce:
let idCiclo = 0;
function ciclo(): void {
idCiclo = requestAnimationFrame(ciclo);
// …disegno…
}
function fermaCiclo(): void {
cancelAnimationFrame(idCiclo);
}
Spegnere la camera davvero
Questa è la parte che quasi tutti sbagliano la prima volta. Mettere in pausa il
<video> non spegne la telecamera: il flusso resta
aperto e la lucina accanto all'obiettivo resta accesa. Bisogna fermare le
tracce, cioè i singoli canali del flusso, una per una:
function spegniCamera(video: HTMLVideoElement, flusso: MediaStream | null): void {
flusso?.getTracks().forEach((t) => t.stop()); // ← questa è la riga che conta
video.pause();
video.srcObject = null;
}
La verifica è fisica e non ammette discussioni: la spia della webcam deve spegnersi. Se resta accesa, non hai spento niente.
Demo del docente (15')
- Pagina con due pulsanti e un
<video>. ScriviaccendiCameradal vivo e fai comparire la richiesta di permesso sul proiettore. - Nega il permesso apposta. Mostra che la pagina non dice niente, poi aggiungi il
try/catche il messaggio tradotto. È il momento più utile della lezione. - Scrivi
spegniCameraprima senzagetTracks().forEach(...): la spia resta accesa. Poi aggiungi la riga: la spia si spegne. Fai vedere la lucina, non il codice. - Aggiungi il ciclo con
requestAnimationFramee ildrawImagesul canvas: adesso ci sono due immagini identiche, il video e la tela. Chiedi alla classe perché ne servono due — la risposta («sulla tela possiamo disegnarci sopra») è la lezione 8.
Laboratorio (60')
Esercizio 7.1 — Accendi la camera base
Nella tua pagina metti un pulsante «Accendi la camera» e un elemento
<video>. Al clic sul pulsante, la webcam si accende e si vede.
Niente di più.
<!-- index.html -->
<button id="accendi">Accendi la camera</button>
<button id="spegni">Spegni</button>
<p id="messaggio"></p>
<video id="video" autoplay muted playsinline></video>
<canvas id="tela" width="640" height="480"></canvas>
Soluzione
// src/main.ts
import "./style.css";
const bottoneAccendi = document.getElementById("accendi") as HTMLButtonElement;
const video = document.getElementById("video") as HTMLVideoElement;
let flusso: MediaStream | null = null;
async function accendiCamera(): Promise<void> {
flusso = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
video.srcObject = flusso;
await video.play();
}
bottoneAccendi.addEventListener("click", () => {
void accendiCamera();
});
Quel void davanti alla chiamata dice «so che è una promessa e so che non
la sto aspettando»: senza, il compilatore in modalità severa può protestare. La
camera si accende solo al clic, mai all'apertura della pagina: è
una regola del progetto ed è anche il modo in cui il browser mostra la richiesta di
permesso in un momento che ha senso per l'utente.
Esercizio 7.2 — Errori che si capiscono base
Metti l'accensione dentro try/catch e scrivi nel paragrafo
#messaggio una frase comprensibile. Usa un oggetto di
messaggi invece di una catena di if. Poi provalo davvero: nega il
permesso dalla finestrella del browser.
Soluzione
const messaggio = document.getElementById("messaggio") as HTMLParagraphElement;
/** I nomi standard degli errori del browser, tradotti in frasi utili. */
const MESSAGGI: Record<string, string> = {
NotAllowedError: "Permesso negato: consenti l'uso della camera dalle impostazioni del browser e riprova.",
NotFoundError: "Nessuna webcam trovata su questo dispositivo.",
NotReadableError: "La webcam è occupata da un'altra applicazione.",
OverconstrainedError: "La webcam non supporta la risoluzione richiesta.",
};
async function accendiCamera(): Promise<void> {
messaggio.textContent = "";
try {
flusso = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
video.srcObject = flusso;
await video.play();
messaggio.textContent = "● Camera accesa";
} catch (e) {
const nome = e instanceof DOMException ? e.name : "";
messaggio.textContent = MESSAGGI[nome] ?? "Non riesco ad accendere la camera.";
}
}
Record<string, string> è il tipo di «un oggetto con chiavi di
testo e valori di testo»: un piccolo dizionario. Il ?? della lezione 4
copre i casi non previsti. Per rimettere il permesso com'era: icona della
telecamera nella barra degli indirizzi, oppure impostazioni del sito.
Esercizio 7.3 — Spegni davvero base
Fai funzionare il pulsante «Spegni». La prova non è il codice: è la spia della webcam che si spegne. Provala prima senza fermare le tracce, poi fermandole, e guarda la differenza.
Soluzione
const bottoneSpegni = document.getElementById("spegni") as HTMLButtonElement;
function spegniCamera(): void {
flusso?.getTracks().forEach((t) => t.stop());
video.pause();
video.srcObject = null;
flusso = null;
messaggio.textContent = "Camera spenta";
}
bottoneSpegni.addEventListener("click", spegniCamera);
Un MediaStream contiene una o più tracce (qui una sola, video).
Finché una traccia è viva, la telecamera è accesa. flusso?.getTracks()
usa il ?. della lezione 4: se il flusso è null, la riga non
fa niente invece di esplodere.
Esercizio 7.4 — Il video sulla tela, specchiato medio
Avvia un ciclo con requestAnimationFrame che copia ogni fotogramma sul
canvas. Poi specchia l'immagine: alzando la mano destra, sullo schermo
deve alzarsi la mano che ti sembra destra, come davanti a uno specchio. Ferma il ciclo
quando spegni la camera.
Soluzione
const tela = document.getElementById("tela") as HTMLCanvasElement;
const ctx = tela.getContext("2d");
if (!ctx) throw new Error("questo browser non sa disegnare in 2D");
let idCiclo = 0;
function ciclo(): void {
idCiclo = requestAnimationFrame(ciclo);
ctx.save(); // mette da parte l'orientamento attuale
ctx.translate(tela.width, 0); // sposta l'origine sul bordo destro
ctx.scale(-1, 1); // ribalta l'asse x: ecco lo specchio
ctx.drawImage(video, 0, 0, tela.width, tela.height);
ctx.restore(); // rimette tutto com'era
}
save e restore sono importanti: senza
restore, tutto quello che disegnerai dopo — punti,
ossa, scritte — resterebbe specchiato, e le scritte al contrario si notano parecchio.
Il ciclo va avviato dopo video.play() e fermato in
spegniCamera con cancelAnimationFrame(idCiclo), seguito da
ctx.clearRect(0, 0, tela.width, tela.height) per lasciare la tela pulita.
Esercizio 7.5 — Quanti fotogrammi al secondo medio
Mostra in un angolo della pagina quanti giri al secondo sta facendo il ciclo. Non si calcola sul singolo giro (il numero ballerebbe in modo illeggibile): si contano i giri e si divide una volta al secondo.
Soluzione
const etichettaFps = document.getElementById("fps") as HTMLSpanElement;
let fotogrammi = 0;
let inizioFinestra = performance.now();
function contaFps(): void {
fotogrammi++;
const adesso = performance.now();
const trascorso = adesso - inizioFinestra;
if (trascorso >= 1000) {
etichettaFps.textContent = `${Math.round((fotogrammi * 1000) / trascorso)} fps`;
fotogrammi = 0;
inizioFinestra = adesso;
}
}
Si chiama contaFps() dentro il ciclo.
performance.now() dà i millisecondi trascorsi dall'apertura della
pagina, con i decimali: è l'orologio giusto per misurare le durate, e lo ritroverai
tale e quale alla lezione 8. Su un computer normale leggerai circa 60: è il ritmo
dello schermo. Alla lezione 8, quando in mezzo ci sarà il riconoscimento, quel numero
diventerà molto più interessante.
Esercizio 7.6 — Camera spenta se nessuno guarda sfida
Fai in modo che cambiando scheda del browser la camera si spenga da sola. Lo strumento
è l'evento visibilitychange sul document e la proprietà
document.hidden. Verifica con la spia della webcam.
Soluzione
document.addEventListener("visibilitychange", () => {
if (document.hidden && flusso) spegniCamera();
});
Tre righe, ma è una scelta di riservatezza, non un ottimizzazione: se non stai
guardando la pagina, non c'è motivo che la pagina guardi te. Nel progetto vero c'è
esattamente questo, più un secondo controllo per il caso limite in cui l'utente
cambia scheda mentre la camera si sta accendendo. Chi vuole provarci:
dopo l'await dell'accensione, se document.hidden è vero,
spegni subito.
Errori tipici in aula
- Permesso negato per sbaglio la prima volta: il browser ricorda e non richiede più niente, quindi la pagina sembra rotta. Si rimedia dall'icona della telecamera nella barra degli indirizzi. Da mostrare a tutti prima del laboratorio, non dopo.
awaitfuori da una funzioneasync: errore del compilatore. La parolinaasyncva davanti allafunctionche contiene l'await.- Il video si vede ma la tela è nera: il ciclo non è partito, oppure il canvas ha larghezza e altezza zero.
widtheheightdel canvas si scrivono come attributi HTML, non come CSS. - Tutto specchiato, scritte comprese: manca
ctx.restore(). - La spia resta accesa dopo «Spegni»: manca
getTracks().forEach((t) => t.stop()). È il motivo per cui questo controllo va fatto con gli occhi sulla webcam. - Due cicli in parallelo: accendi, spegni, riaccendi, e improvvisamente gli fps raddoppiano. Ogni accensione avviava un ciclo senza fermare il precedente. Si risolve con
cancelAnimationFrame(idCiclo)prima di ogni avvio. - La webcam non parte nell'ambiente online: è l'anteprima incorniciata. Da qui in poi si lavora in locale, o in coppia con chi ce l'ha; l'anteprima in scheda separata è l'ultima possibilità (lezione 6).
Verifica veloce (5 domande)
Domande e risposte
- Cosa restituisce
getUserMedia, di preciso? → UnaPromise: la promessa di un flusso video, non il flusso. - Dove si può scrivere
await? → Dentro una funzione dichiarataasync. - Su quali indirizzi si accende la webcam? → Solo
https://…elocalhost: il contesto sicuro. - Perché
requestAnimationFramee nonsetInterval? → Perché segue il ritmo dello schermo, non accumula ritardi e si sospende quando la scheda non è visibile. - Basta
video.pause()per spegnere la camera? → No: finché le tracce del flusso non vengono fermate, la telecamera resta accesa.
Compito per casa (30-40 minuti)
Sposta tutto quello che riguarda la camera in un modulo src/camera.ts, che
esporta due funzioni sole: accendiCamera(video) che restituisce
Promise<MediaStream> e spegniCamera(video, flusso). In
main.ts devono restare i pulsanti, i messaggi e il ciclo. Aggiungi anche il
controllo del contesto sicuro all'inizio dell'accensione. È il file camera.ts
del progetto vero, scritto da te: la prossima lezione lo userai così com'è.
MediaPipe: i 21 punti della mano vera
È la lezione in cui il progetto funziona. Alla fine di queste due ore muovi la mano davanti alla webcam e sullo schermo si muove una mano disegnata da te, con le tue dita e i tuoi movimenti. Tutto il resto del corso sono rifiniture.
console.log alla mano completa sullo schermoTeoria
Cos'è un modello, in due paragrafi
Finora ogni riga del tuo programma diceva al computer esattamente cosa fare: «disegna un cerchio qui», «moltiplica questo per quello». Trovare una mano in un'immagine così non si può fare: nessuno sa scrivere le regole per riconoscere una mano — con qualunque luce, qualunque pelle, qualunque anello, qualunque sfondo. Ci hanno provato per vent'anni.
Un modello è un altro approccio: invece delle regole, gli
esempi. Qualcuno — qui Google — ha raccolto decine di migliaia di fotografie di
mani e ha segnato a mano, su ognuna, dove stanno le 21 articolazioni. Poi un programma di
addestramento ha aggiustato, un pochino alla volta, milioni di numeri interni finché il
sistema non ha cominciato a indovinare quei 21 punti anche su fotografie mai viste. Il
risultato di quel lavoro è un file: gesture_recognizer.task,
otto megabyte di numeri. Non contiene fotografie, non contiene mani: contiene solo i
numeri che servono a fare quel calcolo.
Da questo discendono tre cose pratiche, che vale la pena avere chiare:
- Il modello si scarica una volta e gira sul tuo computer. Non c'è nessun server da interrogare: l'immagine della tua mano non esce dal browser, mai. È il motivo per cui questo progetto può promettere quello che promette.
- Il modello sbaglia. Non «può sbagliare»: sbaglia, e lo vedrai oggi. In controluce, con la mano storta o mezza fuori dall'inquadratura, i punti ballano o la mano sparisce. Non è un bug tuo: è la natura della cosa, e alla lezione 9 imparerai a conviverci.
- Il modello non capisce niente. Dà numeri, e i numeri hanno una confidenza. Non sa cos'è una mano, non sa che è la tua.
Installare una libreria, e fissarne la versione
npm install @mediapipe/tasks-vision@1.0.1
Questo comando scarica la libreria di Google dentro node_modules e aggiunge
una riga in package.json. Quel @1.0.1 alla fine è la parte
importante della lezione.
"dependencies": {
"@mediapipe/tasks-vision": "1.0.1"
}
Scritto così — senza ^, senza ~, senza
@latest — vuol dire «esattamente questa versione». Chiunque installi il
progetto fra due anni avrà gli stessi identici file, e il progetto funzionerà come oggi.
@latest il tuo progetto cambia da solo, in un giorno che non hai scelto,
mentre tu non stai guardando. Una mattina si apre e non funziona più, e tu non hai toccato
niente: è una delle esperienze più frustranti che esistano nel mestiere. Nel progetto vero
della mano sono fissati con precisione tre elementi, e devono essere
allineati fra loro: la libreria, la cartella wasm che la accompagna e il file
del modello .task. La documentazione ufficiale di Google usa
@latest negli esempi. Va benissimo per un esempio; non per qualcosa che deve
continuare a funzionare. Da non confondere con il comando della lezione 6,
npm create vite@latest: lì @latest è la versione dello
strumento che crea la cartella una volta sola, non una dipendenza che resta nel
progetto.
Caricare il riconoscitore
Servono due indirizzi: il runtime (il motore di calcolo, scritto in
WebAssembly) e il modello. Il caricamento richiede tempo — sono una ventina di megabyte
la prima volta — quindi è tutto async, come la webcam della lezione scorsa.
// src/tracker.ts
import { FilesetResolver, GestureRecognizer } from "@mediapipe/tasks-vision";
const WASM = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.0.1/wasm";
const MODELLO =
"https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/1/gesture_recognizer.task";
export async function creaRiconoscitore(): Promise<GestureRecognizer> {
const vision = await FilesetResolver.forVisionTasks(WASM);
return GestureRecognizer.createFromOptions(vision, {
baseOptions: { modelAssetPath: MODELLO },
runningMode: "VIDEO",
numHands: 1,
});
}
Riga per riga, le tre cose che contano:
-
FilesetResolver.forVisionTasksprepara il motore: guarda cosa sa fare il tuo processore e sceglie da solo la variante giusta del WebAssembly. -
runningMode: "VIDEO"dice «ti arriveranno fotogrammi in sequenza, non foto scollegate». In questa modalità il riconoscitore insegue la mano da un fotogramma all'altro invece di ricercarla ogni volta da zero: è molto più veloce e molto più stabile. L'alternativa è"IMAGE", per le fotografie singole. -
numHands: 1— una mano basta per cominciare. Metterlo a 2 è l'esercizio 8.5.
Chiedere i punti, a ogni fotogramma
const risultato = riconoscitore.recognizeForVideo(video, performance.now());
Questa riga va dentro il ciclo della lezione 7. Due dettagli, entrambi fonte di errori il primo giorno:
-
Non è
await. In modalità VIDEO il calcolo avviene subito e restituisce subito il risultato. È anche il motivo per cui, se il computer è lento, gli fps crollano: il disegno aspetta il riconoscimento. -
Il tempo deve crescere sempre. Il secondo parametro è l'istante del
fotogramma, in millisecondi. Se passi un tempo uguale o minore del precedente, la
libreria si ferma con un errore. Con
performance.now()non succede, perché un orologio va avanti — ma la regola va conosciuta, perché è la causa di un errore che altrimenti sembra incomprensibile.
C'è anche un accorgimento che fa risparmiare metà del lavoro. Lo schermo si ridisegna 60 volte al secondo, ma la webcam manda 30 fotogrammi al secondo: metà dei giri del ciclo vedono la stessa immagine di prima. Analizzarla due volte è lavoro buttato:
let ultimoIstante = -1;
function ciclo(): void {
idCiclo = requestAnimationFrame(ciclo);
// readyState >= 2 significa "c'è un fotogramma pronto"
if (video.readyState >= 2 && video.currentTime !== ultimoIstante) {
ultimoIstante = video.currentTime;
const risultato = riconoscitore.recognizeForVideo(video, performance.now());
disegnaTutto(risultato);
}
}
Com'è fatto il risultato
risultato.landmarks è un array di mani; ogni mano è un
array di 21 punti; ogni punto ha x, y,
z. Tre livelli, e bisogna tenerli distinti:
risultato.landmarks // [ [21 punti], [21 punti] ] ← 0, 1 o 2 mani
risultato.landmarks[0] // i 21 punti della prima mano
risultato.landmarks[0][8] // la punta dell'indice
risultato.landmarks[0][8].x // 0.42 — un numero fra 0 e 1
| Campo | Significato | Intervallo |
|---|---|---|
x | posizione orizzontale sull'immagine | 0 = bordo sinistro … 1 = bordo destro |
y | posizione verticale | 0 = alto … 1 = basso |
z | profondità rispetto al polso | stessa scala della x; negativa = più vicino alla camera |
Sono le stesse coordinate fra 0 e 1 della mano finta della lezione 5, e non è un caso: il corso è stato costruito perché oggi tu debba cambiare una riga sola. Sono numeri relativi all'immagine, non pixel: se domani cambi la dimensione della tela, i punti continuano a funzionare.
E i numeri dei punti sono sempre gli stessi, per tutti: 0 il polso, 4 la punta del pollice, 8 la punta dell'indice, 12 il medio, 16 l'anulare, 20 il mignolo. Sono i numeri che hai già usato nella tabella delle ossa alla lezione 6.
Quando la mano non c'è
Se togli la mano dall'inquadratura, landmarks è un array
vuoto. Scrivere risultato.landmarks[0][0].x senza
controllare fa esplodere il programma. Il ?. della lezione 4 serve
esattamente a questo:
const mano = risultato.landmarks[0];
if (!mano) return; // niente mano in questo fotogramma: si salta il disegno
// oppure, in una riga:
const polso = risultato.landmarks[0]?.[0];
console.log(polso?.x ?? "nessuna mano");
Ed è la risposta pratica a quello che si diceva alla lezione 6: i tipi spariscono prima dell'esecuzione, quindi non ti proteggono mentre il programma gira. Il controllo lo devi scrivere tu.
Demo del docente (15')
npm install @mediapipe/tasks-vision@1.0.1sul proiettore; apripackage.jsone mostra la riga comparsa, senza^.- Scrivi
creaRiconoscitoree chiamala all'avvio. Mostra il pannello Rete degli strumenti per sviluppatori: si vedono scendere i megabyte del modello. Poi ricarica: la seconda volta è istantaneo, perché il browser li ha in cache. - Nel ciclo,
console.log(risultato.landmarks.length): muovi la mano dentro e fuori dall'inquadratura e commenta gli 1 e gli 0 che scorrono. - Stampa
risultato.landmarks[0]?.[8]e muovi lentamente l'indice: fai leggere alla classe laxche va da 0,1 a 0,9. È il momento in cui i numeri diventano il proprio dito: vale la pena lasciarlo respirare mezzo minuto. - Sbaglia apposta: passa
0invece diperformance.now()e leggi l'errore sui tempi crescenti.
Laboratorio (60')
public/, con modelAssetPath: "/gesture_recognizer.task": è
esattamente la soluzione del progetto vero.
Esercizio 8.1 — Il modello parla base
Installa la libreria alla versione 1.0.1, crea
src/tracker.ts con creaRiconoscitore() e, nel ciclo dei
fotogrammi, stampa in console quante mani vede e quanti punti ha la prima. Muovi la
mano dentro e fuori dall'inquadratura e guarda i numeri cambiare.
Soluzione
// src/main.ts — le parti nuove
import { creaRiconoscitore } from "./tracker";
import type { GestureRecognizer } from "@mediapipe/tasks-vision";
let riconoscitore: GestureRecognizer | null = null;
let ultimoIstante = -1;
async function avvia(): Promise<void> {
messaggio.textContent = "Carico il modello…";
riconoscitore = await creaRiconoscitore(); // prima il modello: non chiede niente all'utente
await accendiCamera(); // poi la camera: il permesso si chiede alla fine
ciclo();
}
function ciclo(): void {
idCiclo = requestAnimationFrame(ciclo);
if (!riconoscitore) return;
if (video.readyState < 2 || video.currentTime === ultimoIstante) return;
ultimoIstante = video.currentTime;
const risultato = riconoscitore.recognizeForVideo(video, performance.now());
console.log("mani:", risultato.landmarks.length, "punti:", risultato.landmarks[0]?.length ?? 0);
}
L'ordine conta: prima il modello, poi la camera. Così la richiesta di permesso arriva quando manca poco al via, invece di tenere la telecamera accesa per dieci secondi mentre si scarica il modello. È lo stesso ordine del progetto vero. Il numero dei punti, quando la mano c'è, è sempre 21.
Esercizio 8.2 — I punti veri sullo schermo base
Basta console. Disegna i 21 punti sulla tela, sopra l'immagine della webcam,
riutilizzando aPixel della lezione 6 e disegnaPunti della
lezione 5. Cancella la tela a ogni fotogramma, altrimenti resta la scia.
Soluzione
import { disegnaPunti, type Punto } from "./disegna";
import { aPixel } from "./punti";
function mostra(punti: Punto[]): void {
ctx.save();
ctx.translate(tela.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(video, 0, 0, tela.width, tela.height);
ctx.restore();
const inPixel = punti.map((p) => aPixel(p, tela.width, tela.height));
disegnaPunti(ctx, inPixel);
}
// nel ciclo, dopo aver ottenuto il risultato:
const mano = risultato.landmarks[0];
if (mano) mostra(mano);
else ctx.clearRect(0, 0, tela.width, tela.height);
I puntini staranno dalla parte sbagliata, ed è giusto così: hai
specchiato l'immagine con scale(-1, 1) ma non i punti. Due strade: o non
specchi il video, o specchi anche le coordinate con x: 1 - p.x. La
seconda è quella del progetto vero, ed è la stessa idea dello 0.5 - x che
incontrerai alla lezione 10.
Esercizio 8.3 — Le ossa medio
Aggiungi le 21 ossa con disegnaOssa, la funzione che hai già scritto alla
lezione 6. Disegna prima le ossa e poi i punti: le articolazioni devono stare sopra le
linee, non sotto.
Soluzione
import { disegnaOssa, disegnaPunti } from "./disegna";
const inPixel = punti.map((p) => aPixel(p, tela.width, tela.height));
disegnaOssa(ctx, inPixel); // prima le linee
disegnaPunti(ctx, inPixel); // poi i pallini, sopra
Ed eccolo il momento della lezione: la funzione non è cambiata di una virgola. Alla lezione 6 riceveva venti punti inventati; adesso riceve la tua mano. Questo è il guadagno vero di aver separato «come si disegna» da «cosa si disegna» — e per il resto del corso continuerà a ripagare.
Esercizio 8.4 — Che gesto sto facendo medio
Il modello che stai usando non dà solo i punti: riconosce anche otto gesti. Mostra il
nome del gesto corrente, tradotto in italiano, in un elemento della
pagina. Quando non riconosce niente il nome è "None", e non va mostrato.
Soluzione
const etichettaGesto = document.getElementById("gesto") as HTMLParagraphElement;
const GESTI: Record<string, string> = {
Closed_Fist: "✊ Pugno",
Open_Palm: "🖐️ Mano aperta",
Pointing_Up: "☝️ Indice in su",
Thumb_Down: "👎 Pollice giù",
Thumb_Up: "👍 Pollice su",
Victory: "✌️ Vittoria",
ILoveYou: "🤟 Ti voglio bene",
};
const gesto = risultato.gestures[0]?.[0];
const nome = gesto?.categoryName;
etichettaGesto.textContent =
nome && nome !== "None" ? GESTI[nome] ?? nome : "nessun gesto riconosciuto";
Due ?. in fila perché i livelli sono due: gestures ha un
elenco per ogni mano, e ogni mano ha un elenco di ipotesi ordinate dalla più probabile
— [0] è quella buona. Chi vuole: gesto?.score è la
confidenza, fra 0 e 1. Mostrarla come percentuale fa capire meglio di qualunque
spiegazione che il modello stima, non sa.
Esercizio 8.5 — Due mani, due colori sfida
Porta numHands a 2 e disegna la prima mano in azzurro e la seconda in
rosa. Serve un ciclo su risultato.landmarks e un colore passato alle
funzioni di disegno.
Soluzione
// tracker.ts: numHands: 2
// disegna.ts: le due funzioni prendono un colore in più
export function disegnaOssa(ctx: CanvasRenderingContext2D, punti: Punto[], colore: string): void {
ctx.strokeStyle = colore;
ctx.lineWidth = 3;
for (const osso of OSSA) {
const a = punti[osso[0]];
const b = punti[osso[1]];
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
// main.ts
const COLORI = ["#38bdf8", "#f472b6"];
risultato.landmarks.forEach((mano, i) => {
const inPixel = mano.map((p) => aPixel(p, tela.width, tela.height));
disegnaOssa(ctx, inPixel, COLORI[i]);
disegnaPunti(ctx, inPixel, "#ffffff");
});
Attenzione a una cosa che scoprirai da solo: l'ordine delle mani non è
stabile. Da un fotogramma all'altro la tua destra può diventare
[1], e i colori si scambiano. Per risolverlo davvero serve
risultato.handedness[i]?.[0]?.categoryName, che dice «Left» o «Right» —
con un tranello: MediaPipe ragiona come se l'immagine fosse già specchiata, quindi
la sua «Left» è la tua destra. Nel progetto vero c'è una funzione di tre righe che
non fa altro che ribaltare quell'etichetta.
Errori tipici in aula
- «Cannot read properties of undefined»: si è letto
landmarks[0][0]in un fotogramma senza mani. Serve il controllo conifo il?.. - Errore sui tempi non crescenti: si è passato un numero fisso, o lo stesso
performance.now()salvato in una variabile e riusato. Il tempo si legge a ogni chiamata. - La pagina si blocca per dieci secondi al primo avvio: è il download dei venti megabyte. Non è un bug — ma va detto all'utente con un messaggio «Carico il modello…», altrimenti sembra rotto.
- Punti tutti ammassati nell'angolo in alto a sinistra: sono le coordinate 0…1 disegnate come se fossero pixel. Manca
aPixel. - Punti specchiati rispetto all'immagine: hai specchiato il video ma non le coordinate (o viceversa). Decidi una volta e sii coerente.
- La scia: manca
clearRect, oppure ildrawImagedel video non copre tutta la tela. - Due o tre fotogrammi al secondo: il riconoscimento gira sul processore invece che sulla scheda grafica, cosa frequente sui portatili scolastici. Non si risolve oggi; si annota e si misura all'esercizio 7.5.
Verifica veloce (5 domande)
Domande e risposte
- Dove gira il modello e dove finiscono le immagini della webcam? → Gira nel browser, sul computer dell'utente; le immagini non vengono inviate da nessuna parte.
- Perché fissare la versione con
@1.0.1? → Perché il progetto non cambi da solo quando esce una versione nuova, e perché libreria, runtime e modello restino allineati. - Che forma ha
risultato.landmarks? → Un array di mani; ogni mano è un array di 21 punti conx,y,zfra 0 e 1. - Cosa succede se passi due volte lo stesso istante a
recognizeForVideo? → Errore: in modalità VIDEO i tempi devono crescere sempre. - Come si passa da
x = 0.42a un pixel su una tela larga 640? → Si moltiplica: 0,42 × 640 ≈ 269. ÈaPixel, scritta alla lezione 3.
Compito per casa (30-40 minuti)
Rendi presentabile quello che hai: un messaggio «Carico il modello…» durante l'attesa, «Nessuna mano inquadrata» quando la mano non c'è, il nome del gesto in grande, e il contatore di fps della lezione 7 lasciato acceso in un angolo. Poi annota su un foglio quanti fps fa il tuo computer con il riconoscimento attivo e in quali condizioni la mano si perde (controluce? mano di taglio? troppo vicina?). Porta i numeri la prossima volta: la lezione 9 comincia da lì.
Rifinire: classi, stato, gesti, tremolio
Il progetto funziona già. Oggi si passa da «funziona» a «si può dare in mano a qualcuno»: la mano smette di vibrare, i pulsanti sanno sempre in che stato sono, e le mani fantasma smettono di lampeggiare.
MediaMobile dal vivo e mostra la mano prima e dopo, con il filtro acceso e spentoTeoria
Le classi: un oggetto che si porta dietro le sue funzioni
Alla lezione 4 hai imparato gli oggetti: scatole con dentro dei valori. Una classe è la stessa idea, con una cosa in più: dentro ci stanno anche le funzioni che lavorano su quei valori.
Guarda il problema di oggi. Per smorzare il tremolio devi ricordarti gli ultimi cinque valori di una coordinata. Con quello che sai finora:
let ultimiValori: number[] = [];
function aggiungi(valore: number): void {
ultimiValori.push(valore);
if (ultimiValori.length > 5) ultimiValori.shift();
}
Funziona… per una coordinata. Ma le coordinate sono 21 punti × 3 assi = 63, e ognuna ha bisogno della propria memoria. Con le variabili sciolte servirebbero 63 array e 63 funzioni. Con una classe, ne scrivi una e ne crei 63.
class MediaMobile {
private valori: number[] = []; // campo: la memoria di QUESTO oggetto
private readonly quanti: number; // quanti valori tenere
constructor(quanti = 5) { // si esegue alla nascita dell'oggetto
this.quanti = quanti;
}
aggiungi(valore: number): void { // metodo: una funzione dell'oggetto
this.valori.push(valore);
if (this.valori.length > this.quanti) this.valori.shift();
}
valore(): number {
let somma = 0;
for (const v of this.valori) somma += v;
return somma / this.valori.length;
}
}
const x = new MediaMobile(5);
const y = new MediaMobile(5); // memoria separata: due oggetti, due storie
x.aggiungi(0.42);
x.aggiungi(0.44);
console.log(x.valore()); // 0.43
Il vocabolario è tutto qui, e sono cinque parole:
- classe — lo stampo: dice com'è fatto e cosa sa fare un oggetto di quel tipo;
new— costruisci un oggetto da quello stampo;- costruttore — quello che va fatto alla nascita;
- campo — un valore che l'oggetto si tiene (
valori,quanti); - metodo — una funzione che l'oggetto sa eseguire (
aggiungi,valore).
this vuol dire «questo oggetto qui, non gli altri 62». E
private vuol dire «da fuori non si tocca»: chi usa un
MediaMobile chiama aggiungi e valore, e non deve
neanche sapere che dentro c'è un array. Se domani cambi il modo di calcolare la media,
nessun altro file se ne accorge.
aPixel, distanza — una funzione è più semplice e va benissimo.
Nel progetto vero ci sono quattro classi in tutto: non di più.
Lo stato dell'interfaccia
Guarda la tua pagina della lezione 8 e conta: il pulsante «Accendi», quello «Spegni», il messaggio, il riquadro d'errore, il contatore di fps. Cinque cose che devono essere coerenti fra loro. Se le accendi e le spegni una per una, sparse nel codice, prima o poi arrivi a una schermata assurda — «Camera spenta» scritto mentre la spia è accesa, il pulsante «Avvio in corso…» che resta lì per sempre dopo un errore.
La soluzione è dire per prima cosa quali stati esistono. È un tipo unione, come quelli della lezione 2:
type Stato = "spenta" | "avvio" | "accesa" | "errore";
Quattro parole, e nessun'altra: se scrivi "acesa", il compilatore ti ferma
subito. Poi una sola funzione decide come si vede ogni stato:
function setState(stato: Stato, messaggio: string): void {
etichettaStato.textContent = messaggio;
bottoneAccendi.disabled = stato === "avvio" || stato === "accesa";
bottoneSpegni.disabled = stato !== "accesa";
riquadroErrore.hidden = stato !== "errore";
document.body.dataset.stato = stato; // utile al CSS e a chi legge la pagina
}
Da quel momento in poi, in tutto il resto del programma, non si tocca più nessun
pulsante: si chiama setState("avvio", "Carico il modello…") e basta. Il
vantaggio non è scrivere meno righe: è che gli stati impossibili diventano
impossibili, invece di restare in agguato.
Perché i punti tremano
Tieni la mano ferma, ferma davvero. I punti sullo schermo continuano a ballare di qualche pixel. Non è colpa tua e non è un bug: il modello analizza ogni fotogramma da solo, e ogni fotogramma è leggermente diverso dal precedente — il rumore del sensore, un filo di luce che cambia. Piccole differenze in ingresso, piccole differenze in uscita. Trenta volte al secondo, e l'occhio le legge come una vibrazione.
L'idea più semplice per toglierla è quella che conosci già dai voti a scuola: invece del valore di adesso, la media degli ultimi cinque. I saltelli in su e in giù si compensano e la mano si calma.
// senza filtro con media su 5
// 0.412 0.418 0.409 0.421 0.414 0.415 0.415 0.416 0.415 0.416
Ma c'è un prezzo, e va capito bene perché è il cuore della lezione. Se muovi la mano di scatto, la media contiene ancora quattro valori vecchi: la mano sullo schermo arriva in ritardo, come se strisciasse dietro alla tua. Più il filtro è forte, più è lento. Con 5 valori il ritardo è appena percettibile; con 20 la mano sembra nella melassa. Provali entrambi: è l'esercizio 9.2.
filter.ts. Oggi scrivi la media mobile perché ti fa capire il problema
con quello che già sai; chi vuole guardare la versione adulta trova il file commentato in
italiano nel codice del sito.
La conferma su più fotogrammi
C'è un secondo difetto, più subdolo. Ogni tanto — un fotogramma su cinquanta, quando la luce cambia o passa qualcosa nell'inquadratura — il modello «vede» una mano che non esiste. Dura un fotogramma solo, ma sullo schermo è un lampo fastidioso.
Il rimedio costa tre righe: non fidarti della prima volta. Si conta da quanti fotogrammi consecutivi la mano è presente, e si disegna solo da tre in poi.
let consecutivi = 0;
const CONFERME = 3;
if (risultato.landmarks.length > 0) consecutivi++;
else consecutivi = 0;
if (consecutivi >= CONFERME) {
// solo adesso la mano si disegna
}
Costo: un decimo di secondo di ritardo alla comparsa, che nessuno nota. Guadagno: le mani fantasma spariscono. È lo stesso ragionamento del filtro — si paga un pochino di ritardo per comprare stabilità — ed è una delle scelte più frequenti in tutto il mestiere.
Demo del docente (15')
- Metti la mano ferma davanti alla webcam e zooma sullo schermo: tutta la classe deve vedere i punti che ballano da soli. Il problema prima della soluzione.
- Scrivi
MediaMobiledal vivo, con calma: è la prima classe che vedono. Fai notare chethis.valoridi due oggetti diversi sono due array diversi. - Applicala e riguarda la mano ferma: i punti si calmano. Poi muovi la mano di scatto e fai vedere il ritardo. Non nasconderlo, mostralo: è la lezione.
- Alza la finestra a 20 valori: la mano diventa gelatina. Riportala a 5. Chiedi alla classe quale preferirebbe, e perché non esiste una risposta giusta per tutti gli usi.
- Mostra il
setStatefinito e prova la sequenza accendi → errore → spegni → riaccendi, per far vedere che i pulsanti non si ingarbugliano mai.
Laboratorio (60')
Esercizio 9.1 — La tua prima classe base
Scrivi in src/filtro.ts la classe MediaMobile, con
aggiungi(valore) e valore(). Provala senza
webcam, con dei numeri scritti a mano, e controlla in console che faccia
quello che pensi.
Soluzione
// src/filtro.ts
export class MediaMobile {
private valori: number[] = [];
private readonly quanti: number;
constructor(quanti = 5) {
this.quanti = quanti;
}
aggiungi(valore: number): void {
this.valori.push(valore);
if (this.valori.length > this.quanti) this.valori.shift();
}
/** La media dei valori in memoria. Se non ce n'è nessuno, restituisce 0. */
valore(): number {
if (this.valori.length === 0) return 0;
let somma = 0;
for (const v of this.valori) somma += v;
return somma / this.valori.length;
}
}
// prova
const m = new MediaMobile(3);
m.aggiungi(10);
console.log(m.valore()); // 10
m.aggiungi(20);
console.log(m.valore()); // 15
m.aggiungi(30);
m.aggiungi(60); // il 10 esce dalla finestra
console.log(m.valore()); // 36.66…
shift() toglie il primo elemento dell'array: è così che la finestra
«scorre». Il controllo su array vuoto evita una divisione per zero, che darebbe
NaN — e un NaN nelle coordinate fa sparire la mano senza
nessun messaggio d'errore.
Esercizio 9.2 — Smorzare la mano vera medio
Scrivi una seconda classe, FiltroMano, che tiene 63 medie mobili (21 punti
× 3 coordinate) e ha un metodo smorza(punti) che restituisce i punti
addolciti. Applicala prima di disegnare. Poi prova con finestre di 2, 5 e 20 valori e
guarda la differenza con i tuoi occhi.
Soluzione
// src/filtro.ts
import type { Punto } from "./disegna";
export class FiltroMano {
private filtri: MediaMobile[] = [];
constructor(quanti = 5) {
// 21 punti × 3 coordinate: un filtro per ciascuna, con la sua memoria
for (let i = 0; i < 21 * 3; i++) this.filtri.push(new MediaMobile(quanti));
}
smorza(punti: Punto[]): Punto[] {
const risultato: Punto[] = [];
for (let i = 0; i < punti.length; i++) {
this.filtri[i * 3].aggiungi(punti[i].x);
this.filtri[i * 3 + 1].aggiungi(punti[i].y);
this.filtri[i * 3 + 2].aggiungi(punti[i].z);
risultato.push({
x: this.filtri[i * 3].valore(),
y: this.filtri[i * 3 + 1].valore(),
z: this.filtri[i * 3 + 2].valore(),
});
}
return risultato;
}
}
// main.ts
import { FiltroMano } from "./filtro";
let filtro: FiltroMano | null = null;
// quando la mano compare
filtro ??= new FiltroMano(5);
const addolciti = filtro.smorza(mano);
// …e da qui in poi si disegna `addolciti` invece di `mano`
// quando la mano sparisce: si butta il filtro
filtro = null;
L'ultima riga è più importante di quanto sembri. Se tieni il filtro quando la mano esce dall'inquadratura, alla ricomparsa la media contiene ancora le posizioni vecchie: la mano arriva strisciando dal punto dove stava prima. Si butta il filtro e se ne crea uno nuovo. Nel progetto vero c'è esattamente questa riga, con lo stesso commento.
Esercizio 9.3 — Un solo posto che comanda medio
Definisci il tipo Stato con le quattro parole e scrivi
setState. Poi togli dal resto del codice ogni riga che tocca direttamente
un pulsante o un messaggio: devono restare solo chiamate a setState.
Soluzione
type Stato = "spenta" | "avvio" | "accesa" | "errore";
function setState(stato: Stato, messaggio: string): void {
etichettaStato.textContent = messaggio;
bottoneAccendi.disabled = stato === "avvio" || stato === "accesa";
bottoneSpegni.disabled = stato !== "accesa";
riquadroErrore.hidden = stato !== "errore";
document.body.dataset.stato = stato;
}
async function avvia(): Promise<void> {
setState("avvio", "Carico il modello…");
try {
riconoscitore ??= await creaRiconoscitore();
await accendiCamera();
setState("accesa", "● Camera accesa");
ciclo();
} catch (e) {
spegni();
riquadroErrore.textContent = messaggioErrore(e);
setState("errore", "Camera spenta");
}
}
function spegni(): void {
cancelAnimationFrame(idCiclo);
spegniCamera(video, flusso);
flusso = null;
filtro = null;
setState("spenta", "Camera spenta");
}
Nota il ??= su riconoscitore: alla seconda accensione il
modello è già in memoria e non si ricarica. Nota anche che spegni()
lascia tutto pulito — ciclo fermo, camera rilasciata, filtro buttato: è la regola
d'oro dello spegnimento.
Esercizio 9.4 — Dillo, che non vedi niente base
Quando nessuna mano è inquadrata, la pagina deve scriverlo: «Nessuna mano inquadrata». Quando la mano torna, il messaggio sparisce. Sembra un dettaglio: è la differenza fra un programma rotto e un programma che sta aspettando.
Soluzione
const mano = risultato.landmarks[0];
if (mano) {
etichettaMani.textContent = "";
// …filtra e disegna…
} else {
etichettaMani.textContent = "Nessuna mano inquadrata";
ctx.clearRect(0, 0, tela.width, tela.height);
filtro = null;
}
Chi finisce presto: mostra anche quante mani vede, e con
numHands: 2 scrivi «Destra» e «Sinistra» usando
risultato.handedness, ricordando che va ribaltato (lezione 8).
Esercizio 9.5 — Niente mani fantasma medio
Aggiungi la conferma su tre fotogrammi: la mano si disegna solo se è stata vista tre volte di fila. Attenzione a un dettaglio: il filtro va alimentato anche durante i tre fotogrammi di attesa, altrimenti quando la mano compare si vede assestarsi sotto gli occhi dell'utente.
Soluzione
const CONFERME = 3;
let consecutivi = 0;
if (mano) {
consecutivi++;
filtro ??= new FiltroMano(5);
const addolciti = filtro.smorza(mano); // si filtra sempre…
if (consecutivi >= CONFERME) {
disegnaOssa(ctx, addolciti.map((p) => aPixel(p, tela.width, tela.height)), "#38bdf8");
disegnaPunti(ctx, addolciti.map((p) => aPixel(p, tela.width, tela.height)));
}
} else {
consecutivi = 0;
filtro = null;
}
Il contatore si azzera al primo fotogramma senza mano: una mano vera resta per decine di fotogrammi, una fantasma no, e la soglia di tre le separa. Per provarlo: passa rapidamente un oggetto davanti alla camera e guarda se compare un lampo.
Esercizio 9.6 — Misura il tremolio sfida
Basta con le impressioni: misura. Scrivi una funzione che calcola quanto si spostano in media i punti da un fotogramma al successivo, e stampala ogni secondo. Poi tieni la mano il più ferma possibile per dieci secondi e annota il numero senza filtro e con il filtro. Porta i due numeri alla chiusura.
Soluzione
let precedenti: Punto[] | null = null;
let sommaMovimento = 0;
let misure = 0;
/** Spostamento medio dei 21 punti rispetto al fotogramma precedente (in unità 0…1). */
function misuraTremolio(punti: Punto[]): void {
if (precedenti && precedenti.length === punti.length) {
let somma = 0;
for (let i = 0; i < punti.length; i++) {
const dx = punti[i].x - precedenti[i].x;
const dy = punti[i].y - precedenti[i].y;
somma += Math.sqrt(dx * dx + dy * dy);
}
sommaMovimento += somma / punti.length;
misure++;
}
// copia dei valori, non dei riferimenti: servono com'erano adesso
precedenti = punti.map((p) => ({ x: p.x, y: p.y, z: p.z }));
}
// una volta al secondo, insieme agli fps:
if (misure > 0) {
etichettaTremolio.textContent = `tremolio: ${(sommaMovimento / misure).toFixed(5)}`;
sommaMovimento = 0;
misure = 0;
}
Con la mano ferma, valori tipici: circa 0,0015 senza filtro e 0,0005 con la media su 5 — tre volte meno. Poi rifai la misura muovendo la mano avanti e indietro: lì il numero deve essere grande in entrambi i casi, perché quello non è tremolio, è movimento. Un filtro che spegnesse anche quello sarebbe un filtro che ti mangia il gesto. Questo esercizio è il modo giusto di giudicare qualunque filtro: due misure, non due opinioni.
Errori tipici in aula
thisdimenticato: dentro un metodo si scrivevalori.push(...)invece dithis.valori.push(...). Il compilatore dice «Cannot find name 'valori'»: il campo esiste solo attraversothis.newdimenticato:const f = FiltroMano();non costruisce niente. Ci vuolenew FiltroMano().- Un filtro solo per tutti i punti: la mano si accartoccia verso un punto unico, ed è uno spettacolo memorabile. Servono 63 filtri indipendenti, uno per coordinata.
- Filtro mai buttato: la mano che rientra nell'inquadratura arriva strisciando da dove era uscita.
- La mano «nella melassa»: finestra troppo lunga. Riportala a 5 e riguarda.
precedenti = puntiinvece della copia: si salva un riferimento all'array, non i valori; al confronto successivo le differenze risultano tutte zero e il tremolio misurato è 0,00000. Sembra un successo clamoroso e non lo è.- Due
setStatein fila con messaggi contraddittori: l'ultimo vince, e spesso è quello sbagliato. Un cammino, uno stato.
Verifica veloce (5 domande)
Domande e risposte
- A cosa serve
private? → A impedire che un campo si usi da fuori della classe: chi la usa vede solo i metodi. - Perché servono 63 medie mobili e non una? → Perché ogni coordinata di ogni punto ha una storia sua: 21 punti × 3 assi.
- Qual è il difetto della media mobile? → Il ritardo: più valori tiene, più la mano arriva in ritardo sui movimenti rapidi.
- Cosa fa il filtro One Euro in più? → Cambia forza da solo secondo la velocità del punto: filtra molto da fermo, poco in movimento.
- Perché aspettare tre fotogrammi prima di disegnare una mano? → Per non mostrare le mani fantasma che il modello vede per un fotogramma solo.
Compito per casa (40 minuti)
Due cose. Una: rifinisci il progetto come vuoi tu — colori, dimensioni dei punti, un titolo, il gesto mostrato in grande, magari il colore della mano che cambia con il gesto riconosciuto. Due: prepara la presentazione della lezione 10. Tre o quattro minuti, in cui devi dire: cosa fa il tuo progetto, la parte di codice di cui sei più soddisfatto, la cosa che ti ha fatto perdere più tempo e come l'hai risolta. Niente diapositive: si mostra il progetto che gira.
La terza dimensione, la pubblicazione, le presentazioni
Ultima lezione, e si respira: la mano esce dal piano e diventa una figura nello spazio, si guarda cosa vuol dire mettere davvero online un sito, e poi si presenta. Venti ore fa nessuno di voi aveva scritto una riga di TypeScript.
Teoria
Three.js: scena, telecamera, luci, mesh
Il browser sa disegnare in tre dimensioni, ma l'interfaccia che offre (WebGL) è di una scomodità feroce: matrici, programmi per la scheda grafica, buffer di numeri. Three.js è la libreria che ci si mette in mezzo e parla come si parla in un teatro. Quattro parole e hai capito tutto quello che serve oggi:
- scena (
Scene) — il palcoscenico: l'elenco delle cose che esistono; - telecamera (
PerspectiveCamera) — da dove si guarda, e con che angolo di campo: le cose lontane rimpiccioliscono, come nella realtà; - luci — senza, è tutto nero. Una luce diffusa che schiarisce dappertutto, una direzionale che dà volume;
- mesh (
Mesh) — un oggetto: una geometria (la forma: una sfera, un cilindro) più un materiale (il colore, quanto è lucido).
const sfera = new THREE.Mesh(
new THREE.SphereGeometry(1, 20, 14), // forma
new THREE.MeshStandardMaterial({ color: 0x38bdf8 }), // aspetto
);
sfera.position.set(0, 0, 0);
scena.add(sfera);
E poi, a ogni fotogramma, una riga sola: renderer.render(scena, camera).
Three.js guarda dove sono gli oggetti in quel momento e disegna il quadro.
Da 21 punti a 21 sfere
Qui c'è tutta la matematica della lezione, e sono tre righe. I punti che arrivano dalla
webcam vivono in un mondo fatto così: x e y fra 0 e 1, origine
nell'angolo in alto a sinistra, y che cresce verso il basso come in
tutte le immagini. Il mondo 3D invece ha l'origine al centro e la y che
cresce verso l'alto, come a scuola.
function nelloSpazio(p: Punto) {
const larghezza = 6; // quanto è largo, nel mondo 3D, il rettangolo del video
const altezza = 4.5;
return {
x: (0.5 - p.x) * larghezza, // centra E specchia
y: (0.5 - p.y) * altezza, // centra E rovescia
z: -p.z * larghezza * 1.6, // profondità, un po' esagerata
};
}
Guarda cosa fa quel 0.5 - x, perché fa due lavori in una sottrazione:
- sposta l'origine dall'angolo al centro: il punto al centro dell'immagine (0,5) diventa 0, cioè il centro della scena;
-
inverte il verso dell'asse. Sulla
yserve a passare dalla «y in giù» delle immagini alla «y in su» della grafica 3D. Sullaxè l'effetto specchio: muovi la mano a destra e la mano sullo schermo va a destra, come davanti a uno specchio. Senza, sembra di guardare qualcun altro, ed è stranamente sgradevole.
La z ha il segno invertito (la profondità di MediaPipe è negativa verso la
camera, in 3D l'osservatore sta dalla parte positiva) e viene moltiplicata per 1,6:
la profondità stimata da una sola telecamera è piccola, e senza un po' di guadagno
avvicinare la mano non si noterebbe quasi. Il valore è scelto a occhio — più in alto, la
mano comincia a sembrare deformata.
Mettere online un sito
Finora hai lavorato su localhost, che è solo il tuo computer. «Mettere
online» vuol dire tre cose, e nessuna è magica:
-
Costruire:
npm run build. Il comando controlla i tipi, traduce il TypeScript in JavaScript, unisce i file, li accorcia e scrive tutto nella cartelladist/. Lì dentro c'è un sito normale — pagine, script, immagini — che non ha più bisogno né di Node né di Vite. - Un computer sempre acceso che serva quei file a chi li chiede, e un nome (un dominio) che punti a quel computer.
-
HTTPS, e qui non c'è scelta: senza certificato, come hai visto alla
lezione 7, la webcam non si accende. Il tuo progetto online in
http://sarebbe una pagina morta.
Il sito che state guardando, per dire, gira su un piccolo computer in affitto: un
programma serve la cartella dist/, un altro (Caddy) si occupa del dominio e
si procura il certificato HTTPS da solo. Prima di pubblicare, uno script controlla che
nei file costruiti non sia comparso niente di strano e che tutte le pagine ci siano; se
un controllo fallisce, la pubblicazione si ferma e online resta la versione di ieri, che
funzionava.
npm run build e a guardare cosa c'è dentro dist/: è la parte
che conta. Chi vuole andare avanti a casa trova nell'elenco finale le strade più
semplici.
Due parole di chiusura: privacy e CSP
Il vostro progetto tratta immagini di persone. È il dato più delicato che esista, e voi l'avete gestito nel modo giusto senza quasi accorgervene: le immagini non escono dal browser, perché non c'è nessun posto dove mandarle; la camera si accende solo al clic; si spegne quando cambiate scheda. Non è un caso, è una scelta di progettazione — e la regola che ci sta sotto vale ben oltre questo corso: il dato che non raccogli è l'unico che non puoi perdere.
C'è anche un modo per non limitarsi a prometterlo. Un sito può dichiarare al browser cosa gli è permesso fare — è la Content Security Policy. Il sito della mano dichiara, fra le altre cose, che la pagina può parlare solo con il proprio dominio. Da quel momento non è più il programmatore a garantire che le immagini non vengano inviate a qualcuno: è il browser che rifiuta di inviarle, anche se il codice ci provasse. La differenza fra una promessa e una garanzia è tutta lì.
Nel progetto vero questa regola ha avuto un effetto collaterale interessante: ha bloccato una chiamata che nessuno aveva scritto. Era la libreria di riconoscimento, che ogni minuto mandava ai suoi server qualche dato d'uso — nessuna immagine, ma comunque una chiamata a un estraneo, e senza alcun modo di spegnerla. È stata disattivata. Senza la CSP, nessuno se ne sarebbe mai accorto.
Demo del docente (10')
- Apri il file di avvio
src/scena3d.ts(lo trovi qui sotto, nel riquadro del docente) e mostra che c'è già tutto: scena, telecamera, luci, 21 sfere allineate in fila. - Collega un solo punto, il polso, e muovilo con la mano davanti alla webcam. Una sfera che segue un polso è già un effetto notevole, e chiarisce il meccanismo meglio di venti.
- Poi collega tutti e 21 i punti: la mano appare. Ruota la testa della classe verso lo schermo e avvicina la mano alla camera per far vedere la profondità.
- Sbaglia apposta: togli il
0.5 -dallay. La mano si capovolge. Rimettilo e spiega la sottrazione mentre tutti guardano l'effetto.
Laboratorio (80')
Esercizio 10.1 — Il file di avvio base
Installa Three.js, copia nel progetto il file di avvio src/scena3d.ts e
aggiungi alla pagina il canvas a tutto schermo che gli serve. Verifica che si veda la
fila di 21 sfere: vuol dire che scena, luci e telecamera funzionano.
npm install three@0.186.0
npm install --save-dev @types/three@0.186.0
Il file di avvio (lo distribuisce il docente)
// src/scena3d.ts — scena 3D già pronta: tu devi solo chiamare mostraMano() e disegna()
import * as THREE from "three";
import type { Punto } from "./disegna";
const tela3d = document.getElementById("scena") as HTMLCanvasElement;
const scena = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
camera.position.set(0, 0, 10);
const renderer = new THREE.WebGLRenderer({ canvas: tela3d, antialias: true, alpha: true });
function adatta(): void {
const w = tela3d.clientWidth || window.innerWidth;
const h = tela3d.clientHeight || window.innerHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix(); // obbligatorio dopo aver cambiato le proporzioni
}
window.addEventListener("resize", adatta);
adatta();
// Luci: una diffusa perché niente resti nero, una direzionale che dà volume.
scena.add(new THREE.HemisphereLight(0xdbeafe, 0x1e1b4b, 1.4));
const principale = new THREE.DirectionalLight(0xffffff, 2.2);
principale.position.set(3, 5, 8);
scena.add(principale);
// 21 sfere, create UNA volta sola: a ogni fotogramma si spostano soltanto.
const geometria = new THREE.SphereGeometry(1, 20, 14);
const materiale = new THREE.MeshStandardMaterial({ color: 0x38bdf8, roughness: 0.35 });
const sfere: THREE.Mesh[] = [];
for (let i = 0; i < 21; i++) {
const s = new THREE.Mesh(geometria, materiale);
s.position.set((i - 10) * 0.35, 0, 0); // in fila, finché non arriva una mano vera
s.scale.setScalar(0.12);
scena.add(s);
sfere.push(s);
}
const LARGHEZZA = 6;
const ALTEZZA = 4.5;
/** Sposta le 21 sfere sui 21 punti della mano (coordinate 0…1 di MediaPipe). */
export function mostraMano(punti: Punto[]): void {
for (let i = 0; i < 21; i++) {
const p = punti[i];
sfere[i].position.set(
(0.5 - p.x) * LARGHEZZA,
(0.5 - p.y) * ALTEZZA,
-p.z * LARGHEZZA * 1.6,
);
}
mostra(true);
}
/** Mostra o nasconde la mano in blocco. */
export function mostra(visibile: boolean): void {
for (const s of sfere) s.visible = visibile;
}
/** Un fotogramma. Va chiamata dentro il ciclo di main.ts. */
export function disegna(): void {
renderer.render(scena, camera);
}
<!-- index.html: il canvas della scena, dietro a tutto il resto -->
<canvas id="scena"></canvas>
Nel foglio di stile dagli larghezza e altezza piene (per esempio
position: fixed; inset: 0; width: 100%; height: 100%;): un canvas senza
dimensioni resta di 300×150 pixel e sembra che non funzioni niente.
Esercizio 10.2 — La tua mano nello spazio base
Collega i punti veri: nel ciclo dei fotogrammi, dopo il filtro della lezione 9, chiama
mostraMano() con i punti normalizzati — non quelli in
pixel — e disegna() a ogni giro. Quando la mano non c'è, nascondila.
Soluzione
// main.ts
import { disegna, mostra, mostraMano } from "./scena3d";
function ciclo(): void {
idCiclo = requestAnimationFrame(ciclo);
if (riconoscitore && video.readyState >= 2 && video.currentTime !== ultimoIstante) {
ultimoIstante = video.currentTime;
const risultato = riconoscitore.recognizeForVideo(video, performance.now());
const mano = risultato.landmarks[0];
if (mano) {
filtro ??= new FiltroMano(5);
mostraMano(filtro.smorza(mano)); // punti 0…1: la conversione la fa la scena
} else {
filtro = null;
mostra(false);
}
}
disegna(); // fuori dall'if: la scena si ridisegna a ogni fotogramma
}
Due errori da evitare: passare i punti già convertiti in pixel (la mano finisce
fuori dallo schermo, a centinaia di unità di distanza) e chiamare
disegna() dentro l'if (la scena si aggiorna solo quando
arriva un fotogramma nuovo, e si vede).
Esercizio 10.3 — Rendila tua medio
Tre ritocchi, scegline almeno due: fai le punte delle dita (4, 8, 12, 16, 20) più grandi del resto; cambia il colore della mano quando il gesto riconosciuto è «pollice su»; fai crescere le sfere quando la mano si avvicina alla camera.
Soluzione
// dentro mostraMano, in scena3d.ts
const PUNTE = [4, 8, 12, 16, 20];
sfere[i].scale.setScalar(PUNTE.includes(i) ? 0.16 : 0.12);
// colore: il materiale è condiviso, quindi una riga cambia tutta la mano
export function coloraMano(colore: number): void {
materiale.color.setHex(colore);
}
// in main.ts
coloraMano(nome === "Thumb_Up" ? 0x22c55e : 0x38bdf8);
// dimensione secondo la distanza: il polso più vicino = z più negativa
const vicinanza = 1 - punti[0].z * 2;
sfere[i].scale.setScalar(0.12 * Math.max(0.5, Math.min(2, vicinanza)));
Il colore è uno solo per tutte le sfere perché il materiale è uno solo,
condiviso: cambiarlo le cambia tutte insieme. È la stessa proprietà che, nel progetto
vero, permette di far svanire la mano intera agendo su un valore solo.
Math.max e Math.min insieme tengono un numero dentro un
intervallo: un trucco che userai spessissimo.
Esercizio 10.4 — Costruisci il sito medio
Lancia npm run build e guarda cosa è comparso in dist/. Poi
npm run preview, che serve quella cartella come farebbe un vero server, e
prova il tuo sito «costruito». Apri uno dei file .js generati: non ci
capirai niente, ed è giusto così.
Soluzione
npm run build
npm run preview # apre http://localhost:4173
In dist/ trovi index.html e una cartella
assets/ con file dai nomi strani tipo index-a1b2c3d4.js.
Quelle lettere sono la firma del contenuto: se cambi una riga di
codice, il nome cambia, e i browser che avevano in memoria la versione vecchia sono
costretti a scaricare quella nuova. Il codice dentro è tutto su poche righe
lunghissime, con i nomi delle variabili ridotti a una lettera: è la
minificazione, che serve a far scaricare meno byte. Ed è anche la prova
finale di quello che si diceva alla lezione 6: dei tuoi tipi, lì dentro, non c'è più
traccia.
Esercizio 10.5 — Conta le dita alzate sfida
L'ultima sfida del corso, e si fa con quello che sai dalla lezione 3. Un dito è alzato
se la sua punta è più in alto della nocca corrispondente — ricorda che
y piccola vuol dire in alto. Mostra quante dita vedi, da 0 a 4 (il pollice
è un caso a parte: si muove di lato, non in su).
Soluzione
/** Indice, medio, anulare, mignolo: punta e nocca di ciascuno. */
const DITA = [
{ punta: 8, nocca: 5 },
{ punta: 12, nocca: 9 },
{ punta: 16, nocca: 13 },
{ punta: 20, nocca: 17 },
];
function contaDita(punti: Punto[]): number {
let alzate = 0;
for (const dito of DITA) {
// y più piccola = più in alto nell'immagine
if (punti[dito.punta].y < punti[dito.nocca].y) alzate++;
}
return alzate;
}
Funziona con la mano dritta e si confonde con la mano inclinata, perché confronta le
altezze sull'immagine e non rispetto al palmo. La versione seria misura le distanze
dal polso (distanza(punta, polso) > distanza(nocca, polso)) ed è
robusta alle rotazioni: è un ottimo primo progetto da fare a casa. Per il pollice,
si guarda la distanza orizzontale dalla nocca dell'indice.
Le presentazioni (40')
Tre o quattro minuti a testa, con il progetto che gira. Quattro domande, sempre le stesse, perché tutti sappiano cosa preparare:
- Cosa fa il tuo progetto? Mostralo, non descriverlo.
- Qual è il pezzo di codice di cui sei più soddisfatto, e perché?
- Cosa ti ha fatto perdere più tempo? Come ne sei uscito?
- Se avessi altre due ore, cosa aggiungeresti?
La terza domanda è la più importante delle quattro. Raccontare un errore e come l'hai risolto è la cosa che, in questo mestiere, distingue chi sa lavorare da chi sa solo far funzionare: succederà in ogni colloquio, in ogni riunione, per il resto della vostra vita professionale. Tanto vale cominciare oggi.
Errori tipici in aula
- Schermo nero: mancano le luci, oppure il canvas non ha dimensioni nel CSS, oppure
disegna()non viene mai chiamata. Si controllano in quest'ordine. - La mano è capovolta: manca il
0.5 -sullay, o è diventatop.y - 0.5. - La mano si muove al contrario: manca lo specchio sulla
x. È lo stesso ragionamento dellay, ma per un motivo diverso — qui è comodità dell'utente, lì è una conversione fra due sistemi di coordinate. - La mano è enorme o invisibile: le sono stati passati punti in pixel invece che fra 0 e 1.
- Tutto a scatti: si creano oggetti nuovi dentro il ciclo. Le sfere si creano una volta, poi si spostano.
npm run buildfallisce mentrenpm run devfunzionava: ètscche controlla i tipi sul serio. Gli errori sono veri e vanno corretti; in sviluppo erano solo sottolineature ignorate.
Verifica veloce (5 domande)
Domande e risposte
- Quali sono i quattro ingredienti di una scena Three.js? → Scena, telecamera, luci e mesh (geometria + materiale).
- Cosa fanno i due lavori di
(0.5 - x)? → Sposta l'origine al centro e inverte il verso dell'asse: sulla x è l'effetto specchio, sulla y la conversione fra «y in giù» e «y in su». - Perché le 21 sfere si creano fuori dal ciclo? → Perché crearle e distruggerle a ogni fotogramma farebbe lavorare il raccoglitore di memoria e produrrebbe scatti.
- Cosa produce
npm run build? → La cartelladist/: un sito statico già tradotto in JavaScript, che non ha più bisogno né di Node né di Vite. - Perché un sito con la webcam deve stare su HTTPS? → Perché fuori da un contesto sicuro il browser non dà accesso alla camera: la pagina non funzionerebbe.
Compito per casa (nessuna scadenza)
Il corso finisce qui, il progetto no. Otto direzioni in cui può crescere, dalla più facile alla più ambiziosa:
- Contare le dita alzate per bene, misurando le distanze dal polso invece delle altezze (esercizio 10.5, versione seria).
- Disegnare con il dito: quando l'indice è alzato e gli altri chiusi, lascia una scia sul canvas. Pugno chiuso = cancella. Sono trenta righe e diverte moltissimo.
- Pilotare un piccolo gioco: una racchetta che segue l'indice, un bersaglio da colpire, un punteggio. Tutto quello che serve lo sai già.
- Due mani insieme, con colori diversi e le etichette destra/sinistra sistemate come si deve.
- Gesti tuoi: misura le distanze fra le punte e inventa i tuoi comandi — pollice e indice che si toccano, mano a L, tre dita. Ricordati la conferma su più fotogrammi della lezione 9.
- Il corpo intero: lo stesso identico giro con 33 punti da testa a piedi. Guarda la pagina Corpo di questo sito: cambia solo il modello.
- Sostituire le sfere con una mano vera: cilindri per le ossa, un palmo semitrasparente. È quello che fa la demo di questo sito, e il codice è commentato in italiano riga per riga nel tutorial.
- Pubblicare il tuo sito e mandare il link a qualcuno. È la cosa più semplice di tutto l'elenco e la più soddisfacente:
npm run build, poi una qualunque piattaforma che ospita siti statici con HTTPS incluso.
Venti ore fa non avevi mai scritto console.log. Adesso hai un'applicazione
che accende una telecamera, chiede a un modello di intelligenza artificiale dove sono le
tue dita, smorza il rumore dei dati, disegna una figura in tre dimensioni sessanta volte
al secondo e non manda i tuoi dati a nessuno. Non è un esercizio: è un programma vero.
Continua.
Come si valuta
Con dei principianti, valutare solo «il programma funziona» premia chi aveva già visto del codice e punisce chi parte da zero e sta imparando davvero. I pesi qui sotto tengono conto del percorso, non solo del risultato dell'ultimo giorno.
| Criterio | Peso | Come si misura |
|---|---|---|
| Compiti consegnati (lezioni 1-5) | 20% | Cinque compiti brevi, consegnati come link del Playground. Conta la consegna e il tentativo, non la perfezione. |
| Il progetto funziona | 30% | La camera si accende, la mano compare, i punti seguono le dita. È il minimo sindacale, e ci arrivano tutti se le lezioni 6-8 sono andate bene. |
| Codice leggibile e tipi usati bene | 25% | Nomi comprensibili, funzioni corte, il lavoro diviso in almeno due file, niente any sparsi per far tacere il compilatore, interfacce al posto di oggetti anonimi. |
| Qualcosa di proprio | 15% | Una modifica personale qualsiasi: colori, un contatore di dita, un messaggio, un suono, la seconda mano. Serve a distinguere chi ha copiato da chi ha capito. |
| Presentazione finale | 10% | 3-4 minuti: cosa fa, come l'hai fatto, cosa non ha funzionato. Sapere raccontare un bug vale quanto averlo risolto. |
await?». Chi ha lavorato risponde anche sbagliando la parola giusta.
Regole d'aula: webcam, privacy, gente stanca
- Si riprende solo la propria mano. Niente riprese dei compagni, niente schermi altrui. È una regola di educazione prima che di legge.
- Chi non vuole comparire non deve. Per l'inquadratura basta una mano, ma se qualcuno è a disagio può lavorare con un video finto (un filmato registrato prima dal docente, che si usa al posto della camera) o in coppia con chi accende la propria.
- Niente esce dal computer. Va detto esplicitamente e mostrato: in tutto il progetto non c'è una riga che invii immagini da qualche parte. È anche una bella occasione per parlare di cosa fanno, invece, le app che chiedono la camera.
- Le spie si spengono. A fine lezione, tutti chiudono la scheda o premono «spegni camera»: è parte dell'esercizio, non un dettaglio.
Se qualcuno resta indietro (succederà)
In una classe di principianti, dopo la lezione 3 la distanza fra il più veloce e il più lento è già grande. Tre accorgimenti che funzionano meglio del «recuperate a casa»:
- Ogni lezione riparte da un punto di ripristino. Il docente tiene pronto il codice completo della lezione precedente: chi si è perso lo copia e riparte da lì, senza trascinarsi il ritardo per venti ore.
- Gli esercizi sono etichettati. base per tutti, medio per la maggioranza, sfida per chi finisce prima — così i veloci hanno da fare e i lenti non si sentono in colpa per non aver finito.
- Coppie che ruotano. Dalla lezione 6 si lavora in due davanti a un computer solo, alternandosi alla tastiera ogni 15 minuti. Chi guarda spiega, chi scrive esegue: si impara di più in entrambi i ruoli.
Cosa serve al docente, lezione per lezione
| Lezione | Da preparare prima |
|---|---|
| 1-4 | Il proiettore, il Playground aperto, i link degli esercizi già pronti da incollare in chat. Nient'altro. |
| 5 | Un progetto vite.new/vanilla-ts già aperto e funzionante, da mostrare; l'array dei 21 punti finti da distribuire. |
| 6 | I file di installazione di Node.js e VS Code su una chiavetta o in rete locale (le connessioni della scuola, il giorno dell'installazione, si intasano sempre); i permessi di amministratore verificati prima. |
| 7 | Un filmato di una mano, registrato in anticipo, per chi non vuole o non può usare la camera. |
| 8 | Il modello di MediaPipe già scaricato e messo in rete locale, se la linea è lenta: sono circa 8 MB per ogni studente. |
| 9 | Il codice completo della lezione 8, come punto di ripristino. |
| 10 | Il file di avvio con Three.js già pronto; l'ordine delle presentazioni; qualcosa per chiudere in bellezza (la demo del corpo intero su /corpo/ funziona benissimo). |
Dopo il corso
Chi ha finito il progetto ha in mano molto più di una mano che si muove: sa dichiarare tipi, dividere un programma in funzioni e file, usare una libreria esterna, leggere un errore del compilatore e far girare un ciclo a 30 fotogrammi al secondo. Da qui, tre direzioni:
- Approfondire il linguaggio con il manuale ufficiale (TypeScript Handbook) e gli esercizi del Playground.
- Approfondire questo progetto: il tutorial di questo sito rifà lo stesso percorso in autonomia e arriva fino al 3D e alla pubblicazione.
- Cambiare dominio: gli stessi strumenti (webcam, canvas, un modello già pronto) si applicano al corpo intero, al viso, al riconoscimento di oggetti. La pagina Corpo di questo sito è esattamente lo stesso codice con un altro modello.