Vai al contenuto
animorio
EN
Tutti i componenti

Il selettore della macinatura

Selettori, Fisica, SVG, TransizioniCanvas, GSAPImpegnativaPeso compresso: Demo 20 KBGSAP 28 KBGratisScarica lo zip

La macinatura del caffè nel carrello di una torrefazione, da Espresso a Whole beans, accanto a un piccolo macinino che riempie un barattolo di vetro: scegliendo, il fondo del barattolo si apre e il caffè scende nella base, la ghiera gira, le macine girano e il macinato nuovo cade dallo scivolo e si ammucchia col suo angolo. I granelli hanno una fisica granulare vera, e la loro grana è la macinatura.

Il codice

Una cartella che funziona da sola: index.html, style.css, script.js e le librerie da CDN. Nelle schede React e Vue la stessa demo è già un componente, che spegne le sue animazioni quando esce dalla pagina.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The grind picker</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the grind picker of a coffee roaster's cart: five real radio buttons, from Espresso to Whole beans, beside a
         small burr grinder that fills a glass jar. Choosing a grind, the jar's floor opens and the coffee in it drops
         into the stand, the grind ring turns to the new setting, the burrs spin and the new coffee falls from the chute
         and heaps up in the jar. The grains are a canvas with real granular physics, and their size is the grind:
         a fine powder for espresso, coarse chips for French press, whole beans with their crease. The grinder and the
         glass are SVG, behind and in front of the grains -->
    <div class="grind" data-grind>
      <div class="grind__machine" aria-hidden="true" data-machine>
        <svg class="grind__art" viewBox="0 0 256 340" data-art="back">
          <defs>
            <linearGradient id="gm-body" x1="0" y1="0" x2="1" y2="0">
              <stop class="s-body-light" offset="0"/>
              <stop class="s-body" offset="0.32"/>
              <stop class="s-body" offset="0.62"/>
              <stop class="s-body-dark" offset="1"/>
            </linearGradient>
            <linearGradient id="gm-steel" x1="0" y1="0" x2="1" y2="0">
              <stop class="s-steel-dark" offset="0"/>
              <stop class="s-steel-light" offset="0.24"/>
              <stop class="s-steel" offset="0.55"/>
              <stop class="s-steel-dark" offset="0.86"/>
              <stop class="s-steel" offset="1"/>
            </linearGradient>
            <linearGradient id="gm-steel-v" x1="0" y1="0" x2="0" y2="1">
              <stop class="s-steel-light" offset="0"/>
              <stop class="s-steel" offset="0.45"/>
              <stop class="s-steel-dark" offset="1"/>
            </linearGradient>
            <linearGradient id="gm-cone" x1="0" y1="0" x2="1" y2="0">
              <stop class="s-steel-dark" offset="0"/>
              <stop class="s-steel-light" offset="0.34"/>
              <stop class="s-steel" offset="0.62"/>
              <stop class="s-steel-deep" offset="1"/>
            </linearGradient>
            <linearGradient id="gm-copper" x1="0" y1="0" x2="0" y2="1">
              <stop class="s-copper-light" offset="0"/>
              <stop class="s-copper" offset="0.6"/>
              <stop class="s-copper-dark" offset="1"/>
            </linearGradient>
            <linearGradient id="gm-glass" x1="0" y1="0" x2="1" y2="0">
              <stop class="s-glass" offset="0" stop-opacity="0.9"/>
              <stop class="s-glass" offset="0.2" stop-opacity="0.35"/>
              <stop class="s-glass" offset="0.8" stop-opacity="0.35"/>
              <stop class="s-glass" offset="1" stop-opacity="0.9"/>
            </linearGradient>
            <linearGradient id="gm-wall" x1="0" y1="0" x2="1" y2="0">
              <stop class="s-glass-edge" offset="0" stop-opacity="0.75"/>
              <stop class="s-glass" offset="0.6" stop-opacity="0.5"/>
              <stop class="s-glint" offset="1" stop-opacity="0.35"/>
            </linearGradient>
            <linearGradient id="gm-wall-r" x1="1" y1="0" x2="0" y2="0">
              <stop class="s-glass-edge" offset="0" stop-opacity="0.75"/>
              <stop class="s-glass" offset="0.6" stop-opacity="0.5"/>
              <stop class="s-glint" offset="1" stop-opacity="0.3"/>
            </linearGradient>
            <linearGradient id="gm-glint" x1="0" y1="0" x2="0" y2="1">
              <stop class="s-glint" offset="0" stop-opacity="0"/>
              <stop class="s-glint" offset="0.18" stop-opacity="0.75"/>
              <stop class="s-glint" offset="0.7" stop-opacity="0.45"/>
              <stop class="s-glint" offset="1" stop-opacity="0"/>
            </linearGradient>
            <linearGradient id="gm-shade" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" stop-color="#000" stop-opacity="0.5"/>
              <stop offset="0.3" stop-color="#000" stop-opacity="0"/>
              <stop offset="0.72" stop-color="#000" stop-opacity="0"/>
              <stop offset="1" stop-color="#000" stop-opacity="0.55"/>
            </linearGradient>
            <filter id="gm-soft" x="-20%" y="-200%" width="140%" height="500%"><feGaussianBlur stdDeviation="3.2"/></filter>
            <clipPath id="gm-cone-clip"><path d="M52 131H68L80 161H40Z"/></clipPath>
            <clipPath id="gm-collar-clip"><rect x="22" y="100" width="76" height="16" rx="3"/></clipPath>
          </defs>

          <!-- the shadow on the page -->
          <ellipse class="m-shadow" cx="128" cy="331" rx="120" ry="4.5" filter="url(#gm-soft)"/>

          <g data-shake>
            <!-- the hopper: its lid, the back of its glass -->
            <rect x="16" y="10" width="88" height="13" rx="5" fill="url(#gm-body)"/>
            <rect class="m-edge-light" x="18" y="11" width="84" height="1.2" rx="0.6"/>
            <rect x="52" y="3.5" width="16" height="8" rx="3" fill="url(#gm-copper)"/>
            <path class="m-glass-back" d="M19 23H101L83 95Q81.5 100 77 100H43Q38.5 100 37 95Z"/>

            <!-- the grind ring: brushed steel, its marks drawn by the script as it turns -->
            <rect x="22" y="100" width="76" height="16" rx="3" fill="url(#gm-steel)"/>
            <g clip-path="url(#gm-collar-clip)" data-ticks></g>
            <rect class="m-ring-shade" x="22" y="100" width="76" height="16" rx="3" fill="url(#gm-shade)"/>

            <!-- the body -->
            <path d="M25 116H95V306H25Z" fill="url(#gm-body)"/>
            <rect class="m-edge-light" x="29" y="116" width="1.4" height="182" rx="0.7"/>
            <path class="m-seam" d="M25 176.5H95"/>
            <path class="m-pointer" d="M56 122.5L60 117.5L64 122.5Z"/>

            <!-- the window onto the burrs: the inner cone with its teeth, the outer ring cut in half -->
            <rect class="m-window" x="36" y="127" width="48" height="36" rx="7"/>
            <path d="M52 131H68L80 161H40Z" fill="url(#gm-cone)"/>
            <g clip-path="url(#gm-cone-clip)"><g class="m-teeth" data-teeth></g></g>
            <path d="M52 131H68L80 161H40Z" fill="url(#gm-shade)" opacity="0.8"/>
            <path class="m-burr-ring" d="M38 129.5H47L41.5 161H38Z M82 129.5H73L78.5 161H82Z"/>
            <path class="m-burr-teeth" d="M47 133L44.5 134.6M46.2 138L43.6 139.6M45.3 143L42.7 144.6M44.4 148L41.8 149.6M43.5 153L40.9 154.6M73 133L75.5 134.6M73.8 138L76.4 139.6M74.7 143L77.3 144.6M75.6 148L78.2 149.6M76.5 153L79.1 154.6"/>
            <path class="m-window-glint" d="M41 158L52 130H57L46 158Z"/>
            <rect class="m-window-rim" x="36" y="127" width="48" height="36" rx="7"/>

            <!-- the switch, low on the body -->
            <rect class="m-switch" x="52" y="236" width="16" height="24" rx="4"/>
            <rect class="m-switch-top" x="54" y="238" width="12" height="9" rx="2.5"/>

            <!-- the foot -->
            <rect x="18" y="298" width="84" height="19" rx="5" fill="url(#gm-body)"/>
            <rect class="m-edge-light" x="21" y="298.6" width="78" height="1.1" rx="0.55"/>
          </g>

          <!-- the jar's glass behind the coffee, and the far half of its rim -->
          <rect x="120" y="206" width="116" height="92" fill="url(#gm-glass)"/>
          <path class="m-rim-back" d="M120.5 206.5C120.5 200.5 235.5 200.5 235.5 206.5"/>
        </svg>

        <canvas class="grind__grounds" data-canvas></canvas>

        <svg class="grind__art" viewBox="0 0 256 340" data-art="front">
          <!-- the hopper's glass, in front of the beans -->
          <g data-shake>
            <path class="m-glass-front" d="M19 23H101L83 95Q81.5 100 77 100H43Q38.5 100 37 95Z"/>
            <path class="m-glint-line" d="M27 28L44.5 90"/>
            <path class="m-glint-line is-thin" d="M93 28L80.5 78"/>

            <!-- the chute, out of the burrs and down into the jar -->
            <path d="M94 135H120C146 135 163.8 165.2 171.3 185.9L148.7 194.1C143.2 179.1 135 165 116 165H94Z" fill="url(#gm-steel-v)"/>
            <path class="m-chute-lip" d="M94 135.6H120C146 135.6 163.6 165.6 170.9 185.6"/>
            <path class="m-chute-mouth" d="M171.3 185.9L148.7 194.1"/>
            <rect x="91.5" y="132" width="6" height="36" rx="2.5" fill="url(#gm-body)"/>
          </g>

          <!-- the jar: its walls seen edge-on, the light on the glass and the near half of the rim -->
          <g data-jar>
            <rect x="120" y="206" width="5" height="90" fill="url(#gm-wall)"/>
            <rect x="231" y="206" width="5" height="90" fill="url(#gm-wall-r)"/>
            <path class="m-glass-line" d="M120.5 206V296M235.5 206V296"/>
            <rect x="131" y="214" width="6" height="80" rx="3" fill="url(#gm-glint)"/>
            <rect x="140" y="222" width="1.6" height="58" rx="0.8" fill="url(#gm-glint)" opacity="0.6"/>
            <rect x="223" y="216" width="2.4" height="72" rx="1.2" fill="url(#gm-glint)" opacity="0.7"/>
            <path class="m-rim-front" d="M120.5 206.5C120.5 212.5 235.5 212.5 235.5 206.5"/>
            <path class="m-rim-glint" d="M128 209.6C150 212 206 212 228 209.6"/>
          </g>

          <!-- the stand the jar sits in: its floor opens into it. The little lever on its side moves with the gate -->
          <g data-lever>
            <path class="m-lever" d="M238 303.2H249"/>
            <rect x="246.5" y="300.2" width="8" height="6" rx="3" fill="url(#gm-copper)"/>
          </g>
          <rect x="116" y="294" width="124" height="23" rx="3" fill="url(#gm-steel-v)"/>
          <rect x="116" y="294" width="124" height="23" rx="3" fill="url(#gm-shade)" opacity="0.5"/>
          <path class="m-stand-lip" d="M118 296.2H238"/>
          <circle class="m-pivot" cx="238.5" cy="303.2" r="2.2"/>

          <!-- the base under it all -->
          <rect x="6" y="316" width="244" height="14" rx="5" fill="url(#gm-body)"/>
          <rect class="m-edge-light" x="9" y="316.6" width="238" height="1.1" rx="0.55"/>
        </svg>
      </div>

      <div class="grind__pick">
        <div class="grind__head">
          <span class="grind__title" id="grind-title">Grind</span>
          <span class="grind__hint">Ground the day we ship</span>
        </div>
        <div class="grind__list" role="radiogroup" aria-labelledby="grind-title" data-list>
          <span class="grind__mark" aria-hidden="true" data-mark></span>
          <label class="opt" data-opt>
            <input class="opt__radio" type="radio" name="grind" value="espresso">
            <span class="opt__name">Espresso</span>
            <span class="opt__line">Fine as table salt</span>
            <span class="opt__size">250 µm</span>
          </label>
          <label class="opt" data-opt>
            <input class="opt__radio" type="radio" name="grind" value="moka">
            <span class="opt__name">Moka</span>
            <span class="opt__line">A touch coarser, like fine sand</span>
            <span class="opt__size">450 µm</span>
          </label>
          <label class="opt" data-opt>
            <input class="opt__radio" type="radio" name="grind" value="filter">
            <span class="opt__name">Filter</span>
            <span class="opt__line">Medium, like beach sand</span>
            <span class="opt__size">700 µm</span>
          </label>
          <label class="opt" data-opt>
            <input class="opt__radio" type="radio" name="grind" value="french-press">
            <span class="opt__name">French press</span>
            <span class="opt__line">Coarse as sea salt</span>
            <span class="opt__size">1 mm</span>
          </label>
          <label class="opt" data-opt>
            <input class="opt__radio" type="radio" name="grind" value="whole-beans">
            <span class="opt__name">Whole beans</span>
            <span class="opt__line">Grind them just before you brew</span>
            <span class="opt__size">Whole</span>
          </label>
        </div>
      </div>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Cambia valori, colori e testi con le manopole e guarda l’animazione rispondere dal vivo. Oppure scrivi tu nel codice. Poi scarica la cartella con le tue modifiche.

58 valori da cambiare

Oppure chiedila a Claude Code

Col connettore di Animorio, Claude Code la prende da qui con tutta la sua guida e la adatta al tuo progetto. Incolla questa richiesta:

Prendi «Il selettore della macinatura» da Animorio e adattala al mio sito

Cosa fa

Su un fondo bianco, a sinistra un macinino a macine nero opaco su una base scura: in cima una tramoggia di vetro piena di chicchi, sotto la ghiera d'acciaio con le sue tacche e una piccola freccia di rame, una finestrella da cui si vede la macina conica, e uno scivolo d'acciaio curvo che scende sopra un barattolo di vetro appoggiato su un supporto con una levetta di rame. Nel barattolo c'è un mucchio di caffè macinato. A destra il titolo Grind, la riga Ground the day we ship e cinque opzioni, ognuna col nome, una riga che descrive la grana e la misura: Espresso (Fine as table salt, 250 µm), Moka, Filter, French press e Whole beans. Quella scelta sta su una scheda color crema con un bordo scuro. Scegliendone un'altra la scheda scivola sulla nuova con un piccolo rimbalzo; nel barattolo i due mezzi fondi si aprono verso il basso e il caffè scende nella base, prima al centro poi dai lati, mentre la levetta si abbassa; la ghiera gira fino alla tacca della nuova macinatura, le macine girano nella finestrella, il macinino vibra appena e i chicchi nella tramoggia tremano. Il fondo si richiude e dallo scivolo cade il caffè nuovo: una pioggia di polvere finissima per l'espresso, granelli per moka e filtro, schegge spigolose per la French press, chicchi interi col loro solco che rimbalzano e rotolano. Il macinato si ammucchia sotto il getto e frana sui fianchi finché il mucchio sta fermo col suo angolo, più ripido quanto più è fine.

Come funziona

Un macinino in due strati

Il macinino e il barattolo sono SVG disegnati a mano, in due strati: dietro la tela il corpo, la tramoggia, la ghiera e il vetro di fondo del barattolo; davanti il vetro della tramoggia, lo scivolo, i fianchi e i riflessi del barattolo, il supporto e la base. Così il caffè passa dietro lo scivolo, dentro il vetro, e sparisce nel supporto. Le tacche della ghiera sono un cilindro visto di fronte: ognuna sta a x = centro + raggio · sin(angolo) e si schiarisce con cos(angolo), e la ghiera gira con una molla di GSAP fino alla tacca della macinatura scelta. I denti della macina scorrono nella finestrella mentre il motore gira.

Granelli sulle posizioni

Ogni granello è un cerchio, e il mondo avanza a passi di 1/240 di secondo (un fotogramma lento conta al massimo per 1/30): la gravità sposta i granelli, poi due passate li separano dove si sovrappongono e dal vetro, e la velocità è lo spostamento diviso il passo. L'attrito agisce sulle posizioni: uno scivolamento più corto dell'aderenza (GRIP) per la sovrapposizione si annulla, uno più lungo si accorcia. Prima di ogni passo quello che si sovrappone ancora si separa senza dare velocità, i granelli più in basso pesano di più, e i vicini si trovano con una griglia ordinata contando.

La grana è la macinatura

Il raggio dei granelli viene da SIZES, con un po' di polvere più fine dentro, come in ogni macinatura vera; il loro numero viene da DOSE, così il volume di caffè è sempre lo stesso: quasi 1500 granelli per l'espresso, meno di 140 per la French press, 35 chicchi interi. L'aria frena di più i granelli piccoli (la resistenza è divisa per il raggio), e la polvere scende morbida mentre le schegge cadono dritte. Un chicco intero sono due cerchi a distanza fissa, che rimbalzano un poco quando urtano: così il chicco si posa di piatto, si gira e rotola.

Il fondo che si apre e il mucchio che si ferma

Il fondo del barattolo sono due piastre che girano verso il basso intorno ai cardini sulle pareti: il caffè scende prima al centro e poi dai lati, e quello che passa sotto la base sparisce. Quando il barattolo è vuoto il fondo si richiude e lo scivolo versa la dose a file di granelli affiancati, mai sovrapposti, in POUR secondi. Un granello che resta entro metà del suo raggio per un attimo si addormenta e conta come fermo; uno coperto da altri si addormenta prima, così si muove solo la pelle del mucchio, come in un mucchio vero. Quando tutto dorme non si ridisegna più niente.

Disegnati una volta

Ogni grana è una dozzina di immagini dipinte una volta per la densità dello schermo: schegge con pochi spigoli, una faccia più chiara dove il chicco è stato tagliato e un bordo scuro; i chicchi hanno il chiaroscuro della tostatura, il solco e la pellicina chiara rimasta dentro. Ogni granello posa la sua immagine girata con lui. Dietro i granelli fermi c'è un tondo più scuro, che fa sembrare caffè anche gli spazi fra loro; i granelli addormentati stanno in un'immagine a parte rifatta solo quando cambiano, e una sfumatura sopra tutto scurisce il caffè verso i fianchi del barattolo, che è tondo.

Valori da cambiare

NomeFileDi partenzaCosa cambia
STARTscript.js2La macinatura scelta all'apertura della pagina (0 è Espresso, 4 Whole beans).
SIZESscript.js[0.85, 1.3, 1.9, 2.8]Quanto è grande un granello per ogni macinatura, dalla più fine: il suo raggio, in pixel del disegno.
BEANscript.js[12.4, 8.6]Un chicco intero, lungo e largo, in pixel del disegno.
DOSEscript.js0.38Quanta parte del barattolo riempie il caffè.
POURscript.js1.35I secondi in cui il macinino riempie il barattolo.
GRAVITYscript.js1700Quanto in fretta cade il caffè, in pixel al secondo al quadrato.
GRIPscript.js[1.15, 0.9, 0.6, 0.44, 0.3]Quanto i granelli si tengono fra loro, per ogni macinatura: di più, un mucchio più ripido.
SPINscript.js1Quanto in fretta girano le macine nella finestrella.

Provali nel Playground

Quando usarla

Nella scheda di un caffè in grani o nel carrello di una torrefazione, per scegliere la macinatura; vale per ogni scelta in cui la differenza fra le opzioni è una grana o una misura che si può far vedere: sale e zucchero, farine, tè sfuso, ghiaia per un acquario, sabbia per i giardini.

Sul telefono

Sotto i 640 pixel il macinino sta sopra le opzioni, largo 212 pixel, e le opzioni prendono tutta la larghezza: niente sfora. La fisica è la stessa, con gli stessi granelli: cambia solo la misura a cui la tela li disegna. Il tocco su un'opzione è un clic subito, senza aspettare il doppio tocco.

Con il movimento ridotto

Niente caffè che cade: il mucchio nuovo si fa subito, già fermo, e sostituisce il vecchio in dissolvenza; la ghiera va alla sua tacca di colpo, le macine non girano e il macinino non vibra. La scheda crema sparisce e ricompare sull'opzione nuova invece di scivolare, e all'apertura tutto compare in dissolvenza.

Trappole

  • I primi granelli schizzavano in tutte le direzioni appena usciti dallo scivolo: nascevano sovrapposti e la correzione delle posizioni, divisa per un passo di 1/240 di secondo, diventava una velocità enorme. Ora escono a file di granelli affiancati, distanti quanto il granello più grosso, e ogni fila nasce già avanti di quanto avrebbe fatto in tempo a scendere.
  • Con l'attrito del vetro che teneva fermi i granelli, una colonna di caffè restava appiccicata alla parete: il vetro ora li rallenta soltanto, non li tiene mai.
  • Il mucchio non si addormentava mai: i granelli in fondo tremavano a una dozzina di pixel al secondo restando al loro posto. Ora un granello si addormenta se per un attimo resta entro metà del suo raggio, quanto che sia la sua velocità; e prima di ogni passo quello che si sovrappone ancora si separa senza dare velocità.
  • Durante lo svuotamento alcuni granelli si addormentavano appoggiati su altri che stavano per cadere, e restavano sospesi a mezz'aria tenendo il fondo aperto fino al limite di sicurezza: mentre il fondo è aperto nessuno dorme.
  • Un mucchio di espresso, bello a cono mentre il getto lo alimentava, si scioglieva piatto in tre decimi di secondo: con le posizioni il carico del caffè sopra non arriva ai granelli sotto, che scivolano come sul ghiaccio, e né più attrito né più passate bastavano. Ora un granello lento coperto da altri si addormenta fino a tre volte prima, di più quanto più è fine: si muove solo la pelle del mucchio, come in un mucchio vero.
  • I chicchi interi si impilavano in colonne in equilibrio: arrivando nello stesso punto si posavano uno sull'altro e si addormentavano mentre cominciavano appena a cadere. Ora rimbalzano un poco quando urtano, escono dallo scivolo un po' sparpagliati e aspettano mezzo secondo prima di dormire.
  • La polvere dell'espresso arrivava sul mucchio troppo veloce e lo scavava: l'aria frena ogni granello in proporzione inversa al suo raggio, e la polvere scende morbida mentre le schegge cadono dritte.
  • Mentre la scheda crema scivolava, il suo bordo scuro passava sulle righe di testo come una cancellatura: ora viaggia solo il fondo crema, dietro le parole, e il bordo ricompare all'arrivo.
  • Disegnare 1500 granelli a ogni fotogramma costava troppo: quelli addormentati stanno in un'immagine a parte, rifatta solo quando uno di loro si sveglia o quando se ne sono addormentati abbastanza.

Librerie

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

Compressi con gzip, i tre file della demo pesano 20 KB; le librerie: GSAP 28 KB.

Collaudo

Provata il 7 ottobre 2026

  • Computer: Fatto
  • Telefono: Fatto
  • Movimento ridotto: Fatto
  • Safari su iPhone: Da fare

Licenza d’uso

Puoi usarla nei tuoi progetti e in quelli dei tuoi clienti. Non puoi rivenderla né ridistribuirla così com’è. GSAP resta sotto la sua licenza Standard. Licenza completa.

Tutti i componenti