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

چگونه کتاب پیمایش برای به اشتراک گذاشتن نکات و ترفندهای سرگرم کننده و ترسناک در این 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

،

چگونه کتاب پیمایش برای به اشتراک گذاشتن نکات و ترفندهای سرگرم کننده و ترسناک در این 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%;
}

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

در سطح بالایی ، مؤلفه OWL ما مقداری 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 S) تولید می کنیم:

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

برای قسمت دوم ، OWL در محور y با استفاده از API انیمیشن های وب متحرک می شود. translate خاصیت تراکم فردی استفاده می شود ، و به یک ViewTimeline مرتبط است. این از طریق ویژگی timeline به CHROMETOBER_TIMELINES[1] مرتبط است. این یک ViewTimeline است که برای چرخش صفحه ایجاد می شود. این انیمیشن OWL را با استفاده از مرحله 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> شده اند. 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;
     }
   }
 }

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

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

.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> برای کتاب بود. ASO استفاده از عنصر <article> برای هر بلوک محتوا ، و عناصر <abbr> که در آن مخفف معرفی می شوند. فکر کردن در حالی که کتاب ساخته شده بود ، همه چیز را در دسترس تر می کرد. استفاده از عناوین و پیوندها باعث می شود که کاربر حرکت کند. استفاده از لیستی برای صفحات همچنین به این معنی است که تعداد صفحات توسط Assive Technology اعلام شده است.
  • اطمینان از اینکه همه تصاویر از ویژگی های مناسب alt استفاده می کنند. برای SVG های درون خطی ، عنصر title در صورت لزوم موجود است.
  • استفاده از ویژگی های aria در جایی که آنها تجربه را بهبود می بخشند. استفاده از aria-label برای صفحات و طرفین آنها با کاربر در کدام صفحه قرار دارد. استفاده از aria-describedBy در پیوندهای "بیشتر بخوانید" متن بلوک محتوا را به هم می پیوندد. این امر ابهام را در مورد اینکه لینک کاربر را به دست می آورد ، از بین می برد.
  • در مورد موضوع بلوک های محتوا ، امکان کلیک بر روی کل کارت و نه تنها پیوند "بیشتر بخوانید" در دسترس است.
  • استفاده از یک IntersectionObserver برای پیگیری اینکه کدام صفحات در حال مشاهده هستند ، زودتر آمده است. این مزایای بسیاری دارد که فقط مربوط به عملکرد نیست. صفحات در مشاهده هیچ انیمیشن یا تعامل متوقف نمی شوند. اما این صفحات همچنین ویژگی inert را اعمال می کنند. این بدان معنی است که کاربرانی که از یک خواننده صفحه نمایش استفاده می کنند می توانند همان محتوای کاربران بینایی را کشف کنند. تمرکز در صفحه ای که در حال مشاهده است باقی مانده است و کاربران نمی توانند به صفحه دیگری بفرستند.
  • نکته آخر اینکه ما از پرس و جوهای رسانه ای برای احترام به اولویت کاربر برای حرکت استفاده می کنیم.

در اینجا یک تصویر از این بررسی وجود دارد که برخی از اقدامات موجود را برجسته می کند.

عنصر به عنوان کل کتاب مشخص می شود ، و این نشان می دهد که باید اصلی ترین مکان برای کاربران فناوری کمکی باشد. بیشتر در تصویر بیان شده است. "عرض =" 800 "ارتفاع =" 465 ">

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

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

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

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

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

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

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

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

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

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

همین!

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

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

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

جی یک برگه برچسب شخصیت ها را از Chrometober نگه داشته است.

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

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

،

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

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

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

نمای کلی

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

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

پیش نویس یک تجربه اسکرولیتالینگ

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

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

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

از آنجا که کاربر به پهلو پیمایش می کند ، بلوک ها می چرخند و مقیاس می شوند. درعوض ، من به سمت طراحی چیزی که در گذشته ساخته ام خم شدم. در سال 2020 ، من خوش شانس بودم که به Scrolltrigger Greensock برای ساخت نسخه های نمایشی نسخه دسترسی پیدا کردم.

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

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

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

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

آشنایی با API

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

ما با view-timeline-name یک ViewTimeline ایجاد می کنیم و محور آن را تعریف می کنیم. در این مثال ، block به block منطقی اشاره دارد. این انیمیشن به پیمایش با ویژگی animation-timeline مرتبط می شود. animation-delay و animation-end-delay (در زمان نوشتن) نحوه تعریف مراحل است.

این مراحل نقاط را تعریف می کند که در آن انیمیشن باید در رابطه با موقعیت یک عنصر در ظرف پیمایش خود مرتبط شود. به عنوان مثال ، ما می گوییم وقتی عنصر وارد می شود ( enter 0% ) ظرف پیمایش را شروع کنید. و هنگامی که 50 ٪ ( cover 50% ) از ظرف پیمایش را پوشش داده است ، تمام شود.

در اینجا نسخه ی نمایشی ما در عمل است:

همچنین می توانید یک انیمیشن را به عنصری که در نمای در حال حرکت است پیوند دهید. شما می توانید این کار را با تنظیم animation-timeline انجام دهید تا از نظر عنصر view-timeline عنصر باشد. این برای سناریوهایی مانند انیمیشن های لیست مفید است. این رفتار شبیه به چگونگی تحریک عناصر پس از ورود با استفاده از IntersectionObserver است.

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

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

با این کار ، "متحرک" با ورود به منظره ، مقیاس می کند و باعث چرخش "اسپینر" می شود.

آنچه من از آزمایش پیدا کردم این بود که API با Scroll-SNAP بسیار خوب کار می کند. 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>

همانطور که پیمایش می کنید ، صفحات کتاب به نوبه خود می چرخند ، اما ضربه محکم و ناگهانی باز یا بسته می شود. این بستگی به تراز Scroll-SNAP محرک ها دارد.

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 وصل نمی کنیم ، اما از 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

این تیم در سال 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%;
}

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

در سطح بالایی ، مؤلفه OWL ما مقداری 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 S) تولید می کنیم:

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

برای قسمت دوم ، OWL در محور y با استفاده از API انیمیشن های وب متحرک می شود. translate خاصیت تراکم فردی استفاده می شود ، و به یک ViewTimeline مرتبط است. این از طریق ویژگی timeline به CHROMETOBER_TIMELINES[1] مرتبط است. این یک ViewTimeline است که برای چرخش صفحه ایجاد می شود. این انیمیشن OWL را با استفاده از مرحله 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> شده اند. 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;
     }
   }
 }

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

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

.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> برای کتاب بود. ASO استفاده از عنصر <article> برای هر بلوک محتوا ، و عناصر <abbr> که در آن مخفف معرفی می شوند. فکر کردن در حالی که کتاب ساخته شده بود ، همه چیز را در دسترس تر می کرد. استفاده از عناوین و پیوندها باعث می شود که کاربر حرکت کند. استفاده از لیستی برای صفحات همچنین به این معنی است که تعداد صفحات توسط Assive Technology اعلام شده است.
  • اطمینان از اینکه همه تصاویر از ویژگی های مناسب alt استفاده می کنند. برای SVG های درون خطی ، عنصر title در صورت لزوم موجود است.
  • استفاده از ویژگی های aria در جایی که آنها تجربه را بهبود می بخشند. استفاده از aria-label برای صفحات و طرفین آنها با کاربر در کدام صفحه قرار دارد. استفاده از aria-describedBy در پیوندهای "بیشتر بخوانید" متن بلوک محتوا را به هم می پیوندد. این امر ابهام را در مورد اینکه لینک کاربر را به دست می آورد ، از بین می برد.
  • در مورد موضوع بلوک های محتوا ، امکان کلیک بر روی کل کارت و نه تنها پیوند "بیشتر بخوانید" در دسترس است.
  • استفاده از یک IntersectionObserver برای پیگیری اینکه کدام صفحات در حال مشاهده هستند ، زودتر آمده است. این مزایای بسیاری دارد که فقط مربوط به عملکرد نیست. صفحات در مشاهده هیچ انیمیشن یا تعامل متوقف نمی شوند. اما این صفحات همچنین ویژگی inert را اعمال می کنند. این بدان معنی است که کاربرانی که از یک خواننده صفحه نمایش استفاده می کنند می توانند همان محتوای کاربران بینایی را کشف کنند. تمرکز در صفحه ای که در حال مشاهده است باقی مانده است و کاربران نمی توانند به صفحه دیگری بفرستند.
  • نکته آخر اینکه ما از پرس و جوهای رسانه ای برای احترام به اولویت کاربر برای حرکت استفاده می کنیم.

در اینجا یک تصویر از این بررسی وجود دارد که برخی از اقدامات موجود را برجسته می کند.

عنصر به عنوان کل کتاب مشخص می شود ، و این نشان می دهد که باید اصلی ترین مکان برای کاربران فناوری کمکی باشد. بیشتر در تصویر بیان شده است. "عرض =" 800 "ارتفاع =" 465 ">

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

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

The motivation behind Chrometober was not only to highlight web content from the community, but was also a way for us to test drive the scroll-linked animations API polyfill that's in development.

We set aside a session while on our team summit in New York to test the project and tackle issues that arose. The team's contribution was invaluable. It was also a great opportunity to list all the things that needed tackling before we could go live.

CSS, UI, and DevTools team sit around the table in a conference room. Una stands at a whiteboard which is covered in sticky notes. Other team members sit around the table with refreshments and laptops.

For example, testing out the book on devices raised a rendering issue. Our book wouldn't render as expected on iOS devices. Viewport units size the page, but when a notch was present, it affected the book. The solution was to use viewport-fit=cover in the meta viewport:

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

This session also raised some issues with the API polyfill. Bramus raised these issues in the polyfill repository. He subsequently found solutions to those issues and got them merged into the polyfill. For example, this pull request made a performance gain by adding caching to part of the polyfill.

A screenshot of a demo open in Chrome. The Developer Tools are open and show a baseline performance measurement.

A screenshot of a demo open in Chrome. The Developer Tools are open and show an improved performance measurement.

همین!

This has been a real fun project to work on, resulting in a whimsical scrolling experience that highlights amazing content from the community. Not only that, it's been great for testing the polyfill, as well as providing feedback to the engineering team to help improve the polyfill.

Chrometober 2022 is a wrap.

امیدواریم از آن لذت برده باشید! ویژگی مورد علاقه شما چیست؟ Tweet me and let us know!

Jhey holding a sticker sheet of the characters from Chrometober.

You might even be able to grab some stickers from one of the team if you see us at an event .

Hero Photo by David Menidrey on Unsplash