چگونه کتاب پیمایش برای به اشتراک گذاشتن نکات و ترفندهای سرگرم کننده و ترسناک در این Chrometober جان گرفت.
به دنبال طراحی دیزاینمبر ، امسال میخواستیم Chrometober را به عنوان راهی برای برجسته کردن و اشتراکگذاری محتوای وب از انجمن و تیم Chrome برای شما بسازیم. Designcember استفاده از Container Queries را به نمایش گذاشت، اما امسال ما API انیمیشن های مرتبط با اسکرول CSS را به نمایش می گذاریم.تجربه کتاب پیمایش را در web.dev/chrometober-2022 بررسی کنید.
نمای کلی
هدف این پروژه ارائه یک تجربه عجیب و غریب با برجسته کردن API انیمیشن های مرتبط با اسکرول بود. اما، در عین عجیب بودن، تجربه باید پاسخگو و در دسترس نیز باشد. این پروژه همچنین یک راه عالی برای آزمایش درایو API polyfill است که در حال توسعه فعال است. که، و همچنین آزمایش تکنیک ها و ابزارهای مختلف در ترکیب. و همه با تم جشن هالووین!
ساختار تیم ما به این صورت بود:
- تایلر رید : تصویرسازی و طراحی
- جی تامپکینز : رهبری معماری و خلاق
- Una Kravets : سرپرست پروژه
- Bramus Van Damme : مشارکت کننده سایت
- آدام آرگیل : بررسی قابلیت دسترسی
- آرون فورینتون: کپی رایتینگ
پیش نویس یک تجربه طومار نویسی
ایدههای Chrometober در ماه مه 2022 در اولین تیم ما در خارج از سایت شروع شد. مجموعهای از خطنوشتهها ما را به فکر راههایی انداخت که از طریق آنها کاربر بتواند مسیر خود را در امتداد نوعی از استوریبورد پیمایش کند. ما با الهام از بازیهای ویدیویی، تجربهای را در صحنههایی مانند قبرستان و خانه خالی از سکنه در نظر گرفتیم.
داشتن آزادی خلاقانه برای بردن اولین پروژه گوگل به مسیری غیرمنتظره هیجان انگیز بود. این یک نمونه اولیه از نحوه حرکت کاربر در محتوا بود.
همانطور که کاربر به طرفین پیمایش می کند، بلوک ها می چرخند و بزرگ می شوند. اما من تصمیم گرفتم از این ایده دور شوم به دلیل نگرانی در مورد اینکه چگونه می توانیم این تجربه را برای کاربران دستگاه های مختلف در اندازه ها عالی کنیم. در عوض، به سمت طراحی چیزی که در گذشته ساخته بودم متمایل شدم. در سال 2020، من خوش شانس بودم که به GreenSock's ScrollTrigger برای ساخت دموهای انتشار دسترسی داشتم.
یکی از دموهایی که من ساخته بودم یک کتاب 3D-CSS بود که در آن صفحات با پیمایش شما می چرخیدند، و این برای آنچه ما برای Chrometober می خواستیم بسیار مناسب تر به نظر می رسید. API انیمیشنهای مرتبط با اسکرول یک جایگزین عالی برای این عملکرد است. همانطور که خواهید دید، با scroll-snap
نیز به خوبی کار می کند!
تصویرگر ما برای پروژه، تایلر رید ، در تغییر طرح با تغییر ایدهها عالی بود. تایلر کار خارقالعادهای انجام داد و تمام ایدههای خلاقانهای را که به سمت او پرتاب میشد، به کار برد و آنها را زنده کرد. ایده های طوفان فکری با هم بسیار سرگرم کننده بود. بخش بزرگی از نحوه عملکرد ما این بود که ویژگیها به بلوکهای مجزا تقسیم شدند. به این ترتیب، میتوانیم آنها را در صحنههایی بسازیم و سپس آنچه را که زنده کردهایم انتخاب و انتخاب کنیم.
ایده اصلی این بود که وقتی کاربر راه خود را از طریق کتاب طی می کرد، بتواند به بلوک های محتوا دسترسی داشته باشد. آنها همچنین میتوانستند با هوسهای هوسبازی، از جمله تخممرغهای عید پاک که در این تجربه ساخته بودیم، تعامل داشته باشند. به عنوان مثال، یک پرتره در یک خانه خالی از سکنه، که چشمانش نشانگر شما را دنبال می کند، یا انیمیشن های ظریفی که توسط پرسش های رسانه ای ایجاد شده اند. این ایده ها و ویژگی ها در اسکرول متحرک خواهند شد. ایده اولیه یک اسم حیوان دست اموز زامبی بود که در امتداد محور x در اسکرول کاربر برمیخیزد و ترجمه میکرد.
آشنایی با API
قبل از اینکه بتوانیم با ویژگی های فردی و تخم مرغ های عید پاک بازی کنیم، به یک کتاب نیاز داشتیم. بنابراین ما تصمیم گرفتیم این را به فرصتی برای آزمایش ویژگیها برای API انیمیشنهای مرتبط با پیمایش CSS تبدیل کنیم. 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
برای چرخش صفحه در یک کتاب بسیار مناسب است.
نمونه سازی مکانیک
پس از مدتی آزمایش، توانستم نمونه اولیه کتاب را به کار بیاورم. برای ورق زدن صفحات کتاب به صورت افقی اسکرول می کنید.
در نسخه ی نمایشی، می توانید محرک های مختلف را ببینید که با حاشیه های چین دار برجسته شده اند.
نشانه گذاری کمی شبیه به این است:
<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 در جاوا اسکریپت استفاده می کنیم. این مزیت افزوده این است که میتوانیم روی مجموعهای از عناصر حلقه بزنیم و 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
تیم در سال 2021 از Astro برای Designcember استفاده کرد و من مشتاق بودم دوباره از آن برای 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 هستند، اما دارای یک حصار کد در بالا هستند شبیه به frontmatter. این ما را قادر میسازد کارهایی مانند وارد کردن اجزای دیگر را انجام دهیم. کامپوننت برای صفحه سه به شکل زیر است:
---
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%;
}
در مورد جغد ما، این یک ویژگی تعاملی است - یکی از بسیاری از ویژگی های این پروژه. این یک مثال کوچک خوب برای مرور است که نشان می دهد چگونه از ViewTimeline مشترکی که ایجاد کردیم استفاده کردیم.
در سطح بالایی، مؤلفه جغد ما مقداری SVG وارد میکند و با استفاده از قطعه 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()
در این بلوک کد دو کار انجام می دهیم:
- تنظیمات برگزیده حرکت کاربر را بررسی کنید.
- اگر ترجیحی ندارند، انیمیشن جغد را به اسکرول پیوند دهید.
برای بخش دوم، جغد با استفاده از Web Animations API روی محور y متحرک می شود. translate
ویژگی تبدیل فردی استفاده میشود و به یک ViewTimeline
پیوند داده میشود. از طریق ویژگی timeline
به CHROMETOBER_TIMELINES[1]
پیوند داده شده است. این یک ViewTimeline
است که برای چرخش صفحه ایجاد می شود. این انیمیشن جغد را با استفاده از مرحله enter
به صفحه تبدیل می کند. تعریف می کند که وقتی صفحه 80٪ چرخید، شروع به حرکت جغد کنید. در 90٪، جغد باید ترجمه خود را تمام کند.
ویژگی های کتاب
اکنون رویکرد ساخت یک صفحه و نحوه عملکرد معماری پروژه را مشاهده کرده اید. میتوانید ببینید که چگونه به مشارکتکنندگان اجازه میدهد وارد صفحه یا ویژگی مورد نظر خود شده و کار کنند. ویژگی های مختلف در کتاب انیمیشن های خود را به ورق زدن صفحه کتاب مرتبط است. به عنوان مثال، خفاشی که به داخل و خارج می شود در صفحه می چرخد.
همچنین دارای عناصری است که توسط انیمیشن های CSS طراحی شده اند.
هنگامی که بلوک های محتوا در کتاب قرار گرفتند، فرصتی برای خلاقیت با ویژگی های دیگر وجود داشت. این فرصتی را برای ایجاد برخی از تعاملات مختلف، و امتحان راه های مختلف برای اجرای چیزها فراهم کرد.
پاسخگو نگه داشتن چیزها
واحدهای نمای پاسخگو اندازه کتاب و ویژگی های آن را دارند. با این حال، پاسخگو نگه داشتن فونت ها چالش جالبی بود. واحدهای جستجوی کانتینر در اینجا مناسب هستند. اگرچه هنوز در همه جا پشتیبانی نمی شوند. اندازه کتاب تنظیم شده است، بنابراین نیازی به درخواست ظرف نداریم. یک واحد کوئری کانتینر درون خطی را می توان با calc()
CSS تولید کرد و برای اندازه فونت استفاده کرد.
.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>
در هنگام بحث در مورد پس زمینه صفحه شده باشند. یونا مشتاق بود تا تعاملی داشته باشد که به ترجیح طرح رنگ واکنش نشان دهد. در نتیجه، پس زمینه ها از هر دو حالت روشن و تاریک با انواع مختلف پشتیبانی می کنند. از آنجا که می توانید از پرس و جوهای رسانه ای با عنصر <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 را بررسی کنید، ممکن است متوجه چیزی شوید. شما تحت نظر هستید! در حین حرکت در صفحه، چشمهای پرتره نشانگر شما را دنبال میکنند. ترفند در اینجا این است که مکان اشاره گر را به یک مقدار ترجمه ترسیم کنید و آن را به 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 منتقل می شوند، استایل ها می توانند آنچه را که می خواهند با آنها انجام دهند. بخش بزرگ در اینجا استفاده از clamp()
CSS برای متفاوت کردن رفتار برای هر چشم است، بنابراین میتوانید بدون لمس مجدد جاوا اسکریپت، رفتار هر چشم را متفاوت کنید.
.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;
}
عنصر <canvas>
بسیار شبیه نحوه گوش دادن پرتره ما به یک رویداد pointermove
در window
است. با این حال، هر بار که رویداد اجرا میشود، یک شی برای متحرک سازی روی عنصر <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
نیز هستند. این بدان معناست که کاربرانی که از صفحهخوان استفاده میکنند میتوانند محتوای مشابهی را با کاربران بینا کاوش کنند. فوکوس در صفحهای که در آن مشاهده میشود باقی میماند و کاربران نمیتوانند به صفحه دیگری برگه بزنند. - آخرین اما نه کم اهمیت ترین، ما از پرسش های رسانه ای برای احترام به اولویت کاربر برای حرکت استفاده می کنیم.
در اینجا تصویری از بررسی وجود دارد که برخی از اقدامات موجود را برجسته می کند.
عنصر در کل کتاب شناسایی شده است، که نشان می دهد باید نقطه عطف اصلی برای کاربران فناوری کمکی باشد. موارد بیشتری در اسکرین شات مشخص شده است." width="800" height="465">
چیزی که یاد گرفتیم
انگیزه پشت کرومتوبر نه تنها برجسته کردن محتوای وب از جامعه بود، بلکه راهی برای ما برای آزمایش درایو پویانمایی های مرتبط با اسکرول API polyfill بود که در حال توسعه است.
زمانی که در اجلاس تیم خود در نیویورک بودیم، جلسه ای را برای آزمایش پروژه و رسیدگی به مسائل پیش آمده اختصاص دادیم. کمک این تیم بسیار ارزشمند بود. همچنین یک فرصت عالی برای فهرست کردن همه چیزهایی بود که قبل از پخش زنده نیاز به مقابله داشتند.
به عنوان مثال، آزمایش کتاب بر روی دستگاهها یک مشکل رندر را ایجاد کرد. کتاب ما در دستگاههای iOS آنطور که انتظار میرود ارائه نمیشود. واحدهای Viewport صفحه را اندازه میدهند، اما وقتی یک بریدگی وجود داشت، روی کتاب تأثیر میگذاشت. راه حل استفاده از viewport-fit=cover
در نمای meta
بود:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
این جلسه همچنین برخی از مشکلات را در مورد API polyfill مطرح کرد. براموس این مسائل را در مخزن polyfill مطرح کرد. او متعاقباً راهحلهایی برای آن مسائل پیدا کرد و آنها را در polyfill ادغام کرد. به عنوان مثال، این درخواست کششی با افزودن حافظه پنهان به بخشی از polyfill افزایش عملکردی ایجاد کرد.
همین!
این یک پروژه سرگرم کننده واقعی برای کار بوده است که منجر به یک تجربه اسکرول عجیب و غریب می شود که محتوای شگفت انگیز جامعه را برجسته می کند. نه تنها این، برای آزمایش پلی فیل، و همچنین ارائه بازخورد به تیم مهندسی برای کمک به بهبود پلی پر بسیار عالی بوده است.
Chrometober 2022 یک بسته بندی است.
امیدواریم از آن لذت برده باشید! ویژگی مورد علاقه شما چیست؟ من را توییت کنید و به ما اطلاع دهید!
اگر ما را در یک رویداد دیدید، حتی ممکن است بتوانید چند برچسب از یکی از تیم بگیرید.
عکس قهرمان توسط دیوید منیدری در Unsplash
،چگونه کتاب پیمایش برای به اشتراک گذاشتن نکات و ترفندهای سرگرم کننده و ترسناک در این Chrometober جان گرفت.
به دنبال طراحی دیزاینمبر ، امسال میخواستیم Chrometober را به عنوان راهی برای برجسته کردن و اشتراکگذاری محتوای وب از انجمن و تیم Chrome برای شما بسازیم. Designcember استفاده از Container Queries را به نمایش گذاشت، اما امسال ما API انیمیشن های مرتبط با اسکرول CSS را به نمایش می گذاریم.تجربه کتاب پیمایش را در web.dev/chrometober-2022 بررسی کنید.
نمای کلی
هدف این پروژه ارائه یک تجربه عجیب و غریب با برجسته کردن API انیمیشن های مرتبط با اسکرول بود. اما، در عین عجیب بودن، تجربه باید پاسخگو و در دسترس نیز باشد. این پروژه همچنین یک راه عالی برای آزمایش درایو API polyfill است که در حال توسعه فعال است. که، و همچنین آزمایش تکنیک ها و ابزارهای مختلف در ترکیب. و همه با تم جشن هالووین!
ساختار تیم ما به این صورت بود:
- تایلر رید : تصویرسازی و طراحی
- جی تامپکینز : رهبری معماری و خلاق
- Una Kravets : سرپرست پروژه
- Bramus Van Damme : مشارکت کننده سایت
- آدام آرگیل : بررسی قابلیت دسترسی
- آرون فورینتون: کپی رایتینگ
پیش نویس یک تجربه طومار نویسی
ایدههای Chrometober در ماه مه 2022 در اولین تیم ما در خارج از سایت شروع شد. مجموعهای از خطنوشتهها ما را به فکر راههایی انداخت که از طریق آنها کاربر بتواند مسیر خود را در امتداد نوعی از استوریبورد پیمایش کند. ما با الهام از بازیهای ویدیویی، تجربهای را در صحنههایی مانند قبرستان و خانه خالی از سکنه در نظر گرفتیم.
داشتن آزادی خلاقانه برای بردن اولین پروژه گوگل به مسیری غیرمنتظره هیجان انگیز بود. این یک نمونه اولیه از نحوه حرکت کاربر در محتوا بود.
همانطور که کاربر به طرفین پیمایش می کند، بلوک ها می چرخند و بزرگ می شوند. اما من تصمیم گرفتم از این ایده دور شوم به دلیل نگرانی در مورد اینکه چگونه می توانیم این تجربه را برای کاربران دستگاه های مختلف در اندازه ها عالی کنیم. در عوض، به سمت طراحی چیزی که در گذشته ساخته بودم متمایل شدم. در سال 2020، من خوش شانس بودم که به GreenSock's ScrollTrigger برای ساخت دموهای انتشار دسترسی داشتم.
یکی از دموهایی که من ساخته بودم یک کتاب 3D-CSS بود که در آن صفحات با پیمایش شما می چرخیدند، و این برای آنچه ما برای Chrometober می خواستیم بسیار مناسب تر به نظر می رسید. API انیمیشنهای مرتبط با اسکرول یک جایگزین عالی برای این عملکرد است. همانطور که خواهید دید، با scroll-snap
نیز به خوبی کار می کند!
تصویرگر ما برای پروژه، تایلر رید ، در تغییر طرح با تغییر ایدهها عالی بود. تایلر کار خارقالعادهای انجام داد و تمام ایدههای خلاقانهای را که به سمت او پرتاب میشد، به کار برد و آنها را زنده کرد. ایده های طوفان فکری با هم بسیار سرگرم کننده بود. بخش بزرگی از نحوه عملکرد ما این بود که ویژگیها به بلوکهای مجزا تقسیم شدند. به این ترتیب، میتوانیم آنها را در صحنههایی بسازیم و سپس آنچه را که زنده کردهایم انتخاب و انتخاب کنیم.
ایده اصلی این بود که وقتی کاربر راه خود را از طریق کتاب طی می کرد، بتواند به بلوک های محتوا دسترسی داشته باشد. آنها همچنین میتوانستند با هوسهای هوسبازی، از جمله تخممرغهای عید پاک که در این تجربه ساخته بودیم، تعامل داشته باشند. به عنوان مثال، یک پرتره در یک خانه خالی از سکنه، که چشمانش نشانگر شما را دنبال می کند، یا انیمیشن های ظریفی که توسط پرسش های رسانه ای ایجاد شده اند. این ایده ها و ویژگی ها در اسکرول متحرک خواهند شد. ایده اولیه یک اسم حیوان دست اموز زامبی بود که در امتداد محور x در اسکرول کاربر برمیخیزد و ترجمه میکرد.
آشنایی با API
قبل از اینکه بتوانیم با ویژگی های فردی و تخم مرغ های عید پاک بازی کنیم، به یک کتاب نیاز داشتیم. بنابراین ما تصمیم گرفتیم این را به فرصتی برای آزمایش ویژگیها برای API انیمیشنهای مرتبط با پیمایش CSS تبدیل کنیم. 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
برای چرخش صفحه در یک کتاب بسیار مناسب است.
نمونه سازی مکانیک
پس از مدتی آزمایش، توانستم نمونه اولیه کتاب را به کار بیاورم. برای ورق زدن صفحات کتاب به صورت افقی اسکرول می کنید.
در نسخه ی نمایشی، می توانید محرک های مختلف را ببینید که با حاشیه های چین دار برجسته شده اند.
نشانه گذاری کمی شبیه به این است:
<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 در جاوا اسکریپت استفاده می کنیم. این مزیت افزوده این است که میتوانیم روی مجموعهای از عناصر حلقه بزنیم و 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
تیم در سال 2021 از Astro برای Designcember استفاده کرد و من مشتاق بودم دوباره از آن برای 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 به نظر می رسند اما دارای حصار کد در قسمت بالای آن شبیه به Frontmatter هستند. این ما را قادر می سازد کارهایی مانند واردات سایر مؤلفه ها را انجام دهیم. مؤلفه صفحه سه به این شکل است:
---
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%;
}
در مورد جغد ما ، این یک ویژگی تعاملی است - یکی از بسیاری در این پروژه. این یک نمونه کوچک خوب برای عبور از آن است که نشان می دهد چگونه ما از ViewTimineline مشترک که ایجاد کردیم استفاده کردیم.
در سطح بالایی ، مؤلفه OWL ما مقداری SVG را وارد می کند و آن را با استفاده از قطعه 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
S) تولید می کنیم:
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()
در این بلوک کد ، ما دو کار انجام می دهیم:
- تنظیمات برگزیده حرکت کاربر را بررسی کنید.
- اگر ترجیح نمی دهند ، انیمیشن جغد را برای پیمایش پیوند دهید.
برای قسمت دوم ، OWL در محور y با استفاده از API انیمیشن های وب متحرک می شود. translate
خاصیت تراکم فردی استفاده می شود ، و به یک ViewTimeline
مرتبط است. این از طریق ویژگی timeline
به CHROMETOBER_TIMELINES[1]
مرتبط است. این یک ViewTimeline
است که برای چرخش صفحه ایجاد می شود. این انیمیشن OWL را با استفاده از مرحله 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 را بررسی کنید ، ممکن است متوجه چیزی شوید. شما تماشا می کنید! چشمان پرتره هنگام حرکت در صفحه ، نشانگر شما را دنبال می کند. ترفند در اینجا نقشه برداری مکان اشاره گر به یک مقدار ترجمه و انتقال آن به 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()
است تا رفتار را برای هر چشم متفاوت کند ، بنابراین می توانید بدون اینکه دوباره جاوا اسکریپت را لمس کنید ، هر چشم متفاوت رفتار کنید.
.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>
برای کتاب بود. ASO استفاده از عنصر<article>
برای هر بلوک محتوا ، و عناصر<abbr>
که در آن مخفف معرفی می شوند. فکر کردن در حالی که کتاب ساخته شده بود ، همه چیز را در دسترس تر می کرد. استفاده از عناوین و پیوندها باعث می شود که کاربر حرکت کند. استفاده از لیستی برای صفحات همچنین به این معنی است که تعداد صفحات توسط Assive Technology اعلام شده است. - اطمینان از اینکه همه تصاویر از ویژگی های مناسب
alt
استفاده می کنند. برای SVG های درون خطی ، عنصرtitle
در صورت لزوم موجود است. - استفاده از ویژگی های
aria
در جایی که آنها تجربه را بهبود می بخشند. استفاده ازaria-label
برای صفحات و طرفین آنها با کاربر در کدام صفحه قرار دارد. استفاده ازaria-describedBy
در پیوندهای "بیشتر بخوانید" متن بلوک محتوا را به هم می پیوندد. این امر ابهام را در مورد اینکه لینک کاربر را به دست می آورد ، از بین می برد. - در مورد موضوع بلوک های محتوا ، امکان کلیک بر روی کل کارت و نه تنها پیوند "بیشتر بخوانید" در دسترس است.
- استفاده از یک
IntersectionObserver
برای پیگیری اینکه کدام صفحات در حال مشاهده هستند ، زودتر آمده است. این مزایای بسیاری دارد که فقط مربوط به عملکرد نیست. صفحات در مشاهده هیچ انیمیشن یا تعامل متوقف نمی شوند. اما این صفحات همچنین ویژگیinert
را اعمال می کنند. این بدان معنی است که کاربرانی که از یک خواننده صفحه نمایش استفاده می کنند می توانند همان محتوای کاربران بینایی را کشف کنند. تمرکز در صفحه ای که در حال مشاهده است باقی مانده است و کاربران نمی توانند به صفحه دیگری بفرستند. - نکته آخر اینکه ما از پرس و جوهای رسانه ای برای احترام به اولویت کاربر برای حرکت استفاده می کنیم.
در اینجا یک تصویر از این بررسی وجود دارد که برخی از اقدامات موجود را برجسته می کند.
عنصر به عنوان کل کتاب مشخص می شود ، و این نشان می دهد که باید اصلی ترین مکان برای کاربران فناوری کمکی باشد. بیشتر در تصویر بیان شده است. "عرض =" 800 "ارتفاع =" 465 ">
آنچه یاد گرفتیم
انگیزه پشت Chrometober نه تنها برجسته کردن محتوای وب از جامعه ، بلکه راهی برای ما بود تا بتوانیم API Polyfill API که در حال توسعه است ، درایو انیمیشن های مرتبط با پیمایش را انجام دهیم.
ما جلسه ای را در حالی که در اجلاس تیم خود در نیویورک قرار داشت ، کنار گذاشتیم تا این پروژه را آزمایش کنیم و با مسائلی که بوجود آمده است مقابله کنیم. سهم تیم بسیار ارزشمند بود. همچنین این یک فرصت عالی برای لیست همه مواردی بود که قبل از اینکه بتوانیم به صورت زنده برویم ، همه مواردی را که نیاز به مقابله داشتند ، لیست کنیم.
به عنوان مثال ، آزمایش کتاب در مورد دستگاه ها مسئله ارائه دهنده را مطرح کرده است. کتاب ما همانطور که در دستگاه های iOS انتظار می رود ارائه نمی شود. واحدهای دیدگاه صفحه را اندازه می گیرند ، اما وقتی یک شکاف وجود داشت ، این کتاب را تحت تأثیر قرار داد. راه حل استفاده از viewport-fit=cover
در Viewport meta
بود:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
این جلسه همچنین برخی از مسائل مربوط به API Polyfill را مطرح کرد. براموس این مسائل را در مخزن Polyfill مطرح کرد. وی متعاقباً راه حل هایی برای این مسائل پیدا کرد و آنها را به صورت پلی فیلی ادغام کرد. به عنوان مثال ، این درخواست کشش با اضافه کردن ذخیره سازی به بخشی از polyfill ، عملکرد عملکرد را بدست آورد.
همین!
این یک پروژه سرگرم کننده واقعی برای کار در آن بوده است ، و در نتیجه یک تجربه پیمایش عجیب و غریب که محتوای شگفت انگیز جامعه را برجسته می کند. نه تنها این ، برای آزمایش Polyfill و همچنین ارائه بازخورد به تیم مهندسی برای کمک به بهبود Polyfill بسیار عالی بوده است.
Chrometober 2022 یک بسته بندی است.
امیدواریم از آن لذت برده باشید! ویژگی مورد علاقه شما چیست؟ مرا توییت کنید و به ما اطلاع دهید!
حتی اگر ما را در یک رویداد ببینید ، حتی ممکن است بتوانید برخی از برچسب ها را از یکی از تیم ها بگیرید.
عکس قهرمان توسط دیوید منیدری در Unsplash
،چگونه کتاب پیمایش به دلیل به اشتراک گذاشتن نکات سرگرم کننده و ترسناک و ترفندهایی که این Chrometober را به اشتراک می گذارد ، زنده شد.
در ادامه از DesignCember ، ما می خواستیم امسال Chrometober را برای شما به عنوان راهی برای برجسته کردن و به اشتراک گذاری محتوای وب از جامعه و تیم Chrome بسازیم. DesignCember استفاده از نمایش داده های کانتینر را به نمایش گذاشت ، اما امسال ما API انیمیشن های مرتبط با CSS را به نمایش می گذاریم.تجربه کتاب پیمایش را در web.dev/chrometober-2022 بررسی کنید.
نمای کلی
هدف از این پروژه ارائه یک تجربه غریبانه در مورد برجسته کردن انیمیشن های مرتبط با پیمایش API بود. اما ، در حالی که غریب بودن ، تجربه لازم برای پاسخگو بودن و در دسترس بودن نیز هست. این پروژه همچنین یک روش عالی برای آزمایش درایو API Polyfill که در حال توسعه فعال است ، بوده است. این ، و همچنین تلاش تکنیک ها و ابزارهای مختلف در ترکیب. و همه با تم هالووین جشن!
ساختار تیمی ما به این شکل بود:
- تایلر رید : تصویر و طراحی
- Jhey Tompkins : سرب معماری و خلاق
- Una Kravets : رهبری پروژه
- Bramus van Damme : همکار سایت
- Adam Argyle : بررسی دسترسی
- هارون فورینتون: نوشتن کپی
پیش نویس یک تجربه اسکرولیتالینگ
ایده های مربوط به Chrometober در اولین تیم ما در خارج از کشور در ماه مه 2022 شروع به کار کرد. مجموعه ای از کتیبه ها باعث شده است که ما به فکر راه هایی باشند که کاربر بتواند راه خود را به نوعی از صفحه داستانی پیمایش کند. با الهام از بازی های ویدیویی ، ما یک تجربه پیمایش را از طریق صحنه هایی مانند گورستان ها و یک خانه خالی از سکنه در نظر گرفتیم.
داشتن آزادی خلاقانه برای اولین بار پروژه Google من در یک جهت غیر منتظره بسیار هیجان انگیز بود. این یک نمونه اولیه اولیه از چگونگی حرکت کاربر از طریق محتوا بود.
از آنجا که کاربر به پهلو پیمایش می کند ، بلوک ها می چرخند و مقیاس می شوند. درعوض ، من به سمت طراحی چیزی که در گذشته ساخته ام خم شدم. در سال 2020 ، من خوش شانس بودم که به Scrolltrigger Greensock برای ساخت نسخه های نمایشی نسخه دسترسی پیدا کردم.
یکی از نسخه های نمایشی که من ساخته ام یک کتاب 3D-CSS بود که در آن صفحات به عنوان پیمایش می چرخیدند و این احساس مناسب تر برای آنچه برای Chrometober می خواستیم. API انیمیشن های مرتبط با پیمایش یک مبادله مناسب برای این قابلیت است. همانطور که می بینید ، با scroll-snap
نیز خوب کار می کند!
تصویرگر ما برای این پروژه ، تایلر رید ، با تغییر ایده ها در تغییر طراحی بسیار عالی بود. تایلر کار خارق العاده ای انجام داد که تمام ایده های خلاقانه ای را که به سمت او پرتاب شده بود و آنها را زنده می کرد ، انجام داد. این ایده های جالب طوفان مغزی با هم بود. بخش بزرگی از چگونگی کار ما این کار بود که ویژگی های شکسته شده در بلوک های جدا شده بود. به این ترتیب ، ما می توانیم آنها را در صحنه ها آهنگسازی کنیم و سپس آنچه را که به زندگی آورده ایم انتخاب و انتخاب کنیم.
ایده اصلی این بود که ، همانطور که کاربر از طریق کتاب راه خود را انجام داد ، می توانستند به بلوک های محتوا دسترسی پیدا کنند. آنها همچنین می توانند با لکه های هوس ، از جمله تخم مرغ های عید پاک که ما در این تجربه ایجاد کرده بودیم ، تعامل داشته باشند. به عنوان مثال ، یک پرتره در یک خانه خالی از سکنه ، که چشمانش به دنبال نشانگر شما بود ، یا انیمیشن های ظریف ناشی از نمایش داده های رسانه ای. این ایده ها و ویژگی ها در پیمایش متحرک می شوند. یک ایده اولیه یک اسم حیوان دست اموز زامبی بود که در امتداد محور X در پیمایش کاربر بالا می رود و ترجمه می شود.
آشنایی با API
قبل از اینکه بتوانیم با ویژگی های فردی و تخم مرغ های عید پاک شروع به بازی کنیم ، به یک کتاب احتیاج داشتیم. بنابراین ما تصمیم گرفتیم که این را به فرصتی تبدیل کنیم تا ویژگی های API انیمیشن های در حال ظهور و مرتبط با CSS را آزمایش کنیم. API انیمیشن های مرتبط با پیمایش در حال حاضر در هیچ مرورگری پشتیبانی نمی شود. با این حال ، در حالی که API را توسعه می دهد ، مهندسان تیم تعامل در حال کار بر روی یک پلی پیل هستند. این راهی برای آزمایش شکل 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;
}
}
ما با view-timeline-name
یک ViewTimeline
ایجاد می کنیم و محور آن را تعریف می کنیم. در این مثال ، 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;
}
}
با این کار ، "متحرک" با ورود به منظره ، مقیاس می کند و باعث چرخش "اسپینر" می شود.
آنچه من از آزمایش پیدا کردم این بود که API با Scroll-SNAP بسیار خوب کار می کند. Scroll-SNAP همراه با ViewTimeline
مناسب برای چرخش صفحه در یک کتاب است.
نمونه سازی مکانیک
بعد از آزمایش ، توانستم نمونه اولیه کتاب را دریافت کنم. شما به صورت افقی پیمایش می کنید تا صفحات کتاب را برگردانید.
در نسخه ی نمایشی ، می توانید محرک های مختلفی را که با مرزهای متراکم برجسته شده اند ، مشاهده کنید.
نشانه گذاری کمی شبیه به این است:
<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>
همانطور که پیمایش می کنید ، صفحات کتاب به نوبه خود می چرخند ، اما ضربه محکم و ناگهانی باز یا بسته می شود. این بستگی به تراز Scroll-SNAP محرک ها دارد.
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 وصل نمی کنیم ، اما از 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
این تیم در سال 2021 از Astro برای DesignCember استفاده کرد و من علاقه داشتم دوباره از آن برای 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 به نظر می رسند اما دارای حصار کد در قسمت بالای آن شبیه به Frontmatter هستند. این ما را قادر می سازد کارهایی مانند واردات سایر مؤلفه ها را انجام دهیم. مؤلفه صفحه سه به این شکل است:
---
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%;
}
در مورد جغد ما ، این یک ویژگی تعاملی است - یکی از بسیاری در این پروژه. این یک نمونه کوچک خوب برای عبور از آن است که نشان می دهد چگونه ما از ViewTimineline مشترک که ایجاد کردیم استفاده کردیم.
در سطح بالایی ، مؤلفه OWL ما مقداری SVG را وارد می کند و آن را با استفاده از قطعه 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
S) تولید می کنیم:
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()
در این بلوک کد ، ما دو کار انجام می دهیم:
- تنظیمات برگزیده حرکت کاربر را بررسی کنید.
- اگر ترجیح نمی دهند ، انیمیشن جغد را برای پیمایش پیوند دهید.
برای قسمت دوم ، OWL در محور y با استفاده از API انیمیشن های وب متحرک می شود. translate
خاصیت تراکم فردی استفاده می شود ، و به یک ViewTimeline
مرتبط است. این از طریق ویژگی timeline
به CHROMETOBER_TIMELINES[1]
مرتبط است. این یک ViewTimeline
است که برای چرخش صفحه ایجاد می شود. این انیمیشن OWL را با استفاده از مرحله 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 را بررسی کنید ، ممکن است متوجه چیزی شوید. شما تماشا می کنید! چشمان پرتره هنگام حرکت در صفحه ، نشانگر شما را دنبال می کند. ترفند در اینجا نقشه برداری مکان اشاره گر به یک مقدار ترجمه و انتقال آن به 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()
است تا رفتار را برای هر چشم متفاوت کند ، بنابراین می توانید بدون اینکه دوباره جاوا اسکریپت را لمس کنید ، هر چشم متفاوت رفتار کنید.
.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>
برای کتاب بود. ASO استفاده از عنصر<article>
برای هر بلوک محتوا ، و عناصر<abbr>
که در آن مخفف معرفی می شوند. فکر کردن در حالی که کتاب ساخته شده بود ، همه چیز را در دسترس تر می کرد. استفاده از عناوین و پیوندها باعث می شود که کاربر حرکت کند. استفاده از لیستی برای صفحات همچنین به این معنی است که تعداد صفحات توسط Assive Technology اعلام شده است. - اطمینان از اینکه همه تصاویر از ویژگی های مناسب
alt
استفاده می کنند. برای SVG های درون خطی ، عنصرtitle
در صورت لزوم موجود است. - استفاده از ویژگی های
aria
در جایی که آنها تجربه را بهبود می بخشند. استفاده ازaria-label
برای صفحات و طرفین آنها با کاربر در کدام صفحه قرار دارد. استفاده ازaria-describedBy
در پیوندهای "بیشتر بخوانید" متن بلوک محتوا را به هم می پیوندد. این امر ابهام را در مورد اینکه لینک کاربر را به دست می آورد ، از بین می برد. - در مورد موضوع بلوک های محتوا ، امکان کلیک بر روی کل کارت و نه تنها پیوند "بیشتر بخوانید" در دسترس است.
- استفاده از یک
IntersectionObserver
برای پیگیری اینکه کدام صفحات در حال مشاهده هستند ، زودتر آمده است. این مزایای بسیاری دارد که فقط مربوط به عملکرد نیست. صفحات در مشاهده هیچ انیمیشن یا تعامل متوقف نمی شوند. اما این صفحات همچنین ویژگیinert
را اعمال می کنند. این بدان معنی است که کاربرانی که از یک خواننده صفحه نمایش استفاده می کنند می توانند همان محتوای کاربران بینایی را کشف کنند. تمرکز در صفحه ای که در حال مشاهده است باقی مانده است و کاربران نمی توانند به صفحه دیگری بفرستند. - نکته آخر اینکه ما از پرس و جوهای رسانه ای برای احترام به اولویت کاربر برای حرکت استفاده می کنیم.
در اینجا یک تصویر از این بررسی وجود دارد که برخی از اقدامات موجود را برجسته می کند.
عنصر به عنوان کل کتاب مشخص می شود ، و این نشان می دهد که باید اصلی ترین مکان برای کاربران فناوری کمکی باشد. بیشتر در تصویر بیان شده است. "عرض =" 800 "ارتفاع =" 465 ">
آنچه یاد گرفتیم
The motivation behind Chrometober was not only to highlight web content from the community, but was also a way for us to test drive the scroll-linked animations API polyfill that's in development.
We set aside a session while on our team summit in New York to test the project and tackle issues that arose. The team's contribution was invaluable. It was also a great opportunity to list all the things that needed tackling before we could go live.
For example, testing out the book on devices raised a rendering issue. Our book wouldn't render as expected on iOS devices. Viewport units size the page, but when a notch was present, it affected the book. The solution was to use viewport-fit=cover
in the meta
viewport:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
This session also raised some issues with the API polyfill. Bramus raised these issues in the polyfill repository. He subsequently found solutions to those issues and got them merged into the polyfill. For example, this pull request made a performance gain by adding caching to part of the polyfill.
همین!
This has been a real fun project to work on, resulting in a whimsical scrolling experience that highlights amazing content from the community. Not only that, it's been great for testing the polyfill, as well as providing feedback to the engineering team to help improve the polyfill.
Chrometober 2022 is a wrap.
امیدواریم از آن لذت برده باشید! ویژگی مورد علاقه شما چیست؟ Tweet me and let us know!
You might even be able to grab some stickers from one of the team if you see us at an event .
Hero Photo by David Menidrey on Unsplash