Criando o Chrometober!

Como o livro de rolagem ganhou vida por compartilhar dicas e truques divertidos e assustadores neste Chrometober.

Depois da Designcember, queremos criar o Chrometober para você este ano como uma forma de destacar e compartilhar conteúdo da Web da comunidade e da equipe do Chrome. A Designcember mostrou o uso de consultas de contêiner, mas este ano vamos apresentar a API de animações de rolagem CSS vinculada.

Confira a experiência de rolagem em web.dev/chrometober-2022 (em inglês).

Visão geral

O objetivo do projeto era proporcionar uma experiência extravagante destacando a API de animações de rolagem. Mas, embora fosse extravagante, a experiência também precisava ser responsiva e acessível. O projeto também tem sido uma ótima maneira de testar o polyfill da API, que está em desenvolvimento ativo, além de testar diferentes técnicas e ferramentas combinadas. E tudo isso com um tema festivo de Halloween!

A estrutura da nossa equipe era assim:

Como criar uma experiência de rolagem

As ideias para o Chrometober começaram a fluir na nossa primeira equipe externa em maio de 2022. Uma coleção de rabiscos nos fez pensar em maneiras pelas quais um usuário poderia percorrer algum tipo de storyboard. Inspirados nos videogames, consideramos uma experiência de rolagem por cenas como cemitérios e uma casa assombrada.

Um caderno está em uma mesa com vários doodles e rabiscos relacionados ao projeto.

Foi muito empolgante ter a liberdade criativa para levar meu primeiro projeto do Google a uma direção inesperada. Esse foi um protótipo inicial de como um usuário poderia navegar pelo conteúdo.

Conforme o usuário rola a tela para os lados, os blocos são girados e ampliados. Mas decidi abandonar essa ideia por preocupação com como poderíamos tornar essa experiência excelente para usuários em dispositivos de todos os tamanhos. Em vez disso, me inclinei para o design de algo que eu tinha feito no passado. Em 2020, tive a sorte de ter acesso ao ScrollTrigger do GreenSock para criar demonstrações de lançamento.

Uma das demonstrações que eu criei foi um livro 3D-CSS no qual as páginas giravam à medida que você rolava, e isso pareceu muito mais apropriado para o que queríamos para o Chrometober. A API de animações com link de rolagem é uma troca perfeita para essa funcionalidade. Como você verá, ela também funciona bem com scroll-snap.

Nosso ilustrador do projeto, Tyler Reed, foi ótimo em alterar o design à medida que mudamos ideias. Tiago fez um ótimo trabalho ao transformar todas as ideias criativas que foram lançadas nele e dar vida a elas. Foi muito divertido discutir ideias juntos. Uma grande parte de como queríamos que isso funcionasse era ter recursos divididos em blocos isolados. Assim, poderíamos compor tudo em cenas e depois escolher o que colocamos em prática.

Uma das cenas da composição mostra uma cobra, um caixão com os braços saindo, uma raposa com a varinha em um caldeirão, uma árvore com rosto assustador e uma gárgula segurando uma lanterna de abóbora.

A ideia principal era que, à medida que o usuário navegasse pelo livro, ele pudesse acessar blocos de conteúdo. Eles também podiam interagir com traços de fantasia, incluindo os easter eggs que incorporamos à experiência, por exemplo, um retrato em uma casa mal-assombrada, cujos olhos seguiram seu ponteiro, ou animações sutis acionadas por consultas de mídia. Essas ideias e recursos ficam animados na rolagem. Uma das primeiras ideias era um coelho zumbi que se ergueria e se deslocaria ao longo do eixo x na rolagem do usuário.

Como se familiarizar com a API

Antes de começar a testar cada recurso e easter eggs, precisávamos de um livro. Por isso, decidimos transformar isso em uma chance de testar o conjunto de recursos para a emergente API CSS de animações vinculadas à rolagem. No momento, a API de animações de vínculo de rolagem não é compatível com nenhum navegador. No entanto, durante o desenvolvimento da API, os engenheiros da equipe de interações trabalharam em um polyfill. Isso oferece uma maneira de testar o formato da API à medida que ela é desenvolvida. Isso significa que poderíamos usar essa API hoje, e projetos divertidos como esse costumam ser um ótimo lugar para testar recursos experimentais e enviar feedback. Confira o que aprendemos e o feedback que fizemos neste artigo.

De modo geral, você pode usar essa API para vincular animações à rolagem. É importante observar que não é possível acionar uma animação ao rolar a tela. Isso pode acontecer depois. As animações com links de rolagem também se enquadram em duas categorias principais:

  1. Aqueles que reagem à posição de rolagem.
  2. Aquelas que reagem à posição de um elemento no contêiner de rolagem.

Para criar o segundo plano, usamos um ViewTimeline aplicado por uma propriedade animation-timeline.

Confira um exemplo de como usar ViewTimeline no 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;
 }
}

Criamos uma ViewTimeline com view-timeline-name e definimos o eixo para ela. Neste exemplo, block refere-se a block lógico. A animação é vinculada à rolagem com a propriedade animation-timeline. No momento em que este artigo foi escrito, animation-delay e animation-end-delay são como definimos as fases.

Essas fases definem os pontos em que a animação precisa ser vinculada em relação à posição de um elemento no contêiner de rolagem. No exemplo, dizemos que a animação será iniciada quando o elemento entrar (enter 0%) no contêiner de rolagem. Termine quando ele cobrir 50% (cover 50%) do contêiner de rolagem.

Veja nossa demonstração em ação:

Também é possível vincular uma animação ao elemento que está se movendo na janela de visualização. Para fazer isso, defina animation-timeline como o view-timeline do elemento. Isso é bom para cenários como animações de lista. O comportamento é semelhante à forma como você anima elementos na entrada usando 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;
  }
}

Com isso, o "Mover" é redimensionado ao entrar na janela de visualização, acionando a rotação do "Controle giratório".

O que descobri durante os testes é que a API funciona muito bem com o scroll-snap. O ajuste de rolagem combinado com o ViewTimeline é ideal para ajustar a virada de página em um livro.

Como prototipar a mecânica

Depois de alguns testes, consegui fazer um protótipo de livro funcionar. Role a tela horizontalmente para virar as páginas do livro.

Na demonstração, os diferentes acionadores são destacados com bordas tracejadas.

A marcação é semelhante a esta:

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

Conforme você rola a tela, as páginas do livro são giradas, mas abertas ou fechadas. Isso depende do alinhamento de rolagem dos gatilhos.

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

Desta vez, não conectamos ViewTimeline no CSS, mas usamos a API Web Animations em JavaScript. Isso tem a vantagem adicional de poder repetir um conjunto de elementos e gerar as ViewTimeline necessárias, em vez de criá-las manualmente.

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 gatilho, geramos um ViewTimeline. Em seguida, animamos a página associada ao gatilho usando esse ViewTimeline. Isso vincula a animação da página à rolagem. Para nossa animação, estamos girando um elemento da página no eixo y para virar a página. Também traduzimos a própria página no eixo z para que se comporte como um livro.

Para resumir

Depois de criar o mecanismo do livro, eu poderia me concentrar em dar vida às ilustrações de Tyler.

Astro

A equipe usou o Astro para a Designcember em 2021, e eu queria voltar a usá-lo no Chrometober. A experiência do desenvolvedor de poder dividir itens em componentes é adequada para este projeto.

O livro em si é um componente. Ele também é um conjunto de componentes de página. Cada página tem dois lados e cenários. Os filhos de um lado da página são componentes que podem ser adicionados, removidos e posicionados com facilidade.

Como criar um livro

Era importante para mim tornar os blocos fáceis de gerenciar. Também queria facilitar a contribuição do restante da equipe.

As páginas em alto nível são definidas por uma matriz de configuração. Cada objeto de página na matriz define o conteúdo, o pano de fundo e outros metadados de uma 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 */
]

Eles são transmitidos ao componente Book.

<Book pages={pages} />

O componente Book é onde o mecanismo de rolagem é aplicado e as páginas do livro são criadas. O mesmo mecanismo do protótipo é usado, mas compartilhamos várias instâncias de ViewTimeline que são criadas globalmente.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

Dessa forma, podemos compartilhar as linhas do tempo para serem usadas em outros lugares, em vez de recriá-las. Falaremos sobre isso mais adiante.

Composição da página

Cada página é um item dentro de uma 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>

A configuração definida é transmitida para cada instância do Page. As páginas usam o recurso de slot do Astro para inserir conteúdo em cada página.

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

Esse código serve principalmente para configurar a estrutura. Os colaboradores podem trabalhar no conteúdo do livro na maior parte do tempo sem ter que mexer nesse código.

Cenários

A mudança criativa para um livro facilitou muito a divisão das seções, e cada propagação do livro é uma cena tirada do design original.

Ilustração de página do livro que mostra uma macieira em um cemitério. O cemitério tem várias lápides, e há um morcego no céu em frente a uma lua gigante.

Como decidimos sobre uma proporção para o livro, o pano de fundo de cada página poderia ter um elemento de imagem. Definir esse elemento como 200% de largura e usar object-position com base no lado da 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;
}

Conteúdo da página

Vamos ver como criar uma das páginas. A página três mostra uma coruja que aparece em uma árvore.

Ela é preenchida com um componente PageThree, conforme definido na configuração. É um componente Asstro (PageThree.astro). Esses componentes parecem arquivos HTML, mas têm um limite de código na parte de cima semelhante a um frontmat. Isso nos permite realizar ações como importar outros componentes. O componente da página três tem a seguinte aparência:

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

Novamente, as páginas são de natureza atômica. Eles são criados com base em uma coleção de recursos. A página três apresenta um bloco de conteúdo e a coruja interativa, então há um componente para cada um.

Os blocos de conteúdo são links para o conteúdo do livro. Eles também são conduzidos por um objeto de configuração.

{
 "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
  ]
}

Essa configuração é importada onde os blocos de conteúdo são necessários. Em seguida, a configuração de bloco relevante é transmitida para o componente ContentBlock.

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

Há também um exemplo de como usamos o componente da página para posicionar o conteúdo. Aqui, um bloco de conteúdo é posicionado.

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

No entanto, os estilos gerais para um bloco de conteúdo são colocalizados com o código do 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%;
}

Já a coruja é um recurso interativo, um dos muitos neste projeto. Esse é um pequeno exemplo interessante que mostra como usamos a ViewTimeline compartilhada que criamos.

Em um nível alto, o componente coruja importa alguns arquivos SVG e os inclui inline usando o fragmento do Astro.

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

Os estilos para posicionar a coruja são colocalizados com o código do componente.

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

Há uma outra parte de estilo que define o comportamento transform para a coruja.

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

O uso de transform-box afeta o transform-origin. Ele o relaciona à caixa delimitadora do objeto no SVG. A coruja é dimensionada a partir do centro de baixo, portanto, o uso de transform-origin: 50% 100%.

A parte divertida é quando vinculamos a coruja a um dos ViewTimelines gerados:

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

Nesse bloco de código, fazemos duas coisas:

  1. Verificar as preferências de movimento do usuário.
  2. Se eles não tiverem preferência, vincule uma animação da coruja para rolar.

Na segunda parte, a coruja é animada no eixo y usando a API Web Animations. A propriedade de transformação individual translate é usada e está vinculada a um ViewTimeline. Ele está vinculado ao CHROMETOBER_TIMELINES[1] pela propriedade timeline. Este é um ViewTimeline que é gerado para as viradas de página. Isso vincula a animação da coruja à virada de página usando a fase enter. Ele define que, quando a página estiver 80% virada, comece a mover a coruja. Em 90%, a coruja deve terminar sua tradução.

Recursos do livro

Agora você viu a abordagem para criar uma página e como funciona a arquitetura do projeto. Você pode conferir como ele permite que os colaboradores participem e trabalhem em uma página ou recurso de sua escolha. Vários recursos do livro têm suas animações vinculadas à virada de página do livro, por exemplo, o morcego que voa para dentro e para fora ao virar as páginas.

Ele também tem elementos com tecnologia de animações CSS.

Depois que os blocos de conteúdo já estavam no livro, deu tempo para usar a criatividade com outros recursos. Isso deu a oportunidade de gerar algumas interações diferentes e tentar maneiras diferentes de implementar coisas.

Como manter a capacidade de resposta

Os blocos responsivos da janela de visualização dimensionam o livro e os recursos dele. No entanto, manter as fontes responsivas foi um desafio interessante. As unidades de consulta do contêiner são adequadas para esse caso. No entanto, eles ainda não são compatíveis com todos os lugares. O tamanho do livro está definido. Por isso, não precisamos de uma consulta de contêiner. Uma unidade de consulta de contêiner inline pode ser gerada com CSS calc() e usada para dimensionamento de fontes.


.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);
}

Abóboras brilhando à noite

Pessoas com um olhar atento podem ter notado o uso de elementos <source> ao discutir os panos de fundo das páginas anteriormente. Una queria ter uma interação que reagisse à preferência do esquema de cores. Como resultado, os panos de fundo são compatíveis com os modos claro e escuro com variantes diferentes. Como é possível usar consultas de mídia com o elemento <picture>, essa é uma ótima maneira de fornecer dois estilos de pano de fundo. O elemento <source> consulta a preferência de esquema de cores e mostra o pano de fundo adequado.

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

Você pode introduzir outras mudanças com base nessa preferência de esquema de cores. As abóboras da segunda página reagem à preferência de esquema de cores de um usuário. O SVG usado possui círculos que representam chamas, que são dimensionados e animados no modo escuro.

.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;
     }
   }
 }

Este retrato está observando você?

Você verá algo na página 10. Você está em observação! Os olhos do retrato seguirão o ponteiro conforme você se movimenta pela página. O truque aqui é mapear a localização do ponteiro para um valor de translação e transmiti-lo ao 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)
 }

Esse código usa intervalos de entrada e saída e mapeia os valores fornecidos. Por exemplo, esse uso resultaria no valor 625.

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

Para o retrato, o valor de entrada é o ponto central de cada olho, mais ou menos uma distância em pixels. O intervalo de saída é quanto os olhos podem traduzir em pixels. A posição do ponteiro no eixo x ou y é transmitida como o valor. Para mover o ponto central dos olhos, eles são duplicados. Os originais não se movem, são transparentes e são usados para referência.

Em seguida, é preciso vincular esses elementos e atualizar os valores da propriedade personalizada do CSS nos olhos para que eles se movam. Uma função está vinculada ao evento pointermove na window. Durante o disparo, os limites de cada olho são usados para calcular os pontos centrais. Em seguida, a posição do ponteiro é mapeada para valores definidos como valores de propriedades personalizadas nos olhos.

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

Depois que os valores são transmitidos para o CSS, os estilos podem fazer o que quiserem com eles. A grande parte aqui é usar CSS clamp() para diferenciar o comportamento de cada olho, permitindo que cada olho se comporte de maneira diferente sem tocar no JavaScript novamente.

.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);
 }

Fazendo feitiços

Ao ler a página seis, você se sente encantado? Esta página adota o design da nossa fantástica raposa mágica. Se você mover o cursor, poderá ver um efeito personalizado de trilha do cursor. Isso usa animação de tela. Um elemento <canvas> fica acima do restante do conteúdo da página com pointer-events: none. Isso significa que os usuários ainda podem clicar nos blocos de conteúdo abaixo dele.

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

Assim como nosso retrato detecta um evento pointermove em window, o mesmo acontece com o elemento <canvas>. No entanto, sempre que o evento é disparado, criamos um objeto para ser animado no elemento <canvas>. Esses objetos representam formas usadas na trilha do cursor. Eles têm coordenadas e um tom aleatório.

A função mapRange anterior é usada novamente, porque podemos usá-la para mapear o delta do ponteiro para size e rate. Os objetos são armazenados em uma matriz que recebe uma repetição quando eles são renderizados no elemento <canvas>. As propriedades de cada objeto informam ao elemento <canvas> onde os itens precisam ser desenhados.

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)

Uma repetição é criada com requestAnimationFrame para desenhar na tela. A trilha do cursor só deve ser renderizada quando a página está em visualização. Temos um IntersectionObserver que atualiza e determina quais páginas ficam em visualização. Se uma página estiver em visualização, os objetos serão renderizados como círculos na tela.

Em seguida, passamos pela matriz de blocks e desenhamos cada parte da trilha. Cada frame reduz o tamanho e muda a posição do objeto de acordo com a rate. Isso produz o efeito de queda e dimensionamento. Se o objeto diminuir completamente, ele será removido da matriz 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)
 }

Se a página sair da visualização, os listeners de eventos serão removidos e o loop de frames da animação será cancelado. A matriz blocks também é apagada.

Veja a trilha do cursor em ação.

Revisão de acessibilidade

É bom criar uma experiência divertida para explorar, mas não será bom se ela não for acessível aos usuários. A experiência de Adam nessa área foi inestimável para preparar o Chrometober para uma análise de acessibilidade antes do lançamento.

Algumas das principais áreas cobertas:

  • Garantir que o HTML usado fosse semântico. Isso incluiu itens como elementos de ponto de referência adequados (por exemplo, <main> para o livro), além do uso do elemento <article> para cada bloco de conteúdo e elementos <abbr> em que as siglas são introduzidas. Pensar no desenvolvimento do livro tornou as coisas mais acessíveis. O uso de cabeçalhos e links facilita a navegação do usuário. O uso de uma lista para as páginas também significa que o número de páginas é anunciado por tecnologia assistiva.
  • Garanta que todas as imagens usem atributos alt apropriados. Para SVGs inline, o elemento title está presente quando necessário.
  • Usar atributos aria para melhorar a experiência. O uso de aria-label para páginas e os lados delas comunica ao usuário em qual página ele está. O uso de aria-describedBy nos links "Leia mais" comunica o texto do bloco de conteúdo. Isso remove a ambiguidade sobre para onde o link levará o usuário.
  • Com relação aos bloqueios de conteúdo, a capacidade de clicar em todo o card, e não apenas no link "Leia mais", está disponível.
  • O uso de um IntersectionObserver para rastrear quais páginas são visualizadas antes. Isso tem muitos benefícios não apenas relacionados ao desempenho. As animações ou interações das páginas que não estiverem em visualização serão pausadas. No entanto, essas páginas também têm o atributo inert aplicado. Isso significa que os usuários que usam um leitor de tela podem explorar o mesmo conteúdo que usuários com visão. O foco permanece na página que está na visualização, e os usuários não podem usar a tecla Tab para acessar outra página.
  • Por último, mas não menos importante, usamos consultas de mídia para respeitar a preferência do usuário por movimento.

A seguir, uma captura de tela da revisão que destaca algumas das medidas adotadas.

é identificado em todo o livro, indicando que ele deve ser o principal ponto de referência para os usuários de tecnologia assistiva encontrarem. Há mais detalhes na captura de tela." width="800" height="465">

Captura de tela do livro Chrometober aberto São fornecidas caixas com contorno em verde em torno de vários aspectos da interface, descrevendo a funcionalidade de acessibilidade pretendida e os resultados da experiência do usuário que a página entregará. Por exemplo, imagens têm texto alternativo. Outro exemplo é um rótulo de acessibilidade que declara que as páginas fora de visualização estão inertes. Saiba mais na captura de tela.

O que descobrimos

A motivação por trás do Chrometober não era apenas destacar o conteúdo da Web da comunidade, mas também era uma maneira de testar o polyfill da API de animações de rolagem que está em desenvolvimento.

Reservamos uma sessão durante nossa reunião de equipes em Nova York para testar o projeto e resolver os problemas que surgiram. A contribuição da equipe foi inestimável. Também foi uma ótima oportunidade de listar tudo o que precisava ser resolvido antes que pudéssemos colocar ao vivo.

As equipes de CSS, UI e DevTools estão sentadas ao redor da mesa em uma sala de conferências. Una está em um quadro branco coberto de notas adesivas. Outros membros da equipe sentam-se ao redor da mesa com bebidas e laptops.

Por exemplo, testar o livro em dispositivos gerou um problema de renderização. Nosso livro não seria renderizado como esperado em dispositivos iOS. Os blocos de janela de visualização dimensionam a página, mas quando havia um entalhe, isso afetava o livro. A solução era usar viewport-fit=cover na janela de visualização meta:

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

Essa sessão também levantou alguns problemas com o polyfill da API. A Bramus (link em inglês) gerou esses problemas no repositório do polyfill. Em seguida, ele encontrou soluções para esses problemas e as mesclava no polyfill. Por exemplo, esta solicitação de envio teve um ganho de desempenho ao adicionar o armazenamento em cache a parte do polyfill.

Captura de tela de uma demonstração aberta no Chrome. As Ferramentas para desenvolvedores estão abertas e mostram uma medição de desempenho de linha de base.

Captura de tela de uma demonstração aberta no Chrome. As Ferramentas para desenvolvedores estão abertas e mostram uma medição de desempenho aprimorada.

Pronto!

Foi um projeto muito divertido de se trabalhar, resultando em uma experiência de rolagem extravagante que destaca o conteúdo incrível da comunidade. Além disso, também foi ótimo testar o polyfill e enviar feedback à equipe de engenharia para ajudar a melhorá-lo.

O Chrometober 2022 foi encerrado.

Esperamos que tenha gostado. Qual é seu recurso favorito? Envie um tweet para mim e avise nossa equipe.

Jhey segurando uma folha de adesivos dos personagens do Chrometober.

Você pode até pegar alguns adesivos de um dos membros da equipe se nos encontrar em um evento.

Foto principal por David Menidrey no Unsplash