Prestatiegericht bouwen uitbreiden & animaties samenvouwen

Stephen McGruer
Stephen McGruer

TL; DR

Gebruik schaaltransformaties bij het animeren van clips. Je kunt voorkomen dat de kinderen tijdens de animatie worden uitgerekt en scheefgetrokken door ze tegen te schalen.

Eerder hebben we updates gepost over het maken van krachtige parallaxeffecten en oneindige scrollers . In dit bericht gaan we bekijken wat erbij komt kijken als je performante clip-animaties wilt. Als je een demo wilt zien, bekijk dan de Sample UI Elements GitHub repository .

Neem bijvoorbeeld een uitvouwbaar menu:

Sommige opties om dit te bouwen zijn performanter dan andere.

Slecht: animatie van breedte en hoogte op een containerelement

Je kunt je voorstellen dat je een beetje CSS gebruikt om de breedte en hoogte van het containerelement te animeren.

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

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

Het directe probleem met deze aanpak is dat er animatie width en height nodig is. Deze eigenschappen vereisen een berekende lay-out en tekenen de resultaten op elk frame van de animatie, wat erg duur kan zijn en er doorgaans voor zorgt dat je 60 fps misloopt. Als dat nieuws voor u is, lees dan onze Rendering Performance- handleidingen, waar u meer informatie kunt krijgen over hoe het renderingproces werkt.

Slecht: gebruik de CSS-clip- of clippad-eigenschappen

Een alternatief voor het animeren van width en height zou kunnen zijn om de (nu verouderde) clip eigenschap te gebruiken om het uitvouw- en samenvouweffect te animeren. Of, als u dat liever heeft, kunt u in plaats daarvan clip-path gebruiken. Het gebruik van clip-path wordt echter minder goed ondersteund dan clip . Maar clip is verouderd. Rechts. Maar wanhoop niet: dit is toch niet de oplossing die je wilde!

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

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

Hoewel het beter is dan het animeren van de width en height van het menu-element, is het nadeel van deze aanpak dat er nog steeds verf wordt geactiveerd. Ook vereist de clip eigenschap, als je die route kiest, dat het element waarop het werkt absoluut of vast gepositioneerd is, wat wat extra gekibbel kan vergen.

Goed: schalen animeren

Omdat dit effect inhoudt dat iets groter en kleiner wordt, kun je een schaaltransformatie gebruiken. Dit is geweldig nieuws, omdat het veranderen van transformaties iets is waarvoor geen lay-out of verf nodig is, en dat de browser kan doorgeven aan de GPU, wat betekent dat het effect wordt versneld en aanzienlijk waarschijnlijker 60 fps zal bereiken.

Het nadeel van deze aanpak is, zoals de meeste dingen bij het renderen, dat er wat installatiewerk voor nodig is. Het is echter absoluut de moeite waard!

Stap 1: Bereken de begin- en eindtoestand

Met een aanpak die schaalanimaties gebruikt, is de eerste stap het lezen van elementen die u vertellen hoe groot het menu moet zijn, zowel wanneer het is samengevouwen als wanneer het is uitgevouwen. Het kan zijn dat je in sommige situaties niet beide stukjes informatie in één keer kunt krijgen, en dat je bijvoorbeeld een aantal klassen moet wisselen om de verschillende statussen van de component te kunnen lezen. Als u dat echter moet doen, wees dan voorzichtig: getBoundingClientRect() (of offsetWidth en offsetHeight ) dwingt de browser om stijlen en lay-outpassen uit te voeren als stijlen zijn gewijzigd sinds ze voor het laatst zijn uitgevoerd.

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
    };
}

In het geval van zoiets als een menu kunnen we redelijkerwijs aannemen dat het in eerste instantie op zijn natuurlijke schaal zal zijn (1, 1). Deze natuurlijke schaal vertegenwoordigt de uitgebreide staat, wat betekent dat je vanuit een verkleinde versie (die hierboven is berekend) moet animeren terug naar die natuurlijke schaal.

Maar wacht! Dit zou toch zeker ook de inhoud van het menu schalen, nietwaar? Nou, zoals je hieronder kunt zien, ja.

Dus wat kun je hieraan doen? Welnu, je kunt een tegentransformatie op de inhoud toepassen, dus als de container bijvoorbeeld wordt verkleind tot 1/5e van de normale grootte, kun je de inhoud 5x vergroten om te voorkomen dat de inhoud wordt platgedrukt. Daaraan zijn twee dingen op te merken:

  1. De tegentransformatie is ook een schaaloperatie . Dat is goed , want het kan ook versneld worden, net als de animatie op de container. Mogelijk moet u ervoor zorgen dat de elementen die worden geanimeerd hun eigen compositorlaag krijgen (waardoor de GPU kan helpen), en daarvoor kunt u will-change: transform naar het element of, als u oudere browsers moet ondersteunen, backface-visiblity: hidden .

  2. De tegentransformatie moet per frame worden berekend. Dit is waar het wat lastiger kan worden, omdat ervan uitgaande dat de animatie in CSS is en een versoepelingsfunctie gebruikt, de versoepeling zelf moet worden tegengegaan bij het animeren van de tegentransformatie. Het berekenen van de inverse curve voor bijvoorbeeld cubic-bezier(0, 0, 0.3, 1) is echter niet zo voor de hand liggend.

Het kan daarom verleidelijk zijn om te overwegen het effect te animeren met JavaScript. Je zou dan immers een versoepelingsvergelijking kunnen gebruiken om de schaal- en tegenschaalwaarden per frame te berekenen. Het nadeel van elke op JavaScript gebaseerde animatie is wat er gebeurt als de hoofdthread (waar uw JavaScript wordt uitgevoerd) bezig is met een andere taak. Het korte antwoord is dat je animatie kan stotteren of helemaal kan stoppen, wat niet goed is voor UX.

Stap 2: Bouw direct CSS-animaties

De oplossing, die op het eerste gezicht misschien vreemd lijkt, is om dynamisch een keyframe-animatie te maken met onze eigen versoepelingsfunctie en deze in de pagina te injecteren voor gebruik door het menu. (Hartelijk dank aan Chrome-ingenieur Robert Flack voor het wijzen hierop!) Het belangrijkste voordeel hiervan is dat een keyframe-animatie die transformaties muteert op de compositor kan worden uitgevoerd, wat betekent dat deze niet wordt beïnvloed door taken op de hoofdthread.

Om de keyframe-animatie te maken, stappen we van 0 naar 100 en berekenen we welke schaalwaarden nodig zijn voor het element en de inhoud ervan. Deze kunnen vervolgens worden samengevat tot een string, die als stijlelement in de pagina kan worden geïnjecteerd. Het injecteren van de stijlen zal ervoor zorgen dat er een herberekening van stijlen op de pagina plaatsvindt, wat extra werk is dat de browser moet doen, maar dit zal slechts één keer gebeuren wanneer de component opstart.

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}
    }`;
}

De eindeloos nieuwsgierige mensen vragen zich misschien af ​​wat de functie ease() in de for-lus is. Je kunt zoiets als dit gebruiken om waarden van 0 tot 1 in kaart te brengen naar een versoepeld equivalent.

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

U kunt Google Zoeken ook gebruiken om in kaart te brengen hoe dat eruit ziet . Handig! Als je andere versoepelingsvergelijkingen nodig hebt, kijk dan eens naar Tween.js van Soledad Penadés , die er een hele hoop bevat.

Stap 3: Schakel de CSS-animaties in

Nu deze animaties in JavaScript zijn gemaakt en op de pagina zijn weergegeven, is de laatste stap het schakelen tussen klassen die de animaties inschakelen.

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

Hierdoor worden de animaties uitgevoerd die in de vorige stap zijn gemaakt. Omdat de gebakken animaties al zijn versoepeld, moet de timingfunctie op linear worden ingesteld, anders verplaats je tussen elk keyframe, wat er heel raar uit zal zien!

Als het gaat om het samenvouwen van het element, zijn er twee opties: update de CSS-animatie zodat deze achteruit in plaats van vooruit wordt uitgevoerd. Dit zal prima werken, maar het 'gevoel' van de animatie zal worden omgekeerd, dus als je een 'ease-out'-curve hebt gebruikt, zal het omgekeerde 'versoepeld' aanvoelen , waardoor het traag aanvoelt. Een geschiktere oplossing is om een ​​tweede paar animaties te maken voor het samenvouwen van het element. Deze kunnen op precies dezelfde manier worden gemaakt als de uitgebreide keyframe-animaties, maar met verwisselde begin- en eindwaarden.

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

Een geavanceerdere versie: ronde onthullingen

Het is ook mogelijk om deze techniek te gebruiken om cirkelvormige uit- en samenvouwanimaties te maken.

De principes zijn grotendeels hetzelfde als in de vorige versie, waarbij je een element schaalt en de directe onderliggende elementen ervan op schaal brengt. In dit geval heeft het element dat wordt opgeschaald een border-radius van 50%, waardoor het cirkelvormig is, en wordt het omgeven door een ander element met overflow: hidden , wat betekent dat je de cirkel niet ziet uitbreiden buiten de elementgrenzen.

Een waarschuwing voor deze specifieke variant: Chrome heeft tijdens de animatie wazige tekst op schermen met een lage DPI vanwege afrondingsfouten als gevolg van de schaal en tegenschaal van de tekst. Als je geïnteresseerd bent in de details daarvan, is er een bug ingediend die je een ster kunt geven en kunt volgen .

De code voor het circulaire expand-effect is te vinden in de GitHub-repository .

Conclusies

Dus daar heb je het: een manier om performante clipanimaties te maken met behulp van schaaltransformaties. In een perfecte wereld zou het geweldig zijn om clip-animaties te versnellen (er is een Chromium-bug voor gemaakt door Jake Archibald), maar totdat we daar zijn, moet je voorzichtig zijn bij het animeren van clip of clip-path , en absoluut animatie vermijden width of height .

Het zou ook handig zijn om webanimaties voor dit soort effecten te gebruiken, omdat ze een JavaScript-API hebben, maar op de compositor-thread kunnen worden uitgevoerd als je alleen transform en opacity animeert. Helaas is de ondersteuning voor webanimaties niet geweldig , hoewel u progressieve verbeteringen kunt gebruiken om ze te gebruiken als ze beschikbaar zijn.

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

Totdat dat verandert, terwijl je op JavaScript gebaseerde bibliotheken kunt gebruiken om de animatie uit te voeren, zul je merken dat je betrouwbaardere prestaties krijgt door een CSS-animatie te maken en die in plaats daarvan te gebruiken. En als uw app al afhankelijk is van JavaScript voor zijn animaties, kunt u er beter aan doen als u op zijn minst consistent bent met uw bestaande codebase.

Als je de code voor dit effect wilt bekijken, bekijk dan de UI Element Samples GitHub-repository en laat ons, zoals altijd, weten hoe het met je gaat in de reacties hieronder.