Vai al contenuto
animorio
EN
Torna alla libreria

La foto in camera oscura

Immagini, SVGGSAPImpegnativaPeso compresso: Demo 13 KBGSAP 28 KBGratisScarica lo zip

Una camera oscura alla luce rossa: la pinza immerge un foglio bianco nello sviluppo, l’immagine affiora piano dalle ombre alle luci, poi passa nel fissaggio e finisce appesa al filo con una molletta, dove dondola e gocciola. Tre foto disegnate in SVG, un clic sviluppa la successiva.

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 darkroom print</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/oswald@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="scena" aria-label="A darkroom: prints come up in the developer and are hung up to dry. Click to develop the next one.">
    <!-- the room: the script places every piece for a wide or a tall screen and builds the trays, the pins and the prints -->
    <svg class="palco" data-stage xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Lowlight print lab">
      <defs>
        <radialGradient id="glow" cx="0.5" cy="0.5" r="0.5">
          <stop offset="0" stop-color="var(--glow)" stop-opacity="0.55" />
          <stop offset="0.45" stop-color="var(--glow)" stop-opacity="0.16" />
          <stop offset="1" stop-color="var(--glow)" stop-opacity="0" />
        </radialGradient>
        <linearGradient id="bench-shade" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stop-color="var(--bench-top)" />
          <stop offset="0.5" stop-color="var(--bench)" />
          <stop offset="1" stop-color="var(--room)" />
        </linearGradient>
        <linearGradient id="slosh" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stop-color="var(--shine)" stop-opacity="0" />
          <stop offset="0.5" stop-color="var(--shine)" stop-opacity="0.22" />
          <stop offset="1" stop-color="var(--shine)" stop-opacity="0" />
        </linearGradient>
        <linearGradient id="wet" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0.2" stop-color="var(--shine)" stop-opacity="0" />
          <stop offset="0.42" stop-color="var(--shine)" stop-opacity="0.5" />
          <stop offset="0.5" stop-color="var(--shine)" stop-opacity="0.12" />
          <stop offset="0.62" stop-color="var(--shine)" stop-opacity="0.32" />
          <stop offset="0.8" stop-color="var(--shine)" stop-opacity="0" />
        </linearGradient>
        <filter id="soft" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="9" /></filter>
        <filter id="soft-small" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3" /></filter>

        <!-- the three pictures, drawn in greys (272 x 172): the developer brings them up from the shadows to the lights -->
        <symbol id="pic-lake" viewBox="0 0 272 172">
          <linearGradient id="lake-sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c4c4c4" /><stop offset="1" stop-color="#f4f4f4" /></linearGradient>
          <linearGradient id="lake-water" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d6d6d6" /><stop offset="1" stop-color="#7a7a7a" /></linearGradient>
          <rect width="272" height="112" fill="url(#lake-sky)" />
          <circle cx="182" cy="72" r="30" fill="#fbfbfb" opacity="0.7" />
          <circle cx="182" cy="72" r="15" fill="#ffffff" />
          <path d="M0 96 L34 72 L58 84 L102 50 L132 74 L150 66 L192 86 L226 64 L272 82 V112 H0Z" fill="#a2a2a2" />
          <path d="M102 50 L112 60 L106 59 L98 66 L93 58Z M226 64 L236 72 L229 71 L221 76Z" fill="#dcdcdc" />
          <path d="M0 104 Q50 88 106 102 T200 98 T272 100 V114 H0Z" fill="#646464" />
          <rect y="110" width="272" height="62" fill="url(#lake-water)" />
          <path d="M150 116 H214 M160 122 H204 M168 128 H196 M174 134 H190 M178 140 H186" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round" />
          <path d="M96 136 Q110 142 128 136 L124 132 H100Z M111 132 V116 L123 130Z" fill="#202020" />
          <path d="M0 172 V118 L14 120 L30 132 L52 150 L78 172Z" fill="#161616" />
          <path d="M18 128 L26 74 L34 128Z M8 132 L14 92 L20 132Z M36 136 L44 98 L52 136Z M22 104 L26 86 L30 104Z" fill="#0c0c0c" />
          <path d="M272 172 V112 L252 116 L236 140 L222 172Z" fill="#141414" />
          <path d="M246 124 L254 70 L262 124Z M258 120 L264 90 L270 120Z" fill="#0c0c0c" />
        </symbol>
        <symbol id="pic-still" viewBox="0 0 272 172">
          <linearGradient id="still-wall" x1="0" y1="0" x2="1" y2="0.3"><stop offset="0" stop-color="#7a7a7a" /><stop offset="1" stop-color="#2c2c2c" /></linearGradient>
          <linearGradient id="still-pear" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#f2f2f2" /><stop offset="0.55" stop-color="#b8b8b8" /><stop offset="1" stop-color="#4a4a4a" /></linearGradient>
          <linearGradient id="still-cup" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#ffffff" /><stop offset="0.6" stop-color="#d8d8d8" /><stop offset="1" stop-color="#7c7c7c" /></linearGradient>
          <rect width="272" height="124" fill="url(#still-wall)" />
          <rect y="120" width="272" height="52" fill="#a8a8a8" />
          <rect y="118" width="272" height="4" fill="#e6e6e6" />
          <ellipse cx="102" cy="140" rx="36" ry="6" fill="#6a6a6a" />
          <ellipse cx="176" cy="142" rx="34" ry="6" fill="#6a6a6a" />
          <ellipse cx="236" cy="138" rx="28" ry="5" fill="#6a6a6a" />
          <path d="M66 140 V74 Q66 60 74 52 V22 H86 V52 Q94 60 94 74 V140Z" fill="#161616" />
          <rect x="72" y="16" width="16" height="8" rx="2" fill="#2e2e2e" />
          <path d="M71 70 Q72 60 77 56 V30" stroke="#e4e4e4" stroke-width="3" fill="none" stroke-linecap="round" />
          <path d="M70 84 H90 V112 H70Z" fill="#d0d0d0" />
          <path d="M150 140 C124 140 116 118 124 104 C130 94 136 92 138 80 C140 68 160 68 162 80 C164 92 170 94 176 104 C184 118 176 140 150 140Z" fill="url(#still-pear)" />
          <path d="M150 70 Q151 60 156 54" stroke="#1a1a1a" stroke-width="3" fill="none" stroke-linecap="round" />
          <path d="M155 60 Q168 50 176 58 Q166 66 155 60Z" fill="#3a3a3a" />
          <ellipse cx="134" cy="112" rx="5" ry="9" fill="#ffffff" opacity="0.7" />
          <path d="M210 98 H260 L256 136 Q235 142 214 136Z" fill="url(#still-cup)" />
          <ellipse cx="235" cy="98" rx="25" ry="5" fill="#2a2a2a" />
          <path d="M258 106 Q272 108 268 120 Q264 128 255 127" stroke="#bdbdbd" stroke-width="5" fill="none" />
        </symbol>
        <symbol id="pic-light" viewBox="0 0 272 172">
          <linearGradient id="light-sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9c9c9c" /><stop offset="1" stop-color="#e6e6e6" /></linearGradient>
          <rect width="272" height="118" fill="url(#light-sky)" />
          <path d="M18 36 Q30 22 48 30 Q60 20 76 32 Q88 34 84 44 H20 Q10 42 18 36Z M120 22 Q130 12 144 20 Q156 14 162 26 H118Z" fill="#f6f6f6" />
          <path d="M196 64 L40 40 L40 78Z" fill="#ffffff" opacity="0.55" />
          <rect y="112" width="272" height="60" fill="#5e5e5e" />
          <path d="M0 108 H120 L110 114 H0Z" fill="#8a8a8a" />
          <path d="M14 126 Q22 122 30 126 M58 138 Q68 133 78 138 M24 154 Q36 148 48 154 M96 128 Q104 124 112 128 M120 150 Q130 145 140 150 M80 162 Q92 156 104 162" stroke="#b4b4b4" stroke-width="2.4" fill="none" stroke-linecap="round" />
          <path d="M150 172 L158 124 L176 104 L196 96 L230 92 L254 100 L272 98 V172Z" fill="#222222" />
          <path d="M162 172 L168 140 L184 122 L200 118 L196 140 L188 172Z" fill="#3c3c3c" />
          <path d="M200 96 L204 46 H222 L226 96Z" fill="#f2f2f2" />
          <path d="M203 84 H223 L224 72 H202Z M204 60 H222 L222.6 50 H203.4Z" fill="#1e1e1e" />
          <path d="M218 46 L222 46 L226 96 L221 96Z" fill="#9a9a9a" opacity="0.6" />
          <rect x="198" y="42" width="30" height="5" fill="#1a1a1a" />
          <rect x="203" y="30" width="20" height="12" fill="#ffffff" />
          <path d="M203 30 H223 L213 20Z" fill="#1a1a1a" />
          <path d="M60 60 l5 3 l5 -3 M84 52 l4 2.5 l4 -2.5 M72 70 l3 2 l3 -2" stroke="#1a1a1a" stroke-width="1.6" fill="none" stroke-linecap="round" />
        </symbol>
      </defs>

      <rect class="pareti" x="-4000" y="-4000" width="9600" height="9600" data-wall />
      <g data-glow-place><ellipse rx="760" ry="620" fill="url(#glow)" /></g>

      <!-- the safelight: a hood on a cord, its red glass lit -->
      <g data-lamp>
        <line x1="0" y1="-900" x2="0" y2="56" class="lampada__filo" />
        <path d="M-74 118 L-52 56 H52 L74 118Z" class="lampada__cappa" />
        <rect x="-10" y="44" width="20" height="16" rx="3" class="lampada__attacco" />
        <path d="M-66 118 H66 L58 136 H-58Z" class="lampada__vetro" />
        <path d="M-50 122 H30" class="lampada__riflesso" />
      </g>

      <!-- the sign of the lab -->
      <g data-sign>
        <rect x="-150" y="-46" width="300" height="92" rx="6" class="insegna" />
        <rect x="-140" y="-36" width="280" height="72" rx="3" class="insegna__bordo" />
        <text y="-2" class="insegna__nome" data-lab-name>LOWLIGHT</text>
        <text y="24" class="insegna__sotto">PRINT LAB · DOOR STAYS SHUT</text>
      </g>

      <!-- the drying line and its hook -->
      <g data-hook><circle r="9" class="gancio" /><circle r="3.5" class="gancio__foro" /></g>
      <path data-wire class="filo" />
      <g data-hung></g>
      <g data-pins></g>

      <!-- the bench -->
      <rect x="-4000" width="9600" height="5000" fill="url(#bench-shade)" data-bench />
      <rect x="-4000" width="9600" height="6" class="banco__bordo" data-bench-edge />

      <!-- the box of printing paper -->
      <g data-box>
        <rect x="-153" y="-82" width="306" height="176" rx="6" class="scatola__ombra" filter="url(#soft)" />
        <rect x="-145" y="-86" width="290" height="164" rx="5" class="scatola" />
        <rect x="-133" y="-74" width="266" height="122" rx="3" class="scatola__dentro" />
        <rect x="-127" y="-70" width="254" height="112" rx="2" class="scatola__fogli" />
        <rect x="-127" y="-70" width="254" height="108" rx="2" class="scatola__foglio" />
        <g data-box-slot></g>
        <rect x="-145" y="52" width="290" height="26" rx="2" class="scatola__fronte" />
        <text y="70" class="etichetta etichetta--scura" data-box-label>LOWLIGHT · FIBRE · GLOSSY</text>
      </g>

      <g data-trays></g>

      <!-- the darkroom timer -->
      <g data-timer>
        <rect x="-110" y="-62" width="220" height="132" rx="10" class="orologio__ombra" filter="url(#soft)" />
        <rect x="-104" y="-66" width="208" height="124" rx="10" class="orologio" />
        <rect x="-88" y="-50" width="176" height="74" rx="5" class="orologio__vetro" />
        <text x="-76" y="-28" class="orologio__fase" data-timer-label>READY</text>
        <text x="78" y="14" class="orologio__cifre" data-timer-digits>0:00</text>
        <rect x="-86" y="34" width="40" height="12" rx="6" class="orologio__tasto" />
        <rect x="-38" y="34" width="40" height="12" rx="6" class="orologio__tasto" />
      </g>

      <g data-air></g>

      <!-- the bamboo tongs: the tips sit on 0 0, the script opens and closes them -->
      <g data-tongs-place>
        <g data-tongs>
          <line y1="-300" class="pinza__braccio" data-arm-a />
          <line y1="-300" class="pinza__braccio" data-arm-b />
          <line class="pinza__gomma" data-tip-a />
          <line class="pinza__gomma" data-tip-b />
          <rect x="-11" y="-286" width="22" height="16" rx="4" class="pinza__elastico" />
        </g>
      </g>
      <g data-fx></g>
    </svg>
  </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.

37 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 foto in camera oscura» da Animorio e adattala al mio sito

Cosa fa

Una stanza buia, rossa: in alto la lampada di sicurezza col vetro acceso, l’insegna di un laboratorio inventato, Lowlight, e un filo teso con tre mollette di legno. Sul banco una scatola di carta da stampa, due vaschette di plastica (sviluppo e fissaggio) e un timer da camera oscura. Una pinza di bambù scende dall’alto, pizzica il primo foglio della scatola e lo solleva: il foglio, che prima era steso, si raddrizza e pende dalla pinza. Lo porta sopra lo sviluppo, oscillando per l’inerzia, e lo stende nel liquido: la vaschetta sobbalza, dai bordi del foglio partono cerchi che si allargano e saltano gocce. La pinza lo spinge sotto una volta, poi si fa da parte. Il timer conta, la vaschetta dondola, il liquido si sposta da un lato all’altro e il foglio scivola un poco: intanto l’immagine affiora, prima i neri, poi i grigi, alla fine le luci. La pinza lo riprende, lo lascia sgocciolare nella vaschetta e lo stende nel fissaggio; poi lo porta su, al filo. La molletta si apre, si richiude di scatto con un rimbalzo, il filo si abbassa e oscilla, la foto dondola e smette piano; dal bordo di sotto si formano gocce che si gonfiano, si staccano e cadono allungandosi, e la lucentezza del bagnato si spegne asciugando. Tre foto (un lago con i pini, una natura morta, un faro), poi il filo viene tirato: le foto escono a destra e tornano le mollette vuote. Un clic sviluppa subito la foto successiva, o fa correre quella che è nelle vaschette. Passando sopra una foto appesa la si fa dondolare.

Come funziona

Due stanze, una misura

Il palco è un SVG con un viewBox fisso, 1600 per 1000 su uno schermo largo e 900 per 1600 su uno alto, e preserveAspectRatio meet; le pareti e il banco sono rettangoli molto più grandi, così non resta mai un bordo vuoto. Lo script sceglie la stanza dalle proporzioni della finestra, sistema lampada, insegna, scatola, timer, vaschette e mollette, e calcola quanto si vede davvero oltre il viewBox: il filo arriva fin fuori dallo schermo e le foto, quando il filo viene tirato, escono lì.

L’immagine che affiora

Le tre foto sono symbol disegnati in grigi dentro la pagina. Ogni foglio ha un suo filtro: un feComponentTransfer con una tabella di undici valori, poi un feColorMatrix che porta il grigio fra il nero della stampa e il colore della carta sotto la luce rossa. Allo sviluppo p da 0 a 1 la tabella si riscrive: un grigio con scurezza D comincia a comparire a (1 − D) × 0,62 e finisce 0,38 dopo, con un’ombra morbida. Così i neri arrivano per primi e le luci per ultime, come nella vaschetta vera.

Il foglio e la pinza

Ogni foglio ha l’origine al centro del bordo di sopra, dove lo prende la pinza e dove lo stringe la molletta: posizione, rotazione, grandezza e quanto è schiacciato (fy) stanno in un oggetto che GSAP anima, e un ticker li scrive come transform. Steso in una vaschetta fy vale 0,6, il foglio sembra visto dall’alto; sollevato fy torna 1 e il foglio pende dritto dalla pinza. Quando la pinza lo porta, il ticker mette la sua punta sul bordo del foglio, quindi non si staccano mai. Il foglio cambia gruppo a ogni passaggio (scatola, aria, vaschetta, filo) per stare sotto il liquido o sopra il banco.

Liquido, onde e gocce

Ogni vaschetta ha il fondo, il foglio, un velo di liquido semitrasparente sopra e una striscia di riflesso che va da un lato all’altro quando dondola, tutto ritagliato da un clipPath. Le onde sono ellissi con lo stesso schiacciamento del foglio che si allargano e sfumano; gli schizzi fanno un arco veloce. Le gocce partono dal bordo di sotto del foglio (contando la sua rotazione), si gonfiano con back.out, cadono con power2.in allungandosi e, se cadono in una vaschetta, lasciano un cerchietto.

Il filo e la regia

Il filo è una curva quadratica: il punto di controllo scende della freccia, e la freccia cresce con ogni foto appesa e fa un rimbalzo elastico quando la molletta si chiude. Mollette e foto leggono dal filo la loro altezza e la sua pendenza. Ogni foto è una timeline (dalla scatola, sviluppo, fissaggio, filo) e alla fine chiama la successiva dopo una pausa; dopo la terza il filo viene tirato e si ricomincia uguale. Un clic salta la pausa o mette la timeline a timeScale 3.

Valori da cambiare

NomeFileDi partenzaCosa cambia
LABscript.js'Lowlight'Il nome del laboratorio, sull’insegna e sulla scatola della carta.
DEVELOPscript.js3.4Quanti secondi ci mette l’immagine ad affiorare nello sviluppo.
RIPPLESscript.js3Quanti cerchi corrono sul liquido quando un foglio ci entra.
DRIPSscript.js1Quanto gocciolano le foto appese (0 = asciutte).
SWINGscript.js1Quanto dondola una foto quando la molletta si chiude o ci passi sopra.
SPEEDscript.js1La velocità di tutta l’animazione: 2 è il doppio, 0.5 la metà.
PAUSEscript.js0.6Quanti secondi passano fra una foto e la successiva.

Provali nel Playground

Quando usarla

Per la pagina di un fotografo, di un laboratorio di stampa, di un corso di fotografia analogica o di una mostra: un’apertura che racconta il mestiere, una galleria in cui le foto arrivano una alla volta, una schermata di caricamento. Al posto dei disegni si possono mettere altre immagini in grigi.

Sul telefono

La stessa animazione in una stanza alta: la lampada e l’insegna in cima, il filo sotto con le tre mollette, le due vaschette affiancate e sotto la scatola e il timer. Un tocco sviluppa la foto successiva.

Con il movimento ridotto

Niente si muove: il foglio compare in dissolvenza nello sviluppo, l’immagine affiora dalle ombre alle luci, poi il foglio svanisce e ricompare appeso al filo. Niente pinza, onde, dondolii né gocce; dopo la terza foto il filo si svuota in dissolvenza.

Trappole

  • Le timeline di ogni foto si costruiscono quando la foto parte, con il foglio ancora nella scatola: il dondolio nella vaschetta, scritto partendo dalla x del foglio, lo trascinava verso la scatola e lo faceva sparire sotto il bordo. Le posizioni nella vaschetta si contano dal centro della vaschetta, non dal foglio.
  • La posizione del filo, in alto, dipende da quante foto sono già appese: il punto d’arrivo del foglio sulla molletta è un valore funzione, letto da GSAP solo quando il volo comincia.
  • Il foglio deve stare sotto il velo di liquido quando è nella vaschetta e sopra tutto quando vola: cambia gruppo con append nell’istante in cui tocca il liquido o viene preso, e le coordinate restano le stesse perché ogni gruppo compensa la posizione della vaschetta.
  • Al passaggio dalla pinza alla molletta il filo deve essere fermo, altrimenti la foto salta: l’abbassamento parte da zero con un tween corto, poi rimbalza.
  • Il filtro dell’SVG lavora in linearRGB e schiarisce i grigi della tabella: con color-interpolation-filters sRGB la tabella vale per i grigi così come sono disegnati.
  • Nella stanza alta il gancio del filo non c’è (il filo esce dallo schermo dai due lati): il suo transform non va scritto, altrimenti la console segnala translate(null).

Librerie

GSAP 3.15.0, Oswald (font variabile) 5.3.0.

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

Collaudo

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