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

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

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

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

खास जानकारी

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

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

स्क्रोल करके पढ़ने लायक कहानी का ड्राफ़्ट तैयार करना

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

डेस्क पर एक नोटबुक है, जिसमें प्रोजेक्ट से जुड़े कई डूडल और स्क्रिबल हैं.

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

जब उपयोगकर्ता बाईं या दाईं ओर स्क्रोल करता है, तो ब्लॉक घूमते हैं और स्केल इन होते हैं. हालांकि, मैंने सोचा था कि इस आइडिया की मदद से हम सभी तरह के डिवाइसों का इस्तेमाल करने वाले लोगों के लिए इस अनुभव को बेहतरीन बना सकते हैं. ऐसा इस सोच के साथ किया जा रहा है. इसके बजाय, मैंने उस डिज़ाइन का इस्तेमाल किया जो मैंने पहले बनाया था. साल 2020 में, मुझे रिलीज़ डेमो बनाने के लिए GreenSock के ScrollTrigger का ऐक्सेस मिला.

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

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

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

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

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

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

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

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

बाद वाले को बनाने के लिए, हम 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()

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

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

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

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

Chrometober का मकसद, कम्यूनिटी के वेब कॉन्टेंट को हाइलाइट करना ही नहीं था. बल्कि, हम इस इवेंट का इस्तेमाल, स्क्रोल से जुड़े ऐनिमेशन वाले API के polyfill को टेस्ट करने के लिए भी कर रहे थे. फ़िलहाल, इस polyfill को डेवलप किया जा रहा है.

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

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

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

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

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

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

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

हो गया!

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

Chrometober 2022 अब एक खास बात है.

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

Jhey, Chrometober के किरदारों की स्टिकर शीट पकड़े हुए.

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

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