Creo Chrometober!

Come ha preso vita il libro a scorrimento per condividere suggerimenti e trucchi divertenti e spaventosi su Chrometober.

Facendo seguito a Designcember, quest'anno volevamo realizzare Chrometober per te, in modo da poter mettere in evidenza e condividere i contenuti web forniti dalla community e dal team di Chrome. Designcember ha presentato l'utilizzo delle query Container, ma quest'anno presenteremo l'API delle animazioni CSS collegate a scorrimento.

Dai un'occhiata all'esperienza del libro a scorrimento all'indirizzo web.dev/chrometober-2022.

Panoramica

L'obiettivo del progetto era offrire un'esperienza stravagante che mettesse in evidenza l'API delle animazioni collegate a scorrimento. Tuttavia, pur essendo stravagante, l'esperienza doveva essere anche reattiva e accessibile. Il progetto è stato anche un ottimo modo per testare il polyfill delle API in fase di sviluppo attivo, oltre a provare diverse tecniche e strumenti in combinazione. Il tutto a tema Halloween.

La struttura del nostro team era simile a questa:

Creazione della bozza di un'esperienza di scorrimento

Le idee per Chrometober hanno iniziato a svilupparsi presso il nostro primo team esterno a maggio 2022. Una raccolta di disegni a mano libera ci ha fatto pensare a come un utente potesse scorrere i propri contenuti lungo uno storyboard. Ispirandoci ai videogiochi, abbiamo pensato a un'esperienza di scorrimento attraverso scene come cimiteri e una casa stregata.

Un taccuino su una scrivania con vari scarabocchi e scarabocchi correlati al progetto.

È stato emozionante avere la libertà creativa di portare il mio primo progetto Google in una direzione inaspettata. Si tratta di un primo prototipo di modalità di esplorazione dei contenuti da parte degli utenti.

Quando l'utente scorre lateralmente, i blocchi ruotano e ridimensionano. Ma ho deciso di abbandonare questa idea per preoccuparmi di come potremmo rendere questa esperienza eccezionale per gli utenti su dispositivi di tutte le dimensioni. Invece, ho puntato sul design di qualcosa che avevo realizzato in passato. Nel 2020 ho avuto la fortuna di avere accesso a GreenSock's scrollTrigger per creare demo delle release.

Una delle demo che avevo creato era un libro in 3D-CSS in cui le pagine giravano mentre scorrono i contenuti e questo mi sembrava molto più appropriato per ciò che volevamo per Chrometober. L'API di animazioni con link a scorrimento è uno scambio perfetto per questa funzionalità. Come potrai vedere, funziona anche bene con scroll-snap.

Il nostro illustratore del progetto, Tyler Reed, è stato bravissimo a modificare il design man mano che cambiavamo le idee. Tyler ha fatto un lavoro fantastico nel prendere tutte le idee creative che gli proponevano e darle vita. È stato molto divertente raccogliere idee insieme. Volevamo che funzionasse in gran parte le funzionalità suddivise in blocchi isolati. In questo modo potremmo suddividerli in scene e poi scegliere ciò che abbiamo dato vita.

Una delle scene di composizione in cui appare un serpente, una bara con le braccia che escono, una volpe con una bacchetta su un calderone, un albero con un volto spettrale e un gargoyle che tiene una lanterna di zucca.

L'idea principale era che, man mano che l'utente leggeva il libro, poteva accedere a blocchi di contenuti. Potrebbero anche interagire con tratti stravaganti, tra cui gli Easter egg che avevamo integrato nell'esperienza; ad esempio, un ritratto in una casa infestata, i cui occhi seguivano il puntatore, o sottili animazioni attivate da query multimediali. Queste idee e funzionalità vengono animate sullo scorrimento. Un'idea iniziale era un coniglietto zombie che sorgeva e si traslava sull'asse x quando l'utente scorre la pagina.

Acquisire familiarità con l'API

Prima di poter iniziare a giocare con le funzionalità individuali e gli Easter egg, avevamo bisogno di un libro. Abbiamo quindi deciso di trasformarla in un'opportunità per testare il set di funzionalità dell'API per le animazioni CSS collegate allo scorrimento. L'API delle animazioni con link a scorrimento non è attualmente supportata in nessun browser. Tuttavia, durante lo sviluppo dell'API, gli ingegneri del team di interazione hanno lavorato a un polyfill. Ciò fornisce un modo per testare la forma dell'API durante il suo sviluppo. Ciò significa che potremmo utilizzare questa API oggi stesso e progetti divertenti come questo sono spesso un ottimo posto per provare le funzionalità sperimentali e per fornire feedback. Scopri cosa abbiamo imparato e cosa siamo riusciti a fornire più avanti nell'articolo.

A livello generale, puoi utilizzare questa API per collegare le animazioni per scorrere. È importante notare che non puoi attivare un'animazione con lo scorrimento perché potrebbe verificarsi in seguito. Anche le animazioni collegate a scorrimento rientrano in due categorie principali:

  1. Quelle che reagiscono alla posizione dello scorrimento.
  2. Quelle che reagiscono alla posizione di un elemento nel relativo contenitore a scorrimento.

Per creare quest'ultimo, utilizziamo un ViewTimeline applicato tramite una proprietà animation-timeline.

Ecco un esempio di come si presenta l'utilizzo di ViewTimeline in 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;
 }
}

Creiamo un oggetto ViewTimeline con view-timeline-name e ne definiamo il relativo asse. In questo esempio, block si riferisce all'block logico. L'animazione viene collegata per scorrere con la proprietà animation-timeline. animation-delay e animation-end-delay (al momento della scrittura) sono il modo in cui definiamo le fasi.

Queste fasi definiscono i punti in cui deve essere collegata l'animazione rispetto alla posizione di un elemento nel relativo contenitore a scorrimento. Nel nostro esempio, stiamo dicendo di avviare l'animazione quando l'elemento entra (enter 0%) nel contenitore a scorrimento. e termina quando ha coperto il 50% (cover 50%) del contenitore a scorrimento.

Ecco la nostra demo in azione:

Puoi anche collegare un'animazione all'elemento che si muove nell'area visibile. Puoi farlo impostando animation-timeline in modo che sia il view-timeline dell'elemento. Questa opzione è utile per scenari come le animazioni di elenchi. Il comportamento è simile all'animazione degli elementi al momento della voce utilizzando 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;
  }
}

In questo modo, l'opzione "Mover" viene ridimensionata quando entra nell'area visibile, attivando la rotazione dello "Spinner".

Dalla sperimentazione è emerso che l'API funziona molto bene con scroll-snap. Scorrimento combinato con ViewTimeline sarebbe un'ottima soluzione per aggredire le pagine di un libro.

Prototipazione della meccanica

Dopo qualche esperimento, sono riuscita a ottenere un prototipo di libro funzionante. Puoi scorrere in orizzontale per girare le pagine del libro.

Nella demo, puoi vedere i diversi attivatori evidenziati con bordi tratteggiati.

Il markup ha un aspetto simile al seguente:

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

Mentre scorri, le pagine del libro girano ma si aprono o si chiudono. Dipende dall'allineamento di scorrimento e scatto dei trigger.

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

Questa volta non colleghiamo ViewTimeline in CSS, ma utilizziamo l'API Web Animations in JavaScript. Questo offre il vantaggio aggiuntivo di essere in grado di eseguire il loop su un insieme di elementi e generare il ViewTimeline di cui abbiamo bisogno, invece di crearli tutti 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);

Per ogni attivatore, generiamo un ViewTimeline. Successivamente antiviamo la pagina associata dell'attivatore utilizzando questo ViewTimeline. che collega l'animazione della pagina allo scorrimento. Per la nostra animazione, ruotiamo un elemento della pagina sull'asse y per voltare pagina. Inoltre traduciamo la pagina sull'asse z in modo che si comporti come un libro.

Riassumendo

Una volta individuato il meccanismo del libro, ho potuto concentrarmi sul dare vita alle illustrazioni di Tyler.

Astro

Nel 2021 il team ha utilizzato Astro per Designcember e io non vedevo l'ora di utilizzarlo di nuovo per Chrometober. L'esperienza degli sviluppatori di essere in grado di suddividere gli elementi in componenti è adatta a questo progetto.

Il libro stesso è un componente. È anche una raccolta di componenti di pagina. Ogni pagina ha due lati e uno sfondo. Gli elementi secondari di un lato pagina sono componenti che possono essere aggiunti, rimossi e posizionati con facilità.

Creazione di un libro

Per me è stato importante semplificare la gestione dei blocchi. Volevo anche fare in modo che il resto del team potesse contribuire facilmente.

Le pagine di alto livello sono definite da un array di configurazione. Ogni oggetto di pagina nell'array definisce i contenuti, lo sfondo e altri metadati di una pagina.

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

Questi vengono trasmessi al componente Book.

<Book pages={pages} />

Il componente Book è il punto in cui viene applicato il meccanismo di scorrimento e vengono create le pagine del libro. Viene utilizzato lo stesso meccanismo del prototipo, ma condividiamo più istanze di ViewTimeline create a livello globale.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

In questo modo, possiamo condividere le tempistiche da utilizzare altrove, invece di ricrearle. Ne parleremo più avanti.

Composizione della pagina

Ogni pagina è una voce all'interno di un elenco:

<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 configurazione definita viene passata a ogni istanza Page. Le pagine utilizzano la funzionalità di slot di Astro per inserire contenuti in ogni pagina.

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

Questo codice serve principalmente per configurare la struttura. I collaboratori possono lavorare ai contenuti del libro per la maggior parte senza dover toccare questo codice.

Sfondi

Lo spostamento creativo verso un libro ha reso molto più facile la suddivisione delle sezioni e ogni stesura del libro è una scena tratta dal design originale.

Illustrazione tratta dal libro che mostra un melo in un cimitero. Il cimitero ha diverse lapidi e c&#39;è un pipistrello in cielo di fronte a una grande luna.

Dato che avevamo deciso le proporzioni per il libro, lo sfondo di ogni pagina potrebbe includere un elemento immagine. Per risolvere il problema, è necessario impostare la larghezza dell'elemento al 200% e utilizzare object-position in base al lato della pagina.

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

Contenuto della pagina

Vediamo come creare una delle pagine. Nella pagina 3 compare un gufo che spunta da un albero.

Viene compilato con un componente PageThree, come definito nella configurazione. È un componente Astro (PageThree.astro). Questi componenti hanno l'aspetto di file HTML, ma in alto hanno un recinto di codice simile a frontmatter. Questo ci consente di eseguire operazioni come l'importazione di altri componenti. Il componente per la terza pagina ha il seguente aspetto:

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

Anche in questo caso, le pagine sono di natura atomica. Si basano su una serie di funzionalità. La pagina tre presenta un blocco di contenuti e il gufo interattivo, quindi esiste un componente per ciascuno.

I blocchi di contenuti sono i link ai contenuti visualizzati all'interno del libro. Anche questi vengono guidati da un oggetto di configurazione.

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

Questa configurazione viene importata dove sono richiesti blocchi di contenuti. La configurazione del blocco pertinente viene quindi trasmessa al componente ContentBlock.

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

C'è anche un esempio qui di come utilizziamo il componente della pagina come posizione per posizionare i contenuti. In questo caso, viene posizionato un blocco di contenuti.

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

Tuttavia, gli stili generali di un blocco di contenuti si trovano insieme al codice 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%;
}

Per quanto riguarda il nostro gufo, è una funzionalità interattiva, una delle tante in questo progetto. Questo è un piccolo esempio per mostrare come abbiamo utilizzato la cronologia delle visualizzazioni condivisa che abbiamo creato.

A livello generale, il nostro componente gufo importa alcuni SVG e lo incorpora utilizzando il frammento di Astro.

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

E gli stili per il posizionamento del nostro gufo vengono collocati insieme al codice del componente.

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

C'è un elemento di stile aggiuntivo che definisce il comportamento di transform per il gufo.

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

L'utilizzo di transform-box influisce su transform-origin. Lo rende relativo al riquadro di delimitazione dell'oggetto all'interno del file SVG. Il gufo scala verso l'alto dal centro in basso, da qui l'uso di transform-origin: 50% 100%.

La parte divertente è quando colleghiamo il gufo a uno dei nostri ViewTimeline generati:

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

In questo blocco di codice, facciamo due cose:

  1. Controlla le preferenze di movimento dell'utente.
  2. Se non hanno preferenze, collega un'animazione del gufo per scorrere.

Per la seconda parte, il gufo si anima sull'asse y usando l'API Web Animations. Viene utilizzata la singola proprietà di trasformazione translate, collegata a una ViewTimeline. È collegato a CHROMETOBER_TIMELINES[1] tramite la proprietà timeline. Questo è un valore ViewTimeline generato per il voltaggio pagina. Questo collega l'animazione del gufo al cambio pagina utilizzando la fase enter. Definisce che, quando la pagina viene girata all'80%, inizia a spostare il gufo. Al 90%, il gufo dovrebbe terminare la sua traduzione.

Funzionalità dei libri

Ora hai visto l'approccio per creare una pagina e come funziona l'architettura del progetto. Puoi vedere come consente ai collaboratori di entrare e lavorare su una pagina o una funzionalità a loro scelta. Le animazioni di varie caratteristiche del libro sono collegate al volta pagina del libro; ad esempio, il pipistrello che vola dentro e fuori quando gira pagina.

Inoltre, contiene elementi basati sulle animazioni CSS.

Una volta inseriti i blocchi di contenuti nel libro, è stato possibile dare sfogo alla creatività con le altre funzionalità. Ciò ha offerto l'opportunità di generare interazioni diverse e provare modi diversi per implementare le azioni.

Massima reattività

Le unità dell'area visibile adattabile ridimensionano il libro e le sue funzionalità. Tuttavia, mantenere la reattività dei caratteri è stata una sfida interessante. Le unità di query del container sono adatte a questo caso. Tuttavia, non sono ancora supportati ovunque. Le dimensioni del libro sono impostate, pertanto non è necessaria una query contenitore. Un'unità di query del contenitore incorporato può essere generata con CSS calc() e utilizzata per il dimensionamento dei caratteri.


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

Zucche che brillano di notte

Le persone con occhio attento potrebbero aver notato l'uso degli elementi <source> quando hanno parlato prima degli sfondi della pagina. Una voleva un'interazione che reagisse alla preferenza della combinazione di colori. Di conseguenza, gli sfondi supportano sia le modalità Luce che Buio con diverse varianti. Poiché puoi utilizzare query supporti con l'elemento <picture>, è un ottimo modo per fornire due stili di sfondo. L'elemento <source> esegue una query sulla preferenza della combinazione di colori e mostra lo sfondo appropriato.

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

Puoi introdurre altre modifiche in base a quella preferenza di combinazione di colori. Le zucche nella seconda pagina reagiscono alla preferenza della combinazione di colori di un utente. Il file SVG utilizzato ha cerchi che rappresentano delle fiamme, che vengono ridimensionate e si animano in modalità Buio.

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

Questo ritratto ti sta guardando?

Se consulti la pagina 10, potresti notare qualcosa. Stai guardando il video! Gli occhi del ritratto seguiranno il puntatore mentre ti sposti nella pagina. Il trucco è mappare la posizione del puntatore a un valore di traduzione e trasmetterla a 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)
 }

Questo codice prende gli intervalli di input e di output e mappa i valori specificati. Ad esempio, questo utilizzo assegna il valore 625.

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

Per l'immagine verticale, il valore di input corrisponde al punto centrale di ciascun occhio, più o meno una distanza in pixel. L'intervallo di output indica quanto gli occhi possono tradurre in pixel. Poi la posizione del puntatore sugli assi x o y viene passata come valore. Per ottenere il punto centrale degli occhi mentre li sposti, gli occhi vengono duplicati. Gli originali non si spostano, sono trasparenti e vengono utilizzati come riferimento.

Poi devi collegarli e aggiornare i valori delle proprietà personalizzate CSS sugli occhi in modo che gli occhi si muovano. Una funzione è associata all'evento pointermove rispetto a window. Quando questa si attiva, i limiti di ciascun occhio vengono utilizzati per calcolare i punti centrali. Quindi la posizione del puntatore viene mappata a valori impostati come valori di proprietà personalizzate sugli occhi.

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 volta trasmessi i valori al CSS, gli stili possono fare ciò che vogliono. L'ideale è utilizzare CSS clamp() per modificare il comportamento di ogni occhio in modo che si comporti in modo diverso senza dover toccare nuovamente 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);
 }

Lanciare incantesimi

Se leggi la sesta pagina, ti senti incantato? Questa pagina adotta il design della nostra fantastica volpe magica. Se sposti il puntatore, potresti vedere un effetto scia del cursore personalizzato. Viene utilizzata l'animazione della tela. Un elemento <canvas> si trova sopra il resto dei contenuti della pagina con pointer-events: none. Ciò significa che gli utenti possono comunque fare clic sui blocchi di contenuti sottostanti.

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

Proprio come il nostro ritratto ascolta un evento pointermove il giorno window, così fa anche il nostro elemento <canvas>. Tuttavia, ogni volta che viene attivato l'evento, viene creato un oggetto che si anima nell'elemento <canvas>. Questi oggetti rappresentano le forme utilizzate nella traccia del cursore. Hanno coordinate e una tonalità casuale.

La funzione mapRange di cui abbiamo parlato in precedenza viene utilizzata di nuovo perché possiamo utilizzarla per mappare il delta del puntatore su size e rate. Gli oggetti vengono archiviati in un array che viene eseguito in loop quando gli oggetti vengono disegnati all'elemento <canvas>. Le proprietà di ogni oggetto indicano all'elemento <canvas> dove devono essere tracciati.

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)

Per il disegno sul canvas, viene creato un loop con requestAnimationFrame. La traccia del cursore deve essere visualizzata solo quando la pagina è visibile. Abbiamo un'IntersectionObserver che si aggiorna e determina quali pagine sono visualizzate. Se una pagina è visualizzata, gli oggetti vengono visualizzati come cerchi sul canvas.

Quindi facciamo un giro sull'array blocks e disegniamo ogni parte del sentiero. Ogni frame riduce le dimensioni e modifica la posizione dell'oggetto in base a rate. Questo produce l'effetto di caduta e scalabilità. Se l'oggetto si restringe completamente, l'oggetto viene rimosso dall'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)
 }

Se la pagina non è più visibile, i listener di eventi vengono rimossi e il loop del frame dell'animazione viene annullato. Viene cancellato anche l'array blocks.

Ecco la traccia del cursore in azione.

Revisione accessibilità

Creare un'esperienza divertente da esplorare è molto positivo, ma non va bene se non è accessibile agli utenti. L'esperienza di Adam in questo settore si è rivelata preziosa per preparare Chrometober a una revisione dell'accessibilità prima del lancio.

Di seguito sono riportate alcune delle aree più importanti interessate:

  • Assicurarsi che il codice HTML utilizzato sia semantico. Sono inclusi, ad esempio, elementi di punto di riferimento appropriati, come <main> per il libro, nonché l'uso dell'elemento <article> per ogni blocco di contenuti e di elementi <abbr> in cui vengono introdotti gli acronimi. Pensare in anticipo durante la realizzazione del libro ha reso le cose più accessibili. L'utilizzo di intestazioni e link facilita la navigazione da parte dell'utente. Se viene utilizzato un elenco per le pagine, anche il numero di pagine viene annunciato dalle tecnologie per la disabilità.
  • Assicurarsi che tutte le immagini utilizzino gli attributi alt appropriati. Per gli SVG in linea, l'elemento title è presente dove necessario.
  • L'utilizzo degli attributi aria migliora l'esperienza. L'uso di aria-label per le pagine e i lati comunica all'utente la pagina in cui si trova. L'utilizzo di aria-describedBy nei link "Scopri di più" comunica il testo del blocco di contenuti. Ciò elimina ambiguità sulla destinazione del link che porterà l'utente.
  • In merito ai blocchi dei contenuti, è disponibile la possibilità di fare clic sull'intera scheda e non solo sul link "Scopri di più".
  • L'utilizzo di un IntersectionObserver per monitorare le pagine visualizzate in precedenza. Questo offre molti vantaggi non solo legati al rendimento. Nelle pagine non visualizzate saranno in pausa animazioni o interazioni. Tuttavia, a queste pagine è applicato anche l'attributo inert. Ciò significa che gli utenti che utilizzano uno screen reader possono esplorare gli stessi contenuti degli utenti vedenti. Lo stato attivo rimane all'interno della pagina visualizzata e gli utenti non possono passare a un'altra pagina.
  • Ultimo ma non meno importante, utilizziamo le query supporti per rispettare la preferenza di un utente per quanto riguarda il movimento.

Ecco uno screenshot della recensione che evidenzia alcune delle misure adottate.

viene identificato intorno all'intero libro, a indicare che dovrebbe essere il punto di riferimento principale che gli utenti delle tecnologie per la disabilità possono trovare. Le informazioni aggiuntive sono descritte nello screenshot." width="800" height="465">

Screenshot del libro Chrometober aperto. Vengono forniti riquadri verdi in corrispondenza di vari aspetti dell&#39;UI, che descrivono la funzionalità di accessibilità prevista e i risultati dell&#39;esperienza utente che la pagina fornirà. Ad esempio, le immagini hanno un testo alternativo. Un altro esempio è un&#39;etichetta di accessibilità che dichiara che le pagine non visibili sono inerte. Nello screenshot sono disponibili altre informazioni.

Che cosa abbiamo imparato

La motivazione alla base di Chrometober non era solo quella di mettere in evidenza i contenuti web della community, ma ci ha anche permesso di testare il polyfill dell'API delle animazioni collegate a scorrimento in fase di sviluppo.

Abbiamo organizzato una sessione durante il summit del nostro team a New York per testare il progetto e affrontare i problemi che sono emersi. Il contributo del team è stato inestimabile. È stata anche una grande opportunità per elencare tutte le cose che dovevano affrontare prima di poter iniziare a trasmettere dal vivo.

Il team di CSS, UI e DevTools siede al tavolo in una sala conferenze. Una è in piedi davanti a una lavagna ricoperta di note adesive. Altri membri del team siedono al tavolo con spuntini e laptop.

Ad esempio, aver provato il libro su dispositivi ha sollevato un problema di rendering. Il nostro libro non verrebbe visualizzato come previsto sui dispositivi iOS. Le unità dell'area visibile ridimensionano la pagina, ma quando era presente un notch, incideva sul libro. La soluzione prevedeva l'utilizzo di viewport-fit=cover nell'area visibile meta:

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

Questa sessione ha sollevato anche alcuni problemi con il polyfill dell'API. Bramus ha sollevato questi problemi nel repository polyfill. In seguito ha trovato soluzioni a questi problemi e li ha uniti nel polyfill. Ad esempio, questa richiesta di pull ha migliorato le prestazioni aggiungendo la memorizzazione nella cache a parte del polyfill.

Uno screenshot di una demo aperta in Chrome. Gli Strumenti per sviluppatori sono aperti e mostrano una misurazione del rendimento di base.

Uno screenshot di una demo aperta in Chrome. Gli Strumenti per sviluppatori sono aperti e mostrano una misurazione del rendimento migliorata.

È tutto.

È stato un progetto davvero divertente su cui lavorare, che ha generato un'esperienza di scorrimento stravagante che mette in evidenza i contenuti straordinari della community. Non solo, è stato ottimo per testare il polyfill, e anche per fornire feedback al team di tecnici per contribuire a migliorare il polyfill.

Chrometober 2022 è terminato.

Speriamo che ti sia piaciuto. Qual è la tua funzionalità preferita? Inviami un tweet e facci sapere.

Jhey che tiene in mano un foglio di adesivi con i personaggi di Chrometober.

Potresti persino ottenere alcuni adesivi da uno dei membri del team se ci vediamo a un evento.

Foto hero di David Menidrey su Unsplash