
Il caricamento che diventa marchio
Transizioni, SVG, Testo
NuovaGratis
Fra un capitolo e l’altro entra una scheda scura: il nome compare di colpo, poi un orologio enorme conta i minuti fino all’ora del capitolo e la pagina nuova arriva di lato.
Una cartella che funziona da sola: index.html, style.css, script.js e le librerie da 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();
});Una scheda fissa entra con xPercent da 100 a 0 in 0,8 secondi con expo.out (dal basso al telefono) e intanto il suo fondo va dal nero al grigio inchiostro. Nome e orologio non si animano: compaiono e spariscono con un set di visibility, tagli netti come in un montaggio.
L'orologio conta in minuti totali (ore per 60 più minuti), così passa le ore e anche la mezzanotte. Un oggetto va dai minuti di partenza a quelli d'arrivo con power2.inOut; in onUpdate si arrotonda per difetto e si riscrive l'ora solo quando il minuto cambia, con un piccolo salto delle cifre al posto del ticchettio.
Ogni cifra sta in una casella di larghezza fissa (0,5em, testo centrato): senza, mentre conta l'orologio trema. Le cifre tabellari (tabular-nums) farebbero lo stesso, ma in Mona Sans hanno lo zero barrato. I due punti lampeggiano in CSS con un'animazione step-end sulla visibility.
Ogni pagina è un livello fisso a sé, così quella nuova può passare sopra la scheda con uno z-index più alto. Entra con x dalla larghezza dello schermo a 0; la foto dentro la cornice è più larga del 14% e parte spostata, così arriva un poco dopo la pagina.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
ENTRATA | script.js | 0.8 | Secondi della scheda che entra. |
TITOLO | script.js | 1.5 | Per quanti secondi resta il nome del capitolo prima dell'orologio. |
CONTA | script.js | 1 | Secondi del conteggio, qualunque sia la distanza fra le due ore. |
ATTESA | script.js | 2.6 | Secondi su ogni pagina prima del capitolo dopo. Con 0 si cambia solo col pulsante. |
data-da, data-a | index.html | 06:15, 06:30 | Su ogni pagina: l'ora da cui parte il conteggio e quella del capitolo. |
Per un racconto diviso in momenti: il programma di una giornata, le tappe di un viaggio, le ore di un evento. Il passaggio dura quattro secondi: va bene fra capitoli, non fra una pagina e l'altra di un sito qualsiasi.
La scheda sale dal basso invece di entrare di lato, la foto va sopra il testo e l'orologio si adatta alla larghezza.
Niente scheda e niente conteggio: il pulsante porta direttamente al capitolo dopo, che dice già la sua ora. I capitoli non cambiano da soli.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Peso: 23 KB dei nostri file.
Provata il 27 settembre 2026
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.
Con l’email e la password del tuo account.
Hai fatto l’accesso come .
Il tuo piano è gratis. Quando arriverà, l’accesso a vita si attiverà su questo stesso account.