كيف تم إنشاء الكتاب المتحرك لمشاركة النصائح والحيل الممتعة والمخيفة في شهر Chrometober
بعد شهر التصميم، أردنا إنشاء شهر Chrometober هذا العام كطريقة للتركيز على محتوى الويب من المنتدى وفريق Chrome ومشاركته. في Designcember، تم عرض استخدام طلبات بحث الحاوية، ولكننا سنعرض هذا العام واجهة برمجة التطبيقات للحركات المرتبطة بميزة الانتقال إلى أعلى الصفحة أو أسفلها في CSS.
يمكنك الاطّلاع على تجربة التصفّح في الكتاب على الرابط web.dev/chrometober-2022.
نظرة عامة
كان الهدف من المشروع هو تقديم تجربة خيالية تسلّط الضوء على واجهة برمجة التطبيقات للرسوم المتحرّكة المرتبطة بميزة الانتقال إلى الأسفل أو الأعلى. ومع أنّها كانت مرحة، كان يجب أن تكون التجربة سريعة الاستجابة ويسهل الوصول إليها أيضًا. كان المشروع أيضًا طريقة رائعة لتجربة polyfill لواجهة برمجة التطبيقات التي لا تزال قيد التطوير، بالإضافة إلى تجربة أساليب وأدوات مختلفة معًا. وكل ذلك بتصميم احتفالي لعيد الهالوين.
كان هيكل فريقنا على النحو التالي:
- تايلر ريد: الرسم التوضيحي والتصميم
- Jhey Tompkins: مدير التصميم والجانب الإبداعي
- Una Kravets: مسؤولة المشروع
- براموس فان دام: مساهم في الموقع الإلكتروني
- آدم أرجيل: مراجعة تسهيل الاستخدام
- آرون فورينتون: كتابة النصوص
صياغة تجربة سردية أثناء الانتقال
بدأت أفكار Chrometober تتدفق في أول رحلة خارجية لفريقنا في أيار (مايو) 2022. من خلال مجموعة من الرسومات، فكرنا في طرق يمكن للمستخدم من خلالها الانتقال إلى أي جزء من لوحة القصة. استوحينا من ألعاب الفيديو تجربة الانتقال عبر مشاهد مثل المقابر والمنازل المسكونة.
كان من المثير الحصول على الحرية الإبداعية لتوجيه مشروعي الأول في Google في اتجاه غير متوقّع. كان هذا نموذجًا أوليًا مبكّرًا لكيفية تنقّل المستخدم في المحتوى.
بينما ينتقل المستخدم إلى الجانب، يتم تدوير الكتل وتوسيعها. ولكنني قررت الابتعاد عن هذه الفكرة بسبب قلقنا بشأن كيفية تقديم تجربة رائعة للمستخدمين على الأجهزة بجميع أحجامها. بدلاً من ذلك، اتّجهت إلى تصميم شيء سبق أن أنشأته. في عام 2020، كان من حسن حظي أن أحصل على ScrollTrigger من GreenSock لإنشاء نماذج تجريبية للإصدار.
كان أحد العروض التوضيحية التي أنشأتها هو كتاب بتنسيق CSS ثلاثي الأبعاد يتم فيه قلب الصفحات أثناء الانتقال للأعلى أو للأسفل، وبدا هذا النموذج أكثر ملاءمةً لما أردناه في Chrometober. وتعدّ واجهة برمجة التطبيقات للصور المتحركة المرتبطة بالتمرير بديلاً مثاليًا لهذه الوظيفة. يعمل هذا الإجراء أيضًا بشكل جيد مع scroll-snap
، كما سترى.
كان رسّام المشروع، تايلر ريد، رائعًا في تغيير التصميم عندما غيّرنا الأفكار. لقد أبدع "تايلر" في تنفيذ كل الأفكار الإبداعية التي تم تقديمها له. لقد كان من الممتع جدًا تبادل الأفكار معًا. كان من المهم أن تكون الميزات مجزّأة إلى وحدات منفصلة. بهذه الطريقة، يمكننا إنشاء مشاهد ثم اختيار ما نريد عرضه.
كانت الفكرة الرئيسية هي أنّه يمكن للمستخدم الوصول إلى مجموعات من المحتوى أثناء تصفّحه للكتاب. ويمكنهم أيضًا التفاعل مع لمسات من الخيال، بما في ذلك الرموز المخفية التي أضفناها إلى التجربة، مثل صورة بورتريه في منزل مسكون تتبع عيون هذا الشخص مؤشرك، أو صور متحركة خفيفة يتم تشغيلها من خلال طلبات البحث عن الوسائط. ستظهر هذه الأفكار والميزات متحركة عند الانتقال إلى الأسفل أو للأعلى. كانت الفكرة الأولى هي أرنب زومبي يرتفع ويتحرك على طول محور x عندما ينتقل المستخدم للأعلى أو للأسفل.
التعرّف على واجهة برمجة التطبيقات
قبل أن نتمكّن من البدء في استخدام الميزات الفردية والرموز المخفية، احتجنا إلى كتاب. لذلك، قرّرنا تحويل هذه الفرصة إلى فرصة لاختبار مجموعة الميزات لواجهة برمجة التطبيقات CSS API للرسوم المتحرّكة المرتبطة بميزة الانتقال إلى أعلى الصفحة أو أسفلها. لا تتوفّر حاليًا واجهة برمجة التطبيقات للرسوم المتحرّكة المرتبطة بالتمرير في أي متصفّحات. ومع ذلك، أثناء تطوير واجهة برمجة التطبيقات، عمل المهندسون في فريق التفاعلات على polyfill. ويوفّر ذلك طريقة لاختبار شكل واجهة برمجة التطبيقات أثناء تطويرها. وهذا يعني أنّه يمكننا استخدام واجهة برمجة التطبيقات هذه اليوم، وغالبًا ما تكون المشاريع الممتعة مثل هذا المشروع مكانًا رائعًا لتجربة الميزات التجريبية وتقديم الملاحظات. في وقت لاحق من المقالة، يمكنك الاطّلاع على ما تعلّمناه والملاحظات التي تمكّنا من تقديمها.
بشكل عام، يمكنك استخدام واجهة برمجة التطبيقات هذه لربط الصور المتحركة بالانتقال للأعلى أو للأسفل. يُرجى العلم أنّه لا يمكنك تشغيل صورة متحركة عند الانتقال للأعلى أو للأسفل، ولكن قد نضيف هذه الميزة لاحقًا. تندرج الرسوم المتحرّكة المرتبطة بالتمرير أيضًا ضمن فئتين رئيسيتين:
- العناصر التي تتفاعل مع موضع التمرير
- هي العناصر التي تستجيب لموضع عنصر في حاوية التمرير.
لإنشاء هذا الأخير، نستخدم 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;
}
}
وبفضل ذلك، يتم تكبير "المتحرك" عند دخوله إطار العرض، ما يؤدي إلى بدء دوران "المشغِّل".
تبيّن لي من خلال التجربة أنّ واجهة برمجة التطبيقات تعمل بشكل جيد جدًا مع 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 في 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 ويضمّنها باستخدام عنصر 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>
عند مناقشة خلفيات الصفحات في وقت سابق. كانت أونا حريصة على إجراء تفاعل يستجيب لإعدادات نظام الألوان المفضّلة. نتيجةً لذلك، تتيح الخلفيات استخدام الوضع الفاتح والداكن مع خيارات مختلفة. ولأنّه يمكنك استخدام طلبات البحث عن الوسائط مع العنصر <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()
لجعل السلوك مختلفًا لكل عين، حتى تتمكّن من جعل كل عين تتصرف بشكل مختلف بدون لمس 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
على هذه الصفحات. وهذا يعني أنّ المستخدمين الذين يستخدمون قارئ شاشة يمكنهم استكشاف المحتوى نفسه الذي يستكشفه المستخدمون المبصرون. يبقى التركيز ضمن الصفحة المعروضة ولا يمكن للمستخدمين الانتقال إلى صفحة أخرى باستخدام مفتاح التبويب. - أخيرًا وليس آخرًا، نستخدم طلبات البحث عن الوسائط للالتزام بخيار المستخدم المفضّل بشأن الصور المتحركة.
في ما يلي لقطة شاشة من المراجعة التي تُبرز بعض الإجراءات المتّبعة.
تم تحديد العنصر على أنّه حول الكتاب بأكمله، ما يشير إلى أنّه يجب أن يكون المَعلم الرئيسي الذي يمكن لمستخدمي التكنولوجيا المساعِدة العثور عليه. يمكنك الاطّلاع على مزيد من التفاصيل في لقطة الشاشة." width="800" height="465">
الاستنتاجات التي توصّلنا إليها
لم يكن الدافع وراء إطلاق Chrometober هو تسليط الضوء على محتوى الويب من المنتدى فحسب، بل كان أيضًا طريقة لنا لاختبار polyfill لواجهة برمجة التطبيقات للرسوم المتحرّكة المرتبطة بالانتقال إلى الأسفل أو للأعلى والتي لا تزال قيد التطوير.
لقد خصّصنا جلسة أثناء قمة فريقنا في نيويورك لاختبار المشروع ومعالجة المشاكل التي ظهرت. كانت مساهمة الفريق قيّمة للغاية. وقد كانت هذه فرصة رائعة أيضًا لتوضيح كل الأمور التي يجب معالجتها قبل نشر التطبيق.
على سبيل المثال، أدى اختبار الكتاب على الأجهزة إلى ظهور مشكلة في العرض. لا يتم عرض كتابنا على النحو المتوقّع على أجهزة iOS. تحدِّد وحدات إطار العرض حجم الصفحة، ولكن عندما يكون هناك نُقطة في الشاشة، يؤثّر ذلك في الكتاب. كان الحلّ هو استخدام viewport-fit=cover
في إطار العرض meta
:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
أثارت هذه الجلسة أيضًا بعض المشاكل في ميزة polyfill لواجهة برمجة التطبيقات. أبلغ فريق Bramus عن هذه المشاكل في مستودع polyfill. بعد ذلك، عثر على حلول لهذه المشاكل وتم دمجها في الpolyfill. على سبيل المثال، أدّى طلب سحب هذا الرمز البرمجي إلى تحسين الأداء من خلال إضافة ميزة التخزين المؤقت إلى جزء من البوليفيل.
هذا كل شيء!
لقد كان هذا المشروع ممتعًا للغاية، ما أدى إلى توفير تجربة تصفّح ممتعة تُبرز المحتوى الرائع من المنتدى. بالإضافة إلى ذلك، كان من الرائع اختبار الpolyfill، بالإضافة إلى تقديم ملاحظات إلى فريق المهندسين للمساعدة في تحسين الpolyfill.
لقد انتهى شهر Chrometober 2022.
نأمل أن تكون هذه المقالة مفيدة. ما هي الميزة المفضّلة لديك؟ يُرجى مراسلتي على Twitter وإعلامنا برأيك.
يمكنك أيضًا الحصول على بعض الملصقات من أحد أعضاء الفريق إذا قابلتنا في أحد الأحداث.
الصورة الرئيسية لأحد الفنادق في David Menidrey على Unsplash