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 passato abbiamo pubblicato aggiornamenti su come creare effetti di parallasse e componenti con scorrimento infinito efficaci. 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 questo tipo di campagna hanno un rendimento migliore 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. Se non lo sapevi, consulta le nostre guide sul rendimento del rendering per saperne di più sul funzionamento della procedura 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 qualcosa aumenti e diminuisca di dimensioni, puoi utilizzare una trasformazione di scala. Questa è un'ottima notizia perché la modifica delle trasformazioni non richiede layout o pittura e il browser può trasferirle alla GPU, il che significa che l'effetto viene accelerato e è molto più probabile che raggiunga i 60 fps.

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

Passaggio 1: calcola gli stati di inizio e di fine

Con un approccio che utilizza animazioni di scala, il primo passaggio consiste nel leggere gli elementi che indicano le dimensioni del menu sia quando è compresso sia quando è espanso. In alcune situazioni potresti non riuscire a ottenere entrambi i dati contemporaneamente e dover attivare/disattivare alcune classi per poter leggere i vari stati del componente. Tuttavia, se devi farlo, fai attenzione: getBoundingClientRect() (o offsetWidth e offsetHeight) forza il browser a eseguire le passate di stili e layout se gli stili sono stati modificati 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 animare da una versione ridotta (calcolata sopra) fino a quella 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 un proprio livello di compositore (consentendo alla GPU di intervenire) e per farlo puoi aggiungere will-change: transform all'elemento o, se devi supportare i browser meno recenti, backface-visiblity: hidden.

  2. La controtrasformazione deve essere calcolata per fotogramma. È 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.

Potrebbe quindi essere allettante prendere in considerazione l'animazione dell'effetto utilizzando JavaScript. 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 interrompersi 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 un'animazione con keyframe che mutava le trasformazioni può essere eseguita sul compositore, il che significa che non è interessato dalle attività sul 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 relativi contenuti. che possono essere ridotte a 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}
    }`;
}

I più curiosi potrebbero chiedersi cosa sia la funzione ease() all'interno del ciclo for. 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à. Comodo! 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

Dopo aver creato e incorporato le animazioni nella pagina in JavaScript, il passaggio finale consiste nell'attivare e disattivare le classi che le attivano.

.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.

Per richiudere l'elemento, hai due opzioni: aggiorna l'animazione CSS in modo che venga eseguita in ordine inverso anziché in ordine normale. 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 con DPI bassi 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 ai preferiti 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 animazioni, ti consigliamo di mantenere almeno la coerenza con il codice di base esistente.

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