docs
This commit is contained in:
393
docs/01-stile-visivo-gambling.md
Normal file
393
docs/01-stile-visivo-gambling.md
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
# Linguaggio visivo del gambling / casino / betting online
|
||||||
|
|
||||||
|
Guida pratica per replicare l'estetica "hyper-gambling" (slot machine, casino online, betting) su web e app. Riferimenti reali e attuali (2023-2026). Ogni sezione include indicazioni concrete direttamente utilizzabili da uno sviluppatore.
|
||||||
|
|
||||||
|
L'obiettivo estetico e' quello di trasmettere **eccitazione, urgenza, lusso e ricompensa immediata**: sfondi scuri, colori neon saturi, oro e metallo, luci lampeggianti, cifre giganti che salgono, effetti di vincita continui.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Palette colori
|
||||||
|
|
||||||
|
Il pattern dominante e' **sfondo scuro + accenti saturi ad alto contrasto**. Il buio fa risaltare i neon, l'oro e i bagliori (il glow neon e' visibile SOLO su fondo scuro).
|
||||||
|
|
||||||
|
### 1.1 Le combinazioni ricorrenti
|
||||||
|
|
||||||
|
| Palette | Colori | Uso / significato |
|
||||||
|
|---|---|---|
|
||||||
|
| **Classic Luxury** | Nero, Oro, Argento | Casino high-end, esclusivita', ricchezza |
|
||||||
|
| **Vibrant / Energetic** | Rosso acceso, Giallo, Verde neon | Aree slot, eccitazione, pubblico giovane |
|
||||||
|
| **Royal Elegance** | Viola profondo, Oro, Bianco | Lusso, regalita', tavoli VIP |
|
||||||
|
| **Neon Nightlife** | Neon blue, Hot pink, Electric yellow | Energia notturna, vibe "arcade" |
|
||||||
|
| **Casino Table** | Verde feltro, Rosso, Oro | Richiamo diretto ai tavoli (roulette, blackjack) |
|
||||||
|
| **Crypto Dark** | Blu notte / Viola, accenti neon green | Stile Stake / Roobet (vedi sez. 6) |
|
||||||
|
|
||||||
|
### 1.2 Palette pronte all'uso (hex)
|
||||||
|
|
||||||
|
Gli articoli di branding elencano combinazioni ma raramente danno hex. Ecco valori concreti coerenti con lo stile, pronti da incollare come CSS custom properties:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* --- Base scura (sfondi) --- */
|
||||||
|
--bg-900: #0a0a12; /* quasi nero, blu-viola */
|
||||||
|
--bg-800: #12121f;
|
||||||
|
--bg-700: #1b1b2e; /* pannelli / card */
|
||||||
|
--bg-purple: #1a0b2e; /* variante viola profondo (Roobet-like) */
|
||||||
|
|
||||||
|
/* --- Oro / metallo --- */
|
||||||
|
--gold-500: #ffd700; /* oro puro */
|
||||||
|
--gold-600: #f5c518;
|
||||||
|
--gold-700: #d4af37; /* oro antico */
|
||||||
|
--gold-glow: #ffdf6b;
|
||||||
|
|
||||||
|
/* --- Neon --- */
|
||||||
|
--neon-green: #39ff14; /* verde neon "vincita" */
|
||||||
|
--neon-cyan: #00fff0;
|
||||||
|
--neon-pink: #ff2e97; /* hot pink */
|
||||||
|
--neon-magenta: #ff00e5;
|
||||||
|
--neon-blue: #2d6bff;
|
||||||
|
--neon-purple: #b026ff;
|
||||||
|
|
||||||
|
/* --- Rosso / verde casino --- */
|
||||||
|
--casino-red: #e21b3c; /* roulette red, urgenza */
|
||||||
|
--casino-green: #0f7b3f; /* feltro tavolo */
|
||||||
|
--casino-green-felt: #1c7a4d;
|
||||||
|
|
||||||
|
/* --- Testo --- */
|
||||||
|
--text: #f5f5fa;
|
||||||
|
--text-dim: #9aa0b5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Gradienti caratteristici
|
||||||
|
|
||||||
|
I gradienti sono ovunque: bottoni, jackpot, badge, sfondi. Tre famiglie:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Oro metallico (bottoni "PLAY", jackpot) */
|
||||||
|
--grad-gold: linear-gradient(180deg, #fff3b0 0%, #ffd700 40%, #d4af37 60%, #a67c00 100%);
|
||||||
|
|
||||||
|
/* Neon bicolore (CTA arcade, banner) */
|
||||||
|
--grad-neon: linear-gradient(135deg, #ff2e97 0%, #b026ff 50%, #2d6bff 100%);
|
||||||
|
|
||||||
|
/* Viola profondo (sfondo hero) */
|
||||||
|
--grad-purple-bg: radial-gradient(circle at 50% 0%, #3a1a5e 0%, #1a0b2e 60%, #0a0a12 100%);
|
||||||
|
|
||||||
|
/* Verde vincita */
|
||||||
|
--grad-win: linear-gradient(180deg, #7dff5a, #39ff14, #0f9b2e);
|
||||||
|
```
|
||||||
|
|
||||||
|
Il gradiente radiale posizionato in alto (`radial-gradient(circle at 50% 0%, ...)`) crea il tipico effetto "riflettore/palcoscenico" dietro l'hero o le slot.
|
||||||
|
|
||||||
|
### 1.4 Psicologia del colore (perche' funziona)
|
||||||
|
|
||||||
|
- **Rosso** = urgenza, eccitazione, fortuna. Usato per CTA, timer, "HOT".
|
||||||
|
- **Verde** = fortuna, denaro, vincita, feltro del tavolo. Usato per importi vinti, "WIN", saldo positivo.
|
||||||
|
- **Oro** = ricchezza, premio, esclusivita'. Jackpot, VIP, monete.
|
||||||
|
- **Viola/Blu profondo** = lusso, mistero, fiducia. Sfondi premium (Roobet, Stake).
|
||||||
|
- **Neon** = energia, notte, dopamina. Bordi, glow, accenti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tipografia
|
||||||
|
|
||||||
|
Lo stile richiede caratteri **grassi, condensati, ad alto impatto** per i titoli, e font **display / LED** per i numeri (jackpot, saldo, timer). Tutti i font sotto sono gratuiti.
|
||||||
|
|
||||||
|
### 2.1 Display / Headline (grassi e condensati) — Google Fonts
|
||||||
|
|
||||||
|
| Font | Caratteristiche | Uso |
|
||||||
|
|---|---|---|
|
||||||
|
| **Anton** | Sans grottesco, condensato, ultra-bold, peso unico | Titoli dominanti, "JACKPOT", banner |
|
||||||
|
| **Bebas Neue** | Condensato alto, elegante, molto diffuso | Headline, etichette, tab |
|
||||||
|
| **Oswald** | Stretto, 6 pesi (ExtraLight→Bold) | Testi UI densi, header editoriali |
|
||||||
|
| **Archivo Black** | Sans nero, largo e pesante | CTA, badge |
|
||||||
|
| **Teko** | Condensato tecnico, moderno | Numeri, contatori, punteggi |
|
||||||
|
| **Fjalla One** | Display medio-bold | Titoli secondari |
|
||||||
|
| **Titan One / Luckiest Guy / Bungee** | Chunky, arcade, cartoon | Vibe "juicy"/arcade, premi |
|
||||||
|
|
||||||
|
`Bungee` (di David Jonathan Ross, Google Fonts) e' particolarmente adatta all'estetica insegna/marquee urbana.
|
||||||
|
|
||||||
|
### 2.2 Numeri LED / 7-segment (jackpot, saldo, timer)
|
||||||
|
|
||||||
|
Per i contatori digitali serve un font 7/14-segmenti che imiti i display LED reali:
|
||||||
|
|
||||||
|
- **DSEG** (keshikan.net) — famiglia free open source, imita display a 7 e 14 segmenti, con varianti Classic/Modern, weight e italic. E' lo standard de facto. Include `DSEG7` (7 segmenti) e `DSEG14`.
|
||||||
|
- **Segment7** (Font Library, open source) — monospace, solo 7 segmenti, look calcolatrice/LED autentico.
|
||||||
|
- **Seven Segment** (FontGet / CDNFonts) — alternativa, free per uso personale.
|
||||||
|
|
||||||
|
In alternativa, per un look "digitale" ma leggibile, va bene una monospace tech: **Share Tech Mono**, **Orbitron** (futuristica, ottima per sci-fi/arcade) o **DotGothic16** (dot-matrix), tutte su Google Fonts.
|
||||||
|
|
||||||
|
### 2.3 Abbinamenti consigliati
|
||||||
|
|
||||||
|
- **Anton** (titoli) + **Inter/Roboto** (corpo) + **DSEG7** (numeri jackpot).
|
||||||
|
- **Bebas Neue** (headline) + **Oswald** (UI) + **Orbitron** (contatori).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Motivi UI ricorrenti
|
||||||
|
|
||||||
|
Elementi grafici che definiscono immediatamente il genere:
|
||||||
|
|
||||||
|
- **Reel della slot** — 3-5 colonne verticali che scorrono con simboli (7, campanelle, frutti, diamanti, monete). Effetto blur in movimento + decelerazione elastica allo stop.
|
||||||
|
- **Ruota della fortuna (wheel of fortune)** — cerchio a spicchi colorati alternati, indicatore/lancetta in alto, decelerazione con `cubic-bezier` per il "quasi-vinci".
|
||||||
|
- **Monete e gettoni (chips)** — monete dorate 3D (spesso animate/rotanti), fiches da poker colorate.
|
||||||
|
- **Jackpot counter / ticker** — cifre grandi LED che salgono di continuo (count-up animato), spesso con ticker scorrevole ("$1,204,556" incrementale).
|
||||||
|
- **Luci da marquee (chase lights)** — cornici di lampadine che si accendono in sequenza attorno a banner/insegne.
|
||||||
|
- **Confetti, scintille, particelle** — esplosione di coriandoli e monete al momento della vincita.
|
||||||
|
- **Badge / medaglie / corone** — livelli VIP, loyalty, "BONUS", "FREE SPINS".
|
||||||
|
- **Banner promozionali + jackpot ticker scorrevole** — tile dei giochi live con jackpot in tempo reale, barra loyalty visibile.
|
||||||
|
- **Simboli portafortuna** — 777, ferri di cavallo, quadrifogli, diamanti, stelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Illuminazione ed effetti
|
||||||
|
|
||||||
|
Il "peso visivo" arriva dagli effetti luce/materiale:
|
||||||
|
|
||||||
|
- **Neon glow** — bagliore colorato attorno a testo e bordi (layer multipli di shadow). Vedi sez. 5.1.
|
||||||
|
- **Drop shadow profonde** — le card "galleggiano" su fondo scuro.
|
||||||
|
- **Chrome / metallico** — gradienti oro/argento con highlight speculari e bordo scuro per dare volume.
|
||||||
|
- **Shine sweep (riflesso che passa)** — striscia di luce diagonale che attraversa bottoni/carte periodicamente.
|
||||||
|
- **Inner glow / tubo neon** — inset shadow chiaro per l'effetto "tubo al neon acceso".
|
||||||
|
- **Bevel / emboss** — bordi smussati su monete e bottoni per il 3D.
|
||||||
|
- **Pulsazione / flicker** — glow che pulsa o lampeggia come insegna al neon.
|
||||||
|
|
||||||
|
> Nota performance/accessibilita': blur grandi e animazioni infinite di `box-shadow` sono costosi da ridipingere. Animare con parsimonia e avvolgere gli effetti pulsanti in `@media (prefers-reduced-motion: reduce)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Tecniche CSS concrete
|
||||||
|
|
||||||
|
### 5.1 Testo neon (text-shadow a layer)
|
||||||
|
|
||||||
|
Layer multipli dello stesso colore con blur crescente = alone luminoso (tecnica CSS-Tricks):
|
||||||
|
|
||||||
|
```css
|
||||||
|
.neon-text {
|
||||||
|
color: #fff;
|
||||||
|
text-shadow:
|
||||||
|
0 0 7px #fff,
|
||||||
|
0 0 10px #fff,
|
||||||
|
0 0 21px #fff,
|
||||||
|
0 0 42px #39ff14,
|
||||||
|
0 0 82px #39ff14,
|
||||||
|
0 0 92px #39ff14,
|
||||||
|
0 0 102px #39ff14,
|
||||||
|
0 0 151px #39ff14;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flicker da insegna al neon */
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 18%, 22%, 25%, 53%, 57%, 100% {
|
||||||
|
text-shadow:
|
||||||
|
0 0 4px #fff, 0 0 11px #fff, 0 0 19px #fff,
|
||||||
|
0 0 40px #39ff14, 0 0 80px #39ff14, 0 0 90px #39ff14,
|
||||||
|
0 0 100px #39ff14, 0 0 150px #39ff14;
|
||||||
|
}
|
||||||
|
20%, 24%, 55% { text-shadow: none; }
|
||||||
|
}
|
||||||
|
.neon-text--flicker { animation: flicker 1.5s infinite alternate; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.neon-text--flicker { animation: none; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Bordo/box neon glow (bottone, card)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.neon-box {
|
||||||
|
border: 2px solid var(--neon-pink);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 5px var(--neon-pink),
|
||||||
|
0 0 15px var(--neon-pink),
|
||||||
|
0 0 30px var(--neon-pink),
|
||||||
|
inset 0 0 8px rgba(255, 46, 151, .6); /* inner glow "tubo" */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Bottone oro metallico (CTA "PLAY / SPIN")
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn-gold {
|
||||||
|
background: linear-gradient(180deg, #fff3b0 0%, #ffd700 40%, #d4af37 60%, #a67c00 100%);
|
||||||
|
color: #3a2a00;
|
||||||
|
font-family: "Anton", sans-serif;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
border: 2px solid #a67c00;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 2px 2px rgba(255,255,255,.7), /* highlight superiore */
|
||||||
|
inset 0 -3px 4px rgba(0,0,0,.35), /* ombra inferiore = volume */
|
||||||
|
0 6px 0 #7a5c00, /* "spessore" 3D */
|
||||||
|
0 10px 20px rgba(0,0,0,.5), /* ombra a terra */
|
||||||
|
0 0 24px rgba(255,215,0,.6); /* glow oro */
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,.5);
|
||||||
|
}
|
||||||
|
.btn-gold:active { transform: translateY(4px); box-shadow: /* riduci il 0 6px 0 ... */ ; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 Bordo a gradiente (gradient border)
|
||||||
|
|
||||||
|
Due tecniche affidabili:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* A) border-image */
|
||||||
|
.grad-border {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-image: linear-gradient(135deg, #ff2e97, #b026ff, #2d6bff) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* B) doppio background (permette border-radius, la A no) */
|
||||||
|
.grad-border-radius {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
linear-gradient(var(--bg-800), var(--bg-800)) padding-box,
|
||||||
|
linear-gradient(135deg, #ff2e97, #b026ff, #2d6bff) border-box;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 Shine sweep (riflesso animato su bottone/card)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.shine { position: relative; overflow: hidden; }
|
||||||
|
.shine::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute; top: 0; left: -150%;
|
||||||
|
width: 60%; height: 100%;
|
||||||
|
background: linear-gradient(120deg,
|
||||||
|
transparent 0%, rgba(255,255,255,.5) 50%, transparent 100%);
|
||||||
|
transform: skewX(-20deg);
|
||||||
|
animation: sweep 3s infinite;
|
||||||
|
}
|
||||||
|
@keyframes sweep { 0% { left: -150%; } 60%,100% { left: 150%; } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 Testo con riempimento a gradiente (oro/neon)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.grad-text {
|
||||||
|
background: linear-gradient(180deg, #fff3b0, #ffd700, #d4af37);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.7 Luci da marquee (cornice di lampadine)
|
||||||
|
|
||||||
|
Con `box-shadow` multipli su uno pseudo-elemento, o piu' semplicemente con un `repeating-radial-gradient` + animazione di opacita' per l'effetto "chase". Per il chase preciso conviene JS o `background-position` animato di un pattern di puntini.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.marquee-lights {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle, #ffdf6b 2px, transparent 3px) 0 0 / 20px 20px;
|
||||||
|
animation: chase .6s steps(2) infinite;
|
||||||
|
}
|
||||||
|
@keyframes chase { to { background-position: 20px 0; } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.8 Slot reel (scorrimento verticale)
|
||||||
|
|
||||||
|
Il pattern base: una colonna piu' alta del viewport, si trasla verso l'alto con `transform: translateY()` e si ferma con easing elastico. Approcci reali su CodePen (sez. 6): pure CSS con `@keyframes`, JS con `requestAnimationFrame` + decelerazione, o 3D con `rotateX`/`translateZ` per il reel cilindrico.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.reel { height: 120px; overflow: hidden; }
|
||||||
|
.reel__strip { display: flex; flex-direction: column;
|
||||||
|
transition: transform 3s cubic-bezier(.15,.9,.25,1); } /* decelerazione */
|
||||||
|
.reel--spinning .reel__strip { filter: blur(2px); } /* motion blur */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.9 Count-up del jackpot
|
||||||
|
|
||||||
|
Il conteggio incrementale si fa in JS (aggiornando `textContent`), abbinato al font DSEG7 e a un container con glow verde/oro. Per performance usare `requestAnimationFrame` e `Intl.NumberFormat` per i separatori delle migliaia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Riferimenti da studiare (URL reali)
|
||||||
|
|
||||||
|
### Prodotti / casino live
|
||||||
|
- **Stake** — https://stake.com/ e https://stake.com/casino/home — tema scuro blu notte, "Stake Originals" con dark theme + accenti colorati, equilibrio professionale/immersivo.
|
||||||
|
- **Roobet** — https://roobet.com/ — estetica viola + verde neon, "neon-lit realm", playground digitale pulsante.
|
||||||
|
|
||||||
|
### Collezioni ispirazione (UI/branding)
|
||||||
|
- Dribbble — Casino UI: https://dribbble.com/tags/casino-ui
|
||||||
|
- Dribbble — Gambling UI: https://dribbble.com/tags/gambling-ui
|
||||||
|
- Dribbble — Casino App: https://dribbble.com/tags/casino_app
|
||||||
|
- Dribbble — esempio piattaforma: https://dribbble.com/shots/23736814-UX-UI-Design-of-Casino-Gambling-Platform
|
||||||
|
- Behance — Casino Game UI: https://www.behance.net/search/projects/casino%20game%20ui
|
||||||
|
- Behance — Casino Web Design: https://www.behance.net/search/projects/casino%20web%20design
|
||||||
|
- Colorlib — 17 Casino Website Design 2026: https://colorlib.com/wp/casino-website-design/
|
||||||
|
- Subframe — 25 Casino Website Design Examples: https://www.subframe.com/tips/casino-website-design-examples
|
||||||
|
|
||||||
|
### CodePen (slot / reel / animazioni)
|
||||||
|
- CSS-only Slot Machine: https://codepen.io/tangxuguo/pen/xqrNmx
|
||||||
|
- Simple CSS Slot Machine Animation: https://codepen.io/cnbkts/pen/QMXPZM
|
||||||
|
- CSS/JS Slot machine: https://codepen.io/MrFirthy/pen/oGVWqK
|
||||||
|
- Slot Machine Spinner: https://codepen.io/Thomasbetten/pen/nPeMPo
|
||||||
|
- Real Slots Reels (3D): https://codepen.io/josfabre/pen/abReBvP
|
||||||
|
- Reel Slot Machine (no JS): https://codepen.io/stevencamilleri-mrg/pen/abzGOGd
|
||||||
|
|
||||||
|
### Font (download)
|
||||||
|
- Bebas Neue — https://fonts.google.com/specimen/Bebas+Neue
|
||||||
|
- Anton — https://fonts.google.com/specimen/Anton
|
||||||
|
- Oswald — https://fonts.google.com/specimen/Oswald
|
||||||
|
- DSEG (7/14-segment) — https://www.keshikan.net/fonts-e.html
|
||||||
|
- Segment7 — https://fontlibrary.org/en/font/segment7
|
||||||
|
|
||||||
|
### CSS glow / neon (tecniche)
|
||||||
|
- CSS-Tricks — Neon Text with CSS: https://css-tricks.com/how-to-create-neon-text-with-css/
|
||||||
|
- Coder's Block — Creating Glow Effects with CSS: https://codersblock.com/blog/creating-glow-effects-with-css/
|
||||||
|
- FreeFrontend — 40+ CSS Glow Effects: https://freefrontend.com/css-glow-effects/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Checklist rapida per replicare l'estetica
|
||||||
|
|
||||||
|
- [ ] Sfondo scuro (near-black blu/viola), MAI bianco puro.
|
||||||
|
- [ ] Palette: base scura + 1-2 neon + oro. Alto contrasto.
|
||||||
|
- [ ] Font titoli condensato ultra-bold (Anton/Bebas Neue).
|
||||||
|
- [ ] Numeri jackpot/saldo con font LED (DSEG7) + glow verde/oro.
|
||||||
|
- [ ] CTA in oro metallico con bevel 3D + glow.
|
||||||
|
- [ ] Bordi neon (box-shadow multipli + inset glow) su card/CTA.
|
||||||
|
- [ ] Gradienti ovunque (oro, neon bicolore, radiale hero).
|
||||||
|
- [ ] Micro-animazioni: shine sweep, pulse, flicker, count-up.
|
||||||
|
- [ ] Feedback vincita: confetti + monete + verde neon + glow.
|
||||||
|
- [ ] Motivi: reel, ruota, monete/chips, marquee lights, ticker jackpot.
|
||||||
|
- [ ] Rispettare `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonti
|
||||||
|
|
||||||
|
- The Best Color Schemes for Gambling Websites — https://mightymegaphone.com/2025/02/12/the-best-color-schemes-for-gambling-websites-designers-guide/
|
||||||
|
- Top 10 Color Palettes for Casino Branding (Lounge Lizard, 2025) — https://www.loungelizard.com/blog/top-10-color-palettes-for-casino-branding/
|
||||||
|
- The Psychology of Color in Casino Design (BetBoyz) — https://betboyz.com/igaming-branding-and-design/igaming-branding-the-psychology-of-color-in-casino-design/
|
||||||
|
- The Color of Luck: Palette Choices in Casino Game Design — https://www.seegreatart.art/the-color-of-luck-a-visual-study-of-palette-choices-in-casino-game-design/
|
||||||
|
- 17 Best Casino Website Design Inspiration 2026 (Colorlib) — https://colorlib.com/wp/casino-website-design/
|
||||||
|
- 25 Casino Website Design Examples (Subframe) — https://www.subframe.com/tips/casino-website-design-examples
|
||||||
|
- Inside the Digital Lobby: Casino Interface Design — https://www.muggsybogues.com/inside-the-digital-lobby-a-feature-spotlight-on-casino-interface-design/
|
||||||
|
- Stake — https://stake.com/ | https://stake.com/casino/home
|
||||||
|
- Stake Review (aesthetic details) — https://www.sportsgambler.com/review/stake/
|
||||||
|
- Roobet — https://roobet.com/
|
||||||
|
- Roobet Review (aesthetic details) — https://www.thespike.gg/reviews/roobet
|
||||||
|
- Dribbble Casino UI — https://dribbble.com/tags/casino-ui
|
||||||
|
- Dribbble Gambling UI — https://dribbble.com/tags/gambling-ui
|
||||||
|
- Behance Casino Game UI — https://www.behance.net/search/projects/casino%20game%20ui
|
||||||
|
- Bebas Neue (Google Fonts) — https://fonts.google.com/specimen/Bebas+Neue
|
||||||
|
- Anton (Google Fonts) — https://fonts.google.com/specimen/Anton
|
||||||
|
- Oswald (Google Fonts) — https://fonts.google.com/specimen/Oswald
|
||||||
|
- DSEG 7/14-segment fonts — https://www.keshikan.net/fonts-e.html
|
||||||
|
- Segment7 (Font Library) — https://fontlibrary.org/en/font/segment7
|
||||||
|
- 17 Royal Casino Fonts (HipFonts) — https://hipfonts.com/casino-fonts/
|
||||||
|
- Best Casino Fonts Free/Premium (Hyperpix) — https://hyperpix.net/fonts/casino-fonts/
|
||||||
|
- How to Create Neon Text With CSS (CSS-Tricks) — https://css-tricks.com/how-to-create-neon-text-with-css/
|
||||||
|
- Creating Glow Effects with CSS (Coder's Block) — https://codersblock.com/blog/creating-glow-effects-with-css/
|
||||||
|
- 47 Best CSS Glow Effects (TestMu AI) — https://www.testmuai.com/blog/glowing-effects-in-css/
|
||||||
|
- 40+ CSS Glow Effects (FreeFrontend) — https://freefrontend.com/css-glow-effects/
|
||||||
|
- CodePen — CSS-only Slot Machine — https://codepen.io/tangxuguo/pen/xqrNmx
|
||||||
|
- CodePen — Real Slots Reels — https://codepen.io/josfabre/pen/abReBvP
|
||||||
|
- CodePen — Reel Slot Machine — https://codepen.io/stevencamilleri-mrg/pen/abzGOGd
|
||||||
334
docs/02-meccaniche-suspense-reveal.md
Normal file
334
docs/02-meccaniche-suspense-reveal.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Meccaniche di Suspense e Reveal: slot machine, ruote della fortuna e loot box
|
||||||
|
|
||||||
|
> Ricerca applicata alla nostra app: assegniamo le persone alle stanze tramite un algoritmo (risultato **predeterminato**), ma lo riveliamo con un'animazione che costruisce **attesa ed eccitazione**, esattamente come lo "spin" di una slot machine. Questo documento raccoglie la psicologia, le meccaniche di animazione e le tecniche JS/CSS concrete per farlo bene.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. La psicologia dello "spin & reveal"
|
||||||
|
|
||||||
|
Il punto centrale, ripetuto in quasi tutta la letteratura di game design, è controintuitivo ma fondamentale per noi:
|
||||||
|
|
||||||
|
> **Il momento della rivelazione (lo spin, il rallentamento, il drumroll) genera più coinvolgimento del premio stesso.** Un'animazione di 3-5 secondi fa "più lavoro motivazionale" del contenuto effettivo che viene rivelato.
|
||||||
|
|
||||||
|
Questo significa che, anche se la stanza è già decisa dall'algoritmo, è la **coreografia della rivelazione** a creare l'emozione. Ecco i meccanismi psicologici da sfruttare.
|
||||||
|
|
||||||
|
### 1.1 Anticipazione e dopamina
|
||||||
|
Il cervello rilascia **più dopamina nell'anticipazione di una ricompensa incerta** che di fronte a una garantita ("dopamine release is significantly higher when a reward is uncertain than when it's guaranteed"). L'incertezza percepita — non il risultato — è il motore emotivo. Da qui la regola: **allungare e drammatizzare la fase di attesa**, non accorciarla.
|
||||||
|
|
||||||
|
### 1.2 Variable reward (rinforzo a rapporto variabile)
|
||||||
|
Le slot machine funzionano su uno **schedule di rinforzo a rapporto variabile** (variable ratio reinforcement), lo stesso principio comportamentale (Skinner) che rende difficile smettere: la ricompensa arriva in modo imprevedibile, e il cervello resta in stato di anticipazione costante. Nel nostro caso l'assegnazione avviene una volta sola, ma possiamo comunque sfruttare l'**imprevedibilità percepita** dell'esito (quale stanza?) durante lo spin.
|
||||||
|
|
||||||
|
### 1.3 Effetto near-miss (quasi-vincita)
|
||||||
|
Il **near-miss** è la situazione in cui i simboli si fermano *quasi* allineati (due jackpot allineati, il terzo a una posizione di distanza). Pur essendo tecnicamente una perdita, stimola il cervello **quasi come una vittoria reale**, rilasciando dopamina e alimentando la sensazione che il colpo grosso sia "a un passo".
|
||||||
|
|
||||||
|
Applicazione al nostro reveal: durante lo scorrimento, far **passare vicino** ad altre stanze/opzioni prima di fermarsi su quella finale amplifica la tensione ("stava per uscire l'altra stanza..."). È la stessa logica della roulette CS:GO che rallenta sfiorando altri item.
|
||||||
|
|
||||||
|
### 1.4 Escalation ed escalation sensoriale
|
||||||
|
I designer aumentano deliberatamente l'**arousal** prima di mostrare il contenuto:
|
||||||
|
- In *Overwatch*, la loot box **trema e vibra** prima di aprirsi.
|
||||||
|
- Fasci di luce dorati e particellari **segnalano un drop raro PRIMA** che l'item appaia.
|
||||||
|
- Suoni, animazioni temporizzate e persino lo **screen shake** enfatizzano l'impatto emotivo.
|
||||||
|
|
||||||
|
L'escalation crea una **curva di tensione crescente**: più segnali "sta per succedere qualcosa di importante" accumuli, più forte è il picco al reveal.
|
||||||
|
|
||||||
|
### 1.5 Il rallentamento finale (slow-down / easing)
|
||||||
|
Elemento chiave e più imitabile: verso la fine, lo spin **decelera progressivamente**. Questo rallentamento è ciò che il cervello legge come "sta per fermarsi, dove cadrà?" — è il momento di massima tensione. In termini tecnici è una curva di **easing-out** (veloce all'inizio, lento alla fine), spesso con un piccolo **overshoot/rimbalzo** finale (`back-out`) per un "settle" soddisfacente (vedi §2.2).
|
||||||
|
|
||||||
|
### 1.6 Principi di design etico da mystery box (da Yu-kai Chou)
|
||||||
|
Riassunto delle leve applicabili in modo sano:
|
||||||
|
1. **Basso costo di partecipazione** — l'utente accetta l'incertezza se lo sforzo è minimo.
|
||||||
|
2. **Investire nel reveal** — il momento teatrale conta più del premio.
|
||||||
|
3. **Pacing dei reveal** — reveal troppo frequenti diluiscono l'impatto e la sensibilità dopaminergica; spaziarli mantiene la novità.
|
||||||
|
4. **Floor garantito / "pity"** — nessuno esce a mani vuote; anche l'esito "comune" deve sembrare significativo (importante per noi: qualunque stanza deve essere presentata come una "bella" assegnazione).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Meccaniche di animazione dei rulli (slot reel)
|
||||||
|
|
||||||
|
### 2.1 Come gira un rullo
|
||||||
|
Il rullo è tipicamente una **striscia verticale di simboli** (un contenitore lungo dentro una finestra con `overflow: hidden`). Lo spin si ottiene traslando la striscia in verticale (`transform: translateY(...)`), non spostando i singoli simboli. Approcci comuni:
|
||||||
|
- **CSS transition/animation** su `translateY` con easing (semplice, performante).
|
||||||
|
- **Web Animations API** (`element.animate`) per controllo dinamico del valore finale (consigliato per risultato predeterminato — vedi §7).
|
||||||
|
- **requestAnimationFrame** per controllo frame-by-frame (fisica custom, blur di movimento).
|
||||||
|
|
||||||
|
Per la **velocità percepita**, durante la fase veloce si può aggiungere un leggero **motion blur** (es. `filter: blur()` verticale) o semplicemente ripetere i simboli in modo che scorrano rapidamente.
|
||||||
|
|
||||||
|
### 2.2 Curve di easing: le più importanti
|
||||||
|
Il carattere dell'animazione dipende quasi tutto dalla **timing function**. Riferimento pratico (`easings.net`):
|
||||||
|
|
||||||
|
| Effetto | Keyword / cubic-bezier | Uso |
|
||||||
|
|---|---|---|
|
||||||
|
| `ease-out` | `cubic-bezier(0, 0, 0.58, 1)` | Decelerazione base a fine spin |
|
||||||
|
| `easeOutBack` (overshoot) | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Il rullo supera il target e torna indietro → "settle" con rimbalzo |
|
||||||
|
| Wheel of fortune (con molla) | `cubic-bezier(0.440, -0.205, 0.000, 1.130)` | Decelerazione + micro-bounce elastico |
|
||||||
|
| `ease-in` | `cubic-bezier(0.42, 0, 1, 1)` | Accelerazione iniziale (partenza dello spin) |
|
||||||
|
|
||||||
|
**Nota tecnica cruciale sui cubic-bezier**: gli assi `x` (tempo) devono stare tra 0 e 1, ma **`y` (progresso) può uscire da 0–1**. Un `y > 1` produce **overshoot** (l'elemento supera la destinazione e rientra → effetto "back"/molla); un `y < 0` produce undershoot. È così che si ottiene la sensazione elastica del rimbalzo finale con **due sole maniglie**.
|
||||||
|
|
||||||
|
Per un vero **elastic/bounce multi-oscillazione** (più rimbalzi smorzati), un singolo cubic-bezier non basta: serve un `@keyframes` complesso o una libreria (GSAP, Motion, Svelte easing). `easeOutElastic` dà "l'illusione di un rullo che ruota e poi si assesta".
|
||||||
|
|
||||||
|
### 2.3 Struttura tipica dello spin di un rullo
|
||||||
|
1. **Accelerazione** breve (`ease-in`) o partenza istantanea.
|
||||||
|
2. **Fase veloce** a velocità costante (loop di simboli, eventuale blur).
|
||||||
|
3. **Decelerazione** lunga (`ease-out`) — è qui che nasce la suspense.
|
||||||
|
4. **Settle / bounce** finale (`easeOutBack`) — il rullo si allinea al simbolo con un micro-scatto.
|
||||||
|
|
||||||
|
### 2.4 Stagger tra i rulli (fermata sequenziale)
|
||||||
|
Elemento firma delle slot: i rulli **non si fermano tutti insieme**, ma **uno dopo l'altro da sinistra a destra**. Questo:
|
||||||
|
- prolunga la tensione (l'esito totale si scopre solo all'ultimo rullo);
|
||||||
|
- abilita l'effetto **near-miss** (i primi rulli mostrano il simbolo giusto, l'ultimo tiene sul filo).
|
||||||
|
|
||||||
|
Implementazione: si applica un **delay incrementale** per indice di rullo (es. `delay = index * 400ms`, oppure lo step di aggiornamento è `50ms + index*stagger`). Con Web Animations API basta impostare `delay` diverso per ogni rullo; con CSS, `transition-delay` o `animation-delay` scalati.
|
||||||
|
|
||||||
|
### 2.5 Il "bounce/settle" finale
|
||||||
|
I videogiochi di slot inseriscono "un piccolo rimbalzo nei rulli verso la fine dello spin, subito prima che l'esito finale venga mostrato". Tecnicamente: si fa fermare il rullo **poco oltre** la posizione target e lo si riporta indietro con un `easeOutBack` o un keyframe che overshoota di ~1 riga e rientra. È il dettaglio che rende la fermata "soddisfacente" invece che secca.
|
||||||
|
|
||||||
|
**Riferimenti implementativi slot**:
|
||||||
|
- Victor Toschi, *How to create a Slot Machine animation with CSS and JavaScript* (metodi `init` / `spin` / `shuffle`) + CodePen `toschivictor/pen/JjNZjEj`.
|
||||||
|
- `ezslots` (jQuery toolkit) e `HTML-slot-machine-example` (vanilla) su GitHub.
|
||||||
|
- CodePen `MrFirthy/pen/oGVWqK` (CSS/JS slot machine).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ruota della fortuna / spinning wheel (fisica e landing)
|
||||||
|
|
||||||
|
### 3.1 Struttura
|
||||||
|
La ruota è un cerchio suddiviso in spicchi; ogni spicchio è ruotato di `360° / n * indice` con `transform-origin` sul bordo/centro. Si anima la **rotazione dell'intera ruota** (`transform: rotate(...)`) e un puntatore fisso indica il vincitore in cima.
|
||||||
|
|
||||||
|
### 3.2 Decelerazione (l'anima dell'effetto)
|
||||||
|
Come per i rulli, la magia è nella curva: la ruota parte veloce e **decelera** fino a fermarsi. Esempio reale dall'articolo DEV Community *Wheel of Fortune with CSS* (Web Animations API):
|
||||||
|
|
||||||
|
```js
|
||||||
|
animation = wheel.animate(
|
||||||
|
[
|
||||||
|
{ transform: `rotate(${previousEndDegree}deg)` },
|
||||||
|
{ transform: `rotate(${newEndDegree}deg)` }
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 4000,
|
||||||
|
easing: 'cubic-bezier(0.440, -0.205, 0.000, 1.130)', // decelerazione + micro-bounce
|
||||||
|
fill: 'forwards',
|
||||||
|
iterations: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Atterrare su un target (landing predeterminato)
|
||||||
|
Formula generale:
|
||||||
|
```
|
||||||
|
angoloSpicchio = 360 / numeroSpicchi
|
||||||
|
offsetTarget = angoloSpicchio * indiceVincitore
|
||||||
|
rotazioneTotale = (giriInteri * 360) + offsetTarget // ± correzione per il puntatore
|
||||||
|
```
|
||||||
|
- Diversi **giri interi** (es. `Math.random() * 360 + 1800`, cioè ≥5 giri) servono a rendere l'effetto credibile: "spinnare direttamente sul premio non sembra giusto, servono dei giri completi".
|
||||||
|
- Si aggiunge una **randomizzazione entro lo spicchio** così che il puntatore non cada sempre esattamente al centro (più realistico).
|
||||||
|
- Importante: **cancellare l'animazione precedente** (`animation.cancel()`) prima di rilanciarne una nuova, per evitare conflitti.
|
||||||
|
|
||||||
|
### 3.4 Riferimenti wheel
|
||||||
|
- `dev.to/madsstoumann/wheel-of-fortune-with-css` (Web Animations API, cubic-bezier a molla).
|
||||||
|
- `jamesrwilliams.ca/posts/css-wheel-of-fortune` (approccio CSS puro).
|
||||||
|
- **Winwheel.js** (`dougtesting.net/winwheel`) — libreria dedicata con `animation.stopAngle`, spin verso segmento predeterminato.
|
||||||
|
- **Konva** (`konvajs.org/docs/sandbox/Wheel_of_Fortune.html`) — canvas 2D.
|
||||||
|
- Emanuele Feronato — *JavaScript + CSS 3D Wheel of Fortune*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Card flip, scratch card, box/envelope e loot box
|
||||||
|
|
||||||
|
Alternative allo spin che possiamo usare come **variazione del reveal** (o come "atto finale" dopo lo spin).
|
||||||
|
|
||||||
|
### 4.1 Card flip (rivelazione a carta girata)
|
||||||
|
Tecnica CSS 3D standard (David DeSandro, *Intro to CSS 3D transforms — Card Flip*):
|
||||||
|
- `perspective` sul contenitore genitore → contesto 3D.
|
||||||
|
- `transform-style: preserve-3d` sulla carta.
|
||||||
|
- Due facce sovrapposte; il retro con `backface-visibility: hidden` e pre-ruotato `rotateY(180deg)`.
|
||||||
|
- Al click/hover si applica `transform: rotateY(180deg)` per rivelare.
|
||||||
|
|
||||||
|
Ottimo come **reveal secco e teatrale**: la carta si gira e mostra la stanza. Si combina bene con un piccolo delay/drumroll prima del flip.
|
||||||
|
|
||||||
|
### 4.2 Scratch card (gratta e vinci)
|
||||||
|
Con `<canvas>` e composite operation:
|
||||||
|
- Si disegna la copertura opaca sul canvas.
|
||||||
|
- `ctx.globalCompositeOperation = "destination-out"` fa sì che il movimento del cursore **cancelli** la copertura, rivelando il contenuto sotto.
|
||||||
|
- Si tracciano archi (`ctx.arc`) alla posizione del puntatore e si può calcolare la **percentuale di area grattata** per auto-rivelare al superamento di una soglia.
|
||||||
|
|
||||||
|
Vantaggio: introduce **interazione fisica** (l'utente "scopre" la propria stanza), aumentando ownership ed engagement. Riferimenti: Envato Tuts+ *scratch card effect in vanilla JavaScript*; Medium (Mr Zouraiz) *Digital Scratch Card*.
|
||||||
|
|
||||||
|
### 4.3 Box / envelope / loot box open
|
||||||
|
Pattern loot box (Overwatch-style):
|
||||||
|
1. Oggetto (box/busta) che **vibra e trema** (keyframe di shake) con intensità crescente.
|
||||||
|
2. Build-up di **particellari e luce** che anticipa la rarità/importanza.
|
||||||
|
3. **Apertura** (scale + fade + burst) e uscita del contenuto (la stanza).
|
||||||
|
|
||||||
|
Lo shake si fa con un `@keyframes` di piccole traslazioni/rotazioni alternate; l'escalation con `animation-duration` decrescente o ampiezza crescente. Il picco luminoso PRIMA del contenuto è il trucco che "vende" il reveal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Countdown, drumroll ed escalation della tensione
|
||||||
|
|
||||||
|
Prima dello spin/reveal, un **build-up** temporizzato aumenta l'attesa.
|
||||||
|
|
||||||
|
- **Countdown ring animato**: si anima `stroke-dasharray` di un cerchio SVG per "svuotare" un anello nel tempo, o si sovrappone un anello colorato che si consuma come progress bar (CSS-Tricks, *How to Create an Animated Countdown Timer*).
|
||||||
|
- **Drumroll audio + visivo**: rullo di tamburi con vibrazione/pulsazione crescente dell'elemento centrale sincronizzata; il picco coincide con l'inizio dello spin.
|
||||||
|
- **Escalation**: aumentare progressivamente frequenza dei flash, ampiezza dello shake, pitch/volume del suono. La curva di tensione deve **salire** fino al reveal e poi **scaricarsi** al settle.
|
||||||
|
- **Attenzione UX**: i countdown troppo aggressivi generano stress/panico (Medium, *The Stress of Countdown Clocks*). Per noi il countdown deve essere **eccitante, non ansiogeno** → durate brevi (2-3s), tono celebrativo, nessuna penalità reale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Come far sembrare RANDOM un risultato PREDETERMINATO
|
||||||
|
|
||||||
|
Questo è il cuore del nostro caso d'uso: l'algoritmo ha già scelto la stanza, ma l'animazione deve costruire suspense come se fosse un sorteggio dal vivo.
|
||||||
|
|
||||||
|
**Principio guida**: in *tutti* questi sistemi (slot, CS:GO case, loot box) **l'esito è già deciso prima dell'animazione** — "the weapon contained in a case is already predetermined... there is no roulette". L'animazione è pura scenografia. Quindi possiamo replicarlo in modo legittimo.
|
||||||
|
|
||||||
|
### Ricetta pratica
|
||||||
|
1. **Decidi il risultato server-side** (la stanza) e passalo al client.
|
||||||
|
2. **Costruisci una lista visiva** che *contiene* il risultato in una posizione nota, circondato da distrattori (altre stanze). Es. per una roulette orizzontale stile CS:GO: array lungo di item con il vincitore posizionato vicino alla fine, poi altri item dopo per poter "sfiorare" e superare.
|
||||||
|
3. **Anima verso quella posizione** con easing-out lungo, così l'utente vede scorrere molte opzioni e non può prevedere dove si fermerà.
|
||||||
|
4. **Sfrutta il near-miss**: fai passare/rallentare vicino ad altre stanze prima di fermarti su quella giusta (l'effetto "stava per uscire l'altra").
|
||||||
|
5. **Settle con bounce** (`easeOutBack`) sull'elemento vincitore + highlight/particellari.
|
||||||
|
6. **Non rivelare mai la posizione target in anticipo** nel DOM visibile: l'ordine può essere reale ma l'occhio non deve poter contare gli spicchi.
|
||||||
|
|
||||||
|
### Roulette orizzontale (CS:GO-style) — la più adatta a "assegnazione stanza"
|
||||||
|
È il pattern che mappa meglio sul nostro problema (una lista di stanze che scorre e si ferma su una).
|
||||||
|
- Striscia orizzontale di card (le stanze, ripetute/mischiate), dentro una finestra con `overflow: hidden` e un **marker centrale** fisso.
|
||||||
|
- Si anima `translateX` negativo fino a portare la card vincitrice sotto il marker, con `ease-out` lungo (3-6s).
|
||||||
|
- La distanza finale include un piccolo **offset random dentro la card** così che non si fermi sempre perfettamente al centro → sembra fisico.
|
||||||
|
- Riferimenti CodePen: `killerek/pen/gLBgWd` (case opening animation), `marctannous/pen/xGRGBz` (layout CS:GO + winner selection), `memir0/pen/QPEPxm` (random name picker side-scrolling). Discussione GSAP: *Creating CS:GO Roulette Horizontal Spinner*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Tecniche JS/CSS pratiche
|
||||||
|
|
||||||
|
### 7.1 Web Animations API (`element.animate`) — consigliata per risultato dinamico
|
||||||
|
Ideale quando il valore finale (posizione/rotazione) è calcolato a runtime dall'esito dell'algoritmo.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const spin = strip.animate(
|
||||||
|
[
|
||||||
|
{ transform: 'translateX(0)' },
|
||||||
|
{ transform: `translateX(${-targetX}px)` }
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 5000,
|
||||||
|
easing: 'cubic-bezier(0.12, 0.8, 0.2, 1)', // decelerazione forte
|
||||||
|
fill: 'forwards'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sapere quando è finita → trigger del settle/celebrazione
|
||||||
|
spin.finished.then(() => {
|
||||||
|
highlightWinner();
|
||||||
|
});
|
||||||
|
// oppure: spin.onfinish = () => { ... }
|
||||||
|
```
|
||||||
|
Opzioni chiave: `duration` (ms), `easing`, `iterations`, `delay` (per lo **stagger** dei rulli), `fill: 'forwards'` (mantiene lo stato finale). Controllo: `play/pause/reverse/finish/cancel`; `cancel()` prima di un nuovo spin per evitare conflitti.
|
||||||
|
|
||||||
|
### 7.2 CSS transition/animation
|
||||||
|
Semplice e performante per spin a target fisso o loop:
|
||||||
|
```css
|
||||||
|
.reel {
|
||||||
|
transition: transform 4s cubic-bezier(0.12, 0.8, 0.2, 1);
|
||||||
|
}
|
||||||
|
.reel.spinning { transform: translateY(-3000px); }
|
||||||
|
|
||||||
|
/* settle con overshoot */
|
||||||
|
.reel.settle {
|
||||||
|
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Per lo stagger: `transition-delay` / `animation-delay` scalati per indice. Nota: un cubic-bezier a 2 maniglie **non** fa bounce multipli → usare `@keyframes` o libreria.
|
||||||
|
|
||||||
|
### 7.3 requestAnimationFrame (fisica custom)
|
||||||
|
Quando serve controllo totale: decelerazione basata su velocità/attrito, snap al simbolo più vicino, motion blur dipendente dalla velocità.
|
||||||
|
```js
|
||||||
|
function frame() {
|
||||||
|
velocity *= friction; // attrito → decelerazione
|
||||||
|
offset += velocity;
|
||||||
|
strip.style.transform = `translateY(${-offset}px)`;
|
||||||
|
if (velocity > threshold) requestAnimationFrame(frame);
|
||||||
|
else snapToNearest(); // allineamento finale + bounce
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`requestAnimationFrame` sincronizza il callback con il ciclo di paint del browser → animazioni fluide ed efficienti.
|
||||||
|
|
||||||
|
### 7.4 Performance
|
||||||
|
- Animare **solo `transform` e `opacity`** (compositor-only, no reflow/repaint).
|
||||||
|
- Evitare di animare `top/left/width/height`.
|
||||||
|
- `will-change: transform` sugli elementi che spinnano; rimuoverlo a fine animazione.
|
||||||
|
- Preferire un **singolo loop rAF** condiviso a molti loop indipendenti.
|
||||||
|
|
||||||
|
### 7.5 Librerie utili
|
||||||
|
- **GSAP** — timeline, easing avanzati (`back`, `elastic`, `bounce`), usato spesso per roulette CS:GO.
|
||||||
|
- **Motion / Web Animations API improvements** (`motion.dev`) — spring nativi, DX migliorata.
|
||||||
|
- **Winwheel.js** — ruote della fortuna con landing su segmento.
|
||||||
|
- **Konva** — animazioni canvas 2D (ruota, particellari).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Applicazione consigliata al nostro reveal "assegnazione stanza"
|
||||||
|
|
||||||
|
Sequenza proposta, orchestrando i pezzi sopra:
|
||||||
|
1. **Build-up** (2-3s): countdown ring + drumroll, elemento centrale che pulsa/vibra con escalation. Tono celebrativo, non ansiogeno (§5).
|
||||||
|
2. **Spin**: roulette orizzontale CS:GO-style (§6) o rulli slot (§2) che scorrono le stanze, con blur/velocità alta.
|
||||||
|
3. **Decelerazione** lunga con `ease-out` (§2.2, §3.2): momento di massima tensione.
|
||||||
|
4. **Near-miss**: rallenta sfiorando 1-2 stanze alternative prima del target (§1.3).
|
||||||
|
5. **Settle** con `easeOutBack` sul target + burst di luce/particellari (§1.4, §2.5).
|
||||||
|
6. **Reveal finale**: opzionale card flip / apertura box sulla stanza assegnata (§4) con celebrazione.
|
||||||
|
7. **Floor positivo**: presentare qualsiasi stanza come esito desiderabile (§1.6, punto pity).
|
||||||
|
|
||||||
|
Il risultato è deciso dall'algoritmo al passo 0, ma l'utente vive un sorteggio dal vivo credibile ed eccitante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonti
|
||||||
|
|
||||||
|
**Psicologia (near-miss, variable reward, anticipazione, loot box)**
|
||||||
|
- The Psychology Behind Near Misses in Slot Games — https://news.eternalslots.com/the-psychology-behind-near-misses-in-slot-games/
|
||||||
|
- Slot Machine Psychology: How the Near Miss Effect Drives Player Behavior — https://www.casinocenter.com/slot-machine-psychology-how-the-near-miss-effect-drives-player-behavior-in-online-gaming/
|
||||||
|
- The Slot Machine Psyche: Variable Ratio Reinforcement (PSU) — https://www.psu.com/news/the-slot-machine-psyche-how-variable-ratio-reinforcement-drives-modern-gaming-engagement/
|
||||||
|
- The Psychology Behind Slot Machines (problemgambling.com) — https://www.problemgambling.com/post/the-psychology-behind-slot-machines-how-they-keep-you-gambling
|
||||||
|
- Yu-kai Chou — Decoding the Mystery Box: Reward Design — https://yukaichou.com/advanced-gamification/decoding-the-mystery-box-a-dive-into-the-intricacies-of-reward-design/
|
||||||
|
- Rare Loot Box Rewards Trigger Larger Arousal and Reward Responses (PMC) — https://pmc.ncbi.nlm.nih.gov/articles/PMC7882574/
|
||||||
|
- Loot Boxes Hook Players — How We Used the Same Psychology Ethically (Medium) — https://medium.com/avianu/loot-boxes-hook-players-heres-how-we-used-the-same-psychology-ethically-296f44261917
|
||||||
|
- Zelda's Treasure Chests and the Psychology of Random Rewards (GameTyrant) — https://gametyrant.com/news/zelda-random-rewards-psychology
|
||||||
|
|
||||||
|
**Animazione rulli slot**
|
||||||
|
- Victor Toschi — Slot Machine animation with CSS and JavaScript (Medium) — https://medium.com/@victortoschi/how-to-create-a-slot-machine-animation-with-css-and-javascript-9073ab9db9ea
|
||||||
|
- CodePen — CSS/JS Slot machine (MrFirthy) — https://codepen.io/MrFirthy/pen/oGVWqK
|
||||||
|
- ezslots (jQuery slots toolkit, GitHub) — https://github.com/kirkjerk/ezslots
|
||||||
|
- HTML slot machine example (vanilla JS, GitHub) — https://github.com/flixrp/HTML-slot-machine-example
|
||||||
|
- Create a slot machine game in vanilla JavaScript (Envato Tuts+) — https://webdesign.tutsplus.com/how-to-create-a-slot-machine-game-in-vanilla-javascript--cms-108974t
|
||||||
|
|
||||||
|
**Ruota della fortuna**
|
||||||
|
- Wheel of Fortune with CSS (DEV / madsstoumann) — https://dev.to/madsstoumann/wheel-of-fortune-with-css-p-pi-1ne9
|
||||||
|
- A CSS wheel of fortune (James R. Williams) — https://jamesrwilliams.ca/posts/css-wheel-of-fortune
|
||||||
|
- Winwheel.js — Wheel of Fortune example — https://dougtesting.net/winwheel/examples/wheel_of_fortune
|
||||||
|
- Wheel of Fortune (Konva canvas) — https://konvajs.org/docs/sandbox/Wheel_of_Fortune.html
|
||||||
|
- JavaScript + CSS 3D Wheel of Fortune (Emanuele Feronato) — https://emanueleferonato.com/2025/10/31/javascript-css-3d-wheel-of-fortune/
|
||||||
|
|
||||||
|
**Roulette orizzontale / case opening (risultato predeterminato)**
|
||||||
|
- CSGO Roulette Wheel Explained — https://csgoskins.games/csgo-roulette-wheel/
|
||||||
|
- Case opening animation (CodePen, killerek) — https://codepen.io/killerek/pen/gLBgWd
|
||||||
|
- CS:GO layout + winner selection (CodePen, marctannous) — https://codepen.io/marctannous/pen/xGRGBz
|
||||||
|
- CS:GO-like side-scrolling random name picker (CodePen, memir0) — https://codepen.io/memir0/pen/QPEPxm
|
||||||
|
- GSAP forum — Creating CS:GO Roulette Horizontal Spinner — https://gsap.com/community/forums/topic/42002-help-needed-creating-csgo-roulette-horizontal-spinner/
|
||||||
|
|
||||||
|
**Card flip, scratch card**
|
||||||
|
- Card Flip — Intro to CSS 3D transforms (David DeSandro) — https://3dtransforms.desandro.com/card-flip
|
||||||
|
- How To Create a Flip Card with CSS (W3Schools) — https://www.w3schools.com/howto/howto_css_flip_card.asp
|
||||||
|
- Scratch card effect in vanilla JavaScript (Envato Tuts+) — https://webdesign.tutsplus.com/how-to-create-a-scratch-card-effect-in-vanilla-javascript--cms-108922t
|
||||||
|
- Digital Scratch Card HTML/CSS/JS (Medium, Mr Zouraiz) — https://medium.com/@mr.zouraiz1580/digital-scratch-card-html-8eaf1dad6095
|
||||||
|
|
||||||
|
**Countdown / drumroll / tensione**
|
||||||
|
- How to Create an Animated Countdown Timer (CSS-Tricks) — https://css-tricks.com/how-to-create-an-animated-countdown-timer-with-html-css-and-javascript/
|
||||||
|
- The Stress of Countdown Clocks in UX Psychology (Medium) — https://medium.com/design-bootcamp/the-stress-of-countdown-clocks-understanding-panic-inducing-timers-in-ux-psychology-b8d1a6333691
|
||||||
|
|
||||||
|
**Easing e tecniche JS/CSS**
|
||||||
|
- Easing Functions Cheat Sheet — https://easings.net/
|
||||||
|
- cubic-bezier() (CSS-Tricks Almanac) — https://css-tricks.com/almanac/functions/c/cubic-bezier/
|
||||||
|
- Understanding easing and cubic-bezier curves (Josh Collinsworth) — https://joshcollinsworth.com/blog/easing-curves
|
||||||
|
- Incorporating elastic ease in CSS animations (LogRocket) — https://blog.logrocket.com/how-to-incorporate-elastic-ease-in-css-animations/
|
||||||
|
- Using the Web Animations API (MDN) — https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API
|
||||||
|
- Improvements to the Web Animations API (Motion) — https://motion.dev/docs/improvements-to-the-web-animations-api-dx
|
||||||
|
- JavaScript Animations with requestAnimationFrame and WAAPI — https://tutorialreference.com/javascript/advanced-topics/animation/javascript-animations-with-requestanimationframe-and-web-animations-api
|
||||||
530
docs/03-interattivita-coinvolgimento.md
Normal file
530
docs/03-interattivita-coinvolgimento.md
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
# 03 — Interattività e Coinvolgimento
|
||||||
|
|
||||||
|
> Come trasformare l'assegnazione di ~20 persone a un set di stanze in uno **spettacolo condiviso**
|
||||||
|
> in stile scommesse / game-show / slot machine, guardato tutti insieme su un unico schermo.
|
||||||
|
|
||||||
|
Questo documento raccoglie idee di game design, meccaniche di gamification, sound design,
|
||||||
|
momenti di celebrazione e soluzioni tecniche concrete (con nomi di librerie e URL CDN reali)
|
||||||
|
per rendere la web app **massimamente divertente e coinvolgente**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Il principio di base: l'attesa vale più del premio
|
||||||
|
|
||||||
|
La ricerca sul gambling e sui game-show è concorde: **l'anticipazione conta più del risultato**.
|
||||||
|
Il rilascio di dopamina è massimo *nei momenti che precedono* la rivelazione, non nella rivelazione
|
||||||
|
stessa. I "reward" imprevedibili (variable ratio reward schedule) generano una risposta emotiva
|
||||||
|
molto più forte di quelli prevedibili.
|
||||||
|
|
||||||
|
Implicazioni di design per la nostra app:
|
||||||
|
|
||||||
|
- **Non rivelare tutto insieme.** Diluire l'assegnazione nel tempo (una persona / una stanza alla
|
||||||
|
volta) moltiplica i picchi di tensione.
|
||||||
|
- **Rallentare proprio prima del risultato.** Come la ruota della fortuna che decelera: gli ultimi
|
||||||
|
secondi prima dello "stop" sono quelli che valgono.
|
||||||
|
- **Sfruttare il "quasi-vinto" (near-miss).** Far vedere che qualcuno stava per finire nella "stanza
|
||||||
|
VIP" ma poi... no. Il near-miss tiene il cervello in stato di attesa e alza l'engagement di tutta
|
||||||
|
la sala.
|
||||||
|
- **Incertezza come feature, non come bug.** Non mostrare mai l'esito prima del tempo; la sorpresa è
|
||||||
|
il prodotto.
|
||||||
|
|
||||||
|
Questi principi guidano tutte le sezioni successive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Meccaniche di engagement e gamification
|
||||||
|
|
||||||
|
### 2.1 Suspense e reveal sociale
|
||||||
|
|
||||||
|
- **Reveal una persona alla volta**: il nome della persona appare, parte lo "spin", e solo dopo si
|
||||||
|
scopre la stanza. Ogni singola assegnazione diventa un micro-evento.
|
||||||
|
- **Spinning wheel / slot reel come metafora principale**: la ruota/rullo che gira e decelera è una
|
||||||
|
delle meccaniche di suspense più efficaci perché il pubblico *vede tutti gli esiti possibili*
|
||||||
|
scorrere davanti e può "tifare" per uno di essi.
|
||||||
|
- **Ordine di rivelazione studiato per il dramma** (vedi §6): non alfabetico, ma costruito a
|
||||||
|
crescendo.
|
||||||
|
- **Countdown visibile** (3… 2… 1…) prima di ogni spin, con drumroll audio e vibrazione.
|
||||||
|
|
||||||
|
### 2.2 Scommesse / pronostici prima del reveal (il cuore "gambling")
|
||||||
|
|
||||||
|
Prima che l'algoritmo riveli le assegnazioni, si apre una **finestra di pronostici**:
|
||||||
|
|
||||||
|
- Ogni partecipante (o la sala a mano alzata, in modalità single-screen) prova a indovinare
|
||||||
|
**in quale stanza finirà una data persona**.
|
||||||
|
- Si possono mostrare **quote/odds** fittizie accanto a ogni stanza ("Stanza VIP paga 5x", "Stanza
|
||||||
|
Sgabuzzino paga 1.2x") per dare sapore da sala scommesse e orientare l'ironia del gruppo.
|
||||||
|
- **Punti / gettoni virtuali** da puntare: chi indovina di più sale in classifica.
|
||||||
|
- Il momento della rivelazione diventa doppiamente teso: non scopri solo dove va la persona, ma
|
||||||
|
anche *se hai vinto la scommessa*.
|
||||||
|
|
||||||
|
Questa meccanica sfrutta esattamente la psicologia dell'incertezza del premio: la posta in gioco
|
||||||
|
personale amplifica l'attenzione di tutti.
|
||||||
|
|
||||||
|
### 2.3 Leaderboard e classifiche
|
||||||
|
|
||||||
|
- **Classifica pronosticatori**: chi ha indovinato più assegnazioni.
|
||||||
|
- Aggiornamento **live dopo ogni reveal**, con animazione di scalata (righe che si riordinano).
|
||||||
|
- Ranking individuale e/o a squadre; le squadre aumentano il tifo e le esultanze collettive.
|
||||||
|
- La leaderboard è essa stessa un momento di spettacolo: mostrarla a schermo intero tra un blocco di
|
||||||
|
reveal e l'altro.
|
||||||
|
|
||||||
|
### 2.4 Reazioni, emoji e tifo
|
||||||
|
|
||||||
|
- **Emoji reactions in tempo reale** che volano sullo schermo condiviso (🎉 😱 🔥 👑 💀) quando esce
|
||||||
|
un'assegnazione clamorosa.
|
||||||
|
- Se i telefoni sono connessi (vedi §7), i partecipanti "lanciano" emoji dal telefono e appaiono
|
||||||
|
sul proiettore.
|
||||||
|
- In modalità puramente single-screen: reazioni scatenate da tasti/hotkey dell'MC, o semplicemente
|
||||||
|
dal tifo dal vivo amplificato dagli effetti audio/visivi.
|
||||||
|
|
||||||
|
### 2.5 Streak e momenti speciali
|
||||||
|
|
||||||
|
- **Streak del pronosticatore**: 3 assegnazioni indovinate di fila → effetto "on fire" 🔥 con audio
|
||||||
|
e confetti dedicati.
|
||||||
|
- **Streak di stanza**: se la stessa stanza riceve N persone di fila → "HOT ROOM" con luci
|
||||||
|
lampeggianti.
|
||||||
|
- **Combo/moltiplicatori**: più la streak è lunga, più i punti valgono (moltiplicatore visibile che
|
||||||
|
sale), come nei giochi arcade.
|
||||||
|
|
||||||
|
### 2.6 Feedback game-show generale
|
||||||
|
|
||||||
|
- **MC / annunciatore** (testo + audio): battute pre-scritte a tema per ogni tipo di reveal (vedi §6.4).
|
||||||
|
- **Barra di "hype"** che si riempie con le reazioni della sala.
|
||||||
|
- **Achievement/badge** sbloccabili ("Primo Jackpot", "Cappotto di pronostici", "Sfortunato del
|
||||||
|
giorno").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Sound design
|
||||||
|
|
||||||
|
Il suono è metà dello spettacolo. Serve un piccolo **kit di effetti** riutilizzabili.
|
||||||
|
|
||||||
|
### 3.1 Gli effetti chiave da avere
|
||||||
|
|
||||||
|
| Momento | Suono | Quando parte |
|
||||||
|
|---|---|---|
|
||||||
|
| Avvio spin | *whoosh* / rullo che accelera | inizio dell'animazione della slot/ruota |
|
||||||
|
| Rotazione | tick-tick-tick dei rulli | durante lo spin (loop) |
|
||||||
|
| Decelerazione | ticchettio che rallenta | ultimi secondi prima dello stop |
|
||||||
|
| Assegnazione normale | *ding* / campanello | quando si ferma su una stanza |
|
||||||
|
| Stanza VIP / jackpot | *jackpot* + monete che cadono (coin drop) | reveal di una stanza speciale |
|
||||||
|
| Pre-reveal drammatico | *drumroll* (rullo di tamburi) | durante il countdown 3-2-1 |
|
||||||
|
| Vittoria / grande momento | *crowd cheer* (folla che esulta) | jackpot, fine evento, streak |
|
||||||
|
| Errore / near-miss | *buzz* / *sad trombone* | quando un pronostico fallisce |
|
||||||
|
| Cambio schermata | *swoosh* UI | transizioni tra i blocchi |
|
||||||
|
|
||||||
|
### 3.2 Dove trovare effetti sonori GRATIS (royalty-free)
|
||||||
|
|
||||||
|
Tutte fonti utilizzabili in progetti personali e commerciali (verificare la licenza del singolo file).
|
||||||
|
|
||||||
|
**Mixkit** — nessuna registrazione, uso libero personale e commerciale:
|
||||||
|
- Slot machine: <https://mixkit.co/free-sound-effects/slot-machine/>
|
||||||
|
- Casino: <https://mixkit.co/free-sound-effects/casino/>
|
||||||
|
- Utili qui: reel spin, coin drop, big win jingle.
|
||||||
|
|
||||||
|
**Pixabay** — royalty-free, nessuna attribuzione richiesta, download MP3:
|
||||||
|
- Slot machine: <https://pixabay.com/sound-effects/search/slot-machine/>
|
||||||
|
- Slot spin: <https://pixabay.com/sound-effects/search/slot%20spin/>
|
||||||
|
- Jackpot: <https://pixabay.com/sound-effects/search/jackpot/>
|
||||||
|
- Casino: <https://pixabay.com/sound-effects/search/casino/>
|
||||||
|
- Per gli altri effetti cercare: `drumroll`, `applause` / `crowd cheer`, `coin`, `ding`, `buzzer`,
|
||||||
|
`whoosh`.
|
||||||
|
|
||||||
|
**Freesound** — database collaborativo Creative Commons (attenzione: alcuni file richiedono
|
||||||
|
attribuzione, controllare la licenza CC0 vs CC-BY):
|
||||||
|
- Home / ricerca: <https://freesound.org/>
|
||||||
|
- Esempio pack slot machine: <https://freesound.org/people/lukaso/packs/4497/>
|
||||||
|
|
||||||
|
Suggerimento pratico: scaricare gli MP3, **normalizzare i volumi** (un jackpot troppo forte rispetto
|
||||||
|
al ding rovina l'esperienza) e tenerli in una cartella `assets/audio/` con nomi parlanti
|
||||||
|
(`spin.mp3`, `ding.mp3`, `jackpot.mp3`, `drumroll.mp3`, `coin.mp3`, `cheer.mp3`, `buzz.mp3`).
|
||||||
|
|
||||||
|
### 3.3 Come far partire i suoni nel browser
|
||||||
|
|
||||||
|
Due strade: `<audio>` semplice, oppure la **Web Audio API** / **Howler.js** per controllo fine e
|
||||||
|
niente latenza.
|
||||||
|
|
||||||
|
**Attenzione all'autoplay policy.** I browser (da Chrome 71 in poi) **bloccano l'audio finché non
|
||||||
|
c'è un'interazione utente**. Un `AudioContext` creato prima di un gesto utente nasce in stato
|
||||||
|
`suspended`. Va **sbloccato al primo click/tap** (es. il bottone "Inizia lo show") chiamando
|
||||||
|
`resume()`. Best practice: un unico "gesto di sblocco" all'avvio dell'evento.
|
||||||
|
|
||||||
|
Sblocco base con Web Audio API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
|
||||||
|
// da chiamare dentro un handler di click/tap (es. bottone "Avvia lo show")
|
||||||
|
function unlockAudio() {
|
||||||
|
if (audioCtx.state === 'suspended') {
|
||||||
|
audioCtx.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.querySelector('#start-btn').addEventListener('click', unlockAudio, { once: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Howler.js** (consigliato: gestisce autoplay, sprite audio, volumi, fallback HTML5 in automatico).
|
||||||
|
|
||||||
|
CDN:
|
||||||
|
```html
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.4/howler.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
```javascript
|
||||||
|
const sfx = {
|
||||||
|
spin: new Howl({ src: ['assets/audio/spin.mp3'], volume: 0.6 }),
|
||||||
|
ding: new Howl({ src: ['assets/audio/ding.mp3'] }),
|
||||||
|
jackpot: new Howl({ src: ['assets/audio/jackpot.mp3'], volume: 0.9 }),
|
||||||
|
drumroll: new Howl({ src: ['assets/audio/drumroll.mp3'] }),
|
||||||
|
coin: new Howl({ src: ['assets/audio/coin.mp3'] }),
|
||||||
|
cheer: new Howl({ src: ['assets/audio/cheer.mp3'] }),
|
||||||
|
buzz: new Howl({ src: ['assets/audio/buzz.mp3'] }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// esempio: sequenza di un reveal
|
||||||
|
sfx.drumroll.play();
|
||||||
|
// ...al momento dello stop
|
||||||
|
sfx.ding.play();
|
||||||
|
```
|
||||||
|
|
||||||
|
Perché Howler: parte da ~7KB gzipped, usa Web Audio con **fallback su HTML5 Audio**, supporta
|
||||||
|
**audio sprite** (utile per impacchettare tutti gli effetti in un solo file e ridurre le richieste)
|
||||||
|
e semplifica volumi/fade. Docs: <https://howlerjs.com/>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Momenti di celebrazione (celebration UX)
|
||||||
|
|
||||||
|
Gli effetti visivi trasformano un reveal in un evento memorabile. La regola d'oro: **scalare
|
||||||
|
l'intensità dell'effetto in base all'importanza del momento** (assegnazione normale = piccolo puff;
|
||||||
|
jackpot VIP = tutto lo schermo esplode).
|
||||||
|
|
||||||
|
### 4.1 Confetti — `canvas-confetti`
|
||||||
|
|
||||||
|
Libreria leggera (< 10KB gzip), performante, esposta come funzione globale `confetti()`.
|
||||||
|
|
||||||
|
CDN:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js"></script>
|
||||||
|
<!-- alternativa cdnjs -->
|
||||||
|
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/canvas-confetti/1.9.3/confetti.min.js"></script> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
**Burst semplice** (assegnazione normale):
|
||||||
|
```javascript
|
||||||
|
confetti({ particleCount: 120, spread: 70, origin: { y: 0.6 } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fireworks / fuochi d'artificio** (jackpot, fine evento) — due getti laterali ripetuti:
|
||||||
|
```javascript
|
||||||
|
const duration = 4 * 1000;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
(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);
|
||||||
|
}());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Confetti a emoji/testo** (es. 👑 per la stanza VIP):
|
||||||
|
```javascript
|
||||||
|
const crown = confetti.shapeFromText({ text: '👑', scalar: 2 });
|
||||||
|
confetti({ shapes: [crown], scalar: 2, particleCount: 40, spread: 100 });
|
||||||
|
```
|
||||||
|
|
||||||
|
Opzioni utili: `particleCount`, `spread`, `angle`, `startVelocity`, `gravity`, `colors` (array HEX),
|
||||||
|
`shapes` (`square`/`circle`/`star`/custom), `origin.x/.y`, `scalar`, `ticks`.
|
||||||
|
**Accessibilità**: usare `disableForReducedMotion: true` per rispettare chi preferisce meno
|
||||||
|
animazioni.
|
||||||
|
|
||||||
|
Demo/preset ufficiali (fireworks, snow, school pride, ecc.): <https://www.kirilv.com/canvas-confetti/>
|
||||||
|
|
||||||
|
Alternativa con supporto emoji nativo: **js-confetti** — <https://github.com/loonywizard/js-confetti>.
|
||||||
|
|
||||||
|
### 4.2 Screen shake, flash e luci lampeggianti
|
||||||
|
|
||||||
|
Effetti CSS/JS a basso costo, molto d'impatto sullo schermo grande:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes shake {
|
||||||
|
0%,100% { transform: translate(0,0); }
|
||||||
|
20% { transform: translate(-8px, 4px); }
|
||||||
|
40% { transform: translate(8px, -4px); }
|
||||||
|
60% { transform: translate(-6px, -6px); }
|
||||||
|
80% { transform: translate(6px, 6px); }
|
||||||
|
}
|
||||||
|
.shake { animation: shake 0.4s ease-in-out; }
|
||||||
|
|
||||||
|
@keyframes flash {
|
||||||
|
0%,100% { background: transparent; }
|
||||||
|
50% { background: rgba(255,255,255,0.85); }
|
||||||
|
}
|
||||||
|
.flash { animation: flash 0.15s ease-in-out 3; } /* triplo flash */
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Screen shake** sul reveal del jackpot (aggiungere/rimuovere la classe `.shake` sul container).
|
||||||
|
- **Flash bianco** al momento dello "stop" della slot.
|
||||||
|
- **Luci lampeggianti** in stile casinò: bordo dello schermo con luci che corrono
|
||||||
|
(box-shadow animato o una fila di "bulbi" attorno al frame).
|
||||||
|
|
||||||
|
### 4.3 Winner spotlight (riflettore sul vincitore)
|
||||||
|
|
||||||
|
Al reveal di una persona in stanza speciale, o del "vincitore" dei pronostici:
|
||||||
|
|
||||||
|
- **Oscurare lo sfondo** (overlay scuro) e lasciare un **cono di luce/riflettore** (radial-gradient)
|
||||||
|
sul nome/foto della persona.
|
||||||
|
- **Zoom-in + pop** dell'elemento (scala da 0.8 a 1.1 e ritorno) con anime.js o CSS.
|
||||||
|
- Nome grande, corona/badge, confetti a corona (§4.1) e crowd cheer (§3).
|
||||||
|
|
||||||
|
### 4.4 Anime.js per orchestrare le animazioni
|
||||||
|
|
||||||
|
Per sequenze complesse (countdown → spin → stop → celebrazione) conviene una **timeline**.
|
||||||
|
|
||||||
|
CDN:
|
||||||
|
```html
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Esempio di reveal a più elementi con **stagger** (le lettere/righe che appaiono a cascata):
|
||||||
|
```javascript
|
||||||
|
anime({
|
||||||
|
targets: '.reveal-letter',
|
||||||
|
translateY: [40, 0],
|
||||||
|
opacity: [0, 1],
|
||||||
|
delay: anime.stagger(60), // 60ms tra un elemento e l'altro
|
||||||
|
easing: 'easeOutBack'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Anime.js gestisce timeline, keyframes, easing e staggering: ideale per sincronizzare animazione
|
||||||
|
visiva e cue audio. Docs: <https://animejs.com/documentation/>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Haptics / feedback su mobile (`navigator.vibrate`)
|
||||||
|
|
||||||
|
Se i partecipanti tengono il telefono in mano (modalità connessa, §7), la **vibrazione** aggiunge un
|
||||||
|
canale sensoriale al reveal.
|
||||||
|
|
||||||
|
API base:
|
||||||
|
```javascript
|
||||||
|
// vibrazione singola (ms)
|
||||||
|
navigator.vibrate(200);
|
||||||
|
|
||||||
|
// pattern: [vibra, pausa, vibra, ...]
|
||||||
|
navigator.vibrate([100, 50, 100]); // doppio buzz "successo"
|
||||||
|
navigator.vibrate([300, 100, 300, 100, 300]); // jackpot
|
||||||
|
```
|
||||||
|
|
||||||
|
Idee d'uso:
|
||||||
|
- **Countdown**: micro-vibrazione a ogni tick (3-2-1).
|
||||||
|
- **Spin**: vibrazione continua/pulsata durante la rotazione.
|
||||||
|
- **Stop/assegnazione**: singolo buzz secco.
|
||||||
|
- **Pronostico indovinato**: doppio buzz "successo"; **fallito**: triplo buzz "errore".
|
||||||
|
- **Jackpot**: pattern lungo e ritmico.
|
||||||
|
|
||||||
|
Best practice (da MDN):
|
||||||
|
- Durate brevi: **50–200ms** sono ideali; una nuova `vibrate()` annulla la precedente, quindi
|
||||||
|
evitare chiamate a raffica.
|
||||||
|
- Va invocata dentro un **gesto utente** (sticky user activation): legarla a click/tap.
|
||||||
|
- **Additiva, mai unico canale di feedback**: offrire un toggle per disattivarla (c'è chi la odia o
|
||||||
|
non la percepisce).
|
||||||
|
|
||||||
|
Supporto: Chrome/Edge/Opera/Samsung Internet e Android. **Safari (iOS/macOS) non la supporta** e
|
||||||
|
**Firefox l'ha rimossa da v129**: fare feature-detection e degradare con grazia.
|
||||||
|
```javascript
|
||||||
|
if ('vibrate' in navigator) { navigator.vibrate(200); }
|
||||||
|
```
|
||||||
|
Riferimento: <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Idee specifiche per il reveal "persone → stanze"
|
||||||
|
|
||||||
|
Qui sta la vera anima dello spettacolo. Suggerimenti per rendere l'assegnazione un evento condiviso.
|
||||||
|
|
||||||
|
### 6.1 Struttura del reveal, una persona alla volta
|
||||||
|
|
||||||
|
1. Appare il **nome (e foto)** della persona di turno al centro.
|
||||||
|
2. Parte lo **spin** della slot/ruota con le stanze come simboli.
|
||||||
|
3. **Countdown 3-2-1** con drumroll + vibrazione.
|
||||||
|
4. La ruota **decelera** (near-miss: rallenta vicino a una stanza "clamorosa" e poi supera).
|
||||||
|
5. **Stop** su una stanza → *ding* / o *jackpot* se speciale → confetti scalati all'importanza.
|
||||||
|
6. Aggiornamento **leaderboard pronostici** e reazioni della sala.
|
||||||
|
|
||||||
|
### 6.2 Scommesse su chi va dove
|
||||||
|
|
||||||
|
- Prima del blocco, la sala pronostica: *"Dove finirà Marco?"*.
|
||||||
|
- **Quote/odds** mostrate accanto alle stanze per ironia e strategia.
|
||||||
|
- La rivelazione risolve due tensioni insieme (dove va Marco + chi ha indovinato).
|
||||||
|
|
||||||
|
### 6.3 Stanze speciali e "VIP room jackpot"
|
||||||
|
|
||||||
|
- Designare una o più stanze come **speciali**: "Stanza VIP" 👑, "Suite Presidenziale", oppure la
|
||||||
|
temutissima "Sgabuzzino" 💀.
|
||||||
|
- Quando qualcuno ci finisce → **trattamento jackpot**: full-screen fireworks, crowd cheer, coin
|
||||||
|
drop, screen shake, spotlight.
|
||||||
|
- Le stanze speciali possono avere **posti limitati** (scarsità = più tensione a ogni assegnazione:
|
||||||
|
"resta un solo posto VIP!").
|
||||||
|
- **Progressive jackpot**: una stanza "misteriosa" il cui contenuto/premio cresce a ogni reveal
|
||||||
|
finché non viene assegnata.
|
||||||
|
|
||||||
|
### 6.4 Ordine di rivelazione per il massimo dramma
|
||||||
|
|
||||||
|
- **Non alfabetico.** Costruire un crescendo: prima assegnazioni "normali", poi i colpi di scena.
|
||||||
|
- Tenere per **ultimi** i reveal più attesi (le stanze speciali, le persone-chiave, il pareggio di
|
||||||
|
una scommessa).
|
||||||
|
- Alternare ritmo: qualche reveal veloce di fila, poi uno **rallentato al massimo** con countdown
|
||||||
|
lungo.
|
||||||
|
- **Blocchi tematici** con schermate-ponte (leaderboard, "coming up next") per far respirare la
|
||||||
|
tensione e ripartire.
|
||||||
|
|
||||||
|
### 6.5 Testi da MC / annunciatore
|
||||||
|
|
||||||
|
Frasi pre-scritte (testo a schermo + eventuale TTS/audio) a seconda dell'esito. Esempi:
|
||||||
|
|
||||||
|
- Apertura: *"Signore e signori, benvenuti alla GRANDE ASSEGNAZIONE! 20 persone, X stanze, un solo
|
||||||
|
destino. Che la sorte decida!"*
|
||||||
|
- Pre-spin: *"Il prossimo concorrente è… MARCO. Dove lo manderà la ruota? Fate le vostre puntate!"*
|
||||||
|
- Near-miss: *"Ohhh! Era a un passo dalla Suite VIP… ma no!"*
|
||||||
|
- Assegnazione normale: *"E Marco va nella Stanza 3! Un applauso."*
|
||||||
|
- Jackpot: *"JACKPOT!!! Marco conquista la STANZA VIP! 👑 Che fortuna sfacciata!"*
|
||||||
|
- Sfortuna: *"Ahia… Sgabuzzino. Condoglianze, Marco. 💀"*
|
||||||
|
- Chiusura: *"E con questo le stanze sono assegnate! Applauso finale per tutti i concorrenti!"*
|
||||||
|
|
||||||
|
### 6.6 Reazioni ed emoji legate al reveal
|
||||||
|
|
||||||
|
- Set di emoji rapide (👑 😱 🔥 💀 🎉 👏) scatenabili dalla sala/telefoni al momento giusto.
|
||||||
|
- Contatore reazioni per rendere "virale" il momento in sala.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Multiplayer / schermo condiviso (anche senza backend)
|
||||||
|
|
||||||
|
Obiettivo: farlo funzionare bene **anche senza server**, con opzioni di upgrade se serve interazione
|
||||||
|
dai telefoni.
|
||||||
|
|
||||||
|
### 7.1 Modalità "single-screen / projector" (zero backend) — consigliata per partire
|
||||||
|
|
||||||
|
- **Un solo schermo** (proiettore o TV) mostra tutto lo show; l'MC guida con tastiera/telecomando.
|
||||||
|
- Tutta la logica gira **client-side** nel browser: l'assegnazione può essere pre-calcolata o
|
||||||
|
generata in loco, poi il reveal è pura animazione.
|
||||||
|
- I pronostici si fanno **a voce / a mano alzata**, l'MC li registra o semplicemente la sala tifa.
|
||||||
|
- Pro: nessuna infrastruttura, nessun rischio di rete, massima affidabilità in evento dal vivo.
|
||||||
|
- Il modello dei party game (Jackbox, Kahoot) conferma che *un solo schermo condiviso* è al centro
|
||||||
|
dell'esperienza collettiva; i telefoni sono solo controller opzionali.
|
||||||
|
|
||||||
|
### 7.2 Aggiungere i telefoni con un QR code
|
||||||
|
|
||||||
|
Come Jackbox.tv / Kahoot: a schermo appare un **QR code + codice stanza**; i partecipanti scansionano
|
||||||
|
e aprono una pagina "controller" nel browser (nessuna app da installare).
|
||||||
|
|
||||||
|
Generare il QR lato client:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||||
|
<div id="qr"></div>
|
||||||
|
<script>
|
||||||
|
new QRCode(document.getElementById('qr'), 'https://tuo-dominio/join?room=AB12');
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
(libreria: **qrcodejs** — <https://github.com/davidshimjs/qrcodejs>)
|
||||||
|
|
||||||
|
### 7.3 Sincronizzazione telefoni ↔ schermo con backend minimale
|
||||||
|
|
||||||
|
Se vuoi che i pronostici/emoji dai telefoni appaiano in tempo reale sul proiettore, serve un piccolo
|
||||||
|
canale realtime. Opzioni leggere:
|
||||||
|
|
||||||
|
- **BroadcastChannel API** — *gratis, zero backend*, ma solo tra schede **dello stesso dispositivo/
|
||||||
|
browser** (utile per prototipare o per setup a più finestre sullo stesso PC). Non attraversa
|
||||||
|
dispositivi diversi.
|
||||||
|
- **WebSocket** su un piccolo server (Node) o servizio serverless.
|
||||||
|
- **Cloudflare Durable Objects** — ottimi per "stanze" realtime multiplayer con stato condiviso
|
||||||
|
(coordinamento di una room, WebSocket, stato per-stanza) senza gestire server tradizionali.
|
||||||
|
- Servizi realtime gestiti (es. Firebase Realtime Database / Firestore, Supabase Realtime) se si
|
||||||
|
preferisce non scrivere backend.
|
||||||
|
|
||||||
|
Raccomandazione: **partire single-screen (§7.1)**, aggiungere il **QR + controller** (§7.2) solo se
|
||||||
|
si vuole l'interazione dai telefoni, e introdurre un backend realtime **solo se** i pronostici live
|
||||||
|
diventano centrali.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Stack tecnico consigliato (riepilogo librerie e CDN)
|
||||||
|
|
||||||
|
| Scopo | Libreria | CDN |
|
||||||
|
|---|---|---|
|
||||||
|
| Confetti / fuochi | **canvas-confetti** | `https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js` |
|
||||||
|
| Confetti con emoji (alt.) | **js-confetti** | <https://github.com/loonywizard/js-confetti> |
|
||||||
|
| Audio / SFX | **Howler.js** | `https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.4/howler.min.js` |
|
||||||
|
| Animazioni / timeline | **anime.js** | `https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js` |
|
||||||
|
| QR code join | **qrcodejs** | `https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js` |
|
||||||
|
| Haptics | **Vibration API** (nativa) | `navigator.vibrate()` |
|
||||||
|
| Sblocco/gestione audio | **Web Audio API** (nativa) | `AudioContext` + `resume()` |
|
||||||
|
| Realtime opzionale | **Cloudflare Durable Objects** / WebSocket / Firebase | — |
|
||||||
|
|
||||||
|
**Checklist di implementazione**
|
||||||
|
|
||||||
|
- [ ] Sbloccare l'audio al primo tap (bottone "Avvia lo show").
|
||||||
|
- [ ] Precaricare tutti gli SFX (Howler) e le immagini prima dell'evento.
|
||||||
|
- [ ] Scalare gli effetti (confetti/shake/audio) sull'importanza del reveal.
|
||||||
|
- [ ] Rispettare `prefers-reduced-motion` (`disableForReducedMotion`) e offrire toggle audio/haptics.
|
||||||
|
- [ ] Feature-detection per `navigator.vibrate` (assente su iOS/Safari e Firefox recenti).
|
||||||
|
- [ ] Testare su schermo grande + audio potente (l'evento è dal vivo, il volume conta).
|
||||||
|
- [ ] Curare l'ordine di reveal per il crescendo drammatico.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonti
|
||||||
|
|
||||||
|
**Confetti / animazioni**
|
||||||
|
- canvas-confetti (GitHub): <https://github.com/catdad/canvas-confetti>
|
||||||
|
- canvas-confetti (npm): <https://www.npmjs.com/package/canvas-confetti>
|
||||||
|
- canvas-confetti (cdnjs): <https://cdnjs.com/libraries/canvas-confetti>
|
||||||
|
- canvas-confetti (jsDelivr): <https://www.jsdelivr.com/package/npm/canvas-confetti>
|
||||||
|
- Demo/preset canvas-confetti (Kiril Vatev): <https://www.kirilv.com/canvas-confetti/>
|
||||||
|
- js-confetti (emoji): <https://github.com/loonywizard/js-confetti>
|
||||||
|
- anime.js (sito): <https://animejs.com/>
|
||||||
|
- anime.js (documentazione): <https://animejs.com/documentation/>
|
||||||
|
- anime.js — stagger: <https://animejs.com/documentation/utilities/stagger/>
|
||||||
|
|
||||||
|
**Audio / Web Audio**
|
||||||
|
- Howler.js (sito): <https://howlerjs.com/>
|
||||||
|
- Howler.js (GitHub): <https://github.com/goldfire/howler.js/>
|
||||||
|
- Howler.js (cdnjs): <https://cdnjs.com/libraries/howler>
|
||||||
|
- MDN — Using the Web Audio API: <https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API>
|
||||||
|
- MDN — Autoplay guide: <https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Autoplay>
|
||||||
|
- MDN — Web Audio API best practices: <https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices>
|
||||||
|
- Chrome for Developers — Web Audio, Autoplay Policy and Games: <https://developer.chrome.com/blog/web-audio-autoplay>
|
||||||
|
- Chrome for Developers — Autoplay policy in Chrome: <https://developer.chrome.com/blog/autoplay>
|
||||||
|
|
||||||
|
**Effetti sonori gratuiti**
|
||||||
|
- Mixkit — Slot Machine: <https://mixkit.co/free-sound-effects/slot-machine/>
|
||||||
|
- Mixkit — Casino: <https://mixkit.co/free-sound-effects/casino/>
|
||||||
|
- Pixabay — Slot Machine: <https://pixabay.com/sound-effects/search/slot-machine/>
|
||||||
|
- Pixabay — Slot Spin: <https://pixabay.com/sound-effects/search/slot%20spin/>
|
||||||
|
- Pixabay — Jackpot: <https://pixabay.com/sound-effects/search/jackpot/>
|
||||||
|
- Pixabay — Casino: <https://pixabay.com/sound-effects/search/casino/>
|
||||||
|
- Freesound (home): <https://freesound.org/>
|
||||||
|
- Freesound — pack Slot Machine (lukaso): <https://freesound.org/people/lukaso/packs/4497/>
|
||||||
|
|
||||||
|
**Haptics**
|
||||||
|
- MDN — Vibration API: <https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API>
|
||||||
|
- MDN — Navigator.vibrate(): <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate>
|
||||||
|
- WICG — Web Haptics API: <https://github.com/WICG/web-haptics>
|
||||||
|
|
||||||
|
**Gamification / game-show / psicologia**
|
||||||
|
- Yu-kai Chou — The Spinning Wheel (gamification): <https://yukaichou.com/gamification-analysis/the-spinning-wheel-a-comprehensive-guide-to-boosting-user-engagement/>
|
||||||
|
- Guidebook — Event App Gamification: <https://www.guidebook.com/post/master-event-gamification-with-these-engaging-app-features>
|
||||||
|
- Contest Factory — Gamification & Interactive Mechanics: <https://contestfactory.com/gamification-interactive-mechanics-that-drive-participation/>
|
||||||
|
- The Conversation — How gambling distorts reality and hooks your brain: <https://theconversation.com/designed-to-deceive-how-gambling-distorts-reality-and-hooks-your-brain-91052>
|
||||||
|
- NCBI/PMC — Reward anticipation and outcome evaluation in gambling: <https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3971161/>
|
||||||
|
- Near-miss effect: <https://accenteyes.com/how-near-misses-deceive-your-brain-into-persistent-betting/>
|
||||||
|
|
||||||
|
**Schermo condiviso / multiplayer**
|
||||||
|
- Jackbox — How to play remotely (QR / room code): <https://www.jackboxgames.com/blog/how-to-play-jackbox-games-with-friends-and-family-remotely>
|
||||||
|
- Kahoot — How to host a live kahoot: <https://support.kahoot.com/hc/en-us/articles/360039422694-How-to-host-a-live-kahoot>
|
||||||
|
- Kahoot — How to join (PIN / link / QR): <https://support.kahoot.com/hc/en-us/articles/360039890713-Kahoot-join-How-to-join-a-Kahoot-game>
|
||||||
|
- qrcodejs: <https://github.com/davidshimjs/qrcodejs>
|
||||||
|
- MDN — BroadcastChannel API: <https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API>
|
||||||
|
- Cloudflare Durable Objects: <https://developers.cloudflare.com/durable-objects/>
|
||||||
708
docs/04-implementazione-vanilla-js.md
Normal file
708
docs/04-implementazione-vanilla-js.md
Normal 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>
|
||||||
44
docs/README.md
Normal file
44
docs/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 🎰 Elsa Gambling — Docs
|
||||||
|
|
||||||
|
Materiale di ricerca per costruire una web app (HTML + JS vanilla) che assegna ~20 persone a un
|
||||||
|
insieme di stanze, e **rivela l'assegnazione come uno show di scommesse/slot machine**: il risultato
|
||||||
|
lo decide un algoritmo, ma l'esperienza è pura suspense da casinò.
|
||||||
|
|
||||||
|
Tutti i documenti sono ricerche fatte sul web (fonti reali con URL in fondo a ogni file).
|
||||||
|
|
||||||
|
## Indice
|
||||||
|
|
||||||
|
| # | Documento | A cosa serve |
|
||||||
|
|---|-----------|--------------|
|
||||||
|
| 01 | [Stile visivo gambling](./01-stile-visivo-gambling.md) | Palette neon/oro, font LED/7-segment, motivi slot/chip/jackpot, tecniche CSS glow copiabili |
|
||||||
|
| 02 | [Meccaniche suspense & reveal](./02-meccaniche-suspense-reveal.md) | Psicologia dello spin&reveal, easing dei rulli, **come far sembrare "random" un risultato già deciso** |
|
||||||
|
| 03 | [Interattività & coinvolgimento](./03-interattivita-coinvolgimento.md) | Gamification, scommesse/quote, sound design (SFX free), coriandoli, modalità schermo condiviso |
|
||||||
|
| 04 | [Implementazione vanilla JS](./04-implementazione-vanilla-js.md) | Codice concreto: rulli, ruota, canvas-confetti, audio, font Google, setup single-file |
|
||||||
|
|
||||||
|
## Idea in una riga
|
||||||
|
|
||||||
|
> L'algoritmo calcola l'assegnazione **persona → stanza**. La UI non la mostra: la **fa girare**.
|
||||||
|
> Rulli che rallentano, near-miss, drumroll, poi il nome della stanza si ferma con un *ding* e
|
||||||
|
> partono i coriandoli. Persona dopo persona, in crescendo, con la VIP room come jackpot finale.
|
||||||
|
|
||||||
|
## Punti chiave emersi dalla ricerca
|
||||||
|
|
||||||
|
- **L'attesa vale più del premio**: la dopamina è nell'anticipazione, non nel risultato → investire
|
||||||
|
sull'animazione di build-up più che sul reveal secco (doc 02, 03).
|
||||||
|
- **Predeterminato ≠ noioso**: si accumulano giri interi casuali e si atterra sull'indice/angolo target
|
||||||
|
con un `cubic-bezier` easeOut/easeOutBack. Sembra random, ma finisce dove vuoi tu (doc 02, 04).
|
||||||
|
- **Solo `transform` e `opacity`** per animare fluido a 60fps; rispettare `prefers-reduced-motion` (doc 01, 04).
|
||||||
|
- **Zero backend per partire**: modalità single-screen/proiettore. Upgrade opzionale con QR code stile
|
||||||
|
Jackbox/Kahoot o realtime (doc 03).
|
||||||
|
- **Stack leggero via CDN**: `canvas-confetti` per le celebrazioni, `Howler.js` per l'audio; tutto il
|
||||||
|
resto CSS/JS vanilla (doc 03, 04).
|
||||||
|
|
||||||
|
## Prossimo passo
|
||||||
|
|
||||||
|
Definire l'**algoritmo di matching** (vincoli: capienza stanze, preferenze, coppie/gruppi, eventuali
|
||||||
|
"quote" per il gioco delle scommesse). Poi lo attacchiamo al livello di reveal descritto nel doc 02/04.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- Le palette hex nel doc 01 sono coerenti e pronte all'uso, ma non sono brand ufficiali (dichiarato nel file).
|
||||||
|
- `DSEG` (font 7-segment) va self-hostato: non è su Google Fonts. Fallback solo-Google: Share Tech Mono / DotGothic16 (doc 04).
|
||||||
Reference in New Issue
Block a user