Vai al contenuto
animorio
EN
Tutti i componenti

Il campo della carta

Moduli, SVG, 3D, Fisica, TestoGSAP, Solo CSS, CanvasImpegnativaPeso compresso: Demo 22 KBGSAP 28 KBGratisScarica lo zip

I campi della carta di un checkout con la carta disegnata sopra: ogni cifra scritta viene coniata nella plastica, si alza in rilievo nuda e poi si riempie d’argento o d’oro, sotto una lampadina che segue il cursore. Le prime cifre scelgono il circuito e il materiale, la luce rilegge il numero da destra come il controllo di Luhn e per il codice di sicurezza la carta si gira.

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 card field</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/hubot-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/martian-mono@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the card fields of a checkout, with the card they describe drawn above them. Every figure typed is struck
         into the plastic and stands up in relief, lit by a small lamp that follows the caret; the first figures
         choose the network and its material, a full number is read by the light from right to left (the Luhn
         check), and the card turns over for the security code. The networks and their marks are invented -->
    <form class="pay" action="#" novalidate data-pay data-autoclic>
      <div class="pay__card" aria-hidden="true" data-stage>
        <div class="card__shadow" data-shadow></div>
        <div class="card is-blank" data-net="blank" data-card>
          <div class="card__face card__face--front" data-face="front">
            <div class="card__mats" data-mats></div>
            <div class="card__lamp" data-lamp></div>
            <div class="card__glint" data-glint></div>
            <svg class="card__art" viewBox="0 0 428 270" data-art>
              <defs>
                <linearGradient id="cc-chip" x1="0" y1="0" x2="1" y2="1">
                  <stop offset="0" stop-color="#f6e2a8"/>
                  <stop offset="0.5" stop-color="#d5ad5c"/>
                  <stop offset="1" stop-color="#a87f37"/>
                </linearGradient>
                <!-- the relief: the alpha of the figures is a height map, lit by a point light (the lamp) -->
                <filter id="cc-emboss" filterUnits="userSpaceOnUse" x="0" y="96" width="428" height="174" color-interpolation-filters="sRGB">
                  <feGaussianBlur in="SourceAlpha" stdDeviation="0.95" result="bump"/>
                  <feDiffuseLighting in="bump" surfaceScale="2.6" diffuseConstant="1.05" lighting-color="#ffffff" result="shade">
                    <fePointLight x="80" y="20" z="220" data-light/>
                  </feDiffuseLighting>
                  <feSpecularLighting in="bump" surfaceScale="2.6" specularConstant="1" specularExponent="16" lighting-color="#ffffff" result="glint">
                    <fePointLight x="80" y="20" z="220" data-light/>
                  </feSpecularLighting>
                  <feComposite in="glint" in2="SourceAlpha" operator="in" result="glintIn"/>
                  <feComposite in="SourceGraphic" in2="shade" operator="arithmetic" k1="0.9" k2="0.42" k3="0" k4="0" result="lit"/>
                  <feComposite in="glintIn" in2="lit" operator="arithmetic" k1="0" k2="0.85" k3="1" k4="0" result="relief"/>
                  <feOffset in="bump" dx="-0.8" dy="1.3" result="off" data-cast/>
                  <feComponentTransfer in="off" result="shadow"><feFuncA type="linear" slope="0.55"/></feComponentTransfer>
                  <feMerge><feMergeNode in="shadow"/><feMergeNode in="relief"/></feMerge>
                </filter>
                <!-- the chip catches the same lamp -->
                <filter id="cc-metal" filterUnits="userSpaceOnUse" x="28" y="72" width="70" height="56" color-interpolation-filters="sRGB">
                  <feGaussianBlur in="SourceAlpha" stdDeviation="0.6" result="bump"/>
                  <feSpecularLighting in="bump" surfaceScale="1.6" specularConstant="0.55" specularExponent="22" lighting-color="#fff4d6" result="glint">
                    <fePointLight x="80" y="20" z="220" data-light/>
                  </feSpecularLighting>
                  <feComposite in="glint" in2="SourceAlpha" operator="in" result="glintIn"/>
                  <feComposite in="glintIn" in2="SourceGraphic" operator="arithmetic" k1="0" k2="0.45" k3="1" k4="0"/>
                </filter>
              </defs>

              <g class="chip" filter="url(#cc-metal)">
                <rect x="34" y="78" width="56" height="42" rx="8" fill="url(#cc-chip)"/>
                <path class="chip__lines" d="M34 92.5h15.5a5 5 0 0 1 5 5v0M34 105.5h15.5a5 5 0 0 0 5-5M90 92.5H74.5a5 5 0 0 0-5 5M90 105.5H74.5a5 5 0 0 1-5-5M54.5 78v9.5M69.5 78v9.5M54.5 120v-9.5M69.5 120v-9.5M54.5 87.5h15v23h-15z"/>
              </g>

              <g class="card__print" data-print>
                <text x="168" y="194">VALID</text>
                <text x="168" y="201.5">THRU</text>
              </g>
              <!-- the Atrium cards print their code on the front, above the number -->
              <text class="card__frontcode" x="394" y="134" text-anchor="end" data-frontcode></text>

              <g class="card__marks" data-marks></g>

              <g class="card__relief" filter="url(#cc-emboss)" data-relief>
                <g class="card__number" data-number></g>
                <g class="card__exp" data-exp></g>
                <g class="card__name" data-name></g>
              </g>
            </svg>
          </div>

          <div class="card__face card__face--back" data-face="back">
            <div class="card__mats" data-mats></div>
            <div class="card__lamp" data-lamp></div>
            <div class="card__glint" data-glint></div>
            <svg class="card__art" viewBox="0 0 428 270">
              <defs>
                <pattern id="cc-panel" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(35)">
                  <rect width="6" height="6" fill="#f1efe9"/>
                  <path d="M0 3h6" stroke="#dcd8ce" stroke-width="1.2"/>
                </pattern>
                <linearGradient id="cc-stripe" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0" stop-color="#2a2a2e"/>
                  <stop offset="0.5" stop-color="#0d0d0f"/>
                  <stop offset="1" stop-color="#1c1c20"/>
                </linearGradient>
              </defs>
              <rect x="0" y="30" width="428" height="48" fill="url(#cc-stripe)"/>
              <rect x="26" y="102" width="272" height="38" rx="3" fill="url(#cc-panel)"/>
              <rect class="card__codebox" x="304" y="102" width="74" height="38" rx="3" data-codebox/>
              <g class="card__code" transform="translate(22.4 0) skewX(-10)" data-code></g>
              <g class="card__small">
                <text x="26" y="184">This card remains the property of the issuer and is used under</text>
                <text x="26" y="196">the cardholder agreement. If you find it, please hand it in at</text>
                <text x="26" y="208">any bank, or cut it in half and throw it away.</text>
              </g>
              <g class="card__marks card__marks--back" data-marks></g>
            </svg>
          </div>
        </div>
      </div>

      <div class="pay__fields">
        <div class="field field--number" data-field="number">
          <input class="field__input field__input--mono" id="cc-number" name="cardnumber" type="text" inputmode="numeric" autocomplete="cc-number" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="1234 1234 1234 1234" aria-describedby="pay-note" data-input="number">
          <label class="field__label" for="cc-number">Card number</label>
          <svg class="field__tick" viewBox="0 0 20 20" aria-hidden="true" data-tick><path d="M4.6 10.4l3.6 3.6 7.2-8"/></svg>
          <span class="field__badge" aria-hidden="true" data-badge></span>
        </div>
        <div class="pay__row">
          <div class="field" data-field="exp">
            <input class="field__input field__input--mono" id="cc-exp" name="cc-exp" type="text" inputmode="numeric" autocomplete="cc-exp" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="MM / YY" aria-describedby="pay-note" data-input="exp">
            <label class="field__label" for="cc-exp">Expiry date</label>
          </div>
          <div class="field field--code" data-field="code">
            <input class="field__input field__input--mono" id="cc-csc" name="cvc" type="text" inputmode="numeric" autocomplete="cc-csc" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="3 digits" aria-describedby="pay-note" data-input="code">
            <label class="field__label" for="cc-csc">Security code</label>
            <span class="field__hint" aria-hidden="true" data-hint>
              <span class="hint__card" data-hint-card>
                <svg class="hint__side hint__side--back" viewBox="0 0 30 20"><rect x="0.75" y="0.75" width="28.5" height="18.5" rx="3"/><path class="hint__stripe" d="M0.75 4.5h28.5v3.6H0.75z"/><rect class="hint__box" x="18" y="11" width="8" height="4.4" rx="1"/></svg>
                <svg class="hint__side hint__side--front" viewBox="0 0 30 20"><rect x="0.75" y="0.75" width="28.5" height="18.5" rx="3"/><rect class="hint__box" x="19" y="4" width="7.5" height="4" rx="1"/><path class="hint__line" d="M4 13.5h18"/></svg>
              </span>
            </span>
          </div>
        </div>
        <div class="field" data-field="name">
          <input class="field__input" id="cc-name" name="ccname" type="text" autocomplete="cc-name" autocapitalize="words" autocorrect="off" spellcheck="false" enterkeyhint="done" maxlength="26" placeholder="As it is printed on the card" aria-describedby="pay-note" data-input="name">
          <label class="field__label" for="cc-name">Name on card</label>
        </div>
      </div>

      <p class="pay__note" id="pay-note" aria-live="polite" data-note>Encrypted and sent straight to your bank.</p>

      <button class="go" type="submit" data-go>
        <span class="go__bg" aria-hidden="true" data-go-bg></span>
        <span class="go__label" data-go-label>Pay</span>
        <span class="go__state" aria-hidden="true" data-wait><svg class="go__spin" viewBox="0 0 24 24" data-spin><circle cx="12" cy="12" r="8.5"/></svg><span>Authorizing</span></span>
        <span class="go__state" aria-hidden="true" data-done><svg class="go__tick" viewBox="0 0 24 24"><path d="M6.2 12.6l3.9 3.9 7.8-8.9"/></svg><span>Paid</span></span>
      </button>
      <span class="sr" aria-live="polite" data-said></span>
    </form>
  </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.

46 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 campo della carta» da Animorio e adattala al mio sito

Cosa fa

Su un fondo bianco, sopra i campi, una carta di plastica chiara ancora da stampare, col suo chip d’oro; sotto, quattro campi grigi alti 60 pixel con le etichette dentro (Card number, poi Expiry date e Security code affiancati, poi Name on card), la riga Encrypted and sent straight to your bank. con un lucchetto e il pulsante nero Pay $64.00. Toccando un campo l’etichetta sale, il bordo diventa nero con un alone e sulla carta una piccola lampada va a illuminare il punto dove arriverà la cifra. Già alla prima cifra il circuito si riconosce: una banda di luce attraversa la carta e dietro di lei compare il materiale (con 4 la grafite spazzolata di Orbit, con 51-55 o 2221-2720 il blu inchiostro a onde di Strata, con 34 o 37 il verderame a quadretti di Atrium, con 6011, 644-649 o 65 il granata perlato di Kite), il marchio si posa in basso a destra e nel campo la carta piccola del circuito si gira come una moneta. Ogni cifra viene coniata: cade come un punzone, la carta sobbalza appena attorno al punto colpito e la cifra resta in rilievo nel colore della plastica, poi l’argento o l’oro ci scorre dentro; scrivendo veloce, dietro il cursore resta una scia di cifre nude che si riempiono. La lampada segue il cursore, così il rilievo prende luce dal lato giusto e sul metallo spazzolato corre un riflesso verticale. Il numero si divide da solo nei gruppi del circuito (4-6-5 per Atrium) e quando è completo la luce lo rilegge da destra a sinistra, bassa e radente: una cifra sì e una no fa un saltino, quelle che il controllo di Luhn conta due volte. Se il numero torna, nel campo si disegna una spunta verde e le cifre fanno un’onda; se non torna, le cifre si abbassano nella plastica, il campo diventa rosso e trema e sotto si legge That card number doesn’t add up. Check for a typo. La data si scrive 08 / 29, con lo zero e la barra che arrivano da soli, e compare sulla carta accanto a VALID THRU; una data passata spegne la carta, che perde colore e riflessi, e la data si abbassa. Per il codice di sicurezza la carta si gira su una molla e mostra il retro con la banda magnetica e il riquadro della firma, dove il codice si stampa in corsivo; sulle carte Atrium il codice è di quattro cifre e sta davanti, sopra il numero, e la carta non si gira. Il nome si conia in maiuscolo e, se è lungo, si stringe. Premendo Pay la scritta sale e lascia Authorizing con una rotella, mentre la lampada passa avanti e indietro sulla carta; poi il pulsante diventa verde con la spunta e Paid, e le cifre rientrano nella plastica una dopo l’altra, tutte tranne le ultime quattro. Dopo qualche secondo il modulo si svuota.

Come funziona

Campi veri, che si formattano da soli

I quattro campi sono input veri con le loro etichette, autocomplete cc-number, cc-exp, cc-csc e cc-name (così il browser e i gestori delle password li riempiono) e inputmode numeric per la tastiera dei numeri. A ogni input il numero si ripulisce e si divide nei gruppi del circuito, e il cursore torna dopo lo stesso numero di cifre che aveva prima. Cancellando uno spazio non succederebbe niente, perché lo spazio tornerebbe subito: nel beforeinput si cancella invece la cifra prima. La data aggiunge lo zero a un mese di una cifra sola e la barra, e capisce anche 12/2029 incollato.

Il circuito dalle prime cifre

Le regole sono quelle vere (4, 51-55 e 2221-2720, 34 e 37, 6011, 644-649 e 65), i nomi e i marchi sono inventati. Ogni circuito ha la sua lunghezza, i suoi gruppi, la lunghezza e il posto del codice, l’argento o l’oro. I materiali sono livelli sovrapposti nelle due facce: il nuovo si scopre con una maschera CSS a gradiente che scorre in 0,7 secondi, e sul bordo della maschera passa una banda di luce in overlay che ne nasconde il taglio; la stessa banda cambia la lamina di ogni cifra che attraversa. I numeri di altri circuiti restano sulla carta bianca, da 16 a 19 cifre, e il controllo di Luhn vale anche per loro.

Il rilievo è un filtro SVG

Cifre e lettere sono testi SVG in un gruppo col filtro: l’alfa sfocato di un pixel è una mappa d’altezza, feDiffuseLighting e feSpecularLighting la illuminano con un fePointLight (la lampada), il colore della lamina si moltiplica per l’ombreggiatura e un feOffset dello stesso rilievo fa l’ombra, spostata dalla parte opposta alla lampada. Ogni cifra è un gruppo che lo script sposta, con dentro la lettera che si anima: entra con l’opacità (che è anche l’altezza del rilievo) e una scala da 1,3, prima nel colore della plastica e poi, in FOIL secondi, in quello della lamina. Il carattere è Martian Mono: per 19 cifre o per un nome lungo si stringe sull’asse della larghezza prima di rimpicciolire.

La carta e la lampada su molle

Sul ticker di GSAP girano a mano le molle della carta (le due rotazioni, la profondità e il giro) e della lampada (x, y e altezza), in passi di 4 millesimi col fotogramma limitato a 1/30 di secondo. Una cifra coniata dà alla carta una spinta attorno al punto colpito, tanto più forte quanto più il punto è lontano dal centro (STRIKE). La lampada va sulla casella della prossima cifra; la sua pozza sul materiale è un gradiente in soft-light, e sul metallo spazzolato una striscia verticale in screen, come fa davvero un metallo spazzolato. Per il codice la carta gira di 180 gradi sulla molla TURN, e la faccia che non si vede è nascosta anche dallo script nel momento in cui la carta è di taglio.

Luhn, errori, pagamento e anteprima

Un numero completo si rilegge in READ secondi: la lampada scende radente e va da destra a sinistra, e le cifre che il controllo di Luhn raddoppia fanno un saltino quando lei passa; il verdetto arriva con la luce sulla prima cifra. Gli errori si mostrano lasciando il campo, o subito quando non possono essere giusti (un mese 13, una carta scaduta), e se ne vanno appena il campo torna giusto; la riga sotto i campi è aria-live e i campi prendono aria-invalid. Pay controlla tutto, aspetta WAIT secondi e mostra Paid. Un clic da script (detail 0, l’anteprima della libreria) scrive senza prendere il focus, un passo per clic: il numero di prova, la data, il codice, il nome, il pagamento, poi un numero con un refuso e la sua correzione.

Valori da cambiare

NomeFileDi partenzaCosa cambia
AMOUNTscript.js'$64.00'Quanto addebita il pulsante.
STRIKEscript.js3.4Quanto sobbalza la carta quando una cifra viene coniata vicino al bordo, in gradi (0: ferma).
FOILscript.js0.4I secondi che l’argento o l’oro impiega a riempire una cifra appena coniata.
LAMPscript.js[70, 0.78]La molla della lampada: quanto è rigida e quanto è frenata (1 = senza rimbalzo).
TURNscript.js[95, 0.62]La molla che gira la carta per il codice di sicurezza.
READscript.js0.95I secondi in cui la luce rilegge il numero da destra a sinistra (il controllo di Luhn).
WAITscript.js1.5I secondi in cui la banca risponde, nella demo.
HOLDscript.js3.4I secondi in cui la carta pagata resta prima che il modulo si svuoti.
SAMPLEscript.js'4242 4242 4242 4242'Il numero di prova che l’anteprima della libreria scrive da sola.
HOLDERscript.js'Ines Okafor'Il nome che scrive l’anteprima.

Provali nel Playground

Quando usarla

Nella cassa di un negozio o di un abbonamento, nella pagina per aggiungere una carta a un conto, nella prenotazione di un albergo o di un volo: ovunque si scrive una carta e aiuta vederla mentre la si scrive. Per un negozio vero si mettono i circuiti e i loro marchi al posto di quelli inventati, e il pagamento vero al posto di quello finto (la funzione submit). I dati di una carta vera vanno mandati al servizio di pagamento, mai al proprio server; i servizi che danno i campi già pronti dentro un iframe non lasciano leggere le cifre, e lì la carta può seguire solo quello che quei campi raccontano (il circuito, completo o no, l’errore).

Sul telefono

Il modulo prende la larghezza dello schermo (358 pixel su un telefono di 390) e la carta resta in proporzione, 358 per 226. I campi restano alti 60 pixel, col testo a 16 pixel così iPhone non ingrandisce la pagina al tocco, e la tastiera è quella dei numeri. Toccando una parte della carta si va al suo campo; la lampada che segue il puntatore, sul telefono, non c’è: segue solo il cursore.

Con il movimento ridotto

Niente colpi, sobbalzi o molle: le cifre compaiono in dissolvenza già nel loro colore, il materiale cambia in dissolvenza, la carta passa dal davanti al retro con una dissolvenza invece di girarsi, la lampada va subito al suo posto, la lettura di Luhn dà subito il verdetto e i campi non tremano.

Trappole

  • Il fondo del pulsante, assoluto, era finito nella griglia con le scritte (grid-area 1 / 1 su tutti i figli) e prendeva solo la cella dentro il padding: la pillola nera era più corta dei campi e spostata a destra. La regola ora esclude il fondo.
  • Le cifre si spostano e si animano con GSAP: un transform-origin scritto nel CSS su un testo SVG si somma all’origine che GSAP calcola da sé. Ogni cifra è un gruppo che GSAP sposta, con dentro la lettera che scala, e niente transform-origin nel CSS; per lo stesso motivo il corsivo del codice sul retro sta su un gruppo che GSAP non tocca.
  • La misura dei caratteri scritta nel CSS sui testi vinceva su quella che lo script mette sul gruppo per stringere il numero: la misura sta sui gruppi, e i testi la ereditano.
  • Il colpo durava 50 millesimi e il rilievo che si alza non si vedeva: ora la cifra arriva nel colore della plastica e la lamina ci scorre dentro dopo, così si vede la cifra nuda e poi l’argento.
  • Un colore in gradiente non si anima: le lamine sono colori pieni (l’ombreggiatura del filtro fa il resto), animati fino al colore e poi lasciati sulla variabile del CSS, così un colore cambiato nel Playground arriva anche alle cifre già coniate.
  • Cancellando lo spazio fra due gruppi, o la barra della data, lo spazio tornava subito e non succedeva niente: nel beforeinput si cancella la cifra prima (e, in fondo alla data, anche la barra rimasta).
  • La grafite scolorita resta grigia come prima: una carta scaduta ora perde anche luce e riflessi, e la sua data si abbassa.
  • Il verdetto di Luhn arrivava solo quando tutti i saltini erano finiti, quasi due secondi dopo l’ultima cifra: ora arriva quando la luce raggiunge la prima.
  • Nell’anteprima il numero col refuso veniva cancellato dal clic dopo prima che arrivasse l’errore: ora il refuso viene dopo il pagamento e la sua correzione è il passo dopo, e il giro riprende dalla data.
  • Lontano dalla lampada le lettere diventavano troppo scure (una luce puntiforme cala con l’angolo): più luce diffusa nel filtro e la lampada un po’ più alta.
  • Con la lampada a riposo sopra il chip, il chip diventava bianco: riflesso più debole e più stretto.

Librerie

GSAP 3.15.0, Hubot Sans (font variabile) 5.3.0, Martian Mono (font variabile) 5.3.0.

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

Collaudo

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