Kortom
Maakt het je niet uit hoe klein het is? Wil je gewoon de demo van Nyan Cat bekijken en de bibliotheek gebruiken? Je vindt de code van de demo in onze GitHub-repository .
LAM;WRA (Lang en wiskundig; zal het toch lezen)
Een tijdje geleden hebben we een parallax-scroller gebouwd (heb je dat artikel gelezen? Het is echt goed, zeker de moeite waard!). Door elementen terug te duwen met behulp van CSS 3D-transformaties, bewogen de elementen langzamer dan onze werkelijke scrollsnelheid.
Samenvatten
Laten we beginnen met een samenvatting van hoe de parallax-scroller werkte.
Zoals te zien in de animatie, bereikten we het parallaxeffect door elementen "naar achteren" te duwen in de 3D-ruimte, langs de Z-as. Het scrollen van een document is in feite een translatie langs de Y-as. Dus als we bijvoorbeeld 100 px naar beneden scrollen, wordt elk element 100 px naar boven getransleerd. Dat geldt voor alle elementen, zelfs de elementen die "verder naar achteren" staan. Maar omdat ze verder van de camera verwijderd zijn, zal hun waargenomen beweging op het scherm minder dan 100 px zijn, wat het gewenste parallaxeffect oplevert.
Natuurlijk zal het verplaatsen van een element terug in de ruimte het ook kleiner doen lijken, wat we corrigeren door het element weer te vergroten. We hebben de exacte berekening al bedacht toen we de parallax-scroller bouwden, dus ik zal niet alle details herhalen.
Stap 0: Wat willen we doen?
Schuifbalken. Dat is wat we gaan bouwen. Maar heb je er ooit echt over nagedacht wat ze doen? Ik in ieder geval niet. Schuifbalken geven aan hoeveel van de beschikbare content momenteel zichtbaar is en hoeveel voortgang jij als lezer hebt geboekt. Als je naar beneden scrolt, scrolt de schuifbalk mee om aan te geven dat je richting het einde komt. Als alle content in de viewport past, is de schuifbalk meestal verborgen. Als de content 2x de hoogte van de viewport heeft, vult de schuifbalk ½ van de hoogte van de viewport. Content ter waarde van 3x de hoogte van de viewport schaalt de schuifbalk naar ⅓ van de viewport, enz. Je ziet het patroon. In plaats van te scrollen, kun je ook op de schuifbalk klikken en slepen om sneller door de site te navigeren. Dat is verrassend veel gedrag voor zo'n onopvallend element. Laten we één strijd tegelijk voeren.
Stap 1: achteruit rijden
Oké, we kunnen elementen langzamer laten bewegen dan de scrollsnelheid met CSS 3D-transformaties, zoals beschreven in het artikel over parallax-scrollen. Kunnen we de richting ook omkeren? Het blijkt dat we dat kunnen en dat is onze manier om een frame-perfecte, aangepaste scrollbalk te bouwen. Om te begrijpen hoe dit werkt, moeten we eerst een paar basisprincipes van CSS 3D doornemen.
Om een perspectiefprojectie in wiskundige zin te krijgen, zul je hoogstwaarschijnlijk homogene coördinaten gebruiken. Ik ga niet in detail in op wat ze zijn en waarom ze werken, maar je kunt ze zien als 3D-coördinaten met een extra, vierde coördinaat genaamd w . Deze coördinaat zou 1 moeten zijn, behalve als je perspectiefvervorming wilt. We hoeven ons geen zorgen te maken over de details van w , omdat we geen andere waarde dan 1 gaan gebruiken. Daarom zijn alle punten vanaf nu 4-dimensionale vectoren [x, y, z, w=1] en moeten matrices dus ook 4x4 zijn.
Een voorbeeld van hoe CSS homogene coördinaten gebruikt, is wanneer je je eigen 4x4-matrices definieert in een transform-eigenschap met de functie matrix3d()
. matrix3d
accepteert 16 argumenten (omdat de matrix 4x4 is), waarbij de ene kolom na de andere wordt gespecificeerd. We kunnen deze functie dus gebruiken om handmatig rotaties, translaties, enz. te specificeren. Maar we kunnen er ook mee spelen, door met die w- coördinaat te spelen!
Voordat we matrix3d()
kunnen gebruiken, hebben we een 3D-context nodig – want zonder een 3D-context zou er geen perspectiefvervorming zijn en zijn er geen homogene coördinaten nodig. Om een 3D-context te creëren, hebben we een container nodig met een perspective
en enkele elementen erin die we kunnen transformeren in de nieuw gecreëerde 3D-ruimte. Bijvoorbeeld :

De elementen in een perspectiefcontainer worden door de CSS-engine als volgt verwerkt:
- Verander elke hoek (vertex) van een element in homogene coördinaten
[x,y,z,w]
, relatief ten opzichte van de perspectiefcontainer. - Pas alle transformaties van het element toe als matrices van rechts naar links .
- Als het perspectiefelement scrollbaar is, past u een scrollmatrix toe.
- Pas de perspectiefmatrix toe.
De scrollmatrix is een translatie langs de y-as. Als we 400 px naar beneden scrollen , moeten alle elementen 400 px omhoog worden verplaatst . De perspectiefmatrix is een matrix die punten dichter naar het verdwijnpunt "trekt" naarmate ze verder naar achteren in de 3D-ruimte staan. Dit bereikt zowel het effect dat objecten kleiner lijken wanneer ze verder naar achteren staan, als het effect dat ze "langzamer bewegen" tijdens het verplaatsen. Dus als een element naar achteren wordt verplaatst, zorgt een translatie van 400 px ervoor dat het element slechts 300 px op het scherm beweegt.
Als u alle details wilt weten, moet u de specificatie van het CSS-transformatierenderingmodel lezen. Voor dit artikel heb ik het bovenstaande algoritme echter vereenvoudigd.
Onze box bevindt zich in een perspectiefcontainer met de waarde p voor het perspective
. Laten we aannemen dat de container scrollbaar is en n pixels naar beneden kan worden gescrold.
De eerste matrix is de perspectiefmatrix, de tweede matrix is de scrollmatrix. Samenvattend: de scrollmatrix zorgt ervoor dat een element omhoog beweegt wanneer we naar beneden scrollen , vandaar het minteken.
Voor onze schuifbalk willen we echter het tegenovergestelde : we willen dat ons element naar beneden beweegt wanneer we naar beneden scrollen. Hier kunnen we een truc gebruiken: de w- coördinaat van de hoeken van onze box omkeren. Als de w- coördinaat -1 is, worden alle vertalingen in de tegenovergestelde richting uitgevoerd. Hoe doen we dat? De CSS-engine zorgt ervoor dat de hoeken van onze box worden omgezet naar homogene coördinaten en stelt w in op 1. Het is tijd voor matrix3d()
om te schitteren!
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
);
}
Deze matrix doet niets anders dan w ontkennen. Dus wanneer de CSS-engine elke hoek heeft omgezet in een vector van de vorm [x,y,z,1]
, zal de matrix deze omzetten in [x,y,z,-1]
.
Ik heb een tussenstap gegeven om het effect van onze elementtransformatiematrix te laten zien. Als je niet vertrouwd bent met matrixwiskunde, is dat geen probleem. Het eurekamoment is dat we in de laatste regel de scrolloffset n bij onze y-coördinaat optellen in plaats van aftrekken. Het element wordt naar beneden verplaatst als we naar beneden scrollen.
Als we deze matrix echter alleen in ons voorbeeld gebruiken, wordt het element niet weergegeven. Dit komt doordat de CSS-specificatie vereist dat elke hoekpunt met w < 0 de weergave van het element blokkeert. En aangezien onze z-coördinaat momenteel 0 is en p 1, zal w -1 zijn.
Gelukkig kunnen we de waarde van z kiezen! Om zeker te zijn dat we eindigen met w=1, moeten we z = -2 instellen.
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
translateZ(-2px);
}
En kijk eens aan, onze box is terug !
Stap 2: Laat het bewegen
Nu staat onze box er en ziet er precies zo uit als zonder transformaties. Op dit moment is de perspectiefcontainer niet scrollbaar, dus we kunnen hem niet zien, maar we weten dat ons element de andere kant op zal gaan wanneer er gescrold wordt. Dus laten we de container scrollen, oké? We kunnen gewoon een spacer-element toevoegen dat ruimte inneemt:
<div class="container">
<div class="box"></div>
<span class="spacer"></span>
</div>
<style>
/* … all the styles from the previous example … */
.container {
overflow: scroll;
}
.spacer {
display: block;
height: 500px;
}
</style>
En scroll nu het vakje ! Het rode vakje beweegt naar beneden.
Stap 3: Geef het een maat
We hebben een element dat naar beneden beweegt wanneer de pagina naar beneden scrollt. Dat is eigenlijk het lastige deel. Nu moeten we het stylen zodat het op een schuifbalk lijkt en het wat interactiever maken.
Een schuifbalk bestaat meestal uit een 'thumb' en een 'track', waarbij de track niet altijd zichtbaar is. De hoogte van de thumb is recht evenredig met hoeveel van de content zichtbaar is.
<script>
const scroller = document.querySelector('.container');
const thumb = document.querySelector('.box');
const scrollerHeight = scroller.getBoundingClientRect().height;
thumb.style.height = /* ??? */;
</script>
scrollerHeight
is de hoogte van het scrollbare element, terwijl scroller.scrollHeight
de totale hoogte van de scrollbare content is. scrollerHeight/scroller.scrollHeight
is het deel van de content dat zichtbaar is. De verhouding van de verticale ruimte die de duim beslaat, moet gelijk zijn aan de verhouding van de content die zichtbaar is:
<script>
// …
thumb.style.height =
scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
// Accommodate for native scrollbars
thumb.style.right =
(scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>
De grootte van de duim ziet er goed uit , maar hij beweegt veel te snel. Hier kunnen we onze techniek van de parallax-scroller gebruiken. Als we het element verder naar achteren verplaatsen, beweegt het langzamer tijdens het scrollen. We kunnen de grootte corrigeren door het te vergroten. Maar hoeveel moeten we het precies naar achteren verplaatsen? Laten we wat – je raadt het al – rekenen! Dit is de laatste keer, beloofd.
De cruciale informatie is dat we willen dat de onderkant van de duim uitgelijnd is met de onderkant van het scrollbare element wanneer er helemaal naar beneden wordt gescrold. Met andere woorden: als we scroller.scrollHeight - scroller.height
pixels hebben gescrold, willen we dat onze duim wordt vertaald met scroller.height - thumb.height
. Voor elke pixel van de scroller willen we dat onze duim een fractie van een pixel beweegt:
Dat is onze schaalfactor. Nu moeten we de schaalfactor omzetten in een translatie langs de z-as, wat we al hebben gedaan in het artikel over parallax scrolling. Volgens het relevante gedeelte in de specificatie : De schaalfactor is gelijk aan p/(p − z). We kunnen deze vergelijking voor z oplossen om te berekenen hoeveel we onze duim langs de z-as moeten transleren. Houd er echter rekening mee dat we vanwege onze w-coördinaten nog eens -2px
langs z moeten transleren. Merk ook op dat de transformaties van een element van rechts naar links worden toegepast, wat betekent dat alle translaties vóór onze speciale matrix niet worden geïnverteerd, maar alle translaties ná onze speciale matrix wel! Laten we dit vastleggen!
<script>
// ... code from above...
const factor =
(scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
thumb.style.transform = `
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
scale(${1/factor})
translateZ(${1 - 1/factor}px)
translateZ(-2px)
`;
</script>
We hebben een schuifbalk ! En het is gewoon een DOM-element dat we naar wens kunnen stylen. Een belangrijke toegankelijkheidsmaatregel is om de duim te laten reageren op klikken en slepen, aangezien veel gebruikers gewend zijn om op die manier met een schuifbalk te werken. Om deze blogpost niet nog langer te maken, ga ik de details hiervan niet uitleggen. Bekijk de bibliotheekcode voor meer informatie als je wilt zien hoe het werkt.
Hoe zit het met iOS?
Ah, mijn oude vriend iOS Safari. Net als bij parallax scrollen lopen we hier tegen een probleem aan. Omdat we scrollen op een element, moeten we -webkit-overflow-scrolling: touch
specificeren, maar dat veroorzaakt 3D-afvlakking en ons hele scrolleffect werkt niet meer. We hebben dit probleem in de parallax scroller opgelost door iOS Safari te detecteren en position: sticky
als tijdelijke oplossing te gebruiken, en we doen hier precies hetzelfde. Bekijk het artikel over parallax scrollen om je geheugen op te frissen.
Hoe zit het met de schuifbalk van de browser?
Op sommige systemen zullen we te maken krijgen met een permanente, native scrollbalk. Historisch gezien kan de scrollbalk niet worden verborgen (behalve met een niet-standaard pseudoselector ). Om hem te verbergen, moeten we dus een (wiskundige) hack gebruiken. We verpakken ons scrollelement in een container met overflow-x: hidden
en maken het scrollelement breder dan de container. De native scrollbalk van de browser is nu onzichtbaar.
Vin
Als we alles bij elkaar voegen, kunnen we een frame-perfecte aangepaste schuifbalk bouwen, zoals die in onze Nyan Cat-demo .
Als je Nyan Cat niet kunt zien, heb je last van een bug die we hebben gevonden en opgelost tijdens het bouwen van deze demo (klik op de thumbnail om Nyan Cat te laten verschijnen). Chrome is erg goed in het vermijden van onnodig werk, zoals het tekenen of animeren van dingen die buiten beeld zijn. Het slechte nieuws is dat onze matrix-trucs Chrome laten denken dat de Nyan Cat-gif daadwerkelijk buiten beeld is. Hopelijk wordt dit snel opgelost.
Zo, dat was een hoop werk. Mijn complimenten dat je het hele stuk hebt gelezen. Het is echt een huzarenstukje om dit werkend te krijgen en het is waarschijnlijk zelden de moeite waard, behalve wanneer een aangepaste schuifbalk een essentieel onderdeel van de ervaring is. Maar goed om te weten dat het mogelijk is, toch? Dat het zo moeilijk is om een aangepaste schuifbalk te maken, laat zien dat er aan de kant van CSS nog werk aan de winkel is. Maar wees niet bang! In de toekomst gaat Houdini 's AnimationWorklet frame-perfecte scroll-linked effecten zoals deze een stuk eenvoudiger maken.