इस Chrometober में, मज़ेदार और डरावने सुझाव और तरकीबें शेयर करने के लिए, स्क्रोल करने वाली किताब को कैसे तैयार किया गया.
Designcember के बाद, हमने इस साल आपके लिए Chrometober शुरू किया है. इसकी मदद से, हम कम्यूनिटी और Chrome टीम के वेब कॉन्टेंट को हाइलाइट और शेयर करेंगे. Designcember में कंटेनर क्वेरी के इस्तेमाल के बारे में बताया गया था. हालांकि, इस साल हम सीएसएस स्क्रोल-लिंक किए गए ऐनिमेशन एपीआई के बारे में बता रहे हैं.
web.dev/chrometober-2022 पर जाकर, किताब को स्क्रोल करने की सुविधा को आज़माएं.
खास जानकारी
इस प्रोजेक्ट का मकसद, स्क्रोल से जुड़े ऐनिमेशन एपीआई को हाइलाइट करके, लोगों को एक बेहतरीन अनुभव देना था. हालांकि, यह सुविधा दिलचस्प होने के साथ-साथ रिस्पॉन्सिव और ऐक्सेस करने लायक भी होनी चाहिए. यह प्रोजेक्ट, एपीआई पॉलीफ़िल को टेस्ट करने का एक बेहतरीन तरीका भी है. एपीआई पॉलीफ़िल, फ़िलहाल डेवलपमेंट में है. साथ ही, इस प्रोजेक्ट की मदद से अलग-अलग तकनीकों और टूल को भी आज़माया जा सकता है. साथ ही, इन सभी इवेंट में हैलोवीन की थीम का इस्तेमाल किया गया है!
हमारी टीम का स्ट्रक्चर कुछ ऐसा था:
- टायलर रीड: इलस्ट्रेशन और डिज़ाइन
- जेही टॉमकिन्स: आर्किटेक्चर और क्रिएटिव लीड
- Una Kravets: प्रोजेक्ट लीड
- ब्रैमस वैन डैम: साइट के लिए योगदान देने वाला व्यक्ति
- आदम आर्गाइल: सुलभता की समीक्षा
- ऐरोन फ़ोरिंटन: कॉपीराइटिंग
स्क्रोल करके पढ़ने लायक कहानी का ड्राफ़्ट तैयार करना
मई 2022 में, हमारी टीम के पहले ऑफ़साइट इवेंट के दौरान, Chrometober के लिए आइडिया मिलना शुरू हो गए थे. स्क्रिबल के कलेक्शन से हमें ऐसे तरीकों के बारे में सोचने में मदद मिली जिनसे उपयोगकर्ता, स्टोरीबोर्ड के किसी फ़ॉर्म पर स्क्रोल कर सके. वीडियो गेम से प्रेरणा लेकर, हमने कब्रिस्तान और प्रेतवाधित घर जैसे सीन के ज़रिए स्क्रोल करने का अनुभव दिया है.
Google के लिए अपना पहला प्रोजेक्ट बनाते समय, मुझे क्रिएटिविटी का पूरा फ़ायदा मिला. यह एक प्रोटोटाइप था, जिसमें यह दिखाया गया था कि उपयोगकर्ता कॉन्टेंट को कैसे नेविगेट कर सकता है.
जब उपयोगकर्ता बाईं या दाईं ओर स्क्रोल करता है, तो ब्लॉक घूमते हैं और स्केल इन होते हैं. हालांकि, मैंने इस आइडिया को छोड़ने का फ़ैसला किया, क्योंकि मुझे यह चिंता थी कि हम सभी साइज़ के डिवाइसों पर उपयोगकर्ताओं के लिए यह अनुभव कैसे बेहतर बना सकते हैं. इसके बजाय, मैंने उस डिज़ाइन का इस्तेमाल किया जो मैंने पहले बनाया था. साल 2020 में, मुझे रिलीज़ डेमो बनाने के लिए GreenSock के ScrollTrigger का ऐक्सेस मिला.
मैंने एक ऐसा डेमो बनाया था जिसमें 3D-CSS वाली किताब थी. इसमें स्क्रोल करने पर पेज पलटते थे. हमें लगा कि यह Chrometober के लिए सबसे सही है. स्क्रोल-लिंक किए गए ऐनिमेशन एपीआई, इस सुविधा के लिए एक बेहतर विकल्प है. यह scroll-snap
के साथ भी अच्छा काम करता है, जैसा कि आपको दिखेगा!
प्रोजेक्ट के लिए हमारे इलस्ट्रेटर, टाइलर रीड ने हमारे आइडिया के हिसाब से डिज़ाइन में बदलाव करने में काफ़ी अच्छा काम किया. टायलर ने लोगों के क्रिएटिव आइडिया को हकीकत में बदलने का बेहतरीन काम किया है. साथ मिलकर आइडिया पर चर्चा करना बहुत मज़ेदार था. हम चाहते थे कि यह सुविधा अलग-अलग ब्लॉक में बंटी हो. इस तरह, हम उन्हें सीन में जोड़ सकते हैं और फिर उनमें से अपनी पसंद के सीन चुन सकते हैं.
इसका मुख्य मकसद यह था कि उपयोगकर्ता किताब को पढ़ते समय, कॉन्टेंट के ब्लॉक ऐक्सेस कर सकें. वे कुछ हंसी-मज़ाक़ वाले तरीके से भी इंटरैक्ट कर सकते थे. जैसे, हमने इस अनुभव में ईस्टर अंडे जोड़े थे. उदाहरण के लिए, एक भूतहाउस में एक पोर्ट्रेट, जिसकी आंखें आपके पॉइंटर को फ़ॉलो करती थीं या मीडिया क्वेरी से ट्रिगर होने वाले छोटे ऐनिमेशन. स्क्रोल करने पर, इन आइडिया और सुविधाओं को ऐनिमेशन के साथ दिखाया जाएगा. शुरुआत में, एक ऐसा ज़ॉम्बी बनी हुई बनी थी जो उपयोगकर्ता के स्क्रोल करने पर, x-ऐक्सिस के साथ-साथ ऊपर और नीचे जाती थी.
एपीआई के बारे में जानकारी
अलग-अलग सुविधाओं और ईस्टर अंडे के साथ खेलने से पहले, हमें एक किताब की ज़रूरत थी. इसलिए, हमने इस अवसर का इस्तेमाल करके, CSS स्क्रोल-लिंक किए गए ऐनिमेशन एपीआई की सुविधाओं को टेस्ट करने का फ़ैसला किया है. फ़िलहाल, स्क्रोल से जुड़े ऐनिमेशन एपीआई का इस्तेमाल किसी भी ब्राउज़र में नहीं किया जा सकता. हालांकि, एपीआई को डेवलप करते समय, इंटरैक्शन टीम के इंजीनियर polyfill पर काम कर रहे हैं. इससे, एपीआई के डेवलप होने के दौरान, उसके आकार की जांच करने का तरीका मिलता है. इसका मतलब है कि हम इस एपीआई का इस्तेमाल आज ही कर सकते हैं. इस तरह के मज़ेदार प्रोजेक्ट, एक्सपेरिमेंटल सुविधाओं को आज़माने और सुझाव देने के लिए अक्सर एक बेहतरीन प्लैटफ़ॉर्म होते हैं. इस लेख में आगे जानें कि हमें क्या सीखने को मिला और हमने क्या सुझाव/राय दी.
इस एपीआई का इस्तेमाल करके, ऐनिमेशन को स्क्रोल से लिंक किया जा सकता है. ध्यान दें कि स्क्रोल करने पर ऐनिमेशन ट्रिगर नहीं किया जा सकता. ऐसा बाद में किया जा सकता है. स्क्रोल से जुड़े ऐनिमेशन भी दो मुख्य कैटगरी में आते हैं:
- वे जो स्क्रोल की पोज़िशन पर प्रतिक्रिया देते हैं.
- वे जो स्क्रोलिंग कंटेनर में किसी एलिमेंट की पोज़िशन पर प्रतिक्रिया देते हैं.
बाद वाले को बनाने के लिए, हम animation-timeline
प्रॉपर्टी के ज़रिए लागू किए गए ViewTimeline
का इस्तेमाल करते हैं.
यहां एक उदाहरण दिया गया है, जिसमें बताया गया है कि CSS में ViewTimeline
का इस्तेमाल करने पर क्या दिखता है:
.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;
}
}
इससे, "Spinner" के घूमने की प्रोसेस शुरू हो जाती है.
एक्सपेरिमेंट करने पर मुझे पता चला कि एपीआई, 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
को कनेक्ट नहीं करते, बल्कि JavaScript में 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-ऐक्सिस पर भी ट्रांसलेट करते हैं, ताकि वह किताब की तरह काम कर सके.
यह रही पूरी जानकारी
किताब के लिए मकैनिज्म तय करने के बाद, मैंने टायलर के इलस्ट्रेशन को हक़ीक़त में बदलने पर ध्यान दिया.
एस्ट्रो
हमारी टीम ने 2021 में Designcember के लिए Astro का इस्तेमाल किया था. मुझे 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
कॉम्पोनेंट से पॉप्युलेट हो जाता है. यह एक एस्ट्रो कॉम्पोनेंट (PageThree.astro
) है. ये कॉम्पोनेंट एचटीएमएल फ़ाइलों की तरह दिखते हैं, लेकिन इनमें सबसे ऊपर फ़्रंटमैटर की तरह कोड फ़ेंस होता है. इससे हमें अन्य कॉम्पोनेंट इंपोर्ट करने जैसे काम करने में मदद मिलती है. तीसरे पेज का कॉम्पोनेंट ऐसा दिखता है:
---
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 का इस्तेमाल करने का तरीका बताया है.
हाई लेवल पर, हमारा ओउल कॉम्पोनेंट कुछ एसवीजी इंपोर्ट करता है और 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-ऐक्सिस पर ऐनिमेट होता है. अलग-अलग ट्रांसफ़ॉर्म प्रॉपर्टी translate
का इस्तेमाल किया जाता है और इसे एक ViewTimeline
से लिंक किया जाता है. यह timeline
प्रॉपर्टी के ज़रिए CHROMETOBER_TIMELINES[1]
से जुड़ी है. यह एक ViewTimeline
है, जो पेज टर्न के लिए जनरेट होता है. यह enter
फ़ेज़ का इस्तेमाल करके, उल्लू के ऐनिमेशन को पेज टर्न से लिंक करता है. इससे यह तय होता है कि पेज के 80% हिस्से के घूमने पर, उल्लू को हिलाना शुरू करें. 90% तक पहुंचने पर, उल्लू का अनुवाद पूरा हो जाना चाहिए.
किताब की सुविधाएं
अब आपको पेज बनाने का तरीका और प्रोजेक्ट का आर्किटेक्चर काम करने का तरीका पता चल गया है. इस इमेज में देखा जा सकता है कि योगदान देने वाले लोग, अपनी पसंद के पेज या सुविधा पर कैसे काम कर सकते हैं. किताब की अलग-अलग सुविधाओं के ऐनिमेशन, किताब के पेज पलटने से जुड़े होते हैं. उदाहरण के लिए, पेज पलटने पर अंदर और बाहर उड़ने वाला चमगादड़.
इसमें ऐसे एलिमेंट भी हैं जो सीएसएस ऐनिमेशन की मदद से काम करते हैं.
किताब में कॉन्टेंट ब्लॉक जोड़ने के बाद, हमें दूसरी सुविधाओं के साथ क्रिएटिव होने का मौका मिला. इससे, कुछ अलग तरह के इंटरैक्शन जनरेट करने और चीज़ों को लागू करने के अलग-अलग तरीकों को आज़माने का मौका मिला.
चीज़ों को रिस्पॉन्सिव रखना
रिस्पॉन्सिव व्यूपोर्ट यूनिट, किताब और उसकी सुविधाओं का साइज़ तय करती हैं. हालांकि, फ़ॉन्ट को रिस्पॉन्सिव रखना एक दिलचस्प चुनौती थी. कंटेनर क्वेरी यूनिट यहां सही विकल्प हैं. हालांकि, यह सुविधा अभी तक हर जगह उपलब्ध नहीं है. किताब का साइज़ सेट है, इसलिए हमें कंटेनर क्वेरी की ज़रूरत नहीं है. सीएसएस 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>
कलर स्कीम की प्राथमिकता के आधार पर, अन्य बदलाव भी किए जा सकते हैं. दूसरे पेज पर मौजूद कद्दू, उपयोगकर्ता की कलर स्कीम की प्राथमिकता के हिसाब से बदलते हैं. इस्तेमाल किए गए एसवीजी में ऐसे सर्कल हैं जो लपटों को दिखाते हैं. ये सर्कल, डार्क मोड में स्केल अप और ऐनिमेट होते हैं.
.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 को देखा, तो आपको कुछ दिख सकता है. आपका वीडियो देखा जा रहा है! पेज पर कर्सर घुमाने पर, पोर्ट्रेट की आंखें आपके कर्सर को फ़ॉलो करेंगी. यहां ट्रिक यह है कि पॉइंटर की जगह को ट्रांसलेट वैल्यू से मैप करें और उसे सीएसएस में पास करें.
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 ऐक्सिस पर पॉइंटर की पोज़िशन को वैल्यू के तौर पर पास कर दिया जाता है. आंखों को घुमाते समय उनका सेंटर पॉइंट पाने के लिए, आंखों की डुप्लीकेट कॉपी बनाई जाती है. ओरिजनल आइटम, एक जगह से दूसरी जगह नहीं जाते, पारदर्शी होते हैं, और इनका इस्तेमाल रेफ़रंस के लिए किया जाता है.
इसके बाद, इसे एक साथ जोड़ें और आंखों पर सीएसएस कस्टम प्रॉपर्टी की वैल्यू अपडेट करें, ताकि आंखें मूव कर सकें. कोई फ़ंक्शन, window
के बजाय pointermove
इवेंट से बाउंड होता है. इस फ़ंक्शन के ट्रिगर होने पर, बीच के पॉइंट का हिसाब लगाने के लिए, हर आंख के बॉउंड का इस्तेमाल किया जाता है. इसके बाद, पॉइंटर की पोज़िशन को उन वैल्यू से मैप किया जाता है जिन्हें आंखों पर कस्टम प्रॉपर्टी वैल्यू के तौर पर सेट किया जाता है.
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)
})
}
वैल्यू को सीएसएस में पास करने के बाद, स्टाइल उनका इस्तेमाल अपनी ज़रूरत के हिसाब से कर सकती हैं. यहां सबसे अच्छी बात यह है कि सीएसएस 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;
}
ठीक उसी तरह जैसे हमारा पोर्ट्रेट window
पर pointermove
इवेंट को सुनता है, वैसे ही हमारा <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 को रिलीज़ से पहले सुलभता की समीक्षा के लिए तैयार करने में काफ़ी मददगार साबित हुई.
इसमें इन विषयों पर खास तौर पर चर्चा की गई है:
- यह पक्का करना कि इस्तेमाल किया गया एचटीएमएल, सिमेंटिक हो. इसमें, किताब के लिए
<main>
जैसे सही लैंडमार्क एलिमेंट शामिल हैं. साथ ही, हर कॉन्टेंट ब्लॉक के लिए<article>
एलिमेंट और जहां संक्षिप्त नाम इस्तेमाल किए गए हैं वहां<abbr>
एलिमेंट का इस्तेमाल भी शामिल है. किताब को बनाते समय, पहले से सोच-विचार करने से, उसे ज़्यादा सुलभ बनाया जा सका. हेडिंग और लिंक का इस्तेमाल करने से, उपयोगकर्ता को नेविगेट करने में आसानी होती है. पेजों के लिए सूची का इस्तेमाल करने का मतलब यह भी है कि सहायक टेक्नोलॉजी से पेजों की संख्या की जानकारी दी जाती है. - यह पक्का करना कि सभी इमेज में सही
alt
एट्रिब्यूट का इस्तेमाल किया गया हो. इनलाइन SVG के लिए,title
एलिमेंट ज़रूरी होने पर मौजूद होता है. aria
एट्रिब्यूट का इस्तेमाल वहां करना जहां इससे अनुभव बेहतर होता है. पेजों और उनके साइड के लिएaria-label
का इस्तेमाल करने से, उपयोगकर्ता को यह पता चलता है कि वह किस पेज पर है. "ज़्यादा पढ़ें" लिंक परaria-describedBy
का इस्तेमाल करने से, कॉन्टेंट ब्लॉक का टेक्स्ट दिखता है. इससे यह साफ़ तौर पर पता चलता है कि लिंक, उपयोगकर्ता को कहां ले जाएगा.- कॉन्टेंट ब्लॉक करने के विषय पर, सिर्फ़ "ज़्यादा पढ़ें" लिंक पर क्लिक करने के बजाय, पूरे कार्ड पर क्लिक करने की सुविधा उपलब्ध है.
IntersectionObserver
का इस्तेमाल करके, यह ट्रैक किया जा सकता है कि कौनसे पेज देखे जा रहे हैं. इससे कई फ़ायदे मिलते हैं, जो सिर्फ़ परफ़ॉर्मेंस से जुड़े नहीं हैं. जिन पेजों को नहीं देखा जा रहा है उन पर मौजूद कोई भी ऐनिमेशन या इंटरैक्शन रोक दिया जाएगा. हालांकि, इन पेजों परinert
एट्रिब्यूट भी लागू होता है. इसका मतलब है कि स्क्रीन रीडर का इस्तेमाल करने वाले लोग, ठीक वैसा ही कॉन्टेंट एक्सप्लोर कर सकते हैं जैसा कि सामान्य लोग करते हैं. फ़ोकस, दिख रहे पेज पर ही बना रहता है और उपयोगकर्ता किसी दूसरे पेज पर टैब नहीं कर सकते.- आखिर में, हम मीडिया क्वेरी का इस्तेमाल करके, उपयोगकर्ता की मोशन की प्राथमिकता को ध्यान में रखते हैं.
यहां समीक्षा का स्क्रीनशॉट दिया गया है, जिसमें कुछ उपायों के बारे में बताया गया है.
एलिमेंट को पूरी किताब के आस-पास के हिस्से के तौर पर पहचाना जाता है. इससे पता चलता है कि सहायक टेक्नोलॉजी का इस्तेमाल करने वाले लोगों के लिए, यह मुख्य लैंडमार्क होना चाहिए. ज़्यादा जानकारी के लिए, स्क्रीनशॉट देखें." width="800" height="465">
हमने क्या सीखा
Chrometober का मकसद, कम्यूनिटी के वेब कॉन्टेंट को हाइलाइट करना ही नहीं था. बल्कि, हम इस इवेंट का इस्तेमाल, स्क्रोल से जुड़े ऐनिमेशन वाले API के polyfill को टेस्ट करने के लिए भी कर रहे थे. फ़िलहाल, इस polyfill को डेवलप किया जा रहा है.
हमने न्यूयॉर्क में टीम के सम्मेलन के दौरान एक सेशन रखा था, ताकि प्रोजेक्ट की जांच की जा सके और आने वाली समस्याओं को हल किया जा सके. टीम का योगदान बहुत अहम था. यह एक अच्छा मौका था, ताकि हम उन सभी चीज़ों की सूची बना सकें जिन्हें लाइव जाने से पहले ठीक करना ज़रूरी था.
उदाहरण के लिए, डिवाइसों पर किताब की जांच करने पर, रेंडर करने से जुड़ी समस्या हुई. हमारी किताब, iOS डिवाइसों पर उम्मीद के मुताबिक रेंडर नहीं हो रही थी. व्यूपोर्ट यूनिट, पेज का साइज़ तय करती हैं. हालांकि, जब कोई नॉच मौजूद होता है, तो इसका असर किताब पर पड़ता है. इसका समाधान, meta
व्यूपोर्ट में viewport-fit=cover
का इस्तेमाल करना था:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
इस सेशन में, एपीआई पॉलीफ़िल से जुड़ी कुछ समस्याओं के बारे में भी बताया गया. Bramus ने पॉलीफ़िल रिपॉज़िटरी में इन समस्याओं के बारे में बताया है. इसके बाद, उन्होंने उन समस्याओं का हल ढूंढा और उन्हें पॉलीफ़िल में मर्ज कर दिया. उदाहरण के लिए, इस पुश अनुरोध ने पॉलीफ़िल के कुछ हिस्से में कैश मेमोरी जोड़कर, परफ़ॉर्मेंस को बेहतर बनाया.
हो गया!
इस प्रोजेक्ट पर काम करना काफ़ी मज़ेदार रहा. इससे, कम्यूनिटी के शानदार कॉन्टेंट को हाइलाइट करने वाला एक ऐसा स्क्रोलिंग अनुभव मिला है जो लोगों को पसंद आएगा. इतना ही नहीं, यह पॉलीफ़िल की जांच करने के साथ-साथ, इंजीनियरिंग टीम को सुझाव देने के लिए भी बहुत अच्छा है. इससे पॉलीफ़िल को बेहतर बनाने में मदद मिलती है.
Chrometober 2022 खत्म हो गया है.
हमें उम्मीद है कि आपको यह पसंद आया होगा! आपकी पसंदीदा सुविधा कौनसी है? मुझे ट्वीट करें और हमें बताएं!
किसी इवेंट में हमसे मिलने पर, आपको हमारी टीम से कुछ स्टिकर भी मिल सकते हैं.
Unsplash पर डेविड मेनिड्रे की हीरो फ़ोटो