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

Benutzerdefinierte Bildlaufleisten sind extrem selten. Das liegt vor allem daran, dass sie eine der verbleibenden Elemente im Web sind, die so gut wie nicht stilbar sind. Sie können JavaScript verwenden, um Ihre eigenen zu erstellen, aber das ist teuer und Low-Fidelity und kann verzögert wirken. In diesem Artikel verwenden wir einige unkonventionelle CSS-Matrizen, um einen benutzerdefinierten Scroller zu erstellen, für den beim Scrollen kein JavaScript, sondern lediglich Einrichtungscode erforderlich ist.

Kurzfassung

Die kleinen Dinge sind dir nicht wichtig? Sie möchten sich nur die Nyan Cat-Demo ansehen und die Bibliothek holen? Den Democode finden Sie in unserem GitHub-Repository.

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

Vor einiger Zeit haben wir einen Parallaxe-Scroller entwickelt. Haben Sie diesen Artikel gelesen? Es ist wirklich lecker und die Zeit wert!). Durch das Zurückschieben von Elementen mithilfe von CSS-3D-Transformationen werden die Elemente langsamer als unsere tatsächliche Scrollgeschwindigkeit bewegt.

Zusammenfassung

Wir beginnen mit einer Zusammenfassung der Funktionsweise des Parallaxe-Scrollers.

Wie in der Animation gezeigt, haben wir den Parallaxe-Effekt erzielt, indem wir Elemente im 3D-Raum entlang der Z-Achse "nach hinten" verschoben haben. Das Scrollen eines Dokuments ist im Grunde eine Verschiebung entlang der Y-Achse. Wenn wir also um 100 Pixel nach unten scrollen, wird jedes Element um 100 Pixel nach oben übersetzt. Dies gilt für alle Elemente, auch für solche, die weiter hinter der Kamera liegen. Da sie jedoch weiter von der Kamera entfernt sind, beträgt die beobachtete Bewegung auf dem Bildschirm weniger als 100 Pixel, wodurch der gewünschte Parallaxe-Effekt erzielt wird.

Wenn Sie ein Element zurück in den Raum verschieben, erscheint es natürlich auch kleiner, was wir korrigieren können, indem Sie das Element nach oben skalieren. Beim Erstellen des Parallaxe-Scrollers haben wir die genaue Berechnung berechnet. Deshalb werde ich nicht alle Details wiederholen.

Schritt 0: Was möchten wir tun?

Bildlaufleisten Und genau das werden wir entwickeln. Aber haben Sie jemals darüber nachgedacht, was sie tun? Habe ich auf jeden Fall nicht. Bildlaufleisten geben Aufschluss darüber, wie viel des verfügbaren Inhalts derzeit sichtbar ist und wie viel Fortschritt Sie als Leser gemacht haben. Wenn Sie nach unten scrollen, zeigt auch die Bildlaufleiste an, dass Sie Fortschritte machen. Wenn der gesamte Inhalt in den Darstellungsbereich passt, ist die Bildlaufleiste normalerweise ausgeblendet. Wenn der Inhalt die doppelte Höhe des Darstellungsbereichs hat, füllt die Bildlaufleiste die Hälfte der Höhe des Darstellungsbereichs aus. Bei Inhalten mit der dreifachen Höhe des Darstellungsbereichs 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 diese ziehen, um schneller durch die Website zu navigieren. Bei einem unauffälligen Element wie diesem ist das überraschend. Kämpfe gegeneinander.

Schritt 1: Umgekehrte Reihenfolge

Mit CSS-3D-Transformationen können wir Elemente langsamer als die Scrollgeschwindigkeit bewegen, wie im Artikel zum Parallaxe-Scrollen beschrieben. Können wir die Richtung auch umkehren? Jetzt haben wir die Möglichkeit, eine benutzerdefinierte Bildlaufleiste mit perfekten Frames zu erstellen. Um zu verstehen, wie das funktioniert, müssen wir zunächst einige CSS-3D-Grundlagen behandeln.

Für jede Art von perspektivischer Projektion im mathematischen Sinn werden höchstwahrscheinlich homogene Koordinaten verwendet. Ich werde nicht ins Detail gehen, 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 haben. Um die Details von w müssen wir uns nicht kümmern, da wir keinen anderen Wert als 1 verwenden werden. Daher sind ab jetzt alle Punkte vierdimensionale Vektoren [x, y, z, w=1] und folglich müssen Matrizen auch 4 x 4 sein.

Sie können sehen, dass CSS im Hintergrund homogene Koordinaten verwendet, wenn Sie Ihre eigenen 4x4-Matrizen mit der Funktion matrix3d() in einer Transformationseigenschaft definieren. Für matrix3d sind 16 Argumente erforderlich (da die Matrix 4 x 4 hat), wodurch eine Spalte nach der anderen angegeben wird. Wir können also mit dieser Funktion Drehungen, Übersetzungen usw. manuell angeben. Sie ermöglicht uns aber auch, die w-Koordinate zu ändern.

Bevor wir matrix3d() verwenden können, benötigen wir einen 3D-Kontext, da ohne 3D-Kontext keine perspektivische Verzerrung entsteht und keine homogenen Koordinaten erforderlich sind. Zum Erstellen eines 3D-Kontexts benötigen wir einen Container mit einem perspective und einigen darin enthaltenen Elementen, die wir im neu erstellten 3D-Bereich transformieren können. Beispiel:

Ein CSS-Code, der ein div-Element mithilfe des CSS-Attributs „perspektive“ verzerrt.

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

  • Wandeln Sie jede Ecke (Scheitelpunkt) eines Elements in homogene Koordinaten [x,y,z,w] relativ zum perspektivischen Container um.
  • Alle Transformationen des Elements als Matrizen von rechts nach links anwenden.
  • Wenn das Perspective-Element scrollbar ist, wenden Sie eine Scroll-Matrix an.
  • Die Perspektivenmatrix anwenden.

Die Scroll-Matrix ist eine Verschiebung entlang der y-Achse. Wenn wir um 400 Pixel nach unten scrollen, müssen alle Elemente um 400 Pixel nach oben verschoben werden. Die perspektivische Matrix ist eine Matrix, die gezogen wird und je weiter sie sich im 3D-Raum befinden, näher an den Fluchtpunkt zieht. Dadurch wird beides verkleinert, wenn sie weiter rückwärts liegen, und sie „verlangsamen“ beim Übersetzen. Wenn also ein Element nach hinten verschoben wird, führt eine Übersetzung von 400 Pixeln dazu, dass das Element nur um 300 Pixel auf dem Bildschirm verschoben wird.

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

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

Perspektivische Matrix × Scroll-Matrix × Element-Transformationsmatrix ist vier mal vier Identitätsmatrix mit minus eins über p in der vierten Zeile, dritte Spalte multipliziert mit vier mal vier Einheitsmatrix mit minus n in der zweiten Zeile, vierten Spalte multipliziert mit der Elementtransformationsmatrix.

Die erste Matrix ist die perspektivische Matrix, die zweite Matrix die Scroll-Matrix. Zur Erinnerung: Die Aufgabe der Scroll-Matrix besteht darin, ein Element nach oben zu bewegen, wenn wir nach unten scrollen, daher das negative Vorzeichen.

Bei unserer Bildlaufleiste möchten wir jedoch das Gegenteil: Wir möchten, dass sich das Element nach unten bewegt, wenn wir nach unten scrollen. Hier können wir einen Trick anwenden: Umkehren der w-Koordinate der Ecken der Box. Wenn die w-Koordinate -1 ist, werden alle Übersetzungen in die entgegengesetzte Richtung wirksam. Wie machen wir das also? Die CSS-Engine wandelt die Ecken der Box in homogene Koordinaten um und legt w auf 1 fest. Es ist Zeit, zu matrix3d() zu glänzen!

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

Diese Matrix führt nichts anderes aus, 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 diese in [x,y,z,-1] um.

Vier mal vier Identitätsmatrix mit minus eins über p in der vierten Zeile, vierte Spalte multipliziert vier mal vier Einheitsmatrix mit minus n in der zweiten Zeile vierte Spalte mal viermal vier Identitätsmatrix mit minus eins in der vierten Zeile mal vierdimensionalen Vektor x, y, z, 1 entspricht vierdimensionalen Vektor x, y, z, 1 ist vier mal vierdimensionalen Vektor x, y, z, 1 ist vier mal vierdimensional in der vierten Zeile minus eins minus p in vierter Zeile und viermal n.

Ich habe einen Zwischenschritt aufgelistet, um die Auswirkungen der Element-Transformationsmatrix zu zeigen. Wenn Sie nicht mit Matrixberechnungen vertraut sind, ist das in Ordnung. Der Eureka-Moment besteht darin, dass wir in der letzten Zeile den Scroll-Offset n zu unserer Y-Koordinate addieren, anstatt ihn zu subtrahieren. Das Element wird nach unten übersetzt, wenn wir nach unten scrollen.

Wenn wir diese Matrix jedoch nur in unser Beispiel einfügen, wird das Element nicht angezeigt. Das liegt daran, dass gemäß der CSS-Spezifikation jeder Scheitelpunkt mit w < 0 das Rendern des Elements blockiert. Und da die Z-Koordinate derzeit 0 ist und p 1 ist, ist w -1.

Glücklicherweise können wir den Wert von z wählen! Um sicherzustellen, dass wir 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);
}

Schau mal, unsere Kisten sind zurück!

Schritt 2: Verschieben

Jetzt ist unsere Box hier und sieht genauso aus, wie sie ohne Transformationen aussehen würde. Im Moment ist der Perspektivencontainer nicht scrollbar, daher ist er nicht sichtbar. Wir wissen aber, dass das Element beim Scrollen in die andere Richtung geht. Lasst uns also den Container scrollen, oder? Wir fügen einfach ein Distanzelement hinzu, das Platz belegt:

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

Scrollen Sie jetzt im Feld. Das rote Feld befindet sich weiter nach unten.

Schritt 3: Größe festlegen

Wir haben ein Element, das nach unten bewegt wird, wenn die Seite nach unten scrollt. Das ist der schwierige Teil auf dem Weg zu gehen. Jetzt soll es wie eine Bildlaufleiste aussehen und interaktiver gestaltet werden.

Eine Bildlaufleiste besteht normalerweise aus einem Daumen und einem Titel, der Track ist jedoch nicht immer sichtbar. Die Höhe des Daumens 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 Bruchteil des sichtbaren Inhalts. Das Verhältnis des vertikalen Raums, in dem die Daumenabdeckungen sind, sollte dem Verhältnis des sichtbaren Inhalts entsprechen:

Thumbnail-Punkt-Stil Punkthöhe über ScrollerHöhe entspricht Scroller-Höhe über Scroller Punkt Scroll-Höhe, wenn und nur wenn Daumenpunkt-Stil Punkthöhe Scroller-Höhe mal Scroller-Höhe über Scroller-Punkt Scroll-Höhe.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Der Daumen ist groß, sieht gut aus, schreitet jedoch viel zu schnell voran. Hier können wir unsere Technik vom Parallaxe-Scroller übernehmen. Wird das Element weiter zurück verschoben, wird es beim Scrollen langsamer. Wir können die Größe korrigieren, indem wir sie vergrößern. Aber wie sehr sollten wir es genau zurückhalten? Lass uns ein bisschen rechnen – du hast es erraten! Das ist mein letztes Mal, versprochen.

Wichtig ist, dass der untere Rand des Daumens beim Scrollen ganz nach unten am unteren Rand des scrollbaren Elements ausgerichtet ist. Mit anderen Worten: Wenn wir scroller.scrollHeight - scroller.height Pixel gescrollt haben, möchten wir, dass unser Daumen von scroller.height - thumb.height übersetzt wird. Für jedes Scroller-Pixel soll sich der Daumen um einen Bruchteil eines Pixels bewegen:

Faktor ist gleich der Scroller-Punkthöhe minus der Daumenpunkthöhe über Scroller-Punkt-Scrollhöhe minus Scroller-Punkthöhe.

Das ist unser Skalierungsfaktor. Jetzt müssen wir den Skalierungsfaktor in eine Übersetzung entlang der z-Achse umwandeln, was wir bereits im Artikel mit Parallaxe-Scrollen getan haben. Gemäß dem entsprechenden Abschnitt in der Spezifikation gilt: Der Skalierungsfaktor ist gleich p/(p − z). Wir können diese Gleichung für z lösen, um herauszufinden, wie viel wir unseren Daumen entlang der z-Achse verschieben müssen. Beachten Sie jedoch, dass wir aufgrund von Unstimmigkeiten mit der Koordinaten-Koordinaten ein zusätzliches -2px-Zeichen entlang z. B. übersetzen müssen. Beachten Sie auch, dass die Transformationen eines Elements von rechts nach links angewendet werden. Das bedeutet, dass alle Übersetzungen vor unserer speziellen Matrix nicht invertiert werden, alle Übersetzungen nach der speziellen Matrix jedoch. 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. Und es ist nur ein DOM-Element, das wir nach Belieben gestalten können. Im Hinblick auf die Barrierefreiheit muss der Daumen auf Klicken und Ziehen reagieren, da viele Nutzer es gewohnt sind, mit einer Bildlaufleiste zu interagieren. Damit dieser Blogpost nicht noch länger wird, werde ich die Details dieses Teils nicht erläutern. Die genaue Vorgehensweise findest du im Bibliothekscode.

Und was ist mit iOS?

Ah, mein alter Freund iOS Safari. Wie beim Parallaxe-Scrollen tritt auch hier ein Problem auf. Da wir auf einem Element scrollen, müssen wir -webkit-overflow-scrolling: touch angeben. Das führt jedoch zu einer 3D-Abflachung und der gesamte Scrolleffekt funktioniert nicht mehr. Wir haben dieses Problem im Parallaxe-Scroller gelöst, indem wir iOS Safari erkannt und position: sticky als Workaround verwendet haben. Wir führen hier genau denselben Vorgang aus. Frischen Sie Ihr Wissen mit dem Artikel zu Parallaxe auf.

Was ist mit der Bildlaufleiste im Browser?

Auf einigen Systemen haben wir eine permanente, native Bildlaufleiste. Bisher konnte die Bildlaufleiste nicht ausgeblendet werden (mit Ausnahme eines nicht standardmäßigen Pseudoselektors). Um es zu verstecken, müssen wir uns also auf ein paar (Mathematik-kostenlose) Hackerangriffe umsehen. Wir umschließen das Scrollelement in einem Container mit overflow-x: hidden und machen es breiter als der Container. Die native Bildlaufleiste des Browsers ist jetzt verschwunden.

Flosse

Zusammengenommen können wir eine benutzerdefinierte Bildlaufleiste mit perfektem Frame erstellen – wie in unserer Nyan Cat-Demo.

Wenn Sie Nyan cat nicht sehen können, liegt bei Ihnen ein Programmfehler vor, den wir beim Erstellen dieser Demo gefunden und gemeldet haben. Klicken Sie auf den Daumen, damit Nyan cat angezeigt wird. Chrome vermeidet unnötige Schritte wie das Malen oder Animieren von Dingen, die sich außerhalb des Bildschirms befinden. Das Schlechte daran ist, dass Chrome aufgrund unserer Matrixfehler glaubt, dass das Nyan-Katzen-GIF nicht auf dem Bildschirm zu finden ist. Hoffentlich lässt sich das Problem bald beheben.

Das war es schon. Das war eine Menge Arbeit. Danke, dass Sie das gesamte Dokument gelesen haben. Das ist ein echter Trick, der sich wahrscheinlich nur selten lohnt – es sei denn, eine benutzerdefinierte Bildlaufleiste ist ein wesentlicher Bestandteil. Aber gut zu wissen, dass es möglich ist, oder? Die Tatsache, dass eine benutzerdefinierte Bildlaufleiste so schwierig ist, zeigt, dass auf CSS-Seite noch viel Arbeit getan wird. Aber keine Sorge! In Zukunft wird AnimationWorklet von Houdini viel einfacher machen, wenn man per Scrollen perfekte Frames verlinkt.