Chrometober est en cours de développement !

Découvrez comment ce livre défilant a pris vie pour partager des conseils et astuces amusants et effrayants avec ce Chrometober.

Après Designcember, nous voulions créer Chrometober pour vous cette année afin de mettre en avant et de partager du contenu Web de la communauté et de l'équipe Chrome. Designcember a mis en avant l'utilisation des requêtes Container, mais cette année, nous allons vous présenter l'API CSS associée aux animations liées au défilement.

Découvrez comment faire défiler les livres sur web.dev/chrometober-2022.

Présentation

L'objectif du projet était de proposer une expérience fantaisiste mettant en évidence l'API d'animations liées au défilement. Mais, 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, en plus de la combinaison de plusieurs techniques et outils. Sur un thème festif d'Halloween !

La structure de notre équipe ressemblait à ceci:

Concevoir une expérience de défilement

Les idées pour Chrometober ont commencé à germer dans notre première équipe 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 une certaine forme de storyboard. Inspirés des jeux vidéo, nous avons envisagé de faire défiler des scènes comme des cimetières et une maison hantée.

Un carnet est posé sur un bureau et contient divers doodles et gribouillis en lien avec le projet.

J'ai été ravi d'avoir la liberté créative d'emmener mon premier projet Google dans une direction inattendue. Il s'agissait du premier prototype de la façon dont un utilisateur peut naviguer dans le contenu.

Lorsque l'utilisateur fait défiler la page latéralement, les blocs pivotent et s'ajustent. Cependant, j'ai décidé d'abandonner cette idée par souci d'améliorer l'expérience des utilisateurs sur des appareils de toutes tailles. Au lieu de cela, je me suis penchée sur la conception de quelque chose que j'avais fait dans le passé. En 2020, j'ai eu la chance d'avoir accès à ScrollTrigger de GreenSock pour créer des versions de démonstration.

L'une des démonstrations que j'avais proposées était un livre CSS en 3D dont les pages se tournaient au fur et à mesure que vous faisiez défiler l'écran. Cela nous a semblé beaucoup plus adapté à ce que nous voulions pour Chrometober. L'API d'animations liées au défilement est idéale pour cette fonctionnalité. Cela fonctionne également bien avec scroll-snap, comme vous allez le voir.

Tyler Reed, notre illustrateur du projet, a été très doué pour modifier la conception au fur et à mesure que nous modifiions les idées. Tyler a accompli un travail fantastique en donnant vie à toutes les idées créatives qui lui étaient proposées. Ce fut un plaisir de faire de nombreuses idées ensemble. Nous voulions que cela fonctionne en grande partie parce que les fonctionnalités étaient divisées en blocs isolés. Ainsi, nous pouvions les composer en scènes, puis choisir ce que nous leur avions mis en œuvre.

L'une des scènes de la composition montrant un serpent, un cercueil avec des bras qui sortent, un renard avec une baguette dans un chaudron, un arbre au visage effrayant et une gargouille tenant une lanterne citrouille.

L'idée principale était qu'à mesure que l'utilisateur parcourait le livre, il pouvait accéder à des blocs de contenu. Ils pouvaient aussi interagir avec des traits de fantaisie, y compris des Easter eggs que nous avions intégrés dans l'expérience ; par exemple, un portrait dans une maison hantée dont les yeux suivaient votre curseur ou des animations subtiles déclenchées par des requêtes média. Ces idées et fonctionnalités seraient animées lors du défilement. L'idée de départ était la création d'un lapin zombie qui se lève et se déplace le long de l'axe des abscisses lorsque l'utilisateur fait défiler la page.

Se familiariser avec l'API

Avant de pouvoir commencer à jouer avec les caractéristiques individuelles et les easter eggs, il nous fallait un livre. Nous avons donc décidé d'en profiter pour tester l'ensemble de fonctionnalités de l'API émergente d'animations liées au défilement CSS. L'API des animations liées au défilement n'est actuellement compatible avec aucun navigateur. Toutefois, lors du développement de l'API, les ingénieurs de l'équipe chargée des interactions ont travaillé sur un polyfill. Cela permet de tester la forme de l'API au fur et à mesure de son développement. Nous pouvons donc utiliser cette API aujourd'hui, et des projets amusants comme celui-ci constituent souvent un excellent endroit pour tester des fonctionnalités expérimentales et donner votre avis. Découvrez ce que nous avons appris et les commentaires que nous avons pu partager plus loin dans l'article.

De manière générale, vous pouvez utiliser cette API pour associer des animations au défilement. Notez que vous ne pouvez pas déclencher d'animation lors du défilement. Cette opération peut survenir plus tard. Les animations liées au défilement appartiennent également à 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 une ViewTimeline avec view-timeline-name et définissons son axe. Dans cet exemple, block fait référence à une block logique. L'animation est liée au défilement avec la propriété animation-timeline. Les phases sont définies par animation-delay et animation-end-delay (au moment de la rédaction de ce document).

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

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 comme view-timeline de l'élément. Cette approche est adaptée aux scénarios de listes, par exemple. Le comportement est semblable à celui de l'animation d'éléments lors de la saisie avec 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;
  }
}

Ainsi, le "Mover" (déplaceur) s'agrandit à mesure qu'il entre dans la fenêtre d'affichage, ce qui déclenche la rotation de "Spinner".

Ce que j'ai découvert en testant, c'est que l'API fonctionne très bien avec scroll-snap. L'utilisation de la fonctionnalité d'ancrage et de défilement, combinée à ViewTimeline, est idéale pour modifier le changement de page dans un livre.

Prototypage du mécanisme

Après quelques expériences, j'ai pu obtenir un prototype de livre fonctionnel. Vous pouvez faire défiler les pages du livre horizontalement.

Dans la démonstration, les différents déclencheurs sont mis en évidence, encadrés 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 les pages du livre, celles-ci tournent, mais s'ouvrent ou se ferment automatiquement. Cela dépend de l'alignement de l'ancrage de défilement 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 nous 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 chacun 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 ce ViewTimeline. Cela permet de créer un lien entre l'animation de la page et le défilement. Pour notre animation, nous faisons pivoter un élément de la page sur l'axe Y pour tourner la page. Nous traduisons également la page elle-même sur l'axe Z afin qu'elle se comporte comme un livre.

Synthèse

Une fois que j'ai mis au point le mécanisme du livre, je pouvais me concentrer sur la mise en œuvre des illustrations de Tyler.

Astro

L'équipe a utilisé Astro pour Designcember en 2021 et j'avais hâte de l'utiliser à nouveau pour Chrometober. L'expérience de développement de la capacité à décomposer les choses en composants est bien adaptée à ce projet.

Le livre lui-même est un composant. Il s'agit également d'un ensemble de composants de page. Chaque page a deux côtés et ils ont un fond. Les enfants d'un côté de la page sont des composants qui peuvent être ajoutés, supprimés et positionnés facilement.

Création d'un livre

Il était important pour moi de faciliter la gestion des blocs. Je voulais également permettre au reste de l'équipe d'apporter facilement des contributions.

Les pages de haut niveau sont définies par un tableau de configuration. Chaque objet "page" du tableau définit le contenu, le fond 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 */
]

Ils sont transmis au composant Book.

<Book pages={pages} />

Le composant Book permet d'appliquer le mécanisme de défilement et de créer les pages du livre. Le même mécanisme que celui du prototype est utilisé, mais nous partageons plusieurs instances de ViewTimeline créées dans le monde entier.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

De cette façon, nous pouvons partager les chronologies pour les 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 ensuite transmise à chaque instance Page. Les pages utilisent la fonctionnalité d'emplacement d'Astro pour insérer du contenu dans chaque page.

<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 à modifier ce code.

Toiles de fond

Le passage créatif vers un livre a grandement facilité la division des sections, et chaque étalonnage du livre est une scène tirée de la conception originale.

Illustration de la page du livre qui montre un pommier dans un cimetière. Le cimetière comporte plusieurs pierres tombales, et une chauve-souris se trouve dans le ciel, devant une grande lune.

Comme nous avions choisi un format pour le livre, l'arrière-plan de chaque page pourrait comporter un élément image. Définir cet élément sur 200% de largeur et utiliser object-position en fonction du côté de la page permet de résoudre le problème.

.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 une de ces pages. La page 3 montre une chouette qui saute dans un arbre.

Il est renseigné avec un composant PageThree, tel que défini dans la configuration. Il s'agit d'un composant Astro (PageThree.astro). Ces composants ressemblent à des fichiers HTML, mais ils ont une barrière de code en haut semblable à la première. Cela nous permet d'importer d'autres composants, par exemple. 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 atomiques par nature. Elles reposent sur un ensemble de fonctionnalités. La troisième page présente un bloc de contenu et la chouette interactive, qui comporte donc un composant pour chacune d'elles.

Les blocs de contenu correspondent aux liens qui s'affichent dans le livre. Elles sont également gérées 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 là où des blocs de contenu sont requis. La configuration de bloc appropriée est ensuite transmise au composant ContentBlock.

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

L'exemple illustre également la façon dont nous utilisons le composant de la page 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 sont 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 à la chouette, il s'agit d'une fonctionnalité interactive, parmi d'autres dans ce projet. Il s'agit d'un petit exemple illustrant la façon dont nous avons utilisé la ViewTimeline partagée que nous avons créée.

De manière générale, notre composant "Hibou" importe du SVG et l'intègre à l'aide du fragment d'Astro.

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

Les styles de positionnement de "owl" sont au même endroit que le code du composant.

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

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

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

L'utilisation de transform-box affecte transform-origin. Il se rapporte au cadre de délimitation de l'objet dans le SVG. La chouette s'agrandit à partir du centre du bas, ce qui explique l'utilisation de transform-origin: 50% 100%.

La partie amusante consiste à associer la chouette à l'un des 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 faisons deux choses:

  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 pour qu'elle fasse défiler l'écran.

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 et est liée à une 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 changement de page via la phase enter. Il définit que, lorsque la page est tournée à 80 %, commencer à déplacer la chouette. À 90%, la chouette devrait terminer sa traduction.

Fonctionnalités du livre

Vous avez maintenant vu l'approche pour créer une page et le fonctionnement de l'architecture du projet. Vous pouvez voir comment elle permet aux contributeurs d'intervenir et de travailler sur une page ou une fonctionnalité de leur choix. L'animation de différentes caractéristiques du livre est liée au passage des pages au livre (par exemple, la chauve-souris qui vole entre les pages.

Elle comporte également des éléments basés sur des animations CSS.

Une fois que les blocs de contenu étaient dans le livre, il était temps de faire preuve de créativité avec d'autres fonctionnalités. Cela a permis de générer différentes interactions et d'essayer différentes manières de mettre les choses en œuvre.

Préserver la réactivité

Les unités de fenêtre d'affichage responsives dimensionnent le livre et ses caractéristiques. Cependant, garder les polices réactives représentait un défi intéressant. Les unités de requête de conteneur sont idéales ici. Toutefois, ils ne sont pas encore pris en charge partout. La taille du livre est définie, nous n'avons donc pas besoin d'une requête de conteneur. Une unité de requête de conteneur intégrée peut être générée avec CSS calc() et utilisée pour le dimensionnement de la police.


.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 personnes les plus attentives ont peut-être remarqué l'utilisation des éléments <source> lorsqu'ils discutaient précédemment des arrière-plans de la page. Una souhaitait proposer une interaction qui réagisse à la préférence pour le jeu de couleurs. Par conséquent, les arrière-plans sont compatibles avec les modes clair et sombre, avec différentes variantes. Étant donné que vous pouvez utiliser des requêtes média avec l'élément <picture>, il s'agit d'un excellent moyen de fournir deux styles de fond. L'élément <source> interroge les préférences du 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 introduire d'autres modifications en fonction de cette préférence de jeu de couleurs. Les citrouilles de la deuxième page réagissent à la préférence de jeu de couleurs de l'utilisateur. Le SVG utilisé comporte des cercles représentant des flammes, qui s'agrandissent 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é(e) ! Les yeux du portrait suivent votre pointeur à mesure que vous vous déplacez sur la page. L'astuce consiste à mapper l'emplacement du pointeur sur une valeur de conversion, 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 utilise des plages d'entrée et de sortie et mappe les valeurs données. Par exemple, cette utilisation donne la valeur 625.

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

Pour le portrait, la valeur d'entrée est le point central de chaque œil, plus ou moins une distance en pixels. La plage de sortie est la traduction en pixels des yeux. Et puis la position du pointeur sur l'axe x ou y est transmise en tant que valeur. Pour obtenir le point central des yeux lorsque vous les déplacez, ceux-ci sont dupliqués. Les originaux ne bougent pas, sont transparents et sont utilisés à titre de référence.

Il s'agit ensuite de les associer et de mettre à jour les valeurs des propriétés personnalisées CSS des yeux pour que ceux-ci puissent se déplacer. Une fonction est liée à l'événement pointermove par rapport à window. Lors du déclenchement, 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és 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 les utiliser comme bon leur semble. L'avantage ici est d'utiliser CSS clamp() pour que le comportement diffère pour chaque œil, ce qui vous permet de faire en sorte que chaque œil se comporte différemment sans toucher à nouveau 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 sixième page, vous sentez-vous épinglé ? Sur cette page, vous trouverez le design de notre fantastique renard magique. Si vous déplacez le curseur, un effet de traînée de curseur personnalisé peut s'afficher. Cela utilise l'animation du 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 en dessous.

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

Tout comme la façon dont notre portrait écoute un événement pointermove sur window, l'élément <canvas> aussi. 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 des formes utilisées sur le tracé du curseur. Elles ont des coordonnées et une teinte aléatoire.

La fonction mapRange utilisée précédemment est à nouveau utilisée, car elle nous permet de mapper le delta du pointeur sur size et rate. Les objets sont stockés dans un tableau qui est lu en boucle lorsqu'ils sont dessinés sur l'élément <canvas>. Les propriétés de chaque objet indiquent à l'é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 dans le canevas, une boucle est créée avec requestAnimationFrame. Le tracé du curseur ne doit s'afficher que lorsque la page est visible. Nous disposons d'un IntersectionObserver qui met à jour et détermine quelles pages sont affichées. 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 sentier. Chaque cadre réduit la taille et modifie la position de l'objet par le rate. Cela produit cet effet de chute et de mise à l'échelle. Si l'objet est entièrement réduit, 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 n'est plus visible, les écouteurs d'événements sont supprimés et la boucle des frames d'animation est annulée. Le tableau blocks est également effacé.

Voici le tracé du curseur en action !

Examen de l'accessibilité

C'est bien de créer une expérience amusante à explorer, mais ce n'est pas bien si elle n'est pas accessible aux utilisateurs. L'expertise d'Adam dans ce domaine s'est révélée inestimable pour préparer Chrometober à un examen de l'accessibilité avant sa sortie.

Voici quelques points importants abordés:

  • S'assurer que le code HTML utilisé était sémantique Cela incluait des éléments tels que les é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. Anticiper la rédaction du livre a rendu les choses plus accessibles. L'utilisation d'en-têtes et de liens facilite la navigation de l'utilisateur. L'utilisation d'une liste pour les pages signifie également que le nombre de pages est annoncé par la technologie d'assistance.
  • Assurez-vous que toutes les images utilisent les attributs alt appropriés. Pour les SVG intégrés, l'élément title est présent si nécessaire.
  • Utiliser des 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 dans les liens "En savoir plus" permet de communiquer le texte du bloc de contenu. Cela élimine toute ambiguïté sur la destination du lien vers l'utilisateur.
  • Concernant les blocages de contenu, vous pouvez cliquer sur la fiche entière et non seulement sur le lien "En savoir plus".
  • L'utilisation d'un IntersectionObserver pour suivre les pages vues précédemment. Cela présente de nombreux avantages, qui ne sont pas seulement liés aux performances. Les animations ou les interactions seront mises en pause sur les pages qui ne sont pas visibles. 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 personnes voyantes. Le curseur reste sur la page affichée, et les utilisateurs ne peuvent pas accéder à une autre page à l'aide de la touche de tabulation.
  • Enfin et surtout, nous utilisons des requêtes média afin de respecter les préférences de mouvement de l'utilisateur.

Voici une capture d'écran de l'avis qui met en avant certaines des mesures mises en place.

est identifié comme autour de l'ensemble du livre, ce qui indique qu'il doit s'agir du point de repère principal que les utilisateurs de technologies d'assistance peuvent trouver. La capture d'écran montre d'autres éléments." width="800" height="465">

Capture d&#39;écran du livre Chrometober ouvert. Des cadres verts sont fournis autour de différents aspects de l&#39;interface utilisateur, décrivant la fonctionnalité d&#39;accessibilité prévue et les résultats d&#39;expérience utilisateur que la page fournira. Par exemple, les images ont un texte alternatif. Un autre exemple est un libellé d&#39;accessibilité qui déclare que les pages non visibles sont inertes. La capture d&#39;écran montre d&#39;autres informations.

Les enseignements

Chrometober était non seulement motivé par la mise en avant du contenu Web de la communauté, mais aussi par un moyen pour nous de tester le polyfill de l'API d'animations liées au défilement, en cours de développement.

Nous avons prévu une session lors du sommet de notre équipe à New York pour tester le projet et résoudre les problèmes qui se sont posés. La contribution de l'équipe a été inestimable. C'était aussi une excellente occasion de dresser la liste de tout ce qu'il nous fallait aborder avant de pouvoir passer au direct.

Les équipes chargées du CSS, de l&#39;interface utilisateur et des outils de développement sont assises autour d&#39;une table dans une salle de conférence. Una se tient à un tableau blanc recouvert de notes autocollantes. 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 fait de tester le livre sur des appareils a entraîné un problème de rendu. Notre livre ne s'affiche pas comme prévu sur les appareils iOS. Les unités de la fenêtre d'affichage dimensionnent la page, mais lorsqu'une encoche était présente, cela a affecté 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é certains problèmes liés au polyfill de l'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és dans le polyfill. Par exemple, cette demande d'extraction a amélioré les performances en ajoutant la mise en cache à une partie du polyfill.

Capture d&#39;écran d&#39;une démo 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émo ouverte dans Chrome. Les outils pour les développeurs sont ouverts et affichent une mesure des performances améliorée.

Et voilà !

Ce projet était vraiment amusant, ce qui a donné lieu à une expérience de défilement fantaisiste mettant en avant les contenus incroyables de la communauté. De plus, il s'est avéré très utile pour tester le polyfill et pour fournir des commentaires à l'équipe d'ingénierie afin qu'elle contribue à son amélioration.

Chrometober 2022 est terminé.

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

Jhey tenant une feuille d&#39;autocollants contenant les personnages de Chrometober.

Vous pourrez peut-être même récupérer des autocollants fournis par l'un des membres de l'équipe si vous nous assistez à un événement.

Photo principale par David Menidrey sur Unsplash