Wie das Scrollbuch zum Leben erweckt wurde, um diesen Chrometober lustige und gruselige Tipps und Tricks zu teilen.
Nach dem Designcember wollten wir dieses Jahr den Chrometober für Sie ins Leben rufen, um Webinhalte aus der Community und vom Chrome-Team hervorzuheben und zu teilen. Im Rahmen von Designcember wurde die Verwendung von Containerabfragen vorgestellt. Dieses Jahr präsentieren wir die CSS Scroll-Linked Animations API.
Sehen Sie sich das Scrollbuch unter web.dev/chrometober-2022 an.
Übersicht
Ziel des Projekts war es, eine originelle Website zu erstellen, die die Scroll-Linked Animations API hervorhebt. Doch trotz aller Verspieltheit musste die Website auch responsiv und barrierefrei sein. Das Projekt war auch eine gute Möglichkeit, das API-Polyfill zu testen, das sich in der aktiven Entwicklung befindet, und verschiedene Techniken und Tools in Kombination auszuprobieren. Und das alles im schaurig-schönen Halloween-Design!
Unsere Teamstruktur sah so aus:
- Tyler Reed: Illustration und Design
- Jhey Tompkins: Architectural and Creative Lead
- Una Kravets: Projektleitung
- Bramus Van Damme: Website-Mitwirkender
- Adam Argyle: Überprüfung der Barrierefreiheit im Internet
- Aaron Forinton: Copywriting
Scrollytelling-Erlebnis entwerfen
Die Ideen für Chrometober entstanden bei unserem ersten Team-Offsite im Mai 2022. Eine Sammlung von Scribbles brachte uns auf die Idee, dass Nutzer sich durch eine Art Storyboard scrollen könnten. Inspiriert von Videospielen haben wir uns für ein Scroll-Erlebnis durch Szenen wie Friedhöfe und ein Spukhaus entschieden.

Es war spannend, die kreative Freiheit zu haben, mein erstes Google-Projekt in eine unerwartete Richtung zu lenken. Dies war ein früher Prototyp, der zeigt, wie ein Nutzer durch die Inhalte navigieren könnte.
Wenn der Nutzer seitwärts scrollt, drehen sich die Blöcke und werden vergrößert. Ich habe mich jedoch dagegen entschieden, weil ich mir Sorgen gemacht habe, wie wir diese Funktion für Nutzer auf Geräten aller Größen optimal gestalten können. Stattdessen habe ich mich am Design von etwas orientiert, das ich in der Vergangenheit erstellt hatte. 2020 hatte ich das Glück, Zugriff auf ScrollTrigger von GreenSock zu haben, um Release-Demos zu erstellen.
Eine der von mir erstellten Demos war ein 3D-CSS-Buch, bei dem sich die Seiten beim Scrollen umblätterten. Das schien mir für Chrometober viel besser geeignet. Die Scroll-Linked Animations API ist ein perfekter Ersatz für diese Funktion. Es funktioniert auch gut mit scroll-snap, wie Sie sehen werden.
Unser Illustrator für das Projekt, Tyler Reed, hat das Design immer wieder angepasst, wenn wir unsere Ideen geändert haben. Tyler hat die vielen kreativen Ideen, die ihm vorgeschlagen wurden, fantastisch umgesetzt. Es hat sehr viel Spaß gemacht, gemeinsam Ideen zu sammeln. Ein wichtiger Teil der Funktionsweise bestand darin, die Funktionen in isolierte Blöcke aufzuteilen. So konnten wir sie zu Szenen zusammenstellen und dann auswählen, was wir zum Leben erwecken wollten.

Die Grundidee war, dass Nutzer beim Lesen des Buchs auf Inhaltsblöcke zugreifen können. Sie konnten auch mit verspielten Elementen interagieren, darunter die Easter Eggs, die wir in die Website eingebaut hatten, z. B. ein Porträt in einem Spukhaus, dessen Augen dem Mauszeiger folgten, oder subtile Animationen, die durch Medienabfragen ausgelöst wurden. Diese Ideen und Funktionen werden beim Scrollen animiert. Eine frühe Idee war ein Zombie-Hase, der sich beim Scrollen auf der x-Achse bewegt.
Mit der API vertraut machen
Bevor wir mit einzelnen Funktionen und Easter Eggs experimentieren konnten, brauchten wir ein Buch. Wir haben uns daher entschieden, die Gelegenheit zu nutzen, um die Funktionen der neuen CSS Scroll-Linked Animations API zu testen. Die Scroll-Linked Animations API wird derzeit in keinem Browser unterstützt. Während der Entwicklung der API haben die Entwickler im Interaktionsteam jedoch an einem Polyfill gearbeitet. So lässt sich die Form der API während der Entwicklung testen. Wir könnten diese API also heute schon verwenden. Solche Projekte sind oft eine gute Gelegenheit, experimentelle Funktionen auszuprobieren und Feedback zu geben. Weiter unten in diesem Artikel erfahren Sie, was wir gelernt haben und welches Feedback wir geben konnten.
Grob gesagt können Sie mit dieser API Animationen mit dem Scrollen verknüpfen. Es ist wichtig zu beachten, dass Sie eine Animation nicht durch Scrollen auslösen können. Das ist etwas, das später kommen könnte. Scroll-basierte Animationen lassen sich ebenfalls in zwei Hauptkategorien unterteilen:
- Anzeigen, die auf die Scrollposition reagieren.
- Diejenigen, die auf die Position eines Elements in seinem Scrollcontainer reagieren.
Letzteres erstellen wir mit einem ViewTimeline, das über ein animation-timeline-Attribut 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 ein ViewTimeline-Objekt mit view-timeline-name und definieren die Achse dafür. In diesem Beispiel bezieht sich block auf logisches block. Die Animation wird mit der Eigenschaft animation-timeline mit dem Scrollen verknüpft. animation-delay und animation-end-delay (zum Zeitpunkt der Erstellung dieses Artikels) sind die Phasen, die wir definieren.
Diese Phasen definieren die Punkte, an denen die Animation in Bezug auf die Position eines Elements in seinem Scrollcontainer verknüpft werden soll. In unserem Beispiel soll die Animation gestartet werden, wenn das Element in den Scrollcontainer eintritt (enter 0%). und enden, wenn es 50 % (cover 50%) des Scrollcontainers abdeckt.
Hier ist unsere Demo in Aktion:
Sie können auch eine Animation mit dem Element verknüpfen, das sich im Darstellungsbereich bewegt. Dazu legen Sie für animation-timeline die view-timeline des Elements fest. Das ist für Szenarien wie Listenanimationen nützlich. Das Verhalten ähnelt dem Animieren von Elementen beim Einblenden 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, wenn es in den Darstellungsbereich gelangt, und die Drehung von „Spinner“ wird ausgelöst.
Bei meinen Tests habe ich festgestellt, dass die API sehr gut mit Scroll Snap funktioniert. Scroll Snap in Kombination mit ViewTimeline eignet sich hervorragend für das Umblättern von Seiten in einem Buch.
Prototyping der Mechanik
Nach einigem Experimentieren konnte ich einen Buchprototyp zum Laufen bringen. Sie scrollen horizontal, um die Seiten des Buchs umzublättern.
In der Demo sind die verschiedenen Trigger mit gestrichelten Rahmen hervorgehoben.
Das Markup sieht in etwa 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>
Wenn Sie scrollen, werden die Seiten des Buchs umgeblättert, aber sie werden geöffnet oder geschlossen. Dies 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. Das hat den zusätzlichen Vorteil, dass wir eine Reihe von Elementen durchlaufen und die benötigten ViewTimeline generieren können, anstatt sie einzeln 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 animieren wir die mit dem Trigger verknüpfte Seite mit diesem ViewTimeline. 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 umzublättern. Außerdem übersetzen 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 ausgearbeitet hatte, konnte ich mich darauf konzentrieren, Tylers Illustrationen zum Leben zu erwecken.
Astro
Das Team hatte 2021 Astro für Designcember verwendet und ich wollte es unbedingt noch einmal für Chrometober verwenden. Die Möglichkeit, Dinge in Komponenten aufzuteilen, ist für dieses Projekt gut geeignet.
Das Buch selbst ist eine Komponente. Es ist auch eine Sammlung von Seitenkomponenten. Jede Seite hat zwei Seiten und sie haben Hintergründe. Die untergeordneten Elemente einer Seite sind Komponenten, die sich leicht hinzufügen, entfernen und positionieren lassen.
Buch erstellen
Mir war es wichtig, dass die Blöcke einfach zu verwalten sind. Außerdem wollte ich es dem Rest des Teams leicht machen, Beiträge zu leisten.
Die Seiten auf übergeordneter Ebene werden durch ein Konfigurations-Array 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 Komponente Book wird der Scrollmechanismus angewendet und die Seiten des Buchs werden erstellt. Es wird derselbe Mechanismus wie beim Prototyp verwendet, aber wir teilen mehrere Instanzen von ViewTimeline, die global erstellt werden.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
So können wir die Zeitachsen an anderer Stelle verwenden, anstatt sie neu zu erstellen. Weitere Informationen zu diesem Thema folgen später.
Seitenaufbau
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 in die einzelnen Seiten 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 am Inhalt des Buchs arbeiten, ohne diesen Code bearbeiten zu müssen.
Backdrop-Anzeigen
Die kreative Umstellung auf ein Buch hat die Aufteilung der Abschnitte erheblich erleichtert. Jede Doppelseite des Buchs ist eine Szene aus dem ursprünglichen Design.

Da wir uns für ein Seitenverhältnis für das Buch entschieden hatten, konnte der Hintergrund für jede Seite ein Bildelement haben. Wenn Sie die Breite dieses Elements auf 200 % 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 3 ist eine Eule zu sehen, die in einem Baum auftaucht.
Sie 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 aber oben einen Code-Fence, der Frontmatter ähnelt. 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 bestehen aus einer Sammlung von Features. Seite 3 enthält einen Inhaltsblock und die interaktive Eule, also gibt es für beide eine Komponente.
Inhaltsblöcke sind die Links zu Inhalten, die im Buch zu sehen sind. Diese werden ebenfalls durch ein 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. Anschließend wird die relevante Blockkonfiguration an die Komponente ContentBlock übergeben.
<ContentBlock {...contentBlocks[3]} id="four" />
Außerdem finden Sie hier ein Beispiel dafür, wie wir die Komponente der Seite als Ort für die 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 am selben Ort wie der 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 ein interaktives Element – eines von vielen in diesem Projekt. Dies ist ein gutes kleines Beispiel, das zeigt, wie wir die von uns erstellte freigegebene ViewTimeline verwendet haben.
Auf übergeordneter Ebene importiert unsere Eulenkomponente einige SVGs und fügt sie mithilfe des Astro-Fragments inline ein.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Die Stile für die Positionierung der Eule befinden sich im Komponentencode.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Es gibt ein zusätzliches Styling-Element, 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. Dadurch wird sie relativ zum Begrenzungsrahmen des Objekts innerhalb der SVG-Datei festgelegt. Die Eule wird von der Mitte unten aus vergrößert, daher wird transform-origin: 50% 100% verwendet.
Jetzt kommt der spannende Teil: Wir verknüpfen die Eule mit einem unserer generierten ViewTimelines:
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 tun wir zwei Dinge:
- Prüfe die Bewegungseinstellungen des Nutzers.
- Wenn sie keine Präferenz haben, 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 Transformations-Property translate wird verwendet und ist mit einer ViewTimeline verknüpft. Es ist über das Attribut timeline mit CHROMETOBER_TIMELINES[1] verknüpft. Dies ist ein ViewTimeline, der für das Umblättern generiert wird. Dadurch wird die Animation der Eule mit dem Umblättern der Seite verknüpft. Dazu wird die Phase enter verwendet. Sie legt fest, dass die Eule sich bewegen soll, wenn die Seite zu 80 % umgeblättert ist. Bei 90 % sollte die Eule die Übersetzung beenden.
Buchfunktionen
Sie haben nun den Ansatz zum Erstellen einer Seite und die Funktionsweise der Projektarchitektur kennengelernt. Sie können sehen, wie Mitwirkende sich einbringen 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 ins Bild und wieder heraus fliegt.
Außerdem enthält sie Elemente, die auf CSS-Animationen basieren.
Nachdem die Inhaltsblöcke im Buch waren, war es an der Zeit, mit anderen Funktionen kreativ zu werden. So konnten wir verschiedene Interaktionen generieren und unterschiedliche Implementierungsmethoden ausprobieren.
Reaktionsschnelligkeit
Responsive Darstellungsbereichseinheiten bestimmen die Größe des Buchs und seiner Funktionen. Die Schriftarten responsiv zu halten, war jedoch eine interessante Herausforderung. Containerabfrageeinheiten sind hier eine gute Wahl. Sie werden jedoch noch nicht überall unterstützt. Die Größe des Buchs ist festgelegt, daher benötigen wir keine Containerabfrage. 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
Aufmerksame Leser haben vielleicht bemerkt, dass wir bei der Erörterung der Seitenhintergründe <source>-Elemente verwendet haben. Una wollte eine Interaktion, die auf die Farbschema-Präferenz reagiert. Daher unterstützen die Hintergründe sowohl den hellen als auch den dunklen Modus mit verschiedenen Varianten. Da Sie Medienabfragen mit dem <picture>-Element verwenden können, ist es eine gute Möglichkeit, zwei Hintergrundstile bereitzustellen. Das Element <source> fragt die Farbschema-Präferenz ab 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önnten basierend auf dieser Farbschema-Präferenz weitere Änderungen einführen. Die Kürbisse auf Seite 2 reagieren auf das bevorzugte Farbschema des Nutzers. Die verwendete SVG-Datei enthält Kreise, die Flammen darstellen. Diese werden im dunklen Modus vergrößert und animiert.
.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;
}
}
}
Beobachtet dich dieses Porträt?
Wenn Sie sich Seite 10 ansehen, fällt Ihnen vielleicht etwas auf. Du wirst beobachtet! Die Augen des Porträts folgen dem Mauszeiger, wenn Sie sich auf der Seite bewegen. Der Trick besteht darin, die Position des Mauszeigers einem Übersetzungswert zuzuordnen und diesen an CSS zu übergeben.
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 verwendet Eingabe- und Ausgabebereiche und ordnet die angegebenen Werte zu. In diesem Beispiel würde der Wert 625 zurückgegeben.
mapRange(0, 100, 250, 1000, 50) // 625
Für das Porträt ist der Eingabewert der Mittelpunkt jedes Auges, plus oder minus einer bestimmten Pixelentfernung. Der Ausgabebereich gibt an, wie weit sich die Augen in Pixeln bewegen können. Die Position des Zeigers auf der x- oder y-Achse wird dann als Wert übergeben. Um den Mittelpunkt der Augen beim Bewegen zu ermitteln, werden die Augen dupliziert. Die Originale werden nicht verschoben, sind transparent und dienen als Referenz.
Dann müssen Sie nur noch alles zusammenfügen und die Werte der benutzerdefinierten CSS-Eigenschaften für die Augen aktualisieren, damit sie sich bewegen können. Eine Funktion ist an das pointermove-Ereignis für window gebunden. Wenn dies ausgelöst wird, werden die Grenzen jedes Auges verwendet, um die Mittelpunkte zu berechnen. Anschließend wird die Position des Zeigers 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 damit machen, was sie wollen. Das Tolle daran ist, dass Sie mit CSS-clamp() das Verhalten für jedes Auge unterschiedlich gestalten können, ohne das JavaScript noch einmal anzufassen.
.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
Wenn Sie sich Seite 6 ansehen, sind Sie dann fasziniert? Diese Seite ist dem Design unseres fantastischen magischen Fuchses gewidmet. Wenn Sie den Mauszeiger bewegen, sehen Sie möglicherweise einen benutzerdefinierten Cursor-Trail-Effekt. Dabei wird eine Canvas-Animation verwendet. Ein <canvas>-Element befindet sich mit pointer-events: none über dem restlichen Seiteninhalt. Nutzer können also weiterhin auf die darunter liegenden Inhaltsblöcke klicken.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Ähnlich wie unser Hochformat 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 in der Cursor-Spur verwendet werden. Sie haben Koordinaten und einen zufälligen Farbton.
Wir verwenden wieder die Funktion mapRange von vorhin, um das Zeiger-Delta auf size und rate abzubilden. Die Objekte werden in einem Array gespeichert, das beim Zeichnen der Objekte im <canvas>-Element durchlaufen wird. Die Eigenschaften der einzelnen Objekte geben dem <canvas>-Element an, 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 Mausspur sollte nur gerendert werden, wenn die Seite sichtbar ist. Wir haben einen IntersectionObserver, der aktualisiert und bestimmt, welche Seiten sichtbar sind. Wenn eine Seite sichtbar ist, werden die Objekte als Kreise im Arbeitsbereich gerendert.
Anschließend durchlaufen wir das Array blocks und zeichnen jeden Teil des Pfads. In jedem Frame wird die Größe des Objekts um den rate reduziert und seine Position geändert. Dadurch entsteht der Fall- und Skalierungseffekt. Wenn das Objekt vollständig verkleinert wird, wird es aus dem Array blocks 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 sichtbar ist, werden die Event-Listener entfernt und die Animationsframe-Schleife wird abgebrochen. Das blocks-Array wird ebenfalls gelöscht.
Hier sehen Sie die Cursor-Spur in Aktion.
Überprüfung der Barrierefreiheit im Internet
Es ist zwar schön, eine unterhaltsame Umgebung zum Erkunden zu schaffen, aber das bringt nichts, wenn sie für Nutzer nicht zugänglich ist. Adams Fachwissen in diesem Bereich war von unschätzbarem Wert, um Chrometober vor der Veröffentlichung auf die Barrierefreiheit im Internet vorzubereiten.
Einige der wichtigsten Themenbereiche:
- Sicherstellen, dass das verwendete HTML semantisch war. Dazu gehörten geeignete Landmark-Elemente 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 das vorausschauende Denken bei der Erstellung des Buches wurde es barrierefreier. Die Verwendung von Überschriften und Links erleichtert Nutzern die Navigation. Durch die Verwendung einer Liste für die Seiten wird die Anzahl der Seiten auch von Hilfstechnologien angesagt. - Achten Sie darauf, dass alle Bilder die entsprechenden
alt-Attribute verwenden. Bei Inline-SVGs ist dastitle-Element vorhanden, wo es erforderlich ist. - Verwenden Sie
aria-Attribute, wenn sie die Nutzererfahrung verbessern. Die Verwendung vonaria-labelfür Seiten und deren Seiten kommuniziert dem Nutzer, auf welcher Seite er sich befindet. Durch die Verwendung vonaria-describedByfür die „Mehr lesen“-Links wird der Text des Inhaltsblocks vermittelt. So ist klar, wohin der Link führt. - Bei Inhaltsblöcken kann die gesamte Karte angeklickt werden, nicht nur der Link „Mehr lesen“.
- Wir haben bereits erwähnt, dass ein
IntersectionObserververwendet wird, um zu verfolgen, welche Seiten gerade angezeigt werden. Das hat viele Vorteile, die nicht nur mit der Leistung zu tun haben. Bei Seiten, die nicht sichtbar sind, werden Animationen oder Interaktionen pausiert. Auf diese Seiten wird aber auch das Attributinertangewendet. So können Nutzer, die einen Screenreader verwenden, dieselben Inhalte wie sehende Nutzer erkunden. Der Fokus bleibt auf der angezeigten Seite und Nutzer können nicht zu einer anderen Seite wechseln. - Zu guter Letzt verwenden wir Medienabfragen, um die Präferenz eines Nutzers für Bewegungen zu berücksichtigen.
Hier ist ein Screenshot aus der Überprüfung, auf dem einige der Maßnahmen zu sehen sind.
-Element wird als das gesamte Buch umfassend identifiziert, was darauf hinweist, dass es die Hauptmarkierung für Nutzer von Hilfstechnologien sein sollte. More is outlined in the screenshot." width="800" height="465">

Was wir gelernt haben
Die Motivation hinter Chrometober war nicht nur, Webinhalte aus der Community hervorzuheben, sondern auch, das in der Entwicklung befindliche Polyfill für die Scroll-Linked Animations API zu testen.
Während unseres Team-Summits in New York haben wir uns Zeit genommen, um das Projekt zu testen und aufgetretene Probleme zu beheben. Der Beitrag des Teams war von unschätzbarem Wert. Außerdem war es eine gute Gelegenheit, alle Dinge aufzulisten, die vor dem Start noch erledigt werden mussten.

Beim Testen des Buchs auf Geräten ist beispielsweise ein Rendering-Problem aufgetreten. Unser Buch wurde auf iOS-Geräten nicht wie erwartet gerendert. Die Seite wird anhand von Darstellungsbereichseinheiten dimensioniert. Wenn jedoch eine Notch 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" />
In dieser Sitzung wurden auch einige Probleme mit dem API-Polyfill angesprochen. Bramus hat diese Probleme im Polyfill-Repository angesprochen. Anschließend fand er Lösungen für diese Probleme und fügte sie dem Polyfill hinzu. Beispielsweise wurde mit dieser Pull-Anfrage eine Leistungssteigerung erzielt, indem einem Teil des Polyfills Caching hinzugefügt wurde.


Geschafft!
Die Arbeit an diesem Projekt hat wirklich Spaß gemacht. Das Ergebnis ist ein originelles Scroll-Erlebnis, bei dem tolle Inhalte aus der Community präsentiert werden. Außerdem war es eine gute Möglichkeit, den Polyfill zu testen und dem Entwicklungsteam Feedback zu geben, um den Polyfill zu verbessern.
Der Chrometober 2022 ist vorbei.
Wir hoffen, es hat dir gefallen. Welche Funktion gefällt dir am besten? Schreib mir auf X und lass es uns wissen.

Wenn du uns bei einer Veranstaltung triffst, kannst du dir vielleicht sogar ein paar Sticker von einem Teammitglied holen.