Vai al contenuto
animorio
EN
Tutti i componenti

La lampadina che si abbina

Caricamento, SVG, FisicaSolo CSS, GSAPImpegnativaPeso compresso: Demo 14 KBGSAP 28 KBGratisScarica lo zip

L’abbinamento di una lampadina smart nell’app della casa, e la barra di avanzamento è la lampadina stessa, vista di fianco: mentre il collegamento avanza il filamento di tungsteno si scalda passando per i colori veri di un corpo caldo, dal rosso scuro all’arancio al bianco caldo, il vetro si riempie di luce e la spirale trema un poco sui suoi fili. Se il collegamento cade il filamento sfarfalla e si raffredda; a lampadina abbinata si accende piena, con un lampo, e la sua luce si allarga sul pannello.

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, viewport-fit=cover">
  <title>The pairing bulb</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the pairing bulb: a smart home app pairing a new filament bulb, and the bulb is the progress. Seen from the side,
         its tungsten coil warms through the real colours of a hot body as the pairing goes on, from a dull red to
         orange to a warm white, while the glass fills with light and the coil trembles on its wires. If the link
         drops, the coil flickers and cools; once paired, the bulb comes on in full and its light spreads over the
         panel. The coil, the halo and the light are drawn by script.js -->
    <section class="pair" aria-labelledby="pair-title" data-pair>
      <div class="pair__light" aria-hidden="true" data-light></div>
      <header class="pair__head">
        <div class="pair__words">
          <h2 class="pair__title" id="pair-title"><span class="pair__swap" data-title>Living room lamp</span></h2>
          <p class="pair__meta"><span class="pair__swap" data-meta>New bulb nearby · Not paired</span></p>
        </div>
        <p class="pair__kelvin" aria-hidden="true"><span data-kelvin>Off</span></p>
      </header>

      <div class="pair__bulb" role="progressbar" aria-labelledby="pair-title" aria-valuemin="0" aria-valuemax="100"
           aria-valuenow="0" aria-valuetext="Not paired" data-bar>
        <svg class="pair__art" viewBox="10 30 180 232" aria-hidden="true" data-art>
          <defs>
            <clipPath id="pb-glass-clip"><path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188Z"/></clipPath>
            <radialGradient id="pb-halo-wide" gradientUnits="userSpaceOnUse" cx="100" cy="96" r="84">
              <stop offset="0" stop-color="#ffb070" stop-opacity="0" data-stop="wide0"/>
              <stop offset="0.38" stop-color="#ffb070" stop-opacity="0" data-stop="wide1"/>
              <stop offset="1" stop-color="#ffb070" stop-opacity="0" data-stop="wide2"/>
            </radialGradient>
            <radialGradient id="pb-halo-core" gradientUnits="userSpaceOnUse" cx="100" cy="96" r="26">
              <stop offset="0" stop-color="#fff4e6" stop-opacity="0" data-stop="core0"/>
              <stop offset="1" stop-color="#fff4e6" stop-opacity="0" data-stop="core1"/>
            </radialGradient>
            <radialGradient id="pb-halo-mid" gradientUnits="userSpaceOnUse" cx="100" cy="96" r="36">
              <stop offset="0" stop-color="#ffb070" stop-opacity="0" data-stop="mid0"/>
              <stop offset="1" stop-color="#ffb070" stop-opacity="0" data-stop="mid1"/>
            </radialGradient>
            <linearGradient id="pb-glass-edge" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" stop-color="#ffffff" stop-opacity="0.34"/>
              <stop offset="0.5" stop-color="#ffffff" stop-opacity="0.08"/>
              <stop offset="1" stop-color="#ffffff" stop-opacity="0.22"/>
            </linearGradient>
            <linearGradient id="pb-glass-fill" x1="0" y1="0" x2="1" y2="1">
              <stop offset="0" stop-color="#ffffff" stop-opacity="0.07"/>
              <stop offset="0.55" stop-color="#ffffff" stop-opacity="0.015"/>
              <stop offset="1" stop-color="#ffffff" stop-opacity="0.05"/>
            </linearGradient>
            <linearGradient id="pb-metal" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" style="stop-color: var(--metal-lo)"/>
              <stop offset="0.2" style="stop-color: var(--metal-mid)"/>
              <stop offset="0.36" style="stop-color: var(--metal-hi)"/>
              <stop offset="0.62" style="stop-color: var(--metal-mid)"/>
              <stop offset="1" style="stop-color: var(--metal-lo)"/>
            </linearGradient>
            <linearGradient id="pb-stem" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stop-color="#ffffff" stop-opacity="0.2"/>
              <stop offset="1" stop-color="#ffffff" stop-opacity="0.05"/>
            </linearGradient>
            <linearGradient id="pb-base-lit" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stop-color="#ffb070" stop-opacity="0.55" data-stop="base0"/>
              <stop offset="1" stop-color="#ffb070" stop-opacity="0" data-stop="base1"/>
            </linearGradient>
            <filter id="pb-bloom" x="-60%" y="-160%" width="220%" height="420%" color-interpolation-filters="sRGB">
              <feGaussianBlur in="SourceGraphic" stdDeviation="1.4" result="near" data-blur="near"/>
              <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="far" data-blur="far"/>
              <feMerge><feMergeNode in="far"/><feMergeNode in="far"/><feMergeNode in="near"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>
          </defs>

          <!-- the screw base: a collar, four threads, the black insulator and the contact -->
          <g class="pair__base">
            <path d="M74 186h52v8c0 3-2 5-5 5H79c-3 0-5-2-5-5z" fill="url(#pb-metal)"/>
            <rect x="78" y="198" width="44" height="40" style="fill: var(--metal-lo)"/>
            <g data-threads>
              <rect x="73" y="199" width="54" height="9" rx="4.5" fill="url(#pb-metal)" transform="rotate(-3 100 203.5)"/>
              <rect x="74" y="209" width="52" height="9" rx="4.5" fill="url(#pb-metal)" transform="rotate(-3 100 213.5)"/>
              <rect x="75" y="219" width="50" height="9" rx="4.5" fill="url(#pb-metal)" transform="rotate(-3 100 223.5)"/>
              <rect x="77" y="229" width="46" height="8" rx="4" fill="url(#pb-metal)" transform="rotate(-3 100 233)"/>
            </g>
            <path d="M80 238h40l-4 9H84z" style="fill: var(--insulator)"/>
            <path d="M90 247h20c0 5-4 8-10 8s-10-3-10-8z" fill="url(#pb-metal)"/>
            <path d="M74 186h52v14H74z" fill="url(#pb-base-lit)" style="mix-blend-mode: screen" data-base-lit/>
          </g>

          <!-- the glass and everything inside it -->
          <path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188Z" fill="url(#pb-glass-fill)"/>
          <g clip-path="url(#pb-glass-clip)">
            <rect x="0" y="0" width="200" height="200" fill="url(#pb-halo-wide)"/>
            <rect x="0" y="0" width="200" height="200" fill="url(#pb-halo-mid)"/>
            <rect x="0" y="0" width="200" height="200" fill="url(#pb-halo-core)"/>
          </g>
          <!-- the stem: a glass tube flaring into the neck, with the press that holds the wires -->
          <path d="M84 192C88 184 90 168 91 150Q100 145 109 150C110 168 112 184 116 192Z" fill="url(#pb-stem)"/>
          <path d="M98 150V120" style="stroke: var(--stem)" stroke-width="2.6" stroke-linecap="round" opacity="0.55"/>
          <g class="pair__wires" fill="none" stroke-linecap="round" stroke-linejoin="round" data-wires>
            <path d="M94 156L94 140L74 100L74 93" data-wire="0"/>
            <path d="M106 156L106 140L126 100L126 93" data-wire="1"/>
            <path d="M98 121L100 104" data-wire="2"/>
          </g>
          <g fill="none" stroke-linecap="round" stroke-linejoin="round">
            <path class="pair__coil pair__coil--back" data-coil="back"/>
            <g filter="url(#pb-bloom)" data-glow>
              <path class="pair__coil pair__coil--glow" data-coil="glow"/>
            </g>
            <path class="pair__coil pair__coil--front" data-coil="front"/>
            <path class="pair__coil pair__coil--core" data-coil="core"/>
          </g>
          <!-- the glass's own edge and its two highlights, over everything -->
          <path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188" fill="none" stroke="url(#pb-glass-edge)" stroke-width="1.2"/>
          <path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188" fill="none" stroke="#ffb070" stroke-width="1.3" opacity="0" data-rim/>
          <path d="M50 78C56 60 70 48 86 43" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linecap="round" opacity="0.26"/>
          <path d="M47 96C47 92 47.5 89 48.5 86" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round" opacity="0.2"/>
          <path d="M150 122C146 132 140 140 133 147" fill="none" stroke="#ffffff" stroke-width="1.6" stroke-linecap="round" opacity="0.12"/>
        </svg>
      </div>

      <footer class="pair__foot">
        <div class="pair__readout" aria-hidden="true">
          <p class="pair__pct" data-pct-row>
            <svg class="pair__tick" viewBox="0 0 24 24" data-tick><path d="M5 12.8l4.2 4.2L19 7.2" pathLength="1"/></svg>
            <span class="pair__swap" data-pct>Smart bulb</span>
          </p>
          <p class="pair__sub"><span class="pair__swap" data-sub>E27 · 806 lm · dimmable</span></p>
        </div>
        <button class="pair__btn" type="button" data-action data-autoclic><span class="pair__label" data-label>Pair</span></button>
      </footer>
      <p class="sr" role="status" aria-live="polite" data-live></p>
    </section>
  </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.

31 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 lampadina che si abbina» da Animorio e adattala al mio sito

Cosa fa

Su un fondo nero, da solo al centro, un pannello scuro con gli angoli tondi: in alto Living room lamp e New bulb nearby · Not paired, a destra una piccola etichetta con la temperatura del filamento (Off); in mezzo una lampadina di vetro chiaro vista di fianco, con l’attacco a vite di metallo, il gambo di vetro, due fili e una spirale di tungsteno grigia appesa fra loro; sotto, Smart bulb con E27 · 806 lm · dimmable e il pulsante chiaro Pair. Premuto, il pulsante diventa Cancel, chiaro e col bordo, e l’app cerca la lampadina (Searching for the bulb…): a ogni ricerca la spirale arrossisce appena e si spegne. Quando la lampadina risponde (Connecting to Home Wi-Fi…) la spirale prende un rosso scuro che resta, e da lì si scalda con l’avanzamento: durante l’aggiornamento del firmware (Updating firmware 4.2.1, poi About 3 s left) passa dal rosso all’arancio e verso il bianco caldo, il vetro si riempie di un alone del suo colore, la luce comincia a cadere sul pannello intorno, l’etichetta in alto dice la temperatura in kelvin e la spirale trema un poco, con un piccolo sussulto a ogni pezzo di firmware che arriva; in basso sale la percentuale. Alla fine la lampadina si accende piena con un lampo breve, il filamento diventa quasi bianco, la luce si allarga su tutto il pannello, si disegna una spunta accanto a Paired, la riga dice Connected · Firmware 4.2.1 e sotto 2,700 K · Full brightness; Pair again spegne la lampadina, la lascia raffreddare e ricomincia. Una volta su due, nella demo, a due terzi il collegamento cade: la spirale sfarfalla qualche volta come una lampadina con un contatto lento, si spegne e si raffredda dal giallo al rosso al grigio, la riga dice in rosso Lost the connection to the bulb e il pulsante diventa Retry. Retry ridà corrente, la percentuale torna all’inizio dell’aggiornamento e il firmware ricomincia.

Come funziona

Il colore di un corpo caldo, calcolato

Il colore del filamento non è una sfumatura scelta a mano. Per ogni temperatura lo script somma la legge di Planck, B(λ, T) = 1 ÷ (λ⁵ (e^(c₂ ÷ λT) − 1)), su tutto lo spettro visibile a passi di 5 nanometri, pesata con le tre curve dell’osservatore CIE 1931 (l’approssimazione analitica di Wyman, Sloan e Shirley: qualche gaussiana storta per curva). Ne escono X, Y e Z, che una matrice porta in sRGB lineare; il canale più forte si porta a 1 e si applica la curva gamma. Così a 900 K viene un rosso cupo, a 1500 un arancio, a 2700 il bianco caldo di una lampadina, senza averli scritti da nessuna parte. Il calcolo si fa una volta all’avvio, una tabella ogni 10 kelvin.

La luminosità in scala logaritmica

La luminanza Y di un corpo caldo cresce in modo enorme con la temperatura: a 1000 K è un milionesimo di quella a 2700. Lo script ne prende il logaritmo fra il primo rosso visibile (circa 700 K) e il colore della lampadina, come fa un occhio che si abitua: questa intensità decide quanto è acceso il filamento, quanto il suo cuore va verso il bianco, la forza del bagliore, dei tre aloni nel vetro e della luce sul pannello. La temperatura vera insegue quella voluta con un ritardo del primo ordine, più svelto a scaldare che a raffreddare, così un filamento spento sfuma dal giallo al rosso al grigio.

Una spirale vista di sbieco, ridisegnata a ogni fotogramma

La spirale è un’elica lungo una catenaria fra i due fili: per ogni punto θ = 2πNt, l’altezza è il raggio per sin θ e la profondità cos θ, che guardata di sbieco sposta il punto di lato. I punti con la profondità positiva vanno nel tracciato davanti, gli altri in quello dietro (più scuro e sottile), e ogni tratto ricomincia dall’ultimo punto dell’altro, così le spire si chiudono senza buchi. Sopra c’è una copia di tutta la spirale con un filtro SVG di bagliore (due feGaussianBlur fusi con feMerge, che si allargano con l’intensità) e un’anima sottile che diventa bianca quando la lampadina è piena. Scaldandosi il tungsteno si allunga e la spirale si incurva un po’ di più; il filo di sostegno in mezzo la segue.

Il tremito

La spirale trema come una corda tesa fra due punti: due modi su e giù (mezza onda e onda intera, a 6 e 11,5 vibrazioni al secondo) e uno di lato, ognuno una molla smorzata calcolata a passi di 1/480 di secondo, spinta da un rumore che cresce col lavoro. Ogni pezzo di firmware che arriva dà un colpetto e un piccolo aumento di calore; quando il collegamento cade, ogni sfarfallio della corrente scuote la spirale.

Gli aloni e la luce sul pannello

Dentro il vetro, ritagliati sulla sua forma, ci sono tre gradienti radiali che si colorano a ogni fotogramma: uno largo che riempie il bulbo, uno medio intorno alla spirale e un cuore quasi bianco che si accende solo vicino alla piena potenza; il bordo del vetro e la parte alta dell’attacco prendono il colore della luce. Fuori, dietro la lampadina, due gradienti radiali sul pannello: uno stretto intorno al vetro e uno largo che, a lampadina accesa, si allarga su tutto il pannello con una curva expo.out mentre un lampo breve porta il filamento a circa 3200 K e lo lascia tornare a 2700.

Per chi non vede e per chi non vuole movimento

La lampadina è un role=progressbar con aria-valuenow e un aria-valuetext che dice la fase, il pannello ha aria-busy mentre lavora e una riga aria-live dice quando la lampadina viene trovata, si collega, perde il collegamento ed è abbinata. Il pulsante cambia parola (Pair, Cancel, Retry, Pair again) e la pillola prende la larghezza nuova con un piccolo rimbalzo; un clic da script, quello dell’anteprima della libreria, non annulla mai.

Valori da cambiare

NomeFileDi partenzaCosa cambia
SECONDSscript.js7Quanto dura un abbinamento nella demo, in secondi.
KELVINscript.js2700La temperatura di colore della lampadina accesa, in kelvin (2700 è il bianco caldo di una lampadina a filamento).
WARMESTscript.js2250Quanto si scalda il filamento poco prima della fine, in kelvin.
TURNSscript.js15Quante spire ha la spirale di tungsteno.
TREMBLEscript.js1Quanto trema la spirale sui suoi fili mentre lavora: 0 per niente.
FLASHscript.js1Il lampo quando la lampadina si accende piena: 0 niente, 1 ben visibile.
FAIL_EVERYscript.js2Nella demo, un abbinamento su quanti perde il collegamento (0 mai).
FAIL_ATscript.js0.66Fin dove arriva l’abbinamento che fallisce, da 0 a 1 (sempre durante l’aggiornamento del firmware).
FIRMWAREscript.js'4.2.1'La versione del firmware che riceve la lampadina.
NETWORKscript.js'Home Wi-Fi'La rete a cui si collega la lampadina.

Provali nel Playground

Quando usarla

Per l’abbinamento o la configurazione di un dispositivo della casa (una lampadina, una presa, un termostato), l’aggiornamento del firmware di un oggetto, o qualunque attesa di qualche secondo in un prodotto dal tono caldo, dove l’attesa si può raccontare come «la stiamo accendendo». Il colore che si scalda dice a che punto si è anche senza leggere la percentuale.

Sul telefono

Il pannello prende la larghezza dello schermo meno 32 pixel (358 a 390) e resta alto 349; la lampadina è vettoriale e resta nitida a ogni densità, e la luce sul pannello si ricentra sulla lampadina quando la finestra cambia misura. Il pulsante è una pillola alta 38: col dito si schiaccia e rimbalza, e la scritta si allarga sull’asse della larghezza solo sotto il mouse.

Con il movimento ridotto

La spirale non trema e non sfarfalla, niente lampo: quando il collegamento cade la corrente si spegne in dissolvenza e il filamento si raffredda; a lampadina accesa la luce sul pannello compare in dissolvenza invece di allargarsi. Il filamento si scalda lo stesso con l’avanzamento, perché il suo colore è l’indicatore; le parole nuove arrivano in dissolvenza.

Trappole

  • Con la luminosità presa così com’è dalla legge di Planck il filamento restava nero fino a quasi 2000 K e poi esplodeva: la luminanza a 1000 K è un milionesimo di quella a 2700. Ora l’intensità è il logaritmo della luminanza fra 700 K e il colore finale, e il rosso scuro dell’inizio si vede.
  • Il bordo del vetro colorato pieno col colore della luce trasformava la lampadina in un disegno a filo: ora c’è un secondo bordo, colorato, che si accende solo in parte sopra quello bianco.
  • Il gradiente radiale della luce sul pannello, con tre sole fermate, lasciava un anello visibile dove finiva: con cinque fermate che calano come un’esponenziale il bordo sparisce.
  • La riga del tempo rimasto cambiava ogni pochi decimi di secondo con la dissolvenza sfocata, e non si leggeva mai: ora scivola solo quando cambia il tipo di riga, un numero nuovo nella stessa riga cambia e basta.
  • Le variabili CSS scritte negli attributi stop-color e fill dell’SVG non valgono: i colori del metallo e dell’isolante vanno nello style.

Librerie

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

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

Collaudo

Provata il 8 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