Chrometober बनाया जा रहा है!

Chrometober के इस वर्शन में, स्क्रोलिंग बुक के मज़ेदार और डरावने सुझाव और ट्रिक शेयर करने का तरीका क्या है.

designcember के बाद, हम इस साल आपके लिए Chrometober बनाना चाहते हैं, ताकि समुदाय और Chrome टीम का वेब कॉन्टेंट हाइलाइट और शेयर किया जा सके. Designcember ने कंटेनर क्वेरी का इस्तेमाल दिखाने के बारे में बताया, लेकिन इस साल हम सीएसएस स्क्रोल से लिंक किए गए ऐनिमेशन एपीआई दिखा रहे हैं.

web.dev/chrometober-2022 पर जाकर, किताब को स्क्रोल करने की सुविधा देखें.

खास जानकारी

इस प्रोजेक्ट का मकसद, स्क्रोल से लिंक किए गए ऐनिमेशन एपीआई को हाइलाइट करके एक शानदार अनुभव देना था. हालांकि, अनोखा होने के साथ-साथ, अनुभव ऐसा होना चाहिए जो रिस्पॉन्सिव हो और लोगों तक आसानी से पहुंच सके. यह प्रोजेक्ट, एपीआई पॉलीफ़िल को टेस्ट करने का अच्छा तरीका रहा है, जो अभी काम कर रहा है. साथ ही, इसमें अलग-अलग तकनीकों और टूल के कॉम्बिनेशन भी आज़माए जा सकते हैं. और सब कुछ त्योहार वाली हैलोवीन थीम के साथ!

हमारी टीम का स्ट्रक्चर कुछ ऐसा था:

स्क्रोलिंग एक्सपीरियंस ड्राफ़्ट करना

मई 2022 में, हमारी पहली टीम ऑफ़साइट पर Chrometober के आइडिया आने शुरू हुए. स्क्रिबल के संग्रह को लोगों ने ऐसे तरीकों के बारे में सोचने के लिए कहा जिनकी मदद से वे स्टोरीबोर्ड के किसी रूप को स्क्रोल कर सकें. वीडियो गेम से प्रेरित होकर, हमने वीडियो को स्क्रोल करने का अनुभव लिया. जैसे, कब्रिस्तान और भूतिया घर.

एक नोटबुक, डेस्क पर रखी है. इसमें प्रोजेक्ट से जुड़े अलग-अलग डूडल और आड़ी-तिरछी लाइनें, दोनों मौजूद हैं.

अपने पहले Google प्रोजेक्ट को एक अनचाही दिशा में आगे बढ़ने की क्रिएटिव आज़ादी मेरे लिए काफ़ी रोमांचक थी. यह इस बात का शुरुआती प्रोटोटाइप था कि उपयोगकर्ता, कॉन्टेंट को किस तरह देख सकता है.

जैसे-जैसे उपयोगकर्ता तिरछा स्क्रोल करता है, वैसे-वैसे ब्लॉक घूमते और स्केल इन होते हैं. हालांकि, मैंने इस चिंता को छोड़ दिया कि हम सभी तरह के डिवाइसों पर इस सुविधा को बेहतर कैसे बना सकते हैं. इसके बजाय, मैंने किसी ऐसी चीज़ के डिज़ाइन पर फ़ोकस किया जो मैंने पहले बनाई थी. साल 2020 में, मेरी किस्मत अच्छी थी कि मुझे greenSock's ScrollTrigger का ऐक्सेस मिला. इस टूल का इस्तेमाल करके, मुझे रिलीज़ डेमो बनाना था.

मैंने जो डेमो बनाया उसमें से एक 3D-सीएसएस बुक थी, जिसमें स्क्रोल करते ही पेज पलट जाते थे. यह Chrometober के हिसाब से ज़्यादा सही लगा. स्क्रोल-लिंक किए गए ऐनिमेशन एपीआई, इस सुविधा के लिए एक बेहतरीन विकल्प है. यह scroll-snap के साथ भी अच्छी तरह से काम करता है, जैसा कि आपने देखा होगा!

प्रोजेक्ट के लिए हमारे इलस्ट्रेटर, टाइलर रीड ने डिज़ाइन में बेहतरीन बदलाव किया, क्योंकि हमने आइडिया बदले. टायलर ने अपने अंदर बताए गए सभी क्रिएटिव आइडिया को अच्छी तरह से जीवन में उतारकर, उन्हें हकीकत में बदला. साथ मिलकर सोच-विचार करने में यह काफ़ी मज़ेदार था. इसे बेहतर बनाने के लिए, हम चाहते थे कि सुविधाओं को अलग-अलग ब्लॉक में बांटा जाए. इस तरह, हम उन्हें सीन के तौर पर इस्तेमाल करके, यह तय कर सकते हैं कि दर्शकों को किस तरह के सीन दिखाए जाएं.

इस कंपोज़िशन के एक सीन में सांप, ताबूत, और हाथ बाहर निकलते हुए दिखाया गया है. एक कड़ाही में छड़ी के साथ एक लोमड़ी, डरावने चेहरे वाला पेड़, और कद्दू की लालटेन पकड़े हुए एक गार्गोयल की इमेज है.

मुख्य आइडिया यह था कि जैसे-जैसे लोग किताब को पढ़ें, वैसे-वैसे वे कॉन्टेंट के ब्लॉक ऐक्सेस कर पाएं. वे सनसनीखेज़ डैश के साथ इंटरैक्ट भी कर सकते थे, जैसे कि इसे बनाने के दौरान हमने ईस्टर के अंडों को दिखाया था. उदाहरण के लिए, किसी भूतिया घर में किसी ऐसी तस्वीर का पोर्ट्रेट, जिसकी नज़रें आपके पॉइंटर को फ़ॉलो करती हों या मीडिया क्वेरी से ट्रिगर हुए सूक्ष्म ऐनिमेशन. ये आइडिया और सुविधाएं स्क्रोल करने पर ऐनिमेशन के तौर पर दिखेंगी. शुरुआती आइडिया था एक ज़ॉम्बी खरगोश, जो उपयोगकर्ता स्क्रोल करने पर x-ऐक्सिस के साथ उठता और अनुवाद करता.

एपीआई के बारे में जानना

इससे पहले कि हम अलग-अलग सुविधाओं और ईस्टर अंडों के साथ खेलना शुरू कर सकें, हमें एक किताब की ज़रूरत थी. इसलिए, हमने इस सुविधा को नए, CSS स्क्रोल से लिंक किए गए ऐनिमेशन एपीआई के लिए, फ़ीचरसेट की जांच करने का मौका दिया. स्क्रोल-लिंक किए गए ऐनिमेशन एपीआई फ़िलहाल किसी भी ब्राउज़र पर काम नहीं करते. हालांकि, एपीआई डेवलप करते समय, इंटरैक्शन टीम के इंजीनियर polyfill पर काम कर रहे हैं. इससे, एपीआई के डेवलप होने के दौरान उसका आकार टेस्ट किया जा सकता है. इसका मतलब है कि हम आज भी इस एपीआई का इस्तेमाल कर सकते हैं. एक्सपेरिमेंट के तौर पर उपलब्ध सुविधाओं को आज़माने और सुझाव देने के लिए, इस तरह के मज़ेदार प्रोजेक्ट का इस्तेमाल अक्सर किया जा सकता है. इस लेख में जानें कि हमने क्या सीखा और क्या सुझाव दिया.

बड़े लेवल पर, स्क्रोल करने के लिए ऐनिमेशन लिंक करने के लिए इस एपीआई का इस्तेमाल किया जा सकता है. इस बात पर ध्यान देना ज़रूरी है कि स्क्रोल करने पर ऐनिमेशन को ट्रिगर नहीं किया जा सकता. ऐसा बाद में हो सकता है. स्क्रोल से लिंक किए गए ऐनिमेशन भी दो मुख्य कैटगरी में आते हैं:

  1. स्क्रोल करने की पोज़िशन पर प्रतिक्रिया देने वाले लोग.
  2. ऐसे विज्ञापन जो स्क्रोल करने वाले कंटेनर में किसी एलिमेंट की पोज़िशन पर प्रतिक्रिया देते हैं.

बाद वाला विकल्प बनाने के लिए, हम animation-timeline प्रॉपर्टी के ज़रिए लागू किए गए ViewTimeline का इस्तेमाल करते हैं.

यहां एक उदाहरण दिया गया है कि सीएसएस में 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;
  }
}

इसके साथ,"मूवर", व्यूपोर्ट में आते ही बड़ा हो जाता है और "स्पिनर" का रोटेशन ट्रिगर करता है.

प्रयोग करने से मुझे पता चला कि एपीआई scroll-sनैप के साथ बहुत अच्छी तरह से काम करता है. 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 इंस्टेंस को पास किया जाता है. हर पेज में कॉन्टेंट डालने के लिए, हर पेज पर एस्ट्रो की स्लॉट सुविधा का इस्तेमाल किया जाता है.

<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 की शेयर की गई स्पीड का इस्तेमाल कैसे किया है.

बड़े लेवल पर, उल्लू का कॉम्पोनेंट कुछ SVG फ़ाइल इंपोर्ट करता है और ऐस्ट्रो फ़्रैगमेंट का इस्तेमाल करके उसे इनलाइन करता है.

---
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% का इस्तेमाल किया जाता है.

मज़ेदार बात यह है कि हम उल्लू को हमारे जनरेट किए गए ViewTimelines में से किसी एक से जोड़ते हैं:

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()

कोड के इस ब्लॉक में, हम दो काम करते हैं:

  1. उपयोगकर्ता की मोशन प्राथमिकताएं देखें.
  2. अगर आपने कोई इमेज पसंद नहीं की है, तो स्क्रोल करने के लिए उल्लू का कोई ऐनिमेशन लिंक करें.

दूसरे हिस्से में, उल्लू y-ऐक्सिस पर Web ऐनिमेशन API का इस्तेमाल करके ऐनिमेट करता है. व्यक्तिगत रूपांतरण प्रॉपर्टी 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>

कलर स्कीम की पसंद के हिसाब से, अन्य बदलाव भी किए जा सकते हैं. दूसरे पेज पर मौजूद पंपकिन, उपयोगकर्ता की कलर स्कीम की प्राथमिकता के हिसाब से काम करते हैं. इस्तेमाल किए गए 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 को देखते हैं, तो आपको कुछ ऐसा दिख सकता है. आपके वीडियो देखे जा रहे हैं! जैसे-जैसे आप पेज पर जाएंगे, पोर्ट्रेट की आंखें आपके पॉइंटर के साथ-साथ चलेंगी. यहां ट्रिक है कि पॉइंटर की जगह को अनुवाद की वैल्यू पर मैप करना और उसे सीएसएस को पास करना.

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 एट्रिब्यूट का इस्तेमाल किया गया हो. इनलाइन SVGs के लिए, जहां ज़रूरी हो वहां title एलिमेंट मौजूद होता है.
  • बेहतर अनुभव देने के लिए, aria एट्रिब्यूट का इस्तेमाल करना. पेजों और उनके किनारों के लिए aria-label का इस्तेमाल करने से, उपयोगकर्ता को पता चलता है कि वे किस पेज पर हैं. "ज़्यादा पढ़ें" लिंक पर aria-describedBy का इस्तेमाल करने से, कॉन्टेंट ब्लॉक के टेक्स्ट के बारे में पता चलता है. इससे यह साफ़ तौर पर नहीं पता चलता कि उपयोगकर्ता को लिंक कहां ले जाएगा.
  • कॉन्टेंट ब्लॉक के विषय पर, पूरे कार्ड पर क्लिक करने की सुविधा के साथ-साथ "ज़्यादा पढ़ें" लिंक भी मौजूद होता है.
  • कौनसे पेज व्यू में हैं, यह ट्रैक करने के लिए IntersectionObserver का इस्तेमाल पहले दिखाया गया है. इसके कई फ़ायदे हैं, जो सिर्फ़ परफ़ॉर्मेंस से जुड़े नहीं हैं. जो पेज व्यू में नहीं हैं उन पर कोई ऐनिमेशन या इंटरैक्शन रोक दिया जाएगा. हालांकि, इन पेजों पर inert एट्रिब्यूट भी लागू किया गया है. इसका मतलब है कि स्क्रीन रीडर का इस्तेमाल करने वाले लोग, उन सभी कॉन्टेंट को खोज सकते हैं जो देख सकते हैं. फ़ोकस, व्यू में दिख रहे पेज पर ही रहता है. साथ ही, उपयोगकर्ता किसी दूसरे पेज पर टैब नहीं ले जा सकते.
  • आखिर में, हम मोशन से जुड़ी उपयोगकर्ता की पसंद के हिसाब से मीडिया क्वेरी का भी इस्तेमाल करते हैं.

यहां पर समीक्षा का एक स्क्रीनशॉट दिया गया है, जिसमें कुछ उपायों को हाइलाइट किया गया है.

तत्व की पहचान पूरी किताब के चारों ओर से की गई है. इससे पता चलता है कि यह सहायक टेक्नोलॉजी उपयोगकर्ताओं को ढूंढने के लिए मुख्य लैंडमार्क होना चाहिए. स्क्रीनशॉट में ज़्यादा जानकारी दी गई है." एक्सटेंशन की चौड़ाई="800"height="465">

Chrometober की किताब खुली होने का स्क्रीनशॉट. हरे रंग के आउटलाइन वाले बॉक्स, यूज़र इंटरफ़ेस (यूआई) के अलग-अलग पहलुओं के आस-पास दिए गए हैं. इनमें, सही सुलभता सुविधाओं और पेज से मिलने वाले उपयोगकर्ता अनुभव के बारे में जानकारी दी गई है. उदाहरण के लिए, इमेज में वैकल्पिक लेख होता है. दूसरा उदाहरण एक सुलभता लेबल है. इससे यह पता चलता है कि जिन पेजों को स्क्रीन पर नहीं दिखाया जा रहा है वे इनऐक्टिव हैं. स्क्रीनशॉट में ज़्यादा जानकारी दी गई है.

हमने क्या सीखा

Chrometober के पीछे की वजह न सिर्फ़ कम्यूनिटी का वेब कॉन्टेंट हाइलाइट करना है, बल्कि इससे हमारे लिए स्क्रोल-लिंक किए गए ऐनिमेशन एपीआई पॉलीफ़िल को टेस्ट करने में भी मदद मिली, जिस पर अभी काम चल रहा है.

हमने न्यूयॉर्क में अपनी टीम समिट के दौरान एक सेशन रखा. इस दौरान, हमने प्रोजेक्ट की जांच की और सामने आने वाली समस्याओं को हल किया. टीम का योगदान अनमोल था. साथ ही, यह मौका था कि लाइव जाने से पहले, जिन चीज़ों को हल करने की ज़रूरत थी उनकी जानकारी दें.

कॉन्फ़्रेंस रूम में, सीएसएस, यूज़र इंटरफ़ेस (यूआई), और DevTools की टीम टेबल के आस-पास बैठी है. ऊना, व्हाइटबोर्ड के सामने खड़ा है, जिस पर स्टिकी नोट दिख रहे हैं. टीम के अन्य सदस्य टेबल के आस-पास बैठे हैं. उनके पास स्नैक्स और लैपटॉप हैं.

उदाहरण के लिए, डिवाइस पर किताब को टेस्ट करने से रेंडरिंग की समस्या सामने आई. हमारी किताब iOS डिवाइसों पर उम्मीद के मुताबिक रेंडर नहीं होगी. व्यूपोर्ट इकाइयां पेज को आकार देती हैं, लेकिन एक पायदान मौजूद होने पर इसका असर किताब पर पड़ा. इस समस्या को हल करने के लिए, meta के व्यूपोर्ट में viewport-fit=cover का इस्तेमाल करना था:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

इस सेशन में, एपीआई पॉलीफ़िल से जुड़ी कुछ समस्याएं भी बताई गई थीं. ब्रैमस ने पॉलीफ़िल रिपॉज़िटरी में इन समस्याओं के बारे में बताया था. बाद में, उन्हें इन समस्याओं के हल मिल गए और उन्हें पॉलीफ़िल में मर्ज कर दिया. उदाहरण के लिए, इस पुल अनुरोध ने पॉलीफ़िल के हिस्से में कैश मेमोरी जोड़कर परफ़ॉर्मेंस में सुधार किया.

Chrome में खुले हुए डेमो का स्क्रीनशॉट. डेवलपर टूल खुले हुए हैं और वे बेसलाइन परफ़ॉर्मेंस मेज़रमेंट दिखाते हैं.

Chrome में खुले हुए डेमो का स्क्रीनशॉट. डेवलपर टूल खुले हैं और यह बेहतर परफ़ॉर्मेंस मेज़रमेंट दिखाते हैं.

हो गया!

यह एक मज़ेदार प्रोजेक्ट है, जिस पर काम किया जा सकता है. यह स्क्रोल करने का एक अनोखा अनुभव है, जिसमें कम्यूनिटी के शानदार कॉन्टेंट को हाइलाइट किया जाता है. इतना ही नहीं, यह पॉलीफ़िल की जांच करने के साथ-साथ, इंजीनियरिंग टीम को सुझाव देना भी बहुत अच्छा रहता है. इससे पॉलीफ़िल को बेहतर बनाने में मदद मिलती है.

Chrometober 2022 अब खत्म हो चुका है.

उम्मीद है कि आपको यह पसंद आया होगा! आपकी पसंदीदा सुविधा क्या है? मुझे ट्वीट करें और हमें बताएं!

जे ने Chrometober के किरदारों की एक स्टिकर शीट पकड़ी है.

अगर आप हमें किसी इवेंट में देखते हैं, तो आप टीम की किसी टीम के कुछ स्टिकर भी ले सकते हैं.

Unsplash पर डेविड मेन्ड्रे की हीरो फ़ोटो