איך הספר הנעשה נולד כדי לשתף טיפים וטריקים מצחיקים ומפחידים במהלך חודש Chrometober.
אחרי Designcember, רצינו ליצור את Chrometober השנה כדי להבליט ולשתף תוכן אינטרנט מהקהילה ומצוות Chrome. ב-Designcember הוצג השימוש בשאילתות של מאגרים, אבל השנה אנחנו מציגים את ממשק ה-API של אנימציות מקושרות לגלילה ב-CSS.
אפשר לבדוק את חוויית הקריאה בספרים בweb.dev/chrometober-2022.
סקירה כללית
מטרת הפרויקט הייתה לספק חוויה שובבה שמציגה את ממשק ה-API של האנימציות המקושרות לגלילה. עם זאת, בנוסף למראה השובב, החוויה צריכה להיות גם רספונסיבית ונגישה. הפרויקט גם שימש כדרך מצוינת לבדוק את ה-polyfill של ה-API שנמצא בפיתוח פעיל, וגם לנסות שילובים של שיטות וכלים שונים. והכול בנושא חגיגי של האלווין!
מבנה הצוות שלנו נראה כך:
- Tyler Reed: איור ועיצוב
- Jhey Tompkins: מנהלת אדריכלית ויצירתית
- Una Kravets: מנהלת הפרויקט
- Bramus Van Damme: שותף תוכן באתר
- אדם ארגייל (Adam Argyle): בדיקת נגישות
- Aaron Forinton: קופירייטינג
כתיבת טיוטה של סיפור 'גלילה וסיפור'
הרעיונות ל-Chrometober החלו לזרום במהלך הפגישה הראשונה של הצוות מחוץ למשרדים, במאי 2022. אוסף של שרבוטים גרם לנו לחשוב על דרכים שבהן משתמש יוכל לגלול בדרך כלשהי של סטוריבורד. בהשראת משחקי וידאו, הצענו חוויית גלילה דרך סצנות כמו בתי קברות ובתים רדופים.
היה מרגש לקבל את החופש היצירתי להוביל את הפרויקט הראשון שלי ב-Google לכיוון בלתי צפוי. זה היה אב טיפוס מוקדם של האופן שבו משתמש יכול לנווט בתוכן.
כשהמשתמש גולל לצדדים, הבלוק מסתובב ומתכווץ. אבל החלטתי לוותר על הרעיון הזה כי לא ידעתי איך נוכל להפוך את החוויה הזו למושלמת למשתמשים במכשירים בכל הגדלים. במקום זאת, העדפתי לעצב משהו שיצרתי בעבר. בשנת 2020, הייתה לי הזדמנות לקבל גישה ל-ScrollTrigger של GreenSock כדי ליצור הדגמות של גרסאות זמינות.
אחת מהדמואים שיצרתי הייתה ספר ב-3D-CSS שבו הדפים נפתחים בזמן הגלילה, וזה הרגיש הרבה יותר מתאים למה שרצינו ל-Chrometober. ממשק ה-API של אנימציות מקושרות לגלילה הוא תחליף מושלם לפונקציונליות הזו. הוא עובד טוב גם עם scroll-snap
, כפי שאפשר לראות.
המאייר שלנו בפרויקט, טיילר ריד (Tyler Reed), הצליח לשנות את העיצוב בהתאם לשינויים ברעיונות שלנו. טיילר עשה עבודה נהדרת בהפיכת כל הרעיונות היצירתיים שהוצעו לו למציאות. נהניתי מאוד מהסיעור מוחות המשותף. חלק גדול מהאופן שבו רצינו שהדבר יפעל היה לפצל את התכונות לקטעים מבודדים. כך יכולנו ליצור מהן סצנות ולבחור מה להציג.
הרעיון המרכזי היה שככל שהמשתמש יתקדם בספר, הוא יוכל לגשת לקטעי תוכן. הם גם יכלו לקיים אינטראקציה עם פריטים קטנים ומצחיקים, כולל הביצים החבויות שכללנו בחוויה. לדוגמה, דיוקן בבית רדוף שמעיניו עוקבות אחרי הסמן שלכם, או אנימציות עדינות שמופעלות על ידי שאילתות מדיה. הרעיונות והתכונות האלה יוצגו באנימציה בזמן הגלילה. אחת מהרעיונות הראשונים הייתה ארנב זומבי שיופיע ויזוז לאורך ציר ה-x כשהמשתמש יגולל.
היכרות עם ה-API
לפני שיכולנו להתחיל לשחק עם תכונות ספציפיות וארטיפקטים חבויים, היינו צריכים ספר. לכן החלטנו לנצל את ההזדמנות הזו כדי לבדוק את קבוצת המאפיינים של CSS API לאנימציות שמקושרות לגלילה, שאנחנו מפתחים. כרגע אין תמיכה ב-API של אנימציות מקושרות לגלילה באף דפדפן. עם זאת, במהלך הפיתוח של ממשק ה-API, המהנדסים בצוות האינטראקציות עבדו על polyfill. כך אפשר לבדוק את הצורה של ה-API במהלך הפיתוח שלו. כלומר, אנחנו יכולים להשתמש ב-API הזה כבר היום, ופרויקטים מהנים כמו זה הם לרוב מקום מצוין לנסות תכונות ניסיוניות ולשלוח משוב. בהמשך המאמר מוסבר מה למדנו ומה המשוב שיכולנו לספק.
באופן כללי, אפשר להשתמש ב-API הזה כדי לקשר אנימציות לגלילה. חשוב לציין שאי אפשר להפעיל אנימציה בזמן גלילה – יכול להיות שזו תהיה תכונה שנוספה בהמשך. גם אנימציות שמקושרות לגלילה נכללות בשתי קטגוריות עיקריות:
- אלה שמגיבים למיקום הגלילה.
- אלה שמגיבים למיקום של אלמנט במארז הגלילה שלו.
כדי ליצור את האפשרות השנייה, אנחנו משתמשים ב-ViewTimeline
שמוחל באמצעות נכס animation-timeline
.
כך נראה השימוש ב-ViewTimeline
ב-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;
}
}
יוצרים ViewTimeline
עם view-timeline-name
ומגדירים את הציר שלו. בדוגמה הזו, block
מתייחס ל-block
לוגי. האנימציה מקושרת לגלילה באמצעות המאפיין animation-timeline
. animation-delay
ו-animation-end-delay
(בזמן כתיבת המאמר) הם האופן שבו אנחנו מגדירים שלבים.
השלבים האלה מגדירים את הנקודות שבהן צריך לקשר את ההנפשה ביחס למיקום של רכיב במאגר הגלילה שלו. בדוגמה שלנו, אנחנו אומרים להתחיל את האנימציה כשהרכיב נכנס (enter 0%
) למאגר הגלילה. והיא תסתיים כשהיא תכלול 50% (cover 50%
) ממאגר התמונות שגלול.
הדגמה שלנו בפעולה:
אפשר גם לקשר אנימציה לרכיב שנע בחלון התצוגה. כדי לעשות זאת, מגדירים את animation-timeline
כ-view-timeline
של הרכיב. האפשרות הזו מתאימה למקרים כמו אנימציות של רשימות. ההתנהגות דומה לאופן שבו אפשר להוסיף אנימציה לרכיבים עם כניסה באמצעות 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;
}
}
כך, הרכיב 'Mover' יתרחב כשהוא ייכנס לאזור התצוגה, ויפעיל את הסיבוב של הרכיב 'Spinner'.
מהניסויים שלי הבנתי שה-API פועל בצורה טובה מאוד עם scroll-snap. שילוב של 'הצמדה בגלילה' עם ViewTimeline
יתאים מאוד לצילום של דפים שנפתחים בספר.
יצירת אבטיפוס של המנגנון
אחרי כמה ניסויים, הצלחתי ליצור אב טיפוס של ספר שפועל. כדי לדפדף בדפים של הספר, גוללים אופקית.
בדמו, אפשר לראות את הטריגרים השונים מודגשים באמצעות גבולות מקווקווים.
ה-Markup נראה כך:
<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>
כשגוללים, הדפים בספר נפתחים או נסגרים, אבל לא בצורה חלקה. המיקום הזה תלוי בהתאמה של הטריגרים לתנועת הגלילה.
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;
}
הפעם לא מחברים את ViewTimeline
ב-CSS, אלא משתמשים ב-Web Animations API ב-JavaScript. היתרון הנוסף של השיטה הזו הוא היכולת להריץ לולאה על קבוצת רכיבים וליצור את ה-ViewTimeline
שאנחנו צריכים, במקום ליצור כל אחד מהם באופן ידני.
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);
לכל טריגר אנחנו יוצרים ViewTimeline
. לאחר מכן אנחנו יוצרים אנימציה לדף המשויך לטריגר באמצעות ViewTimeline
הזה. הקוד הזה מקשר את האנימציה של הדף לגלילה. כדי ליצור את האנימציה, אנחנו מסובבים רכיב של הדף בציר y כדי להפוך את הדף. אנחנו גם מעבירים את הדף עצמו בציר z כדי שהוא יפעל כמו ספר.
סיכום של כל המידע
אחרי שסיימתי לעבוד על המנגנון של הספר, יכולתי להתמקד בהפיכת האיורים של טיילר לחיים.
Astro
הצוות השתמש ב-Astro במהלך Designcember בשנת 2021, ורציתי להשתמש בו שוב במהלך Chrometober. חוויית המפתחים, שמאפשרת לפצל דברים לרכיבים, מתאימה מאוד לפרויקט הזה.
הספר עצמו הוא רכיב. הוא גם אוסף של רכיבי דף. לכל דף יש שני צדדים ורקעים. הצאצאים של צד הדף הם רכיבים שאפשר להוסיף, להסיר ולמקם בקלות.
יצירת ספר
חשוב לי שהבלוקים יהיו נוחים לניהול. רציתי גם שיהיה קל לשאר חברי הצוות לתרום.
הדפים ברמה גבוהה מוגדרים על ידי מערך תצורה. כל אובייקט דף במערך מגדיר את התוכן, הרקע ומטא-נתונים אחרים של הדף.
const pages = [
{
front: {
marked: true,
content: PageTwo,
backdrop: spreadOne,
darkBackdrop: spreadOneDark
},
back: {
content: PageThree,
backdrop: spreadTwo,
darkBackdrop: spreadTwoDark
},
aria: `page 1`
},
/* Obfuscated page objects */
]
הם מועברים לרכיב Book
.
<Book pages={pages} />
הרכיב Book
הוא המקום שבו מופעל מנגנון הגלילה ונוצרים דפי הספר. נעשה שימוש באותו מנגנון מהאב טיפוס, אבל אנחנו משתפים כמה מכונות של ViewTimeline
שנוצרות באופן גלובלי.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
כך נוכל לשתף את צירי הזמן לשימוש במקום אחר, במקום ליצור אותם מחדש. בהמשך נסביר על כך.
הרכב הדף
כל דף הוא פריט ברשימה:
<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>
ההגדרה שהוגדרה מועברת לכל מכונה של Page
. בדפים נעשה שימוש בתכונה 'חריץ' של Astro כדי להוסיף תוכן לכל דף.
<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>
הקוד הזה מיועד בעיקר להגדרת המבנה. לרוב, שותפי העריכה יכולים לעבוד על תוכן הספר בלי לגעת בקוד הזה.
רקעים
המעבר ליצירת ספר עזר לנו לפצל את הקטעים בקלות רבה יותר, וכל דף בספר הוא סצנה שנלקחה מהעיצוב המקורי.
מאחר שהחלטנו על יחס גובה-רוחב לספר, הרקע של כל דף יכול לכלול רכיב תמונה. כדי לפתור את הבעיה, צריך להגדיר את הרכיב הזה לרוחב של 200% ולהשתמש ב-object-position
על סמך הצד של הדף.
.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;
}
תוכן הדף
נראה איך יוצרים אחד מהדפים. בדף שלישי מוצגת ינשופת שמופיעה על עץ.
הוא מאוכלס ברכיב PageThree
, כפי שמוגדר בתצורה. זהו רכיב Astro (PageThree.astro
). הרכיבים האלה נראים כמו קובצי HTML, אבל יש להם גדר קוד בחלק העליון, בדומה לתוכן מקדים. כך אנחנו יכולים לייבא רכיבים אחרים. הרכיב של הדף השלישי נראה כך:
---
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>
שוב, הדפים הם אטומיים מטבעם. הם בנויים מאוסף של תכונות. בדף שלישי מוצגים בלוק תוכן והינשוף האינטראקטיבי, ולכן יש רכיב לכל אחד מהם.
בלוקים של תוכן הם הקישורים לתוכן שמוצג בספר. גם הן מופעלות על ידי אובייקט תצורה.
{
"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
]
}
ההגדרה הזו מיובאת למקומות שבהם נדרשים חסמי תוכן. לאחר מכן, הגדרת הבלוק הרלוונטי מועברת לרכיב ContentBlock
.
<ContentBlock {...contentBlocks[3]} id="four" />
הדוגמה הזו גם מראה איך אנחנו משתמשים ברכיב של הדף כמקום למיקום התוכן. כאן ממקמים את בלוק התוכן.
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
עם זאת, הסגנונות הכלליים של בלוק תוכן נמצאים באותו מיקום שבו נמצא קוד הרכיב.
.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%;
}
לגבי הינשוף שלנו, זוהי תכונה אינטראקטיבית – אחת מתוך תכונות רבות בפרויקט הזה. זוהי דוגמה קטנה ושימושית שמראה איך השתמשנו בתצוגת ציר הזמן המשותפת שיצרנו.
ברמה גבוהה, רכיב הינשוף מייבא קובצי SVG מסוימים ומטמיע אותם באמצעות ה-Fragment של Astro.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
הסגנונות למיקום הינשוף נמצאים באותו מיקום שבו נמצא קוד הרכיב.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
יש עוד רכיב עיצוב אחד שמגדיר את ההתנהגות של transform
עבור הינשוף.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
השימוש ב-transform-box
משפיע על transform-origin
. הוא הופך את המיקום ליחסי ביחס לתיבת הגבול של האובייקט בתוך קובץ ה-SVG. הינשוף מתרחב מהמרכז התחתון, ולכן נעשה שימוש ב-transform-origin: 50% 100%
.
החלק הכי מהנה הוא כשמקשרים את הינשוף לאחד מה-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()
בבלוק הקוד הזה אנחנו מבצעים שתי פעולות:
- בודקים את העדפות התנועה של המשתמש.
- אם אין להם העדפה, אפשר לקשר אנימציה של הינשוף לגלילה.
בחלק השני, הינשוף נע באנימציה על ציר ה-y באמצעות Web Animations API. נעשה שימוש בנכס טרנספורמציה ספציפי translate
, שמקושר ל-ViewTimeline
אחד. הוא מקושר ל-CHROMETOBER_TIMELINES[1]
דרך המאפיין timeline
. זהו ViewTimeline
שנוצר עבור הפניית הדפים. כך מקשרים את האנימציה של הינשוף להחלפת הדף באמצעות השלב enter
. ההגדרה קובעת שכאשר הדף מסתובב ב-80%, מתחילים להזיז את הינשוף. כשהתרגום יגיע ל-90%, הינשוף אמור לסיים את התרגום.
תכונות הספר
עכשיו ראינו את הגישה ליצירת דף ואת אופן הפעולה של ארכיטקטורת הפרויקט. אפשר לראות איך הוא מאפשר לכותבים להצטרף ולעבוד על דף או תכונה לבחירתם. לתכונות שונות בספר יש אנימציות שמקושרות להחלפת הדפים בספר. לדוגמה, העטלף שחג כשהוא נכנס ויוצא מהדף.
יש בו גם אלמנטים שמבוססים על אנימציות CSS.
אחרי שהוסיפה את אבני הבניין של התוכן לספר, היה לה זמן להשתמש ביצירתיות עם תכונות אחרות. כך הייתה לנו הזדמנות ליצור אינטראקציות שונות ולנסות דרכים שונות להטמעה.
שמירה על תגובה מהירה
יחידות של אזור תצוגה רספונסיבי מגדירות את הגודל של הספר והתכונות שלו. עם זאת, היה מעניין לאתגר את עצמנו ולשמור על תגובה מהירה של הגופנים. יחידות של שאילתות בקונטיינרים מתאימות למקרה כזה. עם זאת, עדיין אין תמיכה בהם בכל מקום. הגודל של הספר מוגדר, ולכן אין צורך בשאילתת מאגר. אפשר ליצור יחידת שאילתת מאגר מוטמעת באמצעות CSS calc()
ולהשתמש בה כדי לקבוע את גודל הגופן.
.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);
}
דלועים זוהרים בלילה
אם יש לכם עין חדה, יכול להיות ששמתם לב לשימוש ברכיבי <source>
כשדיברנו על הרקעים של הדפים מקודם. Una רצתה ליצור אינטראקציה שתגיב להעדפה של ערכת צבעים. כתוצאה מכך, הרקעים תומכים גם במצב בהיר וגם במצב כהה עם וריאנטים שונים. אפשר להשתמש בשאילתות מדיה עם הרכיב <picture>
, ולכן זוהי דרך מצוינת לספק שני סגנונות של רקע. הרכיב <source>
שולח שאילתות לגבי העדפת ערכת הצבעים, ומציג את הרקע המתאים.
<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>
אפשר לבצע שינויים נוספים על סמך העדפת ערכת הצבעים הזו. הדלועים בדף השני משתנים בהתאם להעדפות של המשתמש לגבי ערכת הצבעים. קובץ ה-SVG שנעשה בו שימוש מכיל עיגולים שמייצגים להבות, שמתרחבים ומקבלים אנימציה במצב כהה.
.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;
}
}
}
האם התמונה הזו צופה בכם?
אם בודקים את דף 10, יכול להיות שמשהו ייראה לכם מוכר. אתם נמצאים במעקב! העיניים של הדיוקן יעקבו אחרי הסמן שלכם כשאתם נעים בדף. הטריק הוא למפות את מיקום הסמן לערך translate ולהעביר אותו ל-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)
}
הקוד הזה מקבל טווחי קלט ופלט וממפה את הערכים שצוינו. לדוגמה, השימוש הזה יניב את הערך 625.
mapRange(0, 100, 250, 1000, 50) // 625
בתמונת הדיוקן, ערך הקלט הוא נקודת המרכז של כל עין, בתוספת או בחיסור מרחק מסוים בפיקסלים. טווח הפלט הוא היקף התרגום של העיניים בפיקסלים. לאחר מכן, מיקום הסמן בציר x או y מועבר כערך. כדי לקבל את נקודת המרכז של העיניים בזמן הזזתן, העיניים מועתקות. המקור לא זז, הוא שקוף ומשמש לצורכי עיון.
לאחר מכן, צריך לקשר את הרכיבים ולעדכן את ערכי המאפיינים המותאמים אישית של ה-CSS בעיניים כדי שהעיניים יוכלו לזוז. פונקציה מקושרת לאירוע pointermove
מול window
. כשהאירוע הזה מופעל, המערכת משתמשת בגבולות של כל עין כדי לחשב את נקודות המרכז. לאחר מכן, מיקום הסמן ממופה לערכים שמוגדרים כערכים של מאפיינים מותאמים אישית בעיניים.
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)
})
}
אחרי שהערכים מועברים ל-CSS, אפשר לעשות איתם מה שרוצים בסגנונות. היתרון הגדול כאן הוא השימוש ב-CSS clamp()
כדי לשנות את ההתנהגות של כל עין, כך שתוכלו לשנות את ההתנהגות של כל עין בלי לגעת שוב ב-JavaScript.
.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);
}
השמעת לחשים
אם בודקים את דף שש, האם מרגישים מוקסמים? בדף הזה מוצג העיצוב של השועל הקסום והנפלא שלנו. אם תזיזו את הסמן, יכול להיות שתראו אפקט מותאם אישית של נתיב הסמן. האנימציה הזו מתבצעת על גבי לוח ציור. רכיב <canvas>
נמצא מעל שאר תוכן הדף עם pointer-events: none
. כלומר, המשתמשים עדיין יוכלו ללחוץ על בלוקים של תוכן שמתחת.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
בדומה לאופן שבו התמונה הסטטית שלנו מקשיבה לאירוע pointermove
ב-window
, כך גם הרכיב <canvas>
. עם זאת, בכל פעם שהאירוע מופעל, אנחנו יוצרים אובייקט כדי ליצור אנימציה ברכיב <canvas>
. האובייקטים האלה מייצגים צורות שמשמשות בנתיב הסמן. יש להם קואורדינטות וגוון אקראי.
הפונקציה mapRange
שלנו מוקדם יותר משמשת שוב, כי אפשר להשתמש בה כדי למפות את הדלתה של הסמן אל size
ו-rate
. האובייקטים מאוחסנים במערך שמתבצעת בו לולאה כשהאובייקטים מצוירים לרכיב <canvas>
. המאפיינים של כל אובייקט אומרים לאלמנט <canvas>
שלנו איפה צריך לצייר את הדברים.
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)
כדי לצייר על הקנבס, יוצרים לולאה באמצעות requestAnimationFrame
. נתיב הסמן צריך להיות מוצג רק כשהדף נמצא בתצוגה. יש לנו IntersectionObserver
שמעדכן ומחליט אילו דפים מוצגים. אם דף מוצג, האובייקטים מוצגים כעיגולים על גבי הלוח.
לאחר מכן, אנחנו מבצעים לולאה על המערך blocks
ומציירים כל חלק מהמסלול. בכל פריים, הגודל של האובייקט מצטמצם והמיקום שלו משתנה לפי הערך של rate
. כך נוצר האפקט של ירידה והקטנה. אם האובייקט מתכווץ לגמרי, הוא מוסר מהמערך 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)
}
אם הדף יוצא מהתצוגה, מעבדי האירועים יוסרו והלוף של מסגרות האנימציה יבוטל. גם המערך blocks
יימחק.
הנה נתיב הסמן בפעולה!
בדיקת נגישות
יצירת חוויה מהנה לעיונם של המשתמשים היא דבר טוב, אבל אם היא לא נגישה למשתמשים, היא לא תהיה שימושית. המומחיות של אדם בתחום הזה הייתה משמעותית בהכנת Chrometober לבדיקה של נגישות לפני השקה.
אלה כמה מהנושאים העיקריים שאנחנו עוסקים בהם:
- מוודאים שה-HTML שנעשה בו שימוש הוא סמנטי. בין היתר, השתמשתם ברכיבי ציון דרך מתאימים, כמו
<main>
לספר, וברכיב<article>
לכל בלוק תוכן, וגם ברכיבי<abbr>
שבהם מוצגים ראשי תיבות. החשיבה מראש במהלך היצירה של הספר עזרה לנו להנגיש את הדברים. שימוש בכותרות ובקישורים מקל על המשתמשים לנווט. השימוש ברשימת דפים גם מאפשר לטכנולוגיה מסייעת להכריז על מספר הדפים. - מוודאים שכל התמונות כוללות את המאפיינים המתאימים של
alt
. בקובצי SVG שמוצגים בתוך שורה, האלמנטtitle
נמצא במקרים הנדרשים. - שימוש במאפייני
aria
במקרים שבהם הם משפרים את חוויית השימוש. השימוש ב-aria-label
לדפים ולצדדים שלהם מאפשר למשתמש לדעת באיזה דף הוא נמצא. השימוש ב-aria-describedBy
בקישורים ל'קריאה נוספת' מעביר את הטקסט של בלוק התוכן. כך תוכלו להסיר את אי-הבהירות לגבי היעד שאליו הקישור יעביר את המשתמש. - בנוגע לחסימות תוכן, יש אפשרות ללחוץ על הכרטיס כולו ולא רק על הקישור 'מידע נוסף'.
- התייחסנו קודם לשימוש ב-
IntersectionObserver
כדי לעקוב אחרי הדפים שמוצגים. לכך יש יתרונות רבים, שלא קשורים רק לביצועים. אנימציות או אינטראקציות בדפים שלא נמצאים בתצוגה יושהו. אבל על הדפים האלה מופעל גם המאפייןinert
. המשמעות היא שמשתמשים שמשתמשים בקורא מסך יכולים לגלוש באותו תוכן כמו משתמשים שרואים. המיקוד נשאר בדף שמוצג, והמשתמשים לא יכולים להקיש על Tab כדי לעבור לדף אחר. - לבסוף, אנחנו משתמשים בבקשות מדיה כדי לכבד את העדפות התנועה של המשתמשים.
לפניכם צילום מסך מהבדיקה, שבו מודגשות כמה מהפעולות שננקטו.
מוגדר כאלמננט שמקיף את כל הספר, ומציין שהוא אמור להיות המאפיין העיקרי שמשתמשים בטכנולוגיה מסייעת יכולים למצוא. פרטים נוספים מפורטים בצילום המסך." width="800" height="465">
מה למדנו
המטרה של Chrometober לא הייתה רק להבליט תוכן אינטרנט מהקהילה, אלא גם לתת לנו הזדמנות לבדוק את ה-polyfill של ממשק ה-API לאנימציות שמקושרות לגלילה, שנמצא בפיתוח.
במהלך כנס הצוות שלנו בניו יורק, הקדשנו זמן לבדוק את הפרויקט ולטפל בבעיות שהתגלו. התרומה של הצוות הייתה לא יסולא בפז. זו הייתה גם הזדמנות מצוינת לרשום את כל הדברים שצריך לטפל בהם לפני שנוכל להשיק את התכונה.
לדוגמה, בדיקת הספר במכשירים העלתה בעיה ברינדור. הספר שלנו לא הוצג כראוי במכשירי iOS. יחידות אזור התצוגה קובעות את גודל הדף, אבל כשהיה חריץ, הוא השפיע על הספר. הפתרון היה להשתמש ב-viewport-fit=cover
באזור התצוגה meta
:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
בסשן הזה הוצגו גם כמה בעיות ב-polyfill של ה-API. Bramus הצביע על הבעיות האלה במאגר ה-polyfill. לאחר מכן הוא מצא פתרונות לבעיות האלה ומיזג אותן ב-polyfill. לדוגמה, בקשת משיכה זו הביאה לשיפור בביצועים על ידי הוספת אחסון במטמון לחלק מה-polyfill.
זהו!
היה לנו כיף לעבוד על הפרויקט הזה, והתוצאה היא חוויית גלילה שובבה שמציגה תוכן מדהים מהקהילה. בנוסף, הבדיקה הזו עזרה לנו לבדוק את ה-polyfill ולספק משוב לצוות מהנדסי התוכנה כדי לשפר את ה-polyfill.
Chrometober 2022 הסתיים.
אנחנו מקווים שנהניתם! מה התכונה האהובה עליך? אפשר לשלוח לי ציוץ ולספר לנו.
יכול להיות שגם תהיה לכם אפשרות לקבל מדבקות מאחד מחברי הצוות אם תפגשו אותנו באירוע.
התמונה הראשית (Hero) של David Menidrey ב-Unsplash