Chrometober wird erstellt!

Wie das scrollbare Buch zum Leben erweckt wurde, um anlässlich des Chrometobers lustige und gruselige Tipps und Tricks zu teilen.

Nach dem Designcember wollten wir dieses Jahr Chrometober für Sie einrichten, um Webinhalte aus der Community und dem Chrome-Team zu präsentieren und zu teilen. Im Designcember wurden Containerabfragen vorgestellt. Dieses Jahr zeigen wir die CSS-API für scrollbezogene Animationen.

Das Scrollbuch können Sie unter web.dev/chrometober-2022 ausprobieren.

Übersicht

Ziel des Projekts war es, eine skurrile Umgebung zu schaffen, in der die API für scrollbasierte Animationen hervorgehoben wird. Die Erfahrung sollte gleichzeitig skurril, aber auch reaktionsschnell und barrierefrei sein. Das Projekt war auch eine gute Möglichkeit, die API-Polyfill zu testen, die sich derzeit in aktiver Entwicklung befindet. Außerdem konnten wir verschiedene Techniken und Tools in Kombination ausprobieren. Und das alles im gruseligen Halloween-Design!

Unsere Teamstruktur sah so aus:

Scrollytelling-Entwurf erstellen

Im Mai 2022 entstand die Idee für Chrometober bei unserem ersten externen Team. Eine Sammlung von Scribble-Skizzen brachte uns auf Ideen, wie sich Nutzer durch eine Art Storyboard scrollen könnten. Inspiriert von Videospielen erfuhren wir davon, wie man durch Szenen wie Friedhöfe und ein Spukhaus scrollt.

Auf einem Schreibtisch liegt ein Notizbuch mit verschiedenen Skizzen und Notizen zum Projekt.

Es war aufregend, die kreative Freiheit zu haben, mein erstes Google-Projekt in eine unerwartete Richtung zu lenken. Dies war ein früher Prototyp dafür, wie sich Nutzer durch die Inhalte bewegen könnten.

Wenn der Nutzer seitlich scrollt, drehen und vergrößern sich die Blöcke. Ich habe mich jedoch dazu entschlossen, mich von diesem Konzept zu lösen, da ich mich nicht mehr darüber beschäftigt habe, wie wir dieses Erlebnis für Nutzer auf Geräten jeder Größe optimieren können. Stattdessen habe ich mich für das Design eines Designs entschieden, das ich in der Vergangenheit entworfen hatte. 2020 hatte ich das Glück, Zugriff auf den ScrollTrigger von GreenSock zu haben, um Release-Demos zu erstellen.

Eine der Demos, die ich erstellt hatte, war ein 3D-CSS-Buch, bei dem sich die Seiten beim Scrollen umblätterten. Das passte viel besser zu dem, was wir uns für Chrometober vorgestellt hatten. Die API für scrollbezogene Animationen ist eine perfekte Alternative für diese Funktion. Wie Sie sehen, funktioniert es auch gut mit scroll-snap.

Unser Illustrator für das Projekt, Tyler Reed, war großartig darin, das Design nach unseren Ideen zu verändern. Tyler hat fantastische Arbeit geleistet, indem er alle kreativen Ideen, die ihm entgegengebracht wurden, in die Tat umgesetzt hat. Es hat viel Spaß gemacht, gemeinsam Ideen zu sammeln. Ein wichtiger Teil unserer Vision war es, die Funktionen in einzelne Blöcke aufzuteilen. So konnten wir sie in Szenen zusammenstellen und dann auswählen, was wir zum Leben erwecken wollten.

Eine der Kompositionsszenen mit einer Schlange, einem Sarg mit herausgestreckten Armen, einem Fuchs mit einem Zauberstab an einem Kessel, einem Baum mit gruseligem Gesicht und einem Wasserspeier, der eine Kürbislaterne hält.

Die Hauptidee war, dass Nutzer beim Durchblättern des Buchs auf Inhaltsblöcken zugreifen können. Sie konnten auch mit den kleinen Details interagieren, einschließlich der Easter Eggs, die wir in die Umgebung eingebaut hatten, z. B. ein Porträt in einem Spukhaus, dessen Augen den Mauszeiger verfolgten, oder subtile Animationen, die durch Medienabfragen ausgelöst wurden. Diese Ideen und Funktionen werden beim Scrollen animiert. Eine erste Idee war ein Zombie-Hase, der sich beim Scrollen entlang der X-Achse erhebt und sich bewegt.

Die API kennenlernen

Bevor wir mit einzelnen Funktionen und Ostereiern spielen konnten, brauchten wir ein Buch. Deshalb haben wir beschlossen, die Gelegenheit zu nutzen, um die Funktionen der neuen CSS-API für scrollbezogene Animationen zu testen. Die API für scrollbasierte Animationen wird derzeit in keinem Browser unterstützt. Während der Entwicklung der API haben die Entwickler des Interaktionsteams jedoch an einer Polyfill gearbeitet. So können Sie die Form der API während der Entwicklung testen. Das bedeutet, dass wir diese API bereits heute verwenden können. Solche spannenden Projekte sind oft eine gute Gelegenheit, experimentelle Funktionen auszuprobieren und Feedback dazu zu geben. Was wir gelernt haben und welches Feedback wir geben konnten, erfahren Sie weiter unten in diesem Artikel.

Im Allgemeinen können Sie mit dieser API Animationen mit dem Scrollen verknüpfen. Beachten Sie, dass Sie eine Animation nicht beim Scrollen auslösen können. Das ist etwas, das später kommen könnte. Scroll-bezogene Animationen lassen sich ebenfalls in zwei Hauptkategorien unterteilen:

  1. Elemente, die auf die Scrollposition reagieren.
  2. Sie reagieren auf die Position eines Elements in seinem scrollbaren Container.

Für Letzteres verwenden wir ein ViewTimeline, das über eine animation-timeline-Eigenschaft angewendet wird.

Hier ein Beispiel für die Verwendung von 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;
 }
}

Wir erstellen eine ViewTimeline mit view-timeline-name und definieren die Achse dafür. In diesem Beispiel bezieht sich block auf das logische block. Die Animation wird mit dem Attribut animation-timeline zum Scrollen verknüpft. Wir definieren Phasen derzeit als animation-delay und animation-end-delay.

Diese Phasen definieren die Punkte, an denen die Animation in Bezug auf die Position eines Elements in seinem scrollbaren Container verknüpft werden soll. In unserem Beispiel geht es darum, die Animation zu starten, wenn das Element in den scrollbaren Container eintritt (enter 0%). und endet, wenn sie 50% (cover 50%) des scrollbaren Containers bedeckt.

Hier ist unsere Demo in Aktion:

Sie können auch eine Animation mit dem Element verknüpfen, das sich im Darstellungsbereich bewegt. Legen Sie dazu animation-timeline als view-timeline des Elements fest. Dies ist gut für Szenarien wie Listenanimationen. Die Funktionsweise ist ähnlich wie die Animation von Elementen nach dem Aufrufen 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;
  }
}

Dadurch wird „Mover“ vergrößert, sobald er den Darstellungsbereich betritt, was die Drehung von „Spinner“ auslöst.

Die Tests haben gezeigt, dass die API sehr gut mit scroll-snap funktioniert. Scroll-Snap in Kombination mit ViewTimeline eignet sich hervorragend, um Seiten in einem Buch umzublättern.

Prototyping der Mechanik

Nach ein paar Experimenten gelang es mir, einen Buchprototyp zum Laufen zu bringen. Sie scrollen horizontal, um die Seiten des Buchs umzublättern.

In der Demo sehen Sie, wie die verschiedenen Trigger mit gestrichelten Rändern hervorgehoben sind.

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>

Beim Scrollen werden die Seiten des Buchs umgeblättert, aber nicht geöffnet oder geschlossen. Das hängt von der Scroll-Snap-Ausrichtung 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 verbinden wir die ViewTimeline nicht in CSS, sondern verwenden die Web Animations API in JavaScript. Dies hat den zusätzlichen Vorteil, dass wir eine Reihe von Elementen durchlaufen und die benötigten ViewTimeline generieren können, anstatt sie einzeln von Hand 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 eine ViewTimeline generiert. Anschließend animieren wir die zugehörige Seite des Trigger mit diesem ViewTimeline. Dadurch wird die Animation der Seite mit dem Scrollen verknüpft. Für unsere Animation drehen wir ein Element der Seite um die Y-Achse, um die Seite umzublättern. Außerdem verschieben wir die Seite selbst entlang der Z‑Achse, damit sie sich wie ein Buch verhält.

Zusammenfassung

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

Astro

Das Team hat Astro für den Designcember 2021 verwendet und ich wollte es für Chrometober wieder verwenden. Die Möglichkeit, Dinge in Komponenten aufzuteilen, eignet sich gut für dieses Projekt.

Das Buch selbst ist eine Komponente. Außerdem ist es eine Sammlung von Seitenkomponenten. Jede Seite hat zwei Seiten und Hintergründe. Die einer Seite untergeordneten Komponenten sind Komponenten, die problemlos hinzugefügt, entfernt und positioniert werden können.

Buch erstellen

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

Die Seiten auf oberster 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 Book-Komponente übergeben.

<Book pages={pages} />

Auf die Komponente „Book“ wird der Scrollmechanismus angewendet und die Seiten des Buchs werden erstellt. Es wird derselbe Mechanismus wie im Prototyp verwendet, aber wir teilen mehrere Instanzen von ViewTimeline, die global erstellt werden.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

So können wir die Zeitleisten für andere Zwecke freigeben, 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 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 Einrichten der Struktur. Mitwirkende können größtenteils an den Inhalten des Buchs arbeiten, ohne diesen Code zu bearbeiten.

Backdrop-Anzeigen

Die kreative Umstellung auf ein Buch hat die Aufteilung der Abschnitte wesentlich erleichtert. Jede Doppelseite des Buches ist eine Szene aus dem ursprünglichen Design.

Doppelseitige Illustration aus dem Buch mit einem Apfelbaum auf einem Friedhof. Auf dem Friedhof befinden sich mehrere Grabsteine und vor einem großen Mond ist eine Fledermaus am Himmel zu sehen.

Da wir ein Seitenverhältnis für das Buch festgelegt hatten, konnte der Hintergrund jeder Seite ein Bildelement enthalten. Wenn Sie dieses Element auf 200% der Breite festlegen und object-position basierend auf der Seite verwenden, funktioniert es.

.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

Sehen wir uns an, wie eine der Seiten erstellt wird. Auf Seite drei ist eine Eule zu sehen, die in einem Baum auftaucht.

Sie wird mit einer PageThree-Komponente ausgefüllt, wie in der Konfiguration definiert. Es ist eine Astro-Komponente (PageThree.astro). Diese Komponenten sehen aus wie HTML-Dateien, haben aber oben einen Code-Abstand, ähnlich wie im Frontmatter. Dadurch 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>

Seiten sind also atomar. Sie bestehen aus einer Reihe von Funktionen. Auf Seite 3 gibt es einen Inhaltsblock und die interaktive Eule, also eine Komponente für jede.

Inhaltsbalken sind die Links zu Inhalten im Buch. Auch diese werden 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 importiert, wenn Inhaltsblöcke erforderlich sind. Die entsprechende Blockkonfiguration wird dann an die ContentBlock-Komponente übergeben.

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

Es gibt auch ein Beispiel dafür, wie wir die Komponente der Seite als Ort zur Positionierung der Inhalte 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%;
}

Die Eule ist eine interaktive Funktion – eine von vielen in diesem Projekt. Dies ist ein schönes kleines Beispiel, das zeigt, wie wir die von uns erstellte ViewTimeline verwendet haben.

Auf übergeordneter Ebene importiert unsere Eulen-Komponente eine SVG-Datei und Inline-Elemente mit dem Fragment von Astro.

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

Die Stile für die Positionierung der Eule befinden sich zusammen mit dem Komponentencode.

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

Es gibt ein zusätzliches Stilelement, das das transform-Verhalten für die Eule definiert.

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

Die Verwendung von transform-box wirkt sich auf die transform-origin aus. Sie wird relativ zum Begrenzungsrahmen des Objekts in der SVG-Datei erstellt. Die Eule skaliert von der unteren Mitte nach oben, daher wird transform-origin: 50% 100% verwendet.

Der lustige Teil kommt, wenn wir die Eule mit einer unserer 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. Prüfen Sie die Bewegungseinstellungen des Nutzers.
  2. Wenn er keine Präferenz hat, verknüpfen Sie eine Animation der Eule mit dem Scrollen.

Im zweiten Teil wird die Eule mithilfe der Web Animations API auf der y-Achse animiert. Die individuelle Transformationseigenschaft translate wird verwendet und mit einer ViewTimeline verknüpft. Sie ist über das Attribut timeline mit CHROMETOBER_TIMELINES[1] verknüpft. Dies ist ein ViewTimeline, das für die Seitenumblätterungen generiert wird. Dadurch wird die Animation der Eule über die Phase enter mit dem Seitenwechsel verknüpft. Hier wird festgelegt, dass die Eule bewegt werden soll, wenn die Seite zu 80% gedreht ist. Bei 90 % sollte die Eule die Übersetzung abgeschlossen haben.

Buchfunktionen

Sie haben nun den Ansatz zum Erstellen einer Seite und die Funktionsweise der Projektarchitektur kennengelernt. Sie sehen, wie Mitwirkende sofort loslegen und an einer Seite oder Funktion ihrer Wahl arbeiten können. Die Animationen verschiedener Elemente im Buch sind mit dem Umblättern der Seiten verknüpft, z. B. die Fledermaus, die beim Umblättern der Seiten ein- und ausfliegt.

Außerdem enthält es Elemente, die mit CSS-Animationen erstellt wurden.

Nachdem die Inhaltsblöcke im Buch waren, konnte ich mich mit anderen Funktionen kreativ austoben. So hatten wir die Möglichkeit, verschiedene Interaktionen zu entwickeln und verschiedene Implementierungsmethoden auszuprobieren.

Immer auf dem neuesten Stand

Responsive Darstellungsbereiche passen die Größe des Buchs und seiner Funktionen an. Die Schriftarten responsiv zu gestalten, war jedoch eine interessante Herausforderung. Containerabfrageeinheiten eignen sich hier gut. Sie werden jedoch noch nicht überall unterstützt. Die Größe des Buchs ist festgelegt, sodass keine Containerabfrage erforderlich ist. Eine Inline-Containerabfrageeinheit 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

Bei genauerem Hinsehen haben Sie vielleicht bemerkt, dass bei den Hintergrundbildern der Seiten <source>-Elemente verwendet wurden. Una wünschte sich eine Interaktion, die auf die Farbschema-Einstellungen reagiert. Daher unterstützen die Hintergründe sowohl den hellen als auch den dunklen Modus mit verschiedenen Varianten. Da Sie mit dem <picture>-Element Mediaabfragen verwenden können, eignet es sich hervorragend, um zwei Hintergrundstile bereitzustellen. Das <source>-Element fragt nach Farbschema-Präferenzen und zeigt den entsprechenden Hintergrund an.

<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önnen anhand dieser Farbschemaeinstellung weitere Änderungen vornehmen. Die Kürbisse auf Seite zwei reagieren auf die Farbschema-Einstellungen der Nutzenden. Die verwendete SVG-Datei enthält Kreise, die Flammen darstellen, die im Dunkelmodus vergrößert und animiert werden.

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

Siehst du dich von diesem Porträt an?

Wenn Sie sich Seite 10 ansehen, fällt Ihnen vielleicht etwas auf. Du wirst beobachtet! Die Augen des Porträts folgen dem Cursor, wenn Sie sich auf der Seite bewegen. Der Trick besteht darin, die Position des Cursors einem Wert für „translate“ 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 an und ordnet die angegebenen Werte zu. In diesem Fall würde der Wert beispielsweise 625 lauten.

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

Für das Porträt ist der Eingabewert der Mittelpunkt jedes Auges plus oder minus einer Pixelentfernung. Der Ausgabebereich gibt an, wie viel die Augen in Pixeln umsetzen können. Die Position des Cursors auf der X‑ oder Y‑Achse wird dann als Wert übergeben. Um den Mittelpunkt der Augen zu erhalten, während sie bewegt werden, werden die Augen dupliziert. Die Originale bewegen sich nicht, sind transparent und dienen als Referenz.

Anschließend müssen Sie die Werte der benutzerdefinierten CSS-Eigenschaften für die Augen aktualisieren, damit sie sich bewegen können. Eine Funktion ist mit dem pointermove-Ereignis gegen window verknüpft. Wenn dieser Trigger ausgelöst wird, werden die Begrenzungen der einzelnen Augen verwendet, um die Mittelpunkte zu berechnen. Die Position des Mauszeigers wird dann auf Werte zugeordnet, die als benutzerdefinierte Property-Werte 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 damit machen, was sie wollen. Das Tolle dabei ist, CSS clamp() zu verwenden, um das Verhalten für jedes Auge unterschiedlich zu gestalten. So können Sie jedes Auge anders verhalten, ohne den JavaScript-Code erneut berühren zu müssen.

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

Zauber wirken

Sind Sie von Seite 6 verzaubert? Auf dieser Seite sehen Sie das Design unseres fantastischen magischen Fuchses. Wenn Sie den Zeiger bewegen, sehen Sie möglicherweise einen benutzerdefinierten Cursor-Spur-Effekt. Dazu wird die Canvas-Animation verwendet. Ein <canvas>-Element befindet sich mit pointer-events: none über dem Rest des Seiteninhalts. Nutzer können also weiterhin auf die Inhaltsblöcke darunter klicken.

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

Ähnlich wie unser Porträt auf ein pointermove-Ereignis auf window wartet, wartet auch unser <canvas>-Element darauf. Jedes Mal, wenn das Ereignis ausgelöst wird, erstellen wir jedoch ein Objekt, das auf dem <canvas>-Element animiert werden soll. Diese Objekte stellen Formen dar, die im Cursorverlauf verwendet werden. Sie haben Koordinaten und eine zufällige Farbe.

Die Funktion mapRange wird noch einmal verwendet, da wir damit das Delta des Zeigers auf size und rate abbilden können. Die Objekte werden in einem Array gespeichert, das durchlaufen wird, wenn die Objekte in das <canvas>-Element gezeichnet werden. Die Eigenschaften jedes Objekts teilen unserem <canvas>-Element mit, wo die gezeichneten Elemente zu zeichnen sind.

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 im Canvas wird mit requestAnimationFrame eine Schleife erstellt. Der Cursorverlauf sollte nur dann gerendert werden, wenn die Seite im Blickfeld ist. Wir haben eine IntersectionObserver, die aktualisiert und bestimmt, welche Seiten sichtbar sind. Wenn eine Seite sichtbar ist, werden die Objekte auf dem Canvas als Kreise gerendert.

Anschließend führen wir eine Schleife über das blocks-Array aus und zeichnen jeden Teil des Wegs. Mit jedem Frame wird die Größe und Position des Objekts um rate reduziert. Dies erzeugt diesen Fall- und Skalierungseffekt. Wenn das Objekt vollständig schrumpft, 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 im Blickfeld ist, werden Ereignis-Listener entfernt und die Animation wird abgebrochen. Das blocks-Array wird ebenfalls gelöscht.

Hier sehen Sie den Cursorverlauf in Aktion.

Prüfung der Barrierefreiheit

Es macht zwar Spaß, die Umgebung zu erkunden, aber es bringt nichts, wenn sie nicht für Nutzende zugänglich sind. Adams Fachwissen in diesem Bereich war unverzichtbar, um Chrometober vor der Veröffentlichung auf eine Prüfung der Barrierefreiheit vorzubereiten.

Einige der erwähnenswerten Bereiche:

  • Sicherstellen, dass das verwendete HTML semantisch ist Dazu gehörten geeignete Markierungselemente wie <main> für das Buch, die Verwendung des <article>-Elements für jeden Inhaltsblock und <abbr>-Elemente, in denen Akronyme eingeführt werden. Durch vorausschauendes Denken beim Erstellen des Buches konnten wir es barrierefreier gestalten. Die Verwendung von Überschriften und Links erleichtert die Navigation für Nutzer. Wenn Sie eine Liste für die Seiten verwenden, wird die Anzahl der Seiten auch von Hilfstechnologien angesagt.
  • Achte darauf, dass bei allen Bildern die richtigen alt-Attribute verwendet werden. Bei Inline-SVGs ist das Element title gegebenenfalls vorhanden.
  • aria-Attribute verwenden, wenn sie die Nutzerfreundlichkeit verbessern Die Verwendung von aria-label für Seiten und ihre Seitenteile informiert den Nutzer darüber, auf welcher Seite er sich befindet. Durch die Verwendung von aria-describedBy in den „Mehr erfahren“-Links wird der Text des Inhaltsblocks vermittelt. So ist klar, wohin der Link den Nutzer führt.
  • Bei Inhaltsblöcken können Nutzer nicht nur auf den Link „Weiterlesen“ klicken, sondern auch auf die gesamte Karte.
  • Die Verwendung eines IntersectionObservers zum Erfassen, welche Seiten aufgerufen werden, wurde bereits früher verwendet. Das hat viele Vorteile, die nicht nur mit der Leistung zusammenhängen. Auf Seiten, die nicht angezeigt werden, werden Animationen oder Interaktionen pausiert. Auf diesen Seiten ist jedoch auch das Attribut inert angewendet. Das bedeutet, dass Nutzer mit einem Screenreader dieselben Inhalte wie sehfähige Nutzer sehen können. Der Fokus bleibt dabei auf der angezeigten Seite und Nutzer können nicht mit der Tabulatortaste zu einer anderen Seite wechseln.
  • Und zu guter Letzt: Wir verwenden Media-Abfragen, um die Einstellungen der Nutzer für Bewegungen zu berücksichtigen.

Hier ist ein Screenshot aus der Rezension, in dem einige der Maßnahmen hervorgehoben sind.

wird angegeben, dass es sich um das gesamte Buch handelt, d. h., es sollte die wichtigste Orientierungshilfe für Nutzer von Hilfstechnologien sein. Weitere Informationen finden Sie im Screenshot.“ width="800" height="465">

Screenshot des geöffneten Chrometober-Buchs Verschiedene Aspekte der Benutzeroberfläche sind grün umrandet. Darin werden die beabsichtigten Funktionen zur Barrierefreiheit und die Nutzerfreundlichkeit der Seite beschrieben. Bilder haben beispielsweise Alt-Text. Ein weiteres Beispiel ist ein Label zur Barrierefreiheit, das angibt, dass nicht sichtbare Seiten inaktiv sind. Weitere Informationen finden Sie im Screenshot.

Was wir gelernt haben

Mit Chrometober wollten wir nicht nur Webinhalte aus der Community präsentieren, sondern auch die in der Entwicklung befindliche polyfill für die scrollbasierte Animations-API testen.

Wir haben eine Sitzung bei unserem Team-Summit in New York abgehalten, um das Projekt zu testen und aufgetretene Probleme anzugehen. Der Beitrag des Teams war von unschätzbarem Wert. Es war auch eine großartige Gelegenheit, alle Punkte aufzulisten, die erledigt werden mussten, bevor wir online gehen konnten.

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

Beim Testen des Buchs auf Geräten ist beispielsweise ein Rendering-Problem aufgetreten. Unser Buch würde auf iOS-Geräten nicht wie erwartet gerendert werden. Mit Darstellungsbereichseinheiten wird die Größe der Seite festgelegt. Wenn jedoch eine Kerbe vorhanden war, wirkte sich das auf das Buch aus. Die Lösung bestand darin, viewport-fit=cover im meta-Darstellungsbereich zu verwenden:

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

In dieser Sitzung wurden auch einige Probleme mit der API-Polyfill angesprochen. Bramus hat diese Probleme im polyfill-Repository gemeldet. Er fand später Lösungen für diese Probleme und ließ sie in die Polyfill einfließen. Diese Pull-Anfrage führte beispielsweise zu einer Leistungssteigerung, da einem Teil des Polyfills Caching hinzugefügt wurde.

Screenshot einer Demo, die in Chrome geöffnet ist Die Entwicklertools sind geöffnet und zeigen eine Leistungsmessung für den Referenzwert an.

Screenshot einer Demo, die in Chrome geöffnet ist Die Entwicklertools sind offen und zeigen eine verbesserte Leistungsmessung.

Fertig!

An diesem Projekt zu arbeiten, hat wirklich Spaß gemacht. Das Ergebnis ist ein spielerisches Scrollen, bei dem tolle Inhalte aus der Community hervorgehoben werden. Außerdem eignet sich das Tool hervorragend zum Testen der Polyfill und zum Geben von Feedback an das Entwicklerteam, um die Polyfill zu verbessern.

Chrometober 2022 ist vorbei.

Wir hoffen, es hat Ihnen gefallen. Was ist deine Lieblingsfunktion? Schreib mir einfach einen Tweet.

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

Wenn du uns auf einer Veranstaltung triffst, kannst du dir vielleicht sogar Sticker von einem Mitglied des Teams sichern.

Hero-Foto von David Menidrey auf Unsplash