CSS Deep-Dive : matrix3d() pour une barre de défilement personnalisée parfaite à l'image

Les barres de défilement personnalisées sont extrêmement rares. Cela s'explique principalement par le fait que les barres de défilement sont l'un des bits restants sur le Web qui ne sont pas assez stylisés (le sélecteur de date vous entoure). Vous pouvez utiliser JavaScript pour créer votre propre code, mais cela est coûteux, en basse-fidélité et peut sembler lent. Dans cet article, nous allons utiliser des matrices CSS non conventionnelles pour créer un conteneur de défilement personnalisé qui ne nécessite pas de code JavaScript pendant le défilement, mais seulement un code de configuration.

Résumé

Vous ne vous souciez pas des petites choses ? Vous voulez juste regarder la démonstration du chat Nyan et obtenir la bibliothèque ? Vous trouverez le code de la démonstration dans notre dépôt GitHub.

LAM;WRA (long et mathématique ; lirea quand même)

Il y a quelque temps, nous avons créé un conteneur de défilement parallaxe. Avez-vous lu cet article ? C'est vraiment bien, ça vaut la peine d'y prendre du temps !). En repoussant des éléments à l'aide de transformations CSS 3D, les éléments sont déplacés plus lent que la vitesse de défilement réelle.

Récapitulatif

Commençons par un récapitulatif du fonctionnement du conteneur de défilement parallaxe.

Comme le montre l'animation, nous avons obtenu l'effet de parallaxe en poussant les éléments "vers l'arrière" dans l'espace 3D, le long de l'axe Z. Le défilement d'un document est en fait une traduction le long de l'axe Y. Ainsi, si nous faisons défiler vers le bas de, disons 100 px, chaque élément sera traduit de 100 px vers le haut. Cela s'applique à tous les éléments, y compris ceux qui se trouvent "en arrière-plan". Toutefois, car ils sont plus éloignés de la caméra, leur mouvement observé à l'écran sera inférieur à 100 pixels, ce qui donne l'effet de parallaxe souhaité.

Bien sûr, le fait de déplacer un élément dans l'espace le réduit également. Nous le redimensionnons en le redimensionnant. Nous avons déterminé le calcul exact lors de la création du défilement parallaxe. Je ne vais donc pas répéter tous les détails.

Étape 0: Qu'est-ce qu'on veut faire ?

Barres de défilement C'est ce que nous allons développer. Mais avez-vous déjà vraiment pensé à ce qu’ils font ? Certainement pas. Les barres de défilement indiquent la quantité du contenu disponible actuellement visible et la progression que vous avez accomplie en tant que lecteur. Si vous faites défiler la page vers le bas, la barre de défilement indique que vous progressez vers la fin. Si tout le contenu tient dans la fenêtre d'affichage, la barre de défilement est généralement masquée. Si la hauteur de la fenêtre d'affichage est doublée, la barre de défilement occupe la moitié de la hauteur de la fenêtre d'affichage. Un contenu qui correspond à trois fois la hauteur de la fenêtre d'affichage redimensionne la barre de défilement à un tiers de la fenêtre d'affichage, etc. Vous voyez le schéma. Au lieu de faire défiler le site, vous pouvez également cliquer sur la barre de défilement et la faire glisser pour parcourir le site plus rapidement. C'est un comportement surprenant pour un élément peu visible comme celui-ci. Combattons un par un.

Étape 1: Remédiez à l'envers

Nous pouvons donc faire en sorte que les éléments se déplacent plus lentement que la vitesse de défilement à l'aide de transformations CSS 3D, comme indiqué dans l'article concernant le défilement parallaxe. Pouvons-nous aussi inverser la direction ? Il s'avère que c'est possible, et que c'est pour cela que nous pouvons créer une barre de défilement personnalisée parfaite et parfaitement adaptée. Pour comprendre comment cela fonctionne, nous devons d'abord aborder quelques principes de base du CSS 3D.

Pour obtenir n'importe quel type de projection en perspective au sens mathématique, vous finirez probablement par utiliser des coordonnées homogènes. Je n'entre pas dans les détails à ce sujet ni pourquoi ils fonctionnent, mais vous pouvez les considérer comme des coordonnées 3D avec une quatrième coordonnée supplémentaire appelée w. Cette coordonnée doit être 1, sauf si vous souhaitez créer une distorsion de perspective. Nous n'avons pas à nous soucier des détails de w, car nous n'utiliserons aucune autre valeur que 1. Par conséquent, tous les points seront à partir de maintenant des vecteurs à 4 dimensions [x, y, z, w=1]. Par conséquent, les matrices devront également être de 4x4.

Vous pouvez constater que CSS utilise des coordonnées homogènes en arrière-plan lorsque vous définissez vos propres matrices 4x4 dans une propriété de transformation à l'aide de la fonction matrix3d(). matrix3d accepte 16 arguments (car la matrice est 4x4), en spécifiant une colonne après l'autre. Nous pouvons donc utiliser cette fonction pour spécifier manuellement des rotations, des traductions, etc. Mais elle nous permet également de manipuler la coordonnée W.

Avant de pouvoir utiliser matrix3d(), nous avons besoin d'un contexte 3D, car sans contexte 3D, il n'y aurait pas de distorsion de la perspective ni de coordonnées homogènes. Pour créer un contexte 3D, nous avons besoin d'un conteneur avec une perspective et d'autres éléments que nous pouvons transformer dans l'espace 3D que vous venez de créer. Exemple:

Extrait de code CSS qui déforme un élément div à l'aide de l'attribut de perspective du CSS.

Les éléments d'un conteneur de perspective sont traités par le moteur CSS comme suit:

  • Transformez chaque angle (vertex) d'un élément en coordonnées homogènes [x,y,z,w], par rapport au conteneur de la perspective.
  • Appliquez toutes les transformations de l'élément en tant que matrices de droite à gauche.
  • S'il est possible de faire défiler l'élément de perspective, appliquez une matrice de défilement.
  • Appliquez la matrice de perspective.

La matrice de défilement est une translation le long de l'axe des y. Si nous déplaçons l'élément vers le bas de 400 pixels, tous les éléments doivent être déplacés vers le haut de 400 pixels. La matrice de perspective est une matrice qui "tire" les points plus près du point de fuite à mesure qu'ils sont éloignés dans l'espace 3D. Cela permet de réduire la taille des éléments lorsqu'ils sont plus éloignés et de "se déplacer plus lentement" lors de la traduction. Ainsi, si un élément est repoussé, une translation de 400 pixels ne le déplacera que de 300 pixels sur l'écran.

Pour connaître tous les détails, lisez les spec concernant le modèle de rendu des transformations CSS. Toutefois, pour les besoins de cet article, j'ai simplifié l'algorithme ci-dessus.

Notre boîte se trouve dans un conteneur de perspective avec la valeur p pour l'attribut perspective. Supposons que le conteneur puisse être défilé et que l'utilisateur fasse défiler n pixels vers le bas.

Matrice d'identité x matrice de défilement x matrice de transformation d'élément est égale à matrice d'identité de 4x4 avec moins un sur p dans la quatrième ligne, troisième colonne x matrice d'identité quatre par quatre, et moins n dans la deuxième ligne, quatrième colonne x matrice de transformation d'élément.

La première matrice est la matrice de perspective, la seconde est la matrice de défilement. Pour résumer, le rôle de la matrice de défilement consiste à faire en sorte qu'un élément se déplace vers le haut lorsque l'utilisateur effectue un défilement vers le bas, d'où le signe négatif.

En revanche, pour notre barre de défilement, nous voulons l'inverse : nous voulons que l'élément se déplace vers le bas lorsque l'utilisateur fait défiler la page vers le bas. Voici une astuce : inversez la coordonnée W des coins de la boîte. Si la coordonnée w est -1, toutes les traductions prendront effet dans la direction opposée. Comment faire ? Le moteur CSS se charge de convertir les coins de notre cadre en coordonnées homogènes et définit w sur 1. Il est temps pour matrix3d() de briller !

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

Cette matrice ne fait rien d'autre que négocier w. Ainsi, lorsque le moteur CSS a transformé chaque angle en vecteur de la forme [x,y,z,1], la matrice le convertit en [x,y,z,-1].

matrice d'identité 4x4 avec moins un sur p dans la quatrième ligne troisième colonne multipliée par moins un sur quatre matrices d'identité quatre par quatre avec moins n dans la deuxième ligne quatrième colonne fois quatre par quatre matrices d'identité avec moins un dans la quatrième ligne quatrième colonne fois quatre dimensions vecteurs x, y, z, 1 égal à quatre par quatre matrices d'identités moins un sur p dans la quatrième ligne de la quatrième ligne n

J'ai listé une étape intermédiaire pour montrer l'effet de la matrice de transformation des éléments. Si vous n'êtes pas à l'aise avec les mathématiques matricielles, ce n'est pas grave. Le moment Eurêka est que, dans la dernière ligne, nous ajoutons le décalage de défilement n à notre coordonnée y au lieu de la soustraire. L'élément sera traduit vers le bas si nous faisons défiler vers le bas.

Toutefois, si nous nous contentons de mettre cette matrice dans notre exemple, l'élément ne sera pas affiché. En effet, la spécification CSS exige que tout sommet dont la valeur w est inférieure à 0 empêche l'affichage de l'élément. Et comme notre coordonnée z est actuellement 0 et que p est 1, w sera -1.

Heureusement, nous pouvons choisir la valeur de z ! Pour nous assurer d'obtenir w=1, nous devons définir z = -2.

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

Et voilà, notre boîte est de retour !

Étape 2: Déplacer votre appareil

Notre boîtier est présent et se présente comme si sans transformation. Pour le moment, il n'est pas possible de faire défiler le conteneur de la perspective. Nous ne pouvons donc pas le voir, mais nous savons que notre élément ira dans l'autre direction lors du défilement. Faisons défiler la page du conteneur. Nous pouvons simplement ajouter un élément d'espacement qui prend de l'espace:

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

Vous pouvez maintenant faire défiler la case. Le cadre rouge descend.

Étape 3: Attribuez-lui une taille

Nous avons un élément qui se déplace vers le bas lorsque la page défile vers le bas. C'est vraiment la partie la plus difficile. Nous devons maintenant lui donner un style pour ressembler à une barre de défilement et la rendre un peu plus interactive.

Une barre de défilement se compose généralement d'un pouce et d'une piste, qui ne sont pas toujours visibles. La hauteur du curseur est directement proportionnelle à la proportion du contenu visible.

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

scrollerHeight est la hauteur de l'élément à faire défiler, tandis que scroller.scrollHeight est la hauteur totale du contenu à faire défiler. scrollerHeight/scroller.scrollHeight est la fraction du contenu visible. Le ratio de l'espace vertical sur les pouces doit être égal au ratio du contenu visible:

Pouce point style point height sur ScrollerHeight est égal à la hauteur du conteneur de défilement sur la hauteur de défilement du point de défilement si, et seulement si, la hauteur du point correspond à la hauteur du conteneur de défilement multipliée par la hauteur du conteneur de défilement au-dessus de la hauteur du défilement du point de défilement.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

La taille du pouce est agréable, mais elle se déplace beaucoup trop rapidement. C'est là que nous pouvons utiliser notre technique à partir du conteneur de défilement parallaxe. Si vous reculez l'élément, il se déplace plus lentement pendant le défilement. Nous pouvons corriger cette taille en la augmentant. Mais dans quelle mesure devrions-nous repousser cela exactement ? Vous avez deviné, allons faire des maths ! C'est la dernière fois, je vous le promets.

L'essentiel est de faire en sorte que le bord inférieur du pouce soit aligné avec le bord inférieur de l'élément à faire défiler lorsque l'utilisateur fait défiler l'écran vers le bas. En d'autres termes, si nous avons fait défiler scroller.scrollHeight - scroller.height pixels, nous voulons que notre pouce soit traduit par scroller.height - thumb.height. Pour chaque pixel de conteneur de défilement, nous voulons que notre pouce se déplace d'une fraction de pixel:

Facteur égal à la hauteur des points de défilement moins la hauteur des points de défilement au-dessus de la hauteur du point de défilement moins la hauteur des points de défilement.

C'est notre facteur de scaling. Nous devons maintenant convertir le facteur de mise à l'échelle en translation le long de l'axe Z, ce que nous avons déjà fait dans l'article sur le défilement parallaxe. Conformément à la section pertinente de la spécification : Le facteur de scaling est égal à p/(p − z). Nous pouvons résoudre cette équation pour z afin de déterminer combien nous devons traduire le pouce le long de l'axe z. Toutefois, n'oubliez pas qu'en raison de nos exigences en matière de coordonnées W, nous devons traduire une valeur -2px supplémentaire en fonction de la lettre z. Notez également que les transformations d'un élément sont appliquées de droite à gauche, ce qui signifie que toutes les traductions effectuées avant notre matrice spéciale ne seront pas inversées, contrairement à toutes les traductions après notre matrice spéciale. Faisons du codification !

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

Une barre de défilement s'affiche. Et il s'agit simplement d'un élément DOM que nous pouvons styliser comme nous le souhaitons. En termes d'accessibilité, il est important de faire en sorte que le curseur réagisse au clic et au déplacement, car de nombreux utilisateurs sont habitués à interagir avec une barre de défilement de cette façon. Afin de ne pas rallonger cet article de blog, je ne vais pas expliquer les détails de cette partie. Pour savoir comment procéder, consultez le code de la bibliothèque.

Qu'en est-il d'iOS ?

Mon ami iOS Safari. Comme pour le défilement parallaxe, nous rencontrons un problème ici. Comme nous faisons défiler un élément, nous devons spécifier -webkit-overflow-scrolling: touch, mais cela entraîne un aplatissement 3D et l'ensemble de notre effet de défilement cesse de fonctionner. Nous avons résolu ce problème dans le conteneur de défilement parallaxe en détectant iOS Safari et en utilisant position: sticky comme solution de contournement. Nous ferons exactement la même chose ici. Consultez cet article pour vous rafraîchir la mémoire.

Qu'en est-il de la barre de défilement du navigateur ?

Sur certains systèmes, nous devrons utiliser une barre de défilement native permanente. Par le passé, la barre de défilement ne peut pas être masquée (sauf avec un pseudo-sélecteur non standard). Pour le cacher, nous devons donc faire appel au hacker (sans maths). Encapsuler l'élément à défilement dans un conteneur avec overflow-x: hidden et rendre l'élément à défilement plus large que le conteneur. La barre de défilement native du navigateur n'est plus visible.

Nageoire

En combinant tout cela, nous pouvons désormais créer une barre de défilement personnalisée parfaite, comme celle de notre démonstration sur le chat Nyan.

Si vous ne voyez pas le chat Nyan, cela signifie que vous rencontrez un bug que nous avons détecté et signalé lors de la création de cette démonstration (cliquez sur le pouce pour faire apparaître le chat Nyan). Chrome permet d'éviter les tâches inutiles comme peindre ou animer des éléments qui ne sont pas à l'écran. Malheureusement, en raison de nos manigances matricielles, Chrome considère que le GIF du chat Nyan se trouve bien hors de l'écran. J'espère que ce problème sera bientôt résolu.

Et voilà. Cela représente beaucoup de travail. Je vous félicite d'avoir lu l'intégralité de ce document. Il s'agit d'une véritable astuce pour faire fonctionner tout cela et cela en vaut probablement rarement la peine, sauf lorsqu'une barre de défilement personnalisée est un élément essentiel de l'expérience. Mais bon de savoir que c'est possible, non ? La difficulté d'utiliser une barre de défilement personnalisée prouve que le CSS a du travail à faire. Mais n'ayez aucune crainte ! À l'avenir, AnimationWorklet de Houdini facilitera considérablement la création d'effets de défilement parfaits comme celui-ci.