CSS-Deep-Dive – matrix3d() für eine benutzerdefinierte Bildlaufleiste mit perfekten Frame-Optionen

Benutzerdefinierte Scrollbalken sind extrem selten, was vor allem daran liegt, dass Scrollbalken zu den wenigen Elementen im Web gehören, die sich kaum gestalten lassen (ich schaue dich an, Datumsauswahl). Sie können JavaScript verwenden, um eigene zu erstellen, aber das ist teuer, wenig originalgetreu und kann sich verzögert anfühlen. In diesem Artikel verwenden wir einige unkonventionelle CSS-Matrizen, um einen benutzerdefinierten Scroller zu erstellen, für den beim Scrollen kein JavaScript erforderlich ist, sondern nur etwas Einrichtungscode.

Kurzfassung

Sie kümmern sich nicht um Kleinigkeiten? Sie möchten sich nur die Nyan Cat-Demo ansehen und die Bibliothek herunterladen? Den Code für die Demo finden Sie in unserem GitHub-Repository.

LAM;WRA (Lang und mathematisch; wird trotzdem gelesen)

Vor einiger Zeit haben wir einen Parallax-Scroller entwickelt. Haben Sie den Artikel dazu gelesen? Es ist wirklich gut und lohnt sich, sich die Zeit dafür zu nehmen. Durch das Zurücksetzen von Elementen mit CSS-3D-Transformationen bewegten sich die Elemente langsamer als unsere tatsächliche Scrollgeschwindigkeit.

Zusammenfassung

Sehen wir uns zuerst noch einmal an, wie der Parallax-Scroller funktioniert hat.

Wie in der Animation zu sehen ist, haben wir den Parallaxeneffekt erzielt, indem wir Elemente im 3D-Raum entlang der Z-Achse „nach hinten“ verschoben haben. Das Scrollen eines Dokuments entspricht einer Translation entlang der Y-Achse. Wenn wir also beispielsweise nach unten scrollen, werden alle Elemente um 100 Pixel nach oben verschoben. Das gilt für alle Elemente, auch für die, die „weiter hinten“ sind. Da sie jedoch weiter von der Kamera entfernt sind, ist die beobachtete Bewegung auf dem Bildschirm geringer als 100 Pixel, was den gewünschten Parallaxeneffekt erzeugt.

Wenn ein Element nach hinten verschoben wird, erscheint es natürlich auch kleiner. Das korrigieren wir, indem wir das Element wieder vergrößern. Wir haben die genaue Berechnung beim Erstellen des Parallax-Scrollers herausgefunden, daher werde ich nicht alle Details wiederholen.

Schritt 0: Was möchten wir tun?

Bildlaufleisten Genau das werden wir entwickeln. Aber haben Sie sich jemals wirklich Gedanken darüber gemacht, was sie tun? Ich ganz sicher nicht. Scrollbalken geben an, wie viel der verfügbaren Inhalte derzeit sichtbar ist und wie weit Sie als Leser bereits gekommen sind. Wenn Sie nach unten scrollen, bewegt sich auch die Scrollleiste nach unten, um anzuzeigen, dass Sie sich dem Ende nähern. Wenn alle Inhalte in den Darstellungsbereich passen, wird die Scrollleiste normalerweise ausgeblendet. Wenn der Inhalt die doppelte Höhe des Darstellungsbereichs hat, füllt die Scrollleiste die Hälfte der Höhe des Darstellungsbereichs aus. Wenn der Inhalt dreimal so hoch ist wie der Darstellungsbereich, wird die Scrollleiste auf ein Drittel des Darstellungsbereichs skaliert. Anstatt zu scrollen, können Sie auch auf die Scrollleiste klicken und sie ziehen, um schneller durch die Website zu navigieren. Das ist eine überraschende Menge an Verhalten für ein so unauffälliges Element. Wir kämpfen eine Schlacht nach der anderen.

Schritt 1: Rückwärtsgang einlegen

Okay, mit CSS-3D-Transformationen können wir Elemente langsamer als die Scrollgeschwindigkeit bewegen, wie im Artikel zum Parallax-Scrolling beschrieben. Können wir die Richtung auch umkehren? Es stellt sich heraus, dass wir das können, und das ist unser Weg, um eine bildgenaue, benutzerdefinierte Scrollleiste zu erstellen. Um zu verstehen, wie das funktioniert, müssen wir uns zuerst einige Grundlagen von CSS 3D ansehen.

Um eine perspektivische Projektion im mathematischen Sinne zu erhalten, werden Sie höchstwahrscheinlich homogene Koordinaten verwenden. Ich werde nicht im Detail darauf eingehen, was sie sind und warum sie funktionieren, aber Sie können sie sich wie 3D-Koordinaten mit einer zusätzlichen, vierten Koordinate namens w vorstellen. Diese Koordinate sollte 1 sein, es sei denn, Sie möchten eine perspektivische Verzerrung. Wir müssen uns nicht um die Details von w kümmern, da wir keinen anderen Wert als 1 verwenden werden. Daher sind alle Punkte ab sofort 4-dimensionale Vektoren [x, y, z, w=1] und folglich müssen auch Matrizen 4x4 sein.

Ein Beispiel dafür, dass CSS im Hintergrund homogene Koordinaten verwendet, ist, wenn Sie eigene 4×4-Matrizen in einer „transform“-Eigenschaft mit der Funktion matrix3d() definieren. matrix3d verwendet 16 Argumente (da die Matrix 4 × 4 ist), die eine Spalte nach der anderen angeben. Wir können diese Funktion also verwenden, um Rotationen, Translationen usw. manuell anzugeben. Sie ermöglicht es uns aber auch, mit der w-Koordinate zu arbeiten.

Bevor wir matrix3d() verwenden können, benötigen wir einen 3D-Kontext, da es ohne 3D-Kontext keine perspektivische Verzerrung und keinen Bedarf an homogenen Koordinaten gäbe. Um einen 3D-Kontext zu erstellen, benötigen wir einen Container mit einem perspective und einigen Elementen darin, die wir im neu erstellten 3D-Raum transformieren können. Beispiel:

Ein CSS-Code, mit dem ein Div-Element mithilfe des CSS-Attributs „perspective“ verzerrt wird.

Die Elemente in einem Perspektiven-Container werden von der CSS-Engine so verarbeitet:

  • Wandeln Sie jede Ecke (Vertex) eines Elements in homogene Koordinaten [x,y,z,w] relativ zum Perspektivcontainer um.
  • Wenden Sie alle Transformationen des Elements als Matrizen von rechts nach links an.
  • Wenn das Perspektivenelement scrollbar ist, wenden Sie eine Scrollmatrix an.
  • Perspektivmatrix anwenden

Die Scrollmatrix ist eine Translation entlang der Y-Achse. Wenn wir 400 px nach unten scrollen, müssen alle Elemente um 400 px nach oben verschoben werden. Die Perspektivmatrix ist eine Matrix, die Punkte umso näher an den Fluchtpunkt zieht, je weiter sie sich im 3D‑Raum hinten befinden. Dadurch werden Objekte, die weiter hinten liegen, kleiner dargestellt und sie „bewegen sich langsamer“, wenn sie verschoben werden. Wenn ein Element also nach hinten verschoben wird, führt eine Verschiebung um 400 px dazu, dass sich das Element auf dem Bildschirm nur um 300 px bewegt.

Wenn Sie alle Details wissen möchten, sollten Sie die Spezifikation des Transform-Rendering-Modells des CSS lesen. Für diesen Artikel habe ich den Algorithmus oben jedoch vereinfacht.

Unser Feld befindet sich in einem Perspektivcontainer mit dem Wert „p“ für das Attribut perspective. Nehmen wir an, der Container ist scrollbar und wird um „n“ Pixel nach unten gescrollt.

Die Perspektivmatrix multipliziert mit der Scrollmatrix multipliziert mit der Elementtransformationsmatrix ergibt eine 4 × 4-Einheitsmatrix mit minus 1 über p in der vierten Zeile, dritten Spalte multipliziert mit einer 4 × 4-Einheitsmatrix mit minus n in der zweiten Zeile, vierten Spalte multipliziert mit der Elementtransformationsmatrix.

Die erste Matrix ist die Perspektivmatrix, die zweite die Scrollmatrix. Zusammenfassend lässt sich sagen, dass die Scrollmatrix dafür sorgt, dass sich ein Element nach oben bewegt, wenn wir nach unten scrollen. Daher das Minuszeichen.

Für unsere Scrollleiste möchten wir jedoch das Gegenteil: Das Element soll sich nach unten bewegen, wenn wir nach unten scrollen. Hier können wir einen Trick anwenden: Wir kehren die w-Koordinate der Ecken unseres Rechtecks um. Wenn die w-Koordinate -1 ist, werden alle Übersetzungen in die entgegengesetzte Richtung ausgeführt. Wie funktioniert das? Die CSS-Engine kümmert sich darum, die Ecken unseres Rechtecks in homogene Koordinaten zu konvertieren und w auf 1 zu setzen. Jetzt ist es an der Zeit, dass matrix3d() glänzt!

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

Diese Matrix negiert lediglich w. Wenn die CSS-Engine also jede Ecke in einen Vektor der Form [x,y,z,1] umgewandelt hat, wird er durch die Matrix in [x,y,z,-1] umgewandelt.

Vier mal vier Einheitsmatrix mit minus eins über p in der vierten Zeile, dritten Spalte mal vier mal vier Einheitsmatrix mit minus n in der zweiten Zeile, vierten Spalte mal vier mal vier Einheitsmatrix mit minus eins in der vierten Zeile, vierten Spalte mal vierdimensionaler Vektor x, y, z, 1 gleich vier mal vier Einheitsmatrix mit minus eins über p in der vierten Zeile, dritten Spalte, minus n in der zweiten Zeile, vierten Spalte und minus eins in der vierten Zeile, vierten Spalte gleich vierdimensionaler Vektor x, y plus n, z, minus z über p minus 1.

Ich habe einen Zwischenschritt aufgeführt, um die Wirkung unserer Elementtransformationsmatrix zu zeigen. Wenn Sie sich mit Matrixmathematik nicht auskennen, ist das kein Problem. Wichtig ist, dass wir in der letzten Zeile den Scroll-Offset n zu unserer y-Koordinate addieren, anstatt ihn zu subtrahieren. Das Element wird nach unten verschoben, wenn wir nach unten scrollen.

Wenn wir diese Matrix jedoch einfach in unser Beispiel einfügen, wird das Element nicht angezeigt. Das liegt daran, dass in der CSS-Spezifikation festgelegt ist, dass jeder Eckpunkt mit w < 0 das Rendern des Elements verhindert. Da unsere z-Koordinate derzeit 0 und p 1 ist, ist w gleich -1.

Glücklicherweise können wir den Wert von z auswählen. Damit w=1 ist, muss z = -2 sein.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

Und siehe da, unsere Box ist wieder da!

Schritt 2: Bewegung einfügen

Jetzt ist unser Rechteck da und sieht genauso aus wie ohne Transformationen. Der Perspektivcontainer ist derzeit nicht scrollbar, daher können wir ihn nicht sehen. Wir wissen aber, dass sich unser Element beim Scrollen in die andere Richtung bewegt. Lassen Sie uns also dafür sorgen, dass der Container scrollt. Wir können einfach ein Platzhalterelement hinzufügen, das Platz einnimmt:

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

Und jetzt scrollen Sie im Feld! Das rote Feld wird nach unten verschoben.

Schritt 3: Größe angeben

Wir haben ein Element, das sich nach unten bewegt, wenn die Seite nach unten gescrollt wird. Das ist eigentlich der schwierige Teil. Jetzt müssen wir das Element so gestalten, dass es wie eine Scrollleiste aussieht, und es etwas interaktiver machen.

Eine Scrollleiste besteht in der Regel aus einem „Ziehpunkt“ und einer „Spur“, wobei die Spur nicht immer sichtbar ist. Die Höhe des Thumbnails ist direkt proportional dazu, wie viel vom Inhalt sichtbar ist.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight ist die Höhe des scrollbaren Elements und scroller.scrollHeight die Gesamthöhe des scrollbaren Inhalts. scrollerHeight/scroller.scrollHeight ist der Anteil des Inhalts, der sichtbar ist. Das Verhältnis des vertikalen Raums, den der Thumb abdeckt, sollte dem Verhältnis des sichtbaren Inhalts entsprechen:

Die Höhe des Thumb-Punkts im Verhältnis zur Höhe des Scrollbalkens entspricht der Höhe des Scrollbalkens im Verhältnis zur Höhe des Scrollbalken-Punkts, wenn und nur wenn die Höhe des Thumb-Punkts der Höhe des Scrollbalkens multipliziert mit der Höhe des Scrollbalkens im Verhältnis zur Höhe des Scrollbalken-Punkts entspricht.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Die Größe des Daumens sieht gut aus, aber er bewegt sich viel zu schnell. Hier können wir uns die Technik vom Parallax-Scroller ansehen. Wenn wir das Element weiter nach hinten verschieben, bewegt es sich beim Scrollen langsamer. Wir können die Größe korrigieren, indem wir sie vergrößern. Aber wie viel genau? Rechnen wir mal ein bisschen. Das ist das letzte Mal, versprochen.

Wichtig ist, dass die Unterkante des Thumbnails mit der Unterkante des scrollbaren Elements übereinstimmt, wenn ganz nach unten gescrollt wird. Anders ausgedrückt: Wenn wir scroller.scrollHeight - scroller.height Pixel gescrollt haben, soll der Thumb um scroller.height - thumb.height verschoben werden. Für jedes Pixel des Scrollers soll sich der Thumb um einen Bruchteil eines Pixels bewegen:

Der Faktor entspricht der Höhe des Scroller-Punkts minus der Höhe des Thumb-Punkts geteilt durch die Scrollhöhe des Scroller-Punkts minus der Höhe des Scroller-Punkts.

Das ist unser Skalierungsfaktor. Jetzt müssen wir den Skalierungsfaktor in eine Translation entlang der Z-Achse umwandeln. Das haben wir bereits im Artikel zum Parallax-Scrolling getan. Gemäß dem relevanten Abschnitt in der Spezifikation gilt: Der Skalierungsfaktor entspricht p/(p − z). Wir können diese Gleichung nach z auflösen, um herauszufinden, wie weit wir den Daumen entlang der z-Achse verschieben müssen. Aufgrund der w-Koordinate müssen wir jedoch zusätzlich -2px entlang der Z-Achse verschieben. Außerdem werden die Transformationen eines Elements von rechts nach links angewendet. Das bedeutet, dass alle Translationen vor unserer speziellen Matrix nicht invertiert werden, alle Translationen nach unserer speziellen Matrix jedoch schon. Lass uns das codieren!

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

Wir haben eine Bildlaufleiste. Es ist nur ein DOM-Element, das wir nach Belieben gestalten können. In Bezug auf die Barrierefreiheit ist es wichtig, dass der Ziehpunkt auf Klicken und Ziehen reagiert, da viele Nutzer es gewohnt sind, auf diese Weise mit einer Scrollleiste zu interagieren. Um diesen Blogpost nicht noch länger zu machen, werde ich die Details für diesen Teil nicht erklären. Wenn Sie wissen möchten, wie das funktioniert, können Sie sich den Bibliothekscode ansehen.

Wie sieht es mit iOS aus?

Ah, mein alter Freund iOS Safari. Wie beim Parallaxen-Scrolling gibt es auch hier ein Problem. Da wir auf einem Element scrollen, müssen wir -webkit-overflow-scrolling: touch angeben. Das führt jedoch zu einer 3D-Verebnung und der gesamte Scrolling-Effekt funktioniert nicht mehr. Wir haben dieses Problem im Parallax-Scroller behoben, indem wir iOS Safari erkannt und position: sticky als Workaround verwendet haben. Genau das werden wir auch hier tun. Hier findest du einen Artikel zum Thema Parallaxing.

Was ist mit der Scrollleiste des Browsers?

Bei einigen Systemen müssen wir mit einer permanenten, nativen Scrollleiste arbeiten. Bisher konnte die Scrollleiste nur mit einem nicht standardmäßigen Pseudoselektor ausgeblendet werden. Um es zu verbergen, müssen wir also auf einige (mathefreie) Tricks zurückgreifen. Wir umschließen das Scrolling-Element mit einem Container mit overflow-x: hidden und machen das Scrolling-Element breiter als den Container. Die native Scrollleiste des Browsers ist jetzt nicht mehr sichtbar.

Fin

Wenn wir alles zusammenfügen, können wir jetzt eine benutzerdefinierte Scrollleiste erstellen, die framegenau ist – wie in unserer Nyan Cat-Demo.

Wenn Sie die Nyan Cat nicht sehen, liegt das an einem Fehler, den wir bei der Entwicklung dieser Demo gefunden und gemeldet haben. Klicken Sie auf das Vorschaubild, um die Nyan Cat anzuzeigen. Chrome ist sehr gut darin, unnötige Arbeit zu vermeiden, z. B. das Rendern oder Animieren von Elementen, die sich außerhalb des Bildschirms befinden. Die schlechte Nachricht ist, dass Chrome aufgrund unserer Matrix-Spielereien denkt, dass das Nyan-Cat-GIF tatsächlich außerhalb des Bildschirms ist. Wir hoffen, dass das Problem bald behoben wird.

Das war es auch schon. Das war viel Arbeit. Ich ziehe meinen Hut vor Ihnen, dass Sie sich das alles durchgelesen haben. Das ist ein echter Trick, um das zum Laufen zu bringen, und es lohnt sich wahrscheinlich nur selten, außer wenn eine benutzerdefinierte Scrollleiste ein wesentlicher Bestandteil der Nutzererfahrung ist. Aber es ist gut zu wissen, dass es möglich ist, oder? Die Tatsache, dass es so schwierig ist, eine benutzerdefinierte Scrollleiste zu erstellen, zeigt, dass es auf CSS-Seite noch einiges zu tun gibt. Aber keine Sorge. In Zukunft wird Houdinis AnimationWorklet framegenaue, scrollgebundene Effekte wie diesen viel einfacher machen.