Complessità di orchestrazione con l'API di animazioni Web
Pubblicato: 2022-03-10Non c'è via di mezzo tra semplici transizioni e animazioni complesse. O stai bene con ciò che forniscono le transizioni e le animazioni CSS o hai improvvisamente bisogno di tutta la potenza che puoi ottenere. L'API Animazioni Web ti offre molti strumenti per lavorare con le animazioni. Ma devi sapere come gestirli. Questo articolo ti guiderà attraverso i punti e le tecniche principali che potrebbero aiutarti a gestire animazioni complesse rimanendo flessibile.
Prima di approfondire l'articolo, è fondamentale che tu abbia familiarità con le basi dell'API Web Animations e JavaScript. Per chiarire ed evitare distrazioni dal problema in questione, gli esempi di codice forniti sono semplici. Non ci sarà niente di più complesso di funzioni e oggetti. Come buoni punti di ingresso nelle animazioni stesse, suggerirei MDN come riferimento generale, l'eccellente serie di Daniel C. Wilson e l'API CSS Animations vs Web Animations di Ollie Williams. Non esamineremo i modi per definire gli effetti e metterli a punto per ottenere il risultato desiderato. Questo articolo presuppone che le animazioni siano definite e che siano necessarie idee e tecniche per gestirle.
Iniziamo con una panoramica delle interfacce e a cosa servono. Quindi esamineremo i tempi e i livelli di controllo per definire cosa, quando e per quanto tempo. Successivamente, impareremo come trattare diverse animazioni come una sola avvolgendole in oggetti. Sarebbe un buon inizio per utilizzare l'API Web Animations.
Interfacce
L'API Web Animations ci offre una nuova dimensione di controllo. Prima di allora, le transizioni e l'animazione CSS, pur fornendo un modo efficace per definire gli effetti, avevano ancora un unico punto di attivazione . Come un interruttore della luce, era acceso o spento. Potresti giocare con ritardi e funzioni di allentamento per creare effetti piuttosto complessi. Tuttavia, a un certo punto, diventa ingombrante e difficile da lavorare.
L'API Web Animations trasforma questo singolo punto di attivazione in un controllo completo sulla riproduzione . L'interruttore della luce si trasforma in un interruttore dimmer con un cursore. Se lo desideri, puoi trasformarlo nell'intera cosa della casa intelligente, perché oltre al controllo della riproduzione ora puoi definire e modificare gli effetti in fase di esecuzione. Ora puoi adattare gli effetti al contesto o implementare un editor di animazioni con anteprima in tempo reale.
Iniziamo con l'interfaccia Animazione. Per ottenere un oggetto di animazione, possiamo usare il metodo Element.animate
. Gli dai fotogrammi chiave e opzioni e riproduce immediatamente la tua animazione. Quello che fa anche è restituire un'istanza dell'oggetto Animation
. Il suo scopo è controllare la riproduzione.
Pensalo come un lettore di cassette , se ricordi questi. Sono consapevole che alcuni dei lettori potrebbero non avere familiarità con ciò che è. È inevitabile che qualsiasi tentativo di applicare concetti del mondo reale per descrivere cose informatiche astratte vada in pezzi rapidamente. Ma lascia che ti rassicuri - un lettore che non conosce la gioia di riavvolgere un nastro con una matita - che le persone che sanno cos'è un lettore di cassette saranno confuse ancora di più entro la fine di questo articolo.
Immagina una scatola. Ha uno slot dove va la cassetta e ha pulsanti per riprodurre, fermare e riavvolgere. Ecco cos'è l'istanza dell'interfaccia di animazione: una casella che contiene un'animazione definita e fornisce modi per interagire con la sua riproduzione. Gli dai qualcosa da suonare e ti restituisce i controlli.
I controlli che ottieni sono convenientemente simili a quelli che ottieni dagli elementi audio e video. Sono metodi di riproduzione e pausa e la proprietà dell'ora corrente . Con questi tre controlli, puoi creare qualsiasi cosa quando si tratta di riproduzione.
La cassetta stessa è un pacchetto che contiene un riferimento all'elemento animato, la definizione degli effetti e le opzioni che includono il tempo, tra le altre cose. Ed è questo che è il KeyframeEffect
. La nostra cassetta è qualcosa che contiene tutte le registrazioni e le informazioni sulla durata delle registrazioni. Lascerò all'immaginazione del pubblico più anziano l'abbinamento di tutte queste proprietà con i componenti di una cassetta fisica. Quello che ti mostrerò è come appare nel codice.
Quando crei un'animazione tramite Element.animate
, stai usando una scorciatoia che fa tre cose. Crea un'istanza KeyframeEffect
. Si inserisce in una nuova istanza di Animation
. Inizia subito a riprodurlo.
const animation = element.animate(keyframes, options);
Analizziamolo e vediamo il codice equivalente che fa la stessa cosa.
const animation = new Animation( // (2) new KeyframeEffect(element, keyframes, options) // (1) ); animation.play(); (3)
Prendi la cassetta (1), inseriscila in un lettore (2), quindi premi il pulsante Play (3).
Il punto di sapere come funziona dietro le quinte è poter separare la definizione dei fotogrammi chiave e decidere quando riprodurli. Quando hai molte animazioni da coordinare, potrebbe essere utile raccoglierle tutte prima in modo da sapere che sono pronte per giocare. Generarli al volo e sperare che inizino a suonare al momento giusto non è qualcosa che vorresti sperare. È troppo facile interrompere l'effetto desiderato trascinando alcuni fotogrammi. In caso di una lunga sequenza si accumula il trascinamento risultando in un'esperienza per nulla convincente.
Tempi
Come nella commedia, il tempismo è tutto nelle animazioni. Per far funzionare un effetto, per ottenere una certa sensazione è necessario essere in grado di mettere a punto il modo in cui le proprietà cambiano. Ci sono due livelli di tempo che puoi controllare nell'API Web Animations.
A livello di singole proprietà, abbiamo offset
. L'offset ti dà il controllo sulla tempistica di una singola proprietà . Dandogli un valore da zero a uno, definisci quando si attiva ogni effetto. Quando omesso è uguale a zero.
Potresti ricordare da @keyframes
in CSS come puoi usare le percentuali invece di from
/ to
. Ecco cos'è l' offset
, ma diviso per cento. Il valore di offset
è una parte della durata di una singola iterazione .
L' offset
consente di disporre i fotogrammi chiave all'interno di un KeyframeEffect
. Essendo un numero di offset relativo assicura che, indipendentemente dalla durata o dalla velocità di riproduzione, tutti i fotogrammi chiave inizino nello stesso momento l'uno rispetto all'altro.
Come abbiamo affermato in precedenza, offset
è una parte della durata . Ora voglio che tu eviti i miei errori e la perdita di tempo su questo. È importante capire che la durata dell'animazione non è la stessa cosa della durata complessiva di un'animazione. Di solito sono la stessa cosa e questo è ciò che potrebbe confonderti e ciò che sicuramente ha confuso me.
La durata è la quantità di tempo in millisecondi necessaria per terminare un'iterazione. Sarà uguale alla durata complessiva per impostazione predefinita. Una volta aggiunto un ritardo o aumentato il numero di iterazioni in un'animazione, la durata smette di dirti il numero che vuoi sapere. Questo è importante da capire per usarlo a tuo vantaggio.
Quando è necessario coordinare la riproduzione di un fotogramma chiave in un contesto più ampio, come la riproduzione multimediale, è necessario utilizzare le opzioni di temporizzazione. L'intera durata dell'animazione dall'inizio all'evento "finito" nella seguente equazione:
delay + (iterations × duration) + end delay
Puoi vederlo in azione nella seguente demo:
Ciò che ci consente di fare è allineare diverse animazioni nel contesto di media a lunghezza fissa. Mantenendo intatta la durata desiderata dell'animazione si potrebbe “tamponare” con delay
all'inizio e delayEnd
alla fine per inserirla in un contesto con una durata maggiore. Se ci pensi, il delay
in questo senso agirebbe come l'offset nei fotogrammi chiave. Ricorda solo che il ritardo è impostato in millisecondi, quindi potresti volerlo convertire in un valore relativo.
Un'altra opzione di temporizzazione che aiuterebbe ad allineare l'animazione è iterationStart
. Imposta la posizione iniziale di un'iterazione. Partecipa alla demo della palla da biliardo. Regolando il cursore iterationStart
puoi impostare la posizione iniziale della pallina e la rotazione, ad esempio, puoi impostarla per iniziare a saltare dal centro dello schermo e fare in modo che il numero sia dritto nella fotocamera nell'ultimo fotogramma.
Controlla diversi come uno
Quando ho lavorato all'editor di animazioni per un'app di presentazione, ho dovuto organizzare diverse animazioni per un singolo elemento su una sequenza temporale. Il mio primo tentativo è stato quello di utilizzare l' offset
per posizionare la mia animazione al punto di partenza corretto su una timeline.
Questo si è rapidamente rivelato il modo sbagliato di usare l' offset
. In termini di questa particolare interfaccia utente in movimento, l'animazione sulla timeline significava spostare la sua posizione iniziale senza modificare la durata dell'animazione. Con offset
ciò significava che dovevo cambiare diverse cose, l' offset
stesso e anche modificare l' offset
della proprietà di chiusura per assicurarmi che la durata non cambiasse. La soluzione si è rivelata troppo complessa per essere compresa.
Il secondo problema è venuto con la proprietà transform
. A causa del fatto che può rappresentare diversi cambiamenti caratteristici di un elemento, può diventare complicato farlo fare ciò che vuoi. In caso di desiderio di modificare quelle proprietà indipendentemente l'una dall'altra, potrebbe diventare ancora più difficile. La funzione di cambio scala influenza tutte le funzioni che la seguono. Ecco perché succede.
La proprietà di trasformazione può assumere diverse funzioni in una sequenza come valore. A seconda dell'ordine della funzione, il risultato cambia. Prendi scale
e translate
. A volte è utile definire la translate
in percentuale, che significa relativa alla dimensione di un elemento. Supponiamo che tu voglia che una palla salti esattamente tre diametri di altezza. Ora, a seconda di dove si posiziona la funzione di scala, prima o dopo la translate
, il risultato cambia da tre altezze della dimensione originale o di quella ridimensionata.
È un tratto importante della proprietà di transform
. Ne hai bisogno per ottenere una trasformazione piuttosto complessa. Ma quando hai bisogno che quelle trasformazioni siano distinte e indipendenti da altre trasformazioni di un elemento, questo ti intralcia.
Ci sono casi in cui non puoi mettere tutti gli effetti in una proprietà di transform
. Può diventare troppo abbastanza rapidamente. Soprattutto se i fotogrammi chiave provengono da luoghi diversi, è necessario disporre di una fusione molto complessa di una stringa trasformata . Difficilmente potresti fare affidamento su un meccanismo automatico perché la logica non è semplice. Inoltre, potrebbe essere difficile capire cosa aspettarsi. Per semplificare e mantenere la flessibilità, dobbiamo separarli in canali diversi.
Una soluzione è avvolgere i nostri elementi in div
che possono essere animati separatamente, ad esempio un div per il posizionamento sulla tela, un altro per il ridimensionamento e un terzo per la rotazione. In questo modo, non solo semplificherai enormemente la definizione delle animazioni, ma aprirai anche la possibilità di definire diverse origini di trasformazione, ove applicabile.
Potrebbe sembrare che le cose sfuggano al controllo con quel trucco. Che stiamo moltiplicando il numero di problemi che abbiamo avuto prima. In effetti, quando ho trovato questo trucco per la prima volta, l'ho scartato perché troppo. Ho pensato che potevo semplicemente assicurarmi che la mia proprietà di transform
fosse compilata da tutti i pezzi nell'ordine giusto in un unico pezzo. Ci voleva un'altra funzione di transform
per rendere le cose troppo complesse da gestire e alcune cose impossibili da fare. Il mio compilatore di stringhe di proprietà di transform
ha iniziato a impiegare sempre più tempo per funzionare correttamente, quindi ho rinunciato.
Si è scoperto che controllare la riproduzione di diverse animazioni non è così difficile come sembra inizialmente. Ricordi l'analogia del lettore di cassette dall'inizio? E se potessi usare il tuo lettore che accetta un numero qualsiasi di cassette? Inoltre, puoi aggiungere tutti i pulsanti che desideri su quel lettore.
L'unica differenza tra la chiamata di play
su una singola animazione e una serie di animazioni è che è necessario eseguire l'iterazione. Ecco il codice che puoi utilizzare per qualsiasi metodo di istanze di Animation
:
// To play just call play on all of them animations.forEach((animation) => animation.play());
Lo useremo per creare tutti i tipi di funzioni per il nostro lettore.
Creiamo quella scatola che conterrà le animazioni e le riprodurrà. Puoi creare quelle scatole in qualsiasi modo sia adatto. Per chiarire, ti mostrerò un esempio di come farlo con una funzione e un oggetto. La funzione createPlayer
accetta una serie di animazioni che devono essere riprodotte in sincronia. Restituisce un oggetto con un unico metodo di play
.
function createPlayer(animations) { return Object.freeze({ play: function () { animations.forEach((animation) => animation.play()); } }); }
Questo è sufficiente per te per iniziare a espandere la funzionalità. Aggiungiamo i metodi pause e currentTime
.
function createPlayer(animations) { return Object.freeze({ play: function () { animations.forEach((animation) => animation.play()); }, pause: function () { animations.forEach((animation) => animation.pause()); }, currentTime: function (time = 0) { animations.forEach((animation) => animation.currentTime = time); } }); }
Il createPlayer
con questi tre metodi ti offre un controllo sufficiente per orchestrare un numero qualsiasi di animazioni . Ma spingiamolo un po' oltre. Facciamo in modo che il nostro lettore possa accettare non solo un numero qualsiasi di cassette, ma anche altri lettori.
Come abbiamo visto in precedenza, l'interfaccia di Animation
è simile alle interfacce multimediali. Usando quella somiglianza potresti mettere tutti i tipi di cose nel tuo lettore. Per adattarlo, modifichiamo il metodo currentTime
per farlo funzionare sia con gli oggetti di animazione che con gli oggetti provenienti da createPlayer
.
function currentTime(time = 0) { animations.forEach(function (animation) { if (typeof animation.currentTime === "function") { animation.currentTime(time); } else { animation.currentTime = time; } }); }
Il player che abbiamo appena creato è quello che ti permetterà di nascondere la complessità di diversi div
per i canali di animazione a elemento singolo. Questi elementi potrebbero essere raggruppati in una scena. E ogni scena potrebbe far parte di qualcosa di più grande. Tutto ciò che si potrebbe fare con questa tecnica.
Per dimostrare la demo dei tempi, ho diviso tutte le animazioni in tre giocatori. Il primo è controllare la riproduzione dell'anteprima a destra. Il secondo combina l'animazione di salto di tutti i contorni delle palline a sinistra e di quella in anteprima.
Infine, il terzo è un giocatore che ha combinato le animazioni di posizione delle palline in un contenitore sinistro. Quel giocatore consente alle palline di diffondersi in una dimostrazione continua dell'animazione con fette di circa 60 fotogrammi al secondo.
Conclusione
Le interfacce Web come l'API Web Animations ci espongono alcune cose che i browser hanno sempre fatto. I browser sanno come eseguire il rendering velocemente passando il lavoro alla GPU. Con l'API Web Animations, abbiamo il controllo su di essa. Anche se quel controllo potrebbe sembrare un po' estraneo o confuso, non significa che anche usarlo dovrebbe essere fonte di confusione. Con una comprensione del tempo e del controllo della riproduzione, hai gli strumenti per adattare quell'API alle tue esigenze. Dovresti essere in grado di definire quanto dovrebbe essere complesso.
Ulteriori letture
- "Tecniche pratiche sulla progettazione dell'animazione", Sarah Drasner
- "Progettare con movimento ridotto per sensibilità al movimento", Val Head
- "Un'interfaccia utente vocale alternativa agli assistenti vocali", Ottomatias Peura
- "Progettazione di descrizioni comandi migliori per interfacce utente mobili", Eric Olive