正在构建 Chrometober!

这款 Chrometober 会如何分享趣味横生的恐怖提示和技巧,让这本卷轴式图书跃然眼前。

Designcember 之后,我们希望在今年为您打造 Chrometober,以重点推广和分享来自社区和 Chrome 团队的 Web 内容。Designcember 展示了容器查询的使用,但今年,我们展示了 CSS 滚动链接动画 API。

访问 web.dev/chrometober-2022,体验滚动式图书体验。

概览

该项目的目标是提供一种别具一格的体验,重点展示滚动链接的动画 API。不过,虽然设计上不可思议,但游戏体验也需要快速响应和无障碍。此外,该项目也非常有助于测试正在积极开发的 API polyfill;以及尝试不同的技术和工具组合。这一切都是以节日主题为主题的!

我们的团队结构如下所示:

草拟滚动讲述体验

早在 2022 年 5 月,我们的第一支外出团队便开始围绕 Chrometober 提出自己的想法。涂鸦集合让我们开始思考,用户可以通过哪些方式沿着某种形式的故事板滚动浏览路径。受视频游戏的启发,我们考虑了让用户通过滚动浏览墓地和鬼屋等场景的体验。

桌子上放着一本笔记本,上面放着各种与该项目相关的涂鸦和涂鸦。

我很高兴能有机会尽情挥洒创意,朝着意想不到的方向发展我的首个 Google 项目。这是用户如何浏览内容的早期原型。

当用户向侧面滚动时,这些块会旋转和放大。但考虑到我们可以如何为使用各种尺寸的设备的用户打造出色的体验,我决定放弃这个想法。相反,我倾向于采用过去打造的产品。2020 年,我有幸能够使用 GreenSock 的 ScrollTrigger 构建发布演示。

我制作的其中一个演示是一本 3D-CSS 书,页面会随着用户滚动而翻转,这让我们感觉更适合我们对 Chrometober 的设计。滚动链接动画 API 可以完美地替换该功能。同样,它也可以与 scroll-snap 配合使用,如下所示!

我们的项目插画师 Tyler Reed 擅长随着我们改变想法而改变设计。Tyler 将其他人提出的各种创意想法付诸实践,表现得非常棒。大家一起集思广益的创意过程非常有趣。我们希望实现这一目标的一大好处是,将功能拆分成多个孤立的块。这样,我们就可以把这些图案组合成不同的场景,挑选要呈现的场景。

其中一个构图场景呈现了一条蛇、一个伸出双臂的棺材、一只筒子上有一根魔杖的狐狸、一只长着恐怖脸的树以及一个拿着南瓜灯的石像鬼。

其主要思路是,在用户浏览图书的过程中,可以访问部分内容。他们还可以与奇思妙想互动,包括我们在体验中融入的复活节彩蛋;例如,描绘在鬼屋里的肖像画,眼睛跟着您的指针,或者由媒体查询触发的微妙动画。这些提示和功能会在用户滚动浏览时以动画形式呈现。我们最初的理念是开发一只僵尸兔,它在用户滚动时可以沿 X 轴向上平移。

熟悉该 API

在开始玩各个功能和复活节彩蛋之前,我们需要一本书。因此,我们决定趁此机会测试新推出的 CSS 滚动链接动画 API 的功能集。目前,任何浏览器都不支持滚动链接动画 API。不过,在开发该 API 时,互动团队的工程师一直在开发 polyfill。这提供了一种在 API 开发过程中对其进行测试的方法。这意味着我们现在就可以使用此 API,而像这样的有趣项目通常是试用实验性功能并提供反馈的好地方。如需了解我们学到的经验以及我们能够提供的反馈,请参阅本文后面的部分

大体上讲,您可以使用此 API 关联要滚动的动画。请务必注意,您无法在滚动时触发动画,但可能会稍后触发。滚动链接的动画也分为两大类:

  1. 响应滚动位置的应用。
  2. 那些对元素在其滚动容器中的位置做出反应的应用。

为了创建后者,我们使用通过 animation-timeline 属性应用的 ViewTimeline

下面是一个在 CSS 中使用 ViewTimeline 的示例:

.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-delayanimation-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;
}

这一次,我们没有在 CSS 中连接 ViewTimeline,而是在 JavaScript 中使用 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 轴上平移了页面本身,这样它就会像图书一样显示。

两种方式对比

确定好这本书的机制后,我可以专心让 Tyler 的插图变得生动有趣。

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。这使我们能够执行导入其他组件等操作。第 3 页的组件如下所示:

---
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>

同样,页面在本质上是原子性的。它们以一系列功能为基础构建而成。第 3 页显示了内容块和交互式猫头鹰,因此它们各有一个组件。

内容块是指向图书内内容的链接。这些操作也是由配置对象驱动的。

{
 "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。

大体上讲,我们的 owl 组件会导入一些 SVG,并使用 Astro 的 fragment 将其内嵌。

---
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 动画提供支持的元素。

内容块添加到图书中后,您就可以利用其他功能发挥创意。这让我们有机会进行一些不同的互动,并尝试不同的实施方式。

保持响应

自适应视口单元可调整图书及其功能的尺寸。不过,让字体保持自适应是一个有趣的挑战。容器查询单元非常适合这种情况。不过,并非所有地区都支持这些加密方式。图书的大小已设置,因此我们不需要容器查询。内嵌容器查询单元可通过 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 自定义属性值,以便眼睛可以移动。函数会根据 window 绑定到 pointermove 事件。触发时,系统会使用每只眼睛的边界来计算中心点。然后,指针位置会映射到设置为眼睛上的自定义属性值的值。

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

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

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

将值传递给 CSS 后,样式便可对其执行所需的操作。这里最棒的是使用 CSS clamp() 让每只眼睛都能看到不同的行为,这样一来,您无需再次触摸 JavaScript,即可让每只眼睛的行为有所不同。

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

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

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

施展法术

如果浏览第 6 页,您会感到被迷住了吗?这一页介绍了我们奇妙的魔法狐狸的设计。如果您四处移动指针,可能会看到自定义光标轨迹效果。这会使用画布动画。<canvas> 元素位于其余网页内容之上,并且带有 pointer-events: none。这意味着用户仍然可以点击下方的内容块。

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

与纵向监听 window 上的 pointermove 事件的方式非常相似,<canvas> 元素也会如此。不过,每次触发该事件时,我们都会创建一个对象,以在 <canvas> 元素上添加动画效果。这些对象表示光标路径中使用的形状。它们具有坐标和随机色调。

我们再次使用了前面提到的 mapRange 函数,因为我们可以使用它来将指针增量映射到 sizerate。这些对象存储在一个数组中,当对象被绘制到 <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 数组也会被清除。

以下是光标路径的实际应用!

无障碍功能审核

打造有趣的探索体验没什么好处,但如果无法为用户提供,那就没什么用了。事实证明,Adam 在这方面的专业知识非常宝贵,能够帮助 Chrometober 在发布前为无障碍功能审核做好准备。

该课程涵盖的一些重要领域:

  • 确保所使用的 HTML 符合语义规范。这包括适当的地标元素(例如图书的 <main>)、为每个内容块使用 <article> 元素,以及在引入首字母缩写词的 <abbr> 元素。随着这本书的创作,展望未来,使人人更加无障碍。标题和链接的使用使用户能够更轻松地进行导航。使用网页列表还意味着由辅助技术公布网页数量。
  • 确保所有图片都使用适当的 alt 属性。对于内嵌 SVG,系统会在必要时显示 title 元素。
  • 在改善体验时使用 aria 属性。对页面及其侧边使用 aria-label 可以告知用户它们所在的页面。对“阅读更多”链接使用 aria-describedBy 可传达内容块的文本。这样就能清楚了解链接会将用户转到何处。
  • 在内容区块的主题上,可以点击整张卡片,而不仅仅是“阅读更多”链接。
  • 前面使用了 IntersectionObserver 来跟踪哪些网页处于可见状态。这样做有很多好处,不仅仅与性能相关。如果页面不在用户视野范围内,任何动画或互动都会暂停。但是,这些网页还应用了 inert 属性。也就是说,使用屏幕阅读器的用户可以像视力正常的用户一样探索相同的内容。焦点仍保留在用户视野范围内,用户无法使用 Tab 键转到其他页面。
  • 最后但同样重要的是,我们利用媒体查询尊重用户的动作偏好。

这是该评价的屏幕截图,其中突出显示了我们已实施的部分措施。

元素表示为整本图书,这表示它应该是辅助技术用户找到的主要地标。详见屏幕截图。" width="800" height="465">

已打开 Chrometober 图书的屏幕截图。该界面的各个方面都配有绿框框,用于描述预期的无障碍功能和页面将提供的用户体验结果。例如,图片包含替代文本。另一个示例是无障碍功能标签,声明不在视图范围内的页面是无效的。屏幕截图中列出了更多信息。

经验总结

Chrometober 背后的动机不仅是为了突出显示来自社区的 Web 内容,也让我们有机会测试正在开发中的滚动链接动画 API polyfill。

在纽约举办的团队峰会上,我们留出了一个专题演讲,以测试该项目并解决出现的问题。该团队的贡献意义非凡。这也是列出所有需要解决的问题才能进行直播的绝佳机会。

CSS、界面和开发者工具团队围坐在会议室的桌旁。Una 站在一张布满便笺的白板前。其他团队成员坐在桌旁,拿着茶点和笔记本电脑。

例如,在设备上测试图书就会导致呈现问题。我们的图书在 iOS 设备上无法按预期呈现。视口单元会调整页面大小,但如果存在凹口,会影响图书。解决方案是在 meta 视口中使用 viewport-fit=cover

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

此会话还引发了 API polyfill 的一些问题。Bramus 在 polyfill 代码库中引发了这些问题。随后,他找到了这些问题的解决方案,并将它们合并到 polyfill。例如,此拉取请求通过向部分 polyfill 添加缓存来提升性能。

在 Chrome 中打开的演示的屏幕截图。开发者工具是打开的,会显示基准性能测量值。

在 Chrome 中打开的演示的屏幕截图。开发者工具处于打开状态,显示经过改进的性能衡量功能。

就是这样!

这是一个值得持续参与的真正有趣的项目,它打造了一种异想天开的滚动体验,重点介绍来自社区的精彩内容。不仅如此,它还非常适合测试 polyfill,以及向工程团队提供反馈,帮助改进 polyfill。

Chrometober 2022 即将大功告成。

希望您会喜欢!你最喜欢什么功能?您可以发 Twitter 微博告诉我们!

Jhey 拿着一张 Chrometober 人物的贴片集。

如果您在活动中见到我们,甚至还可以从某个团队那里抓到一些贴纸。

主打照片:David Menidrey 来自 Unsplash 网站