internettoolbox
← Torna agli strumenti

Guida

Estrarre i frame da un video AI per un'animazione canvas allo scroll

Un'animazione canvas guidata dallo scroll è una sequenza di immagini numerate più una sessantina di righe di JavaScript. Questa guida prende una clip da Sora, Runway, Kling o Veo (o qualsiasi MP4, WebM, GIF o WebP animato), la trasforma nei file da frame_0001.jpg a frame_0240.jpg e collega quei frame alla posizione di scroll, come fanno le pagine prodotto di Apple.

Cos'è un'animazione canvas allo scroll, e perché servono i frame invece di un tag video

In una pagina prodotto di Apple l'immagine principale non parte da sola: segue lo scroll. Scorri in basso e il quadrante di un orologio ruota, un iPhone si apre, un paio di cuffie gira su se stesso, e il movimento segue esattamente il tuo dito. Torni su e va all'indietro alla stessa velocità. Nella pagina non c'è nessun elemento video. C'è un <canvas>, qualche centinaio di JPG già renderizzati e un gestore dello scroll che sceglie quale disegnare.

L'alternativa più ovvia, impostare video.currentTime in base alla posizione di scroll, nella pratica non regge. Un seek su un video compresso deve trovare il keyframe più vicino e decodificare in avanti da lì, quindi il browser risponde alla maggior parte dei seek in ritardo e fuori ordine. Il risultato sono blocchi su iOS, una coda di decodifica che resta un secondo intero indietro rispetto alla barra di scorrimento e frame persi appena due seek cadono nello stesso tick. I codec video sono fatti per riprodurre in avanti a velocità costante, non per essere interrogati in punti casuali 60 volte al secondo.

Una sequenza di frame non ha nessuno di questi problemi. Il frame 137 è una bitmap già decodificata in memoria, e disegnarla è una sola chiamata a drawImage che costa molto meno di un millisecondo. Il passaggio dalla posizione di scroll all'indice del frame è deterministico: lo stesso scroll disegna sempre la stessa immagine, in ogni browser e in entrambe le direzioni. Lo paghi in byte, ed è per questo che gran parte di questa guida parla di come tenere leggera la sequenza.

Passo 1: ottieni una clip pulita da Sora, Runway, Kling o Veo

La sequenza vale quanto la clip di partenza, e i vincoli sono più stretti di quelli di un video da riprodurre e basta.

  • Durata: da 3 a 8 secondi. Le animazioni allo scroll sono brevi per natura, perché chi visita la pagina deve scorrerle tutte. Otto secondi distribuiti su 500vh di pagina sono già una sezione fissata lunga. La maggior parte dei generatori AI crea clip da 5 o 10 secondi per impostazione predefinita, quindi una clip da 5 secondi non va nemmeno tagliata.
  • Frame rate costante. Alcune esportazioni AI hanno un frame rate variabile, per cui il frame N cade in un istante imprevedibile. Chiedere all'estrattore 24 o 30 fps fissi ricampiona la clip su una griglia regolare, che è proprio ciò che presuppone il passaggio lineare da scroll a indice.
  • Risoluzione: genera in grande, esporta a 1920. Genera alla risoluzione più alta che offre il modello, poi lascia che sia l'estrattore a ridimensionare. 1920 px di larghezza è il valore predefinito e corrisponde a quello che usa Apple (le sue sequenze stanno tra 1500 e 2000 px). Andare oltre appesantisce il download senza un guadagno visibile su nessuno schermo che la mostrerà in apertura di pagina.
  • 24 o 30 fps bastano. I modelli AI generano a 24 o 30 fps, quindi sopra non c'è niente da catturare. Chiedere 60 fps a una sorgente a 30 fps ti dà 60 file al secondo di cui uno su due è un duplicato: il doppio del peso, zero movimento in più.
  • Tieni ferma la camera o il soggetto. Una sequenza si legge come un unico oggetto continuo quando la camera o il soggetto restano fermi. Le clip in cui si muovono entrambi sembrano un video mandato avanti e indietro, che è proprio l'effetto da evitare.

Passo 2: estrai i frame in una sequenza numerata

Trascina la clip nell'estrattore di questo sito. Decodifica in locale con WebCodecs, quindi il file non lascia mai il tuo computer, e ti restituisce uno ZIP di immagini numerate.

Apri il Video Frame Extractor →Entra un MP4, WebM, MOV, GIF o WebP animato, esce uno ZIP da frame_0001.jpg in poi. Funziona nel browser, nessun file caricato.

Le cinque impostazioni che contano, e come regolarle:

  • FPS (cursore, da 1 a 60, predefinito 24). Metti 24 per un effetto cinematografico, 30 quando il movimento è così rapido che a 24 sembra a scatti. È di gran lunga la leva che pesa di più sul totale.
  • Larghezza di output (originale, 1920, 1440, 1080, 720 o un valore personalizzato). 1920 per un'apertura di pagina su desktop, 1080 o 720 per una sequenza solo mobile. Le proporzioni restano quelle originali.
  • Formato (JPG o WebP). WebP per il web, JPG quando qualcosa più avanti nella catena lo richiede.
  • Qualità (cursore, da 0,10 a 1,00, predefinita 0,85). 0,85 è il punto di equilibrio per le immagini fotografiche. Dato che ogni frame resta sullo schermo per una frazione di secondo, di solito puoi scendere a 0,75 prima che qualcuno se ne accorga.
  • Output ZIP e nomi dei file. Un solo download, frames.zip, che contiene frame_0001.jpg, frame_0002.jpg e così via, con gli zeri iniziali fino a quattro cifre. È questo riempimento che permette al ciclo qui sotto di costruire un URL a partire da un indice con String(i + 1).padStart(4, "0").

Prima di avviare l'estrazione lo strumento mostra il numero di frame che otterrai e una stima del peso dello ZIP. Per una clip di 8 secondi sono 192 frame a 24 fps o 240 a 30 fps, lo stesso ordine di grandezza della sequenza degli AirPods Max di Apple. Lo strumento ti avvisa sopra i 1.000 frame e si rifiuta oltre i 2.500, perché JSZip tiene tutto in memoria finché non scarichi.

Decomprimi in public/frames/ (o nella tua cartella dei file statici) e la parte video è fatta.

Passo 3: HTML, CSS e JavaScript per un canvas guidato dallo scroll

Tre pezzi: un contenitore alto che fornisce la distanza di scroll, una traccia sticky che tiene fermo il canvas al suo interno e un gestore che trasforma la posizione del contenitore nella finestra in un indice di frame.

index.html

<section class="scroll-stage">
  <div class="scroll-track">
    <canvas id="hero" class="scroll-canvas"></canvas>
  </div>
</section>

style.css

/* 500vh of scroll drives 100vh of sticky canvas. */
.scroll-stage {
  position: relative;
  height: 500vh;
}

.scroll-track {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

.scroll-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* No scroll hijacking for anyone who asked for less motion:
   the stage collapses to one screen and shows a single frame. */
@media (prefers-reduced-motion: reduce) {
  .scroll-stage { height: 100vh; }
}

scroll-frames.js

const FRAME_COUNT = 240;               // 8 s of source at 30 fps
const PRELOAD = 20;                    // frames that must decode before first paint

const stage = document.querySelector(".scroll-stage");
const canvas = document.getElementById("hero");
const ctx = canvas.getContext("2d", { alpha: false });

const images = new Array(FRAME_COUNT);

function frameUrl(i) {
  // Matches the extractor's output: frame_0001.jpg ... frame_0240.jpg
  return `/frames/frame_${String(i + 1).padStart(4, "0")}.jpg`;
}

function load(i) {
  if (!images[i]) {
    const img = new Image();
    img.src = frameUrl(i);
    images[i] = img;
  }
  return images[i];
}

// Match the backing store to the CSS box at the device pixel ratio.
// Capped at 2: a 3x phone would quadruple the fill cost for nothing.
function resize() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.round(rect.width * dpr);
  canvas.height = Math.round(rect.height * dpr);
  draw(current);
}

// cover-fit: fill the canvas, crop the overflow, never stretch.
function draw(i) {
  const img = images[i];
  if (!img || !img.complete || img.naturalWidth === 0) return;
  const scale = Math.max(
    canvas.width / img.naturalWidth,
    canvas.height / img.naturalHeight,
  );
  const w = img.naturalWidth * scale;
  const h = img.naturalHeight * scale;
  ctx.drawImage(img, (canvas.width - w) / 2, (canvas.height - h) / 2, w, h);
}

let current = 0;
let ticking = false;

function frameIndexFromScroll() {
  const rect = stage.getBoundingClientRect();
  const scrollable = rect.height - window.innerHeight;
  const progress = scrollable > 0 ? -rect.top / scrollable : 0;
  const clamped = Math.min(Math.max(progress, 0), 1);
  return Math.round(clamped * (FRAME_COUNT - 1));
}

// One draw per animation frame, never one per scroll event.
function onScroll() {
  if (ticking) return;
  ticking = true;
  requestAnimationFrame(() => {
    const next = frameIndexFromScroll();
    if (next !== current) {
      current = next;
      draw(current);
    }
    ticking = false;
  });
}

async function start() {
  await Promise.all(
    Array.from({ length: PRELOAD }, (_, i) =>
      load(i).decode().catch(() => {}),
    ),
  );

  current = frameIndexFromScroll();   // survives a reload mid-page
  resize();

  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
    draw(0);
    return;                           // static poster frame, no scroll handler
  }

  for (let i = PRELOAD; i < FRAME_COUNT; i++) load(i);
  window.addEventListener("scroll", onScroll, { passive: true });
  window.addEventListener("resize", resize);
}

start();

Qui dentro ci sono quattro dettagli che di solito si sbagliano. Il throttle: l'evento scroll scatta molto più spesso di quanto lo schermo si aggiorni, quindi il gestore imposta un flag e fa il lavoro vero dentro requestAnimationFrame, al massimo una volta per frame disegnato. L'adattamento a riempimento: drawImage con quattro argomenti di destinazione deforma il frame senza avvisarti, quindi la scala è il maggiore dei due rapporti e il risultato viene centrato. L'indice iniziale: leggere la posizione di scroll prima del primo disegno evita che la sequenza torni al frame 1 quando qualcuno ricarica la pagina a metà. Il ramo per il movimento ridotto: con prefers-reduced-motion: reduce il contenitore si riduce a una sola schermata e la pagina disegna un unico frame, senza nessun gestore dello scroll collegato.

E CSS scroll-timeline?

animation-timeline: scroll() e view() collegano un'animazione CSS alla posizione di scroll senza JavaScript, e sono lo strumento giusto per testi fissati, livelli in parallasse ed effetti di comparsa all'ingresso:

@keyframes rise {
  from { opacity: 0; transform: translateY(40px); }
  to   { opacity: 1; transform: none; }
}

.caption {
  animation: rise linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

Però non sostituiscono il canvas. Cambiare la bitmap disegnata non è una proprietà CSS animabile, quindi una sequenza di frame ha comunque bisogno del gestore dello scroll qui sopra. Usali insieme: timeline CSS per le didascalie che scorrono con la pagina, JavaScript per i frame.

Passo 4: prestazioni, ovvero come tenere una sequenza di frame sotto pochi megabyte

  • WebP invece di JPG. Dal 25 al 35% più leggero a parità di qualità percepita. Su 240 frame a 1920 px sono circa 8 MB invece di 12 MB, cambiando una sola riga in frameUrl().
  • Il numero di frame conta più della qualità. Dimezzare i frame dimezza i byte e costa fluidità solo sotto i 24 fps. Scendere di qualità da 0,85 a 0,60 fa risparmiare meno e si vede come una poltiglia sull'immagine ferma. Accorcia la clip e abbassa gli fps per prima cosa.
  • Decodifica prima di disegnare. img.decode() si risolve quando la bitmap è davvero pronta. Attenderlo sul primo gruppo fa sì che il primo drawImage trovi un'immagine decodificata invece di non disegnare nulla in silenzio, che è la causa più comune di un'apertura di pagina vuota al caricamento.
  • Precaricane 20, poi carica il resto in streaming. Venti frame sono circa un secondo di scroll, abbastanza per partire. Avviare subito dopo le richieste rimanenti tiene occupata la rete senza bloccare il primo disegno. Non aspettare tutti i 240.
  • Servi i frame da una CDN con una cache lunga. La sequenza è fatta di 240 file immutabili richiesti tutti insieme. Metti un hash nel nome della cartella, imposta Cache-Control: public, max-age=31536000, immutable e mettila dietro una CDN, così le richieste partono in parallelo e vicino all'utente. Qui HTTP/2 o HTTP/3 fanno la differenza: 240 richieste su HTTP/1.1 fanno la coda sei alla volta.
  • Pubblica una sequenza più piccola per mobile. Esporta un secondo passaggio a 720 o 1080 px e scegli la cartella in base a window.innerWidth prima che parta il precaricamento.

Problemi comuni con le sequenze di frame per animazioni allo scroll

  • I frame sembrano sfocati o morbidi. Il buffer del canvas è in pixel CSS mentre lo schermo è a 2x o 3x. Imposta canvas.width e canvas.height al rettangolo dell'elemento moltiplicato per devicePixelRatio (con un massimo di 2) e lascia che il CSS tenga l'elemento al 100%, esattamente come fa resize() qui sopra. Ridimensionare il canvas lo svuota, quindi ridisegna subito dopo.
  • L'animazione sfarfalla o si svuota durante lo scroll. Stai disegnando frame non ancora decodificati. Un Image incompleto non disegna proprio nulla. Controlla img.complete, attendi decode() sul gruppo precaricato e avvia il caricamento in background abbastanza presto perché uno scroll rapido non lo superi.
  • Salta al caricamento. L'indice iniziale era fisso a 0 mentre il browser ripristinava una posizione di scroll a metà del contenitore. Calcola l'indice dallo scroll attuale prima del primo disegno.
  • La pagina pesa 40 MB. Troppi frame, troppo larghi o con qualità troppo alta. Una clip di 5 secondi a 24 fps, 1920 px, qualità 0,85 in WebP pesa circa da 4 a 6 MB. Se sei molto sopra, estrai di nuovo con meno fps prima di toccare qualsiasi altra cosa.
  • L'ultimo frame non compare mai. Il contenitore è più basso di height + 100vh, quindi l'avanzamento dello scroll non arriva mai a 1. Dai al contenitore un'altezza sufficiente perché rect.height - window.innerHeight sia una distanza comoda: 500vh per 240 frame è un buon punto di partenza.

Domande frequenti

Quanti frame servono per un'animazione allo scroll?

Circa 240 per 8 secondi di scroll a 30 fps, oppure 192 a 24 fps. La pagina degli AirPods Max di Apple usa più o meno 240 frame; la sequenza dell'iPhone che si apre è a 24 fps. Sotto i 24 fps il movimento scatta quando qualcuno scorre piano, sopra i 30 fps raddoppi il download per un movimento che nessuno nota. Scegli prima la durata della clip, moltiplicala per gli fps e hai il numero di frame.

Posso usare un video di Sora o Runway?

Sì. Sora, Runway, Kling, Veo e Pika esportano tutti in MP4 o WebM, che è proprio quello che legge l'estrattore. Le clip AI sono brevi (di solito da 4 a 10 secondi) e a volte hanno un frame rate insolito o variabile: imposta gli fps di destinazione a 24 o 30 e l'estrattore ricampiona la clip a quel valore.

Meglio JPG o WebP per una sequenza di frame?

WebP, a meno che tu non abbia un motivo per evitarlo. A parità di qualità percepita è più leggero del JPG del 25–35% e lo decodificano tutti i browser usciti dal 2020 in poi. Su una sequenza di 240 frame è la differenza tra circa 12 MB e 8 MB. Usa il JPG se devi supportare versioni molto vecchie di Safari o se i frame finiscono anche in una pipeline datata.

Un'animazione canvas allo scroll funziona su mobile?

Sì, ma dimezza il budget. Servi una seconda sequenza larga 720 o 1080 px e limita il device pixel ratio a 2, altrimenti il telefono scarica frame da desktop su una connessione mobile. Safari su iOS genera eventi di scroll anche durante lo scorrimento per inerzia, quindi il throttle con requestAnimationFrame dell'esempio in questa guida è ciò che tiene stabile il frame rate, non un'ottimizzazione facoltativa.

Mi servono GSAP o ScrollTrigger?

No. Bastano circa 60 righe di JavaScript puro: un canvas sticky, un array di immagini precaricate e un gestore dello scroll che converte l'avanzamento in un indice di frame. GSAP ScrollTrigger ha senso quando devi coordinare più sezioni fissate tra loro, non per una singola sequenza di frame.

Il video viene caricato da qualche parte?

No. L'estrattore decodifica, ridimensiona e comprime in ZIP i frame dentro la scheda del browser, con WebCodecs e JSZip. Apri DevTools e guarda la scheda Network mentre avvii l'estrazione: non parte nessuna richiesta. La clip resta sul tuo computer.

Perché la mia animazione canvas sfarfalla durante lo scroll?

Perché il frame che stai disegnando non è ancora decodificato. Un elemento Image ancora in caricamento non disegna nulla, quindi il canvas mostra quello che c'era prima oppure resta vuoto. Attendi decode() sul primo gruppo di frame prima del primo disegno e continua a caricare il resto della sequenza in background, così uno scroll veloce non supera mai la rete.

Posso farlo con CSS scroll-timeline?

Solo per le parti che puoi esprimere come animazione CSS. animation-timeline: scroll() e view() animano trasformazioni, opacità e colore in base alla posizione di scroll senza JavaScript, e coprono testi fissati e parallax. Cambiare l'immagine disegnata in un canvas non è una proprietà CSS animabile, quindi una sequenza di frame ha comunque bisogno del gestore dello scroll descritto in questa guida.

Strumenti correlati

Cambia strumento

Cerca e apri qualsiasi strumento