
The loader that becomes a logo
Transitions, SVG, Text
NewFree
Between one chapter and the next a dark card slides in: the name cuts in, then a huge clock counts the minutes up to the chapter's time and the new page arrives from the side.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>L’orologio che conta i minuti</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- i fotogrammi: scene in bianco e nero disegnate a mano, riquadro 300 x 200 -->
<svg class="scene" aria-hidden="true" focusable="false" width="0" height="0">
<defs>
<linearGradient id="g-notte" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#090909" /><stop offset="1" stop-color="#262626" /></linearGradient>
<radialGradient id="g-faro"><stop offset="0" stop-color="#ffffff" /><stop offset="0.32" stop-color="#ececec" /><stop offset="1" stop-color="#ececec" stop-opacity="0" /></radialGradient>
<linearGradient id="g-fascio" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff" stop-opacity="0.55" /><stop offset="1" stop-color="#ffffff" stop-opacity="0" /></linearGradient>
<linearGradient id="g-cielo" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#dcdcdc" /><stop offset="1" stop-color="#9c9c9c" /></linearGradient>
<radialGradient id="g-luce"><stop offset="0" stop-color="#ffffff" /><stop offset="0.5" stop-color="#d9d9d9" /><stop offset="1" stop-color="#d9d9d9" stop-opacity="0" /></radialGradient>
<pattern id="p-scacchi" width="26" height="26" patternUnits="userSpaceOnUse" patternTransform="skewY(-6)">
<rect width="26" height="26" fill="#efefef" /><rect width="13" height="13" fill="#121212" /><rect x="13" y="13" width="13" height="13" fill="#121212" />
</pattern>
<symbol id="s-fari" viewBox="0 0 300 200">
<rect width="300" height="200" fill="url(#g-notte)" />
<rect y="150" width="300" height="50" fill="#121212" />
<path d="M58 140 Q150 112 242 140 L252 152 L48 152 Z" fill="#040404" />
<path d="M104 128 L0 200 L150 200 Z" fill="url(#g-fascio)" opacity="0.45" />
<path d="M196 128 L300 200 L150 200 Z" fill="url(#g-fascio)" opacity="0.45" />
<circle cx="104" cy="130" r="30" fill="url(#g-faro)" />
<circle cx="196" cy="130" r="30" fill="url(#g-faro)" />
</symbol>
<symbol id="s-gomme" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#bdbdbd" />
<rect y="160" width="300" height="40" fill="#8f8f8f" />
<g fill="none">
<circle cx="104" cy="126" r="40" stroke="#151515" stroke-width="26" />
<circle cx="104" cy="126" r="53" stroke="#2b2b2b" stroke-width="4" stroke-dasharray="3 5" />
<circle cx="196" cy="126" r="40" stroke="#151515" stroke-width="26" />
<circle cx="196" cy="126" r="53" stroke="#2b2b2b" stroke-width="4" stroke-dasharray="3 5" />
<circle cx="150" cy="44" r="30" stroke="#1d1d1d" stroke-width="20" />
</g>
<circle cx="104" cy="126" r="17" fill="#7a7a7a" />
<circle cx="196" cy="126" r="17" fill="#7a7a7a" />
<circle cx="150" cy="44" r="12" fill="#8a8a8a" />
</symbol>
<symbol id="s-bandiera" viewBox="0 0 300 200">
<rect width="300" height="200" fill="url(#g-cielo)" />
<rect x="68" y="26" width="6" height="174" fill="#1a1a1a" />
<path d="M74 32 C118 16 160 48 204 34 C230 26 250 28 264 32 L264 124 C250 120 230 118 204 126 C160 140 118 108 74 124 Z" fill="url(#p-scacchi)" />
<path d="M74 32 C118 16 160 48 204 34 C230 26 250 28 264 32 L264 124 C250 120 230 118 204 126 C160 140 118 108 74 124 Z" fill="#000" opacity="0.14" />
</symbol>
<symbol id="s-casco" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#2a2a2a" />
<rect y="152" width="300" height="48" fill="#191919" />
<path d="M90 152 C86 92 122 54 170 54 C218 54 238 92 238 128 L238 152 Z" fill="#e3e3e3" />
<path d="M148 96 C170 86 214 86 238 100 L238 130 C212 124 172 124 148 130 Z" fill="#0f0f0f" />
<path d="M100 120 C118 72 170 62 214 72" fill="none" stroke="#8c8c8c" stroke-width="10" />
</symbol>
<symbol id="s-rettilineo" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#cdcdcd" />
<rect y="90" width="300" height="110" fill="#5a5a5a" />
<path d="M141 90 L159 90 L300 200 L0 200 Z" fill="#222" />
<path d="M141 90 L0 200" stroke="#f1f1f1" stroke-width="7" stroke-dasharray="9 9" />
<path d="M159 90 L300 200" stroke="#f1f1f1" stroke-width="7" stroke-dasharray="9 9" />
<g fill="#e9e9e9">
<path d="M149.2 98 L150.8 98 L151.3 106 L148.7 106 Z" />
<path d="M148.2 116 L151.8 116 L152.8 130 L147.2 130 Z" />
<path d="M146.6 144 L153.4 144 L155.4 166 L144.6 166 Z" />
<path d="M144 182 L156 182 L158.4 200 L141.6 200 Z" />
</g>
</symbol>
<symbol id="s-semaforo" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#141414" />
<rect x="146" y="0" width="8" height="70" fill="#222" />
<rect x="38" y="68" width="224" height="60" rx="8" fill="#2a2a2a" />
<circle cx="70" cy="98" r="30" fill="url(#g-luce)" />
<circle cx="110" cy="98" r="30" fill="url(#g-luce)" />
<circle cx="70" cy="98" r="14" fill="#fafafa" />
<circle cx="110" cy="98" r="14" fill="#fafafa" />
<circle cx="150" cy="98" r="14" fill="#3b3b3b" />
<circle cx="190" cy="98" r="14" fill="#3b3b3b" />
<circle cx="230" cy="98" r="14" fill="#3b3b3b" />
</symbol>
<symbol id="s-volante" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#d4d4d4" />
<circle cx="150" cy="104" r="68" fill="none" stroke="#141414" stroke-width="18" />
<path d="M150 104 L86 90 M150 104 L214 90 M150 104 L150 170" stroke="#141414" stroke-width="12" stroke-linecap="round" />
<circle cx="150" cy="104" r="22" fill="#2b2b2b" />
</symbol>
<symbol id="s-box" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#999" />
<rect x="40" y="34" width="220" height="166" fill="#0e0e0e" />
<rect x="40" y="34" width="220" height="72" fill="#bcbcbc" />
<g stroke="#7b7b7b" stroke-width="3">
<path d="M40 44 H260 M40 54 H260 M40 64 H260 M40 74 H260 M40 84 H260 M40 94 H260" />
</g>
<rect x="118" y="146" width="64" height="34" fill="#3a3a3a" />
<circle cx="128" cy="186" r="7" fill="#5a5a5a" />
<circle cx="172" cy="186" r="7" fill="#5a5a5a" />
</symbol>
<symbol id="s-tribuna" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#ececec" />
<path d="M0 36 L300 16 L300 28 L0 50 Z" fill="#1c1c1c" />
<path d="M0 70 L300 54 L300 64 L0 82 Z" fill="#8a8a8a" />
<path d="M0 96 L300 78 L300 90 L0 110 Z" fill="#747474" />
<path d="M0 124 L300 104 L300 118 L0 140 Z" fill="#5e5e5e" />
<path d="M0 154 L300 132 L300 148 L0 172 Z" fill="#484848" />
<rect y="178" width="300" height="22" fill="#2e2e2e" />
</symbol>
<symbol id="s-pioggia" viewBox="0 0 300 200">
<rect width="300" height="200" fill="#4a4a4a" />
<g stroke="#cfcfcf" stroke-width="1.6" stroke-linecap="round" opacity="0.7">
<path d="M20 10 l8 22 M60 30 l8 22 M100 6 l8 22 M140 40 l8 22 M180 12 l8 22 M220 34 l8 22 M260 8 l8 22 M40 70 l8 22 M80 94 l8 22 M120 66 l8 22 M160 100 l8 22 M200 72 l8 22 M240 96 l8 22 M280 60 l8 22 M30 130 l8 22 M70 150 l8 22 M110 124 l8 22 M150 150 l8 22 M190 128 l8 22 M230 150 l8 22 M270 120 l8 22" />
</g>
<path d="M0 200 C80 146 220 146 300 200 Z" fill="#232323" />
<path d="M150 196 L70 150" stroke="#0f0f0f" stroke-width="6" stroke-linecap="round" />
</symbol>
</defs>
</svg>
<!-- i capitoli: uno alla volta; fra l'uno e l'altro passa la scheda con l'orologio -->
<main class="pagine">
<section class="pagina pagina--giallo attiva" data-titolo="Partenza" data-giorno="Primo giorno" data-da="06:15" data-a="06:30" data-sotto="Il furgone lascia l’officina prima che faccia chiaro" aria-label="Partenza">
<div class="pagina__testo">
<p class="pagina__quando">Primo giorno, 06:30</p>
<h2 class="pagina__titolo">Partenza</h2>
<p class="pagina__corpo">Il furgone esce quando è ancora buio. Dietro, le gomme legate con le cinghie e un thermos che nessuno ha chiuso bene.</p>
</div>
<figure class="pagina__foto" aria-hidden="true"><div class="pagina__foto-dentro"><svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid slice"><use href="#s-gomme" /></svg></div></figure>
</section>
<section class="pagina pagina--carta" data-titolo="Il box" data-giorno="Primo giorno" data-da="06:30" data-a="09:15" data-sotto="Le serrande si alzano una dopo l’altra" aria-label="Il box">
<div class="pagina__testo">
<p class="pagina__quando">Primo giorno, 09:15</p>
<h2 class="pagina__titolo">Il box</h2>
<p class="pagina__corpo">Ogni box ha già il suo rumore: un compressore, una radio accesa, una chiave che cade sul cemento.</p>
</div>
<figure class="pagina__foto" aria-hidden="true"><div class="pagina__foto-dentro"><svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid slice"><use href="#s-box" /></svg></div></figure>
</section>
<section class="pagina pagina--inchiostro" data-titolo="La notte" data-giorno="Primo giorno" data-da="21:40" data-a="22:30" data-sotto="I fari passano più veloci del rumore" aria-label="La notte">
<div class="pagina__testo">
<p class="pagina__quando">Primo giorno, 22:30</p>
<h2 class="pagina__titolo">La notte</h2>
<p class="pagina__corpo">Dopo il tramonto la pista diventa una fila di fari. Il rumore arriva sempre un attimo dopo la luce.</p>
</div>
<figure class="pagina__foto" aria-hidden="true"><div class="pagina__foto-dentro"><svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid slice"><use href="#s-fari" /></svg></div></figure>
</section>
<section class="pagina pagina--carta" data-titolo="L’alba" data-giorno="Secondo giorno" data-da="05:20" data-a="06:05" data-sotto="La pioggia smette quando nessuno guarda" aria-label="L’alba">
<div class="pagina__testo">
<p class="pagina__quando">Secondo giorno, 06:05</p>
<h2 class="pagina__titolo">L’alba</h2>
<p class="pagina__corpo">Ha piovuto tutta la notte. Il primo a vedere il cielo che si apre è chi asciuga i cordoli.</p>
</div>
<figure class="pagina__foto" aria-hidden="true"><div class="pagina__foto-dentro"><svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid slice"><use href="#s-pioggia" /></svg></div></figure>
</section>
<section class="pagina pagina--giallo" data-titolo="L’arrivo" data-giorno="Secondo giorno" data-da="15:10" data-a="15:30" data-sotto="La bandiera scende una volta sola" aria-label="L’arrivo">
<div class="pagina__testo">
<p class="pagina__quando">Secondo giorno, 15:30</p>
<h2 class="pagina__titolo">L’arrivo</h2>
<p class="pagina__corpo">Ventiquattro ore per venti secondi di bandiera. Poi tutti corrono in pista e i box restano vuoti.</p>
</div>
<figure class="pagina__foto" aria-hidden="true"><div class="pagina__foto-dentro"><svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid slice"><use href="#s-bandiera" /></svg></div></figure>
</section>
</main>
<!-- la scheda scura: prima il nome del capitolo, poi l'orologio che conta fino all'ora giusta -->
<div class="scheda" aria-hidden="true">
<p class="scheda__titolo"><span class="scheda__parola">Partenza</span></p>
<div class="scheda__orologio">
<p class="scheda__giorno"><span>Primo giorno</span></p>
<p class="scheda__ora"><span class="cifra">0</span><span class="cifra">6</span><span class="due-punti">:</span><span class="cifra">1</span><span class="cifra">5</span></p>
<p class="scheda__sotto"></p>
</div>
</div>
<button type="button" class="avanti">Capitolo dopo</button>
<p class="annuncio" aria-live="polite"></p>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* L'orologio che conta i minuti */
:root {
--giallo: #f5dc3a;
--inchiostro: #161616;
--carta: #f2f2ee;
--margine: clamp(16px, 3vw, 44px);
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
height: 100%;
}
body {
font-family: 'Mona Sans Variable', system-ui, sans-serif;
color: var(--inchiostro);
background: var(--inchiostro);
overflow: hidden;
}
.scene {
position: absolute;
width: 0;
height: 0;
}
/* ------------------------------------------------------------------ i capitoli */
/* ogni pagina è un livello a sé: così quella che arriva può passare sopra la scheda */
.pagina {
position: fixed;
inset: 0;
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--margine);
padding: var(--margine);
visibility: hidden;
}
.pagina.attiva {
visibility: visible;
}
.pagina--giallo { background: var(--giallo); color: var(--inchiostro); }
.pagina--carta { background: var(--carta); color: var(--inchiostro); }
.pagina--inchiostro { background: var(--inchiostro); color: var(--carta); }
.pagina--inchiostro .pagina__titolo { color: var(--giallo); }
.pagina__testo {
align-self: end;
display: grid;
gap: 18px;
padding-bottom: 64px;
}
.pagina__quando {
margin: 0;
font-size: 0.95rem;
font-weight: 620;
}
.pagina__titolo {
margin: 0;
font-size: clamp(3rem, 1rem + 7vw, 9.5rem);
font-weight: 900;
font-stretch: 75%;
line-height: 0.84;
text-transform: uppercase;
}
/* rientro della prima riga al posto dello spazio fra i paragrafi, come in una rivista */
.pagina__corpo {
max-width: 34ch;
margin: 0;
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.35rem);
line-height: 1.35;
text-indent: 2.2em;
}
.pagina__foto {
position: relative;
margin: 0;
overflow: hidden;
}
/* la foto è più larga della cornice: all'arrivo della pagina scorre dentro di lei */
.pagina__foto-dentro {
position: absolute;
top: 0;
bottom: 0;
left: -7%;
width: 114%;
}
.pagina__foto svg {
display: block;
width: 100%;
height: 100%;
}
/* ------------------------------------------------------------------ la scheda con l'orologio */
.scheda {
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--inchiostro);
color: var(--carta);
visibility: hidden;
}
.scheda__titolo {
position: absolute;
inset: 0;
display: grid;
place-items: center;
margin: 0;
color: var(--giallo);
font-weight: 900;
font-stretch: 75%;
line-height: 0.8;
text-transform: uppercase;
white-space: nowrap;
}
.scheda__parola {
display: block;
padding-top: 0.06em;
}
.scheda__orologio {
display: grid;
justify-items: center;
gap: 12px;
padding: var(--margine);
text-align: center;
}
.scheda__giorno,
.scheda__sotto {
margin: 0;
overflow: clip;
}
.scheda__giorno {
font-size: clamp(0.95rem, 0.85rem + 0.4vw, 1.25rem);
font-weight: 620;
}
.scheda__giorno span {
display: block;
}
.scheda__ora {
display: flex;
margin: 0;
color: var(--giallo);
font-size: min(40vw, 56vh);
font-weight: 900;
font-stretch: 75%;
line-height: 0.82;
}
/* ogni cifra nella sua casella di larghezza fissa: l'orologio non balla mentre conta.
(le cifre tabellari di Mona Sans hanno lo zero barrato) */
.cifra {
display: inline-block;
width: 0.5em;
text-align: center;
}
/* i due punti lampeggiano come in un orologio da cruscotto */
.due-punti {
animation: lampeggia 0.8s step-end infinite;
}
@keyframes lampeggia {
50% { visibility: hidden; }
}
.scheda__sotto {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0 0.3em;
max-width: 32ch;
font-size: clamp(1rem, 0.9rem + 0.6vw, 1.5rem);
line-height: 1.25;
}
.parola {
display: inline-block;
overflow: clip;
padding-bottom: 0.08em;
}
.parola span {
display: inline-block;
}
/* ------------------------------------------------------------------ comandi */
.avanti {
position: fixed;
right: var(--margine);
bottom: var(--margine);
z-index: 30;
min-height: 48px;
padding: 0 22px;
border: 0;
border-radius: 999px;
background: var(--inchiostro);
color: var(--carta);
box-shadow: 0 0 0 1.5px rgba(242, 242, 238, 0.35);
font: inherit;
font-weight: 650;
cursor: pointer;
transition: transform 0.16s cubic-bezier(0.23, 1, 0.32, 1);
}
.avanti:active {
transform: scale(0.97);
}
.avanti:focus-visible {
outline: 2px solid var(--giallo);
outline-offset: 3px;
}
.annuncio {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
@media (max-width: 700px) {
.pagina {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 1fr) auto;
}
.pagina__foto {
order: -1;
}
.pagina__testo {
padding-bottom: 72px;
}
}
@media (prefers-reduced-motion: reduce) {
.due-punti {
animation: none;
}
}/* L'orologio che conta i minuti
Fra un capitolo e l'altro entra una scheda scura: prima il nome del capitolo compare di colpo, poi
al suo posto un orologio enorme conta i minuti dall'ora di prima a quella del capitolo, con i due
punti che lampeggiano. Quando ha finito, la pagina nuova arriva di lato e copre la scheda; la sua
foto, più larga della cornice, scorre ancora un poco dentro di lei. */
/* ---- valori da cambiare ---- */
const ENTRATA = 0.8; /* secondi della scheda che entra */
const TITOLO = 1.5; /* per quanto resta il nome del capitolo */
const CONTA = 1; /* secondi del conteggio dei minuti */
const ATTESA = 2.6; /* secondi su ogni pagina prima del capitolo dopo (0 = solo col pulsante) */
const SPINTA = 6; /* quanto resta indietro la foto nella cornice mentre la pagina arriva, in % */
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const pagine = [...document.querySelectorAll('.pagina')];
const scheda = document.querySelector('.scheda');
const titoloScheda = document.querySelector('.scheda__titolo');
const parola = document.querySelector('.scheda__parola');
const orologio = document.querySelector('.scheda__orologio');
const giorno = document.querySelector('.scheda__giorno span');
const cifre = [...document.querySelectorAll('.scheda__ora .cifra')];
const minuti = cifre.slice(2);
const sotto = document.querySelector('.scheda__sotto');
const avanti = document.querySelector('.avanti');
const annuncio = document.querySelector('.annuncio');
let attuale = 0;
let inCorso = false;
let prossimo = null;
const due = (n) => String(n).padStart(2, '0');
const inMinuti = (hhmm) => { const [h, m] = hhmm.split(':').map(Number); return h * 60 + m; };
/* il nome del capitolo riempie la larghezza dello schermo (misurato a 100px e scalato) */
function adattaTitolo() {
titoloScheda.style.fontSize = '100px';
const margine = Math.min(44, Math.max(16, innerWidth * 0.03));
const perLarghezza = ((innerWidth - margine * 2) / parola.offsetWidth) * 100;
const perAltezza = (innerHeight * 0.6) / 0.8;
titoloScheda.style.fontSize = `${Math.min(perLarghezza, perAltezza)}px`;
}
function scriviOra(totale) {
const m = ((totale % 1440) + 1440) % 1440;
const testo = due(Math.floor(m / 60)) + due(m % 60);
cifre.forEach((c, i) => { c.textContent = testo[i]; });
}
/* il conteggio: un oggetto va dai minuti di partenza a quelli d'arrivo; a ogni minuto nuovo le cifre
cambiano e fanno un piccolo salto (al posto del ticchettio che il sito originale suona) */
function conta(da, a) {
const inizio = inMinuti(da);
let fine = inMinuti(a);
if (fine < inizio) fine += 1440;
const stato = { m: inizio };
let ultimo = inizio;
scriviOra(inizio);
return gsap.to(stato, {
m: fine, duration: CONTA, ease: 'power2.inOut',
onUpdate() {
const v = Math.floor(stato.m);
if (v === ultimo) return;
ultimo = v;
scriviOra(v);
gsap.fromTo(minuti, { yPercent: -9 }, { yPercent: 0, duration: 0.16, ease: 'power2.out', overwrite: true });
},
onComplete: () => scriviOra(fine),
});
}
/* il sottotitolo parola per parola, ognuna nella sua maschera */
function scriviSotto(testo) {
sotto.innerHTML = '';
return testo.split(' ').map((p) => {
const m = document.createElement('span');
m.className = 'parola';
const s = document.createElement('span');
s.textContent = p;
m.appendChild(s);
sotto.appendChild(m);
return s;
});
}
function mostra(i) {
pagine.forEach((p, j) => p.classList.toggle('attiva', j === i));
const d = pagine[i].dataset;
annuncio.textContent = `${d.titolo}, ${d.giorno.toLowerCase()}, ${d.a}`;
attuale = i;
}
function programma() {
if (prossimo) prossimo.kill();
if (RIDOTTO || !ATTESA) return;
prossimo = gsap.delayedCall(ATTESA, () => vai((attuale + 1) % pagine.length));
}
function vai(i) {
if (inCorso) return;
if (prossimo) prossimo.kill();
if (RIDOTTO) { mostra(i); return; }
inCorso = true;
const pagina = pagine[i];
const d = pagina.dataset;
const basso = matchMedia('(max-width: 700px)').matches;
parola.textContent = d.titolo;
adattaTitolo();
giorno.textContent = d.giorno;
const parole = scriviSotto(d.sotto);
scriviOra(inMinuti(d.da));
const t = { titolo: ENTRATA * 0.55 };
t.orologio = t.titolo + TITOLO + 0.35;
t.pagina = t.orologio + 0.15 + CONTA + 0.9;
gsap.timeline({ onComplete: () => { inCorso = false; programma(); } })
/* la scheda entra (dal basso al telefono) e il nero diventa inchiostro */
.set(scheda, { visibility: 'visible', backgroundColor: '#000' })
.set(titoloScheda, { visibility: 'hidden' })
.set(orologio, { visibility: 'hidden' })
.fromTo(scheda, basso ? { xPercent: 0, yPercent: 100 } : { xPercent: 100, yPercent: 0 }, { xPercent: 0, yPercent: 0, duration: ENTRATA, ease: 'expo.out' }, 0)
.to(scheda, { backgroundColor: '#161616', duration: 0.4, ease: 'none' }, ENTRATA - 0.4)
/* tagli netti: il nome, poi l'orologio */
.set(titoloScheda, { visibility: 'visible' }, t.titolo)
.set(titoloScheda, { visibility: 'hidden' }, t.titolo + TITOLO)
.set(orologio, { visibility: 'visible' }, t.orologio)
.fromTo(giorno, { yPercent: 110 }, { yPercent: 0, duration: 0.8, ease: 'expo.out' }, t.orologio)
.add(conta(d.da, d.a), t.orologio + 0.15)
.fromTo(parole, { yPercent: 110 }, { yPercent: 0, duration: 0.9, ease: 'expo.out', stagger: 0.1 }, t.orologio + 0.2)
/* la pagina nuova arriva sopra la scheda; la sua foto scorre ancora un poco nella cornice */
.call(() => { pagina.classList.add('attiva'); pagina.style.zIndex = 20; }, null, t.pagina)
.fromTo(pagina, basso ? { x: 0, y: () => innerHeight * 1.2 } : { x: () => innerWidth, y: 0 }, { x: 0, y: 0, duration: 1, ease: 'expo.out' }, t.pagina)
.fromTo(pagina.querySelector('.pagina__foto-dentro'), { xPercent: basso ? 0 : SPINTA }, { xPercent: 0, duration: 1.5, ease: 'expo.out' }, t.pagina)
.call(() => {
mostra(i);
pagina.style.zIndex = '';
/* anche nome e orologio: un figlio con visibility visible resta acceso dentro un genitore nascosto */
gsap.set([scheda, titoloScheda, orologio], { visibility: 'hidden' });
}, null, t.pagina + 1);
}
avanti.addEventListener('click', () => vai((attuale + 1) % pagine.length));
addEventListener('resize', () => { if (inCorso) adattaTitolo(); });
document.fonts.ready.then(() => {
mostra(0);
programma();
});A fixed card enters with xPercent from 100 to 0 in 0.8 seconds with expo.out (from the bottom on phones) while its background goes from black to ink grey. Name and clock are not animated: they appear and disappear with a visibility set, hard cuts as in film editing.
The clock counts in total minutes (hours times 60 plus minutes), so it crosses hours and even midnight. An object goes from the starting minutes to the arrival ones with power2.inOut; in onUpdate the value is floored and the time is rewritten only when the minute changes, with a small jump of the digits instead of a tick.
Each digit sits in a fixed-width box (0.5em, centred text): without it, the clock shakes while it counts. Tabular figures (tabular-nums) would do the same, but in Mona Sans they have a slashed zero. The colon blinks in CSS with a step-end animation on visibility.
Each page is a fixed layer of its own, so the new one can pass over the card with a higher z-index. It enters with x from the screen width to 0; the photo inside the frame is 14% wider and starts offset, so it arrives a little after the page.
| Name | File | Default | What it changes |
|---|---|---|---|
ENTRATA | script.js | 0.8 | Seconds for the card to slide in. |
TITOLO | script.js | 1.5 | How many seconds the chapter name stays before the clock. |
CONTA | script.js | 1 | Seconds of the count, whatever the distance between the two times. |
ATTESA | script.js | 2.6 | Seconds on each page before the next chapter. With 0 it only changes with the button. |
data-da, data-a | index.html | 06:15, 06:30 | On each page: the time the count starts from and the chapter's time. |
For a story split into moments: the schedule of a day, the stages of a trip, the hours of an event. The transition takes four seconds: fine between chapters, not between any two pages of a site.
The card rises from the bottom instead of entering from the side, the photo goes above the text and the clock adapts to the width.
No card and no count: the button goes straight to the next chapter, which already shows its time. Chapters do not change on their own.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Weight: 23 KB of our own files.
Tested on 27 September 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
You’re signed in as .
Your plan is free. When lifetime access arrives, it will switch on for this same account.