Chrometober wird erstellt!

Hier erfährst du, wie das Buch mit lustigen und angsteinflößenden Tipps und Tricks in Chrometober zum Leben erweckt wurde.

Nach Designcember wollten wir Chrometober dieses Jahr für Sie entwickeln, damit Sie Webinhalte von der Community und vom Chrome-Team hervorheben und teilen können. Designcember hat die Verwendung von Containerabfragen demonstriert. Dieses Jahr möchten wir Ihnen die durch Scrollen verknüpfte Animations-API von CSS vorstellen.

Sehen Sie sich das Scrolling Book unter web.dev/chrometober-2022 an.

Überblick

Ziel des Projekts war es, die durch Scrollen verknüpfte Animations-API hervorzuheben. Die App sollte zwar originell, aber auch ansprechend und barrierefrei sein. Das Projekt war auch eine gute Möglichkeit, den API-Polyfill zu testen, der derzeit entwickelt wird. Außerdem konnten wir verschiedene Techniken und Tools in Kombination ausprobieren. Und alles mit einem festlichen Halloween-Design!

Unsere Teamstruktur sah so aus:

Scrollytelling entwerfen

Die Ideen für Chrometober kamen im Mai 2022 bei unserem ersten externen Team in die Praxis. Eine Sammlung von Skizzen hat uns dazu gebracht, darüber nachzudenken, wie Nutzende sich in einer Form eines Storyboards durchscrollen können. Inspiriert von Videospielen sahen wir das Scrollen durch Szenen wie Friedhöfe und ein Spukhaus aus.

Ein Notizbuch liegt auf einem Schreibtisch mit verschiedenen Zeichnungen und Skizzen, die mit dem Projekt zu tun haben.

Es war aufregend, bei meinem ersten Google-Projekt die kreative Freiheit in eine unerwartete Richtung zu lenken. Dies war ein früher Prototyp dafür, wie Nutzende durch den Inhalt navigieren könnten.

Wenn der Nutzer zur Seite scrollt, drehen und skalieren die Blöcke. Aber ich beschloss, mich von dieser Idee zu verabschieden und nicht zu sorgen, wie wir diese User Experience für Nutzer auf Geräten aller Größen verbessern können. Stattdessen neigte ich zum Design von etwas, das ich in der Vergangenheit gemacht habe. 2020 hatte ich Glück, dass ich Zugriff auf den ScrollTrigger von GreenSock hatte, um Release-Demos zu erstellen.

Eines der Demos, die ich erstellt hatte, war ein 3D-CSS-Buch, in dem sich die Seiten beim Scrollen umblättern. Das kam gut zu dem, was wir für Chrometober wollten. Die durch Scrollen verknüpfte Animations-API ist ein perfekter Ersatz für diese Funktion. Wie du siehst, funktioniert das auch gut mit scroll-snap.

Unser Illustrator für das Projekt, Tyler Reed, war großartig darin, das Design anzupassen, wenn wir Ideen veränderten. Tyler hat alle seine kreativen Ideen umgesetzt und verwirklicht. Es war eine Menge Spaß beim Brainstorming. Ein wesentlicher Teil unserer Vorhaben, dass dies funktionieren sollte, bestand darin, Funktionen in isolierte Blöcke aufzuteilen. Auf diese Weise konnten wir sie zu Szenen zusammensetzen und dann auswählen, was wir zum Leben erweckt haben.

Eine der Kompositionsszenen mit einer Schlange, einem Sarg mit ausgestreckten Armen, einem Fuchs mit einem Stab an einem Kessel, einem Baum mit gruseligem Gesicht und einem Wasserspeier mit einer Kürbislaterne.

Die Grundidee war, dass die Nutzenden beim Durchstöbern des Buches auf Inhaltsblöcke zugreifen können. Sie können aber auch mit skurrilen Ideen interagieren, zum Beispiel mit den Easter Eggs, die wir in das Erlebnis eingebaut hatten, zum Beispiel ein Porträt in einem Spukhaus, dessen Augen Ihrem Zeiger folgen, oder subtile Animationen, die durch Medienabfragen ausgelöst werden. Diese Ideen und Funktionen werden beim Scrollen animiert. Eine frühe Idee war ein Zombie-Hase, das sich beim Scrollen der Nutzenden entlang der X-Achse bewegt.

Mit der API vertraut machen

Bevor wir mit einzelnen Spielzügen und Easter Eggs spielen konnten, brauchten wir ein Buch. Daher beschlossen wir, dies zu einer Gelegenheit zu machen, die Funktionen für die neue CSS API für scrollbare Animation zu testen. Die durch Scrollen verknüpfte Animations-API wird derzeit in keinem Browser unterstützt. Während der Entwicklung der API haben die Entwickler des Interaction-Teams jedoch an einem polyfill gearbeitet. So lässt sich die Form der API während der Entwicklung testen. Das bedeutet, dass wir diese API heute verwenden könnten. Solche Projekte sind oft ein guter Ort, um experimentelle Funktionen zu testen und Feedback zu geben. Was wir daraus gelernt haben und welches Feedback wir geben konnten, erfährst du weiter unten in diesem Artikel.

Auf übergeordneter Ebene können Sie diese API verwenden, um Animationen zum Scrollen zu verknüpfen. Beachten Sie, dass Sie beim Scrollen keine Animation auslösen können. Dies ist etwas, das später kommen könnte. Animationen, die per Scrollen verknüpft sind, lassen sich ebenfalls in zwei Hauptkategorien einteilen:

  1. Die Variablen, die auf die Scrollposition reagieren.
  2. Elemente, die auf die Position eines Elements im Scrollcontainer reagieren.

Um Letzteres zu erstellen, verwenden wir ein ViewTimeline, das über eine animation-timeline-Property angewendet wird.

Hier ein Beispiel dafür, wie die Verwendung von ViewTimeline in CSS aussieht:

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

Wir erstellen ein ViewTimeline mit view-timeline-name und definieren die Achse dafür. In diesem Beispiel bezieht sich block auf das logische block. Die Animation wird verknüpft, um mit der Eigenschaft animation-timeline zu scrollen. animation-delay und animation-end-delay (zum Zeitpunkt der Erstellung dieses Dokuments) sind unsere Definition von Phasen.

Diese Phasen definieren, an welchen Punkten die Animation in Bezug auf die Position eines Elements im Scrollcontainer verknüpft werden soll. In unserem Beispiel sagen wir, dass die Animation gestartet wird, wenn das Element in den scrollbaren Container (enter 0%) gelangt. Fertig, wenn 50% (cover 50%) des Scrollcontainers abgedeckt sind.

Hier ist unsere Demo in Aktion:

Sie können auch eine Animation mit dem Element verknüpfen, das sich im Darstellungsbereich bewegt. Setzen Sie dazu animation-timeline auf view-timeline des Elements. Diese Option eignet sich gut für Szenarien wie Listenanimationen. Das Verhalten ähnelt dem Animieren von Elementen bei der Eingabe mit 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;
  }
}

Damit wird „Mover“ vertikal skaliert, wenn er in den Darstellungsbereich eintritt, und die Rotation von „Spinner“ ausgelöst wird.

Bei den Tests habe ich festgestellt, dass die API sehr gut mit Scroll-Snap funktioniert. Die Kombination aus Scroll-Snap in Kombination mit ViewTimeline eignet sich hervorragend zum Umblättern von Seiten in einem Buch.

Prototypen für die Mechanik erstellen

Nach einigen Experimenten habe ich es geschafft, einen Prototyp für ein Buch zum Laufen zu bringen. Sie scrollen horizontal, um im Buch umzublättern.

In der Demo werden die verschiedenen Trigger durch gestrichelte Rahmen hervorgehoben.

Das Markup sieht ungefähr so aus:

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

Während Sie scrollen, drehen sich die Seiten des Buchs, rasten aber ein oder aus. Dies hängt von der Ausrichtung der Trigger beim Scrollen und Andocken der Trigger ab.

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

Dieses Mal wird ViewTimeline nicht in CSS verbunden, sondern die Web Animations API in JavaScript verwendet. Dies hat den zusätzlichen Vorteil, dass eine Schleife über eine Gruppe von Elementen möglich ist und die benötigte ViewTimeline generiert wird, anstatt jedes Element manuell zu erstellen.

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

Für jeden Trigger wird ein ViewTimeline generiert. Anschließend wird die mit dem Trigger verknüpfte Seite mithilfe dieses ViewTimeline animiert. Dadurch wird die Animation der Seite mit dem Scrollen verknüpft. Für unsere Animation drehen wir ein Element der Seite auf der Y-Achse, um die Seite zu drehen. Außerdem verschieben wir die Seite selbst auf der Z-Achse, damit sie sich wie ein Buch verhält.

Zusammenfassung

Nachdem ich den Mechanismus für das Buch erarbeitet hatte, konnte ich mich darauf konzentrieren, Tylers Illustrationen zum Leben zu erwecken.

Astro

2021 nutzte das Team Astro für Designcember und ich würde es gern wieder für Chrometober einsetzen. Die Erfahrung der Entwickler, Dinge in Komponenten aufzuteilen, ist für dieses Projekt gut geeignet.

Das Buch selbst ist ein Bestandteil. Es ist auch eine Sammlung von Seitenkomponenten. Jede Seite hat zwei Seiten und einen Hintergrund. Die untergeordneten Elemente einer Seite sind Komponenten, die problemlos hinzugefügt, entfernt und positioniert werden können.

Ein Buch erstellen

Für mich war es wichtig, die Blöcke einfach zu verwalten. Außerdem wollte ich es dem Rest des Teams so einfach wie möglich machen, Beiträge zu leisten.

Die Seiten auf einer übergeordneten Ebene werden durch ein Konfigurationsarray definiert. Jedes Seitenobjekt im Array definiert den Inhalt, den Hintergrund und andere Metadaten für eine Seite.

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

Diese werden an die Komponente Book übergeben.

<Book pages={pages} />

In der Book-Komponente wird der Scroll-Mechanismus angewendet und die Seiten des Buchs werden erstellt. Der gleiche Mechanismus wie im Prototyp wird verwendet, aber es gibt mehrere Instanzen von ViewTimeline, die global erstellt werden.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

Auf diese Weise können wir die Zeitpläne teilen, die an anderer Stelle verwendet werden sollen, anstatt sie neu zu erstellen. Mehr dazu später.

Seitenzusammensetzung

Jede Seite ist ein Listenelement in einer Liste:

<ul class="book">
  {
    pages.map((page, index) => {
      const FrontSlot = page.front.content
      const BackSlot = page.back.content
      return (
        <Page
          index={index}
          cover={page.cover}
          aria={page.aria}
          backdrop={
            {
              front: {
                light: page.front.backdrop,
                dark: page.front.darkBackdrop
              },
              back: {
                light: page.back.backdrop,
                dark: page.back.darkBackdrop
              }
            }
          }>
          {page.front.content && <FrontSlot slot="front" />}    
          {page.back.content && <BackSlot slot="back" />}    
        </Page>
      )
    })
  }
</ul>

Die definierte Konfiguration wird dann an jede Page-Instanz übergeben. Auf den Seiten wird die Slot-Funktion von Astro verwendet, um Inhalte auf jeder Seite einzufügen.

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

Dieser Code dient hauptsächlich zum Aufbau einer Struktur. Beitragende können den Inhalt des Buchs größtenteils bearbeiten, ohne diesen Code bearbeiten zu müssen.

Kulissen

Die kreative Verlagerung hin zu einem Buch machte die Aufteilung der Abschnitte viel einfacher und jede Aufteilung des Buchs ist eine Szene aus dem ursprünglichen Design.

Illustration eines Buches mit einem Apfelbaum auf einem Friedhof. Auf dem Friedhof sind mehrere Grabsteine zu sehen. Am Himmel ist vor einem großen Mond eine Fledermaus zu sehen.

Da wir ein Seitenverhältnis für das Buch festgelegt hatten, konnte der Hintergrund für jede Seite ein Bildelement haben. Am besten legen Sie für das Element 200% Breite fest und verwenden object-position basierend auf der Seite.

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

Seiteninhalte

Schauen wir uns eine der Seiten an. Auf Seite 3 ist eine Eule zu sehen, die in einem Baum aufspringt.

Es wird mit einer PageThree-Komponente gefüllt, wie in der Konfiguration definiert. Es handelt sich um eine Astro-Komponente (PageThree.astro). Diese Komponenten sehen aus wie HTML-Dateien, haben oben jedoch einen Codezaun ähnlich wie Frontmatter. So können wir beispielsweise andere Komponenten importieren. Die Komponente für Seite 3 sieht so aus:

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

Auch hier sind Seiten atomarer Natur. Sie basieren auf einer Reihe von Funktionen. Auf Seite 3 gibt es einen Inhaltsblock und die interaktive Eule. Für jede Seite gibt es eine Komponente.

Inhaltsblöcke sind die Links zu Inhalten, die im Buch zu sehen sind. Diese werden auch von einem Konfigurationsobjekt gesteuert.

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

Diese Konfiguration wird dort importiert, wo Inhaltsblöcke erforderlich sind. Die entsprechende Blockkonfiguration wird dann an die Komponente ContentBlock übergeben.

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

Es gibt hier auch ein Beispiel dafür, wie wir die Komponente der Seite als Ort zur Positionierung des Inhalts verwenden. Hier wird ein Inhaltsblock positioniert.

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

Die allgemeinen Stile für einen Inhaltsblock befinden sich jedoch zusammen mit dem Komponentencode.

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

Unsere Eule ist eine interaktive Funktion – eine von vielen in diesem Projekt. Dieses kleine Beispiel zeigt, wie wir die von uns erstellte gemeinsame ViewTimeline verwendet haben.

Unsere Eulenkomponente importiert einen Teil der SVG-Dateien und fügt sie mithilfe des Astro-Fragments inline ein.

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

Und die Stile für die Positionierung der Eule finden sich zusammen mit dem Komponentencode.

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

Es gibt einen zusätzlichen Stil, mit dem das transform-Verhalten für die Eule definiert wird.

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

Die Verwendung von transform-box wirkt sich auf transform-origin aus. Sie richtet sich nach dem Begrenzungsrahmen des Objekts innerhalb der SVG. Die Eule wird von der unteren Mitte aus nach oben skaliert, daher wird transform-origin: 50% 100% verwendet.

Der unterhaltsame Teil ist, wenn wir die Eule mit einer der generierten ViewTimeline verknüpfen:

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 diesem Codeblock machen wir zwei Dinge:

  1. Überprüfen Sie die Bewegungseinstellungen des Nutzers.
  2. Wenn sie keine Präferenz hat, verlinke eine Animation der Eule, um zu scrollen.

Im zweiten Teil wird die Eule auf der Y-Achse mithilfe der Web Animations API animiert. Die einzelne Transformationseigenschaft translate wird verwendet und mit einer ViewTimeline verknüpft. Es ist über die Property timeline mit CHROMETOBER_TIMELINES[1] verknüpft. Dies ist ein ViewTimeline, der beim Umblättern generiert wird. Dadurch wird die Animation der Eule mit dem Umblättern der Seite in der enter-Phase verknüpft. Sie definiert, dass die Eule bewegt wird, wenn die Seite zu 80% gedreht wird. Bei 90 % sollte die Eule mit der Übersetzung fertig sein.

Buchfunktionen

Jetzt haben Sie den Ansatz zum Erstellen einer Seite und die Funktionsweise der Projektarchitektur gesehen. Sie sehen, dass Beitragende damit direkt an einer Seite oder Funktion ihrer Wahl arbeiten können. Bei verschiedenen Elementen des Buches sind die Animationen mit dem Umblättern des Buchs verknüpft, beispielsweise der Schläger, der beim Umblättern hin- und herfliegt.

Außerdem enthält sie Elemente, die durch CSS-Animationen unterstützt werden.

Sobald sich die Inhaltsblöcke im Buch befanden, war Zeit, kreativ mit anderen Funktionen zu werden. Dies bot die Gelegenheit, verschiedene Interaktionen zu generieren und verschiedene Möglichkeiten der Implementierung auszuprobieren.

Responsiv sein

Größe des Buchs und seiner Funktionen für responsive Darstellungsbereiche. Es war jedoch eine interessante Herausforderung, die Schriftarten responsiv zu halten. Container-Abfrageeinheiten sind dafür gut geeignet. Allerdings werden sie noch nicht überall unterstützt. Die Größe des Buchs ist festgelegt, sodass wir keine Container-Abfrage benötigen. Eine Inline-Container-Abfrageeinheit kann mit CSS calc() generiert und für die Schriftgröße verwendet werden.


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

Kürbisse leuchten nachts

Diejenigen, die ein gutes Auge haben, haben vielleicht schon die Verwendung von <source>-Elementen bemerkt, als sie über die Seitenhintergründe gesprochen haben. Una wünschte sich eine Interaktion, die auf bevorzugte Farbschemas reagierte. Daher unterstützen die Hintergründe sowohl den hellen als auch den dunklen Modus mit unterschiedlichen Varianten. Da Sie Medienabfragen mit dem <picture>-Element verwenden können, eignet es sich hervorragend, zwei Hintergrundstile bereitzustellen. Mit dem <source>-Element wird die Farbschemaeinstellung abgefragt und der entsprechende Hintergrund angezeigt.

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

Sie könnten auf Grundlage dieser Farbschema-Präferenz weitere Änderungen vornehmen. Die Kürbis auf Seite zwei reagieren auf das Farbschema der Nutzenden. Das verwendete SVG besteht aus Kreisen, die Flammen darstellen, die sich vergrößern und im dunklen Modus animieren.

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

Sehen Sie sich dieses Porträt an?

Wenn Sie sich Seite 10 ansehen, fällt Ihnen vielleicht etwas auf. Ihr werdet beobachtet! Die Augen des Porträts folgen Ihrem Mauszeiger, während Sie sich auf der Seite bewegen. Der Trick hier besteht darin, die Zeigerposition einem Translate-Wert zuzuordnen und an CSS weiterzugeben.

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

Dieser Code nimmt Eingabe- und Ausgabebereiche und ordnet die angegebenen Werte zu. Diese Verwendung würde beispielsweise den Wert 625 ergeben.

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

Für das Hochformat ist der Eingabewert der Mittelpunkt jedes Auges, zuzüglich oder abzüglich eines Pixelabstands. Der Ausgabebereich gibt an, wie viel die Augen in Pixel übertragen können. Dann wird die Zeigerposition auf der x- oder y-Achse als Wert übergeben. Um beim Bewegen der Augen den Mittelpunkt zu bestimmen, werden die Augen dupliziert. Die Originale bewegen sich nicht, sind transparent und dienen als Referenz.

Anschließend müssen Sie sie miteinander verbinden und die Werte der benutzerdefinierten CSS-Eigenschaften für die Augen aktualisieren, damit sich die Augen bewegen können. Eine Funktion ist an das pointermove-Ereignis gegenüber dem window gebunden. Während dieses Ereignisses ausgelöst wird, werden die Grenzen jedes Auges verwendet, um die Mittelpunkte zu berechnen. Dann wird die Zeigerposition den Werten zugeordnet, die als benutzerdefinierte Eigenschaftswerte für die Augen festgelegt sind.

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

Sobald die Werte an CSS übergeben wurden, können die Stile mit ihnen tun, was sie wollen. Das Tolle dabei ist, dass Sie CSS clamp() verwenden, um das Verhalten für jedes Auge individuell anzupassen. So können Sie dafür sorgen, dass sich jedes Auge anders verhält, ohne den JavaScript-Code noch einmal zu berühren.

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

Zaubersprüche

Sieh dir Seite 6 an. Fühlst du dich fasziniert? Diese Seite zeigt das Design unseres fantastischen magischen Fuchses. Wenn Sie den Zeiger bewegen, sehen Sie möglicherweise einen benutzerdefinierten Effekt für die Cursorspur. Hierbei wird eine Canvas-Animation verwendet. Ein <canvas>-Element befindet sich mit pointer-events: none über dem übrigen Seiteninhalt. Das bedeutet, dass Nutzer weiterhin auf die darunter liegenden Inhaltsblöcke klicken können.

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

Ähnlich wie das Portrait-Objekt auf window hin auf ein pointermove-Ereignis wartet, gilt es auch für das <canvas>-Element. Jedes Mal, wenn das Ereignis ausgelöst wird, erstellen wir jedoch ein Objekt, das auf dem <canvas>-Element animiert wird. Diese Objekte stellen Formen dar, die in der Cursorspur verwendet werden. Sie haben Koordinaten und einen zufälligen Farbton.

Die zuvor erwähnte mapRange-Funktion wird noch einmal verwendet, um das Zeigerdelta size und rate zuzuordnen. Die Objekte werden in einem Array gespeichert, das eine Schleife erhält, wenn die Objekte auf das <canvas>-Element gezeichnet werden. Anhand der Eigenschaften der einzelnen Objekte wird dem <canvas>-Element mitgeteilt, wo die Elemente gezeichnet werden sollen.

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)

Zum Zeichnen auf dem Canvas wird mit requestAnimationFrame eine Schleife erstellt. Die Cursorspur sollte nur gerendert werden, wenn die Seite sichtbar ist. Es gibt eine IntersectionObserver, die aktualisiert und bestimmt, welche Seiten angezeigt werden. Wenn eine Seite zu sehen ist, werden die Objekte auf dem Canvas als Kreise gerendert.

Dann wird eine Schleife über das blocks-Array ausgeführt und jeder Teil des Wegs gezeichnet. Jeder Frame verringert die Größe und ändert die Position des Objekts durch die rate. Dies erzeugt diesen abfallenden und Skalierungseffekt. Wenn das Objekt vollständig kleiner wird, wird es aus dem blocks-Array entfernt.

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

Wenn die Seite nicht mehr angezeigt wird, werden Event-Listener entfernt und die Animations-Frameschleife wird abgebrochen. Das Array blocks wird ebenfalls gelöscht.

Hier ist die Cursorspur in Aktion!

Prüfung der Barrierefreiheit

All diese Inhalte sollen Spaß machen, aber es nutzt nichts, wenn sie Nutzern nicht zur Verfügung stehen. Adams Fachwissen auf diesem Gebiet erwies sich als von unschätzbarem Wert, um Chrometober auf eine Prüfung der Barrierefreiheit vor der Veröffentlichung vorzubereiten.

Hier einige der wichtigsten Bereiche:

  • Sicherstellen, dass der verwendete HTML-Code semantisch war. Dazu gehörten unter anderem geeignete Sehenswürdigkeiten wie <main> für das Buch sowie die Verwendung des <article>-Elements für jeden Inhaltsblock und <abbr>-Elemente, bei denen Akronyme eingeführt wurden. Das Nachdenken über die Entwicklung des Buches machte die Dinge barrierefreier. Die Verwendung von Überschriften und Links erleichtert den Nutzenden die Navigation. Die Verwendung einer Liste für die Seiten bedeutet auch, dass die Anzahl der Seiten durch Hilfstechnologien angekündigt wird.
  • Sie müssen dafür sorgen, dass für alle Bilder die entsprechenden alt-Attribute verwendet werden. Bei Inline-SVGs ist gegebenenfalls das title-Element vorhanden.
  • Verwenden Sie aria-Attribute, um die Nutzerfreundlichkeit zu verbessern. Durch die Verwendung von aria-label für Seiten und ihre Seiten erkennt der Nutzer, auf welcher Seite er sich befindet. Durch die Verwendung von aria-describedBy in den Links "Weitere Informationen" wird der Text des Inhaltsblocks übermittelt. Dadurch werden Unklarheiten darüber beseitigt, wohin der Link die Nutzenden führt.
  • Im Hinblick auf Inhaltsblöcke gibt es die Möglichkeit, auf die gesamte Karte und nicht nur auf den Link „Mehr erfahren“ zu klicken.
  • Die Verwendung von IntersectionObserver, um zu erfassen, welche Seiten zuvor angezeigt wurden. Das hat viele Vorteile, die nicht nur in Bezug auf die Leistung sind. Auf nicht sichtbaren Seiten werden Animationen oder Interaktionen pausiert. Auf diese Seiten wurde aber auch das Attribut inert angewendet. Das bedeutet, dass Nutzer, die einen Screenreader verwenden, dieselben Inhalte wie sehende Nutzer erkunden können. Der Fokus bleibt auf der sichtbaren Seite und Nutzer können mit der Tabulatortaste nicht zu einer anderen Seite wechseln.
  • Nicht zuletzt nutzen wir Medienabfragen, um die Bewegungseinstellung der Nutzenden zu respektieren.

Hier ist ein Screenshot aus dem Bericht, in dem einige der bestehenden Maßnahmen hervorgehoben werden.

als um das gesamte Buch herum gekennzeichnet ist, was darauf hinweist, dass es für Nutzende von assistiven Technologien der wichtigste Orientierungspunkt sein sollte. Mehr ist im Screenshot zu sehen." width="800" size="465">

Screenshot des geöffneten Chrome-Buchs. Grün umrissene Felder rund um verschiedene Aspekte der Benutzeroberfläche enthalten eine Beschreibung der beabsichtigten Bedienungshilfe und der Ergebnisse der User Experience, die die Seite liefern wird. Bilder haben beispielsweise Alt-Text. Ein weiteres Beispiel ist ein Bedienungshilfen-Label, das erklärt, dass Seiten, die nicht sichtbar sind, inaktiv sind. Weitere Informationen sind im Screenshot zu sehen.

Was wir gelernt haben

Die Motivation hinter Chrometober bestand nicht nur darin, Webinhalte aus der Community hervorzuheben, sondern war für uns auch die Möglichkeit, den Polyfill für die durch Scrollen verknüpfte Animations-API zu testen, der sich noch in der Entwicklung befindet.

Wir planen eine Sitzung während unseres Team-Summit in New York, um das Projekt zu testen und aufgetretene Probleme anzugehen. Der Beitrag des Teams war von unschätzbarem Wert. Außerdem war die Gelegenheit groß, alle Punkte aufzuführen, die noch vor der Veröffentlichung geklärt werden mussten.

Das CSS-, UI- und DevTools-Team sitzt in einem Konferenzraum um einen Tisch. Una steht an einem Whiteboard, das mit Haftnotizen bedeckt ist. Andere Teammitglieder sitzen mit Erfrischungen und Laptops an einem Tisch.

Beispielsweise haben wir beim Testen des Buchs auf Geräten ein Rendering-Problem festgestellt. Unser Buch würde auf iOS-Geräten nicht wie erwartet gerendert werden. Die Größe der Darstellungsbereich-Einheiten hat die Größe der Seite. Wenn jedoch eine Aussparung vorhanden war, wirkte sich das auf das Buch aus. Die Lösung bestand darin, viewport-fit=cover im Darstellungsbereich meta zu verwenden:

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

Bei dieser Sitzung haben wir auch Probleme mit der API-Polyfill-Funktion aufgedeckt. Bramus hat diese Probleme im Polyfill-Repository gemeldet. Er fand Lösungen für diese Probleme und ließ sie mit dem Polyfill zusammenführen. Beispielsweise hat diese Pull-Anfrage eine Leistungssteigerung erzielt, indem ein Teil des Polyfills Caching hinzugefügt wurde.

Screenshot einer in Chrome geöffneten Demo Die Entwicklertools sind geöffnet und zeigen eine Referenzleistungsmessung.

Screenshot einer in Chrome geöffneten Demo Die Entwicklertools sind offen und bieten eine verbesserte Leistungsmessung.

Fertig!

Es war ein richtig gutes Projekt, an dem wir arbeiten müssen. Es hat zu einem skurrilen Scrolling-Erlebnis geführt, das tolle Inhalte der Community hervorhebt. Die Funktion eignet sich nicht nur zum Testen von Polyfill, sondern auch zum Senden von Feedback an das Technikteam, um zur Verbesserung des Polyfills beizutragen.

Chrometober 2022 ist beendet.

Wir hoffen, es hat dir gefallen. Was ist Ihre Lieblingsfunktion? Dann schreib mir einen Tweet.

Jhey hält ein Sticker Sheet mit den Figuren aus Chrometober in der Hand.

Vielleicht können Sie sogar Aufkleber von einem Mitglied unseres Teams erhalten, wenn Sie uns bei einer Veranstaltung sehen.

Hero-Foto von David Menidrey auf Unsplash