# 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>
