جارٍ إنشاء Chrometober.

تعرَّف على كيفية ابتكار الكتاب القابل للتنقُّل في مكتبة Chrometober من خلال مشاركة نصائحه مرحة ومخيفة.

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

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

نظرة عامة

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

بدا هيكل فريقنا كما يلي:

بدء صياغة تجربة لسرد الفيديوهات

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

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

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

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

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

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

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

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

التعرّف على واجهة برمجة التطبيقات

قبل أن نبدأ في اللعب بالعناصر الفردية ومفاجآت عيد الفصح، كنا بحاجة إلى كتاب. لذا قرّرنا تحويل هذه التجربة إلى فرصة لاختبار مجموعة ميزات واجهة برمجة التطبيقات للصور المتحركة الجديدة والمترابطة بالتمرير في 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;
  }
}

ومن خلال هذا، يزيد حجم "Mover" عند دخوله إطار العرض، ما يؤدي إلى دوران "Spinner".

بعد إجراء التجربة، توصّلت إلى أنّ واجهة برمجة التطبيقات تعمل بشكل جيد للغاية مع ميزة التمرير السريع. يمكن أن تكون ميزة الالتقاط عند التمرير مع استخدام ميزة 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 في 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%;
}

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

على مستوى عالٍ، يستورد مكوّن البومة بعض الرسومات الموجّهة التي يمكن تغيير حجمها (SVG) ويضمّنه باستخدام Fragment من 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 باستخدام 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

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

ثم يتعلق الأمر بربطهما معًا وتحديث قيم خصائص 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;
}

يشبه عنصر <canvas> إلى حد كبير طريقة استماع عمودي إلى حدث pointermove في window. ومع ذلك، كل مرة يتم فيها تنشيط الحدث، ننشئ كائنًا لتحريكه على العنصر <canvas>. تمثل هذه الكائنات الأشكال المستخدمة في ممر المؤشر. لكل منها إحداثيات وتدرج لون عشوائي.

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

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

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

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

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

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

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

   }
   wandFrame = requestAnimationFrame(drawBlocks)
 }

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

إليك مسار المؤشر عمليًا:

مراجعة إمكانية الوصول

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

إليك بعض المناطق البارزة التي تشملها:

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

إليك لقطة شاشة من المراجعة تسلّط الضوء على بعض الإجراءات المُطبّقة.

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

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

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

لم يكن الدافع وراء 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. على سبيل المثال، حقّق طلب السحب هذا تحسينًا في الأداء من خلال إضافة التخزين المؤقت إلى جزء من رمز polyfill.

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

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

أكملت هذه الخطوة.

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

اختتام إصدار Chrometober لعام 2022

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

&quot;جي&quot; يحمل ورقة لاصقة للشخصيات من Chrometober.

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

صورة البطل من تصوير ديفيد ميندري على UnLaunch