درحال ساختن Chrometober!

چگونه کتاب پیمایشی برای هم‌رسانی نکته‌ها و ترفندهای جالب و ترسناک در این Chrometober زنده شد.

به‌دنبال Designcember، امسال می‌خواستیم Chrometober را برای شما بسازیم تا بتوانیم محتوای وب را از انجمن و تیم Chrome برجسته کنیم و به‌اشتراک بگذاریم. Designcember استفاده از Container Queries را به‌نمایش گذاشت، اما امسال CSS scroll-linked animations API را به‌نمایش می‌گذاریم.

تجربه کتاب پیمایشی را در web.dev/chrometober-2022 بررسی کنید.

نمای کلی

هدف این پروژه ارائه تجربه‌ای عجیب و غریب بود که API پویانمایی‌های مرتبط با پیمایش را برجسته می‌کرد. اما این تجربه درعین‌حال که باید عجیب‌وغریب می‌بود، باید واکنش‌گرا و دسترس‌پذیر هم می‌بود. این پروژه همچنین روشی عالی برای آزمایش شبیه‌ساز مرورگر میانای برنامه‌سازی کاربردی است که درحال توسعه فعال است؛ و همچنین امتحان کردن تکنیک‌ها و ابزارهای مختلف به‌صورت ترکیبی. و همه با زمینه جشن هالووین!

ساختار تیم ما به این شکل بود:

درحال پیش‌نویس کردن تجربه داستان‌سرایی پیمایشی

ایده‌های Chrometober در اولین جلسه خارج از محل تیم ما در ماه مه ۲۰۲۲ شروع به شکل‌گیری کردند. مجموعه‌ای از خط‌خطی‌ها ما را به فکر راه‌هایی انداخت که کاربر بتواند در امتداد نوعی فیلم‌نامه مصور پیمایش کند. با الهام از بازی‌های ویدیویی، تجربه پیمایش در صحنه‌هایی مانند قبرستان و خانه جن‌زده را درنظر گرفتیم.

دفتر یادداشتی روی میز قرار دارد که طرح‌های سرهم‌بندی‌شده و خط‌خطی‌های مختلفی مربوط به پروژه در آن دیده می‌شود.

داشتن آزادی خلاقانه برای هدایت اولین پروژه Google من در جهتی غیرمنتظره هیجان‌انگیز بود. این یک نمونه اولیه از نحوه پیمایش کاربر در محتوا بود.

وقتی کاربر به پهلو پیمایش می‌کند، بلوک‌ها می‌چرخند و بزرگ می‌شوند. اما تصمیم گرفتم از این ایده فاصله بگیرم چون نگران بودم که چطور می‌توانیم این تجربه را برای کاربران در دستگاه‌هایی با اندازه‌های مختلف عالی کنیم. درعوض، به طراحی چیزی که در گذشته ساخته بودم متمایل شدم. در سال ۲۰۲۰، خوش‌شانس بودم که برای ساختن نسخه‌های نمایشی انتشار به ScrollTrigger از GreenSock دسترسی داشتم.

یکی از نسخه‌های نمایشی که ساخته بودم کتابی با CSS سه‌بعدی بود که با پیمایش کردن، صفحه‌های آن ورق می‌خورد و این برای چیزی که برای Chrometober می‌خواستیم بسیار مناسب‌تر به‌نظر می‌رسید. ‫scroll-linked animations API جایگزین مناسبی برای این عملکرد است. همان‌طور که خواهید دید، با scroll-snap هم خوب کار می‌کند!

تصویرگر ما برای این پروژه، تایلر رید، در تغییر طراحی با تغییر ایده‌های ما عالی بود. تایلر کار فوق‌العاده‌ای انجام داد و تمام ایده‌های خلاقانه‌ای را که به او پیشنهاد شد، به واقعیت تبدیل کرد. طوفان فکری ایده‌ها باهم بسیار جالب بود. بخش بزرگی از نحوه عملکردی که برای این ویژگی می‌خواستیم این بود که ویژگی‌ها به بلوک‌های مجزا تقسیم شوند. به این ترتیب، می‌توانستیم آن‌ها را در صحنه‌ها ترکیب کنیم و سپس آنچه را که به زندگی آورده‌ایم انتخاب کنیم.

یکی از صحنه‌های ترکیبی که ماری را نشان می‌دهد، تابوتی که دست‌هایی از آن بیرون آمده، روباهی با عصای جادویی کنار دیگ، درختی با چهره‌ای ترسناک، و ناودان مجسمه‌ای که فانوس کدوتنبل در دست دارد.

ایده اصلی این بود که وقتی کاربر در کتاب پیش می‌رود، بتواند به بخش‌های محتوا دسترسی پیدا کند. آن‌ها همچنین می‌توانستند با عناصر عجیب و غریب تعامل داشته باشند، ازجمله تخم‌مرغ‌های عید پاک که در تجربه ساخته بودیم؛ برای مثال، پرتره‌ای در یک خانه جن‌زده که چشمانش نشانگر شما را دنبال می‌کرد، یا پویانمایی‌های ظریفی که با پرس‌وجوهای رسانه‌ای فعال می‌شدند. این ایده‌ها و ویژگی‌ها هنگام پیمایش متحرک می‌شوند. یکی‌از ایده‌های اولیه، خرگوش زامبی بود که با پیمایش کاربر، در امتداد محور x بالا می‌آمد و حرکت می‌کرد.

آشنایی با API

قبل‌از اینکه بتوانیم با ویژگی‌های فردی و غافلگیری‌های جالب بازی کنیم، به یک کتاب نیاز داشتیم. بنابراین تصمیم گرفتیم این را به فرصتی برای آزمایش مجموعه ویژگی‌های CSS scroll-linked animations API نوظهور تبدیل کنیم. درحال‌حاضر هیچ مرورگری از API پویانمایی‌های پیوندشده به پیمایش پشتیبانی نمی‌کند. بااین‌حال، درحین توسعه دادن میانای برنامه‌سازی کاربردی، مهندسان تیم تعاملات روی یک شبیه‌ساز مرورگر کار کرده‌اند. این کار روشی برای آزمایش شکل میانای برنامه‌سازی کاربردی درحین توسعه آن ارائه می‌دهد. این یعنی می‌توانیم امروز از این API استفاده کنیم و پروژه‌های سرگرم‌کننده‌ای مثل این اغلب مکان عالی‌ای برای امتحان کردن ویژگی‌های آزمایشی و ارائه بازخورد هستند. در ادامه مقاله، با آنچه یاد گرفتیم و بازخوردی که توانستیم ارائه دهیم آشنا شوید.

در سطح بالا، می‌توانید از این میانای برنامه‌سازی کاربردی برای پیوند دادن پویانمایی‌ها به پیمایش استفاده کنید. توجه به این نکته مهم است که نمی‌توانید پویانمایی را هنگام پیمایش راه‌اندازی کنید—این چیزی است که ممکن است بعداً اضافه شود. پویانمایی‌های پیوندخورده به پیمایش نیز به دو دسته اصلی تقسیم می‌شوند:

  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%) محفظه پیمایش می‌شود. و وقتی ۵۰٪ (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 در جاوا اسکریپت استفاده می‌کنیم. این کار مزیت دیگری هم دارد که می‌توانیم روی مجموعه‌ای از عناصر حلقه بزنیم و 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 در سال ۲۰۲۱ استفاده کرد و من مشتاق بودم که دوباره از آن برای 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>

این کد بیشتر برای راه‌اندازی ساختار است. همیاران می‌توانند در بیشتر موارد بدون نیاز به دست زدن به این کد، روی محتوای کتاب کار کنند.

پساویز

تغییر خلاقانه به‌سمت کتاب باعث شد تقسیم کردن بخش‌ها بسیار آسان‌تر شود و هر صفحه کتاب صحنه‌ای است که از طراحی اصلی گرفته شده است.

تصویر دوصفحه‌ای از کتاب که درخت سیبی را در قبرستان نشان می‌دهد. قبرستان چندین سنگ قبر دارد و خفاشی در آسمان مقابل ماه بزرگ دیده می‌شود.

چون نسبت ابعادی کتاب را تعیین کرده بودیم، پساویز هر صفحه می‌توانست عنصر تصویری داشته باشد. تنظیم آن عنصر روی عرض ۲۰۰٪ و استفاده از 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 هستند اما در بالا یک حصار کد مشابه frontmatter دارند. این کار به ما امکان می‌دهد کارهایی مثل وارد کردن مؤلفه‌های دیگر را انجام دهیم. مؤلفه صفحه سه به این شکل است:

---
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. اگر ترجیحی ندارند، پویانمایی جغد را به پیمایش پیوند دهید.

برای بخش دوم، جغد بااستفاده از Web Animations API در محور y پویانمایی می‌شود. از دارایی تبدیل تکی translate استفاده می‌شود و به یک ViewTimeline پیوند داده شده است. این حساب ازطریق دارایی timeline به CHROMETOBER_TIMELINES[1] پیوند شده است. این ViewTimeline برای ورق زدن صفحه تولید شده است. این کار پویانمایی جغد را بااستفاده از فاز enter به ورق زدن صفحه پیوند می‌دهد. این کد تعریف می‌کند که وقتی صفحه ۸۰٪ ورق خورد، حرکت جغد شروع شود. در ۹۰٪، جغد باید ترجمه‌اش را تمام کند.

ویژگی‌های کتاب

اکنون رویکرد ساختن صفحه و نحوه عملکرد معماری پروژه را دیده‌اید. می‌توانید ببینید که چگونه به همیاران اجازه می‌دهد تا وارد شوند و روی صفحه یا ویژگی انتخابی خود کار کنند. ویژگی‌های مختلف کتاب پویانمایی‌های خود را به ورق زدن کتاب پیوند داده‌اند؛ برای مثال، خفاشی که با ورق زدن صفحه به داخل و خارج پرواز می‌کند.

همچنین عناصری دارد که با پویانمایی‌های 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> در بحث قبلی درباره پساویزهای صفحه شده باشند. Una مشتاق بود تعاملی داشته باشد که به اولویت آرایش رنگ‌ها واکنش نشان دهد. درنتیجه، پساویزها از هر دو حالت روشن و تاریک با انواع مختلف پشتیبانی می‌کنند. چون می‌توانید از پرسمان‌های رسانه با عنصر <picture> استفاده کنید، این روش عالی برای ارائه دو سبک پساویز است. عنصر <source> برای اولویت آرایش رنگ‌ها پرسمان می‌کند و پساویز مناسب را نشان می‌دهد.

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

می‌توانید تغییرات دیگری را براساس اولویت آرایش رنگ‌ها معرفی کنید. کدو تنبل‌های صفحه دو به ترجیح آرایش رنگ‌های کاربر واکنش نشان می‌دهند. SVG استفاده‌شده دارای دایره‌هایی است که شعله‌ها را نشان می‌دهند، که در حالت تاریک بزرگ می‌شوند و پویانمایی می‌شوند.

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

 .pumpkin__flame {
   scale: 0.8;
 }

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

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

   .pumpkin__flame circle {
     scale: 1;
   }

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

آیا این پرتره شما را تماشا می‌کند؟

اگر صفحه ۱۰ را بررسی کنید، ممکن است متوجه چیزی شوید. شما تحت نظارت هستید! وقتی در صفحه حرکت می‌کنید، چشمان پرتره اشاره‌گرتان را دنبال می‌کند. ترفند در اینجا این است که مکان اشاره‌گر را به مقدار ترجمه نگاشت کنید و آن را به 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)
 }

این کد محدوده‌های ورودی و برونداد را می‌گیرد و مقادیر داده‌شده را نگاشت می‌کند. برای مثال، این کاربرد مقدار ۶۲۵ را ارائه می‌دهد.

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() برای متفاوت کردن رفتار هر چشم است، بنابراین می‌توانید بدون لمس مجدد جاوا اسکریپت، رفتار هر چشم را متفاوت کنید.

.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 را نیز دارند. این یعنی کاربرانی که از صفحه‌خوان استفاده می‌کنند می‌توانند همان محتوایی را کاوش کنند که کاربران بینا کاوش می‌کنند. تمرکز در صفحه‌ای که درحال مشاهده است باقی می‌ماند و کاربران نمی‌توانند با کلید جهش به صفحه دیگری بروند.
  • درنهایت، از پرسمان‌های رسانه‌ای استفاده می‌کنیم تا به اولویت کاربر برای حرکت احترام بگذاریم.

در اینجا نماگرفتی از بازبینی ارائه شده است که برخی‌از اقدامات انجام‌شده را برجسته می‌کند.

عنصر به‌عنوان اطراف کل کتاب شناسایی شده است، که نشان می‌دهد باید نشانگر مکانی اصلی برای کاربران فناوری کمکی باشد تا آن را پیدا کنند. در نماگرفت، موارد بیشتری نشان داده شده است.» width="800" height="465">

نماگرفت کتاب Chrometober بازشده. کادرهایی با حاشیه سبز در اطراف جنبه‌های مختلف میانای کاربری ارائه شده است که عملکرد دسترس‌پذیری موردنظر و نتایج تجربه کاربری را که صفحه ارائه خواهد داد توصیف می‌کند. برای مثال، تصاویر دارای نوشتار جایگزین هستند. نمونه دیگر برچسب دسترس‌پذیری است که اعلام می‌کند صفحه‌های خارج از دید غیرفعال هستند. اطلاعات بیشتر در نماگرفت مشخص شده است.

آنچه یاد گرفتیم

انگیزه پشت Chrometober نه‌تنها برجسته کردن محتوای وب از جامعه بود، بلکه راهی برای ما بود تا شبیه‌ساز مرورگر API پویانمایی‌های مرتبط با پیمایش را که درحال توسعه است، آزمایش کنیم.

درطول اجلاس تیممان در نیویورک، جلسه‌ای را برای آزمایش پروژه و حل مشکلات پیش‌آمده اختصاص دادیم. مشارکت تیم بسیار ارزشمند بود. همچنین فرصت خوبی بود تا همه چیزهایی را که باید قبل‌از فعال شدن به آن‌ها رسیدگی می‌کردیم فهرست کنیم.

تیم CSS،‏ UI، و DevTools دور میز در اتاق کنفرانس نشسته‌اند. یونا کنار تخته سفیدی ایستاده است که با یادداشت‌های چسبی پوشانده شده است. اعضای دیگر تیم با نوشیدنی و رایانه کیفی دور میز نشسته‌اند.

برای مثال، آزمایش کتاب در دستگاه‌ها مشکل پردازشی را نشان داد. کتاب ما در دستگاه‌های iOS طبق انتظار ارائه نمی‌شد. واحدهای ناحیه نمایش اندازه صفحه را تعیین می‌کنند، اما وقتی بریدگی وجود داشت، بر کتاب تأثیر می‌گذاشت. راه‌حل استفاده از viewport-fit=cover در ناحیه نمایش meta بود:

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

این جلسه همچنین برخی مشکلات مربوط به شبیه‌ساز مرورگر میانای برنامه‌سازی کاربردی را مطرح کرد. Bramus این مشکلات را در مخزن polyfill مطرح کرد. او متعاقباً راه‌حل‌هایی برای آن مشکلات پیدا کرد و آن‌ها را در شبیه‌ساز مرورگر ادغام کرد. برای مثال، این درخواست واکشی با افزودن حافظه پنهان به بخشی از شبیه‌ساز مرورگر، عملکرد را بهبود بخشید.

نماگرفتی از نسخه نمایشی بازشده در Chrome. «ابزارهای توسعه‌دهندگان» باز است و اندازه‌گیری عملکرد مبنا را نشان می‌دهد.

نماگرفتی از نسخه نمایشی بازشده در Chrome. «ابزارهای توسعه‌دهندگان» باز است و اندازه‌گیری عملکرد بهبودیافته را نشان می‌دهد.

فقط همین!

این پروژه واقعاً سرگرم‌کننده بود و منجر به تجربه پیمایش عجیبی شد که محتوای شگفت‌انگیز انجمن را برجسته می‌کند. نه تنها این، بلکه برای آزمایش شبیه‌ساز مرورگر و همچنین ارائه بازخورد به تیم مهندسی برای کمک به بهبود شبیه‌ساز مرورگر عالی بوده است.

Chrometober 2022 به پایان رسید.

امیدواریم از آن لذت برده باشید! What's your favorite feature?‎ (ویژگی موردعلاقه‌تان چیست؟) برایم توییت کنید و نظرتان را با ما درمیان بگذارید!

جی درحال نگه داشتن برگه‌ای از برچسب‌های نویسه‌های Chrometober.

اگر ما را در رویدادی ببینید، حتی ممکن است بتوانید چند برچسب از یکی از اعضای تیم بگیرید.