Chrometober est en cours de développement !

Découvrez comment le livre à défilement a vu le jour pour partager des conseils et astuces amusants et effrayants pour ce Chrometober.

Dans la continuité de Designcember, nous voulions créer Chrometober pour vous cette année afin de mettre en avant et de partager le contenu Web de la communauté et de l'équipe Chrome. Designcember a présenté l'utilisation des requêtes de conteneur, mais cette année, nous présentons l'API d'animations CSS liées au défilement.

Découvrez l'expérience de livre à faire défiler sur web.dev/chrometober-2022.

Présentation

L'objectif du projet était de proposer une expérience fantaisiste mettant en avant l'API d'animations associées au défilement. Toutefois, tout en étant fantaisiste, l'expérience devait également être réactive et accessible. Le projet a également été un excellent moyen de tester le polyfill d'API en cours de développement, ainsi que d'essayer différentes techniques et outils en combinaison. Le tout sur le thème festif d'Halloween !

Notre structure d'équipe se présentait comme suit:

Ébaucher une expérience de scrollytelling

Les idées pour Chrometober ont commencé à fuser lors de notre premier événement hors site en mai 2022. Une collection de gribouillis nous a amenés à réfléchir aux moyens par lesquels un utilisateur pourrait faire défiler son chemin dans une forme de storyboard. Inspiré des jeux vidéo, nous avons envisagé de faire défiler des scènes comme des cimetières et une maison hantée.

Un cahier est posé sur un bureau, avec différents gribouillis et doodles liés au projet.

C'était passionnant d'avoir la liberté créative de donner une direction inattendue à mon premier projet Google. Il s'agissait d'un prototype préliminaire de la façon dont un utilisateur pourrait naviguer dans le contenu.

Lorsque l'utilisateur fait défiler l'écran latéralement, les blocs pivotent et se réduisent. Mais j'ai décidé de m'éloigner de cette idée, car je me demandais comment rendre cette expérience optimale pour les utilisateurs sur des appareils de toutes tailles. J'ai donc opté pour la conception d'un élément que j'avais déjà créé. En 2020, j'ai eu la chance d'avoir accès à ScrollTrigger de GreenSock pour créer des démos de versions.

L'une des démonstrations que j'avais créées était un livre CSS 3D dans lequel les pages se tournaient lorsque vous faisiez défiler l'écran. Ce format nous semblait beaucoup plus approprié pour Chrometober. L'API d'animations liées à des défilements constitue une alternative idéale à cette fonctionnalité. Il fonctionne également bien avec scroll-snap, comme vous le verrez !

Notre illustrateur pour le projet, Tyler Reed, a su adapter la conception à nos idées. Tyler a fait un travail fantastique en concrétisant toutes les idées créatives qui lui ont été proposées. C'était très amusant de brainstormer ensemble. Une grande partie de notre objectif était de diviser les fonctionnalités en blocs isolés. Nous pouvions ainsi les composer en scènes, puis choisir ce que nous avions donné vie.

Une des scènes de composition avec un serpent, un cercueil avec des bras qui en sortent, un renard avec une baguette magique près d'un chaudron, un arbre avec un visage effrayant et une gargouille tenant une lanterne en forme de citrouille.

L'idée principale était que l'utilisateur pouvait accéder à des blocs de contenu au fur et à mesure de sa progression dans le livre. Ils pouvaient également interagir avec des touches d'humour, y compris les œufs de Pâques que nous avions intégrés à l'expérience. Par exemple, un portrait dans une maison hantée dont les yeux suivaient votre pointeur ou des animations subtiles déclenchées par des requêtes multimédias. Ces idées et fonctionnalités seraient animées lorsque l'utilisateur fait défiler la page. Une première idée consistait à utiliser un lapin zombie qui s'élèverait et se déplacerait le long de l'axe X lorsque l'utilisateur faisait défiler l'écran.

Se familiariser avec l'API

Avant de pouvoir commencer à tester les différentes fonctionnalités et les easter eggs, il nous fallait un livre. Nous avons donc décidé de profiter de cette occasion pour tester l'ensemble de fonctionnalités de l'API CSS Animations liées au défilement émergente. L'API d'animation liée au défilement n'est actuellement compatible avec aucun navigateur. Cependant, lors du développement de l'API, les ingénieurs de l'équipe Interactions ont travaillé sur un polyfill. Cela permet de tester la forme de l'API au fur et à mesure de son développement. Cela signifie que nous pouvons utiliser cette API aujourd'hui. Les projets amusants comme celui-ci sont souvent l'endroit idéal pour tester des fonctionnalités expérimentales et nous faire part de vos commentaires. Découvrez ce que nous avons appris et les commentaires que nous avons pu vous fournir, plus loin dans cet article.

De manière générale, vous pouvez utiliser cette API pour associer des animations au défilement. Il est important de noter que vous ne pouvez pas déclencher d'animation lors du défilement. Vous pourrez le faire plus tard. Les animations liées au défilement se divisent également en deux catégories principales:

  1. Ceux qui réagissent à la position de défilement.
  2. Ceux qui réagissent à la position d'un élément dans son conteneur de défilement.

Pour créer ce dernier, nous utilisons un ViewTimeline appliqué via une propriété animation-timeline.

Voici un exemple d'utilisation de ViewTimeline en CSS:

.element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
}

.element-scroll-linked {
  animation: rotate both linear;
  animation-timeline: foo;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
}

@keyframes rotate {
 to {
   rotate: 360deg;
 }
}

Nous créons un ViewTimeline avec view-timeline-name et définissons l'axe correspondant. Dans cet exemple, block fait référence à la block logique. L'animation est associée au défilement avec la propriété animation-timeline. animation-delay et animation-end-delay (au moment de la rédaction) sont les valeurs que nous utilisons pour définir les phases.

Ces phases définissent les points auxquels l'animation doit être associée par rapport à la position d'un élément dans son conteneur de défilement. Dans notre exemple, nous démarrons l'animation lorsque l'élément entre (enter 0%) dans le conteneur de défilement. et se termine lorsqu'il a couvert 50% (cover 50%) du conteneur à faire défiler.

Voici notre démonstration en action:

Vous pouvez également associer une animation à l'élément qui se déplace dans la fenêtre d'affichage. Pour ce faire, définissez animation-timeline sur l'view-timeline de l'élément. Cette option est utile pour les animations de liste, par exemple. Ce comportement est semblable à celui que vous pouvez utiliser pour animer des éléments à l'entrée à l'aide de IntersectionObserver.

element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
  animation: scale both linear;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
  animation-timeline: foo;
}

@keyframes scale {
  0% {
    scale: 0;
  }
}

"Mover" se met à l'échelle lorsqu'il entre dans la fenêtre d'affichage, ce qui déclenche la rotation de "Spinner".

J'ai constaté que l'API fonctionne très bien avec scroll-snap. Le forçage de défilement combiné à ViewTimeline est idéal pour le forçage de changement de page dans un livre.

Prototypage de la mécanique

Après quelques expériences, j'ai pu faire fonctionner un prototype de livre. Vous faites défiler l'écran horizontalement pour tourner les pages du livre.

Dans la démonstration, vous pouvez voir les différents déclencheurs mis en évidence par des bordures en pointillés.

Le balisage ressemble à ceci:

<body>
  <div class="book-placeholder">
    <ul class="book" style="--count: 7;">
      <li
        class="page page--cover page--cover-front"
        data-scroll-target="1"
        style="--index: 0;"
      >
        <div class="page__paper">
          <div class="page__side page__side--front"></div>
          <div class="page__side page__side--back"></div>
        </div>
      </li>
      <!-- Markup for other pages here -->
    </ul>
  </div>
  <div>
    <p>intro spacer</p>
  </div>
  <div data-scroll-intro>
    <p>scale trigger</p>
  </div>
  <div data-scroll-trigger="1">
    <p>page trigger</p>
  </div>
  <!-- Markup for other triggers here -->
</body>

Lorsque vous faites défiler la page, les pages du livre se tournent, mais s'ouvrent ou se ferment brusquement. Cela dépend de l'alignement de la fonction de glissement des déclencheurs.

html {
  scroll-snap-type: x mandatory;
}

body {
  grid-template-columns: repeat(var(--trigger-count), auto);
  overflow-y: hidden;
  overflow-x: scroll;
  display: grid;
}

body > [data-scroll-trigger] {
  height: 100vh;
  width: clamp(10rem, 10vw, 300px);
}

body > [data-scroll-trigger] {
  scroll-snap-align: end;
}

Cette fois, nous ne connectons pas le ViewTimeline en CSS, mais utilisons l'API Web Animations en JavaScript. Cela présente l'avantage supplémentaire de pouvoir effectuer une boucle sur un ensemble d'éléments et de générer les ViewTimeline dont nous avons besoin, au lieu de les créer manuellement.

const triggers = document.querySelectorAll("[data-scroll-trigger]")

const commonProps = {
  delay: { phase: "enter", percent: CSS.percent(0) },
  endDelay: { phase: "enter", percent: CSS.percent(100) },
  fill: "both"
}

const setupPage = (trigger, index) => {
  const target = document.querySelector(
    `[data-scroll-target="${trigger.getAttribute("data-scroll-trigger")}"]`
  );

  const viewTimeline = new ViewTimeline({
    subject: trigger,
    axis: 'inline',
  });

  target.animate(
    [
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`
      },
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`,
        offset: 0.75
      },
      {
        transform: `translateZ(${(triggers.length - index) * -1}px)`
      }
    ],
    {
      timeline: viewTimeline,
      commonProps,
    }
  );
  target.querySelector(".page__paper").animate(
    [
      {
        transform: "rotateY(0deg)"
      },
      {
        transform: "rotateY(-180deg)"
      }
    ],
    {
      timeline: viewTimeline,
      commonProps,
    }
  );
};

const triggers = document.querySelectorAll('[data-scroll-trigger]')
triggers.forEach(setupPage);

Pour chaque déclencheur, nous générons un ViewTimeline. Nous animons ensuite la page associée au déclencheur à l'aide de cet élément ViewTimeline. qui associe l'animation de la page au défilement. Pour notre animation, nous faisons pivoter un élément de la page sur l'axe Y pour la faire tourner. Nous traduisons également la page elle-même sur l'axe Z pour qu'elle se comporte comme un livre.

Synthèse

Une fois que j'ai élaboré le mécanisme du livre, je pouvais me concentrer sur la mise en valeur des illustrations de Tyler.

Astro

L'équipe a utilisé Astro pour Designcember en 2021, et j'avais envie de l'utiliser à nouveau pour Chrometober. L'expérience de développement permettant de diviser les éléments en composants est bien adaptée à ce projet.

Le livre lui-même est un composant. Il s'agit également d'une collection de composants de page. Chaque page a deux côtés et un arrière-plan. Les enfants d'un côté de page sont des composants que vous pouvez facilement ajouter, supprimer et positionner.

Créer un livre

Il était important pour moi de faciliter la gestion des blocs. Je voulais également aider le reste de l'équipe à apporter des contributions.

Les pages de haut niveau sont définies par un tableau de configuration. Chaque objet de page du tableau définit le contenu, l'arrière-plan et d'autres métadonnées d'une page.

const pages = [
  {
    front: {
      marked: true,
      content: PageTwo,
      backdrop: spreadOne,
      darkBackdrop: spreadOneDark
    },
    back: {
      content: PageThree,
      backdrop: spreadTwo,
      darkBackdrop: spreadTwoDark
    },
    aria: `page 1`
  },
  /* Obfuscated page objects */
]

Elles sont transmises au composant Book.

<Book pages={pages} />

Le composant Book est l'endroit où le mécanisme de défilement est appliqué et où les pages du livre sont créées. Le même mécanisme que celui du prototype est utilisé, mais nous partageons plusieurs instances de ViewTimeline créées globalement.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

Nous pouvons ainsi partager les chronologies à utiliser ailleurs au lieu de les recréer. Nous reviendrons sur ce point.

Composition de la page

Chaque page est un élément de liste dans une liste:

<ul class="book">
  {
    pages.map((page, index) => {
      const FrontSlot = page.front.content
      const BackSlot = page.back.content
      return (
        <Page
          index={index}
          cover={page.cover}
          aria={page.aria}
          backdrop={
            {
              front: {
                light: page.front.backdrop,
                dark: page.front.darkBackdrop
              },
              back: {
                light: page.back.backdrop,
                dark: page.back.darkBackdrop
              }
            }
          }>
          {page.front.content && <FrontSlot slot="front" />}    
          {page.back.content && <BackSlot slot="back" />}    
        </Page>
      )
    })
  }
</ul>

La configuration définie est transmise à chaque instance Page. Les pages utilisent la fonctionnalité d'emplacement d'Astro pour insérer du contenu sur chacune d'elles.

<li
  class={className}
  data-scroll-target={target}
  style={`--index:${index};`}
  aria-label={aria}
>
  <div class="page__paper">
    <div
      class="page__side page__side--front"
      aria-label={`Right page of ${index}`}
    >
      <picture>
        <source
          srcset={darkFront}
          media="(prefers-color-scheme: dark)"
          height="214"
          width="150"
        >
        <img
          src={lightFront}
          class="page__background page__background--right"
          alt=""
          aria-hidden="true"
          height="214"
          width="150"
        >
      </picture>
      <div class="page__content">
        <slot name="front" />
      </div>
    </div>
    <!-- Markup for back page -->
  </div>
</li>

Ce code sert principalement à configurer la structure. Les contributeurs peuvent travailler sur le contenu du livre dans la plupart des cas sans avoir à toucher à ce code.

Backdrops

Le passage à un livre a permis de diviser les sections beaucoup plus facilement. Chaque double page du livre est une scène tirée de la conception d'origine.

Illustration de double page du livre représentant un pommier dans un cimetière. Le cimetière comporte plusieurs pierres tombales et une chauve-souris est visible dans le ciel devant une grande lune.

Comme nous avons choisi un format pour le livre, le fond de chaque page peut comporter un élément image. Définissez cet élément sur une largeur de 200% et utilisez object-position en fonction du côté de la page.

.page__background {
  height: 100%;
  width: 200%;
  object-fit: cover;
  object-position: 0 0;
  position: absolute;
  top: 0;
  left: 0;
}

.page__background--right {
  object-position: 100% 0;
}

Contenu de la page

Voyons comment créer l'une des pages. La page 3 présente un hibou qui apparaît dans un arbre.

Il est renseigné par un composant PageThree, comme défini dans la configuration. Il s'agit d'un composant Astro (PageThree.astro). Ces composants ressemblent à des fichiers HTML, mais comportent, dans leur partie supérieure, une bordure de code semblable à l'élément frontal. Cela nous permet, par exemple, d'importer d'autres composants. Le composant de la page 3 se présente comme suit:

---
import TreeOwl from '../TreeOwl/TreeOwl.astro'
import { contentBlocks } from '../../assets/content-blocks.json'
import ContentBlock from '../ContentBlock/ContentBlock.astro'
---
<TreeOwl/>
<ContentBlock {...contentBlocks[3]} id="four" />

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

Encore une fois, les pages sont de nature atomique. Ils sont créés à partir d'une collection d'éléments géographiques. La page 3 comporte un bloc de contenu et la chouette interactive. Il y a donc un composant pour chacun.

Les blocs de contenu correspondent aux liens vers le contenu affiché dans le livre. Ils sont également gérés par un objet de configuration.

{
 "contentBlocks": [
    {
      "id": "one",
      "title": "New in Chrome",
      "blurb": "Lift your spirits with a round up of all the tools and features in Chrome.",
      "link": "https://www.youtube.com/watch?v=qwdN1fJA_d8&list=PLNYkxOF6rcIDfz8XEA3loxY32tYh7CI3m"
    },
    …otherBlocks
  ]
}

Cette configuration est importée lorsque des blocs de contenu sont requis. La configuration de bloc appropriée est ensuite transmise au composant ContentBlock.

<ContentBlock {...contentBlocks[3]} id="four" />

Vous trouverez également un exemple d'utilisation du composant de la page comme emplacement pour positionner le contenu. Ici, un bloc de contenu est positionné.

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

Toutefois, les styles généraux d'un bloc de contenu se trouvent au même endroit que le code du composant.

.content-block {
  background: hsl(0deg 0% 0% / 70%);
  color: var(--gray-0);
  border-radius:  min(3vh, var(--size-4));
  padding: clamp(0.75rem, 2vw, 1.25rem);
  display: grid;
  gap: var(--size-2);
  position: absolute;
  cursor: pointer;
  width: 50%;
}

Quant à notre hibou, il s'agit d'une fonctionnalité interactive, parmi tant d'autres dans ce projet. Il s'agit d'un petit exemple qui montre comment nous avons utilisé la ViewTimeline partagée que nous avons créée.

De manière générale, notre composant chouette importe des SVG et les insère en ligne à l'aide du fragment d'Astro.

---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />

Les styles de positionnement de notre hibou sont situés à côté du code du composant.

.owl {
  width: 34%;
  left: 10%;
  bottom: 34%;
}

Un élément de style supplémentaire définit le comportement transform pour la chouette.

.owl__owl {
  transform-origin: 50% 100%;
  transform-box: fill-box;
}

L'utilisation de transform-box affecte le transform-origin. Il le rend relatif au cadre de délimitation de l'objet dans le SVG. L'hibou se met à l'échelle à partir du centre du bas, d'où l'utilisation de transform-origin: 50% 100%.

L'aspect amusant est que nous pouvons associer la chouette à l'un de nos ViewTimeline générés:

const setUpOwl = () => {
   const owl = document.querySelector('.owl__owl');

   owl.animate([
     {
       translate: '0% 110%',
     },
     {
       translate: '0% 10%',
     },
   ], {
     timeline: CHROMETOBER_TIMELINES[1],
     delay: { phase: "enter", percent: CSS.percent(80) },
     endDelay: { phase: "enter", percent: CSS.percent(90) },
     fill: 'both' 
   });
 }

 if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches)
   setUpOwl()

Dans ce bloc de code, nous effectuons deux opérations:

  1. Vérifiez les préférences de mouvement de l'utilisateur.
  2. S'il n'a pas de préférence, associez une animation de la chouette au défilement.

Dans la deuxième partie, la chouette s'anime sur l'axe des ordonnées à l'aide de l'API Web Animations. La propriété de transformation individuelle translate est utilisée. Elle est liée à un ViewTimeline. Il est associé à CHROMETOBER_TIMELINES[1] via la propriété timeline. Il s'agit d'un ViewTimeline généré pour le changement de page. Cela permet d'associer l'animation de la chouette au retournement de page à l'aide de la phase enter. Il définit que, lorsque la page est tournée à 80 %, le hibou doit commencer à bouger. À 90%, la chouette doit terminer sa traduction.

Fonctionnalités du livre

Vous savez maintenant comment créer une page et comment fonctionne l'architecture du projet. Vous pouvez voir comment cela permet aux contributeurs de se lancer et de travailler sur une page ou une fonctionnalité de leur choix. Les animations de diverses fonctionnalités du livre sont liées au fait que les pages se tournent. Par exemple, la chauve-souris qui vole et sort à chaque page tournée.

Il comporte également des éléments alimentés par des animations CSS.

Une fois les blocs de contenu présents dans le livre, nous avons pu faire preuve de créativité concernant d'autres fonctionnalités. Cela nous a permis de générer différentes interactions et d'essayer différentes manières d'implémenter des éléments.

Assurer la réactivité

Les unités de fenêtre d'affichage responsives définissent la taille du livre et de ses fonctionnalités. Toutefois, maintenir la réactivité des polices a été un défi intéressant. Les unités de requête de conteneur sont un bon choix ici. Toutefois, elles ne sont pas encore disponibles partout. La taille du livre est définie. Il n'est donc pas nécessaire d'utiliser une requête de conteneur. Une unité de requête de conteneur intégrée peut être générée avec calc() CSS et utilisée pour la mise en page des polices.


.book-placeholder {
  --size: clamp(12rem, 72vw, 80vmin);
  --aspect-ratio: 360 / 504;
  --cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}

.content-block h2 {
  color: var(--gray-0);
  font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}

.content-block :is(p, a) {
  font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}

Citrouilles qui brillent la nuit

Les plus attentifs auront peut-être remarqué l'utilisation d'éléments <source> lorsque nous avons parlé des arrière-plans de page précédemment. Una souhaitait que l'interaction réagisse aux préférences de jeu de couleurs. Les arrière-plans sont donc compatibles avec les modes clair et sombre, avec différentes variantes. Étant donné que vous pouvez utiliser des requêtes multimédias avec l'élément <picture>, il s'agit d'un excellent moyen de fournir deux styles de toile de fond. L'élément <source> interroge la préférence de jeu de couleurs et affiche le fond approprié.

<picture>
  <source srcset={darkFront} media="(prefers-color-scheme: dark)" height="214" width="150">
  <img src={lightFront} class="page__background page__background--right" alt="" aria-hidden="true" height="214" width="150">
</picture>

Vous pouvez apporter d'autres modifications en fonction de cette préférence de jeu de couleurs. Les citrouilles de la deuxième page réagissent aux préférences de jeu de couleurs de l'utilisateur. Le SVG utilisé comporte des cercles représentant des flammes, qui se développent et s'animent en mode sombre.

.pumpkin__flame,
 .pumpkin__flame circle {
   transform-box: fill-box;
   transform-origin: 50% 100%;
 }

 .pumpkin__flame {
   scale: 0.8;
 }

 .pumpkin__flame circle {
   transition: scale 0.2s;
   scale: 0;
 }

@media(prefers-color-scheme: dark) {
   .pumpkin__flame {
     animation: pumpkin-flicker 3s calc(var(--index, 0) * -1s) infinite linear;
   }

   .pumpkin__flame circle {
     scale: 1;
   }

   @keyframes pumpkin-flicker {
     50% {
       scale: 1;
     }
   }
 }

Ce portrait vous regarde-t-il ?

Si vous consultez la page 10, vous remarquerez peut-être quelque chose. Vous êtes surveillé ! Les yeux du portrait suivent votre pointeur lorsque vous vous déplacez sur la page. L'astuce consiste à mapper l'emplacement du pointeur sur une valeur de translation, puis à la transmettre au CSS.

const mapRange = (inputLower, inputUpper, outputLower, outputUpper, value) => {
   const INPUT_RANGE = inputUpper - inputLower
   const OUTPUT_RANGE = outputUpper - outputLower
   return outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
 }

Ce code prend en charge les plages d'entrée et de sortie, et mappe les valeurs données. Par exemple, cette utilisation donnerait la valeur 625.

mapRange(0, 100, 250, 1000, 50) // 625

Pour le portrait, la valeur d'entrée correspond au point central de chaque œil, plus ou moins une distance de quelques pixels. La plage de sortie correspond à la traduction en pixels des yeux. La position du pointeur sur l'axe X ou Y est ensuite transmise en tant que valeur. Pour obtenir le point central des yeux lorsque vous les déplacez, les yeux sont dupliqués. Les originaux ne bougent pas, sont transparents et servent de référence.

Ensuite, il s'agit de l'associer et de mettre à jour les valeurs des propriétés personnalisées CSS sur les yeux afin qu'ils puissent bouger. Une fonction est liée à l'événement pointermove par rapport à window. Lorsque ce déclencheur se produit, les limites de chaque œil sont utilisées pour calculer les points centraux. La position du pointeur est ensuite mappée sur des valeurs définies en tant que valeurs de propriété personnalisées sur les yeux.

const RANGE = 15
const LIMIT = 80
const interact = ({ x, y }) => {
   // map a range against the eyes and pass in via custom properties
   const LEFT_EYE_BOUNDS = LEFT_EYE.getBoundingClientRect()
   const RIGHT_EYE_BOUNDS = RIGHT_EYE.getBoundingClientRect()

   const CENTERS = {
     lx: LEFT_EYE_BOUNDS.left + LEFT_EYE_BOUNDS.width * 0.5,
     rx: RIGHT_EYE_BOUNDS.left + RIGHT_EYE_BOUNDS.width * 0.5,
     ly: LEFT_EYE_BOUNDS.top + LEFT_EYE_BOUNDS.height * 0.5,
     ry: RIGHT_EYE_BOUNDS.top + RIGHT_EYE_BOUNDS.height * 0.5,
   }

   Object.entries(CENTERS)
     .forEach(([key, value]) => {
       const result = mapRange(value - LIMIT, value + LIMIT, -RANGE, RANGE)(key.indexOf('x') !== -1 ? x : y)
       EYES.style.setProperty(`--${key}`, result)
     })
 }

Une fois les valeurs transmises au CSS, les styles peuvent en faire ce qu'ils veulent. L'avantage est que vous pouvez utiliser clamp() CSS pour différencier le comportement de chaque œil, et ainsi faire en sorte que chaque œil se comporte différemment sans avoir à modifier le code JavaScript.

.portrait__eye--mover {
   transition: translate 0.2s;
 }

 .portrait__eye--mover.portrait__eye--left {
   translate:
     clamp(-10px, var(--lx, 0) * 1px, 4px)
     clamp(-4px, var(--ly, 0) * 0.5px, 10px);
 }

 .portrait__eye--mover.portrait__eye--right {
   translate:
     clamp(-4px, var(--rx, 0) * 1px, 10px)
     clamp(-4px, var(--ry, 0) * 0.5px, 10px);
 }

Lancer des sorts

Si vous consultez la page 6, vous sentez-vous envoûté ? Cette page présente la conception de notre renard magique fantastique. Si vous déplacez votre pointeur, vous pouvez voir un effet de traînée de curseur personnalisé. Cette animation utilise le canevas. Un élément <canvas> se trouve au-dessus du reste du contenu de la page, avec pointer-events: none. Cela signifie que les utilisateurs peuvent toujours cliquer sur les blocs de contenu situés en dessous.

.wand-canvas {
  height: 100%;
  width: 200%;
  pointer-events: none;
  right: 0;
  position: fixed;
}

Tout comme notre portrait écoute un événement pointermove sur window, notre élément <canvas> fait de même. Pourtant, chaque fois que l'événement se déclenche, nous créons un objet à animer sur l'élément <canvas>. Ces objets représentent les formes utilisées dans la traînée du curseur. Elles ont des coordonnées et une teinte aléatoire.

Notre fonction mapRange précédente est utilisée à nouveau, car nous pouvons l'utiliser pour mapper le delta du pointeur sur size et rate. Les objets sont stockés dans un tableau qui est itéré lorsque les objets sont dessinés sur l'élément <canvas>. Les propriétés de chaque objet indiquent à notre élément <canvas> où les éléments doivent être dessinés.

const blocks = []
  const createBlock = ({ x, y, movementX, movementY }) => {
    const LOWER_SIZE = CANVAS.height * 0.05
    const UPPER_SIZE = CANVAS.height * 0.25
    const size = mapRange(0, 100, LOWER_SIZE, UPPER_SIZE, Math.max(Math.abs(movementX), Math.abs(movementY)))
    const rate = mapRange(LOWER_SIZE, UPPER_SIZE, 1, 5, size)
    const { left, top, width, height } = CANVAS.getBoundingClientRect()
    
    const block = {
      hue: Math.random() * 359,
      x: x - left,
      y: y - top,
      size,
      rate,
    }
    
    blocks.push(block)
  }
window.addEventListener('pointermove', createBlock)

Pour dessiner sur le canevas, une boucle est créée avec requestAnimationFrame. La traînée du curseur ne doit s'afficher que lorsque la page est visible. Nous avons un IntersectionObserver qui met à jour et détermine les pages visibles. Si une page est visible, les objets s'affichent sous forme de cercles sur le canevas.

Nous effectuons ensuite une boucle sur le tableau blocks et dessinons chaque partie du parcours. Chaque image réduit la taille et modifie la position de l'objet selon le rate. Cela produit un effet de baisse et de mise à l'échelle. Si l'objet se réduit complètement, il est supprimé du tableau blocks.

let wandFrame
const drawBlocks = () => {
   ctx.clearRect(0, 0, CANVAS.width, CANVAS.height)
  
   if (PAGE_SIX.className.indexOf('in-view') === -1 && wandFrame) {
     blocks.length = 0
     cancelAnimationFrame(wandFrame)
     document.body.removeEventListener('pointermove', createBlock)
     document.removeEventListener('resize', init)
   }
  
   for (let b = 0; b < blocks.length; b++) {
     const block = blocks[b]
     ctx.strokeStyle = ctx.fillStyle = `hsla(${block.hue}, 80%, 80%, 0.5)`
     ctx.beginPath()
     ctx.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI)
     ctx.stroke()
     ctx.fill()

     block.size -= block.rate
     block.y += block.rate

     if (block.size <= 0) {
       blocks.splice(b, 1)
     }

   }
   wandFrame = requestAnimationFrame(drawBlocks)
 }

Si la page disparaît de l'écran, les écouteurs d'événements sont supprimés et la boucle de frame d'animation est annulée. Le tableau blocks est également effacé.

Voici le tracé du curseur en action !

Examen de l'accessibilité

Il est tout à fait acceptable de créer une expérience amusante à explorer, mais cela ne sert à rien si elle n'est pas accessible aux utilisateurs. L'expertise d'Adam dans ce domaine s'est avérée inestimable pour préparer Chrometober à un examen d'accessibilité avant sa sortie.

Voici quelques-uns des domaines abordés:

  • Vérifier que le code HTML utilisé était sémantique. Cela incluait des éléments de repère appropriés tels que <main> pour le livre, ainsi que l'utilisation de l'élément <article> pour chaque bloc de contenu et des éléments <abbr> où des acronymes sont introduits. En réfléchissant à l'avance au moment de la création du livre, nous avons rendu les choses plus accessibles. L'utilisation de titres et de liens facilite la navigation pour l'utilisateur. L'utilisation d'une liste pour les pages signifie également que le nombre de pages est annoncé par les technologies d'assistance.
  • Assurez-vous que toutes les images utilisent des attributs alt appropriés. Pour les SVG intégrés, l'élément title est présent si nécessaire.
  • Utilisation d'attributs aria pour améliorer l'expérience. L'utilisation de aria-label pour les pages et leurs côtés indique à l'utilisateur sur quelle page il se trouve. L'utilisation de aria-describedBy sur les liens "Lire la suite" permet de communiquer le texte du bloc de contenu. Cela élimine toute ambiguïté quant à la destination du lien.
  • Concernant les blocs de contenu, il est possible de cliquer sur l'ensemble de la fiche et non seulement sur le lien "Lire la suite".
  • Nous avons déjà vu que vous pouvez utiliser un IntersectionObserver pour suivre les pages affichées. Cela présente de nombreux avantages, qui ne sont pas uniquement liés aux performances. Les animations ou les interactions seront mises en pause sur les pages qui ne sont pas affichées. Toutefois, l'attribut inert est également appliqué à ces pages. Cela signifie que les utilisateurs d'un lecteur d'écran peuvent explorer le même contenu que les utilisateurs voyants. Le curseur reste sur la page visible, et les utilisateurs ne peuvent pas accéder à une autre page à l'aide de la touche de tabulation.
  • Enfin, nous utilisons des requêtes multimédias pour respecter les préférences de l'utilisateur en termes de mouvement.

Voici une capture d'écran de l'examen mettant en évidence certaines des mesures en place.

est identifié comme correspondant à l'ensemble du livre, ce qui indique qu'il doit être le point de repère principal que les utilisateurs de technologies d'assistance doivent trouver. Plus d'informations sont disponibles dans la capture d'écran." width="800" height="465">

Capture d&#39;écran du livre Chrometober ouvert. Des encadrés verts sont fournis autour de divers aspects de l&#39;interface utilisateur, décrivant la fonctionnalité d&#39;accessibilité prévue et les résultats de l&#39;expérience utilisateur que la page fournira. Par exemple, les images comportent un texte alternatif. Un autre exemple est un libellé d&#39;accessibilité indiquant que les pages hors champ sont inactives. La capture d&#39;écran présente d&#39;autres éléments.

Les enseignements

L'objectif de Chrometober n'était pas seulement de mettre en avant le contenu Web de la communauté, mais aussi de tester le polyfill de l'API d'animations liées au défilement qui est en cours de développement.

Lors de notre sommet d'équipe à New York, nous avons réservé une session pour tester le projet et résoudre les problèmes qui se sont présentés. La contribution de l'équipe a été inestimable. C'était aussi une excellente occasion de dresser la liste de tous les éléments à traiter avant de pouvoir lancer le service.

L&#39;équipe chargée du CSS, de l&#39;interface utilisateur et des outils de développement est réunie dans une salle de conférence. Una se tient à un tableau blanc recouvert de notes adhésives. Les autres membres de l&#39;équipe sont assis autour d&#39;une table avec des rafraîchissements et des ordinateurs portables.

Par exemple, le test du livre sur les appareils a soulevé un problème de rendu. Notre livre ne s'affichait pas comme prévu sur les appareils iOS. Les unités de fenêtre d'affichage dimensionnent la page, mais lorsqu'une encoche était présente, cela affectait le livre. La solution consistait à utiliser viewport-fit=cover dans la fenêtre d'affichage meta:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

Cette session a également soulevé quelques problèmes avec le polyfill d'API. Bramus a signalé ces problèmes dans le dépôt de polyfills. Il a ensuite trouvé des solutions à ces problèmes et les a fusionnées dans le polyfill. Par exemple, cette demande d'extraction a amélioré les performances en ajoutant une mise en cache à une partie du polyfill.

Capture d&#39;écran d&#39;une démonstration ouverte dans Chrome. Les outils pour les développeurs sont ouverts et affichent une mesure des performances de référence.

Capture d&#39;écran d&#39;une démonstration ouverte dans Chrome. Les outils pour les développeurs sont ouverts et affichent une mesure des performances améliorée.

Et voilà !

Il s'agissait d'un projet vraiment amusant sur lequel travailler, ce qui se traduisait par une expérience de défilement fantaisiste qui met en avant des contenus incroyables de la communauté. De plus, il a été très utile pour tester le polyfill et fournir des commentaires à l'équipe d'ingénieurs pour l'améliorer.

Chrometober 2022 est terminé.

Nous espérons qu'elle vous a plu. Quelle est votre fonctionnalité préférée ? N'hésitez pas à me envoyer un tweet.

Jhey tient une feuille d&#39;autocollants représentant les personnages de Chrometober.

Vous pourrez peut-être même en obtenir auprès d'un membre de l'équipe si vous nous rencontrez lors d'un événement.

Photo d'illustration par David Menidrey sur Unsplash