Performante parallaxing

Houd ervan of haat het, parallaxen is een blijvertje. Bij oordeelkundig gebruik kan het diepte en subtiliteit toevoegen aan een webapp. Het probleem is echter dat het implementeren van parallaxing op een performante manier een uitdaging kan zijn. In dit artikel bespreken we een oplossing die zowel performant is als, net zo belangrijk, cross-browser werkt.

Parallaxillustratie.

TL; DR

  • Gebruik geen scrollgebeurtenissen of background-position om parallaxanimaties te maken.
  • Gebruik CSS 3D-transformaties om een ​​nauwkeuriger parallaxeffect te creëren.
  • Gebruik voor Mobile Safari position: sticky om ervoor te zorgen dat het parallax-effect zich verspreidt.

Als je de drop-in-oplossing wilt, ga dan naar de UI Element Samples GitHub-repository en pak de Parallax-helper JS ! Je kunt een live demo van de parallax-scroller zien in de GitHub-repository.

Probleem parallaxers

Laten we om te beginnen eens kijken naar twee veel voorkomende manieren om een ​​parallaxeffect te bereiken, en in het bijzonder waarom ze niet geschikt zijn voor onze doeleinden.

Slecht: scrollgebeurtenissen gebruiken

De belangrijkste vereiste van parallaxen is dat het scroll-gekoppeld moet zijn; voor elke afzonderlijke verandering in de schuifpositie van de pagina moet de positie van het parallaxelement worden bijgewerkt. Hoewel dat eenvoudig klinkt, is een belangrijk mechanisme van moderne browsers hun vermogen om asynchroon te werken. Dit is in ons specifieke geval van toepassing op scroll-gebeurtenissen. In de meeste browsers worden scroll-gebeurtenissen geleverd als "best-effort" en het is niet gegarandeerd dat ze op elk frame van de scroll-animatie worden weergegeven!

Dit belangrijke stukje informatie vertelt ons waarom we een op JavaScript gebaseerde oplossing moeten vermijden die elementen verplaatst op basis van scrollgebeurtenissen: JavaScript garandeert niet dat parallaxing gelijke tred houdt met de scrollpositie van de pagina . In oudere versies van Mobile Safari werden scrollgebeurtenissen feitelijk aan het einde van de scroll weergegeven, waardoor het onmogelijk was om een ​​op JavaScript gebaseerd scrolleffect te maken. Recentere versies leveren scrollgebeurtenissen tijdens de animatie, maar, net als bij Chrome, op een "best effort"-basis. Als de rode draad bezig is met ander werk, worden scrollgebeurtenissen niet onmiddellijk afgeleverd, wat betekent dat het parallaxeffect verloren gaat.

Slecht: background-position bijwerken

Een andere situatie die we graag willen vermijden is het schilderen op elk frame. Veel oplossingen proberen de background-position te veranderen om een ​​parallax-look te creëren, waardoor de browser de getroffen delen van de pagina opnieuw tekent bij het scrollen, en dat kan kostbaar genoeg zijn om de animatie aanzienlijk te verstoren.

Als we de belofte van parallaxbeweging willen waarmaken, willen we iets dat kan worden toegepast als een versnelde eigenschap (wat tegenwoordig betekent dat we vasthouden aan transformaties en dekking), en dat niet afhankelijk is van scrollgebeurtenissen.

CSS in 3D

Zowel Scott Kellum als Keith Clark hebben veel werk verricht op het gebied van het gebruik van CSS 3D om parallaxbeweging te bereiken, en de techniek die zij gebruiken is in feite deze:

  • Stel een bevattend element in om te scrollen met overflow-y: scroll (en waarschijnlijk overflow-x: hidden ).
  • Pas op datzelfde element een perspective toe, en een perspective-origin ingesteld op top left , of 0 0 .
  • Pas op de kinderen van dat element een vertaling in Z toe en schaal ze weer omhoog om parallaxbeweging te bieden zonder hun grootte op het scherm te beïnvloeden.

De CSS voor deze aanpak ziet er als volgt uit:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

Hierbij wordt uitgegaan van een HTML-fragment zoals dit:

<div class="container">
    <div class="parallax-child"></div>
</div>

Schaal aanpassen voor perspectief

Als u het onderliggende element terugduwt, wordt het kleiner in verhouding tot de perspectiefwaarde. Je kunt berekenen hoeveel het moet worden opgeschaald met deze vergelijking: (perspectief - afstand) / perspectief . Omdat we hoogstwaarschijnlijk willen dat het parallaxende element parallaxt, maar verschijnt op de grootte waarin we het hebben geschreven, zou het op deze manier moeten worden opgeschaald, in plaats van te blijven zoals het is.

In het geval van de bovenstaande code is het perspectief 1px en is de Z-afstand van het parallax-child -2px . Dit betekent dat het element moet worden opgeschaald met 3x , wat je kunt zien aan de waarde die in de code is ingeplugd: scale(3) .

Voor alle inhoud waarop geen translateZ waarde is toegepast, kunt u de waarde nul vervangen. Dit betekent dat de schaal (perspectief - 0) / perspectief is, wat uitkomt op een waarde van 1, wat betekent dat de schaal niet omhoog of omlaag is geschaald. Heel handig, eigenlijk.

Hoe deze aanpak werkt

Het is belangrijk om duidelijk te maken waarom dit werkt, omdat we die kennis binnenkort gaan gebruiken. Scrollen is in feite een transformatie en daarom kan het worden versneld; het gaat meestal om het verschuiven van lagen met de GPU. In een typische scroll, die er een is zonder enig idee van perspectief, gebeurt het scrollen op een 1:1 manier bij het vergelijken van het scrollelement en zijn kinderen. Als u een element 300px naar beneden scrolt, worden de onderliggende elementen met dezelfde hoeveelheid naar boven getransformeerd: 300px .

Het toepassen van een perspectiefwaarde op het scrollelement zorgt echter voor problemen met dit proces; het verandert de matrices die ten grondslag liggen aan de scrolltransformatie. Nu kan een scroll van 300px de kinderen slechts 150px verplaatsen, afhankelijk van het perspective en translateZ waarden die u hebt gekozen. Als een element een translateZ waarde van 0 heeft, wordt het met een verhouding van 1:1 gescrolld (zoals vroeger), maar een kind dat in Z is geduwd, weg van de perspectiefoorsprong, zal met een andere snelheid worden gescrolld! Nettoresultaat: parallaxbeweging. En, heel belangrijk, dit wordt automatisch afgehandeld als onderdeel van de interne scroll-machinerie van de browser, wat betekent dat het niet nodig is om naar scroll gebeurtenissen te luisteren of background-position te veranderen.

Een vlieg in de zalf: Mobile Safari

Er zijn kanttekeningen bij elk effect, en een belangrijke bij transformaties gaat over het behoud van 3D-effecten voor onderliggende elementen. Als er elementen in de hiërarchie zijn tussen het element met een perspectief en zijn parallaxerende kinderen, wordt het 3D-perspectief "afgevlakt", wat betekent dat het effect verloren gaat.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

In de bovenstaande HTML is de .parallax-container nieuw, en deze zal de perspective effectief afvlakken en we verliezen het parallax-effect. De oplossing is in de meeste gevallen redelijk eenvoudig: je voegt transform-style: preserve-3d toe aan het element, waardoor het eventuele 3D-effecten (zoals onze perspectiefwaarde) propageert die verderop in de boom zijn toegepast.

.parallax-container {
  transform-style: preserve-3d;
}

In het geval van Mobile Safari zijn de zaken echter iets ingewikkelder. overflow-y: scroll naar het containerelement werkt technisch gezien, maar gaat ten koste van het kunnen gooien van het scrollende element. De oplossing is om -webkit-overflow-scrolling: touch toe te voegen, maar het zal ook het perspective afvlakken en we krijgen geen parallaxing.

Vanuit het oogpunt van progressieve verbetering is dit waarschijnlijk niet zo'n groot probleem. Als we niet in elke situatie kunnen parallaxen, zal onze app nog steeds werken, maar het zou leuk zijn om een ​​oplossing te bedenken.

position: sticky aan de redding!

Er is in feite enige hulp in de vorm van position: sticky , die ervoor zorgt dat elementen tijdens het scrollen aan de bovenkant van de viewport of aan een bepaald bovenliggend element kunnen "plakken". De specificatie is, zoals de meeste, behoorlijk fors, maar bevat een nuttig klein juweeltje:

Dit lijkt op het eerste gezicht misschien niet zoveel te betekenen, maar een belangrijk punt in die zin is wanneer het verwijst naar hoe precies de plakkerigheid van een element wordt berekend: "de offset wordt berekend met verwijzing naar de dichtstbijzijnde voorouder met een scrollvak" . Met andere woorden, de afstand die het plakkerige element moet verplaatsen (zodat het lijkt alsof het aan een ander element of de viewport is bevestigd) wordt berekend voordat er andere transformaties worden toegepast, en niet erna . Dit betekent dat, net als bij het schuifvoorbeeld eerder, als de offset is berekend op 300px, er een nieuwe mogelijkheid is om perspectieven (of een andere transformatie) te gebruiken om die offsetwaarde van 300px te manipuleren voordat deze wordt toegepast op vastgezette elementen.

Door position: -webkit-sticky toe te passen op het parallaxende element, kunnen we het afvlakkende effect van -webkit-overflow-scrolling: touch effectief "omkeren". Dit zorgt ervoor dat het parallaxelement verwijst naar de dichtstbijzijnde voorouder met een scrollvak, in dit geval .container . Vervolgens past de .parallax-container net als voorheen, een perspective toe, waardoor de berekende scroll-offset verandert en een parallax-effect ontstaat.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

Dit herstelt het parallax-effect voor Mobile Safari, wat in alle opzichten uitstekend nieuws is!

Kleverige positioneringsvoorbehouden

Er is hier echter een verschil: position: sticky verandert de parallax-mechanica. Sticky positionering probeert het element aan de scrollende container te plakken, terwijl een niet-sticky versie dat niet doet. Dit betekent dat de parallax met sticky uiteindelijk het omgekeerde is van de parallax zonder:

  • Met position: sticky , hoe dichter het element bij z=0 is, hoe minder het beweegt.
  • Zonder position: sticky , hoe dichter het element bij z=0 is, hoe meer het beweegt.

Als dat allemaal een beetje abstract lijkt, kijk dan eens naar deze demo van Robert Flack, die laat zien hoe elementen zich anders gedragen met en zonder sticky positioning. Om het verschil te zien, heb je Chrome Canary (op het moment van schrijven versie 56) of Safari nodig.

Schermafbeelding van parallaxperspectief

Een demo van Robert Flack die laat zien hoe position: sticky parallax-scrollen beïnvloedt.

Diverse bugs en oplossingen

Zoals met alles zijn er echter nog steeds hobbels en oneffenheden die moeten worden gladgestreken:

  • Vaste ondersteuning is inconsistent. Ondersteuning wordt nog steeds geïmplementeerd in Chrome, Edge ontbeert volledig ondersteuning en Firefox vertoont fouten bij het schilderen wanneer sticky wordt gecombineerd met perspectieftransformaties . In dergelijke gevallen is het de moeite waard om een ​​beetje code toe te voegen om alleen position: sticky (de versie met het voorvoegsel -webkit- ) toe te voegen wanneer dat nodig is, wat alleen voor Mobile Safari is.
  • Het effect werkt niet "gewoon" in Edge. Edge probeert het scrollen op OS-niveau af te handelen, wat over het algemeen een goede zaak is, maar in dit geval voorkomt het dat het de perspectiefveranderingen tijdens het scrollen detecteert. Om dit op te lossen, kunt u een element met een vaste positie toevoegen, omdat dit Edge lijkt over te schakelen naar een niet-OS-scrollmethode en ervoor zorgt dat er rekening wordt gehouden met perspectiefveranderingen.
  • "De inhoud van de pagina is zojuist enorm geworden!" Veel browsers houden rekening met de schaal bij het bepalen hoe groot de inhoud van de pagina is, maar helaas houden Chrome en Safari geen rekening met perspectief . Dus als er bijvoorbeeld een schaal van 3x op een element wordt toegepast, zie je mogelijk schuifbalken en dergelijke, zelfs als het element op 1x staat nadat het perspective is toegepast. Het is mogelijk om dit probleem te omzeilen door elementen vanuit de rechterbenedenhoek te schalen (met transform-origin: bottom right ), wat werkt omdat het ervoor zorgt dat te grote elementen uitgroeien tot het "negatieve gebied" (meestal linksboven) van de afbeelding. schuifbaar gebied; Door de schuifbare gebieden kunt u nooit inhoud in het negatieve gebied zien of ernaar scrollen.

Conclusie

Parallaxen is een leuk effect als het doordacht wordt gebruikt. Zoals u kunt zien, is het mogelijk om het te implementeren op een manier die performant, scroll-gekoppeld en cross-browser is. Omdat het een beetje wiskundig geworstel en een kleine hoeveelheid standaardwerk vereist om het gewenste effect te krijgen, hebben we een kleine helperbibliotheek en voorbeeld samengesteld, die je kunt vinden in onze UI Element Samples GitHub-repository .

Speel mee en laat ons weten hoe het met je gaat.