Costruire siti immersivi scroll‑telling 3D
con Claude + Veo 3.1 + Gemini
Dalla teoria dell'effetto "la camera entra dentro la scena" fino a una Claude Skill che genera l'intero sito — assets inclusi — con un singolo prompt, e a due build lanciate in parallelo. Codice reale, non slide.
00TL;DR & cosa costruisci
Uno scroll‑telling website (o sito immersivo) è una pagina in cui lo scroll non scorre del testo: pilota una "camera" che entra dentro una scena 3D — una porta che si apre, un treno in stazione, un fiore che si illumina — mentre il testo appare in sincronia. L'effetto 3D è in realtà una sequenza di immagini renderizzata a "scrub" sullo scroll, identica alla tecnica delle landing Apple.
La parte nuova: gli asset li genera l'AI (Gemini per i fotogrammi, Veo 3.1 per le clip) e l'intero sito lo assembla una Claude Skill con un prompt solo — risolvendo da sola i tre problemi che rendono questi siti di solito lenti e buggy.
- Il motore di scroll fluido (Lenis) + lo scrub dell'image‑sequence su
<canvas>con GSAP ScrollTrigger - Un text‑reveal consistente e non "saltellante", sincronizzato allo scroll
- La pipeline di asset AI: come generare N fotogrammi coerenti tra loro con Gemini e una clip con Veo 3.1
- Una Claude Skill (
SKILL.mdcompleto) che fa tutto questo one‑shot - Il dispatch parallelo per sfornare due siti diversi in contemporanea
01L'illusione: come funziona davvero
Nessun WebGL obbligatorio, nessun modello 3D. Il "3D" percepito nasce da tre layer sincronizzati sulla stessa barra di scroll:
1 · Background scrubbed
Una sequenza di 60–180 fotogrammi (la camera che avanza) disegnata su un canvas pinnato. Lo scroll non scrolla la pagina: avanza l'indice del frame.
2 · Overlay di testo
Blocchi di copy posizionati a percentuali precise della timeline, che entrano con fade + translate coerenti, mai in ordine casuale.
3 · Navigazione "diegetica"
La nav (RAIL · CABIN · DINING) fa parte della scena: cliccando, scrolli a un certo frame, non cambi pagina.
4 · Smooth scroll
Lo scroll nativo è a scatti. Un motore di inertial smoothing (Lenis) rende il movimento della camera continuo e cinematografico.
Un <video> non si lascia "scrubbare" frame‑per‑frame in modo affidabile cross‑browser (seek lento, decode a scatti su mobile). Una sequenza di JPG/WebP pre‑caricati e disegnati su canvas ti dà controllo esatto su quale frame mostrare ad ogni pixel di scroll. È il motivo per cui la skill genera immagini sequenziali, non solo un filmato.
02Stack tecnico
| Layer | Strumento | Perché |
|---|---|---|
| Smooth scroll | lenis (studio‑freight) | Inerzia configurabile, si aggancia a ScrollTrigger via raf |
| Scroll animation | gsap + ScrollTrigger | Pin, scrub, timeline; lo standard de‑facto |
| Rendering camera | <canvas> 2D | Disegna l'image‑sequence, zero dipendenze 3D |
| Immagini fotogramma | Gemini (Imagen / "Nano Banana") | Genera frame coerenti per soggetto e luce |
| Clip video | Veo 3.1 | Loop ambientali, hero in background |
| Asset di prodotto | Higgsfield (opz.) | Render product‑shot da immagine di partenza |
| Build | Vite (o file statico) | Dev server + bundling; per un demo basta un singolo HTML |
| Orchestrazione | Claude Code + Skill | Genera prompt, chiama le API, assembla il sito |
Beggiato sottolinea che Google è oggi sottovalutato per i siti immersivi: con l'API di Gemini per le immagini e Veo 3.1 per il video copri l'intera pipeline visiva senza uscire dall'ecosistema Google. Higgsfield resta utile quando parti da un'immagine reale di prodotto da animare.
03Pipeline asset AI
Il punto critico non è "fare una bella immagine": è fare N immagini che sembrino lo stesso mondo, in progressione logica (la camera che avanza). Tre regole:
- Blocca il soggetto e l'illuminazione. Un solo seed/stile, e un prompt che descrive la stessa scena con la camera che avanza di step costanti ("dolly‑in 5%").
- Genera in sequenza, non in parallelo per i frame di una stessa scena: ogni frame referenzia il precedente per continuità (image‑to‑image), così la camera non "salta".
- Esporta numerato e zero‑padded (
frame_001.webp…frame_120.webp) — il canvas li indicizza per numero.
Generare i frame con Gemini (REST)
# Genera un frame con il modello immagini di Gemini.
# Per una camera "dolly-in", varia solo la clausola di camera mantenendo soggetto+luce.
curl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contents":[{"parts":[{
"text":"Cinematic interior of a grand glass train station at golden hour,
high-speed train at platform, volumetric light, ultra-detailed,
photoreal. CAMERA: slow dolly-in, step 14 of 120, 5% closer than previous frame.
Consistent subject, lighting and color grade across the sequence."
}]}],
"generationConfig":{"responseModalities":["IMAGE"]}
}' | jq -r '.candidates[0].content.parts[0].inlineData.data' | base64 -d > frame_014.webp
Le API Google/OpenAI da Python su macOS python.org possono fallire con CERTIFICATE_VERIFY_FAILED: usa il bundle certifi nel contesto SSL. E ricorda i limiti di rate/size: genera i frame a batch con backoff, non tutti insieme.
Una clip ambientale con Veo 3.1
# Veo genera una breve clip (loop di sfondo o hero). Operazione asincrona: si fa polling.
POST https://generativelanguage.googleapis.com/v1beta/models/veo-3.1:predictLongRunning
# body: { "instances":[{ "prompt":"camera glides through a blooming flower field,
# sunrise, dreamy, seamless loop" }], "parameters":{"durationSeconds":6} }
# → ritorna un operation name; fai polling finché done:true, poi scarichi l'MP4.
Usa Veo per il loop di background a bassa priorità (dietro al canvas) e l'image‑sequence per l'azione scrubbata in primo piano: video per l'atmosfera, frame per il controllo.
04Smooth scroll con Lenis
Senza smoothing, lo scrub sembra a scatti. Lenis interpola lo scroll e lo passa a GSAP nello stesso loop requestAnimationFrame — un solo loop, niente jitter da loop concorrenti.
import Lenis from 'lenis'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true })
// 1 solo rAF: Lenis avanza, poi ScrollTrigger si aggiorna
lenis.on('scroll', ScrollTrigger.update)
gsap.ticker.add((time) => lenis.raf(time * 1000))
gsap.ticker.lagSmoothing(0)
lerp: 0.1 è cinematografico; alza a 0.15–0.2 se l'inerzia sembra "molle". Su mobile valuta smoothTouch:false — lo smoothing del touch nativo è già buono e quello forzato può dare lag.
05Il cuore: image‑sequence scrubbata su canvas
Questo è il pezzo che crea l'effetto "la camera entra". Si pinna una sezione alta (es. 500vh): mentre scrolli quei 5 viewport, l'indice del frame va da 0 all'ultimo, e il canvas ridisegna.
HTML
<section class="scene">
<canvas id="seq"></canvas>
</section>
<style>
.scene{ height:500vh; } /* lunghezza dello scrub */
.scene canvas{ position:sticky; top:0; width:100vw; height:100vh; object-fit:cover; }
</style>
JS — preload + scrub
const FRAMES = 120
const url = i => `/img/seq/frame_${String(i+1).padStart(3,'0')}.webp`
const canvas = document.getElementById('seq')
const ctx = canvas.getContext('2d')
// 1) Preload sequenziale e ORDINATO (continuità garantita)
const images = []
async function preload(){
for (let i=0; i<FRAMES; i++){
const img = new Image()
img.src = url(i)
await img.decode().catch(()=>{}) // decode esplicito = niente flash al primo draw
images[i] = img
}
}
function render(i){
const img = images[i | 0]; if (!img) return
// cover responsivo
const s = Math.max(canvas.width/img.width, canvas.height/img.height)
const w = img.width*s, h = img.height*s
ctx.drawImage(img, (canvas.width-w)/2, (canvas.height-h)/2, w, h)
}
function resize(){ canvas.width = innerWidth*devicePixelRatio; canvas.height = innerHeight*devicePixelRatio; render(state.f) }
addEventListener('resize', resize)
// 2) Lo scroll guida l'indice del frame
const state = { f: 0 }
preload().then(()=>{
resize()
gsap.to(state, {
f: FRAMES-1, ease: 'none', snap: 'f',
scrollTrigger:{ trigger:'.scene', start:'top top', end:'bottom bottom', scrub:0.5 },
onUpdate:()=>render(state.f)
})
})
img.decode()prima di disegnare → niente frame bianco al primo paint.scrub: 0.5→ un filo di inerzia che "insegue" lo scroll, più morbido discrub:true.devicePixelRatiosul canvas → niente immagine sfocata su display Retina.
06Text reveal consistente
Il difetto tipico dei siti "fatti dalla skill prefatta": il testo appare in modo incoerente — alcuni blocchi saltano, altri no. La regola è una sola: stesso trigger, stesso easing, stesso offset per tutti i blocchi, agganciati alla timeline della scena, non al tempo.
gsap.utils.toArray('.reveal').forEach((el)=>{
gsap.fromTo(el,
{ autoAlpha:0, y:40 },
{ autoAlpha:1, y:0, ease:'power2.out', duration:0.6,
scrollTrigger:{ trigger:el, start:'top 80%', toggleActions:'play none none reverse' }
})
})
Se vuoi che la frase appaia esattamente quando la camera arriva alla cabina, non usare top 80%: aggiungi label sulla timeline della scena e posiziona i reveal a scrub con quella, così copy e immagine sono perfettamente sincronizzati. Coerenza > sorpresa.
Lo stesso vale per le micro‑animazioni di nav e per il banner finale (quel blocco a tutta larghezza che Beggiato ama): un componente riusabile, con la stessa curva di entrata di tutto il resto del sito.
07Le 3 cose che la skill aggiusta in automatico
Il valore della skill non è "scrive HTML": è che codifica le decisioni difficili così ogni sito esce production‑grade. Sono esattamente i tre punti che, fatti a mano da template generici, "diventano un disastro: lenti, super buggy".
| # | Problema dei template generici | Cosa impone la skill |
|---|---|---|
| 1 | Fluidità: scroll lento e a scatti | Un solo loop rAF (Lenis↔GSAP), scrub con inerzia, will-change mirato, canvas a DPR corretto |
| 2 | Sequenzialità: frame che "saltano", scena incoerente | Generazione immagini in ordine logico (image‑to‑image incatenato), numerazione zero‑padded, preload ordinato |
| 3 | Reveal: testo che appare a caso | Stesso trigger/easing/offset per ogni blocco, agganciato alla timeline della scena |
La skill aggiunge anche il banner a tutta larghezza come componente firma, e generalizza il design tra temi diversi (treno, astronave, fiore) riusando lo stesso scheletro: cambi soggetto e palette, non l'architettura.
08Costruire la Claude Skill scroll‑journey
Una skill è una cartella con un SKILL.md: front‑matter (nome + descrizione che fa da trigger) e istruzioni operative. Claude la attiva quando il task corrisponde alla descrizione. Ecco uno scheletro pensato per il one‑shot.
SKILL.md nell'editor: front‑matter in cima, poi gli step che Claude esegue (download, build, generazione assets, fix fluidità/reveal).Struttura cartella
scroll-journey/
├─ SKILL.md # istruzioni + trigger
├─ scripts/
│ ├─ gen_frames.py # Gemini: N frame sequenziali
│ ├─ gen_video.py # Veo 3.1: clip ambientale
│ └─ scaffold.py # copia il template e inietta gli assets
└─ template/ # index.html + main.js (Lenis+GSAP) già cablati
SKILL.md
---
name: scroll-journey
description: >-
Genera un sito immersivo scroll-telling 3D one-shot: crea i frame con Gemini,
una clip con Veo 3.1, e assembla un template Lenis+GSAP con scroll fluido,
sequenza immagini coerente e text-reveal consistente. Usare quando l'utente
chiede un sito immersivo, scroll-telling, "camera che entra nella scena" o simili.
---
# scroll-journey
## Input
Dall'utente ricava: soggetto/scena, sezioni (nav), palette, e n. di frame (default 120).
## Step 1 — Storyboard
Scrivi un prompt-base che fissa soggetto, luce e color grade. Definisci il movimento
camera come dolly-in in N step costanti. NON cambiare lo stile tra i frame.
## Step 2 — Frame sequenziali (CRUCIALE)
Esegui scripts/gen_frames.py: genera i frame IN ORDINE, ognuno in image-to-image dal
precedente per continuità. Salva img/seq/frame_001.webp ... zero-padded.
## Step 3 — Clip ambientale
Esegui scripts/gen_video.py (Veo 3.1) per il loop di background. Polling fino a done.
## Step 4 — Scaffold
Copia template/, inietta gli assets e le sezioni. Il template ha GIÀ:
- 1 solo loop rAF (Lenis↔ScrollTrigger)
- canvas sticky a devicePixelRatio, preload+decode ordinato
- reveal con trigger/easing/offset UNIFORMI
- banner full-width finale
## Step 5 — Verifica
Apri in preview, controlla: nessun frame mancante, scroll fluido, reveal coerente.
Se un frame "salta", rigeneralo in image-to-image dal vicino. Riporta il path finale.
Metti la conoscenza non‑ovvia nel template (il loop rAF unico, il DPR, il preload ordinato) e lascia al modello solo le decisioni creative (soggetto, palette, copy). Così il rischio "buggy" è già neutralizzato a monte: il modello non re‑inventa l'engine ad ogni run.
09Dispatch parallelo: due siti insieme
Nel demo vengono lanciati due siti in parallelo (treno + astronave), pronti in 2–4 minuti. Con Claude Code li mandi in background e li lasci correre insieme — ognuno è un'invocazione indipendente della skill.
# Prompt unico alla skill, due scene:
"Usa scroll-journey e generami DUE siti in parallelo:
A) lo scroll entra dentro un treno in stazione, sezioni RAIL/CABIN/DINING;
B) lo scroll entra dentro un'astronave e ne vende i vari pezzi.
Lanciali in background, riportami i due path quando pronti."
Fai girare ogni build in una cartella/worktree separata: gli assets generati (decine di frame) non devono collidere. Se usi sub‑agent, dà a ciascuno il proprio output dir e raccogli i due path alla fine.
10Performance & gotchas
Peso degli assets
120 frame possono pesare. Usa WebP/AVIF, dimensiona al viewport reale, e considera due set (mobile/desktop) con matchMedia.
Preload progressivo
Mostra i primi 10–15 frame subito e continua a caricare il resto in background; blocca lo scrub finché non sei oltre la soglia di sicurezza.
Memoria su mobile
Tenere 120 Image decodificate satura la RAM. Riduci il frame‑count su mobile (es. 60) e/o scarica i frame fuori range.
will-change
Solo sugli elementi animati e solo durante l'animazione: abusarne aumenta il consumo GPU invece di ridurlo.
Accessibilità
Rispetta prefers-reduced-motion: in quel caso mostra un frame statico + testo scrollabile normale.
SEO
Il canvas non è indicizzabile: tieni il copy reale nel DOM (i blocchi .reveal), non dentro le immagini.
Far girare due loop rAF (uno di Lenis, uno tuo) è la causa #1 di scroll a scatti. Un solo gsap.ticker che pilota lenis.raf(), come nello snippet del §04. Mai due.
11Dal demo al business
Beggiato chiude con l'osservazione più importante per chi vende questi siti: ora che generarli è quasi una commodity, il valore non è l'animazione — è il risultato di business. La metafora della vetrina:
Domanda 1 — Traffico
«Con una vetrina di un tipo o di un altro, entrano più clienti nel negozio?» → il sito porta effettivamente visite/lead qualificati?
Domanda 2 — Conversione
«Una volta entrati, qual è la disposizione ottimale degli oggetti perché si convertano in clienti paganti?» → CTA, gerarchia, frizione.
- Strumenta tutto: scroll‑depth, tempo sulla scena, click sulle CTA diegetiche.
- A/B testa la prima scena (la "vetrina"): è ciò che decide se restano.
- L'immersività deve servire il messaggio, non sostituirlo: una CTA chiara batte un frame in più.
- Vendi l'esito (più lead, più conversioni), non "il sito 3D". Il 3D è il mezzo.
12Checklist & risorse
Checklist di build
- ☐ Lenis + GSAP su un solo loop rAF
- ☐ Canvas sticky,
devicePixelRatio,object-fit:covervia drawImage - ☐ Frame zero‑padded, generati in ordine (image‑to‑image)
- ☐ Preload +
decode()prima del primo render; scrub bloccato finché non pronto - ☐ Reveal con trigger/easing/offset uniformi
- ☐ Set asset ridotto per mobile +
prefers-reduced-motion - ☐ Copy reale nel DOM (SEO/accessibilità)
- ☐ Banner finale come componente riusabile
- ☐ Analytics di scroll‑depth e CTA
Stack
lenisgsapScrollTrigger Gemini image APIVeo 3.1Higgsfield Claude Code + SkillViteWebP/AVIF
Crea la cartella scroll-journey/ con il SKILL.md del §08 e un template/ che incorpora gli snippet dei §04–06. Dopodiché un singolo prompt — "entra in un treno, sezioni RAIL/CABIN/DINING" — ti produce il sito completo, assets inclusi.