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 evitare che i bambini vengano allungati e inclinati durante l'animazione applicando una controscala.

In precedenza abbiamo pubblicato aggiornamenti su come creare effetti parallasse e operatori di scorrimento illimitati ad alte prestazioni. In questo post, vedremo cosa comporta l'utilizzo di clip di animazione ad alte prestazioni. Se vuoi visualizzare una demo, consulta il repository GitHub di elementi UI di esempio.

Prendi ad esempio un menu espandibile:

Alcune opzioni per crearli sono più performanti di altre.

Non valida: animazione della larghezza e dell'altezza su un elemento contenitore

Puoi immaginare di utilizzare una piccola porzione 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. Per queste proprietà è necessario calcolare il layout e applicare i risultati a ogni fotogramma dell'animazione, il che può essere molto costoso e in genere comporta una perdita di 60 FPS. Per saperne di più, leggi le nostre guide alle prestazioni di rendering, in cui puoi trovare ulteriori informazioni sul funzionamento del processo di rendering.

Non corretto: utilizza le proprietà del clip CSS o del percorso di clip

Un'alternativa all'animazione di width e height potrebbe essere quella di utilizzare la proprietà clip (ora deprecata) per animare l'effetto di espansione e compressione. Oppure, se preferisci, puoi usare clip-path. Tuttavia, l'utilizzo di clip-path è meno supportato rispetto a clip. Tuttavia, clip è deprecato. Infatti, Ma non disperare, questa non è la soluzione che volevi mai.

.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 che animare width e height dell'elemento del menu, l'aspetto negativo di questo approccio è che attiva comunque la colorazione. Anche la proprietà clip, nel caso in cui dovessi seguire questa strada, richiede che l'elemento su cui opera sia in posizione assoluta o fissa, il che può richiedere un ulteriore wrangling.

Buona: animazione delle scale

Poiché questo effetto comporta un aumento sempre maggiore di elementi, puoi utilizzare una trasformazione di scala. Questa è un'ottima notizia, perché cambiare le trasformazioni 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 FPS.

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

Passaggio 1: calcola gli stati iniziale e finale

Con un approccio che utilizza le animazioni in scala, la prima cosa da fare è leggere gli elementi che indicano le dimensioni del menu che devono essere sia quando è compresso, sia quando è espanso. Può darsi che per alcune situazioni non sia possibile ottenere entrambi questi bit di informazioni in una volta e che sia necessario, ad esempio, attivare alcune classi per poter leggere i vari stati del componente. In questo caso, tuttavia, 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 menù, possiamo dare un'ipotesi ragionevole che inizierà a essere nella sua scala naturale (1, 1). Questa scala naturale rappresenta il suo stato espanso, il che significa che dovrai animare da una versione ridotta (calcolata in precedenza) fino a quella scala naturale.

Ma aspetta! Sicuramente questo amplierebbe anche i contenuti del menù, non è vero? Beh, come puoi vedere sotto, sì.

Cosa puoi fare? Puoi applicare una trasformazione counter-trasformazione ai contenuti; quindi, ad esempio, se il container viene ridimensionato a 1/5 delle sue dimensioni normali, puoi scalare i contenuti verso l'alto di 5 volte per evitare che i contenuti vengano compressi. Ci sono due aspetti da considerare in merito:

  1. La trasformazione del contatore è anche un'operazione di scalabilità. Questo è positivo perché può anche essere accelerato, proprio come l'animazione sul container. Potresti dover assicurarti che gli elementi animati ricevano il proprio livello compositore (consentendo alla GPU l'aiuto) e per questo puoi aggiungere will-change: transform all'elemento o, se devi supportare browser meno recenti, backface-visiblity: hidden.

  2. La controtrasformazione deve essere calcolata per frame. A questo punto le cose possono essere un po' più complicate, 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, diciamo, cubic-bezier(0, 0, 0.3, 1) non è così ovvio.

Potresti avere la tentazione di animare l'effetto con JavaScript. Dopo tutto, potresti utilizzare un'equazione di easing per calcolare i valori di scala e contatore per frame. Lo svantaggio di qualsiasi animazione basata su JavaScript è ciò che accade quando il thread principale (su cui viene eseguito JavaScript) è impegnato in altre attività. In breve, l'animazione può interrompersi o interrompersi del tutto, il che non è fantastico per l'esperienza utente.

Passaggio 2: crea animazioni CSS all'istante

La soluzione, che all'inizio potrebbe sembrare strana, consiste nel creare un'animazione con fotogrammi chiave con la nostra funzione di easing in modo dinamico e inserirla nella pagina per utilizzarla dal menu. (Grazie di cuore all'ingegnere di Chrome Robert Flack per la segnalazione). Il vantaggio principale di ciò è che un'animazione con fotogrammi chiave che cambia le trasformazioni può essere eseguita sul compositore, il che significa che non è interessata dalle attività nel thread principale.

Per creare l'animazione dei fotogrammi chiave, passiamo da 0 a 100 e calcoliamo i valori di scala necessari per l'elemento e i suoi contenuti. Questi possono essere ridotti a una stringa, che può essere inserita nella pagina come elemento di stile. L'inserimento degli stili causerà il passaggio di Ricalcola stili nella pagina, operazione aggiuntiva che il browser deve svolgere, ma che 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}
    }`;
}

Gli incessanti potrebbero chiedersi qual è la funzione ease() all'interno del ciclo for. Puoi utilizzare qualcosa di simile per mappare i valori da 0 a 1 a un equivalente con eased.

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

Puoi utilizzare anche la Ricerca Google per tracciare l'aspetto. Utile! Se hai bisogno di altre equazioni di easing, consulta Tween.js di Soledad Penadés, che ne contiene moltissime.

Passaggio 3: attiva le animazioni CSS

Dopo aver creato queste animazioni e adattate alla pagina in JavaScript, il passaggio finale consiste nell'attivare o disattivare le classi che attivano 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;
}

Questo determina l'esecuzione delle animazioni create nel passaggio precedente. Poiché le animazioni fornite sono già state semplificate, la funzione di temporizzazione deve essere impostata su linear, altrimenti tra un fotogramma chiave risulterà molto strano.

Per comprimere nuovamente l'elemento, sono disponibili due opzioni: aggiornare l'animazione CSS in modo che venga eseguita in senso inverso anziché in avanti. Questa funzione andrà bene, ma la "sensazione" dell'animazione verrà invertita, quindi se hai utilizzato una curva di facilitazione, l'inversione risulterà attenuata all'interno, il che la renderà fiacco. Una soluzione più appropriata è creare una seconda coppia di animazioni per comprimere l'elemento. Queste possono essere create esattamente nello stesso modo delle animazioni dei fotogrammi chiave di espansione, ma con valori di inizio e fine scambiati.

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

Una versione più avanzata: rivelazioni circolari

Questa tecnica può anche essere utilizzata per creare animazioni circolari di espansione e compressione.

I principi sono in gran parte gli stessi della versione precedente, in cui puoi scalare un elemento e controbilanciare i relativi elementi secondari immediati. In questo caso, l'elemento di cui viene fatto lo scale up ha un valore border-radius pari al 50%, che lo rende circolare, e contiene un altro elemento che ha overflow: hidden, il che significa che il cerchio non si espande al di fuori dei limiti dell'elemento.

Un avvertimento per questa particolare variante: Chrome ha testo sfocato sulle schermate con DPI basso durante l'animazione a causa di errori di arrotondamento dovuti alle dimensioni e al contatore di scala del testo. Se vuoi saperne di più, è 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 delle clip ad alte prestazioni utilizzando le trasformazioni di scala. In un mondo perfetto, sarebbe fantastico vedere le animazioni dei clip accelerate (esiste un bug di Chromium in merito realizzato da Jake Archibald), ma, finché non ci arriveremo, devi fare attenzione quando anima clip o clip-path ed evitare decisamente l'animazione di width o height.

Sarebbe utile anche usare le animazioni web per effetti come questo, perché hanno un'API JavaScript ma possono essere eseguiti sul thread del compositore se si animano solo transform e opacity. Sfortunatamente, l'assistenza per le animazioni web non è ottimale, ma potresti utilizzare il miglioramento progressivo per utilizzarle, se disponibili.

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

Fino a quel momento, anche se puoi utilizzare le librerie basate su JavaScript per eseguire l'animazione, potresti ottenere prestazioni più affidabili creando un'animazione CSS e utilizzandola al suo posto. Analogamente, se la tua app si basa già su JavaScript per le animazioni, potresti ottenere risultati migliori mantenendo la coerenza con il codebase esistente.

Se vuoi dare un'occhiata al codice per ottenere questo effetto, dai un'occhiata al repository GitHub di esempi di elementi UI e, come sempre, facci sapere come procedere nei commenti di seguito.