Vai al contenuto
animorio
EN
Torna alla libreria

La radio d’epoca da sintonizzare

Hover, SVGGSAPImpegnativaPeso compresso: Demo 11 KBGSAP 28 KBGratisScarica lo zip

Nello stile ricco: il lettore di una web radio fatto come una radio a valvole degli anni Quaranta, in noce lucido. Il tasto On scalda le valvole piano, con un tremolio; i tasti di bachelite, la manopola zigrinata, la rotella o il dito sul vetro spostano la lancetta rossa sul quadrante illuminato. Fra una stazione e l’altra il vetro frigge e la luce trema; sulla stazione giusta l’occhio magico verde si chiude, il nome e il brano compaiono sul vetro e la tela dell’altoparlante batte a tempo.

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 vintage radio to tune</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <header class="top">
      <span class="brand" data-brand>Hearth</span>
      <span class="tag">Live radio, all night long</span>
    </header>

    <!-- the player of an invented web radio, drawn as a tabletop valve radio from the forties: a walnut cabinet with
         its grain, a woven speaker cloth behind wooden fretwork, a glass dial lit from behind with the stations and a
         red needle, a green magic eye, two knurled bakelite knobs and a row of bakelite keys. The grain, the knurling,
         the ticks and the static are drawn by the script. No sound: the cloth beats instead -->
    <section class="radio" aria-label="Vintage radio player" data-radio>
      <svg class="set" viewBox="0 0 600 430" data-svg>
        <defs>
          <linearGradient id="walnut" x1="0" y1="0" x2="0.35" y2="1">
            <stop offset="0" style="stop-color:var(--wood-light)"/>
            <stop offset="0.45" style="stop-color:var(--wood)"/>
            <stop offset="1" style="stop-color:var(--wood-dark)"/>
          </linearGradient>
          <linearGradient id="veneer" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0" style="stop-color:var(--wood)"/>
            <stop offset="0.5" style="stop-color:var(--wood-light)"/>
            <stop offset="1" style="stop-color:var(--wood)"/>
          </linearGradient>
          <linearGradient id="lacquer" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stop-color="#fff" stop-opacity="0.55"/>
            <stop offset="1" stop-color="#fff" stop-opacity="0"/>
          </linearGradient>
          <radialGradient id="valves" cx="0.5" cy="0.55" r="0.6">
            <stop offset="0" style="stop-color:var(--valve)" stop-opacity="1"/>
            <stop offset="0.55" style="stop-color:var(--valve)" stop-opacity="0.45"/>
            <stop offset="1" style="stop-color:var(--valve)" stop-opacity="0"/>
          </radialGradient>
          <linearGradient id="backlight" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--amber-light)"/>
            <stop offset="1" style="stop-color:var(--amber)"/>
          </linearGradient>
          <linearGradient id="glassShine" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0" stop-color="#fff" stop-opacity="0"/>
            <stop offset="0.45" stop-color="#fff" stop-opacity="0.32"/>
            <stop offset="0.55" stop-color="#fff" stop-opacity="0.05"/>
            <stop offset="1" stop-color="#fff" stop-opacity="0"/>
          </linearGradient>
          <radialGradient id="bakelite" cx="0.36" cy="0.3" r="0.8">
            <stop offset="0" style="stop-color:var(--bakelite-light)"/>
            <stop offset="0.6" style="stop-color:var(--bakelite)"/>
            <stop offset="1" style="stop-color:var(--bakelite-dark)"/>
          </radialGradient>
          <linearGradient id="keyFace" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--bakelite-light)"/>
            <stop offset="1" style="stop-color:var(--bakelite)"/>
          </linearGradient>
          <linearGradient id="brass" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stop-color="#f6dc93"/>
            <stop offset="0.5" style="stop-color:var(--brass)"/>
            <stop offset="1" stop-color="#8a6420"/>
          </linearGradient>
          <radialGradient id="eyeGlow" cx="0.5" cy="0.5" r="0.5">
            <stop offset="0" stop-color="#d9ffd9"/>
            <stop offset="0.55" style="stop-color:var(--eye)"/>
            <stop offset="1" stop-color="#1f6b38"/>
          </radialGradient>
          <pattern id="cloth" width="6" height="6" patternUnits="userSpaceOnUse">
            <rect width="6" height="6" style="fill:var(--cloth-dark)"/>
            <path d="M0 1.5h6M0 4.5h6" style="stroke:var(--cloth)" stroke-width="1.6"/>
            <path d="M1.5 0v6M4.5 0v6" style="stroke:var(--cloth-light)" stroke-width="1.1" stroke-opacity="0.85"/>
            <path d="M0 1.5h3M3 4.5h3" style="stroke:var(--cloth-light)" stroke-width="1.6" stroke-opacity="0.5"/>
          </pattern>
          <clipPath id="cabClip"><path data-cab-path/></clipPath>
          <clipPath id="frontClip"><rect x="46" y="46" width="508" height="328" rx="26"/></clipPath>
          <clipPath id="grilleClip"><rect x="72" y="78" width="214" height="214" rx="20"/></clipPath>
          <clipPath id="glassClip"><rect x="314" y="78" width="214" height="118" rx="14"/></clipPath>
        </defs>

        <ellipse class="shadow" cx="300" cy="418" rx="290" ry="12"/>
        <rect class="foot" x="78" y="396" width="56" height="16" rx="5"/>
        <rect class="foot" x="466" y="396" width="56" height="16" rx="5"/>

        <!-- the cabinet: walnut with its grain, a lacquer shine on the shoulders and a bevel -->
        <g data-light>
          <path class="cab" data-cab-path fill="url(#walnut)"/>
          <g clip-path="url(#cabClip)" data-grain></g>
          <path class="bevel" data-cab-path/>
          <path class="lacquer" fill="url(#lacquer)" data-shine/>
        </g>

        <!-- the front panel: book-matched veneer, a shade lighter -->
        <rect class="front" x="46" y="46" width="508" height="328" rx="26" fill="url(#veneer)"/>
        <g clip-path="url(#frontClip)" data-veneer></g>
        <rect class="front-edge" x="46" y="46" width="508" height="328" rx="26"/>

        <!-- the speaker: woven cloth, the valves' glow through it, the wooden fretwork over it -->
        <g clip-path="url(#grilleClip)">
          <rect x="72" y="78" width="214" height="214" style="fill:var(--cloth-dark)"/>
          <g data-cone>
            <rect x="62" y="68" width="234" height="234" fill="url(#cloth)"/>
            <circle class="cone" cx="179" cy="185" r="62" data-ring/>
          </g>
          <ellipse cx="179" cy="205" rx="130" ry="120" fill="url(#valves)" opacity="0" class="valve-glow" data-glow/>
          <rect x="72" y="78" width="214" height="214" class="cloth-shade"/>
        </g>
        <rect class="grille-rim" x="72" y="78" width="214" height="214" rx="20"/>
        <g class="fret" data-fret></g>

        <!-- the brass badge -->
        <g transform="translate(144 300)">
          <rect width="70" height="16" rx="3" fill="url(#brass)" stroke="#6d4c14" stroke-width="0.8"/>
          <text x="35" y="11.6" class="badge" data-badge>HEARTH</text>
        </g>

        <!-- the dial: glass lit from behind, the scale, the stations, the needle, the static -->
        <rect x="310" y="74" width="222" height="126" rx="17" class="dial-rim"/>
        <g clip-path="url(#glassClip)">
          <rect x="314" y="78" width="214" height="118" style="fill:var(--glass-off)"/>
          <rect x="314" y="78" width="214" height="118" fill="url(#backlight)" opacity="0" data-back/>
          <g data-scale></g>
          <g data-stations></g>
          <text class="now-station" x="421" y="176" data-now-station></text>
          <text class="now-song" x="421" y="189" data-now-song></text>
          <g data-noise></g>
          <g data-needle>
            <line class="needle" x1="0" y1="84" x2="0" y2="164"/>
            <rect class="needle-foot" x="-3" y="160" width="6" height="5" rx="1"/>
          </g>
          <rect x="314" y="78" width="214" height="118" fill="url(#glassShine)" data-glass/>
        </g>
        <rect x="314" y="78" width="214" height="118" rx="14" class="glass-edge"/>

        <!-- the magic eye -->
        <g transform="translate(421 232)">
          <circle r="15" class="eye-rim"/>
          <circle r="11.5" fill="#183020" data-eye-dark/>
          <circle r="11.5" fill="url(#eyeGlow)" opacity="0" data-eye-glow/>
          <path fill="#0f2416" data-eye-wedge/>
          <circle r="3.4" fill="#0c1a10"/>
          <circle r="11.5" class="eye-glass"/>
        </g>

        <!-- the knobs: volume and tuning -->
        <g class="knob" transform="translate(352 254)" role="slider" tabindex="0" aria-label="Volume" aria-valuemin="0" aria-valuemax="100" data-knob="vol">
          <ellipse cx="2" cy="6" rx="27" ry="25" class="knob-shadow"/>
          <g data-turn><circle r="26" fill="url(#bakelite)"/><g data-knurl></g><circle r="17" class="knob-cap"/><circle r="11" class="knob-ring"/><line class="knob-mark" x1="0" y1="-24" x2="0" y2="-14"/></g>
          <ellipse cx="-8" cy="-9" rx="9" ry="5" class="knob-shine"/>
        </g>
        <g class="knob" transform="translate(490 254)" role="slider" tabindex="0" aria-label="Tuning" aria-valuemin="88" aria-valuemax="108" data-knob="tune">
          <ellipse cx="2" cy="6" rx="27" ry="25" class="knob-shadow"/>
          <g data-turn><circle r="26" fill="url(#bakelite)"/><g data-knurl></g><circle r="17" class="knob-cap"/><circle r="11" class="knob-ring"/><line class="knob-mark" x1="0" y1="-24" x2="0" y2="-14"/></g>
          <ellipse cx="-8" cy="-9" rx="9" ry="5" class="knob-shine"/>
        </g>
        <text class="small" x="352" y="296">VOLUME</text>
        <text class="small" x="490" y="296">TUNING</text>

        <!-- the bakelite keys: power and five stations -->
        <g data-keys></g>
      </svg>
    </section>

    <p class="onair"><span class="onair__label">On air</span> <span data-caption>Press On, then a station</span></p>
    <p class="sr" aria-live="polite" data-live></p>
  </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.

50 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 «La radio d’epoca da sintonizzare» da Animorio e adattala al mio sito

Cosa fa

Su una parete verde petrolio scaldata da una lampada c’è una radio da tavolo in noce lucido, con le venature e la lacca che riflette la luce. A sinistra la tela intrecciata dell’altoparlante dietro tre listelli di legno e una targhetta d’ottone; a destra il quadrante di vetro con la scala da 88 a 108 MHz, cinque stazioni (Jazz, News, Classical, Lo-fi, Folk) e la lancetta rossa; sotto l’occhio magico verde, le manopole del volume e della sintonia, zigrinate, e una fila di sei tasti di bachelite. Il tasto On scalda le valvole: un tremolio, poi il vetro e la tela si accendono d’ambra in due secondi e mezzo. Un tasto di stazione resta giù e la lancetta corre fin lì; la manopola della sintonia si gira trascinandola (un giro e mezzo per tutto il quadrante), la rotella o il dito sul vetro spostano la lancetta direttamente. Fra due stazioni il vetro si riempie di graffi bianchi, la luce trema e la lancetta vibra; vicino a una stazione il disturbo sparisce, l’occhio magico chiude il suo spicchio scuro, il nome e il brano compaiono sul vetro e la tela dell’altoparlante batte al tempo della stazione (le notizie a colpi irregolari, come una voce). Il volume cambia quanto batte. Da sola si accende, prova le stazioni, gira la manopola a mano e si spegne.

Come funziona

Tre numeri

Tutto viene da tre valori: l’accensione (da 0 a 1), il posto della lancetta sul quadrante (da 0 a 1, da 88 a 108 MHz) e il volume. GSAP li anima e un solo ciclo del ticker disegna il resto a ogni fotogramma.

Il segnale e il disturbo

Ogni stazione è una campana attorno al suo posto: il segnale è la campana più alta sotto la lancetta, il disturbo è uno meno il segnale. Il disturbo decide quanti graffi bianchi compaiono sul vetro, quanto trema la luce e quanto vibra la lancetta; il segnale chiude lo spicchio dell’occhio magico e accende il nome sul vetro.

Le valvole che si scaldano

Il tasto On non accende di colpo: una timeline fa due o tre tremolii brevi dell’accensione e poi la sale piano fino a 1 in due secondi e mezzo. Luce del quadrante, bagliore dietro la tela e occhio magico seguono lo stesso numero.

La tela che batte

Sulla stazione la tela dell’altoparlante si gonfia a ogni battito: un esponenziale che parte a ogni colpo, al tempo della stazione, moltiplicato per il segnale e per il volume. Le notizie sono parlato: un prodotto di sinusoidi lente che dà colpi irregolari.

Le mani

La manopola si gira trascinando: l’angolo del puntatore attorno al suo centro, misurato nelle coordinate dell’SVG, sposta la lancetta di un giro e mezzo per tutto il quadrante. Il dito sul vetro porta la lancetta dove tocca, la rotella la sposta poco alla volta, le frecce della tastiera di un decimo di MHz.

Valori da cambiare

NomeFileDi partenzaCosa cambia
STATIONSscript.js[['Jazz', 'Blue Hour Session', 92], …]Le stazioni sul quadrante: nome, cosa suona e battiti al minuto (0 per il parlato).
BRANDscript.js'Hearth'Il nome della radio, nella testata e sulla targhetta.
WARMscript.js2.4I secondi che le valvole impiegano a scaldarsi.
SWEEPscript.js1.1I secondi che la lancetta impiega ad arrivare a una stazione da un tasto.
STATICscript.js1Quanto frigge il quadrante fra le stazioni.
BEATscript.js1Quanto batte la tela dell’altoparlante su una stazione.
LIGHTscript.js1Quanto il riflesso sulla lacca segue il puntatore.

Provali nel Playground

Quando usarla

Per il lettore di una web radio o di un podcast, il sito di un negozio di modernariato o di un bar con la musica dal vivo, una pagina «ascolta» con poche stazioni o playlist, un caffè con un’atmosfera d’altri tempi.

Sul telefono

La radio si stringe con lo schermo ed è alta al massimo quanto lascia la finestra; tutto è un SVG solo, quindi i dettagli restano gli stessi. Col dito si preme un tasto, si gira la manopola o si scorre sul vetro.

Con il movimento ridotto

Niente tremolii né graffi sul vetro e la tela non batte: l’accensione è una dissolvenza e la lancetta salta sulla stazione; l’occhio magico e il nome sul vetro cambiano lo stesso.

Trappole

  • L’angolo della manopola va misurato nelle coordinate dell’SVG (getScreenCTM), non della pagina: la radio è scalata e il centro della manopola si sposta con la misura dello schermo.
  • Fra un fotogramma e l’altro l’angolo del puntatore può saltare da più a meno 180 gradi: la differenza va riportata fra meno e più pi, se no la lancetta fa un balzo.
  • I colori dei gradienti dentro l’SVG vanno scritti come style="stop-color:var(--…)" e non come attributo: così la Palette del Playground li ricolora.

Librerie

GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

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

Collaudo

Provata il 22 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.

Tutte le animazioni