Découvrez comment le livre à défilement a vu le jour pour partager des conseils et astuces amusants et effrayants pour ce Chrometober.
Après Designcember, nous avons souhaité créer Chrometober cette année pour mettre en avant et partager les contenus Web de la communauté et de l'équipe Chrome. Designcember a mis en avant l'utilisation des requêtes de conteneur, mais cette année, nous mettons en avant l'API CSS pour les animations 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 lié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. Et le tout sur un thème festif d'Halloween !
Notre structure d'équipe se présentait comme suit:
- Tyler Reed: illustration et conception
- Jhey Tompkins: responsable de l'architecture et de la création
- Una Kravets: chef de projet
- Bramus Van Damme: contributeur au site
- Adam Argyle: examen de l'accessibilité
- Aaron Forinton: rédaction publicitaire
Ébaucher une expérience de scrollytelling
Les idées pour Chrometober ont commencé à germer lors de notre premier événement hors site en mai 2022. Une collection de gribouillages nous a amenés à réfléchir aux façons dont un utilisateur pourrait faire défiler un storyboard. Inspirés par les jeux vidéo, nous avons envisagé une expérience de défilement à travers des scènes telles que des cimetières et une maison hantée.
C'était excitant de pouvoir 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 au 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'animation liée au défilement est un remplacement parfait pour cette fonctionnalité. Il fonctionne également bien avec scroll-snap
, comme vous le verrez !
Notre illustrateur pour le projet, Tyler Reed, a su modifier la conception au fur et à mesure que nous changions d'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.
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 lors du défilement. 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 la page.
Se familiariser avec l'API
Avant de pouvoir commencer à jouer avec les fonctionnalités et les œufs de Pâques individuels, nous avions besoin d'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 dès aujourd'hui. De tels projets amusants sont souvent un excellent moyen d'essayer des fonctionnalités expérimentales et de nous faire part de vos commentaires. Découvrez ce que nous avons appris et les commentaires que nous avons pu fournir dans la suite de 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:
- Ceux qui réagissent à la position de défilement.
- 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 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 le viewport. 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 essais, j'ai réussi à 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 comporte deux côtés et des arrière-plans. 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 rendre les blocs faciles à gérer. Je voulais également que le reste de l'équipe puisse facilement contribuer.
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 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 à 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.
Comme nous avons choisi un format pour le livre, le fond de chaque page peut comporter un élément image. Définir cet élément sur une largeur de 200% et utiliser object-position
en fonction du côté de la page suffit.
.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 ils comportent une clôture de code en haut, semblable à la partie "avant-propos". 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 sont les liens vers le contenu du 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 ici 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 sont situés avec 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:
- Vérifiez les préférences de mouvement de l'utilisateur.
- 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 Y à l'aide de l'API Web Animations. La propriété de transformation individuelle translate
est utilisée et est associée à un ViewTimeline
. Il est associé à CHROMETOBER_TIMELINES[1]
via la propriété timeline
. Il s'agit d'un ViewTimeline
généré pour les changements 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 des livres
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 insérés dans le livre, il a eu le temps de laisser libre cours à sa créativité avec d'autres fonctionnalités. Cela nous a permis de générer différentes interactions et d'essayer différentes façons de les implémenter.
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 adaptées à cet usage. 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);
}
Les citrouilles 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. 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 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 mettent à l'échelle 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 quantité que les yeux peuvent traduire en pixels. 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.
Il vous suffit ensuite de les associer et de mettre à jour les valeurs de la propriété personnalisée CSS des yeux pour 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 comme 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 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 comportent 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 à l'écran, les objets sont affichés sous forme de cercles sur le canevas.
Nous effectuons ensuite une boucle sur le tableau blocks
et dessinons chaque partie du parcours. Chaque frame réduit la taille et modifie la position de l'objet par le rate
. Cela produit l'effet de chute 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 créer le 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émenttitle
est présent si nécessaire. - Utiliser des attributs
aria
là où ils améliorent l'expérience L'utilisation dearia-label
pour les pages et leurs côtés indique à l'utilisateur sur quelle page il se trouve. L'utilisation dearia-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, vous pouvez cliquer sur l'ensemble de la fiche et non seulement sur le lien "Lire la suite".
- Nous avons déjà vu plus tôt comment 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 et les interactions des pages qui ne sont pas à l'écran sont mises en veille. Toutefois, l'attributinert
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 focus reste sur la page affichée et les utilisateurs ne peuvent pas passer à une autre page. - 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 entourant l'ensemble du livre, ce qui indique qu'il doit être le repère principal que les utilisateurs de technologies d'assistance doivent pouvoir trouver. Plus d'informations sont disponibles dans la capture d'écran." width="800" height="465">
Les enseignements
L'objectif de Chrometober n'était pas seulement de mettre en avant le contenu Web de la communauté, mais aussi de nous permettre de tester le polyfill de l'API d'animations liées au défilement 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.
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 définissent la taille de la page, mais la présence d'une encoche avait un impact sur 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 de tirage a permis d'améliorer les performances en ajoutant la mise en cache à une partie du polyfill.
Et voilà !
Ce projet a été très amusant à réaliser. Il a abouti à une expérience de défilement fantaisiste qui met en avant les 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 ? Envoyez-moi un tweet pour nous en faire part.
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