
Persiane sulle immagini
Immagini, Scroll
NuovaA vita
Una fila infinita inclinata di otto gradi, con i quadri raddrizzati, che scivola di lato mentre scorri la pagina e si trascina e si lancia con l'inerzia.
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>La galleria in diagonale</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>
<svg class="disegni" aria-hidden="true" focusable="false" width="0" height="0">
<defs>
<symbol id="d-girasole" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#F3E9D2" />
<g fill="#2F5DA8"><circle cx="0" cy="0" r="46" /><circle cx="200" cy="0" r="46" /><circle cx="0" cy="200" r="46" /><circle cx="200" cy="200" r="46" /></g>
<g fill="#2F5DA8" transform="translate(100 100)">
<ellipse rx="12" ry="40" cy="-38" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(45)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(90)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(135)" />
<ellipse rx="12" ry="40" cy="-38" transform="rotate(180)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(225)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(270)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(315)" />
</g>
<circle cx="100" cy="100" r="22" fill="#E2A93B" />
</symbol>
<symbol id="d-onda" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#E7EEF3" />
<g fill="none" stroke-width="14" stroke-linecap="round">
<path d="M-10 40 Q15 16 40 40 T90 40 T140 40 T190 40 T240 40" stroke="#1E4F8C" />
<path d="M-10 80 Q15 56 40 80 T90 80 T140 80 T190 80 T240 80" stroke="#3D7CC2" />
<path d="M-10 120 Q15 96 40 120 T90 120 T140 120 T190 120 T240 120" stroke="#1E4F8C" />
<path d="M-10 160 Q15 136 40 160 T90 160 T140 160 T190 160 T240 160" stroke="#3D7CC2" />
</g>
</symbol>
<symbol id="d-stella" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#F6E7D8" />
<path d="M100 18 L122 78 L182 100 L122 122 L100 182 L78 122 L18 100 L78 78 Z" fill="#B5452B" />
<path d="M100 42 L141 59 L158 100 L141 141 L100 158 L59 141 L42 100 L59 59 Z" fill="none" stroke="#B5452B" stroke-width="6" />
<rect x="80" y="80" width="40" height="40" fill="#F2C14E" transform="rotate(45 100 100)" />
</symbol>
<symbol id="d-foglia" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#EAF1E3" />
<path d="M100 190 C100 140 100 80 100 14" stroke="#2F5C3C" stroke-width="5" fill="none" />
<g fill="#3F7A4E"><ellipse cx="72" cy="60" rx="30" ry="13" transform="rotate(-30 72 60)" /><ellipse cx="128" cy="96" rx="30" ry="13" transform="rotate(30 128 96)" /><ellipse cx="72" cy="132" rx="30" ry="13" transform="rotate(-30 72 132)" /></g>
<g fill="#6FA36A"><ellipse cx="128" cy="40" rx="24" ry="10" transform="rotate(30 128 40)" /><ellipse cx="72" cy="96" rx="24" ry="10" transform="rotate(-30 72 96)" /><ellipse cx="128" cy="150" rx="24" ry="10" transform="rotate(30 128 150)" /></g>
</symbol>
<symbol id="d-rombo" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#F4EEE6" />
<path d="M100 8 L192 100 L100 192 L8 100 Z" fill="#2A3F6B" />
<path d="M100 38 L162 100 L100 162 L38 100 Z" fill="#E0A43B" />
<path d="M100 68 L132 100 L100 132 L68 100 Z" fill="#B94A36" />
</symbol>
<symbol id="d-nodo" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#EDE3F0" />
<g fill="none" stroke="#5B3A7A" stroke-width="10">
<circle cx="76" cy="76" r="44" /><circle cx="124" cy="76" r="44" /><circle cx="76" cy="124" r="44" /><circle cx="124" cy="124" r="44" />
</g>
</symbol>
<symbol id="freccia" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6" /></symbol>
</defs>
</svg>
<header class="apertura">
<p class="apertura__sopra">Una bottega di maiolica</p>
<h1 class="apertura__titolo">Sei decori,<br>in diagonale</h1>
<p class="apertura__testo">Scorri la pagina: la fila sale di traverso. Puoi anche trascinarla, o usare le frecce.</p>
</header>
<!-- la fila: inclinata di -8 gradi, ogni quadro raddrizzato di +8; le copie prima e dopo la fanno infinita -->
<section class="galleria" data-galleria aria-label="Decori">
<div class="galleria__inclinato">
<div class="galleria__binario" data-binario>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-girasole" /></svg></div>
<figcaption><span class="vagone__nome">Girasole</span><span class="vagone__nota">Blu cobalto e giallo zafferano</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-onda" /></svg></div>
<figcaption><span class="vagone__nome">Onda</span><span class="vagone__nota">Due azzurri che si inseguono</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-stella" /></svg></div>
<figcaption><span class="vagone__nome">Stella</span><span class="vagone__nota">Rosso mattone su crema</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-foglia" /></svg></div>
<figcaption><span class="vagone__nome">Foglia</span><span class="vagone__nota">Verde salvia e verde bosco</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-rombo" /></svg></div>
<figcaption><span class="vagone__nome">Rombo</span><span class="vagone__nota">Blu notte, ocra e ruggine</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-nodo" /></svg></div>
<figcaption><span class="vagone__nome">Nodo</span><span class="vagone__nota">Viola prugna, a mano libera</span></figcaption>
</figure>
</div>
</div>
<div class="galleria__frecce">
<button class="freccia freccia--indietro" type="button" data-indietro aria-label="Decoro precedente"><svg aria-hidden="true"><use href="#freccia" /></svg></button>
<button class="freccia" type="button" data-avanti aria-label="Decoro successivo"><svg aria-hidden="true"><use href="#freccia" /></svg></button>
</div>
</section>
<section class="chiusura">
<p>Ogni decoro nasce dipinto a mano su una piastrella cruda, poi va in forno una seconda volta. Nessuno è uguale al vicino.</p>
</section>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* La galleria in diagonale */
:root {
--carta: #F6F2EA;
--inchiostro: #1F2440;
--tenue: #6A6E83;
--inclinazione: -8deg; /* quanto sale la fila; i quadri girano al contrario */
--largo: 52vw; /* un quadro */
--alto: 33.7vw;
--spazio: 2.6vw;
--margine: clamp(18px, 4.5vw, 64px);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Mona Sans Variable', system-ui, sans-serif;
color: var(--inchiostro);
background: var(--carta);
}
.disegni {
position: absolute;
width: 0;
height: 0;
}
button {
font: inherit;
}
.apertura {
display: grid;
align-content: end;
gap: 18px;
min-height: 70vh;
padding: var(--margine);
}
.apertura__sopra {
margin: 0;
color: var(--tenue);
font-size: 0.95rem;
font-weight: 600;
}
.apertura__titolo {
margin: 0;
font-size: clamp(3rem, 1rem + 8vw, 10rem);
font-weight: 700;
font-stretch: 112%;
line-height: 0.88;
letter-spacing: -0.045em;
}
.apertura__testo {
max-width: 34ch;
margin: 0;
font-size: clamp(1.05rem, 0.95rem + 0.4vw, 1.35rem);
line-height: 1.4;
}
/* ------------------------------------------------------------------ la fila inclinata */
.galleria {
position: relative;
height: calc(var(--alto) + 30vw);
min-height: 420px;
overflow: hidden;
cursor: grab;
touch-action: pan-y;
user-select: none;
-webkit-user-select: none;
}
.galleria.presa {
cursor: grabbing;
}
.galleria__inclinato {
position: absolute;
left: 50%;
top: 46%;
transform: translate(-50%, -50%) rotate(var(--inclinazione));
}
.galleria__binario {
display: flex;
gap: var(--spazio);
width: max-content;
will-change: transform;
}
.vagone {
flex: none;
width: var(--largo);
margin: 0;
transform: rotate(calc(var(--inclinazione) * -1));
}
.vagone__quadro {
height: var(--alto);
overflow: hidden;
background: #ddd;
}
.vagone__quadro svg {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
}
.vagone figcaption {
display: flex;
justify-content: space-between;
gap: 16px;
padding-top: 12px;
font-size: clamp(0.9rem, 0.8rem + 0.35vw, 1.1rem);
}
.vagone__nome {
font-weight: 650;
}
.vagone__nota {
color: var(--tenue);
text-align: right;
}
.galleria__frecce {
position: absolute;
right: var(--margine);
bottom: var(--margine);
display: flex;
gap: 10px;
}
.freccia {
display: grid;
place-items: center;
width: 54px;
height: 54px;
border: 0;
border-radius: 50%;
background: var(--inchiostro);
color: var(--carta);
cursor: pointer;
transition: transform 0.16s cubic-bezier(0.23, 1, 0.32, 1);
}
.freccia:active {
transform: scale(0.94);
}
.freccia:focus-visible {
outline: 2px solid var(--inchiostro);
outline-offset: 3px;
}
.freccia svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.freccia--indietro svg {
transform: rotate(180deg);
}
.chiusura {
min-height: 70vh;
padding: var(--margine);
display: grid;
align-content: center;
}
.chiusura p {
max-width: 30ch;
margin: 0;
font-size: clamp(1.4rem, 1rem + 1.6vw, 2.6rem);
font-weight: 560;
line-height: 1.15;
letter-spacing: -0.02em;
}
@media (max-width: 700px) {
:root {
--inclinazione: -4deg;
--largo: 82vw;
--alto: 85vw;
--spazio: 16px;
}
.galleria {
height: calc(var(--alto) + 60vw);
}
.galleria__frecce {
left: 0;
right: 0;
justify-content: center;
}
.vagone figcaption {
flex-direction: column;
gap: 2px;
}
.vagone__nota {
text-align: left;
}
}/* La galleria in diagonale
La fila è inclinata di -8 gradi e ogni quadro è raddrizzato di +8: le immagini salgono di traverso ma
restano dritte. Il binario si sposta di un pixel di lato per ogni pixel di pagina scorsa; si può anche
trascinare (col dito o col mouse) e lanciare, e allora scivola con l'inerzia. Copie prima e dopo, e
una posizione che si avvolge: la fila non finisce mai. */
/* ---- valori da cambiare ---- */
const ACCOPPIATA = 1; /* pixel di lato per ogni pixel di pagina scorsa (0 = solo trascinamento) */
const TRASCINA = 2.5; /* quanto corre la fila rispetto al dito */
const LANCIO = 1200; /* forza del lancio quando lasci andare */
const ATTRITO = 0.94; /* quanto frena il lancio a ogni sessantesimo di secondo */
const DERIVA = 18; /* pixel al secondo di movimento da sola quando nessuno la tocca (0 = ferma) */
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const galleria = document.querySelector('[data-galleria]');
const binario = document.querySelector('[data-binario]');
const originali = [...binario.children];
/* tre serie: copie prima, originali, copie dopo; la posizione si avvolge su una serie */
originali.forEach((v) => { const c = v.cloneNode(true); c.setAttribute('aria-hidden', 'true'); binario.appendChild(c); });
originali.slice().reverse().forEach((v) => { const c = v.cloneNode(true); c.setAttribute('aria-hidden', 'true'); binario.prepend(c); });
let passo = 0;
let giro = 0;
let avvolgi = (x) => x;
function misura() {
const a = binario.children[0].getBoundingClientRect();
const stile = getComputedStyle(binario);
passo = binario.children[0].offsetWidth + (parseFloat(stile.columnGap) || 0);
giro = passo * originali.length;
avvolgi = gsap.utils.wrap(-giro / 2, giro / 2);
return a;
}
misura();
addEventListener('resize', misura);
let bersaglio = 0;
let corrente = 0;
let momento = 0;
let preso = null;
let sopra = false;
const imposta = gsap.quickSetter(binario, 'x', 'px');
/* la pagina scorre: quanto la sezione è vicina allo schermo, tanto la fila si sposta di lato */
const inVista = () => {
const r = galleria.getBoundingClientRect();
return r.bottom > -innerHeight * 0.12 && r.top < innerHeight * 1.12;
};
let scorsoPrima = scrollY;
gsap.ticker.add((t, dtMs) => {
const dt = Math.min(0.1, dtMs / 1000);
const scorso = scrollY - scorsoPrima;
scorsoPrima = scrollY;
if (!RIDOTTO && inVista()) bersaglio -= scorso * ACCOPPIATA;
if (!preso && !RIDOTTO) {
if (Math.abs(momento) > 1) {
bersaglio += momento * dt;
momento *= Math.pow(ATTRITO, dt * 60);
} else if (!sopra && inVista()) {
bersaglio -= DERIVA * dt;
}
}
const k = RIDOTTO ? 1 : preso ? 0.08 : 0.12;
corrente += (bersaglio - corrente) * (1 - Math.pow(1 - k, dt * 120));
imposta(avvolgi(corrente));
});
/* ---------------------------------------------------------------- trascinare e lanciare */
galleria.addEventListener('pointerdown', (e) => {
if (e.button !== 0 || e.target.closest('button')) return;
preso = { x: e.clientX, y: e.clientY, bloccato: null, campioni: [], id: e.pointerId };
momento = 0;
});
addEventListener('pointermove', (e) => {
if (!preso || e.pointerId !== preso.id) return;
const dx = e.clientX - preso.x;
const dy = e.clientY - preso.y;
if (preso.bloccato === null) {
if (Math.hypot(dx, dy) < 5) return;
/* decide l'asse: di lato è nostro, in verticale è della pagina */
preso.bloccato = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
if (preso.bloccato === 'x') {
galleria.classList.add('presa');
galleria.setPointerCapture(e.pointerId);
}
}
if (preso.bloccato !== 'x') return;
const passoDito = e.clientX - (preso.ultimoX ?? preso.x);
preso.ultimoX = e.clientX;
bersaglio += passoDito * (RIDOTTO ? 1 : TRASCINA);
preso.campioni.push({ v: passoDito, t: performance.now() });
if (preso.campioni.length > 5) preso.campioni.shift();
});
const lascia = (e) => {
if (!preso || e.pointerId !== preso.id) return;
if (preso.bloccato === 'x' && preso.campioni.length > 1 && !RIDOTTO) {
const c = preso.campioni;
const tempo = Math.max(1, c[c.length - 1].t - c[0].t);
const v = c.reduce((s, x) => s + x.v, 0) / tempo;
if (Math.abs(v) > 0.15) momento = v * LANCIO;
}
galleria.classList.remove('presa');
preso = null;
};
addEventListener('pointerup', lascia);
addEventListener('pointercancel', lascia);
/* il trackpad di lato (o maiuscolo + rotella) sposta la fila */
galleria.addEventListener('wheel', (e) => {
const lato = Math.abs(e.deltaX) > Math.abs(e.deltaY) * 1.75 ? e.deltaX : e.shiftKey ? e.deltaY : 0;
if (!lato) return;
e.preventDefault();
bersaglio -= lato * 0.8;
}, { passive: false });
galleria.addEventListener('pointerenter', (e) => { if (e.pointerType === 'mouse') sopra = true; });
galleria.addEventListener('pointerleave', () => { sopra = false; });
/* le frecce: un quadro alla volta, fermandosi sul più vicino */
const vai = (verso) => {
momento = 0;
bersaglio = Math.round((bersaglio - verso * passo) / passo) * passo;
};
document.querySelector('[data-avanti]').addEventListener('click', () => vai(1));
document.querySelector('[data-indietro]').addEventListener('click', () => vai(-1));Il contenitore della fila ruota di -8 gradi e ogni quadro ruota di +8: il binario corre sulla diagonale, le immagini restano in piedi. Lo spostamento si dà al binario dentro il contenitore ruotato, così avanza lungo la diagonale e non in orizzontale.
Gli originali stanno in mezzo a due serie di copie. La posizione passa per gsap.utils.wrap(-giro/2, giro/2), dove il giro è la larghezza di una serie: quando esce da un lato rientra dall'altro, e con copie identiche lo scatto non si vede.
Scorrimento della pagina, trascinamento, lancio, frecce e deriva cambiano tutti lo stesso bersaglio; ogni fotogramma la posizione vera lo insegue con una frenata indipendente dai fotogrammi al secondo (0,08 mentre trascini, 0,12 dopo).
Durante il trascinamento si tengono gli ultimi cinque spostamenti; lasciando andare, la loro velocità media per 1200 diventa un momento che si aggiunge al bersaglio e perde il 6% ogni sessantesimo di secondo.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
ACCOPPIATA | script.js | 1 | Pixel di lato per ogni pixel di pagina scorsa. Con 0 la fila si muove solo trascinandola. |
TRASCINA | script.js | 2.5 | Quanto corre la fila rispetto al dito. |
LANCIO | script.js | 1200 | Forza del lancio quando lasci andare. |
DERIVA | script.js | 18 | Pixel al secondo di movimento da sola quando nessuno la tocca. Con 0 sta ferma. |
--inclinazione | style.css | -8deg | Quanto sale la fila; al telefono -4 gradi. |
Per mostrare una collezione di immagini grandi (lavori, prodotti, stanze) in una sezione che si attraversa scorrendo, senza fermare la pagina.
I quadri diventano quasi larghi quanto lo schermo e l'inclinazione scende a quattro gradi. Il dito in verticale scorre la pagina, in orizzontale trascina la fila.
La fila resta inclinata ma ferma: niente deriva, niente legame con lo scorrimento e niente inerzia. Si trascina uno a uno e le frecce saltano al quadro dopo.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Peso: 16 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.