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 éléments restants sur le Web qui ne peuvent pas être stylisés (je vous regarde, sélecteur de date). Vous pouvez utiliser JavaScript pour créer le vôtre, mais il est coûteux, est une opération basse fidélité et peut sembler long. Dans cet article, nous allons utiliser des matrices CSS non conventionnelles pour créer un conteneur de défilement personnalisé qui ne nécessite aucun code JavaScript lors du défilement. Il suffit d'un code de configuration.

Résumé

Vous ne vous souciez pas des détails ? Vous souhaitez simplement consulter la démonstration de Nyan Cat 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 ; se lit quand même)

Il y a quelque temps, nous avons créé un conteneur de défilement parallaxe. (Avez-vous lu cet article ? Elle est très intéressante et vaut bien la peine d'y consacrer du temps !). En repoussant les éléments à l'aide de transformations 3D CSS, ils se sont déplacés plus lentement que la vitesse de défilement réelle.

Récapitulatif

Commençons par récapituler le fonctionnement du conteneur de défilement parallaxe.

Comme le montre l'animation, nous avons obtenu l'effet de parallaxe en poussant des éléments vers l'arrière dans un 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 100 pixels par exemple, chaque élément sera traduit vers le haut de 100 pixels. Cela s'applique à tous les éléments, même ceux qui sont "plus en arrière". Toutefois, comme ils sont plus éloignés de la caméra, le mouvement observé à l'écran sera inférieur à 100 px, ce qui génère l'effet de parallaxe souhaité.

Bien entendu, replacer un élément dans l'espace le rendra également plus petit, ce que nous corrigeons en redimensionnant l'élément à la hausse. Nous avons trouvé la formule mathématique exacte lors de la création du défileur parallaxe. Je ne vais donc pas répéter tous les détails.

Étape 0: Que voulons-nous faire ?

Barres de défilement. C'est ce que nous allons créer. Mais avez-vous déjà vraiment réfléchi à ce qu’ils font ? Certainement pas. Les barres de défilement indiquent la part du contenu disponible actuellement visible et la progression que vous avez réalisée en tant que lecteur. Si vous faites défiler l'écran vers le bas, il en va de même pour la barre de défilement pour indiquer que vous progressez vers la fin. Si tout le contenu s'adapte à la fenêtre d'affichage, la barre de défilement est généralement masquée. Si le contenu a deux fois la hauteur de la fenêtre d'affichage, la barre de défilement occupe la moitié de la hauteur de la fenêtre d'affichage. Le contenu équivalant à trois fois la hauteur de la fenêtre d'affichage adapte la barre de défilement au tiers de la fenêtre d'affichage, etc. Vous voyez le motif. Au lieu de faire défiler la page, vous pouvez également cliquer sur la barre de défilement et la faire glisser pour vous déplacer plus rapidement dans le site. Ce comportement est surprenant pour un élément aussi discret que celui-ci. Luttons une bataille à la fois.

Étape 1: Inversion

Nous pouvons faire en sorte que les éléments se déplacent plus lentement que la vitesse de défilement grâce aux transformations CSS 3D, comme indiqué dans l'article sur le défilement parallaxe. Pouvons-nous aussi inverser le sens ? Il s'avère que c'est possible et que c'est ainsi que nous pouvons créer une barre de défilement personnalisée et parfaite. Pour en comprendre le fonctionnement, nous devons d'abord aborder quelques principes de base de la 3D CSS.

Pour obtenir n'importe quel type de projection de perspective au sens mathématique, vous vous retrouverez très probablement à utiliser des coordonnées homogènes. Je n'expliquerai pas en détail en quoi elles consistent et pourquoi elles 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 égale à 1, sauf si vous souhaitez avoir une distorsion de la perspective. Nous n'avons pas à nous soucier des détails de w, car nous n'utiliserons pas d'autre valeur que 1. Par conséquent, tous les points se trouvent désormais sur des vecteurs à quatre dimensions [x, y, z, w=1]. Par conséquent, les matrices doivent également être égales à 4x4.

Vous pouvez constater que le 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 de 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. Toutefois, elle nous permet également de manipuler la coordonnée w.

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

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

Les éléments à l'intérieur 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 perspective.
  • Appliquer toutes les transformations de l'élément sous forme de matrices de droite à gauche.
  • Si l'élément de perspective est déroulant, appliquez une matrice de défilement.
  • Appliquez la matrice de perspective.

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

Si vous souhaitez connaître tous les détails, vous devez lire les spec du modèle de rendu des transformations du CSS. Toutefois, pour les besoins de cet article, nous avons simplifié l'algorithme ci-dessus.

Notre cadre se trouve à l'intérieur d'un conteneur de perspective dont l'attribut perspective est défini sur la valeur p. Supposons qu'il soit possible de faire défiler le conteneur de n pixels vers le bas.

Matrice perspective temps de défilement de la matrice temps de la matrice d'identité est égale à quatre par quatre matrice d'identité avec moins un sur p dans la quatrième ligne, troisième colonne multiplié par quatre par quatre matrice d'identité avec moins n dans la deuxième ligne, quatrième colonne fois la matrice de transformation des éléments.

La première matrice est la matrice de perspective, la deuxième est la matrice de défilement. En résumé, le rôle de la matrice de défilement est de déplacer un élément vers le haut lorsque nous défilerons vers le bas, d'où le signe négatif.

Pour la barre de défilement, nous voulons toutefois l'inverse : nous voulons que l'élément descend lorsque nous faisons défiler la page vers le bas. Une astuce consiste à inverser la coordonnée W des angles de la boîte. Si la coordonnée w est définie sur -1, toutes les traductions prennent effet dans la direction opposée. Comment faire ? Le moteur CSS convertit les angles 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 la négation de w. Ainsi, lorsque le moteur CSS a transformé chaque angle en un vecteur de la forme [x,y,z,1], la matrice le convertit en [x,y,z,-1].

matrice d'identité quatre par quatre avec moins un sur p dans la quatrième ligne,
 troisième colonne multipliée par quatre par quatre, matrice d'identités quatre par quatre, moins n dans la deuxième ligne, quatrième colonne multipliée par quatre par quatre, matrice d'identité avec moins un sur la quatrième ligne, quatrième colonne multipliée par quatre dimensions, x, y, z, 1 est égal à quatre par quatre, moins une colonne d'identité moins quatre, moins une ligne p, quatrième ligne z vecteur nx quatrième ligne, nx quatrième ligne, nx quatrième ligne.

J'ai listé une étape intermédiaire pour montrer l'effet de notre matrice de transformation des éléments. Si vous n'êtes pas à l'aise avec les mathématiques matricielles, ce n'est pas grave. 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 est traduit vers le bas si vous faites défiler la page vers le bas.

Toutefois, si nous plaçons simplement cette matrice dans notre exemple, l'élément ne sera pas affiché. En effet, la spécification CSS exige que tout sommet dont w < 0 empêche l'affichage de l'élément. Et puisque notre coordonnée z est actuellement égale à 0 et que p est égale à 1, w est égal à -1.

Heureusement, nous pouvons choisir la valeur de z ! Pour nous assurer que nous nous retrouvons avec 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: Bougez

Notre cadre est présent et se présente comme sans transformation. Pour le moment, il n'est pas possible de faire défiler le conteneur de perspective. Nous ne pouvons donc pas le voir, mais nous savons que notre élément ira dans l'autre direction lorsqu'il sera fait défiler. Faisons défiler le conteneur. Nous pouvons simplement ajouter un élément d'espacement qui occupe 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>

Maintenant, faites défiler la page. Le cadre rouge est déplacé vers le bas.

Étape 3: Indiquez une taille

Nous avons un élément qui descend lorsque la page fait défiler vers le bas. C’est vraiment la partie difficile du chemin. Nous devons maintenant styliser l'image pour qu'elle ressemble à 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, alors que la piste n'est pas toujours visible. La hauteur du curseur est directement proportionnelle à la quantité de 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 recouvert par le pouce doit être égal à la proportion de contenu visible:

thumb point style point height over droperHeight égal à la hauteur du conteneur de défilement par rapport à la hauteur du défilement point de défilement si et seulement si la hauteur des points est égale à la hauteur du curseur de défilement multiplié par la hauteur du point de défilement sur la hauteur du défilement par 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 curseur est esthétique, mais elle se déplace beaucoup trop rapidement. C'est ici que nous pouvons récupérer notre technique dans le conteneur de défilement parallaxe. Si vous déplacez l'élément plus en arrière, il se déplacera plus lentement lors du défilement. Nous pouvons corriger la taille en l'agrandissant. Mais dans quelle mesure devons-nous le repousser exactement ? Faisons des calculs, vous l'avez deviné ! C'est la dernière fois, je vous le promets.

L'information essentielle est que le bord inférieur du curseur doit s'aligner sur le bord inférieur de l'élément à faire défiler lorsque l'utilisateur fait défiler l'écran jusqu'en bas. En d'autres termes, si nous avons fait défiler scroller.scrollHeight - scroller.height pixels, nous voulons que notre curseur soit traduit par scroller.height - thumb.height. Pour chaque pixel de conteneur de défilement, notre pouce doit déplacer une fraction de pixel:

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

C'est notre facteur de scaling. Nous devons maintenant convertir le facteur de mise à l'échelle en traduction le long de l'axe Z, comme nous l'avons déjà fait dans l'article sur le défilement parallaxe. Conformément à la section correspondante 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 de combien nous avons besoin pour déplacer notre pouce le long de l'axe Z. Cependant, gardez à l'esprit qu'en raison de la manipulation de nos coordonnées w, nous devons traduire une valeur -2px supplémentaire le long de "z". Notez également que les transformations d'un élément sont appliquées de droite à gauche, ce qui signifie que toutes les traductions précédant notre matrice spéciale ne seront pas inversées, contrairement à toutes les traductions après notre matrice spéciale. Codifions cela !

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

Nous avons une barre de défilement ! Il s'agit simplement d'un élément DOM dont nous pouvons personnaliser le style. En termes d'accessibilité, il est important que le curseur réagisse au clic et au déplacement, car de nombreux utilisateurs ont l'habitude d'interagir avec une barre de défilement de cette manière. Afin de ne pas allonger encore davantage cet article de blog, je ne vais pas vous expliquer en détail cette partie. Pour savoir comment procéder, consultez le code de la bibliothèque.

Qu'en est-il d'iOS ?

Ah, mon vieil ami iOS Safari. Comme pour le défilement parallaxe, nous rencontrons un problème ici. Étant donné que 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 l'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 allons faire exactement la même chose ici. Consultez l'article sur le parallaxe pour vous rafraîchir la mémoire.

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

Sur certains systèmes, nous devons gérer une barre de défilement native permanente. Auparavant, la barre de défilement ne pouvait pas être masquée (sauf avec un pseudo-sélecteur non standard). Donc, pour le dissimuler, nous devons faire appel à un peu de hacker (sans maths). Nous encapsulons l'élément de défilement dans un conteneur avec overflow-x: hidden et l'élargissons par rapport au conteneur. La barre de défilement native du navigateur n'est plus visible.

Aileron

En combinant tous ces éléments, nous pouvons maintenant créer une barre de défilement personnalisée parfaite, comme celle de notre démonstration Nyan Cat.

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

Voilà. Cela demandait 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 cela et l'effort en vaut probablement rarement la peine, sauf lorsqu'une barre de défilement personnalisée est une partie essentielle de l'expérience. Mais bon de savoir que c'est possible, non ? La difficulté de créer une barre de défilement personnalisée montre qu'il y a un travail à faire côté CSS. Mais n'ayez crainte ! À l'avenir, l'outil AnimationWorklet d'Houdini va faciliter considérablement les effets de défilement parfaits pour des frames comme celui-ci.