جارٍ إنشاء Chrometober.

كيف تم إنشاء الكتاب المتحرك لمشاركة النصائح والحيل الممتعة والمخيفة في شهر Chrometober

بناءً على ما ورد في موقع Designcember، طرحنا تصميم Chrometober من أجلك هذا العام كوسيلة لإبراز ومشاركة محتوى الويب من المنتدى وفريق Chrome. في Designcember، تم عرض استخدام طلبات بحث الحاوية، ولكننا سنعرض هذا العام واجهة برمجة التطبيقات للحركات المرتبطة بميزة الانتقال إلى أعلى الصفحة أو أسفلها في CSS.

يمكنك الاطّلاع على تجربة التصفّح في الكتاب على الرابط web.dev/chrometober-2022.

نظرة عامة

كان الهدف من المشروع هو تقديم تجربة خيالية تسلّط الضوء على واجهة برمجة التطبيقات للرسوم المتحرّكة المرتبطة بميزة الانتقال للأعلى أو للأسفل. ومع أنّها كانت مرحة، كان يجب أن تكون التجربة سريعة الاستجابة ويسهل الوصول إليها أيضًا. كان المشروع أيضًا طريقة رائعة لتجربة polyfill لواجهة برمجة التطبيقات التي لا تزال قيد التطوير، بالإضافة إلى تجربة أساليب وأدوات مختلفة معًا. وكل ذلك بتصميم احتفالي لعيد الهالوين.

كان هيكل فريقنا على النحو التالي:

صياغة تجربة سردية أثناء الانتقال

بدأت أفكار Chrometober تتدفق في أول رحلة خارجية لفريقنا في أيار (مايو) 2022. من خلال مجموعة من الرسومات، فكرنا في طرق يمكن للمستخدم من خلالها الانتقال إلى شكل من أشكال لوحة العمل. استوحينا من ألعاب الفيديو تجربة الانتقال عبر مشاهد مثل المقابر والمنازل المسكونة.

دفتر ملاحظات على مكتب مع رسومات وخربشات مختلفة متعلقة بالمشروع

كان من المثير الحصول على الحرية الإبداعية لتوجيه مشروعي الأول في Google في اتجاه غير متوقّع. كان هذا نموذجًا أوّليًا مبكرًا لكيفية تنقل المستخدم خلال المحتوى.

بينما ينتقل المستخدم إلى الجانب، يتم تدوير الكتل وتوسيعها. ولكنني قررت الابتعاد عن هذه الفكرة لأنّني كنت أفكر في كيفية تقديم تجربة رائعة للمستخدمين على الأجهزة بجميع أحجامها. بدلاً من ذلك، اتّجهت إلى تصميم شيء سبق أن أنشأته. في عام 2020، كان من حسن حظي أن أحصل على ScrollTrigger من GreenSock لإنشاء نماذج تجريبية للإصدار.

كان أحد العروض التوضيحية التي أنشأتها هو كتاب بتنسيق CSS ثلاثي الأبعاد يتم فيه قلب الصفحات أثناء الانتقال للأعلى أو للأسفل، وبدا هذا النموذج أكثر ملاءمةً لما أردناه في Chrometober. وتعدّ واجهة برمجة التطبيقات للصور المتحركة المرتبطة بالتمرير بديلاً مثاليًا لهذه الوظيفة. يعمل هذا الإجراء أيضًا بشكل جيد مع scroll-snap، كما سترى.

كان رسّام المشروع، تايلر ريد، رائعًا في تغيير التصميم عندما غيّرنا الأفكار. لقد أبدع "تايلر" في تنفيذ كل الأفكار الإبداعية التي تم تقديمها له. لقد كان من الممتع جدًا تبادل الأفكار معًا. كان من المهم أن تكون الميزات مجزّأة إلى وحدات منفصلة. بهذه الطريقة، يمكننا إنشاء مشاهد ثم اختيار ما نريد عرضه.

تظهر في أحد مَشاهد المقطوعة الموسيقية ثعبان وتابوت بذراعَين يخرجان وثعلبًا بعصا في مرجل، وشجرة ذات وجه مخيف، وثعلب صغيرة تحمل فانوس يقطين.

كانت الفكرة الرئيسية هي أنه عندما يشق المستخدم طريقه عبر الكتاب، يمكنه الوصول إلى كتل من المحتوى. ويمكنهم أيضًا التفاعل مع لمسات من الخيال، بما في ذلك الرموز المخفية التي أضفناها إلى التجربة، مثل صورة بورتريه في منزل مسكون تتّبع عيناه مؤشر الماوس، أو صور متحركة خفيفة يتم تشغيلها من خلال طلبات البحث عن الوسائط. ستظهر هذه الأفكار والميزات متحركة عند الانتقال إلى الأسفل أو للأعلى. كانت الفكرة الأولى هي أرنب زومبي يرتفع ويتحرك على طول محور x عندما ينتقل المستخدم للأعلى أو للأسفل.

الإلمام بواجهة برمجة التطبيقات

قبل أن نتمكّن من البدء في استخدام الميزات الفردية والرموز المخفية، احتجنا إلى كتاب. لذلك، قرّرنا تحويل هذه الفرصة إلى فرصة لاختبار مجموعة الميزات لواجهة برمجة التطبيقات CSS للرسوم المتحرّكة المرتبطة بميزة الانتقال إلى أعلى الصفحة أو أسفلها. لا تتوفّر حاليًا واجهة برمجة التطبيقات للرسوم المتحرّكة المرتبطة بالتمرير في أي متصفّحات. ومع ذلك، أثناء تطوير واجهة برمجة التطبيقات، عمل المهندسون في فريق التفاعلات على polyfill. ويوفّر ذلك طريقة لاختبار شكل واجهة برمجة التطبيقات أثناء تطويرها. وهذا يعني أنّه يمكننا استخدام واجهة برمجة التطبيقات هذه اليوم، وغالبًا ما تكون المشاريع الممتعة مثل هذا المشروع مكانًا رائعًا لتجربة الميزات التجريبية وتقديم الملاحظات. في وقت لاحق من المقالة، يمكنك الاطّلاع على ما تعلّمناه والملاحظات التي تمكّنا من تقديمها.

بشكل عام، يمكنك استخدام واجهة برمجة التطبيقات هذه لربط الصور المتحركة بالانتقال للأعلى أو للأسفل. يُرجى العلم أنّه لا يمكنك تشغيل صورة متحركة عند الانتقال للأعلى أو للأسفل، ولكن قد نضيف هذه الميزة لاحقًا. تندرج أيضًا الصور المتحركة المرتبطة بالتمرير في فئتين رئيسيتين:

  1. العناصر التي تتفاعل مع موضع التمرير
  2. الصفحات التي تتفاعل مع موضع عنصر في حاوية التمرير

لإنشاء الصيغة الثانية، نستخدم ViewTimeline من خلال السمة animation-timeline.

في ما يلي مثال على طريقة استخدام ViewTimeline في CSS:

.element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
}

.element-scroll-linked {
  animation: rotate both linear;
  animation-timeline: foo;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
}

@keyframes rotate {
 to {
   rotate: 360deg;
 }
}

ننشئ ViewTimeline باستخدام view-timeline-name ونحدّد محوره. في هذا المثال، يشير block إلى block المنطقي. يتم ربط الحركة بالانتقال إلى الأسفل باستخدام السمة animation-timeline. animation-delay وanimation-end-delay (في وقت كتابة هذه المقالة) هما الطريقتان اللتان نحدّد بهما المراحل.

تحدد هذه المراحل النقاط التي يجب أن يرتبط بها الرسم المتحرك في ما يتعلق بموضع العنصر في حاوية التمرير. في مثالنا، نطلب بدء الصورة المتحركة عندما يدخل العنصر (enter 0%) إلى حاوية التمرير. ويمكنك إنهاء العملية عند تغطية 50% (cover 50%) من حاوية التمرير.

إليك عرضنا التوضيحي:

يمكنك أيضًا ربط صورة متحركة بالعنصر الذي يتحرك في مساحة العرض. يمكنك إجراء ذلك من خلال ضبط animation-timeline ليكون view-timeline للعنصر. وهذا مناسب لسيناريوهات مثل الصور المتحرّكة للقوائم. يشبه السلوك طريقة تحريك العناصر عند الإدخال باستخدام IntersectionObserver.

element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
  animation: scale both linear;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
  animation-timeline: foo;
}

@keyframes scale {
  0% {
    scale: 0;
  }
}

وبفضل ذلك، يتم تكبير "المتحرك" عند دخوله إطار العرض، ما يؤدي إلى بدء دوران "المشغِّل".

وقد تبيّن لنا من خلال إجراء التجارب أنّ واجهة برمجة التطبيقات تعمل بشكل جيد جدًا مع ميزة scroll-snap. إنّ ميزة "التمرير السريع" مع رمز ViewTimeline ستكون مثالية لتشغيل ميزة "التمرير السريع" في الصفحات في كتاب.

إنشاء نماذج أولية للآليات

بعد إجراء بعض التجارب، تمكّنت من إنشاء نموذج أوّلي لكتاب. يمكنك التمرير أفقيًا لقلب صفحات الكتاب.

في العرض التجريبي، يمكنك الاطّلاع على عوامل التفعيل المختلفة التي تم تمييزها بحدود مخطّطة.

يبدو الترميز كما يلي:

<body>
  <div class="book-placeholder">
    <ul class="book" style="--count: 7;">
      <li
        class="page page--cover page--cover-front"
        data-scroll-target="1"
        style="--index: 0;"
      >
        <div class="page__paper">
          <div class="page__side page__side--front"></div>
          <div class="page__side page__side--back"></div>
        </div>
      </li>
      <!-- Markup for other pages here -->
    </ul>
  </div>
  <div>
    <p>intro spacer</p>
  </div>
  <div data-scroll-intro>
    <p>scale trigger</p>
  </div>
  <div data-scroll-trigger="1">
    <p>page trigger</p>
  </div>
  <!-- Markup for other triggers here -->
</body>

وأثناء التمرير، تتقلّب صفحات الكتاب، ولكن يتم فتحها أو إغلاقها. ويعتمد ذلك على محاذاة الالتفاف السريع لعناصر التفعيل.

html {
  scroll-snap-type: x mandatory;
}

body {
  grid-template-columns: repeat(var(--trigger-count), auto);
  overflow-y: hidden;
  overflow-x: scroll;
  display: grid;
}

body > [data-scroll-trigger] {
  height: 100vh;
  width: clamp(10rem, 10vw, 300px);
}

body > [data-scroll-trigger] {
  scroll-snap-align: end;
}

هذه المرة، لن نربط ViewTimeline في CSS، بل سنستخدم Web Animations API في JavaScript. ويتيح ذلك إمكانية إنشاء حلقة التكرار على مجموعة من العناصر وإنشاء ViewTimeline الذي نحتاجه بدلاً من إنشاء كل منها يدويًا.

const triggers = document.querySelectorAll("[data-scroll-trigger]")

const commonProps = {
  delay: { phase: "enter", percent: CSS.percent(0) },
  endDelay: { phase: "enter", percent: CSS.percent(100) },
  fill: "both"
}

const setupPage = (trigger, index) => {
  const target = document.querySelector(
    `[data-scroll-target="${trigger.getAttribute("data-scroll-trigger")}"]`
  );

  const viewTimeline = new ViewTimeline({
    subject: trigger,
    axis: 'inline',
  });

  target.animate(
    [
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`
      },
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`,
        offset: 0.75
      },
      {
        transform: `translateZ(${(triggers.length - index) * -1}px)`
      }
    ],
    {
      timeline: viewTimeline,
      commonProps,
    }
  );
  target.querySelector(".page__paper").animate(
    [
      {
        transform: "rotateY(0deg)"
      },
      {
        transform: "rotateY(-180deg)"
      }
    ],
    {
      timeline: viewTimeline,
      commonProps,
    }
  );
};

const triggers = document.querySelectorAll('[data-scroll-trigger]')
triggers.forEach(setupPage);

لكل مشغِّل، ننشئ ViewTimeline. بعد ذلك، نضيف حركة إلى الصفحة المرتبطة بالعامل المشغِّل باستخدام هذا الرمز ViewTimeline. يربط هذا العنصر الرسوم المتحركة للصفحة بالتمرير. بالنسبة إلى الرسوم المتحركة الخاصة بنا، نقوم بتدوير عنصر من الصفحة على المحور y لقلب الصفحة. ونترجم أيضًا الصفحة نفسها على محور z لكي تعمل مثل الكتاب.

خلاصة ما سبق ذكره

بعد أن حدّدت آلية عمل الكتاب، تمكّنت من التركيز على إضفاء الحيوية على الرسوم التوضيحية التي رسمها تايلر.

Astro

استخدم الفريق Astro في حملة Designcember في عام 2021، وكنت حريصًا على استخدامه مرة أخرى في حملة Chrometober. تعد تجربة المطور المتمثلة في القدرة على تقسيم الأشياء إلى مكونات مناسبة جدًا لهذا المشروع.

الكتاب نفسه هو مكوّن. وهي أيضًا مجموعة من مكونات الصفحة. تحتوي كل صفحة على جانبَين وخلفيات. العناصر الفرعية لأحد جوانب الصفحة هي مكونات يمكن إضافتها وإزالتها وتحديد مواضعها بسهولة.

إنشاء دفتر

كان من المهم بالنسبة إليّ تسهيل إدارة الكتل. أردت أيضًا تسهيل تقديم المساهمات لبقية أعضاء الفريق.

يتم تحديد الصفحات ذات المستوى العالي من خلال مصفوفة ضبط. يحدِّد كل عنصر صفحة في الصفيف المحتوى والخلفية والبيانات الوصفية الأخرى للصفحة.

const pages = [
  {
    front: {
      marked: true,
      content: PageTwo,
      backdrop: spreadOne,
      darkBackdrop: spreadOneDark
    },
    back: {
      content: PageThree,
      backdrop: spreadTwo,
      darkBackdrop: spreadTwoDark
    },
    aria: `page 1`
  },
  /* Obfuscated page objects */
]

ويتم تمريرها إلى المكوّن Book.

<Book pages={pages} />

المكوِّن Book هو المكان الذي يتم فيه تطبيق آلية التمرير ويتم إنشاء صفحات الكتاب. ويتم استخدام الآلية نفسها من النموذج الأولي، ولكننا نشارك مثيلات متعددة من ViewTimeline تم إنشاؤها عالميًا.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

بهذه الطريقة، يمكننا مشاركة المخططات الزمنية لاستخدامها في مكان آخر بدلاً من إعادة إنشائها. سنقدّم المزيد من المعلومات حول هذا الموضوع لاحقًا.

إنشاء الصفحة

كل صفحة هي عنصر قائمة داخل قائمة:

<ul class="book">
  {
    pages.map((page, index) => {
      const FrontSlot = page.front.content
      const BackSlot = page.back.content
      return (
        <Page
          index={index}
          cover={page.cover}
          aria={page.aria}
          backdrop={
            {
              front: {
                light: page.front.backdrop,
                dark: page.front.darkBackdrop
              },
              back: {
                light: page.back.backdrop,
                dark: page.back.darkBackdrop
              }
            }
          }>
          {page.front.content && <FrontSlot slot="front" />}    
          {page.back.content && <BackSlot slot="back" />}    
        </Page>
      )
    })
  }
</ul>

ويتم تمرير الإعدادات المحدّدة إلى كل مثيل من Page. تستخدم الصفحات ميزة الشرائح في Astro لإدراج محتوى في كل صفحة.

<li
  class={className}
  data-scroll-target={target}
  style={`--index:${index};`}
  aria-label={aria}
>
  <div class="page__paper">
    <div
      class="page__side page__side--front"
      aria-label={`Right page of ${index}`}
    >
      <picture>
        <source
          srcset={darkFront}
          media="(prefers-color-scheme: dark)"
          height="214"
          width="150"
        >
        <img
          src={lightFront}
          class="page__background page__background--right"
          alt=""
          aria-hidden="true"
          height="214"
          width="150"
        >
      </picture>
      <div class="page__content">
        <slot name="front" />
      </div>
    </div>
    <!-- Markup for back page -->
  </div>
</li>

يُستخدَم هذا الرمز البرمجي في الغالب لإعداد البنية. يمكن للمساهمين العمل على محتوى الكتاب في معظم الأحيان بدون الحاجة إلى تعديل هذا الرمز.

صور خلفية

من خلال التحول الإبداعي إلى تصميم كتاب، أصبح من الأسهل تقسيم الأقسام، وكل صفحة من صفحات الكتاب هي مشهد مأخوذ من التصميم الأصلي.

رسم توضيحي لانتشار الصفحة من الكتاب الذي يعرض شجرة تفاح في مقبرة يحتوي المقبرة على عدة شواهد قبور وهناك خفاش في السماء أمام قمر كبير.

بما أنّنا حدّدنا نسبة عرض إلى ارتفاع للكتاب، يمكن أن يتضمّن خلفية كل صفحة عنصر صورة. يمكنك ضبط عرض هذا العنصر على ‎200% واستخدام object-position استنادًا إلى جانب الصفحة لحلّ هذه المشكلة.

.page__background {
  height: 100%;
  width: 200%;
  object-fit: cover;
  object-position: 0 0;
  position: absolute;
  top: 0;
  left: 0;
}

.page__background--right {
  object-position: 100% 0;
}

محتوى الصفحة

دعونا نلقِ نظرة على إنشاء إحدى الصفحات. تعرض الصفحة الثالثة بومًا يظهر فجأة في شجرة.

وتتم تعبئته بالمكوِّن PageThree، كما هو موضَّح في عملية الإعداد. وهو مكوّن Astro (PageThree.astro). تبدو هذه المكوّنات مثل ملفات HTML، ولكنّها تتضمّن رمزًا في أعلى الصفحة مشابهًا للجزء التقديمي. هذا يمكّننا من القيام بأمور مثل استيراد مكونات أخرى. يظهر المكوّن للصفحة الثالثة على النحو التالي:

---
import TreeOwl from '../TreeOwl/TreeOwl.astro'
import { contentBlocks } from '../../assets/content-blocks.json'
import ContentBlock from '../ContentBlock/ContentBlock.astro'
---
<TreeOwl/>
<ContentBlock {...contentBlocks[3]} id="four" />

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

تجدر الإشارة إلى أنّ الصفحات تكون أساسية بطبيعتها. وهي تستند إلى مجموعة من الميزات. تعرض الصفحة الثالثة كتلة محتوى وبومًا تفاعليًا، لذا يتوفّر مكوّن لكل منهما.

وحدات المحتوى هي الروابط المؤدية إلى المحتوى المعروض في الكتاب. ويتم أيضًا تشغيلها بواسطة عنصر إعدادات.

{
 "contentBlocks": [
    {
      "id": "one",
      "title": "New in Chrome",
      "blurb": "Lift your spirits with a round up of all the tools and features in Chrome.",
      "link": "https://www.youtube.com/watch?v=qwdN1fJA_d8&list=PLNYkxOF6rcIDfz8XEA3loxY32tYh7CI3m"
    },
    …otherBlocks
  ]
}

يتم استيراد هذه الإعدادات حيث تكون عمليات حظر المحتوى مطلوبة. بعد ذلك، يتم تمرير إعدادات الكتلة ذات الصلة إلى المكوّن ContentBlock.

<ContentBlock {...contentBlocks[3]} id="four" />

في ما يلي أيضًا مثال على كيفية استخدام مكوّن الصفحة كمكان لوضع المحتوى. يتمّ هنا وضع كتلة محتوى.

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

ولكنّ الأنماط العامة لكتلة محتوى تكون مضمّنة مع رمز المكوّن.

.content-block {
  background: hsl(0deg 0% 0% / 70%);
  color: var(--gray-0);
  border-radius:  min(3vh, var(--size-4));
  padding: clamp(0.75rem, 2vw, 1.25rem);
  display: grid;
  gap: var(--size-2);
  position: absolute;
  cursor: pointer;
  width: 50%;
}

أما بالنسبة إلى البومة، فهي ميزة تفاعلية - واحدة من العديد من الميزات في هذا المشروع. في ما يلي مثال صغير وجميل يوضّح كيفية استخدامنا لعرض "المخطط الزمني" المشترَك الذي أنشأناه.

على مستوى عالٍ، يستورد مكوّن البومة بعض ملفات SVG ويضمّنها باستخدام عنصر Astro.

---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />

ويتم وضع أنماط تحديد موضع البومة مع رمز المكوّن.

.owl {
  width: 34%;
  left: 10%;
  bottom: 34%;
}

هناك قطعة إضافية من التصميم تحدّد سلوك transform للبوم.

.owl__owl {
  transform-origin: 50% 100%;
  transform-box: fill-box;
}

يؤثّر استخدام transform-box في transform-origin. ويجعلها نسبية إلى مربّع حدود الكائن ضمن ملف SVG. يتم تكبير البومة من أسفل الوسط، وبالتالي استخدام transform-origin: 50% 100%.

الجزء الممتع هو عندما نربط البومة بإحدى 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 Animations API. يتم استخدام سمة التحويل الفردية translate، ويتم ربطها بعنصر ViewTimeline واحد. ومرتبط بـ CHROMETOBER_TIMELINES[1] من خلال الموقع timeline. هذا ViewTimeline يتم إنشاؤه لقلب الصفحات. يؤدي هذا إلى ربط حركة البومة المتحركة مع تدوير الصفحة باستخدام المرحلة enter. ويحدّد هذا الزر أنّه عندما يتم عرض محتوى الصفحة بنسبة% 80، عليك بدء تحريك البومة. عند بلوغ نسبة %90، من المفترض أن تنتهي عملية الترجمة.

ميزات الحجز

لقد رأيتَ الآن نهج إنشاء صفحة وكيف تعمل بنية المشروع. يمكنك الاطّلاع على كيفية السماح للمساهمين بالمشاركة في العمل على صفحة أو ميزة من اختيارهم. تحتوي الميزات المختلفة في الكتاب على رسوم متحركة مرتبطة بتقليب صفحة الكتاب؛ على سبيل المثال، الخفافيش الذي يطير داخل وخارجًا بتقلبات الصفحة.

وتتضمّن أيضًا عناصر مستندة إلى الصور المتحركة في CSS.

بعد وضع أجزاء المحتوى في الكتاب، كان هناك وقت للإبداع في استخدام ميزات أخرى. وقد وفّر ذلك فرصة لإنشاء بعض التفاعلات المختلفة وتجربة طرق مختلفة لتنفيذ الإجراءات.

الحفاظ على سرعة الاستجابة

تحدِّد وحدات إطار العرض المتجاوبة حجم الكتاب وميزاته. ومع ذلك، كان الحفاظ على استجابة الخطوط تحديًا مثيرًا للاهتمام. تعتبر وحدات طلب الحاوية مناسبة هنا. ومع ذلك، لا تتوفّر هذه الميزة في كل مكان بعد. تم ضبط حجم الكتاب، لذا لا نحتاج إلى طلب حاوية. يمكن إنشاء وحدة طلب بحث حاوية مضمّنة باستخدام CSS calc() واستخدامها لتغيير حجم الخط.


.book-placeholder {
  --size: clamp(12rem, 72vw, 80vmin);
  --aspect-ratio: 360 / 504;
  --cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}

.content-block h2 {
  color: var(--gray-0);
  font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}

.content-block :is(p, a) {
  font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}

قرع مضيء في الليل

قد يكون خبراء التصميم قد لاحظوا استخدام عناصر <source> عند مناقشة خلفيات الصفحات في وقت سابق. كانت أونا حريصة على إجراء تفاعل يستجيب لإعدادات نظام الألوان المفضّلة. نتيجةً لذلك، تتيح الخلفيات استخدام الوضعَين الفاتح والداكن مع خيارات مختلفة. ولأنّه يمكنك استخدام طلبات البحث عن الوسائط مع العنصر <picture>، فإنّه يشكّل طريقة رائعة لتقديم نمطين للخلفية. يبحث عنصر <source> عن الإعدادات المفضّلة لنظام الألوان، ويعرض الخلفية المناسبة.

<picture>
  <source srcset={darkFront} media="(prefers-color-scheme: dark)" height="214" width="150">
  <img src={lightFront} class="page__background page__background--right" alt="" aria-hidden="true" height="214" width="150">
</picture>

يمكنك إجراء تغييرات أخرى استنادًا إلى الألوان المفضّلة هذه. تتفاعل القرع في الصفحة الثانية مع الألوان المفضّلة للمستخدم. تحتوي رسومات SVG المستخدَمة على دوائر تمثّل اللهب، ويتم تكبيرها وعرضها بالحركة في الوضع الداكن.

.pumpkin__flame,
 .pumpkin__flame circle {
   transform-box: fill-box;
   transform-origin: 50% 100%;
 }

 .pumpkin__flame {
   scale: 0.8;
 }

 .pumpkin__flame circle {
   transition: scale 0.2s;
   scale: 0;
 }

@media(prefers-color-scheme: dark) {
   .pumpkin__flame {
     animation: pumpkin-flicker 3s calc(var(--index, 0) * -1s) infinite linear;
   }

   .pumpkin__flame circle {
     scale: 1;
   }

   @keyframes pumpkin-flicker {
     50% {
       scale: 1;
     }
   }
 }

هل هذه الصورة الشخصية تراقبك؟

إذا راجعت الصفحة 10، فقد تلاحظ شيئًا ما. يتم مراقبتك ستتّبع عيون الصورة الرمزية مؤشرك أثناء تنقّلك في الصفحة. تكمن الحيلة هنا في ربط موقع المؤشر بقيمة ترجمة، ونقلها إلى CSS.

const mapRange = (inputLower, inputUpper, outputLower, outputUpper, value) => {
   const INPUT_RANGE = inputUpper - inputLower
   const OUTPUT_RANGE = outputUpper - outputLower
   return outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
 }

تأخذ هذه الرموز نطاقات الإدخال والإخراج وتربط القيم المحدّدة. على سبيل المثال، سيؤدي هذا الاستخدام إلى القيمة 625.

mapRange(0, 100, 250, 1000, 50) // 625

بالنسبة إلى الصورة العمودية، تكون قيمة الإدخال هي نقطة مركز كل عين، بالإضافة إلى بعض المسافة بالبكسل أو ناقصها. يشير نطاق الإخراج إلى مقدار ما يمكن للعين ترجمته بالبكسل. وبعد ذلك، يتم تمرير موضع المؤشر على محور x أو y كقيمة. للحصول على نقطة مركز العينَين أثناء تحريكهما، يتم تكرار العينَين. لا يتم نقل العناصر الأصلية، وهي شفافة وتُستخدَم للرجوع إليها.

بعد ذلك، عليك ربطها ببعضها وتعديل قيم السمات المخصّصة في CSS على العينين حتى تتمكّن العينان من التحرك. يتم ربط الدالة بالحدث pointermove في مقابل window. عند اشتعال النيران، يتم استخدام حدود كل عين لحساب النقاط المركزية. بعد ذلك، يتمّ ربط موضع المؤشر بالقيم التي تمّ ضبطها كقيم خاصّة للموقع على العينَين.

const RANGE = 15
const LIMIT = 80
const interact = ({ x, y }) => {
   // map a range against the eyes and pass in via custom properties
   const LEFT_EYE_BOUNDS = LEFT_EYE.getBoundingClientRect()
   const RIGHT_EYE_BOUNDS = RIGHT_EYE.getBoundingClientRect()

   const CENTERS = {
     lx: LEFT_EYE_BOUNDS.left + LEFT_EYE_BOUNDS.width * 0.5,
     rx: RIGHT_EYE_BOUNDS.left + RIGHT_EYE_BOUNDS.width * 0.5,
     ly: LEFT_EYE_BOUNDS.top + LEFT_EYE_BOUNDS.height * 0.5,
     ry: RIGHT_EYE_BOUNDS.top + RIGHT_EYE_BOUNDS.height * 0.5,
   }

   Object.entries(CENTERS)
     .forEach(([key, value]) => {
       const result = mapRange(value - LIMIT, value + LIMIT, -RANGE, RANGE)(key.indexOf('x') !== -1 ? x : y)
       EYES.style.setProperty(`--${key}`, result)
     })
 }

بعد تمرير القيم إلى CSS، يمكن أن تُجري الأنماط ما تريده بها. والميزة الرائعة هنا هي استخدام CSS clamp() لجعل السلوك مختلفًا لكل عين، ما يتيح لك جعل كل عين تتصرف بشكل مختلف بدون تعديل JavaScript مرة أخرى.

.portrait__eye--mover {
   transition: translate 0.2s;
 }

 .portrait__eye--mover.portrait__eye--left {
   translate:
     clamp(-10px, var(--lx, 0) * 1px, 4px)
     clamp(-4px, var(--ly, 0) * 0.5px, 10px);
 }

 .portrait__eye--mover.portrait__eye--right {
   translate:
     clamp(-4px, var(--rx, 0) * 1px, 10px)
     clamp(-4px, var(--ry, 0) * 0.5px, 10px);
 }

كتابة التعويذات

إذا اطّلعت على الصفحة السادسة، هل تشعر بسحرها؟ تتضمّن هذه الصفحة تصميم الثعلب السحرية الرائع. إذا حرّكت مؤشرك، قد يظهر لك تأثير مخصّص لمسار المؤشر. يستخدم هذا الرسم المتحرك في لوحة الرسم. يظهر عنصر <canvas> فوق باقي محتوى الصفحة مع pointer-events: none. هذا يعني أنّه لا يزال بإمكان المستخدمين النقر على أجزاء المحتوى أسفله.

.wand-canvas {
  height: 100%;
  width: 200%;
  pointer-events: none;
  right: 0;
  position: fixed;
}

تمامًا مثلما يستمع عنصر الصورة الشخصية إلى حدث pointermove في window، يفعل عنصر <canvas> ذلك أيضًا. ومع ذلك، في كل مرة يتم فيها تشغيل الحدث، ننشئ كائنًا لإنشاء تأثير متحرك على عنصر <canvas>. تمثّل هذه العناصر الأشكال المستخدَمة في مسار المؤشر. وتتضمّن إحداثيات ونغمة عشوائية.

ويتم استخدام دالة mapRange السابقة مرة أخرى، حيث يمكننا استخدامها لتعيين دلتا المؤشر إلى size وrate. يتم تخزين العناصر في صفيف يتم تكراره عند رسم العناصر على عنصر <canvas>. تُخبر سمات كل عنصر عنصر <canvas> بأماكن رسم العناصر.

const blocks = []
  const createBlock = ({ x, y, movementX, movementY }) => {
    const LOWER_SIZE = CANVAS.height * 0.05
    const UPPER_SIZE = CANVAS.height * 0.25
    const size = mapRange(0, 100, LOWER_SIZE, UPPER_SIZE, Math.max(Math.abs(movementX), Math.abs(movementY)))
    const rate = mapRange(LOWER_SIZE, UPPER_SIZE, 1, 5, size)
    const { left, top, width, height } = CANVAS.getBoundingClientRect()
    
    const block = {
      hue: Math.random() * 359,
      x: x - left,
      y: y - top,
      size,
      rate,
    }
    
    blocks.push(block)
  }
window.addEventListener('pointermove', createBlock)

للرسم على اللوحة، يتم إنشاء حلقة باستخدام requestAnimationFrame. يجب ألا يتم عرض مسار المؤشر إلا عندما تكون الصفحة ظاهرة. لدينا IntersectionObserver يعدّل الصفحات التي تظهر ويحدّدها. إذا كانت الصفحة معروضة، يتم عرض العناصر كدوائر على اللوحة.

بعد ذلك، نكرّر الصفيف blocks ونرسم كل جزء من المسار. يقلّل كل إطار من حجم الجسم ويغيّر موضعه بمقدار rate. ويؤدي ذلك إلى ظهور تأثير السقوط والتكبير. إذا تقلّص الجسم بالكامل، تتم إزالة الجسم من صفيف blocks.

let wandFrame
const drawBlocks = () => {
   ctx.clearRect(0, 0, CANVAS.width, CANVAS.height)
  
   if (PAGE_SIX.className.indexOf('in-view') === -1 && wandFrame) {
     blocks.length = 0
     cancelAnimationFrame(wandFrame)
     document.body.removeEventListener('pointermove', createBlock)
     document.removeEventListener('resize', init)
   }
  
   for (let b = 0; b < blocks.length; b++) {
     const block = blocks[b]
     ctx.strokeStyle = ctx.fillStyle = `hsla(${block.hue}, 80%, 80%, 0.5)`
     ctx.beginPath()
     ctx.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI)
     ctx.stroke()
     ctx.fill()

     block.size -= block.rate
     block.y += block.rate

     if (block.size <= 0) {
       blocks.splice(b, 1)
     }

   }
   wandFrame = requestAnimationFrame(drawBlocks)
 }

إذا اختفت الصفحة من الشاشة، تتم إزالة أدوات معالجة الأحداث ويتم إلغاء حلقة عرض إطارات الصورة المتحركة. يتم أيضًا محو صفيف blocks.

إليك مثال على مسار المؤشر.

مراجعة لتسهيل الاستخدام

من الجيد إنشاء تجربة ممتعة لاستكشافها، ولكن لا فائدة منها إذا لم يكن بإمكان المستخدمين الوصول إليها. كانت خبرة آدم في هذا المجال قيّمة في إعداد Chrometober لمراجعة سهولة الاستخدام قبل الإصدار.

في ما يلي بعض الجوانب البارزة التي تمّ تناولها:

  • التأكّد من أنّ ترميز HTML المستخدَم كان دلاليًا وشمل ذلك عناصر معالم مناسبة، مثل <main> للكتاب، واستخدام عنصر <article> لكلّ كتلة محتوى، وعناصر <abbr> عند تقديم الاختصارات. كان التفكير في المستقبل بعد إنشاء الكتاب يجعل الوصول إلى الأشياء أكثر سهولة. يسهّل استخدام العناوين والروابط على المستخدم التنقل. يعني استخدام قائمة للصفحات أيضًا أنه يتم الإعلان عن عدد الصفحات بواسطة التكنولوجيا المساعدة.
  • التأكّد من أنّ جميع الصور تستخدم سمات alt المناسبة بالنسبة إلى ملفات SVG المضمّنة، يكون العنصر title متوفّرًا عند الضرورة.
  • استخدام سمات aria حيث تُحسِّن التجربة يُعلم استخدام aria-label للصفحات وجوانبها المستخدم بالصفحة التي يتصفّحها. يشير استخدام الرمز aria-describedBy في روابط "قراءة المزيد" إلى نص كتلة المحتوى. ويزيل ذلك الغموض حول الوجهة التي سينقل إليها الرابط المستخدم.
  • في ما يتعلّق بوحدات المحتوى، تتوفّر إمكانية النقر على البطاقة بأكملها وليس على رابط "قراءة المزيد" فقط.
  • سبق أن أوضحنا استخدام IntersectionObserver لتتبُّع الصفحات التي تظهر في العرض. ويعود ذلك بالعديد من المزايا التي لا تقتصر على الأداء فقط. بالنسبة إلى الصفحات التي لا تظهر في العرض، سيتم إيقاف أيّ صورة متحركة أو تفاعل مؤقتًا. في المقابل، يتم أيضًا تطبيق السمة inert على هذه الصفحات. وهذا يعني أنّ المستخدمين الذين يستخدمون قارئ شاشة يمكنهم استكشاف المحتوى نفسه الذي يستكشفه المستخدمون المبصرون. يظل التركيز داخل الصفحة المعروضة ولا يمكن للمستخدمين الانتقال إلى صفحة أخرى.
  • أخيرًا وليس آخرًا، نستخدم طلبات البحث عن الوسائط للالتزام بخيار المستخدم المفضّل بشأن الصور المتحركة.

في ما يلي لقطة شاشة من المراجعة التي تُبرز بعض الإجراءات المتّبعة.

تم تحديد العنصر على أنّه حول الكتاب بأكمله، ما يشير إلى أنّه يجب أن يكون المَعلم الرئيسي الذي يمكن لمستخدمي التكنولوجيا المساعِدة العثور عليه. تم توضيح المزيد في لقطة الشاشة." العرض="800" height="465">

لقطة شاشة لكتاب Chrometober مفتوح يتم توفير مربّعات خضراء مُحدَّدة حول جوانب مختلفة من واجهة المستخدم، والتي تصف وظيفة تسهيل الاستخدام المقصودة ونتائج تجربة المستخدم التي ستقدّمها الصفحة. على سبيل المثال، تحتوي الصور على نص بديل. ومن الأمثلة الأخرى تصنيف تسهيل الاستخدام الذي يصرّح بأنّ الصفحات غير متاحة للاستخدام. تم توضيح المزيد في لقطة الشاشة.

الاستنتاجات التي توصّلنا إليها

لم يكن الدافع وراء إطلاق Chrometober هو تسليط الضوء على محتوى الويب من المنتدى فحسب، بل كان أيضًا طريقة لنا لاختبار polyfill لواجهة برمجة التطبيقات للرسوم المتحرّكة المرتبطة بالانتقال إلى الأسفل أو للأعلى والتي لا تزال قيد التطوير.

لقد خصّصنا جلسة أثناء قمة فريقنا في نيويورك لاختبار المشروع ومعالجة المشاكل التي ظهرت. كانت مساهمة الفريق قيّمة للغاية. وقد كانت هذه فرصة رائعة أيضًا لتوضيح كل الأمور التي يجب معالجتها قبل نشر التطبيق.

يجلس فريق CSS وواجهة المستخدم وأدوات مطوري البرامج حول الطاولة في غرفة مؤتمرات. تقف &quot;أونا&quot; أمام سبورة بيضاء مغطاة بالملاحظات اللاصقة. يجلس أعضاء الفريق الآخرون حول الطاولة مع مشروبات منعشة وأجهزة كمبيوتر محمولة.

على سبيل المثال، أدى اختبار الكتاب على الأجهزة إلى ظهور مشكلة في العرض. لن يتم عرض كتابنا على النحو المتوقّع على أجهزة iOS. تحدِّد وحدات إطار العرض حجم الصفحة، ولكن عندما يكون هناك نُقطة في الشاشة، يؤثّر ذلك في الكتاب. كان الحلّ هو استخدام viewport-fit=cover في إطار العرض meta:

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

وأثرت هذه الجلسة أيضًا في بعض المشاكل المتعلّقة بواجهة برمجة التطبيقات polyfill. أبلغ فريق Bramus عن هذه المشاكل في مستودع polyfill. بعد ذلك، عثر على حلول لهذه المشاكل وتم دمجها في الpolyfill. على سبيل المثال، أدّى طلب سحب هذا الرمز البرمجي إلى تحسين الأداء من خلال إضافة ميزة التخزين المؤقت إلى جزء من العنصر البديل.

لقطة شاشة لعرض توضيحي يتم فتحه في Chrome تكون &quot;أدوات المطوّرين&quot; مفتوحة وتعرض قياسًا أساسيًا للأداء.

لقطة شاشة لعرض توضيحي مفتوح في Chrome أدوات المطوّرين مفتوحة وتتميّز بقياس محسَّن للأداء.

هذا كل شيء!

لقد كان هذا المشروع ممتعًا للغاية، وقد أدّى إلى توفير تجربة تصفّح ممتعة تُبرز المحتوى الرائع من المنتدى. بالإضافة إلى ذلك، كان من الرائع اختبار الpolyfill، بالإضافة إلى تقديم ملاحظات إلى فريق المهندسين للمساعدة في تحسين الpolyfill.

لقد انتهى شهر Chrometober 2022.

نأمل أن تكون هذه المقالة مفيدة. ما هي الميزة المفضّلة لديك؟ يُرجى مراسلتي على Twitter وإعلامنا برأيك.

شاب يحمل ورقة ملصقات تتضمّن شخصيات من Chrometober

وقد تتمكن أيضًا من الحصول على بعض الملصقات من أحد أعضاء الفريق إذا رأيتنا في فعالية معيّنة.

الصورة الرئيسية لأحد الفنادق في David Menidrey على Unsplash