
The loader that becomes a logo
Transitions, SVG, Text
NewFree
A drawn scene and, in front of it, a window showing one of its details up close. Every now and then the window blinks: it closes into an almond and then a line, the scene changes, and it reopens on another detail.
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>La finestra che sbatte le palpebre</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>
<main class="scena" data-scena>
<!-- lo sfondo: una scena disegnata per strato, una sola visibile alla volta -->
<svg class="sfondo" data-sfondo viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"></svg>
<p class="nota" data-nota>Ogni tanto la finestra sbatte le palpebre e si riapre su un’altra scena. Premila per farlo subito.</p>
<!-- la finestra: la stessa scena vista da vicino, dentro un’apertura a mandorla -->
<button class="finestra" type="button" data-finestra data-autoclic aria-label="Cambia scena">
<span class="finestra__occhio" data-occhio>
<svg class="finestra__vista" data-vista viewBox="0 0 160 90" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><use data-uso href="#" /></svg>
</span>
<span class="finestra__fessura" aria-hidden="true"></span>
<span class="finestra__angoli" aria-hidden="true"></span>
<span class="finestra__didascalia">
<span data-dettaglio></span>
<span class="finestra__luogo" data-luogo></span>
</span>
</button>
</main>
<!-- le scene, disegnate una volta sola: le usano sia lo sfondo sia la finestra -->
<svg class="nascosto" aria-hidden="true" focusable="false"><defs data-scene></defs></svg>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* La finestra che sbatte le palpebre */
:root {
--notte: #0F1116;
--velo: rgba(12, 14, 18, 0.58);
--bianco: #F6F4EF;
--margine: clamp(18px, 4vw, 56px);
--alto: 100vh;
}
@supports (height: 100svh) {
:root { --alto: 100svh; }
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
height: 100%;
}
body {
background: var(--notte);
color: var(--bianco);
font-family: 'Mona Sans Variable', system-ui, sans-serif;
overflow: hidden;
}
.nascosto {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.scena {
position: relative;
height: var(--alto);
overflow: hidden;
}
.sfondo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.strato {
transform-box: view-box;
transform-origin: 50% 50%;
}
.nota {
position: absolute;
z-index: 2;
top: var(--margine);
left: var(--margine);
max-width: 34ch;
margin: 0;
padding: 0.7em 0.95em;
border-radius: 12px;
background: var(--velo);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
font-size: clamp(0.92rem, 0.84rem + 0.3vw, 1.08rem);
font-weight: 500;
line-height: 1.38;
}
/* ---- la finestra ---- */
.finestra {
position: absolute;
z-index: 1;
top: 54%;
left: 50%;
width: min(48vw, 660px);
aspect-ratio: 16 / 9;
padding: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
transform: translate(-50%, -50%);
-webkit-tap-highlight-color: transparent;
/* quanto è aperta la palpebra: 1 aperta, 0 chiusa (la muove lo script) */
--palpebra: 1;
}
.finestra:focus-visible {
outline: 2px solid var(--bianco);
outline-offset: 18px;
}
/* l'apertura è un’ellisse larga: chiudendosi diventa una mandorla e poi una riga */
.finestra__occhio {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--notte);
clip-path: ellipse(80% calc(var(--palpebra) * 82%) at 50% 50%);
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.35);
}
.finestra__vista {
display: block;
width: 100%;
height: 100%;
transform: scaleY(calc(0.93 + var(--palpebra) * 0.07));
}
.finestra__fessura {
position: absolute;
top: 50%;
left: 3%;
right: 3%;
height: 1px;
background: var(--bianco);
opacity: calc(1 - var(--palpebra));
box-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
}
/* gli angoli del mirino: quattro squadre di luce fuori dalla finestra */
.finestra__angoli {
--l: 20px;
--s: 1.5px;
--c: rgba(246, 244, 239, 0.95);
position: absolute;
inset: -12px;
pointer-events: none;
background:
linear-gradient(var(--c), var(--c)) left top / var(--l) var(--s) no-repeat,
linear-gradient(var(--c), var(--c)) left top / var(--s) var(--l) no-repeat,
linear-gradient(var(--c), var(--c)) right top / var(--l) var(--s) no-repeat,
linear-gradient(var(--c), var(--c)) right top / var(--s) var(--l) no-repeat,
linear-gradient(var(--c), var(--c)) left bottom / var(--l) var(--s) no-repeat,
linear-gradient(var(--c), var(--c)) left bottom / var(--s) var(--l) no-repeat,
linear-gradient(var(--c), var(--c)) right bottom / var(--l) var(--s) no-repeat,
linear-gradient(var(--c), var(--c)) right bottom / var(--s) var(--l) no-repeat;
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.45));
transition: inset 320ms cubic-bezier(0.23, 1, 0.32, 1);
}
@media (hover: hover) and (pointer: fine) {
.finestra:hover .finestra__angoli {
inset: -18px;
}
}
.finestra:active .finestra__angoli {
inset: -8px;
transition-duration: 120ms;
}
.finestra__didascalia {
position: absolute;
top: calc(100% + 18px);
left: 50%;
display: flex;
gap: 0.9em;
padding: 0.45em 0.85em;
border-radius: 999px;
background: var(--velo);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
font-size: clamp(0.82rem, 0.78rem + 0.2vw, 0.95rem);
font-weight: 600;
white-space: nowrap;
transform: translateX(-50%);
}
.finestra__luogo {
color: rgba(246, 244, 239, 0.66);
font-weight: 500;
}
@media (max-width: 640px) {
.finestra {
top: 52%;
width: 86vw;
}
}/* La finestra che sbatte le palpebre
Una scena disegnata a tutto schermo e, davanti, una finestra che la mostra da vicino: un dettaglio
ingrandito della stessa scena. Ogni tanto la finestra sbatte le palpebre: l’apertura a mandorla si
stringe fino a una riga, mentre è chiusa la scena dietro cambia, e si riapre su un altro dettaglio.
Le scene sono SVG disegnati una volta sola e usati due volte: grandi dietro, ingranditi nella finestra. */
/* ---- valori da cambiare ---- */
const APERTA = 2.4; /* secondi a occhi aperti */
const CHIUDE = 0.9; /* secondi per chiudere */
const BUIO = 0.25; /* secondi a occhi chiusi, mentre la scena cambia */
const APRE = 1.1; /* secondi per riaprire */
/* un numero a caso che è sempre lo stesso: le scene si disegnano uguali a ogni visita */
function caso(seme) {
return () => {
seme = (seme + 0x6d2b79f5) | 0;
let t = Math.imul(seme ^ (seme >>> 15), 1 | seme);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const n1 = (v) => v.toFixed(1);
/* ---------------------------------------------------------------- le scene (1600 × 1000) */
function mare() {
const r = caso(7);
let s = '<linearGradient id="cielo-mare" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#F9DDB8"/><stop offset="1" stop-color="#F09A73"/></linearGradient>';
s += '<rect width="1600" height="640" fill="url(#cielo-mare)"/><circle cx="1060" cy="600" r="130" fill="#FCE7B0"/>';
for (let i = 0; i < 6; i++) {
const x = 260 + r() * 760; const y = 150 + r() * 210; const w = 14 + r() * 10;
s += `<path d="M${n1(x)} ${n1(y)}q${n1(w / 2)} ${n1(-w / 2.5)} ${n1(w)} 0q${n1(w / 2)} ${n1(-w / 2.5)} ${n1(w)} 0" fill="none" stroke="#6B4A3F" stroke-width="3" stroke-linecap="round"/>`;
}
s += '<rect y="620" width="1600" height="380" fill="#2C5E73"/>';
for (let i = 0; i < 9; i++) {
const w = 220 - i * 20 + r() * 30;
s += `<rect x="${n1(1060 - w / 2)}" y="${632 + i * 16}" width="${n1(w)}" height="5" rx="2.5" fill="#F7C58F" opacity="${(0.75 - i * 0.06).toFixed(2)}"/>`;
}
for (let i = 0; i < 16; i++) {
const x = r() * 1500; const y = 670 + r() * 310; const w = 40 + r() * 60;
s += `<path d="M${n1(x)} ${n1(y)}q${n1(w / 4)} -8 ${n1(w / 2)} 0t${n1(w / 2)} 0" fill="none" stroke="#4B8198" stroke-width="3" stroke-linecap="round"/>`;
}
s += '<path d="M1300 1000V660Q1360 612 1480 600Q1560 594 1600 598V1000Z" fill="#3A2E2A"/>';
s += '<path d="M1428 600L1436 470H1464L1472 600Z" fill="#F4EFE6"/><path d="M1432.6 540H1467.4L1469 560H1431ZM1430.2 580H1469.8L1471 596H1429Z" fill="#C8453B"/><rect x="1438" y="452" width="24" height="18" fill="#FCE7B0"/><path d="M1434 452H1466L1450 434Z" fill="#C8453B"/>';
/* il dettaglio: la barca */
s += '<g transform="translate(84 10)"><path d="M556 598H646L632 616H572Z" fill="#8E3B2F"/><circle cx="584" cy="605" r="2.2" fill="#F4EFE6"/><circle cx="600" cy="605" r="2.2" fill="#F4EFE6"/><circle cx="616" cy="605" r="2.2" fill="#F4EFE6"/><rect x="599" y="530" width="3" height="68" fill="#3A2E2A"/><path d="M603 534V592H640Z" fill="#F4EFE6"/><path d="M597 546V592H566Z" fill="#EADBC4"/><path d="M602 530L620 535L602 540Z" fill="#C8453B"/><circle cx="624" cy="590" r="3.2" fill="#3A2E2A"/><rect x="621.5" y="592.5" width="5" height="6" fill="#3A2E2A"/><path d="M560 624q10 -3 20 0t20 0t20 0t20 0" fill="none" stroke="#8E3B2F" stroke-width="2" opacity=".35"/></g>';
return s;
}
function bosco() {
const r = caso(21);
const pino = (x, y, h, c) => `<path d="M${n1(x)} ${n1(y - h)}L${n1(x + h * 0.32)} ${n1(y)}H${n1(x - h * 0.32)}Z" fill="${c}"/>`;
let s = '<linearGradient id="cielo-bosco" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#EEF1DC"/><stop offset="1" stop-color="#C7DCC7"/></linearGradient>';
s += '<rect width="1600" height="1000" fill="url(#cielo-bosco)"/><circle cx="1180" cy="230" r="70" fill="#F7EBC4"/>';
s += '<path d="M0 560Q300 470 600 520T1200 500T1600 520V1000H0Z" fill="#A9C7AE"/>';
for (let i = 0; i < 40; i++) s += pino(r() * 1600, 560 + r() * 30, 70 + r() * 50, '#7FA88A');
s += '<path d="M0 700Q400 640 800 690T1600 670V1000H0Z" fill="#6E9A74"/>';
for (let i = 0; i < 30; i++) {
const x = r() * 1600; const h = 120 + r() * 80;
if (x > 760 && x < 1060) continue;
s += pino(x, 700 + r() * 20, h, '#4B7A5E');
}
/* il dettaglio: la baita col fumo e la finestra accesa */
s += '<g><path d="M948 598q-10 -30 6 -50q14 -18 4 -44" fill="none" stroke="#F2F5F0" stroke-width="10" stroke-linecap="round" opacity=".75"/><rect x="940" y="600" width="16" height="36" fill="#5A3526"/><path d="M858 652L924 606L990 652Z" fill="#5A3526"/><rect x="866" y="650" width="116" height="56" fill="#8A5A3C"/><path d="M866 662H982M866 676H982M866 690H982" stroke="#74482F" stroke-width="2"/><rect x="880" y="664" width="30" height="24" fill="#FFD27A"/><path d="M895 664V688M880 676H910" stroke="#74482F" stroke-width="2.5"/><rect x="936" y="668" width="22" height="38" fill="#4A2E20"/><circle cx="953" cy="688" r="1.8" fill="#FFD27A"/><path d="M846 706H1002" stroke="#3F6B4E" stroke-width="4"/></g>';
s += '<path d="M0 800Q500 740 1000 790T1600 780V1000H0Z" fill="#3F6B4E"/>';
for (let i = 0; i < 12; i++) s += pino(i < 6 ? r() * 380 : 1220 + r() * 380, 820 + r() * 30, 260 + r() * 140, '#2C4F3B');
return s;
}
function citta() {
const r = caso(3);
let s = '<linearGradient id="cielo-citta" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#141B33"/><stop offset="1" stop-color="#34406B"/></linearGradient>';
s += '<rect width="1600" height="1000" fill="url(#cielo-citta)"/>';
for (let i = 0; i < 70; i++) s += `<circle cx="${Math.round(r() * 1600)}" cy="${Math.round(r() * 420)}" r="${n1(0.8 + r() * 1.6)}" fill="#F3E9C6" opacity="${(0.4 + r() * 0.6).toFixed(2)}"/>`;
s += '<circle cx="1290" cy="190" r="62" fill="#F3E9C6"/><circle cx="1312" cy="176" r="10" fill="#E4D9B3"/><circle cx="1272" cy="206" r="7" fill="#E4D9B3"/>';
let x = -20;
while (x < 1600) {
const w = 60 + r() * 90; const h = 180 + r() * 260;
s += `<rect x="${Math.round(x)}" y="${Math.round(760 - h)}" width="${Math.round(w)}" height="${Math.round(h + 240)}" fill="#1F2A4A"/>`;
x += w + 6;
}
/* i palazzi davanti, con le finestre accese un po’ a caso */
x = -10;
while (x < 1600) {
const w = 110 + r() * 120; const h = 260 + r() * 300; const cima = 1000 - h;
s += `<rect x="${Math.round(x)}" y="${Math.round(cima)}" width="${Math.round(w)}" height="${Math.round(h)}" fill="#121A31"/>`;
for (let wy = cima + 24; wy < 980; wy += 36) {
for (let wx = x + 16; wx < x + w - 24; wx += 30) s += `<rect x="${Math.round(wx)}" y="${Math.round(wy)}" width="14" height="20" fill="${r() < 0.34 ? '#F9D77E' : '#223052'}"/>`;
}
x += w + 14;
}
/* il dettaglio: il gatto sul davanzale, con un vaso di fiori */
s += '<g><rect x="640" y="540" width="190" height="460" fill="#0E152A"/><rect x="700" y="580" width="70" height="86" fill="#F9D77E"/><path d="M704 584H731V612Q717 598 704 612Z" fill="#E9A15A" opacity=".55"/><rect x="700" y="580" width="70" height="86" fill="none" stroke="#3A2E2A" stroke-width="5"/><path d="M735 580V666M700 623H770" stroke="#3A2E2A" stroke-width="3"/><rect x="694" y="664" width="82" height="7" fill="#3A2E2A"/><path d="M744 664C743 652 746 642 752 638L750 628L756 634H762L767 628L766 639C771 644 772 654 770 664Z" fill="#161616"/><path d="M768 664C776 670 778 684 772 694" fill="none" stroke="#161616" stroke-width="4" stroke-linecap="round"/><path d="M708 650H724L721 664H711Z" fill="#B5553C"/><circle cx="712" cy="644" r="5" fill="#E8637A"/><circle cx="720" cy="641" r="5" fill="#F3A5B8"/><rect x="715" y="645" width="2" height="6" fill="#3E7C4F"/></g>';
return s;
}
function campo() {
const nuvola = (x, y, k) => `<g fill="#FFFFFF" opacity=".92"><ellipse cx="${x}" cy="${y}" rx="${70 * k}" ry="${22 * k}"/><ellipse cx="${x - 30 * k}" cy="${y - 14 * k}" rx="${36 * k}" ry="${22 * k}"/><ellipse cx="${x + 24 * k}" cy="${y - 20 * k}" rx="${42 * k}" ry="${28 * k}"/></g>`;
let s = '<linearGradient id="cielo-campo" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#A9D8EE"/><stop offset="1" stop-color="#E3F3F7"/></linearGradient>';
s += '<rect width="1600" height="1000" fill="url(#cielo-campo)"/>';
s += nuvola(1180, 180, 1.4) + nuvola(260, 120, 1) + nuvola(1440, 330, 0.8) + nuvola(520, 300, 0.7);
s += '<path d="M0 640Q260 560 560 600T1160 580T1600 600V1000H0Z" fill="#9DB85A"/><path d="M0 720Q400 640 820 700T1600 680V1000H0Z" fill="#D8C15E"/>';
for (let i = 0; i < 12; i++) {
const y = 740 + i * 22;
s += `<path d="M0 ${y}Q400 ${y - 60 + i * 3} 820 ${y - 10}T1600 ${y - 24}" fill="none" stroke="#C4AA45" stroke-width="3" opacity=".7"/>`;
}
s += '<path d="M0 860Q500 800 1000 850T1600 840V1000H0Z" fill="#7E9A45"/>';
s += '<rect x="1180" y="560" width="90" height="56" fill="#C8453B"/><path d="M1170 562L1225 526L1280 562Z" fill="#7A2E27"/><rect x="1215" y="586" width="18" height="30" fill="#5A2520"/><rect x="1190" y="574" width="16" height="14" fill="#FCE7B0"/>';
/* il dettaglio: l’aquilone, col filo che scende fino a chi lo tiene */
s += '<path d="M740 300C820 420 860 560 904 752" fill="none" stroke="#3A2E2A" stroke-width="1.6"/>';
s += '<g><path d="M740 220L778 262L740 300L702 262Z" fill="#FFD166"/><path d="M740 220L778 262H740Z" fill="#E84A5F"/><path d="M702 262H740V300Z" fill="#4F9DDE"/><path d="M740 262H778L740 300Z" fill="#2EC4B6"/><path d="M740 220V300M702 262H778" stroke="#3A2E2A" stroke-width="2"/><path d="M740 300C732 316 748 330 738 346C730 360 746 372 740 388" fill="none" stroke="#3A2E2A" stroke-width="1.6"/><path d="M734 316L746 312L740 322ZM732 346L744 342L738 352ZM734 374L746 370L740 380Z" fill="#E84A5F"/></g>';
s += '<g fill="#3A2E2A"><circle cx="910" cy="734" r="9"/><path d="M902 744H918L922 782H898Z"/><path d="M900 782h8v24h-8zM912 782h8v24h-8z"/></g>';
return s;
}
/* ogni scena: il luogo, il suo dettaglio e il riquadro del dettaglio (x, y, larghezza, altezza, in 16:9) */
const SCENE = [
{ id: 'mare', luogo: 'Il mare', dettaglio: 'La barca', vista: [579, 528, 220, 123.75], disegna: mare },
{ id: 'bosco', luogo: 'Il bosco', dettaglio: 'La baita', vista: [800, 578, 240, 135], disegna: bosco },
{ id: 'citta', luogo: 'La città', dettaglio: 'Il gatto', vista: [615, 560, 240, 135], disegna: citta },
{ id: 'campo', luogo: 'Il campo', dettaglio: 'L’aquilone', vista: [655, 196, 240, 135], disegna: campo },
];
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const scena = document.querySelector('[data-scena]');
const sfondo = document.querySelector('[data-sfondo]');
const finestra = document.querySelector('[data-finestra]');
const occhio = document.querySelector('[data-occhio]');
const vista = document.querySelector('[data-vista]');
const uso = document.querySelector('[data-uso]');
const dettaglio = document.querySelector('[data-dettaglio]');
const luogo = document.querySelector('[data-luogo]');
if (RIDOTTO) document.querySelector('[data-nota]').textContent = 'Premi la finestra per passare alla scena successiva.';
/* le scene si disegnano una volta: dietro ognuna ha il suo strato, la finestra usa quella del momento */
document.querySelector('[data-scene]').innerHTML = SCENE.map((s) => `<g id="quadro-${s.id}">${s.disegna()}</g>`).join('');
sfondo.innerHTML = SCENE.map((s, i) => `<g class="strato" data-strato style="opacity:${i ? 0 : 1}"><use href="#quadro-${s.id}"/></g>`).join('');
const strati = [...sfondo.querySelectorAll('[data-strato]')];
let indice = 0;
const riquadro = (v, dx = 0) => `${n1(v[0] + dx)} ${n1(v[1])} ${n1(v[2])} ${n1(v[3])}`;
function mostra(i, anima) {
const s = SCENE[i];
uso.setAttribute('href', `#quadro-${s.id}`);
dettaglio.textContent = s.dettaglio;
luogo.textContent = s.luogo;
finestra.setAttribute('aria-label', `${s.dettaglio}, ${s.luogo}: cambia scena`);
if (!anima) {
vista.setAttribute('viewBox', riquadro(s.vista));
return;
}
/* mentre è aperta la finestra scivola piano sul dettaglio, e la scena dietro si avvicina */
const giro = APERTA + CHIUDE + BUIO + APRE;
gsap.fromTo(vista, { attr: { viewBox: riquadro(s.vista, -s.vista[2] * 0.03) } }, { attr: { viewBox: riquadro(s.vista, s.vista[2] * 0.03) }, duration: giro, ease: 'none', overwrite: true });
gsap.fromTo(strati[i], { scale: 1, transformOrigin: '50% 50%' }, { scale: 1.05, duration: giro + 1, ease: 'none' });
}
function cambia() {
const prima = indice;
indice = (indice + 1) % SCENE.length;
gsap.to(strati[prima], { opacity: 0, duration: RIDOTTO ? 0.25 : 0.7, ease: 'power1.inOut', overwrite: 'auto' });
gsap.to(strati[indice], { opacity: 1, duration: RIDOTTO ? 0.25 : 0.7, ease: 'power1.inOut', overwrite: 'auto' });
mostra(indice, !RIDOTTO);
}
/* ---------------------------------------------------------------- il battito */
let battito = null;
if (!RIDOTTO) {
battito = gsap.timeline({ repeat: -1, paused: true })
.to({}, { duration: APERTA })
.addLabel('chiude')
.to(finestra, { '--palpebra': 0, duration: CHIUDE, ease: 'power3.inOut' })
.call(cambia)
.to({}, { duration: BUIO })
.to(finestra, { '--palpebra': 1, duration: APRE, ease: 'power3.inOut' });
/* fuori dallo schermo non batte */
new IntersectionObserver(([e]) => { if (e.isIntersecting) battito.play(); else battito.pause(); }).observe(scena);
}
/* ---------------------------------------------------------------- il gesto */
finestra.addEventListener('click', () => {
if (RIDOTTO) {
gsap.fromTo(occhio, { opacity: 0.2 }, { opacity: 1, duration: 0.25, ease: 'none' });
cambia();
return;
}
/* a occhi aperti si chiudono subito; se sta già battendo, si lascia finire */
if (battito.time() < battito.labels.chiude) battito.seek('chiude');
});
mostra(0, !RIDOTTO);Each scene is an SVG group drawn once inside a defs. The background uses it with a full-screen use (preserveAspectRatio xMidYMid slice), the window with another use inside an SVG whose viewBox is tight on the detail: being vector, enlarged four times it stays sharp. The gradients live inside the group and follow both.
The opening is a clip-path: ellipse(80% calc(var(--palpebra) × 82%) at 50% 50%). With the variable at 1 the ellipse covers the corners too; going down to 0 it narrows only in height, and the rectangle becomes first an almond and then a line. GSAP animates the CSS variable, and the same variable lights the line of light and slightly squashes the content.
A repeating timeline: 2.4 seconds open, 0.9 to close with power3.inOut, a callback that changes the scene, 0.25 in the dark, 1.1 to reopen. The scene behind fades over 0.7 seconds exactly while the window is closed, so the change is never seen with the eye open.
While the window is open, the viewBox drifts by 6% of its width, like a moving shot, and the scene behind goes from scale 1 to 1.05. Pressing the window makes the timeline jump to the blink label, but only if the eyelid is open: if it is already blinking, it finishes.
| Name | File | Default | What it changes |
|---|---|---|---|
SCENE | script.js | 4 scene | For each scene: the place, the detail’s name, the detail’s 16:9 box and the function that draws it (SVG in 1600 × 1000). |
APERTA, CHIUDE, BUIO, APRE | script.js | 2.4, 0.9, 0.25, 1.1 | The seconds of the blink: eye open, closing, in the dark, reopening. |
ellipse(80% … 82%) | style.css | 80%, 82% | The eyelid’s shape: narrower in width, rounder the almond. |
To tell a place or a product through details: the overall view behind, the thing to look at in front, and a gesture that suggests a gaze.
The window takes almost the full width; the background, cropped to the centre, still shows the four details, which all sit in the middle part of the scene.
No blink, no drift and no zoom: the window stays open and, when pressed, scene and detail move to the next with a quarter-second fade.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Weight: 19 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.