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

Benutzerdefinierte Bildlaufleisten sind extrem selten. Das liegt vor allem daran, dass Bildlaufleisten zu den wenigen Elementen im Web gehören, die sich kaum stilisieren lassen (ich schaue Sie an, Datumsauswahl). Sie können Ihre eigenen mit JavaScript erstellen, aber das ist teuer, hat eine geringe Wiedergabetreue und kann zu Verzögerungen führen. In diesem Artikel verwenden wir einige unkonventionelle CSS-Matrizen, um einen benutzerdefinierten Scroller zu erstellen, der beim Scrollen kein JavaScript, sondern nur etwas Einrichtungscode erfordert.

Kurzfassung

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

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

Vor einiger Zeit haben wir einen Paralax-Scroller erstellt. Haben Sie diesen Artikel gelesen? Es ist wirklich gut und lohnt sich!). Durch das Zurückschieben von Elementen mithilfe von CSS-3D-Transformationen bewegten sich die Elemente langsamer als die tatsächliche Scrollgeschwindigkeit.

Zusammenfassung

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

Wie in der Animation zu sehen, haben wir den Parallaxeneffekt erzielt, indem wir Elemente im 3D-Raum entlang der Z‑Achse „nach hinten“ geschoben haben. Das Scrollen in einem Dokument ist eine Verschiebung entlang der Y-Achse. Wenn wir also beispielsweise 100 Pixel nach unten scrollen, wird jedes Element 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, beträgt ihre beobachtete Bewegung auf dem Bildschirm weniger als 100 Pixel, was den gewünschten Parallaxeneffekt ergibt.

Wenn ein Element nach hinten verschoben wird, erscheint es natürlich auch kleiner. Das korrigieren wir, indem wir das Element wieder vergrößern. Die genauen Berechnungen haben wir beim Erstellen des Parallax-Scrollers herausgefunden. Ich werde also nicht alle Details wiederholen.

Schritt 0: Was möchten wir tun?

Bildlaufleisten Das ist es, was wir bauen werden. Aber haben Sie sich schon einmal wirklich darüber Gedanken gemacht, was sie tun? Ich habe das jedenfalls nicht getan. Scrollbalken geben Aufschluss darüber, wie viel der verfügbaren Inhalte derzeit sichtbar ist und welchen Fortschritt Sie als Leser bereits gemacht haben. Wenn Sie nach unten scrollen, bewegt sich auch die Bildlaufleiste, um anzuzeigen, dass Sie dem Ende näher kommen. Wenn alle Inhalte in den Darstellungsbereich passen, ist die Bildlaufleiste normalerweise ausgeblendet. Wenn der Inhalt doppelt so hoch wie der Darstellungsbereich ist, nimmt der Bildlaufbalken die Hälfte der Höhe des Darstellungsbereichs ein. Bei Inhalten, die dreimal so hoch wie der Darstellungsbereich sind, wird die Bildlaufleiste auf ein Drittel des Darstellungsbereichs skaliert usw. Sie sehen das Muster. Anstatt zu scrollen, können Sie auch auf die Bildlaufleiste klicken und sie ziehen, um sich schneller auf der Website zu bewegen. Das ist eine überraschende Menge an Verhalten für ein unscheinbares Element wie dieses. Lass uns einen Schritt nach dem anderen machen.

Schritt 1: Rückwärtsgang einlegen

Okay, wir können Elemente mit CSS-3D-Transformationen langsamer als die Scrollgeschwindigkeit bewegen, wie im Artikel zum Parallax-Scrolling beschrieben. Können wir auch die Richtung umkehren? Das ist möglich und so können wir eine scrollbare Ansicht erstellen, die perfekt zu den Frames passt. Um zu verstehen, wie das funktioniert, müssen wir zuerst einige CSS 3D-Grundlagen behandeln.

Wenn Sie eine perspektivische Projektion im mathematischen Sinne erhalten möchten, verwenden Sie am besten homogene Koordinaten. Ich gehe nicht näher darauf ein, was sie sind und warum sie funktionieren, aber Sie können sie sich als 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 haben. Wir müssen uns keine Gedanken über die Details von w machen, da wir keinen anderen Wert als 1 verwenden werden. Daher sind alle Punkte ab sofort vierdimensionale Vektoren [x, y, z, w=1] und Matrizen müssen ebenfalls 4 × 4 sein.

Eine Möglichkeit, zu sehen, dass in CSS homogene Koordinaten verwendet werden, ist, wenn Sie eigene 4 × 4-Matrizen in einer Transform-Eigenschaft mithilfe der Funktion matrix3d() definieren. matrix3d nimmt 16 Argumente an (da die Matrix 4 × 4 Elemente hat), wobei eine Spalte nach der anderen angegeben wird. Mit dieser Funktion können wir also Drehungen, Verschiebungen usw. manuell angeben. Außerdem können wir damit die w-Koordinate ändern.

Bevor wir matrix3d() verwenden können, benötigen wir einen 3D-Kontext. Denn ohne 3D-Kontext gäbe es keine perspektivische Verzerrung und keine Notwendigkeit für homogene Koordinaten. Um einen 3D-Kontext zu erstellen, benötigen wir einen Container mit einer perspective und einigen Elementen, die wir im neu erstellten 3D-Raum transformieren können. Beispiel:

Ein CSS-Code-Snippet, das ein Div-Element mithilfe des CSS-Attributs „perspective“ verzerrt.

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

  • Wandeln Sie jede Ecke (Knotenpunkt) eines Elements in homogene Koordinaten [x,y,z,w] um, bezogen auf den perspektivischen Container.
  • Wenden Sie alle Transformationen des Elements als Matrizen von rechts nach links an.
  • Wenn das perspektivische Element scrollbar ist, wenden Sie eine Scrollmatrix an.
  • Wenden Sie die Perspektivmatrix an.

Die Scrollmatrix ist eine Verschiebung 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 je weiter sie im 3D-Raum nach hinten liegen, näher an den Fluchtpunkt heranzieht. Dadurch wirken Objekte, die weiter hinten sind, kleiner und bewegen sich beim Schwenken langsamer. Wenn ein Element also nach hinten verschoben wird, bewegt es sich bei einer Verschiebung von 400 Pixeln nur um 300 Pixel auf dem Bildschirm.

Wenn Sie alle Details erfahren möchten, sollten Sie die Spezifikation zum Transformierungs-Rendering-Modell des Preisvergleichsportals lesen. Für diesen Artikel habe ich den Algorithmus jedoch vereinfacht.

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

Die Matrix für die Perspektive multipliziert mit der Scrollmatrix multipliziert mit der Matrix für die Elementtransformation ist gleich der vier mal vier großen Identitätsmatrix mit minus 1 ÷ p in der vierten Zeile, dritten Spalte multipliziert mit der vier mal vier großen Identitätsmatrix mit minus n in der zweiten Zeile, vierten Spalte multipliziert mit der Matrix für die Elementtransformation.

Die erste Matrix ist die Perspektivematrix, die zweite die Scrollmatrix. Zur Wiederholung: Die Scrollmatrix sorgt dafür, dass sich ein Element nach oben bewegt, wenn wir nach unten scrollen, daher das negative Vorzeichen.

Bei unserer Bildlaufleiste möchten wir jedoch das Gegenteil erreichen: 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 Quadrats um. Wenn die w-Koordinate -1 ist, werden alle Verschiebungen in die entgegengesetzte Richtung ausgeführt. Wie gehen wir vor? Die CSS-Engine kümmert sich um die Umwandlung der Ecken unseres Quadrats in homogene Koordinaten und setzt w auf 1. 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 tut nichts anderes, als w zu negieren. Wenn die CSS-Engine also jede Ecke in einen Vektor der Form [x,y,z,1] umgewandelt hat, wandelt die Matrix ihn in [x,y,z,-1] um.

Die 4 × 4-Einheitsmatrix mit − 1 ÷ p in der 4. Zeile, 3. Spalte multipliziert mit der 4 × 4-Einheitsmatrix mit − n in der 2. Zeile, 4. Spalte multipliziert mit der 4 × 4-Einheitsmatrix mit − 1 in der 4. Zeile, 4. Spalte multipliziert mit dem vierdimensionalen Vektor x, y, z, 1 ergibt die 4 × 4-Einheitsmatrix mit − 1 ÷ p in der 4. Zeile, 3. Spalte, − n in der 2. Zeile, 4. Spalte und − 1 in der 4. Zeile, 4. Spalte, multipliziert mit dem vierdimensionalen Vektor x, y + n, z, − z ÷ p − 1.

Ich habe einen Zwischenschritt aufgeführt, um die Wirkung unserer Elementtransformationsmatrix zu veranschaulichen. Wenn Sie mit Matrizenmathematik nicht vertraut sind, ist das in Ordnung. Der Aha-Moment ist, dass wir in der letzten Zeile den Scroll-Offset n unserer y-Koordinate hinzufügen, anstatt ihn abzuziehen. 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 gemäß der CSS-Spezifikation jeder Eckpunkt mit w < 0 das Rendern des Elements verhindert. Da unsere Z‑Koordinate derzeit 0 und p 1 ist, ist w −1.

Glücklicherweise können wir den Wert von z auswählen. Damit wir am Ende w=1 erhalten, müssen wir z = −2 festlegen.

.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 zurück!

Schritt 2: Bewegung hinzufügen

Jetzt ist unser Feld da und sieht genauso aus wie ohne Transformationen. Der perspektivische Container ist derzeit nicht scrollbar, sodass wir ihn nicht sehen können. Wir wissen jedoch, dass sich unser Element beim Scrollen in die andere Richtung bewegt. Lassen Sie uns also den Container scrollen. Wir können einfach ein Abstandselement 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 durch das Feld. Das rote Feld wird nach unten verschoben.

Schritt 3: Größe festlegen

Wir haben ein Element, das nach unten bewegt wird, wenn auf der Seite nach unten gescrollt wird. Das Schwierige ist geschafft. Jetzt müssen wir ihm ein scrollbares Aussehen geben und es etwas interaktiver gestalten.

Eine Bildlaufleiste besteht in der Regel aus einem „Schieberegler“ und einem „Track“, wobei der Track nicht immer sichtbar ist. Die Höhe des Vorschaubilds 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, während scroller.scrollHeight die Gesamthöhe des scrollbaren Inhalts ist. scrollerHeight/scroller.scrollHeight ist der sichtbare Anteil des Inhalts. Das Verhältnis des vertikalen Bereichs, den der Vorschaubereich einnimmt, sollte dem Verhältnis der sichtbaren Inhalte entsprechen:

Die Höhe des Punkts im Stil des Schiebereglers geteilt durch die Höhe des Schiebereglers entspricht der Höhe des Schiebereglers geteilt durch die Höhe des Scrollpunkts, wenn und nur wenn die Höhe des Punkts im Stil des Schiebereglers der Höhe des Schiebereglers multipliziert mit der Höhe des Schiebereglers geteilt durch die Höhe des Scrollpunkts 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 Vorschaubildes sieht gut aus, aber es bewegt sich viel zu schnell. Hier können wir unsere Technik aus dem Parallax-Scroller übernehmen. 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 weit sollten wir sie genau zurückschieben? Jetzt wird es mathematisch. Das ist das letzte Mal, versprochen.

Der untere Rand des Schiebereglers muss mit dem unteren Rand des scrollbaren Elements übereinstimmen, wenn Sie ganz nach unten gescrollt haben. Mit anderen Worten: Wenn wir scroller.scrollHeight - scroller.height Pixel gescrollt haben, soll der Daumen um scroller.height - thumb.height verschoben werden. Für jedes Pixel des Scrollers soll sich der Schieberegler um einen Bruchteil eines Pixels bewegen:

Der Faktor ist gleich der Höhe des Scrollpunkts abzüglich der Höhe des Schiebepunkts geteilt durch die Scrollhöhe des Punkts abzüglich der Höhe des Scrollpunkts.

Das ist unser Skalierungsfaktor. Jetzt müssen wir den Skalierungsfaktor in eine Verschiebung entlang der Z‑Achse umwandeln. Das haben wir bereits im Artikel zum Parallax-Scrolling getan. Gemäß dem entsprechenden Abschnitt in der Spezifikation: Der Skalierungsfaktor ist gleich p ÷ (p − z). Wir können diese Gleichung nach z lösen, um herauszufinden, wie weit wir unseren Daumen entlang der Z‑Achse verschieben müssen. Denken Sie aber daran, dass wir aufgrund unserer W‑Koordinaten-Spielereien eine zusätzliche -2px entlang von z verschieben müssen. Beachten Sie auch, dass die Transformationen eines Elements von rechts nach links angewendet werden. Das bedeutet, dass alle Verschiebungen vor unserer speziellen Matrix nicht umgekehrt werden, alle Verschiebungen nach unserer speziellen Matrix jedoch. Lasst uns das codifizieren.

<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 stylen können. Aus Sicht der Barrierefreiheit ist es wichtig, dass der Schieberegler auf „Klicken und Ziehen“ reagiert, da viele Nutzer mit einer solchen Interaktion mit einer Bildlaufleiste vertraut sind. Um diesen Blogpost nicht noch länger zu machen, werde ich die Details zu diesem Teil nicht erläutern. Weitere Informationen dazu finden Sie im Bibliothekscode.

Wie sieht es mit iOS aus?

Ah, mein alter Freund iOS Safari. Wie beim Parallaxen-Scrolling treten hier Probleme auf. Da wir über ein Element scrollen, müssen wir -webkit-overflow-scrolling: touch angeben. Das führt jedoch zu einer 3D-Ebenenansicht und der gesamte Scrolleffekt funktioniert nicht mehr. Wir haben dieses Problem im Paralax-Scroller gelöst, indem wir iOS Safari erkannt und position: sticky als Problemumgehung verwendet haben. Genau das machen wir hier auch. Sehen Sie sich den Artikel zu Parallaxen noch einmal an, um Ihr Gedächtnis aufzufrischen.

Was ist mit der Bildlaufleiste des Browsers?

Bei einigen Systemen müssen wir mit einer permanenten, nativen Bildlaufleiste arbeiten. Bisher konnte die Bildlaufleiste nicht ausgeblendet werden (außer mit einem nicht standardmäßigen Pseudo-Sellektor). Um sie zu verbergen, müssen wir also zu einigen (mathematikfreien) Hackertricks greifen. Wir legen unser scrollbares Element mit overflow-x: hidden in einen Container und machen es breiter als den Container. Die native Bildlaufleiste des Browsers ist jetzt nicht mehr sichtbar.

Fin

Wenn wir alles zusammenfügen, können wir jetzt einen benutzerdefinierten Bildlaufbalken erstellen, der rahmengenau ist – wie der in unserer Nyan-Katze-Demo.

Wenn Sie Nyan Cat nicht sehen, liegt ein Fehler vor, den wir bei der Erstellung dieser Demo gefunden und gemeldet haben. Klicken Sie auf den Daumen, um Nyan Cat zu sehen. Chrome vermeidet unnötige Arbeit, z. B. das Zeichnen oder Animieren von Elementen, die nicht auf dem Bildschirm zu sehen sind. 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 eine Menge Arbeit. Ich beglückwünsche Sie, dass Sie sich die Zeit genommen haben, den gesamten Artikel zu lesen. Es ist ziemlich schwierig, das zum Laufen zu bringen, und es lohnt sich wahrscheinlich nur selten, es zu versuchen – es sei denn, eine benutzerdefinierte Bildlaufleiste ist ein wesentlicher Bestandteil der Website. Aber es ist gut zu wissen, dass es möglich ist, oder? Die Tatsache, dass es so schwierig ist, eine benutzerdefinierte Bildlaufleiste zu erstellen, zeigt, dass es noch viel zu tun gibt. Aber keine Sorge! In Zukunft wird das AnimationWorklet von Houdini die Erstellung solcher frameperfekten scrollbasierten Effekte erheblich vereinfachen.