Jak ta przewijająca się książka ożyła, aby podzielić się w tym roku w ramach Chrometobera przydatnymi i strasznymi poradami i sztuczkami.
Po Designcemberze w tym roku postanowiliśmy zorganizować dla Was Chrometober, aby wyróżnić i udostępnić treści z internetu stworzone przez społeczność i zespół Chrome. W ubiegłym roku w ramach Designcember zaprezentowaliśmy zapytania do kontenera, ale w tym roku pokazujemy interfejs API animacji związanych z przewijaniem w CSS.
Zapoznaj się z przewijaną książką na stronie web.dev/chrometober-2022.
Omówienie
Celem projektu było dostarczenie zabawnego doświadczenia, które prezentuje interfejs API animacji związanych z przewijaniem. Ale mimo że miała być zabawna, musiała też być responsywna i dostępna. Projekt był też świetnym sposobem na przetestowanie interfejsu API polyfill, który jest w trakcie opracowywania. Pozwalał też na wypróbowanie różnych technik i narzędzi w połączeniu. Wszystko w świątecznym klimacie Halloween.
Struktura naszego zespołu wyglądała tak:
- Tyler Reed: ilustracje i projektowanie
- Jhey Tompkins: architekt i kierownik zespołu ds. kreacji
- Una Kravets: kierownik projektu
- Bramus Van Damme: współtwórca witryny
- Adam Argyle: sprawdzenie ułatwień dostępu
- Aaron Forinton: copywriting
Tworzenie wersji roboczej doświadczenia typu scrollytelling
Pomysły na temat Chrometober zaczęły napływać w maju 2022 r. podczas pierwszego spotkania zespołu poza biurem. Kolekcja rysunków skłoniła nas do zastanowienia się nad sposobami, w jakie użytkownik mógłby przewijać storyboard. Zainspirowani grami wideo, rozważaliśmy przewijanie obrazu przez takie sceny jak cmentarze czy nawiedzone domy.
Cieszył mnie fakt, że mogę wykorzystać moją kreatywność i podjąć się nieoczekiwanego projektu w Google. Był to wczesny prototyp sposobu poruszania się użytkownika po treściach.
Gdy użytkownik przewija ekran w poziomie, bloki obracają się i powiększają. Postanowiłem jednak zrezygnować z tego pomysłu, ponieważ nie byłem pewien, jak zapewnić użytkownikom urządzeń o różnych rozmiarach jak najlepsze wrażenia. Zamiast tego zdecydowałem się na projekt, który stworzyłem wcześniej. W 2020 r. miałem szczęście mieć dostęp do ScrollTrigger firmy GreenSock, aby tworzyć wersje demonstracyjne.
Jednym z moich demo było 3D-CSS book, w którym strony przewracały się podczas przewijania. Uznałem, że to znacznie lepiej pasuje do tego, co chcieliśmy osiągnąć w ramach Chrometober. Interfejs API animacji powiązanych z przewijaniem jest idealnym zamiennikiem tej funkcji. Jak zobaczysz, dobrze współpracuje też z scroll-snap
.
Nasz ilustrator, Tyler Reed, świetnie dostosowywał projekt do naszych zmieniających się pomysłów. Tyler świetnie wcielił w życie wszystkie pomysły, które mu rzucono. Wspólna burza mózgów była bardzo owocna. Ważnym elementem tego procesu było podzielenie funkcji na odrębne bloki. Dzięki temu mogliśmy komponować je w sceny, a potem wybierać i decydowac, które z nich wprowadzimy do życia.
Głównym założeniem było to, że użytkownik mógł uzyskać dostęp do bloków treści w miarę czytania książki. Mogą też wchodzić w interakcje z elementami szaleństwa, w tym z tzw. jajkami wielkanocnymi, które wbudziliśmy w użytkowniku. Przykładem może być portret w nawianym domu, którego oczy podążają za kursorem, lub subtelne animacje wywoływane przez zapytania dotyczące multimediów. Te pomysły i funkcje byłyby animowane podczas przewijania. Na początku rozważaliśmy pomysł, aby zając-zombie pojawiał się i przesuwał wzdłuż osi X, gdy użytkownik przewija stronę.
Poznawanie interfejsu API
Zanim zaczęliśmy bawić się poszczególnymi funkcjami i jajkami wielkanocnymi, potrzebowaliśmy książki. Dlatego postanowiliśmy wykorzystać tę okazję do przetestowania funkcji interfejsu CSS API animacji związanych z przewijaniem. Interfejs API animacji powiązanych z przewijaniem nie jest obecnie obsługiwany w żadnej przeglądarce. Podczas tworzenia interfejsu API inżynierowie z zespołu interakcji pracowali jednak nad polyfillem. Dzięki temu możesz testować interfejs API w trakcie jego tworzenia. Oznacza to, że możemy już teraz używać tego interfejsu API. Takie ciekawe projekty są często świetnym miejscem do testowania funkcji eksperymentalnych i przesyłania opinii. W dalszej części tego artykułu znajdziesz więcej informacji o tym, czego się nauczyliśmy i jakiej opinii udzieliliśmy.
Ogólnie rzecz biorąc, możesz użyć tego interfejsu API, aby połączyć animacje z przewijaniem. Pamiętaj, że nie możesz aktywować animacji podczas przewijania – może się to zmienić w przyszłości. Animacje powiązane z przewijaniem również dzielą się na 2 główne kategorie:
- reagują na pozycję przewijania.
- Te, które reagują na pozycję elementu w jego przewijanym kontenerze.
Aby utworzyć tę drugą, używamy ViewTimeline
zastosowanego za pomocą usługi animation-timeline
.
Oto przykład użycia właściwości ViewTimeline
w kodzie 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;
}
}
Tworzymy ViewTimeline
z view-timeline-name
i definiujemy dla niego oś. W tym przykładzie block
odnosi się do logicznego block
. Animacja zostaje powiązana z przewijaniem z usługą animation-timeline
. animation-delay
i animation-end-delay
(w momencie pisania tego artykułu) to definicje faz.
Te fazy określają punkty, w których animacja powinna być powiązana z pozycją elementu w przesuwanym kontenerze. W naszym przykładzie animacja jest uruchamiana, gdy element wchodzi (enter 0%
) do przewijanego kontenera. Zakończenie nastąpi, gdy element zajmie 50% (cover 50%
) przesuwanego kontenera.
Oto nasza prezentacja w akcji:
Możesz też połączyć animację z elementem, który porusza się w widoku. Możesz to zrobić, ustawiając animation-timeline
jako view-timeline
elementu. Jest to przydatne w przypadku animacji list. Działania te są podobne do tych, które wykonujesz, aby animować elementy po wejściu za pomocą 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;
}
}
W tym przypadku „Mover” powiększa się, gdy wchodzi do widocznego obszaru, co powoduje obrót „Spinnera”.
Z doświadczeń wynika, że interfejs API działa bardzo dobrze z przewijaniem z automatycznym dopasowaniem. Przewijanie z dopasowaniem w połączeniu z funkcją ViewTimeline
świetnie sprawdzi się w przypadku przewracania stron w książce.
Tworzenie prototypu mechaniki
Po kilku eksperymentach udało mi się uruchomić prototyp książki. Aby przewracać strony książki, przewijaj w poziomie.
W tym filmie uczymy się tworzyć reguły, a różne reguły są wyróżnione przerywanymi obrzeżeniami.
Oznakowanie wygląda mniej więcej tak:
<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>
Podczas przewijania strony książki się przewracają, ale nie można ich otworzyć ani zamknąć. Zależy to od wyrównania przesunięcia elementów sterujących.
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;
}
Tym razem nie łączymy ViewTimeline
w CSS, ale używamy interfejsu Web Animations API w JavaScript. Dodatkową zaletą jest możliwość przetworzenia zestawu elementów i wygenerowania potrzebnych ViewTimeline
zamiast tworzenia ich ręcznie.
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);
W przypadku każdego wyzwalacza generujemy ViewTimeline
. Następnie za pomocą tego ViewTimeline
animujemy stronę powiązaną z wyzwalaczem. Połączenie animacji strony z przewijaniem. W naszej animacji obracamy element strony wokół osi y, aby ją obrócić. Przesuwamy też samą stronę wzdłuż osi z, aby zachowywała się jak książka.
Podsumowanie
Gdy udało mi się opracować mechanizm książki, mogłam skupić się na stworzeniu ilustracji Tylera.
Astro
W 2021 r. zespół użył Astro w ramach Designcember, a ja chciałam ponownie wykorzystać tę czcionkę w Chrometoberze. W tym projekcie dobrze sprawdza się możliwość dzielenia elementów na komponenty.
Książka jest komponentem. Jest to też zbiór komponentów strony. Każda strona ma 2 strony i tło. Podrzędne strony to komponenty, które można łatwo dodawać, usuwać i umieszczać.
Tworzenie fotoksiążki
Ważne było dla mnie, aby można było łatwo zarządzać blokami. Chciałem też ułatwić reszcie zespołu wnoszenie swoich uwag.
Strony na najwyższym poziomie są definiowane przez tablicę konfiguracji. Każdy obiekt strony w tablicy określa zawartość, tło i inne metadane strony.
const pages = [
{
front: {
marked: true,
content: PageTwo,
backdrop: spreadOne,
darkBackdrop: spreadOneDark
},
back: {
content: PageThree,
backdrop: spreadTwo,
darkBackdrop: spreadTwoDark
},
aria: `page 1`
},
/* Obfuscated page objects */
]
Zostaną one przekazane do komponentu Book
.
<Book pages={pages} />
W komponencie Book
jest stosowany mechanizm przewijania i tworzy się w nim strony książki. Używamy tego samego mechanizmu co w prototypie, ale udostępniamy wiele instancji ViewTimeline
, które są tworzone globalnie.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
Dzięki temu możemy udostępniać osi czasu do użycia w innych miejscach zamiast ich ponownego tworzenia. Więcej informacji na ten temat znajdziesz później.
Skład strony
Każda strona jest elementem listy:
<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>
Zdefiniowana konfiguracja jest przekazywana do każdego wystąpienia Page
. Strony korzystają z funkcji slotu w Astro, aby wstawiać treści na poszczególnych stronach.
<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>
Ten kod służy głównie do konfigurowania struktury. Współtwórcy mogą w większości pracować nad treścią książki bez konieczności dotykania tego kodu.
W tle
Przejście na kreację w postaci książki znacznie ułatwiło podział na sekcje, a każdy rozkład w książce to scena pochodząca z pierwotnego projektu.
Ponieważ zdecydowaliśmy się na format obrazu, tło każdej strony może zawierać element graficzny. Ustawienie szerokości tego elementu na 200% i użycie atrybutu object-position
w zależności od strony powinno rozwiązać problem.
.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;
}
Zawartość strony
Przyjrzyjmy się tworzeniu jednej z takich stron. Strona 3 zawiera sowę, która pojawia się na drzewie.
Jest on wypełniany komponentem PageThree
zgodnie z definicją w konfiguracji. Jest to komponent Astro (PageThree.astro
). Takie komponenty wyglądają jak pliki HTML, ale na górze mają element kodu podobny do frontmatter. Dzięki temu możemy na przykład importować inne komponenty. Komponent na stronie 3 wygląda tak:
---
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>
Ponownie, strony są atomowe. Składają się one z zestawu funkcji. Strona 3 zawiera blok treści i interaktywną sowę, więc dla każdego z nich jest komponent.
Bloki treści to linki do treści widoczne w książce. Te dane są też określane przez obiekt konfiguracji.
{
"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
]
}
Ta konfiguracja jest importowana w przypadku bloków treści. Następnie odpowiednia konfiguracja bloku jest przekazywana do komponentu ContentBlock
.
<ContentBlock {...contentBlocks[3]} id="four" />
Poniżej znajdziesz też przykład użycia komponentu strony jako miejsca na treści. Tutaj umieszczany jest blok treści.
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
Ogólne style bloku treści są jednak zlokalizowane razem z kodem komponentu.
.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%;
}
Sowa to interaktywna funkcja – jedna z wielu w tym projekcie. Ten krótki przykład pokazuje, jak wykorzystaliśmy utworzony przez nas udostępniony widok Oś czasu.
Ogólnie rzecz biorąc, nasza sowa importuje plik SVG i wstawia go w postaci fragmentu za pomocą funkcji Fragment w Astro.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Styli pozycjonowania sowy są zlokalizowane razem z kodem komponentu.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Jest jeden dodatkowy element stylizacji, który określa zachowanie transform
sowy.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
Korzystanie z funkcji transform-box
wpływa na transform-origin
. Jest ona względna względem prostokąta ograniczającego obiekt w pliku SVG. Sowa powiększa się od dołu środka, dlatego używamy transform-origin: 50% 100%
.
Najciekawsza część to ta, w której sowa łączy się z jednym z wygenerowanych ViewTimeline
:
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()
W tym bloku kodu wykonujemy 2 działania:
- Sprawdź preferencje użytkownika dotyczące ruchu.
- Jeśli nie ma preferencji, dodaj animację sowy, która będzie się przewijać.
W drugiej części sowa porusza się wzdłuż osi Y za pomocą interfejsu Web Animations API. Używana jest indywidualna właściwość transformacji translate
, która jest powiązana z jednym elementem ViewTimeline
. Jest ona połączona z usługą CHROMETOBER_TIMELINES[1]
za pomocą usługi timeline
. Jest to ViewTimeline
generowany w przypadku przewracania stron. W ten sposób łączysz animację sowy z przewracaniem strony za pomocą fazy enter
. Określa, że gdy strona jest przewinięta w 80%, sowa zaczyna się poruszać. Na 90% sowa powinna skończyć tłumaczenie.
Funkcje książki
Poznaliśmy już metodę tworzenia strony i architekturę projektu. Możesz zobaczyć, jak pozwala ona współtwórcom przejść do pracy nad wybraną przez nich stroną lub funkcją. Różne elementy książki mają animacje powiązane z przewracaniem stron, np. nietoperz, który pojawia się i znika podczas przewracania stron.
Zawiera też elementy, które działają dzięki animacjom CSS.
Gdy bloki treści znalazły się w książce, nadszedł czas na kreatywne wykorzystanie innych funkcji. Daje to możliwość generowania różnych interakcji i wypróbowywania różnych sposobów implementacji.
Zapewnienie responsywności
Elastyczne widoczne obszary określają rozmiar książki i jej funkcji. Jednak utrzymanie responsywności czcionek było ciekawym wyzwaniem. W takim przypadku dobrze sprawdzą się jednostki zapytania dotyczące kontenera. Nie są one jednak obsługiwane wszędzie. Rozmiar książki jest ustawiony, więc nie potrzebujemy zapytania dotyczącego kontenera. Jednostka zapytania wbudowanego kontenera może być generowana za pomocą CSS calc()
i używana do określania rozmiaru czcionki.
.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);
}
Dynie świecące w nocy
Osoby z dobrą pamięcią mogą pamiętać, że podczas omawiania tła strony używaliśmy elementów <source>
. Una chciała, aby interakcja reagowała na preferencje dotyczące schematu kolorów. W efekcie tła obsługują zarówno tryb jasny, jak i ciemny z różnymi wariantami. Element <picture>
umożliwia korzystanie z zapytań dotyczących multimediów, dzięki czemu możesz podać 2 style tła. Element <source>
wysyła zapytanie o preferowany schemat kolorów i wyświetla odpowiednie tło.
<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>
Możesz wprowadzić inne zmiany na podstawie preferowanego schematu kolorów. Dynie na stronie 2 reagują na preferowany schemat kolorów użytkownika. Użyty plik SVG zawiera koła przedstawiające płomienie, które powiększają się i animują w ciemnym trybie.
.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;
}
}
}
Czy ten portret na Ciebie patrzy?
Jeśli sprawdzisz stronę 10, możesz coś zauważyć. Jesteś obserwowany! Gdy będziesz poruszać kursorem po stronie, oczy portretu będą podążać za nim. Chodzi o to, aby zmapować położenie wskaźnika na wartość tłumaczenia i przekazać ją do CSS.
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)
}
Ten kod przyjmuje zakresy wejściowe i wyjściowe oraz mapuje podane wartości. W tym przykładzie wartość wynosi 625.
mapRange(0, 100, 250, 1000, 50) // 625
W przypadku portretu wartością wejściową jest punkt środkowy każdego oka z dodanym lub odjętym pewną liczbą pikseli. Zakres wyjściowy to liczba pikseli, na które oczy mogą przetłumaczyć obraz. Następnie jako wartość przekazywana jest pozycja wskaźnika na osi X lub Y. Aby uzyskać punkt środkowy oczu podczas ich przemieszczania, oczy są powielane. Oryginały nie poruszają się, są przezroczyste i służą do celów informacyjnych.
Następnie należy połączyć te elementy i zaktualizować wartości właściwości niestandardowych CSS w oczach, aby mogły się poruszać. Funkcja jest powiązana z wydarzeniem pointermove
w przypadku window
. Gdy to zdarzenie zostanie wywołane, do obliczenia punktów środkowych zostaną użyte granice każdego oka. Następnie pozycja wskaźnika jest mapowana na wartości ustawione jako wartości właściwości niestandardowych oczu.
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)
})
}
Gdy wartości zostaną przekazane do CSS, style mogą z nimi robić, co chcą. Najważniejsze jest tu użycie CSS clamp()
, aby zmienić zachowanie każdego oka. Dzięki temu możesz zmienić zachowanie każdego oka bez konieczności ponownego korzystania z JavaScriptu.
.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);
}
rzucanie zaklęć;
Czy po przeczytaniu strony 6 czujesz się zaczarowany? Ta strona wykorzystuje projekt naszej fantastycznej magicznej lisicy. Jeśli przesuniesz kursor, możesz zobaczyć niestandardowy efekt ścieżki kursora. Ta animacja wykorzystuje animację na płótnie. Element <canvas>
znajduje się nad resztą treści strony z atrybutem pointer-events: none
. Oznacza to, że użytkownicy nadal mogą klikać bloki treści znajdujące się pod reklamą.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Podobnie jak w przypadku elementu pointermove
, element <canvas>
reaguje na zdarzenie pointermove
w komponencie window
. Jednak za każdym razem, gdy zdarzenie zostanie uruchomione, tworzymy obiekt, który ma być animowany w elemencie <canvas>
. Te obiekty reprezentują kształty używane w śladzie kursora. Mają współrzędne i losowy odcień.
Funkcja mapRange
jest używana ponownie, ponieważ możemy jej użyć do zmapowania delty wskaźnika na size
i rate
. Obiekty są przechowywane w tablicy, która jest iterowana, gdy obiekty są rysowane w elemencie <canvas>
. Właściwości każdego obiektu określają, gdzie element <canvas>
powinien narysować elementy.
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)
Aby rysować na płótnie, pętla jest tworzona za pomocą funkcji requestAnimationFrame
. Ślad kursora powinien być renderowany tylko wtedy, gdy strona jest widoczna. Mamy IntersectionObserver
, który aktualizuje i określa, które strony są widoczne. Jeśli strona jest widoczna, obiekty są renderowane jako koła na obszarze roboczym.
Następnie wykonujemy pętlę po tablicy blocks
i rysujemy każdą część ścieżki. Każda klatka zmniejsza rozmiar i zmienia pozycję obiektu o rate
. Daje to efekt spadania i zmiany rozmiaru. Jeśli obiekt całkowicie się skurczy, zostanie usunięty z tablicy blocks
.
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)
}
Jeśli strona zniknie z pola widzenia, detektory zdarzeń zostaną usunięte, a pętla ramki animacji zostanie anulowana. Tablica blocks
również jest wyczyszczona.
Oto ślad kursora w akcji.
Weryfikacja ułatwień dostępu
Fajnie jest tworzyć ciekawe treści, ale nie ma sensu, jeśli użytkownicy nie mogą ich znaleźć. Adam ma ogromną wiedzę w tej dziedzinie, co okazało się bezcenne w przygotowaniu Chrometobera do sprawdzenia ułatwień dostępu przed jego udostępnieniem.
Oto niektóre z nich:
- Upewnij się, że użyty kod HTML jest semantyczny. Dotyczyło to takich elementów jak odpowiednie elementy orientacyjne, np.
<main>
w książce, użycie elementu<article>
w każdym bloku treści oraz elementów<abbr>
, w których pojawiają się akronimy. Przewidywanie przyszłości podczas tworzenia książki ułatwiło mi tworzenie treści. Użycie nagłówków i linków ułatwia użytkownikowi poruszanie się po stronie. Użycie listy stron oznacza też, że liczba stron jest ogłaszana przez technologię ułatwień dostępu. - Upewnij się, że wszystkie obrazy używają odpowiednich atrybutów
alt
. W przypadku wstawianych plików SVG elementtitle
jest obecny w miejscach, w których jest potrzebny. - Używanie atrybutów
aria
, które polepszają działanie aplikacji. Użyciearia-label
dla stron i ich stron bocznych informuje użytkownika, na której stronie się znajduje. Użyciearia-describedBy
w linkach „Pokaż więcej” informuje o treści bloku treści. Dzięki temu użytkownik nie będzie miał wątpliwości, dokąd prowadzi link. - Jeśli chodzi o bloki treści, użytkownicy mogą kliknąć całą kartę, a nie tylko link „Czytaj więcej”.
- Korzystanie z elementu
IntersectionObserver
do śledzenia, które strony są widoczne, zostało omówione wcześniej. Ma to wiele zalet, które nie są związane tylko z wydajnością. Animacje i interakcje na stronach, które nie są widoczne, są wstrzymywane. Te strony mają też zastosowany atrybutinert
. Oznacza to, że użytkownicy korzystający z czytnika ekranu mogą przeglądać te same treści co użytkownicy widzący. Punkt skupienia pozostaje na stronie, która jest widoczna, a użytkownicy nie mogą przejść na inną stronę. - Na koniec warto wspomnieć, że korzystamy z zapytań dotyczących multimediów, aby uwzględnić preferencje użytkownika dotyczące animacji.
Oto zrzut ekranu z sprawdzaniem, który pokazuje niektóre zastosowane środki.
element jest zidentyfikowany jako obejmujący całą książkę, co oznacza, że powinien być głównym punktem orientacyjnym dla użytkowników technologii wspomagających. Więcej informacji znajdziesz na zrzucie ekranu." width="800" height="465">
Czego się nauczyliśmy?
Motywacją do zorganizowania Chrometobera było nie tylko wyróżnienie treści internetowych od społeczności, ale też przetestowanie interfejsu API polyfill do animacji związanych z przewijaniem, który jest w trakcie tworzenia.
Podczas szczytu naszego zespołu w Nowym Jorku poświęciliśmy sesję na przetestowanie projektu i rozwiązanie powstałych problemów. Ich wkład był nieoceniony. Była to też świetna okazja, aby wymienić wszystkie kwestie, które trzeba było rozwiązać, zanim mogliśmy wdrożyć usługę.
Na przykład testowanie książki na urządzeniach spowodowało problem z renderowaniem. Na urządzeniach z iOS nasza książka nie była renderowana zgodnie z oczekiwaniami. Jednostki widocznego obszaru określają rozmiar strony, ale gdy występuje wycięcie, wpływa ono na książkę. Rozwiązaniem było użycie viewport-fit=cover
w widocznym obszarze meta
:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
W ramach tej sesji poruszono też kilka problemów z polyfillem interfejsu API. Bramus zgłosił te problemy w repozytorium polyfill. Następnie znalazł rozwiązania tych problemów i złączył je z polyfill. Na przykład ta prośba o wybieranie zwiększyła wydajność dzięki dodaniu do części polyfill pamięci podręcznej.
Znakomicie.
Praca nad tym projektem była naprawdę przyjemna. Wynikiem jest zabawna animacja, która wyróżnia niesamowite treści od społeczności. Poza tym świetnie nadaje się do testowania polyfilla i do przesyłania opinii zespołowi programistów, który może wykorzystać te informacje do ulepszania polyfilla.
Chrometober 2022 dobiegł końca.
Mamy nadzieję, że Ci się podobało. Jaka jest Twoja ulubiona funkcja? Tweetuj do mnie i daj nam znać.
Jeśli spotkasz nas na wydarzeniu, możesz nawet otrzymać kilka naklejek od członka zespołu.
Zdjęcie główne: David Menidrey, Unsplash