ساخت کرومتوبر!

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

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

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

بررسی اجمالی

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

ساختار تیم ما به این صورت بود:

پیش نویس یک تجربه طومار نویسی

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

یک دفترچه یادداشت روی یک میز با ابله ها و خط خطی های مختلف مربوط به پروژه قرار دارد.

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

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

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

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

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

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

آشنایی با API

قبل از اینکه بتوانیم با ویژگی های فردی و تخم مرغ های عید پاک بازی کنیم، به یک کتاب نیاز داشتیم. بنابراین ما تصمیم گرفتیم این را به فرصتی برای آزمایش ویژگی‌ها برای API انیمیشن‌های مرتبط با پیمایش CSS تبدیل کنیم. API انیمیشن‌های مرتبط با پیمایش در حال حاضر در هیچ مرورگری پشتیبانی نمی‌شود. با این حال، در حین توسعه API، مهندسان تیم تعاملات روی یک polyfill کار کرده‌اند. این روشی را برای آزمایش شکل 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% ) شود. و زمانی که 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" را آغاز می کند.

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

تیم در سال 2021 از 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>

این کد بیشتر برای تنظیم ساختار است. مشارکت کنندگان می توانند در اکثر موارد بدون نیاز به لمس این کد روی محتوای کتاب کار کنند.

پس زمینه

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

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

همانطور که ما در مورد نسبت تصویر برای کتاب تصمیم گرفته بودیم، پس زمینه برای هر صفحه می تواند یک عنصر تصویر داشته باشد. تنظیم آن عنصر روی 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 هستند، اما دارای یک حصار کد در بالا هستند شبیه به 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 وارد می‌کند و با استفاده از قطعه 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 به صفحه تبدیل می کند. تعریف می کند که وقتی صفحه 80٪ چرخید، شروع به حرکت جغد کنید. در 90٪، جغد باید ترجمه خود را تمام کند.

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

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

همچنین دارای عناصری است که توسط انیمیشن های CSS طراحی شده اند.

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

پاسخگو نگه داشتن چیزها

واحدهای نمای پاسخگو اندازه کتاب و ویژگی های آن را دارند. با این حال، پاسخگو نگه داشتن فونت ها چالش جالبی بود. واحدهای جستجوی کانتینر در اینجا مناسب هستند. اگرچه هنوز در همه جا پشتیبانی نمی شوند. اندازه کتاب تنظیم شده است، بنابراین نیازی به درخواست ظرف نداریم. یک واحد کوئری کانتینر درون خطی را می توان با calc() CSS تولید کرد و برای اندازه فونت استفاده کرد.


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

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

چیزی که یاد گرفتیم

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

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

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

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

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

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

اسکرین شات از یک نسخه نمایشی باز در کروم. ابزارهای توسعه دهنده باز هستند و یک اندازه گیری عملکرد پایه را نشان می دهند.

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

خودشه!

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

Chrometober 2022 یک بسته بندی است.

امیدواریم از آن لذت برده باشید! ویژگی مورد علاقه شما چیست؟ من را توییت کنید و به ما اطلاع دهید!

جی، برگه برچسبی از شخصیت‌های کرومتوبر را در دست دارد.

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

عکس قهرمان توسط دیوید منیدری در Unsplash