# 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
🍒
🍋
🔔
⭐
7️⃣
```
### 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) => `
${s}
`)
.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
```
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 `