This commit is contained in:
2026-07-02 12:55:16 +02:00
commit c62e79e4c6
5 changed files with 2009 additions and 0 deletions

View File

@@ -0,0 +1,708 @@
# Implementazione in Vanilla JS — Tecniche pratiche per una web app "slot machine / ruota / betting"
Guida operativa per costruire un'app in stile slot-machine / spinning-reel / betting con **HTML + CSS + JavaScript puri**, senza framework e senza build step. Ogni sezione contiene snippet pronti da incollare. L'obiettivo è un progetto che parte semplicemente aprendo `index.html` (o con un piccolo live server).
---
## 1. La reel della slot: strip verticale con `transform: translateY`
L'idea di base di una reel (rullo) è banale e robusta: una **striscia verticale di elementi** (i simboli) dentro un contenitore con `overflow: hidden`. Si sposta la striscia con `transform: translateY(...)` e si fa in modo che si fermi (snapping) su un **indice target predeterminato**, con un easing `cubic-bezier` in uscita (ease-out) così il rullo decelera e "atterra" sul risultato deciso in anticipo.
Concetti chiave:
- Ogni simbolo ha un'altezza fissa (`--item-h`). Fermarsi sull'indice `i` significa `translateY(-i * item-h)`.
- Per dare l'illusione di velocità, si **ripete la lista di simboli N volte**: si anima da un offset alto verso l'indice finale, così la reel scorre parecchio prima di fermarsi.
- L'atterraggio deterministico si ottiene calcolando l'offset target **prima** di lanciare l'animazione: il risultato è già deciso, l'animazione è solo estetica.
- La curva `cubic-bezier(0.23, 1, 0.32, 1)` (nota come *easeOutQuint*) dà quella decelerazione "morbida" tipica delle slot. `ease-out` puro equivale a `cubic-bezier(0, 0, 0.58, 1)`.
### HTML
```html
<div class="reel" id="reel1">
<div class="reel__strip">
<!-- i simboli vengono generati/ripetuti via JS -->
<div class="reel__item">🍒</div>
<div class="reel__item">🍋</div>
<div class="reel__item">🔔</div>
<div class="reel__item"></div>
<div class="reel__item">7</div>
</div>
</div>
```
### CSS
```css
:root {
--item-h: 100px; /* altezza di un simbolo */
--visible: 3; /* simboli visibili nella finestra */
}
.reel {
height: calc(var(--item-h) * var(--visible));
width: 120px;
overflow: hidden; /* finestra: taglia tutto il resto */
border-radius: 12px;
background: #111;
box-shadow: inset 0 0 24px rgba(0,0,0,.8);
}
.reel__strip {
display: flex;
flex-direction: column;
will-change: transform; /* hint per il compositor / GPU */
}
.reel__item {
height: var(--item-h);
display: grid;
place-items: center;
font-size: 56px;
user-select: none;
}
/* classe applicata via JS per la fase di spin */
.reel__strip--spinning {
transition: transform 3.2s cubic-bezier(0.23, 1, 0.32, 1);
}
```
### JavaScript — atterraggio su indice predeterminato
```js
const SYMBOLS = ['🍒', '🍋', '🔔', '⭐', '7⃣'];
const ITEM_H = 100;
const LOOPS = 8; // quante volte ripeto la lista per "allungare" lo scorrimento
function buildStrip(stripEl) {
// Ripeto i simboli LOOPS volte così la reel ha molto materiale da scorrere.
const repeated = Array.from({ length: LOOPS }, () => SYMBOLS).flat();
stripEl.innerHTML = repeated
.map((s) => `<div class="reel__item">${s}</div>`)
.join('');
return repeated.length;
}
/**
* Fa girare la reel e la ferma ESATTAMENTE su `targetIndex` (0..SYMBOLS.length-1).
* Il risultato è deciso a priori: passo l'indice che voglio far uscire.
*/
function spinTo(stripEl, targetIndex) {
const totalItems = buildStrip(stripEl);
// Posizione finale: penultimo "giro" + l'indice target, così scorre a lungo.
const finalItem = (LOOPS - 2) * SYMBOLS.length + targetIndex;
const finalY = -finalItem * ITEM_H;
// Reset istantaneo in cima, senza transizione.
stripEl.classList.remove('reel__strip--spinning');
stripEl.style.transform = 'translateY(0)';
// Forzo un reflow così il browser "vede" lo stato iniziale prima di animare.
void stripEl.offsetHeight;
// Attivo la transizione ease-out e imposto il target: la reel decelera e atterra.
stripEl.classList.add('reel__strip--spinning');
stripEl.style.transform = `translateY(${finalY}px)`;
return new Promise((resolve) => {
stripEl.addEventListener('transitionend', () => resolve(targetIndex), {
once: true,
});
});
}
// Uso:
const strip = document.querySelector('#reel1 .reel__strip');
const wanted = Math.floor(Math.random() * SYMBOLS.length); // o un valore deciso dal server
spinTo(strip, wanted).then((idx) => {
console.log('Atterrato su', SYMBOLS[idx]);
});
```
Per una slot a più rulli, si lancia `spinTo` su ogni reel con un piccolo **stagger** (ritardo crescente) e/o durate leggermente diverse, così i rulli si fermano uno dopo l'altro:
```js
const reels = document.querySelectorAll('.reel__strip');
const result = [2, 4, 2]; // simboli target per rullo (deciso a priori)
Promise.all(
[...reels].map((strip, i) =>
new Promise((res) =>
setTimeout(() => spinTo(strip, result[i]).then(res), i * 250)
)
)
).then(() => console.log('Tutti i rulli fermi:', result));
```
> Nota: `transitionend` può scattare più volte se animi più proprietà. Qui animiamo solo `transform`, e usiamo `{ once: true }` per sicurezza. Se serve robustezza extra, si può fare fallback con un `setTimeout` pari alla durata.
---
## 2. Web Animations API vs CSS transitions vs `requestAnimationFrame`
Tre strumenti, tre casi d'uso. Regola generale (web.dev / MDN): **usa CSS dove possibile**, sali di livello solo quando serve controllo imperativo o logica per-frame.
| Tecnica | Quando usarla | Punti di forza | Limiti |
|---|---|---|---|
| **CSS transitions** | Passaggio tra due stati (hover, `.is-open`, classe applicata via JS). È quello che usiamo nello snippet reel sopra. | Semplice, dichiarativo, girato sul compositor (GPU) per `transform`/`opacity`. | Solo A→B; poco controllo dinamico; sincronizzare più elementi è scomodo. |
| **CSS animations (`@keyframes`)** | Animazioni multi-fase/loop puramente estetiche (pulsare di una luce, glow). | Dichiarativo, riavviabile via classe, performante. | I keyframe sono statici; parametrizzarli a runtime richiede custom properties o `@property`. |
| **Web Animations API (`element.animate()`)** | Quando il target è **calcolato a runtime** (es. angolo finale della ruota) e vuoi Promise, `reverse()`, `pause()`, `playbackRate`. Ideale per ruota e slot orchestrate. | Controllo imperativo + performance CSS-like; `animation.finished` è una Promise; keyframe dinamici. | API un filo più verbosa; niente per canvas/WebGL. |
| **`requestAnimationFrame`** | Serve logica **per ogni frame**: canvas 2D/WebGL, fisica, particelle, valori che dipendono dall'input in tempo reale (es. coriandoli, counter animati). | Massimo controllo; unico modo di disegnare su canvas frame-by-frame; sincronizzato al refresh del monitor. | Tutto a mano (easing, timing, cleanup); gira sul main thread. |
### `element.animate()` (WAAPI) — esempio con Promise
```js
const el = document.querySelector('.animate-me');
const anim = el.animate(
[
{ transform: 'translateY(-300px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 },
],
{ duration: 700, easing: 'ease-out', fill: 'forwards' }
);
anim.finished.then(() => console.log('Fatto!')); // niente listener manuali
// anim.reverse(); anim.pause(); anim.playbackRate = 2;
```
`fill: 'forwards'` mantiene lo stato finale invece di tornare allo stato iniziale.
### `requestAnimationFrame` — scheletro per canvas/particelle
```js
let rafId;
function loop(now) {
// update(now); draw();
rafId = requestAnimationFrame(loop);
}
rafId = requestAnimationFrame(loop);
// stop: cancelAnimationFrame(rafId);
```
Per la nostra app: **transitions** per la reel semplice, **WAAPI** per la ruota (angolo target calcolato), **rAF** solo se disegniamo particelle/coriandoli custom su canvas (o deleghiamo a `canvas-confetti`, che lo fa per noi).
---
## 3. Ruota della fortuna: `conic-gradient` + `rotate` verso un angolo target
Due parti indipendenti:
1. **Disegno degli spicchi** con `conic-gradient` (una sola dichiarazione CSS, zero immagini).
2. **Spin** con `transform: rotate(...)` che atterra su un angolo target calcolato in JS (WAAPI o transition).
### CSS — sfondo a spicchi con `conic-gradient`
Per N spicchi uguali, ogni spicchio occupa `360 / N` gradi. Con `conic-gradient` si alternano i colori con degli hard-stop:
```css
.wheel {
--n: 8; /* numero di spicchi */
width: 320px;
aspect-ratio: 1;
border-radius: 50%;
/* 8 spicchi da 45deg alternando due colori */
background: conic-gradient(
#e63946 0deg 45deg,
#f1a208 45deg 90deg,
#e63946 90deg 135deg,
#f1a208 135deg 180deg,
#e63946 180deg 225deg,
#f1a208 225deg 270deg,
#e63946 270deg 315deg,
#f1a208 315deg 360deg
);
transform: rotate(0deg);
will-change: transform;
}
/* indicatore/freccia fisso in alto (ore 12) */
.wheel-wrap { position: relative; display: inline-block; }
.wheel-wrap::after {
content: '';
position: absolute;
top: -6px; left: 50%;
transform: translateX(-50%);
border-left: 14px solid transparent;
border-right: 14px solid transparent;
border-top: 22px solid #222; /* triangolo che punta verso il centro */
}
```
Si può anche generare il gradient da JS per N arbitrario:
```js
function conicFor(colors) {
const step = 360 / colors.length;
const stops = colors
.map((c, i) => `${c} ${i * step}deg ${(i + 1) * step}deg`)
.join(', ');
return `conic-gradient(${stops})`;
}
wheel.style.background = conicFor(['#e63946','#f1a208','#2a9d8f','#264653',
'#e63946','#f1a208','#2a9d8f','#264653']);
```
### JavaScript — spin che atterra su uno spicchio target
Il trucco per l'atterraggio deterministico: parti dall'angolo corrente, aggiungi **diversi giri interi** (es. `5 * 360`) più l'offset necessario per portare lo spicchio target sotto l'indicatore.
```js
const SEGMENTS = 8;
const SEG = 360 / SEGMENTS; // ampiezza di uno spicchio
let currentRotation = 0; // stato: rotazione accumulata
/**
* Ferma la ruota sullo spicchio `targetIndex` (0 = quello a ore 12 all'inizio).
* L'indicatore è fisso in alto: dobbiamo ruotare la ruota così che il centro
* dello spicchio target finisca a ore 12.
*/
function spinWheel(wheel, targetIndex) {
const fullSpins = 5; // giri "scenografici"
// centro dello spicchio target, misurato in senso orario dal top:
const targetCenter = targetIndex * SEG + SEG / 2;
// per portarlo a ore 12 devo ruotare all'indietro di targetCenter (mod 360)
const landing = (360 - targetCenter) % 360;
// parto dall'attuale angolo normalizzato e aggiungo i giri interi
const base = currentRotation - (currentRotation % 360);
const newRotation = base + fullSpins * 360 + landing;
currentRotation = newRotation;
const anim = wheel.animate(
[{ transform: `rotate(${wheel.dataset.rot || 0}deg)` },
{ transform: `rotate(${newRotation}deg)` }],
{ duration: 4200,
easing: 'cubic-bezier(0.23, 1, 0.32, 1)', // decelerazione morbida
fill: 'forwards' }
);
wheel.dataset.rot = newRotation;
return anim.finished.then(() => targetIndex);
}
// Uso:
const wheel = document.querySelector('.wheel');
spinWheel(wheel, 3).then((i) => console.log('Vinto lo spicchio', i));
```
Versione alternativa con **CSS transition** invece di WAAPI (stesso principio, si legge l'angolo finale con `transitionend`):
```js
wheel.style.transition = 'transform 4.2s cubic-bezier(0.23,1,0.32,1)';
wheel.style.transform = `rotate(${newRotation}deg)`;
wheel.addEventListener('transitionend', () => { /* ... */ }, { once: true });
```
> Attenzione all'accumulo: non resettare mai a `rotate(0)` senza `transition: none`, o la ruota "srotolerà" all'indietro. Meglio tenere la rotazione **sempre crescente** come nello snippet (`currentRotation` accumula).
---
## 4. Coriandoli con `canvas-confetti`
[`canvas-confetti`](https://github.com/catdad/canvas-confetti) è la libreria de-facto per l'effetto coriandoli: performante, zero-config, usa canvas + `requestAnimationFrame` internamente (opzionalmente un Web Worker).
### Inclusione via CDN
```html
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js"></script>
```
Esposta come funzione globale `confetti()` su `window`. Ritorna una Promise che si risolve al termine.
### Uso base e opzioni
```js
// burst semplice al centro
confetti();
// burst personalizzato dal punto del click
button.addEventListener('click', (e) => {
confetti({
particleCount: 120,
spread: 70,
startVelocity: 45,
origin: {
x: e.clientX / window.innerWidth,
y: e.clientY / window.innerHeight,
},
colors: ['#e63946', '#f1a208', '#2a9d8f', '#ffd166'],
});
});
```
Opzioni principali: `particleCount` (default 50), `angle` (90 = verso l'alto), `spread` (apertura in gradi), `startVelocity`, `origin.x/.y` (0..1), `colors` (array HEX).
### Celebrazione "a raffica" (per il momento del risultato)
```js
function celebrate(durationMs = 2500) {
const end = Date.now() + durationMs;
(function frame() {
confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 } });
confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 } });
if (Date.now() < end) requestAnimationFrame(frame);
})();
}
```
### Canvas dedicato (per non toccare il DOM globale)
```js
const myCanvas = document.querySelector('#fx');
const myConfetti = confetti.create(myCanvas, { resize: true, useWorker: true });
myConfetti({ particleCount: 100, spread: 160 });
```
---
## 5. Suoni: pool di `<audio>` preload vs Web Audio API / Howler.js
### Opzione A — pool di `<audio>` preload (zero dipendenze)
Un singolo `<audio>` non si può sovrapporre a sé stesso (il secondo play interrompe il primo). Per effetti rapidi ripetuti (tick della reel, click) conviene un **pool** di elementi clonati.
```html
<audio id="snd-spin" src="sfx/spin.mp3" preload="auto"></audio>
<audio id="snd-win" src="sfx/win.mp3" preload="auto"></audio>
<audio id="snd-tick" src="sfx/tick.mp3" preload="auto"></audio>
```
```js
function makePool(src, size = 6) {
const pool = Array.from({ length: size }, () => {
const a = new Audio(src);
a.preload = 'auto';
return a;
});
let i = 0;
return () => {
const a = pool[i = (i + 1) % pool.length];
a.currentTime = 0;
a.play().catch(() => {}); // ignora blocco autoplay finché non c'è interazione
};
}
const playTick = makePool('sfx/tick.mp3', 8);
// durante lo spin: playTick() a ogni simbolo che passa
```
> **Autoplay policy**: i browser bloccano l'audio finché non c'è un gesto utente (click/tap). "Sblocca" l'audio al primo click (es. sul pulsante Spin) facendo partire e mettendo subito in pausa un suono muto.
### Opzione B — Web Audio API (latenza minima, controllo fine)
Per timing preciso e nessuna latenza (importante per i tick sincronizzati con l'animazione) conviene decodificare una volta il buffer e riprodurlo con `AudioBufferSourceNode` (usa-e-getta).
```js
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const buffers = {};
async function load(name, url) {
const res = await fetch(url);
const arr = await res.arrayBuffer();
buffers[name] = await ctx.decodeAudioData(arr);
}
function play(name, { volume = 1 } = {}) {
const src = ctx.createBufferSource();
src.buffer = buffers[name];
const gain = ctx.createGain();
gain.gain.value = volume;
src.connect(gain).connect(ctx.destination);
src.start();
}
// sblocco su primo gesto utente:
document.addEventListener('pointerdown', () => ctx.resume(), { once: true });
await Promise.all([load('tick', 'sfx/tick.mp3'), load('win', 'sfx/win.mp3')]);
```
### Opzione C — Howler.js (comodo, audio sprite, cross-browser)
[Howler.js](https://howlerjs.com/) astrae Web Audio con fallback HTML5, gestisce autoplay, volume, loop e **audio sprite** (un solo file con più suoni). Ottimo se vuoi meno codice a mano.
```html
<script src="https://cdn.jsdelivr.net/npm/howler@2.2.4/dist/howler.min.js"></script>
```
```js
// suono singolo
const win = new Howl({ src: ['sfx/win.mp3'], volume: 0.8 });
win.play();
// audio sprite: un file, tanti effetti (offset e durata in ms)
const sfx = new Howl({
src: ['sfx/sprite.webm', 'sfx/sprite.mp3'],
sprite: {
tick: [0, 120],
spin: [200, 1800],
win: [2100, 2500],
},
});
sfx.play('tick');
sfx.play('win');
```
Per la nostra app: **pool `<audio>`** o **Howler sprite** per gli effetti brevi, va benissimo. Web Audio se vuoi sincronizzazione al millisecondo tra tick e animazione.
---
## 6. Struttura di stato: partecipanti, stanze, assegnazioni + reveal sequenziale
Per un'app "assegna partecipanti a stanze" (o premi), tieni uno stato semplice, serializzabile e centralizzato. Nessuna libreria: un oggetto + funzioni pure.
```js
const state = {
participants: [
{ id: 'p1', name: 'Alice' },
{ id: 'p2', name: 'Bob' },
{ id: 'p3', name: 'Carla' },
{ id: 'p4', name: 'Dario' },
],
rooms: [
{ id: 'r1', name: 'Stanza Rossa', capacity: 2 },
{ id: 'r2', name: 'Stanza Blu', capacity: 2 },
],
// risultato: mappa participantId -> roomId (riempita dall'estrazione)
assignments: {}, // { p1: 'r1', p2: 'r2', ... }
revealedCount: 0, // quanti risultati sono già stati mostrati
};
```
### Calcolo delle assegnazioni (deciso a priori, poi rivelato)
```js
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function assign(state) {
const pool = shuffle(state.participants);
const slots = state.rooms.flatMap((r) =>
Array.from({ length: r.capacity }, () => r.id)
);
const result = {};
pool.forEach((p, i) => { result[p.id] = slots[i]; });
state.assignments = result;
state.revealedCount = 0;
return result;
}
```
### Reveal sequenziale (un risultato alla volta, con animazione)
L'idea: l'estrazione è già calcolata; il **reveal** la mostra uno per uno, ognuno accompagnato da spin della reel/ruota + suono + eventuale confetti. `async/await` rende la sequenza leggibile.
```js
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function revealAll(state, { onReveal }) {
const entries = Object.entries(state.assignments); // [ [pId, rId], ... ]
for (const [participantId, roomId] of entries) {
const participant = state.participants.find((p) => p.id === participantId);
const room = state.rooms.find((r) => r.id === roomId);
// 1) anima (reel/ruota) fino all'indice della stanza target
const roomIndex = state.rooms.findIndex((r) => r.id === roomId);
await spinWheel(document.querySelector('.wheel'), roomIndex);
// 2) effetti + aggiorna UI
playTick(); // o sfx.play('win')
onReveal(participant, room);
state.revealedCount++;
// 3) pausa prima del prossimo
await wait(700);
}
celebrate(); // confetti finale
}
// Uso:
assign(state);
revealAll(state, {
onReveal: (p, r) => {
const li = document.createElement('li');
li.textContent = `${p.name}${r.name}`;
document.querySelector('#results').appendChild(li);
},
});
```
Vantaggi di questa struttura: lo stato è un **singolo oggetto** facile da loggare/persistere (`localStorage.setItem('state', JSON.stringify(state))`), la logica di estrazione è separata dal reveal (testabile), e il reveal sequenziale è una semplice iterazione `for...of` con `await`.
---
## 7. Font gratuiti consigliati (Google Fonts) + `<link>` reali
Mix consigliato: un **display bold** per titoli/brand, un **mono/LED** per numeri e counter stile slot, e un **pixel/arcade** opzionale per il tocco retro-casino.
```html
<!-- preconnect: velocizza il fetch dei font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Orbitron: display geometrico "sci-fi / HUD", ottimo per titoli e numeri -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap" rel="stylesheet">
<!-- Share Tech Mono: monospace stile terminale/LED, perfetto per i contatori -->
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet">
<!-- Press Start 2P: bitmap arcade anni '80 (usare a 8/16/24px) -->
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Bungee: display bold "insegna/luna park", ottimo per bottoni e brand -->
<link href="https://fonts.googleapis.com/css2?family=Bungee&display=swap" rel="stylesheet">
```
```css
:root {
--font-display: 'Orbitron', system-ui, sans-serif;
--font-led: 'Share Tech Mono', ui-monospace, monospace;
--font-arcade: 'Press Start 2P', monospace;
}
h1, .brand { font-family: var(--font-display); font-weight: 800; }
.counter { font-family: var(--font-led); letter-spacing: 2px; }
.badge { font-family: var(--font-arcade); font-size: 12px; }
```
### Font 7-segment "vero" (LED): DSEG (self-hosted)
Google Fonts non ha un font 7-segmenti. Per un display LED autentico usa **DSEG** (SIL Open Font License), da self-hostare con `@font-face`:
```css
/* scarica i .woff2 da https://www.keshikan.net/fonts-e.html e mettili in /fonts */
@font-face {
font-family: 'DSEG7 Classic';
src: url('fonts/DSEG7Classic-Bold.woff2') format('woff2');
font-weight: 700;
font-display: swap;
}
.led-display { font-family: 'DSEG7 Classic', monospace; }
```
> In alternativa a DSEG, se vuoi restare solo su Google Fonts, **Share Tech Mono** o **DotGothic16** danno un look "digitale" più che sufficiente senza self-hosting.
---
## 8. Setup vanilla "apri e vai" (uno o pochi file)
Obiettivo: doppio click su `index.html` e funziona; al massimo un mini live server.
Struttura minima consigliata:
```
elsa-gambling/
├─ index.html # markup + <link> font + <script> CDN + <script src="app.js">
├─ style.css # tutti gli stili
├─ app.js # stato + logica (usa import ES module se ti serve)
├─ sfx/ # suoni (mp3/webm)
└─ fonts/ # eventuale DSEG self-hosted
```
Scheletro `index.html`:
```html
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Elsa Gambling</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main id="app"><!-- UI --></main>
<!-- helper via CDN -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/howler@2.2.4/dist/howler.min.js"></script>
<!-- il tuo codice -->
<script src="app.js"></script>
</body>
</html>
```
Consigli pratici:
- **Un solo file va benissimo per prototipare** (CSS in `<style>`, JS in `<script>`): parte davvero solo aprendo il file. Separa in più file quando cresce.
- **ES Modules** (`<script type="module" src="app.js">` + `import`) funzionano solo via `http://`, **non** con `file://` (blocco CORS). Se usi `type="module"`, serve un live server; se vuoi il puro doppio-click su `file://`, resta su script classici (niente `import`).
- **Fetch di file locali** (es. `decodeAudioData` via `fetch('sfx/..')`, o caricare un JSON) **non** funziona su `file://`: serve un server. Aggira precaricando i suoni con `new Audio()`/Howler invece di `fetch`, oppure usa un live server.
- **Live server istantaneo**, zero install: `npx serve` oppure `python3 -m http.server 8000` nella cartella del progetto. Anche l'estensione "Live Server" di VS Code.
- **`will-change: transform`** su reel/ruota e usa solo `transform`/`opacity` per le animazioni: restano sul compositor e non causano reflow.
- **Rispetta `prefers-reduced-motion`**: riduci/annulla spin e coriandoli per chi lo richiede.
```css
@media (prefers-reduced-motion: reduce) {
.reel__strip--spinning { transition-duration: 0.2s; }
.wheel { transition: none; }
}
```
---
## 9. Librerie helper via CDN (riepilogo `<script>`)
```html
<!-- Coriandoli: performante, canvas + rAF, opzionale Web Worker -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js"></script>
<!-- Audio: Web Audio con fallback HTML5, audio sprite, volume/loop -->
<script src="https://cdn.jsdelivr.net/npm/howler@2.2.4/dist/howler.min.js"></script>
```
Opzionali, se servono:
```html
<!-- bezier-easing: calcola easing cubic-bezier in JS (per animazioni rAF custom) -->
<script src="https://cdn.jsdelivr.net/npm/bezier-easing@2.1.0/dist/bezier-easing.min.js"></script>
<!-- js-confetti: alternativa a canvas-confetti, supporta emoji come coriandoli -->
<script src="https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js"></script>
```
> Buona pratica: **fissa la versione** (es. `@1.9.4`) invece di `@latest`, così l'app non si rompe quando esce un major.
---
## Fonti
- Slot machine con HTML/CSS/JS (translateY + cubic-bezier): <https://javascript.plainenglish.io/step-by-step-guide-to-building-a-retro-slot-machine-with-html-css-and-javascript-for-beginners-3f866041c7c0>
- Simple CSS Slot Machine Animation (CodePen): <https://codepen.io/cnbkts/pen/QMXPZM>
- `cubic-bezier()` — MDN: <https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/cubic-bezier>
- `cubic-bezier()` — CSS-Tricks: <https://css-tricks.com/almanac/functions/c/cubic-bezier/>
- Interactive Guide to CSS Transitions — Josh W. Comeau: <https://www.joshwcomeau.com/animation/css-transitions/>
- CSS vs JavaScript animations — web.dev: <https://web.dev/articles/css-vs-javascript>
- CSS & JavaScript animation performance — MDN: <https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/CSS_JavaScript_animation_performance>
- Using the Web Animations API — MDN: <https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API>
- CSS Animations vs Web Animations API — CSS-Tricks: <https://css-tricks.com/css-animations-vs-web-animations-api/>
- `requestAnimationFrame()` — MDN: <https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame>
- Wheel of Fortune with CSS — Mads Stoumann (DEV): <https://dev.to/madsstoumann/wheel-of-fortune-with-css-p-pi-1ne9>
- JavaScript + CSS 3D Wheel of Fortune — Emanuele Feronato: <https://emanueleferonato.com/2025/10/31/javascript-css-3d-wheel-of-fortune/>
- A CSS wheel of fortune — James R. Williams: <https://jamesrwilliams.ca/posts/css-wheel-of-fortune>
- `conic-gradient()` — CSS-Tricks: <https://css-tricks.com/almanac/functions/c/conic-gradient/>
- canvas-confetti (GitHub): <https://github.com/catdad/canvas-confetti>
- canvas-confetti su jsDelivr: <https://www.jsdelivr.com/package/npm/canvas-confetti>
- Howler.js (sito): <https://howlerjs.com/>
- Howler.js (GitHub + README): <https://github.com/goldfire/howler.js/>
- Web Audio API — MDN: <https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API>
- DSEG — font 7/14 segmenti: <https://www.keshikan.net/fonts-e.html>
- DSEG (GitHub): <https://github.com/keshikan/DSEG>
- Orbitron — Google Fonts: <https://fonts.google.com/specimen/Orbitron>
- Share Tech Mono — Google Fonts: <https://fonts.google.com/specimen/Share+Tech+Mono>
- Press Start 2P — Google Fonts: <https://fonts.google.com/specimen/Press+Start+2P>
- Bungee — Google Fonts: <https://fonts.google.com/specimen/Bungee>
- DotGothic16 — Google Fonts: <https://fonts.google.com/specimen/DotGothic16>
</content>
</invoke>