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. Aber die Website musste nicht nur skurril, sondern auch responsiv 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:
- Tyler Reed: Illustration und Design
- Jhey Tompkins: Leiterin für Architektur und Design
- Una Kravets: Projektleitung
- Bramus Van Damme: Mitwirkender an der Website
- Adam Argyle: Überprüfung der Barrierefreiheit
- Aaron Forinton: Copywriting
Scrollytelling-Entwurf erstellen
Die Ideen für Chrometober kamen bereits bei unserem ersten Team-Offsite im Mai 2022 auf. Eine Sammlung von Scribble-Entwürfen brachte uns auf Ideen, wie sich Nutzer durch eine Art Storyboard scrollen könnten. Inspiriert von Videospielen haben wir uns überlegt, wie es wäre, durch Szenen wie Friedhöfe und ein Spukhaus zu scrollen.
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 aber dazu entschieden, diese Idee aufzugeben, da ich mir nicht sicher war, wie wir diese Funktion für Nutzer auf Geräten aller Größen optimieren könnten. 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 scroll-linked animations 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 sehr gut darin, das Design anzupassen, wenn sich unsere Ideen änderten. 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.
Die Hauptidee war, dass Nutzer beim Durchblättern des Buchs auf Inhaltsblöcke 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 frühe Idee war ein Zombie-Kaninchen, das beim Scrollen des Nutzers aufsteigt und sich entlang der X-Achse bewegt.
Die API kennenlernen
Bevor wir mit einzelnen Funktionen und Ostereiern spielen konnten, brauchten wir ein Buch. Wir haben uns daher entschieden, die Gelegenheit zu nutzen, um die Funktionen der neuen CSS-API für scrollbezogene Animationen zu testen. Die API für scrollgekoppelte 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. Weitere Informationen zu den Erkenntnissen und dem Feedback, das wir geben konnten, finden Sie weiter unten im 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:
- Anzeigen, die auf die Scrollposition reagieren.
- 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 logische block
. Die Animation wird mit der Property animation-timeline
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 soll die Animation gestartet werden, wenn das Element (enter 0%
) den scrollbaren Container betritt. 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. Das ist beispielsweise für Listenanimationen geeignet. Das Verhalten ähnelt dem, wie Sie Elemente beim Eingeben mit IntersectionObserver
animieren können.
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.
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 einigen Experimenten konnte ich einen funktionierenden Buchprototyp erstellen. 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>
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 das 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 2021 Astro für den Designcember 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 untergeordneten Elemente einer Seitenseite sind Komponenten, die ganz einfach 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 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 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.
Seitengestaltung
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 berühren.
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.
Da wir uns für ein Seitenverhältnis für das Buch entschieden hatten, konnte der Hintergrund jeder Seite ein Bildelement haben. Legen Sie die Breite dieses Elements auf 200% fest und verwenden Sie 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
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 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. 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>
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" />
Außerdem sehen Sie hier ein Beispiel dafür, wie wir die Komponente der Seite als Platz für die Platzierung 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.
Unsere Eulenkomponente importiert auf einer allgemeinen Ebene ein SVG und fügt es mit dem Fragment von Astro 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 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. Dadurch wird es relativ zum Begrenzungsrahmen des Objekts im SVG. Die Eule wird von unten in der Mitte nach oben skaliert, 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 tun wir zwei Dinge:
- Prüfen Sie die Bewegungseinstellungen des Nutzers.
- 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.
Für eine gute Reaktionsfähigkeit sorgen
Mit responsiven Darstellungsbereichseinheiten wird die Größe des Buchs und seiner Funktionen festgelegt. 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, ist es eine gute Möglichkeit, zwei Hintergrundstile bereitzustellen. Das Element <source>
fragt nach der Farbschema-Einstellung 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>
Anhand dieser Farbschema-Einstellung können Sie weitere Änderungen vornehmen. Die Kürbisse auf Seite 2 reagieren auf die Farbschema-Einstellungen eines Nutzers. 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;
}
}
}
Sieht mich dieses 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
Beim Porträt ist der Eingabewert der Mittelpunkt jedes Auges, zuzüglich oder abzüglich einer bestimmten Pixeldistanz. 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 daran ist, dass Sie mit CSS clamp()
das Verhalten für jedes Auge unterschiedlich gestalten können, sodass sich jedes Auge anders verhalten kann, ohne dass Sie das JavaScript noch einmal bearbeiten 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 Cursor bewegen, sehen Sie möglicherweise einen benutzerdefinierten Cursor-Spureffekt. 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 der einzelnen Objekte geben unserem <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. 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 im Blickfeld ist, werden die Objekte als Kreise auf dem Canvas gerendert.
Wir durchlaufen dann das Array blocks
und zeichnen jeden Teil des Pfades. Mit jedem Frame wird die Größe des Objekts reduziert und seine Position um den Wert von rate
geändert. Das erzeugt den Effekt, dass die Bilder fallen und sich verkleinern. 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 sichtbar 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 ist zwar schön, eine unterhaltsame Umgebung zu schaffen, aber es ist nicht gut, wenn sie für Nutzer nicht zugänglich ist. Adams Fachwissen in diesem Bereich war unerlässlich, um Chrometober vor der Veröffentlichung auf eine Prüfung der Barrierefreiheit vorzubereiten.
Zu den wichtigsten Bereichen gehören:
- Achten Sie darauf, dass das verwendete HTML semantisch ist. Dazu gehörten unter anderem 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. - Achten Sie darauf, dass für alle Bilder die entsprechenden
alt
-Attribute verwendet werden. Bei Inline-SVGs ist das Elementtitle
gegebenenfalls vorhanden. aria
-Attribute verwenden, wenn sie die Nutzerfreundlichkeit verbessern Die Verwendung vonaria-label
für Seiten und ihre Seitenteile informiert den Nutzer darüber, auf welcher Seite er sich befindet. Durch die Verwendung vonaria-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
IntersectionObserver
, um zu verfolgen, welche Seiten angezeigt werden, wurde bereits erwähnt. Das hat viele Vorteile, die nicht nur mit der Leistung zusammenhängen. Animationen oder Interaktionen auf nicht sichtbaren Seiten werden pausiert. Auf diesen Seiten ist jedoch auch das Attributinert
angewendet. Das bedeutet, dass Nutzer mit einem Screenreader dieselben Inhalte wie sehbehinderte Nutzer sehen können. Der Fokus bleibt auf der Seite, die gerade angezeigt wird, 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 Überprüfung, in dem einige der Maßnahmen hervorgehoben sind.
Element wird als „um das gesamte Buch herum“ angegeben, was bedeutet, dass es die Hauptmarkierung für Nutzer von Hilfstechnologien sein sollte. Weitere Informationen finden Sie im Screenshot.“ width="800" height="465">
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 Scroll-bezogene Animations-API testen.
Während unseres Team-Summits in New York haben wir eine Sitzung eingeplant, 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 der Veröffentlichung angegangen 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. 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 anschließend Lösungen für diese Probleme und ließ sie in die Polyfill einfließen. Beispielsweise wurde durch diesen Pull-Request die Leistung verbessert, indem einem Teil der polyfill ein Caching hinzugefügt wurde.
Geschafft!
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. Welche Funktion gefällt dir am besten? Schreib mir einfach einen Tweet.
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