Creare animazioni di espansione e compressione performanti

Paul Lewis
Stephen McGruer
Stephen McGruer

TL;DR

Utilizza le trasformazioni di scala per animare i clip. Puoi impedire che i bambini vengano allungati e inclinati durante l'animazione applicando una scala inversa.

In precedenza abbiamo pubblicato aggiornamenti su come creare effetti di parallasse ad alte prestazioni e strumenti di scorrimento infiniti. In questo post, esamineremo cosa occorre per creare animazioni di clip efficaci. Se vuoi vedere una demo, dai un'occhiata al repository GitHub di Sample UI Elements.

Prendiamo ad esempio un menu espandibile:

Alcune opzioni per la creazione di questa funzionalità hanno prestazioni più elevate di altre.

Cattivo: animazione della larghezza e dell'altezza in un elemento contenitore

Potresti immaginare di utilizzare un po' di CSS per animare la larghezza e l'altezza dell'elemento contenitore.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

Il problema immediato di questo approccio è che richiede l'animazione di width e height. Queste proprietà richiedono il calcolo del layout e dipingono i risultati in ogni frame dell'animazione, il che può essere molto costoso e in genere ti fa perdere i 60 fps. Per saperne di più, leggi le nostre guide sulle prestazioni del rendering, dove puoi trovare maggiori informazioni sul funzionamento del processo di rendering.

Sbagliato: utilizza le proprietà CSS clip o clip-path

Un'alternativa all'animazione di width e height potrebbe essere l'utilizzo della proprietà clip (ora deprecata) per animare l'effetto di espansione e chiusura. In alternativa, se preferisci, puoi utilizzare clip-path. L'utilizzo di clip-path, tuttavia, è meno supportato rispetto a clip. Tuttavia, clip è deprecato. Infatti, Ma non disperare, non è comunque la soluzione che volevi.

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

Sebbene sia meglio di animare width e height dell'elemento menu, il rovescio della medaglia di questo approccio è che attiva comunque la pittura. Inoltre, la proprietà clip, se scegli questa strada, richiede che l'elemento su cui opera sia posizionato in modo assoluto o fisso, il che può richiedere un po' di lavoro in più.

Buona: animazioni delle scale

Poiché questo effetto prevede che un elemento aumenti e diminuisca di dimensioni, puoi utilizzare una trasformazione di scala. Questa è un'ottima notizia perché la modifica delle trasformazioni è qualcosa che non richiede layout o colorazione e che il browser può trasferire alla GPU, il che significa che l'effetto è accelerato e ha una probabilità notevolmente maggiore di raggiungere i 60 f/s.

Lo svantaggio di questo approccio, come per la maggior parte degli aspetti relativi alle prestazioni di rendering, è che richiede un po' di configurazione. Tuttavia, ne vale davvero la pena.

Passaggio 1: calcola gli stati di inizio e di fine

Con un approccio che utilizza le animazioni in scala, il primo passaggio consiste nel leggere gli elementi che indicano le dimensioni necessarie per il menu sia quando è compresso sia quando viene espanso. In alcune situazioni potresti non riuscire a ottenere entrambi i dati contemporaneamente e potresti dover attivare/disattivare alcune classi per poter leggere i vari stati del componente. Se devi farlo, fai attenzione: getBoundingClientRect() (o offsetWidth e offsetHeight) obbliga il browser a eseguire stili e pass di layout se gli stili sono cambiati dall'ultima esecuzione.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

Nel caso di un menu, possiamo ragionevolmente supporre che inizi con la sua scala naturale (1, 1). Questa scala naturale rappresenta il suo stato espanso, il che significa che dovrai eseguire l'animazione da una versione ridotta (che è stata calcolata in precedenza) di nuovo fino a quella scala naturale.

Ma aspetta! Sicuramente questo ridimensionerebbe anche i contenuti del menu, giusto? Sì, come puoi vedere di seguito.

Che cosa puoi fare a riguardo? Puoi applicare una trasformazione inversa ai contenuti, ad esempio se il contenitore viene ridotto a 1/5 delle sue dimensioni normali, puoi aumentare di 5 volte le dimensioni dei contenuti per evitare che vengano schiacciati. Tieni presente due aspetti:

  1. La controtrasformazione è anche un'operazione di scala. Questo è un bene perché può anche essere accelerato, proprio come l'animazione nel contenitore. Potresti dover assicurarti che gli elementi animati abbiano il proprio livello compositore (in modo che la GPU possa contribuire) e per questo puoi aggiungere will-change: transform all'elemento o, se devi supportare browser precedenti, backface-visiblity: hidden.

  2. La contro-trasformazione deve essere calcolata per frame. È qui che le cose possono diventare un po' più difficili, perché, supponendo che l'animazione sia in CSS e utilizzi una funzione di easing, l'easing stesso deve essere contrastato durante l'animazione della controtrasformazione. Tuttavia, calcolare la curva inversa per, ad esempio, cubic-bezier(0, 0, 0.3, 1) non è così ovvio.

Pertanto, l'idea di animare l'effetto tramite JavaScript potrebbe essere allettante. Dopotutto, potresti poi utilizzare un'equazione di easing per calcolare i valori di scala e controscala per frame. Lo svantaggio di qualsiasi animazione basata su JavaScript è che quando il thread principale (in cui viene eseguito il codice JavaScript) è occupato da un'altra attività, In breve, l'animazione può subire interruzioni o addirittura fermarsi del tutto, il che non è un buon risultato per l'esperienza utente.

Passaggio 2: crea animazioni CSS in tempo reale

La soluzione, che all'inizio potrebbe sembrare strana, consiste nel creare un'animazione con animazioni chiave con la nostra funzione di easing dinamicamente e inserirla nella pagina per l'utilizzo da parte del menu. (Un grande ringraziamento all'ingegnere di Chrome Robert Flack per averci segnalato il problema). Il vantaggio principale è che sul compositore è possibile eseguire un'animazione con fotogrammi chiave che muta le trasformazioni, il che significa che non viene influenzata dalle attività nel thread principale.

Per creare l'animazione del fotogramma chiave, passiamo da 0 a 100 e calcoliamo i valori di scala necessari per l'elemento e i suoi contenuti. Questi possono quindi essere ridotti in una stringa, che può essere inserita nella pagina come elemento di stile. L'inserimento degli stili comporterà un passaggio per ricalcolare gli stili sulla pagina, un'operazione aggiuntiva che il browser deve eseguire, ma lo farà solo una volta all'avvio del componente.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

L'infinita curiosità potrebbe domandarsi sulla funzione ease() all'interno del for-loop. Puoi utilizzare un valore simile per mappare i valori da 0 a 1 a un valore equivalente attenuato.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Puoi anche utilizzare la Ricerca Google per visualizzare una mappa di come sarà. Pratico! Se hai bisogno di altre equazioni di easing, dai un'occhiata a Tween.js di Soledad Penadés, che ne contiene una marea.

Passaggio 3: attiva le animazioni CSS

Con queste animazioni create e integrate nella pagina in JavaScript, il passaggio finale consiste nell'attivare/disattivare le animazioni.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

In questo modo, vengono eseguite le animazioni create nel passaggio precedente. Poiché le animazioni predefinite sono già smorzate, la funzione di temporizzazione deve essere impostata su linear, altrimenti verrà applicata una transizione tra ogni keyframe, il che avrà un aspetto molto strano.

Quando si tratta di comprimere di nuovo l'elemento, ci sono due opzioni: aggiornare l'animazione CSS in modo che venga eseguita in modo inverso anziché in avanti. Funziona perfettamente, ma l'effetto dell'animazione sarà opposto, quindi se hai utilizzato una curva di transizione graduale in uscita, la transizione inversa avrà un'impressione di transizione graduale in, che la farà sembrare lenta. Una soluzione più appropriata è creare una seconda coppia di animazioni per comprimere l'elemento. Possono essere create nello stesso modo delle animazioni con keyframe di espansione, ma con i valori di inizio e di fine scambiati.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

Una versione più avanzata: le rivelazioni circolari

È anche possibile utilizzare questa tecnica per creare animazioni di apertura e chiusura circolari.

I principi sono in gran parte gli stessi della versione precedente, in cui puoi ridimensionare un elemento e ridimensionare gli elementi secondari immediati. In questo caso, l'elemento che viene ridimensionato ha un valore border-radius pari al 50%, che lo rende circolare, ed è racchiuso in un altro elemento che ha overflow: hidden, il che significa che non vedi il cerchio espandersi oltre i limiti dell'elemento.

Un avvertimento su questa particolare variante: Chrome ha un testo sfocato su schermi a basso DPI durante l'animazione a causa di errori di arrotondamento dovuti alla scala e alla controscala del testo. Se ti interessano i dettagli, è stato segnalato un bug che puoi aggiungere a Speciali e seguire.

Il codice per l'effetto di espansione circolare è disponibile nel repository GitHub.

Conclusioni

Ecco un modo per creare animazioni di clip efficaci utilizzando le trasformazioni di scala. In un mondo ideale, sarebbe fantastico accelerare le animazioni dei clip (esiste un bug di Chromium per questo creato da Jake Archibald), ma finché non ci arriveremo, devi prestare attenzione quando animi clip o clip-path e devi assolutamente evitare di animare width o height.

Per effetti come questo, è utile anche utilizzare Web Animations perché hanno un'API JavaScript, ma possono essere eseguite sul thread del compositore se animiamo solo transform e opacity. Purtroppo, il supporto delle animazioni web non è ottimale, anche se puoi utilizzare il miglioramento progressivo per utilizzarle, se sono disponibili.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

Fino a quando non cambierà, anche se puoi utilizzare librerie basate su JavaScript per creare l'animazione, potresti riscontrare prestazioni più affidabili creando un'animazione CSS e utilizzandola. Allo stesso modo, se la tua app si basa già su JavaScript per le sue animazioni, potresti ottenere un servizio migliore se è almeno coerente con il codebase esistente.

Se vuoi dare un'occhiata al codice di questo effetto, dai un'occhiata al repo GitHub di UI Element Samples e, come sempre, facci sapere come va nei commenti qui sotto.