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 restlichen Elementen im Web gehören, die ziemlich unstylisch sind (Datumsauswahl). Sie können JavaScript verwenden, um Ihre eigenen Creatives zu erstellen. Dies ist jedoch kostspielig, Low-Fidelity und kann sich verzögert anfühlen. In diesem Artikel verwenden wir einige unkonventionelle CSS-Matrizen, um einen benutzerdefinierten Scroller zu erstellen, der beim Scrollen kein JavaScript erfordert, sondern nur etwas Einrichtungscode.

Kurzfassung

Dir sind die kleinen Dinge nicht wichtig? Sie möchten sich nur die Nyan-Katze-Demo ansehen und die Bibliothek abrufen? Den Democode 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 diesen Artikel gelesen? Das ist wirklich gut, es lohnt sich!) Indem Elemente mithilfe von CSS-3D-Transformationen zurückgeschoben wurden, bewegten sie sich langsamer als beim Scrollen.

Zusammenfassung

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

Wie in der Animation zu sehen ist, haben wir Elemente im 3D-Raum entlang der Z-Achse nach hinten geschoben, um den Parllax-Effekt zu erzielen. Das Scrollen durch ein Dokument ist praktisch eine Verschiebung entlang der Y-Achse. Wenn wir beispielsweise um 100 Pixel nach unten scrollen, wird jedes Element um 100 Pixel nach oben übersetzt. Dies gilt für alle Elemente, auch für diejenigen, die weiter hinten liegen. Da sie aber da weiter von der Kamera entfernt sind, beträgt die beobachtete Bewegung auf dem Bildschirm weniger als 100 Pixel, wodurch der gewünschte Paralleleffekt erzielt wird.

Natürlich wird ein Element auch kleiner, wenn Sie es zurück in den Raum verschieben. Dies können Sie korrigieren, indem Sie es wieder nach oben skalieren. Das genaue Resultat des Parallax-Scrollers ist uns auf den Weg gebracht. Deshalb werde ich nicht alle Details wiederholen.

Schritt 0: Was möchten wir tun?

Bildlaufleisten Und genau das werden wir entwickeln. Aber haben Sie sich jemals wirklich darüber Gedanken gemacht? Auf jeden Fall nicht. Bildlaufleisten geben an, wie viel des verfügbaren Inhalts derzeit sichtbar ist und wie viel Fortschritt Sie als Leser gemacht haben. Wenn Sie nach unten scrollen, zeigt die Bildlaufleiste an, dass Sie Fortschritte in Richtung Ende machen. Wenn alle Inhalte in den Darstellungsbereich passen, ist die Bildlaufleiste normalerweise ausgeblendet. Wenn der Inhalt doppelt so hoch ist wie der Darstellungsbereich, füllt die Bildlaufleiste die Hälfte seiner Höhe aus. Inhalte, die der dreifachen Höhe des Darstellungsbereichs wert sind, skaliert die Bildlaufleiste auf ein Drittel des Darstellungsbereichs usw. Sie sehen das Muster. Anstatt zu scrollen, können Sie auch auf die Bildlaufleiste klicken und diese ziehen, um sich schneller durch die Website zu bewegen. Das ist eine überraschende Menge an Verhalten für ein so unauffälliges Element. Lasst uns einen Kampf nach dem anderen kämpfen.

Schritt 1: umgekehrt

Mit CSS-3D-Transformationen können wir dafür sorgen, dass sich Elemente langsamer bewegen als das Scrollen. Können wir die Richtung auch umkehren? Es hat sich herausgestellt, dass das möglich ist. So haben wir eine benutzerdefinierte Bildlaufleiste mit perfekten Bildern erstellt. Um die Funktionsweise zu verstehen, müssen wir uns zuerst mit einigen CSS-3D-Grundlagen beschäftigen.

Um eine perspektivische Projektion im mathematischen Sinn zu erhalten, werden höchstwahrscheinlich homogene Koordinaten verwendet. Ich werde hier nicht näher 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 haben. Wir müssen uns keine Gedanken über die Details von w machen, da wir keinen anderen Wert als 1 verwenden. Daher sind von jetzt an alle Punkte vierdimensionale Vektoren [x, y, z, w=1] und folglich müssen auch Matrizen 4 x 4 groß sein.

Sie sehen, dass CSS homogene Koordinaten im Hintergrund verwendet, wenn Sie mithilfe der Funktion matrix3d() eigene 4 x 4-Matrizen in einer Transformationseigenschaft definieren. matrix3d verwendet 16 Argumente (da die Matrix 4 x 4 ist) und gibt eine Spalte nach der anderen an. Wir können also diese Funktion verwenden, um Rotationen, Übersetzungen usw. manuell anzugeben. Sie können aber auch mit dieser w-Koordinate herumspielen.

Bevor wir matrix3d() verwenden können, benötigen wir einen 3D-Kontext, denn ohne 3D-Kontext gäbe es keine perspektivische Verzerrung und es sind keine homogenen Koordinaten erforderlich. Zum Erstellen eines 3D-Kontexts benötigen wir einen Container mit einem perspective und einigen darin enthaltenen Elementen, die wir im neu erstellten 3D-Raum 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:

  • Machen Sie jede Ecke (Scheitelpunkt) eines Elements relativ zum perspektivischen Container in homogene Koordinaten [x,y,z,w].
  • 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 Perspektivenmatrix an.

Die Scrollmatrix ist eine Übersetzung entlang der Y-Achse. Wenn wir um 400 Pixel nach unten scrollen, müssen alle Elemente um 400 Pixel nach oben verschoben werden. Die Perspektivmatrix ist eine Matrix, die Punkte, die sich im 3D-Raum weiter hinten befinden, näher an den Fluchtpunkt "zieht". Dies führt zu beiden Effekten, dass Dinge kleiner erscheinen, wenn sie weiter entfernt sind, und macht sie bei der Übersetzung auch „langsamer“. Wenn also ein Element nach hinten verschoben wird, führt eine Verschiebung von 400 Pixeln dazu, dass das Element auf dem Bildschirm nur um 300 Pixel verschoben wird.

Wenn Sie alle Details kennen möchten, lesen Sie die spec für das Transformations-Rendering-Modell des CSS-Codes. Für diesen Artikel haben wir den obigen Algorithmus vereinfacht.

Die Box befindet sich in einem perspektivischen Container mit dem Wert p für das Attribut perspective. Wir gehen davon aus, dass der Container scrollbar ist und um n Pixel nach unten gescrollt wird.

Perspective-Matrix × Scroll-Matrix × Element-Transformationsmatrix entspricht vier mal vier Identitätsmatrix mit minus eins über p in der dritten Spalte der vierten Zeile × viermal vier Identitätsmatrix mit minus n in der vierten Spalte × der Elementtransformationsmatrix.

Die erste Matrix ist die Perspektivenmatrix, die zweite die Scrollmatrix. Zur Erinnerung: Die Scrollmatrix soll ein Element nach oben bewegen, wenn wir nach unten scrollen, d. h. das negative Vorzeichen.

Für die Bildlaufleiste verwenden wir das Gegenteil, das heißt, das Element soll nach unten verschoben werden, wenn wir nach unten scrollen. Hier können wir einen Trick anwenden: Umkehren der w-Koordinate der Ecken des Felds. Wenn die w-Koordinate -1 ist, werden alle Übersetzungen in die entgegengesetzte Richtung angewendet. Wie machen wir das? Die CSS-Engine wandelt die Ecken der Box in homogene Koordinaten um und legt „w“ auf „1“ fest. matrix3d() kann glänzen!

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

Diese Matrix bewirkt nichts anderes, als w zu negieren. Wenn also die CSS-Engine jede Ecke in einen Vektor der Form [x,y,z,1] umgewandelt hat, wandelt die Matrix sie in [x,y,z,-1] um.

4 mal 4-Identitätsmatrix mit minus 1 über p in der dritten Spalte der vierten Zeile × 4 mal 4 Identitätsmatrix mit Minus n in der vierten Zeile der zweiten Zeile × 4 × 4 Identitätsmatrix mit minus 1 in der vierten Zeile × vierdimensionaler Vektor x, y, z, 1 entspricht vier mal 4 x 4 Identitätsmatrix mit minus 1 über p in der vierten Zeile und vierten Spalte in der vierten Zeile und vierten Zeile minus 1 in der vierten Zeile und in der vierten Zeile plus n minus 1 in der vierten Zeile und in vierter Zeile plus vier minus p in der vierten Zeile und dritten Spalte.

Ich habe einen Zwischenschritt aufgelistet, um den Effekt unserer Element-Transformationsmatrix zu zeigen. Wenn Sie sich mit Matrixberechnungen nicht auskennen, ist das in Ordnung. Der Eureka-Moment ist, dass wir in der letzten Zeile den Scroll-Versatz 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. Da unsere z-Koordinate derzeit 0 ist und p gleich 1 ist, ist w -1.

Glücklicherweise können wir den Wert von z wählen! Um sicherzustellen, dass wir am Ende w=1 haben, müssen wir z = -2 setzen.

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

Und schau mal, unsere Box ist wieder da!

Schritt 2: Bewegen

Jetzt ist unsere Box da und sieht genauso aus, wie sie ohne Transformationen aussehen würde. Derzeit ist der perspektivische Container nicht scrollbar, also können wir ihn nicht sehen, aber wir wissen, dass das Element beim Scrollen in eine andere Richtung wechselt. Lassen Sie uns 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 zum Feld! Das rote Feld wird nach unten verschoben.

Schritt 3: Größe festlegen

Es gibt ein Element, das sich nach unten bewegt, wenn auf der Seite nach unten gescrollt wird. Das ist wirklich eine knifflige Sache. Jetzt müssen wir sie so gestalten, dass sie wie eine Bildlaufleiste aussieht, und sie interaktiver gestalten.

Eine Bildlaufleiste besteht normalerweise aus einem "Daumen" und einem "Track", während der Track nicht immer sichtbar ist. Die Höhe des Daumens hängt direkt davon ab, wie viel des Inhalts 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 Anteil des sichtbaren Inhalts. Das Verhältnis des vertikalen Platzes, den der Daumen abdeckt, sollte dem Verhältnis des sichtbaren Inhalts entsprechen:

&quot;thing Punkt style Punkthöhe&quot; über &quot;scrollerHeight&quot; entspricht der Scrollhöhe
 über der Scrollhöhe des Punkts, wenn und nur dann, wenn &quot;Daumenpunktpunktstil&quot; Punkthöhe
 gleich Scrollhöhe mal Scrollhöhe über Scrollerpunkt-Scrollhöhe ist.
<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, bewegt sich jedoch viel zu schnell. Hier können wir unsere Technik vom Parallaxe-Scroller übernehmen. Wird das Element weiter nach hinten verschoben, wird es beim Scrollen langsamer. Wir können die Größe korrigieren, indem wir sie vertikal skalieren. Aber wie stark sollten wir sie genau zurückschieben? Lass uns rechnen! Das ist das letzte Mal, versprochen.

Entscheidend ist, dass der untere Rand des Daumens beim Scrollen nach unten am unteren Rand des scrollbaren Elements ausgerichtet ist. Mit anderen Worten: Wenn wir scroller.scrollHeight - scroller.height Pixel gescrollt haben, soll unser Daumen nach scroller.height - thumb.height übersetzt werden. Bei jedem Scroller-Pixel soll unser Daumen einen Bruchteil eines Pixels verschieben:

Der Faktor entspricht der Punkthöhe des Bildlaufpunkts minus der Punkthöhe des Bildlaufpunkts über der Scrollhöhe des Bildlaufpunkts minus der Punkthöhe des Bildlaufpunkts.

Das ist unser Skalierungsfaktor. Jetzt müssen wir den Skalierungsfaktor in eine Verschiebung entlang der z-Achse umwandeln, wie wir dies bereits im Artikel zum Scrollen mit Parallax getan haben. Gemäß dem relevanten 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 der Daumen entlang der z-Achse verschoben wird. Aufgrund der W-Koordinaten müssen wir jedoch zusätzlich -2px entlang von z übersetzen. Beachten Sie auch, dass die Transformationen eines Elements von rechts nach links angewendet werden. Das bedeutet, dass alle Übersetzungen vor der speziellen Matrix nicht invertiert werden. Alle Übersetzungen nach der speziellen Matrix werden jedoch schon umgekehrt. Lassen Sie uns dies 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>

Es gibt eine Bildlaufleiste. Es handelt sich nur um ein DOM-Element, das wir nach Belieben gestalten können. Für die Barrierefreiheit ist es wichtig, dass der Daumen auf Klicken und Ziehen reagiert, da viele Nutzer daran gewöhnt sind, auf diese Weise mit einer Bildlaufleiste zu interagieren. Damit dieser Blogpost nicht noch länger wird, werde ich die Details zu diesem Teil nicht erklären. Im Bibliothekscode findest du weitere Informationen.

Was ist mit iOS?

Ah, mein alter Freund iOS Safari. Wie beim Parllax-Scrollen stoßen wir hier auf ein Problem. Da wir durch ein Element scrollen, müssen wir -webkit-overflow-scrolling: touch angeben. Das führt jedoch zu einer 3D-Flachung und der gesamte Scrolleffekt funktioniert nicht mehr. Wir haben dieses Problem im parallax-Scroller gelöst, indem wir iOS Safari erkannt und position: sticky als Problemumgehung verwendet haben. Wir werden hier genau dasselbe tun. Sehen Sie sich den Parallax-Artikel an, um Ihr Gedächtnis aufzufrischen.

Was ist mit der Bildlaufleiste des Browsers?

Bei einigen Systemen müssen Sie es mit einer permanenten, nativen Bildlaufleiste zu tun haben. In der Vergangenheit kann die Bildlaufleiste nicht ausgeblendet werden (außer bei einem nicht standardmäßigen Pseudoselektor). Um es zu verstecken, müssen wir uns auf Hackerangriffe ohne Mathematikfehler stützen. Wir packen das Scrollelement in einen Container mit overflow-x: hidden und machen das Scrollelement breiter als der Container. Die native Bildlaufleiste des Browsers ist jetzt nicht mehr sichtbar.

Flossen

Wenn wir alles zusammengenommen haben, können wir eine benutzerdefinierte Bildlaufleiste für Frames erstellen – wie die in unserer Nyan-Katze-Demo.

Wenn Sie Nyan Cat nicht sehen können, tritt beim Erstellen dieser Demo ein Fehler auf, den wir gefunden und protokolliert haben. Klicken Sie auf den Daumen, damit Nyan Cat angezeigt wird. Chrome vermeidet unnötige Arbeit wie das Streichen oder Animieren von Dingen, die nicht auf dem Bildschirm zu sehen sind. Die schlechte Nachricht ist, dass Chrome aufgrund unserer Matrix-Dransereien glaubt, dass das Nyan-Katze-GIF tatsächlich nicht auf dem Bildschirm zu sehen ist. Ich hoffe, dass das Problem bald behoben ist.

Das war‘s auch schon. Das war eine Menge Arbeit. Ich bedanke mich bei Ihnen, dass Sie den ganzen Artikel gelesen haben. Das ist ein echter Trick, um das Ganze zum Laufen zu bringen. Außerdem lohnt sich die Mühe selten, es sei denn, eine benutzerdefinierte Bildlaufleiste ist ein wesentlicher Bestandteil. Aber es ist gut zu wissen, dass es möglich ist, oder? Die Tatsache, dass es so schwierig ist, eine benutzerdefinierte Bildlaufleiste zu verwenden, zeigt, dass auf CSS-Seite viel Arbeit erledigt werden muss. Aber keine Sorge! Zukünftig soll das AnimationWorklet von Houdini dazu beitragen, dass Frame-perfekte Scroll-Effekte wie diesen deutlich vereinfacht werden.