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:
- 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: Prüfung der Barrierefreiheit
- Aaron Forinton: Copywriting
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.
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.
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:
- Elemente, 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 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.
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:
- 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.
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 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
s 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 Attributinert
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">
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.
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.
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.
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