Cómo se creó el libro desplazable para compartir sugerencias y trucos divertidos y aterradores este Chrometober.
Después de Designcember, quisimos crear Chrometober este año como una forma de destacar y compartir contenido web de la comunidad y el equipo de Chrome. Designcember mostró el uso de Container Queries, pero este año presentaremos la API de animaciones con desplazamiento de CSS.
Descubre la experiencia de desplazamiento de libros en web.dev/chrometober-2022.
Descripción general
El objetivo del proyecto era ofrecer una experiencia fantástica que destaque la API de animaciones vinculadas con el desplazamiento. Pero, aunque era alucinante, la experiencia debía ser responsiva y accesible también. El proyecto también ha sido una excelente manera de probar el polyfill de API que está en desarrollo activo, además de probar diferentes técnicas y herramientas en combinación. ¡Y todo con un tema festivo de Halloween!
La estructura de nuestro equipo se veía de la siguiente manera:
- Tyler Reed: Ilustración y diseño
- Jhey Tompkins: Directora de arquitectura y creatividad
- Una Kravets: Jefa de proyecto
- Bramus Van Damme: Colaborador del sitio
- Adam Argyle: Revisión de accesibilidad
- Aaron Forinton: redacción publicitaria
Cómo crear un borrador de una experiencia de narración a través del desplazamiento
Las ideas para Chrometober comenzaron a surgir en nuestro primer evento fuera de las instalaciones del equipo en mayo de 2022. Una colección de garabatos nos hizo pensar en formas en las que un usuario podría desplazarse por algún tipo de guion gráfico. Inspirados en los videojuegos, consideramos una experiencia de desplazamiento a través de escenas como cementerios y una casa embrujada.
Fue emocionante tener la libertad creativa para llevar mi primer proyecto de Google en una dirección inesperada. Este fue un prototipo inicial de cómo un usuario podría navegar por el contenido.
A medida que el usuario se desplaza lateralmente, los bloques rotan y se acercan. Sin embargo, decidí alejarme de esta idea por la preocupación de cómo podríamos hacer que esta experiencia fuera excelente para los usuarios en dispositivos de todos los tamaños. En cambio, me incliné por el diseño de algo que había hecho en el pasado. En 2020, tuve la suerte de tener acceso a GreenSock's ScrollTrigger para compilar demostraciones de lanzamientos.
Una de las demostraciones que creé fue un libro en 3D con CSS en el que las páginas se giraban a medida que te desplazabas, y esto parecía mucho más adecuado para lo que queríamos en Chrometober. La API de animaciones vinculadas con desplazamiento es un reemplazo perfecto para esa funcionalidad. También funciona bien con scroll-snap
, como verás.
El ilustrador del proyecto, Tyler Reed, fue muy bueno para alterar el diseño a medida que cambiaba las ideas. Tyler hizo un gran trabajo al tomar todas las ideas creativas que se le presentaron y hacerlas realidad. Fue muy divertido intercambiar ideas. Una gran parte de cómo queríamos que esto funcionara era tener componentes divididos en bloques aislados. De esa manera, podríamos componerlos en escenas y, luego, elegir lo que creamos.
La idea principal era que, a medida que el usuario avanzaba en el libro, podía acceder a bloques de contenido. También podían interactuar con toques de extravagancia, incluidos los huevos de Pascua que incorporamos a la experiencia, por ejemplo, un retrato en una casa embrujada cuyos ojos seguían el puntero o animaciones sutiles activadas por consultas de contenido multimedia. Estas ideas y funciones se animarían al desplazarse. Una de las primeras ideas fue un conejo zombi que se elevaba y se traducía a lo largo del eje x cuando el usuario se desplazaba.
Familiarízate con la API
Antes de empezar a jugar con funciones individuales y huevos de Pascua, necesitábamos un libro. Por lo tanto, decidimos convertir esto en una oportunidad para probar el conjunto de funciones de la API emergente de animaciones vinculadas al desplazamiento de CSS. Por el momento, la API de animaciones vinculadas al desplazamiento no es compatible con ningún navegador. Sin embargo, mientras desarrollaban la API, los ingenieros del equipo de interacciones trabajaron en un polyfill. Esto proporciona una manera de probar la forma de la API a medida que se desarrolla. Eso significa que podríamos usar esta API hoy mismo, y los proyectos divertidos como este suelen ser un excelente lugar para probar funciones experimentales y enviar comentarios. Descubre qué aprendimos y los comentarios que pudimos proporcionar más adelante en el artículo.
En un nivel alto, puedes usar esta API para vincular animaciones para desplazarte. Es importante tener en cuenta que no puedes activar una animación durante el desplazamiento. Esto es algo que podrías hacer más adelante. Las animaciones vinculadas al desplazamiento también se dividen en dos categorías principales:
- Son aquellas que reaccionan a la posición de desplazamiento.
- Son aquellas que reaccionan a la posición de un elemento en su contenedor de desplazamiento.
Para crear el último, usamos un ViewTimeline
aplicado a través de una propiedad animation-timeline
.
Este es un ejemplo de cómo se ve el uso 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;
}
}
Creamos una ViewTimeline
con view-timeline-name
y definimos el eje para ella. En este ejemplo, block
hace referencia a un block
lógico. La animación se vincula al desplazamiento con la propiedad animation-timeline
. animation-delay
y animation-end-delay
(al momento de la redacción) son las formas en que definimos las fases.
Estas fases definen los puntos en los que la animación debe vincularse en relación con la posición de un elemento en su contenedor de desplazamiento. En nuestro ejemplo, indicamos que se debe iniciar la animación cuando el elemento ingresa (enter 0%
) al contenedor de desplazamiento. Y termina cuando cubre el 50% (cover 50%
) del contenedor de desplazamiento.
Esta es nuestra demostración en acción:
También puedes vincular una animación al elemento que se mueve en la ventana de visualización. Para ello, configura animation-timeline
como el view-timeline
del elemento. Esto es bueno para situaciones como las animaciones de listas. El comportamiento es similar a la forma en que puedes animar elementos al ingresar con 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;
}
}
Con esto, "Mover" escala verticalmente a medida que ingresa al viewport, lo que activa la rotación de "ícono giratorio".
Lo que descubrí durante la experimentación fue que la API funciona muy bien con scroll-snap. La función de desplazamiento con ajuste combinado con ViewTimeline
sería ideal para ajustar los giros de página en un libro.
Prototipar la mecánica
Después de experimentar un poco, pude hacer que funcionara un prototipo de libro. Para pasar las páginas del libro, desplázate horizontalmente.
En la demostración, puedes ver los diferentes activadores destacados con bordes discontinuos.
El código de marcado se ve de la siguiente manera:
<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>
A medida que te desplazas, las páginas del libro giran, pero se abren o cierran. Esto depende de la alineación del ajuste de desplazamiento de los activadores.
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;
}
Esta vez, no conectamos ViewTimeline
en CSS, pero usamos la API de Web Animations en JavaScript. Esto tiene el beneficio adicional de poder iterar sobre un conjunto de elementos y generar el ViewTimeline
que necesitamos, en lugar de crearlos cada uno de forma manual.
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);
Para cada activador, generamos un ViewTimeline
. Luego, animamos la página asociada del activador con ese ViewTimeline
. Eso vincula la animación de la página para desplazarse. Para nuestra animación, rotaremos un elemento de la página en el eje Y para hacerla girar. También traducimos la página en el eje Z para que se comporte como un libro.
Revisión general
Una vez que haya descubierto el mecanismo del libro, pude concentrarme en dar vida a las ilustraciones de Tyler.
Astrofotografía
El equipo usó Astro para Designcember en 2021 y me encantó volver a usarlo para Chrometober. La experiencia del desarrollador de poder dividir las tareas en componentes es adecuada para este proyecto.
El libro en sí es un componente. También es una colección de componentes de página. Cada página tiene dos lados y fondos. Los elementos secundarios de un lado de la página son componentes que se pueden agregar, quitar y posicionar con facilidad.
Cómo crear un libro
Para mí, era importante que los bloques fueran fáciles de administrar. También quería facilitar que el resto del equipo pudiera hacer contribuciones.
Las páginas de alto nivel se definen mediante un array de configuración. Cada objeto de página del array define el contenido, el fondo y otros metadatos de una página.
const pages = [
{
front: {
marked: true,
content: PageTwo,
backdrop: spreadOne,
darkBackdrop: spreadOneDark
},
back: {
content: PageThree,
backdrop: spreadTwo,
darkBackdrop: spreadTwoDark
},
aria: `page 1`
},
/* Obfuscated page objects */
]
Estos se pasan al componente Book
.
<Book pages={pages} />
El componente Book
es donde se aplica el mecanismo de desplazamiento y se crean las páginas del libro. Se usa el mismo mecanismo del prototipo, pero compartimos varias instancias de ViewTimeline
que se crean de forma global.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
De esta manera, podemos compartir los cronogramas para que se usen en otro lugar en lugar de volver a crearlos. Explicaré eso después.
Composición de la página
Cada página es un elemento de lista dentro de una lista:
<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>
Y la configuración definida se pasa a cada instancia de Page
. Las páginas usan la función de espacio de Astro para insertar contenido en cada una.
<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>
Este código se usa principalmente para configurar la estructura. Los colaboradores pueden trabajar en el contenido del libro en su mayor parte sin tener que tocar este código.
Fondos
El cambio creativo hacia un libro facilitó mucho la división de las secciones, y cada página del libro es una escena tomada del diseño original.
Como decidimos una relación de aspecto para el libro, el fondo de cada página podría tener un elemento de imagen. Configurar ese elemento en un 200% de ancho y usar object-position
en función del lado de la página funciona.
.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;
}
Contenido de la página
Veamos cómo crear una de las páginas. En la página tres, aparece un búho que aparece en un árbol.
Se completa con un componente PageThree
, como se define en la configuración. Es un componente Astro (PageThree.astro
). Estos componentes parecen archivos HTML, pero tienen una valla de código en la parte superior, similar a frontmatter. Esto nos permite realizar acciones como importar otros componentes. El componente de la página tres se ve así:
---
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>
Una vez más, las páginas son atómicas. Se compilan a partir de una colección de componentes. La página tres muestra un bloque de contenido y el búho interactivo, por lo que hay un componente para cada uno.
Los bloques de contenido son los vínculos al contenido que se ve en el libro. Estos también se basan en un objeto de configuración.
{
"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
]
}
Esta configuración se importa cuando se requieren bloqueos de contenido. Luego, la configuración de bloque relevante se pasa al componente ContentBlock
.
<ContentBlock {...contentBlocks[3]} id="four" />
También hay un ejemplo de cómo usamos el componente de la página como lugar para posicionar el contenido. Aquí, se posiciona un bloque de contenido.
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
Sin embargo, los estilos generales de un bloque de contenido se encuentran junto con el código del componente.
.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%;
}
En cuanto a nuestro búho, es una función interactiva, una de las muchas de este proyecto. Este es un pequeño ejemplo que muestra cómo usamos el ViewTimeline compartido que creamos.
En un nivel alto, el componente owl importa algunas imágenes SVG y las alinea con el fragmento de Astro.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Y los estilos para posicionar nuestro búho se encuentran junto con el código del componente.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Hay un elemento de diseño adicional que define el comportamiento de transform
para el búho.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
El uso de transform-box
afecta a transform-origin
. Lo hace relativo al cuadro de límite del objeto dentro del SVG. El búho se escala desde el centro inferior, de ahí el uso de transform-origin: 50% 100%
.
La parte divertida es cuando vinculamos el búho a uno de nuestros ViewTimeline
generados:
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()
En este bloque de código, hacemos dos cosas:
- Verifica las preferencias de movimiento del usuario.
- Si no tiene ninguna preferencia, vincula una animación del búho para desplazarte.
Para la segunda parte, el búho anima en el eje y usando la API de Web Animations. Se usa la propiedad transform individual translate
, que está vinculada a un ViewTimeline
. Está vinculado a CHROMETOBER_TIMELINES[1]
a través de la propiedad timeline
. Este es un ViewTimeline
que se genera para los giros de página. Esto vincula la animación del búho al giro de página con la fase enter
. Define que, cuando la página esté girada en un 80%, se debe comenzar a mover la lechuza. Cuando llegue al 90%, el búho debería terminar su traducción.
Funciones del libro
Ahora ya conoces el enfoque para crear una página y cómo funciona la arquitectura del proyecto. Verás cómo permite que los colaboradores participen y trabajen en una página o función que elijan. Varias características del libro tienen sus animaciones vinculadas al giro de las páginas, por ejemplo, el murciélago que entra y sale cuando se pasa la página.
También tiene elementos potenciados por animaciones de CSS.
Una vez que los bloques de contenido estaban en el libro, hubo tiempo de poner a prueba tu creatividad con otras funciones. Esto brindó la oportunidad de generar interacciones diferentes y probar diferentes formas de implementarlas.
Mantén la capacidad de respuesta
Las unidades de vista del puerto adaptables ajustan el tamaño del libro y sus funciones. Sin embargo, mantener la capacidad de respuesta de las fuentes fue un desafío interesante. Las unidades de consulta de contenedores son una buena opción aquí. Sin embargo, aún no se admiten en todas partes. El tamaño del libro está establecido, por lo que no necesitamos una consulta de contenedor. Se puede generar una unidad de consulta de contenedor intercalada con calc()
de CSS y usarse para el tamaño de la fuente.
.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);
}
Las calabazas brillan por la noche
Los más observadores pueden haber notado el uso de elementos <source>
cuando hablamos de los fondos de página antes. Una deseaba tener una interacción que reaccionara a la preferencia de esquema de colores. Como resultado, los fondos admiten los modos claro y oscuro con diferentes variantes. Dado que puedes usar consultas de medios con el elemento <picture>
, es una excelente manera de proporcionar dos estilos de fondo. El elemento <source>
consulta la preferencia de esquema de colores y muestra el fondo adecuado.
<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>
Puedes realizar otros cambios en función de esa preferencia de esquema de colores. Las calabazas de la página dos reaccionan a la preferencia de esquema de colores de un usuario. El SVG que se usa tiene círculos que representan llamas, que se agrandan y se animan en modo oscuro.
.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;
}
}
}
¿Te está mirando este retrato?
Si revisas la página 10, es posible que notes algo. Te están mirando Los ojos del retrato seguirán tu puntero a medida que te muevas por la página. El truco aquí es asignar la ubicación del puntero a un valor de traducción y pasarlo a través de 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)
}
Este código toma rangos de entrada y salida, y asigna los valores dados. Por ejemplo, este uso daría el valor 625.
mapRange(0, 100, 250, 1000, 50) // 625
Para el retrato, el valor de entrada es el punto central de cada ojo, más o menos una distancia en píxeles. El rango de salida es la cantidad que los ojos pueden traducir en píxeles. Y, luego, la posición del puntero en el eje x o y se pasa como valor. Para obtener el punto central de los ojos mientras los mueves, estos ojos están duplicados. Los originales no se mueven, son transparentes y se usan como referencia.
Se trata de unirlos y actualizar los valores de las propiedades personalizadas de CSS de los ojos para que estos puedan moverse. Una función está vinculada al evento pointermove
en función de window
. A medida que se activa, se usan los límites de cada ojo para calcular los puntos centrales. Luego, la posición del puntero se asigna a valores que se establecen como valores de propiedad personalizada de los ojos.
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)
})
}
Una vez que los valores se pasan a CSS, los estilos pueden hacer lo que quieran con ellos. Lo mejor aquí es usar CSS clamp()
para que el comportamiento sea diferente para cada ojo, de modo que puedas hacer que cada ojo se comporte de manera diferente sin volver a tocar el código 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);
}
Cómo lanzar hechizos
Si miras la página seis, ¿te sientes fascinado? En esta página, se muestra el diseño de nuestro fantástico zorro mágico. Si mueves el puntero, es posible que veas un efecto de rastro del cursor personalizado. Esta función usa la animación de lienzo. Un elemento <canvas>
se ubica por encima del resto del contenido de la página con pointer-events: none
. Esto significa que los usuarios aún pueden hacer clic en los bloques de contenido que aparecen debajo.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Al igual que nuestro retrato escucha un evento pointermove
en window
, nuestro elemento <canvas>
también lo hace. Sin embargo, cada vez que se activa el evento, creamos un objeto para animar en el elemento <canvas>
. Estos objetos representan formas utilizadas en el recorrido del cursor. Tienen coordenadas y un tono aleatorio.
Se vuelve a usar nuestra función mapRange
anterior, ya que podemos usarla para asignar el delta del puntero a size
y rate
. Los objetos se almacenan en un array que se repite cuando los objetos se dibujan en el elemento <canvas>
. Las propiedades de cada objeto le indican a nuestro elemento <canvas>
dónde se deben dibujar.
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)
Para dibujar en el lienzo, se crea un bucle con requestAnimationFrame
. El rastro del cursor solo debe renderizarse cuando la página está en la vista. Tenemos un elemento IntersectionObserver
que se actualiza y determina qué páginas están a la vista. Si una página está en vista, los objetos se renderizan como círculos en el lienzo.
Luego, recorremos el array blocks
y dibujamos cada parte de la ruta. Cada fotograma reduce el tamaño y altera la posición del objeto en función de rate
. Esto produce ese efecto de caída y escalamiento. Si el objeto se reduce por completo, se quita del array 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 página desaparece de la vista, se quitan los objetos de escucha de eventos y se cancela el bucle de fotogramas de animación. También se borra el array blocks
.
Aquí tienes el rastro del cursor en acción.
Revisión de accesibilidad
Está bien crear una experiencia divertida para explorar, pero no es bueno si los usuarios no pueden acceder a ella. La experiencia de Adam en este área fue invaluable para preparar Chrometober para una revisión de accesibilidad antes del lanzamiento.
Estas son algunas de las áreas destacadas que se abordan:
- Asegúrate de que el HTML que usaste sea semántico. Esto incluía elementos de punto de referencia adecuados, como
<main>
para el libro, el uso del elemento<article>
para cada bloque de contenido y los elementos<abbr>
en los que se introducen acrónimos. Pensar en el futuro mientras se creaba el libro hizo que todo fuera más accesible. El uso de encabezados y vínculos facilita la navegación del usuario. El uso de una lista para las páginas también significa que la tecnología de accesibilidad anuncia la cantidad de páginas. - Asegúrate de que todas las imágenes usen los atributos
alt
adecuados. En el caso de los SVG intercalados, el elementotitle
está presente cuando es necesario. - Usar atributos
aria
cuando mejoren la experiencia El uso dearia-label
para las páginas y sus lados le comunica al usuario en qué página se encuentra. El uso dearia-describedBy
en los vínculos "Más información" comunica el texto del bloque de contenido. De esta manera, se elimina la ambigüedad sobre adónde llevará el vínculo al usuario. - En cuanto a los bloqueos de contenido, está disponible la opción de hacer clic en toda la tarjeta y no solo en el vínculo "Leer más".
- Anteriormente, se mencionó el uso de un
IntersectionObserver
para hacer un seguimiento de las páginas que están en vista. Esto tiene muchos beneficios que no solo se relacionan con el rendimiento. Las páginas que no estén en la vista tendrán pausadas las animaciones o interacciones. Sin embargo, estas páginas también tienen aplicado el atributoinert
. Esto significa que los usuarios que usan un lector de pantalla pueden explorar el mismo contenido que los usuarios videntes. El enfoque permanece dentro de la página que está en la vista y los usuarios no pueden cambiar a otra página. - Por último, pero no menos importante, usamos consultas de medios para respetar la preferencia de un usuario por el movimiento.
Esta es una captura de pantalla de la opinión en la que se destacan algunas de las medidas implementadas.
se identifica como alrededor de todo el libro, lo que indica que debe ser el punto de referencia principal que encuentren los usuarios de tecnología de asistencia. Hay más información en la captura de pantalla." width="800" height="465">
Qué aprendimos
El objetivo de Chrometober no solo era destacar el contenido web de la comunidad, sino también probar el polyfill de la API de animaciones vinculadas al desplazamiento que está en desarrollo.
Reservamos una sesión durante nuestra cumbre de equipos en Nueva York para probar el proyecto y abordar los problemas que surgieron. La contribución del equipo fue invaluable. También fue una gran oportunidad para enumerar todo lo que debíamos abordar antes de que pudiéramos lanzarlo.
Por ejemplo, probar el libro en dispositivos generó un problema de renderización. Nuestro libro no se procesaría como se esperaba en los dispositivos iOS. Las unidades de viewport afectan el tamaño de la página, pero cuando hay un recorte, el libro se ve afectado. La solución fue usar viewport-fit=cover
en el viewport meta
:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Esta sesión también planteó algunos problemas con el polyfill de la API. Bramus planteó estos problemas en el repositorio de polyfill. Luego, encontró soluciones para esos problemas y los fusionó en el polyfill. Por ejemplo, esta solicitud de extracción mejoró el rendimiento cuando agregó almacenamiento en caché a parte del polyfill.
Eso es todo.
Fue un proyecto muy divertido en el que trabajar, lo que generó una experiencia de desplazamiento fantástica que destaca el contenido increíble de la comunidad. Además, fue excelente para probar el polyfill y proporcionar comentarios al equipo de ingeniería para mejorarlo.
Chrometober 2022 llegó a su fin.
¡Esperamos que lo hayas disfrutado! ¿Cuál es tu función favorita? Envíanos un tweet y comunícate con nosotros.
Incluso es posible que puedas obtener algunas calcomanías de uno de los miembros del equipo si nos ves en un evento.
Foto hero de David Menidrey en Unsplash