สร้าง Chrometober!

เบื้องหลังการสร้างหนังสือแบบเลื่อนเพื่อแชร์เคล็ดลับและลูกเล่นสนุกๆ น่ากลัวๆ ตลอดช่วง Chrometober

ต่อจาก Designcember ในปีนี้เราจึงอยากสร้าง Chrometober ขึ้นมาเพื่อไฮไลต์และแชร์เนื้อหาบนเว็บจากชุมชนและทีม Chrome Designcember แสดงการใช้คำค้นหาคอนเทนเนอร์ แต่ปีนี้เราจะแสดง API ภาพเคลื่อนไหวที่เชื่อมโยงกับการเลื่อนของ CSS

ดูประสบการณ์การอ่านหนังสือแบบเลื่อนได้ที่ web.dev/chrometober-2022

ภาพรวม

เป้าหมายของโปรเจ็กต์นี้คือมอบประสบการณ์ที่แปลกใหม่โดยเน้นที่ API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อน แต่แม้จะดูแปลกตา แต่ประสบการณ์การใช้งานก็ต้องตอบสนองและเข้าถึงได้เช่นกัน โปรเจ็กต์นี้ยังเป็นวิธีที่ดีในการทดลองใช้ polyfill ของ API ที่อยู่ระหว่างการพัฒนา รวมถึงลองใช้เทคนิคและเครื่องมือต่างๆ ร่วมกัน และทั้งหมดนี้มาในธีมฮาโลวีนที่สนุกสนาน

โครงสร้างทีมของเรามีลักษณะดังนี้

  • Tyler Reed: ภาพประกอบและการออกแบบ
  • Jhey Tompkins: หัวหน้าฝ่ายสถาปัตยกรรมและครีเอทีฟโฆษณา
  • Una Kravets: หัวหน้าโปรเจ็กต์
  • Bramus Van Damme: ผู้มีส่วนร่วมในเว็บไซต์
  • Adam Argyle: การตรวจสอบการช่วยเหลือพิเศษ
  • Aaron Forinton: บริการเขียนโฆษณาและการตลาด

การร่างประสบการณ์การเล่าเรื่องแบบเลื่อน

แนวคิดสำหรับ Chrometober เริ่มก่อตัวขึ้นเมื่อทีมของเราจัดการประชุมนอกสถานที่ครั้งแรกเมื่อเดือนพฤษภาคม 2022 ภาพร่างชุดนี้ทำให้เรานึกถึงวิธีที่ผู้ใช้สามารถเลื่อนดูสตอรีบอร์ดรูปแบบต่างๆ เราได้รับแรงบันดาลใจจากวิดีโอเกมและพิจารณาประสบการณ์การเลื่อนผ่านฉากต่างๆ เช่น สุสานและบ้านผีสิง

สมุดโน้ตวางอยู่บนโต๊ะพร้อมภาพวาดและการขีดเขียนต่างๆ ที่เกี่ยวข้องกับโปรเจ็กต์

เรารู้สึกตื่นเต้นที่ได้แสดงความคิดสร้างสรรค์อย่างเต็มที่เพื่อนำโปรเจ็กต์ Google โปรเจ็กต์แรกไปในทางที่คาดไม่ถึง นี่เป็นโปรโตไทป์ขั้นต้นของวิธีที่ผู้ใช้อาจไปยังส่วนต่างๆ ของเนื้อหา

เมื่อผู้ใช้เลื่อนไปด้านข้าง บล็อกจะหมุนและขยายเข้า แต่เราตัดสินใจที่จะไม่ใช้แนวคิดนี้เนื่องจากกังวลว่าจะทำอย่างไรให้ประสบการณ์นี้ยอดเยี่ยมสำหรับผู้ใช้ในอุปกรณ์ทุกขนาด แต่เรากลับเลือกการออกแบบที่คล้ายกับสิ่งที่เคยทำในอดีต ในปี 2020 เราโชคดีที่ได้เข้าถึง ScrollTrigger ของ GreenSock เพื่อสร้างเดโมรุ่นที่เผยแพร่

หนึ่งในเดโมที่เราสร้างคือสมุดภาพ 3 มิติที่สร้างด้วย CSS ซึ่งหน้าหนังสือจะพลิกไปมาขณะที่คุณเลื่อนดู และวิธีนี้ดูเหมาะสมกับสิ่งที่เราต้องการสำหรับ Chrometober มากกว่า Animations API ที่ลิงก์กับการเลื่อนเป็นตัวเลือกที่เหมาะสําหรับฟังก์ชันการทํางานดังกล่าว และยังใช้กับ scroll-snap ได้ดีด้วย

Tyler Reed นักวาดภาพประกอบของโปรเจ็กต์นี้เก่งมากในการปรับเปลี่ยนการออกแบบเมื่อเราเปลี่ยนไอเดีย Tyler ถ่ายทอดไอเดียสร้างสรรค์ทั้งหมดที่ได้รับมาได้อย่างยอดเยี่ยม เราสนุกมากที่ได้ระดมความคิดร่วมกัน สิ่งสำคัญอย่างหนึ่งที่เราต้องการให้ฟีเจอร์นี้ทำงานคือมีการแยกฟีเจอร์ออกเป็นบล็อกแยกต่างหาก วิธีนี้ช่วยให้เราจัดองค์ประกอบเป็นฉากต่างๆ แล้วเลือกสิ่งที่จะนำมาทำให้มีชีวิตขึ้นมาได้

ฉากองค์ประกอบภาพหนึ่งๆ ที่มีงู โลงศพที่มีมือโผล่ออกมา สุนัขจิ้งจอกถือไม้กายสิทธิ์ที่กาน้ำ ต้นไม้ที่มีใบหน้าน่ากลัว และก็อบลินถือตะเกียงฟักทอง

แนวคิดหลักคือเมื่อผู้ใช้อ่านหนังสือ ผู้ใช้จะสามารถเข้าถึงบล็อกเนื้อหาได้ นอกจากนี้ ผู้ใช้ยังโต้ตอบกับสิ่งต่างๆ ได้อย่างสนุกสนาน เช่น ไข่อีสเตอร์ที่เราใส่ไว้ในประสบการณ์การใช้งาน เช่น ภาพวาดในคฤหาสน์ผีสิงที่ดวงตาจะติดตามเคอร์เซอร์ของคุณ หรือภาพเคลื่อนไหวเล็กๆ น้อยๆ ที่เรียกให้แสดงโดยคําค้นหาสื่อ ไอเดียและฟีเจอร์เหล่านี้จะเคลื่อนไหวเมื่อเลื่อน แนวคิดแรกๆ คือกระต่ายซอมบี้ที่จะลอยขึ้นและเลื่อนไปตามแกน x เมื่อผู้ใช้เลื่อน

ทำความเข้าใจ API

ก่อนที่จะเริ่มเล่นกับฟีเจอร์และอีสเตอร์เอกของแต่ละรายการ เราต้องมีสมุดภาพ เราจึงตัดสินใจใช้โอกาสนี้ในการทดสอบชุดฟีเจอร์สำหรับ CSS scroll-linked animations API ที่เพิ่งเปิดตัว ปัจจุบันเบราว์เซอร์ทุกประเภทยังไม่รองรับ API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อน อย่างไรก็ตาม ในระหว่างการพัฒนา API วิศวกรในทีมการโต้ตอบได้ทํางานเกี่ยวกับ polyfill วิธีนี้ช่วยให้คุณทดสอบรูปแบบของ API ขณะพัฒนาได้ ซึ่งหมายความว่าเราสามารถใช้ API นี้ได้ในวันนี้ และโปรเจ็กต์สนุกๆ แบบนี้มักเป็นโอกาสที่ดีในการลองใช้ฟีเจอร์ทดลองและแสดงความคิดเห็น ดูสิ่งที่เราได้เรียนรู้และความคิดเห็นที่เราให้ไว้ได้ในบทความนี้

ในระดับสูง คุณสามารถใช้ API นี้เพื่อลิงก์ภาพเคลื่อนไหวกับการเลื่อน โปรดทราบว่าคุณไม่สามารถเรียกใช้ภาพเคลื่อนไหวเมื่อเลื่อนดูได้ ซึ่งอาจทำได้ในภายหลัง ภาพเคลื่อนไหวที่เชื่อมโยงกับการเลื่อนแบ่งออกเป็น 2 หมวดหมู่หลัก ได้แก่

  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 และกำหนดแกนให้กับ 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;
  }
}

เมื่อใช้วิธีนี้ "Mover" จะขยายขนาดเมื่อเข้าสู่วิวพอร์ต ซึ่งจะทริกเกอร์การหมุนของ "Spinner"

สิ่งที่เราพบจากการทดสอบคือ API ทำงานร่วมกับ scroll-snap ได้เป็นอย่างดี การเลื่อนแบบ 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 ใน 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 เพื่อให้มีลักษณะเหมือนหนังสือ

สรุปข้อมูลทั้งหมด

เมื่อคิดกลไกของหนังสือได้แล้ว ฉันก็มุ่งเน้นที่การทำให้ภาพประกอบของ Tyler มีชีวิตชีวา

Astro

ทีมใช้ Astro สำหรับ Designcember ในปี 2021 และเราก็อยากใช้ Astro อีกครั้งสำหรับ Chrometober ประสบการณ์ของนักพัฒนาซอฟต์แวร์ในการแยกองค์ประกอบต่างๆ ออกเป็นส่วนๆ เหมาะกับโปรเจ็กต์นี้มาก

หนังสือเองก็เป็นองค์ประกอบหนึ่ง และยังเป็นคอลเล็กชันของคอมโพเนนต์หน้าเว็บด้วย แต่ละหน้ามี 2 ด้านและมีพื้นหลัง องค์ประกอบย่อยของส่วนในหน้าเว็บคือคอมโพเนนต์ที่เพิ่ม นําออก และจัดตําแหน่งได้อย่างง่ายดาย

การสร้างสมุดภาพ

เราจึงต้องทำให้บล็อกจัดการได้ง่าย นอกจากนี้ เรายังต้องการให้สมาชิกคนอื่นๆ ในทีมมีส่วนร่วมได้อย่างง่ายดาย

หน้าเว็บในระดับสูงจะกำหนดโดยอาร์เรย์การกําหนดค่า ออบเจ็กต์หน้าเว็บแต่ละรายการในอาร์เรย์จะกำหนดเนื้อหา พื้นหลัง และข้อมูลเมตาอื่นๆ สำหรับหน้าเว็บ

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

เนื้อหาของหน้า

มาดูการสร้างหน้าเว็บกัน หน้า 3 มีนกฮูกโผล่มาบนต้นไม้

ระบบจะป้อนข้อมูลคอมโพเนนต์ PageThree ตามที่กําหนดไว้ในการกําหนดค่า ไฟล์นี้เป็นคอมโพเนนต์ Astro (PageThree.astro) คอมโพเนนต์เหล่านี้มีลักษณะเหมือนไฟล์ HTML แต่มีเครื่องหมายวงเล็บโค้ดที่ด้านบนคล้ายกับส่วนหน้า ซึ่งช่วยให้เราทําสิ่งต่างๆ ได้ เช่น นําเข้าคอมโพเนนต์อื่นๆ คอมโพเนนต์ของหน้า 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 ที่แชร์ซึ่งสร้างขึ้นอย่างไร

ในระดับสูง คอมโพเนนต์นกฮูกจะนำเข้า SVG บางรายการและแทรกไว้โดยใช้ Fregment ของ Astro

---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />

และสไตล์สำหรับการจัดตําแหน่งนกฮูกอยู่ร่วมกับโค้ดคอมโพเนนต์

.owl {
  width: 34%;
  left: 10%;
  bottom: 34%;
}

มีการจัดรูปแบบเพิ่มเติมอีก 1 รายการซึ่งกำหนดลักษณะการทำงาน 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()

ในบล็อกโค้ดนี้ เราจะทํา 2 อย่างดังนี้

  1. ตรวจสอบค่ากำหนดการเคลื่อนไหวของผู้ใช้
  2. หากไม่มีค่ากำหนด ให้ลิงก์ภาพเคลื่อนไหวนกฮูกเพื่อเลื่อน

ส่วนส่วนที่ 2 นกฮูกจะเคลื่อนไหวบนแกน y โดยใช้ Web Animations API ใช้พร็อพเพอร์ตี้การเปลี่ยนรูปแบบ translate แต่ละรายการ และเชื่อมโยงกับ ViewTimeline รายการเดียว ลิงก์กับ CHROMETOBER_TIMELINES[1] ผ่านพร็อพเพอร์ตี้ timeline นี่คือ ViewTimeline ที่สร้างขึ้นสำหรับการพลิกหน้า ซึ่งจะลิงก์ภาพเคลื่อนไหวของนกฮูกกับการพลิกหน้าโดยใช้เฟส enter ซึ่งกําหนดว่าเมื่อหน้าเว็บเลื่อนไป 80% ให้เริ่มขยับนกฮูก เมื่อถึง 90% นกฮูกจะแปลให้เสร็จ

ฟีเจอร์ของหนังสือ

ตอนนี้คุณได้เห็นแนวทางการสร้างหน้าเว็บและวิธีการทำงานของสถาปัตยกรรมโปรเจ็กต์แล้ว คุณจะเห็นว่าเครื่องมือนี้ช่วยให้ผู้มีส่วนร่วมสามารถเริ่มทำงานในหน้าหรือฟีเจอร์ที่เลือกได้อย่างไร ฟีเจอร์ต่างๆ ในหนังสือมีภาพเคลื่อนไหวที่เชื่อมโยงกับการพลิกหน้าหนังสือ เช่น ค้างคาวที่บินเข้าและออกเมื่อพลิกหน้าหนังสือ

นอกจากนี้ยังมีองค์ประกอบที่ทำงานด้วยภาพเคลื่อนไหว CSS

เมื่อบล็อกเนื้อหาอยู่ในสมุดแล้ว ก็ถึงเวลาใช้ความคิดสร้างสรรค์กับฟีเจอร์อื่นๆ ซึ่งเปิดโอกาสให้สร้างการโต้ตอบที่แตกต่างออกไป และลองใช้วิธีต่างๆ ในการใช้งาน

การทำให้เนื้อหาปรับเปลี่ยนตามอุปกรณ์ได้ตลอด

หน่วยวิวพอร์ตที่ปรับเปลี่ยนตามอุปกรณ์จะปรับขนาดหนังสือและฟีเจอร์ของหนังสือ อย่างไรก็ตาม การรักษาแบบอักษรให้ตอบสนองได้อย่างต่อเนื่องเป็นโจทย์ที่น่าสนใจ หน่วยการค้นหาคอนเทนเนอร์เหมาะสําหรับกรณีนี้ แต่ฟีเจอร์นี้ยังไม่พร้อมให้บริการในทุกพื้นที่ เราได้ตั้งค่าขนาดของสมุดแล้ว จึงไม่จำเป็นต้องใช้การค้นหาคอนเทนเนอร์ คุณสามารถสร้างหน่วยคิวรีคอนเทนเนอร์แบบอินไลน์ด้วย CSS calc() และใช้สำหรับการปรับขนาดแบบอักษรได้


.book-placeholder {
  --size: clamp(12rem, 72vw, 80vmin);
  --aspect-ratio: 360 / 504;
  --cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}

.content-block h2 {
  color: var(--gray-0);
  font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}

.content-block :is(p, a) {
  font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}

ฟักทองที่ส่องแสงตอนกลางคืน

ผู้ที่มีสายตาแหลมคมอาจสังเกตเห็นการใช้องค์ประกอบ <source> เมื่อเราพูดถึงพื้นหลังของหน้าเว็บก่อนหน้านี้ Una ต้องการการโต้ตอบที่ตอบสนองต่อรูปแบบสีที่ต้องการ ด้วยเหตุนี้ พื้นหลังจึงรองรับทั้งโหมดสว่างและโหมดมืดด้วยรูปแบบที่แตกต่างกัน เนื่องจากคุณสามารถใช้ Media Query กับองค์ประกอบ <picture> ได้ จึงเหมาะอย่างยิ่งที่จะระบุสไตล์ฉากหลัง 2 แบบ องค์ประกอบ <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>

คุณอาจทำการเปลี่ยนแปลงอื่นๆ ตามค่ากำหนดรูปแบบสีนั้น ฟักทองในหน้า 2 จะตอบสนองต่อค่ากำหนดรูปแบบสีของผู้ใช้ 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() เพื่อทําให้แต่ละตามีพฤติกรรมแตกต่างกัน คุณจึงทําให้แต่ละตามีพฤติกรรมแตกต่างกันได้โดยไม่ต้องแตะ 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 คุณรู้สึกหลงใหลไหม หน้านี้ใช้การออกแบบของ Fox แสนวิเศษของเรา หากเลื่อนเคอร์เซอร์ไปรอบๆ คุณอาจเห็นเอฟเฟกต์ร่องรอยเคอร์เซอร์ที่กำหนดเอง ซึ่งใช้ภาพเคลื่อนไหว Canvas องค์ประกอบ <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)
 }

หากหน้าเว็บไม่อยู่ในมุมมอง ระบบจะนำ Listener เหตุการณ์ออกและยกเลิกการวนซ้ำเฟรมภาพเคลื่อนไหว ระบบจะล้างอาร์เรย์ blocks ด้วย

นี่เป็นภาพการทำงานของร่องรอยเคอร์เซอร์

การตรวจสอบการช่วยเหลือพิเศษ

การสร้างประสบการณ์การสำรวจที่สนุกสนานนั้นเป็นเรื่องที่ดี แต่จะไม่มีประโยชน์หากผู้ใช้เข้าถึงไม่ได้ ความเชี่ยวชาญของ Adam ในด้านนี้มีประโยชน์อย่างยิ่งในการเตรียม Chrometober ให้พร้อมรับการตรวจสอบการช่วยเหลือพิเศษก่อนการเผยแพร่

หัวข้อที่น่าสนใจบางส่วนที่ครอบคลุมมีดังนี้

  • ตรวจสอบว่า HTML ที่ใช้เป็น HTML เชิงความหมาย ซึ่งรวมถึงองค์ประกอบจุดสังเกตที่เหมาะสม เช่น <main> สำหรับหนังสือ รวมถึงการใช้องค์ประกอบ <article> สำหรับบล็อกเนื้อหาแต่ละบล็อก และองค์ประกอบ <abbr> ที่แสดงคำย่อ การคิดล่วงหน้าขณะสร้างหนังสือทำให้ทุกอย่างเข้าถึงได้ง่ายขึ้น การใช้ส่วนหัวและลิงก์ช่วยให้ผู้ใช้ไปยังส่วนต่างๆ ได้ง่ายขึ้น การใช้รายการสำหรับหน้าเว็บยังหมายความว่าเทคโนโลยีความช่วยเหลือพิเศษจะประกาศจำนวนหน้าด้วย
  • ตรวจสอบว่ารูปภาพทั้งหมดใช้แอตทริบิวต์ alt ที่เหมาะสม สำหรับ SVG แทรกในบรรทัด องค์ประกอบ title จะปรากฏขึ้นตามที่จำเป็น
  • ใช้แอตทริบิวต์ aria ในกรณีที่ช่วยปรับปรุงประสบการณ์การใช้งาน การใช้ aria-label สำหรับหน้าเว็บและส่วนต่างๆ ของหน้าเว็บจะบอกให้ผู้ใช้ทราบว่ากำลังดูหน้าใดอยู่ การใช้ aria-describedBy ในลิงก์ "อ่านเพิ่มเติม" จะสื่อข้อความของบล็อกเนื้อหา วิธีนี้จะช่วยลดความคลุมเครือเกี่ยวกับปลายทางของลิงก์
  • ในส่วนบล็อกเนื้อหา ผู้ใช้สามารถคลิกทั้งการ์ด ไม่ใช่แค่ลิงก์ "อ่านเพิ่มเติม"
  • การใช้ IntersectionObserver เพื่อติดตามหน้าเว็บที่ผู้ใช้ดูอยู่ได้ถูกกล่าวถึงก่อนหน้านี้ ซึ่งมีประโยชน์หลายอย่างนอกเหนือจากประสิทธิภาพ หน้าเว็บที่ไม่ได้อยู่ในมุมมองจะหยุดภาพเคลื่อนไหวหรือการโต้ตอบชั่วคราว แต่หน้าเหล่านี้มีแอตทริบิวต์ inert ด้วย ซึ่งหมายความว่าผู้ใช้ที่ใช้โปรแกรมอ่านหน้าจอจะสำรวจเนื้อหาเดียวกันกับผู้ใช้ที่มองเห็นได้ โฟกัสจะยังคงอยู่ในหน้าเว็บที่แสดงอยู่และผู้ใช้จะกด Tab ไปยังหน้าอื่นไม่ได้
  • สุดท้ายแต่ไม่ท้ายสุด เราใช้ Media Query เพื่อเคารพความต้องการของผู้ใช้เกี่ยวกับภาพเคลื่อนไหว

ภาพหน้าจอจากการตรวจสอบที่ไฮไลต์มาตรการบางส่วนที่ใช้อยู่มีดังนี้

องค์ประกอบเป็นจุดสังเกตหลักที่ผู้ใช้เทคโนโลยีความช่วยเหลือพิเศษควรพบ ดูข้อมูลเพิ่มเติมได้ที่ภาพหน้าจอ" width="800" height="465">

ภาพหน้าจอของสมุด Chrometober ที่เปิดอยู่ กล่องขอบสีเขียวจะแสดงอยู่รอบๆ แง่มุมต่างๆ ของ UI ซึ่งจะอธิบายฟังก์ชันการช่วยเหลือพิเศษที่ต้องการและผลลัพธ์ด้านประสบการณ์ของผู้ใช้ที่หน้าเว็บจะมอบให้ เช่น รูปภาพมีข้อความแสดงแทน อีกตัวอย่างหนึ่งคือป้ายกำกับการช่วยเหลือพิเศษที่ระบุว่าหน้าเว็บที่ผู้ใช้มองไม่เห็นจะไม่มีการโต้ตอบ โปรดดูรายละเอียดเพิ่มเติมในภาพหน้าจอ

สิ่งที่เราได้เรียนรู้

แรงจูงใจเบื้องหลัง Chrometober ไม่ได้มีไว้เพื่อไฮไลต์เนื้อหาบนเว็บจากชุมชนเท่านั้น แต่ยังเป็นวิธีให้เราได้ทดลองใช้ polyfill ของ API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อนที่อยู่ระหว่างการพัฒนาด้วย

เราได้จัดเซสชันไว้ขณะเข้าร่วมการประชุมระดับทีมในนิวยอร์กเพื่อทดสอบโปรเจ็กต์และจัดการกับปัญหาที่เกิดขึ้น ผลงานของทีมนั้นประเมินค่าไม่ได้ และยังเป็นโอกาสที่ดีในการระบุสิ่งที่ต้องจัดการทั้งหมดก่อนที่จะเปิดตัว

ทีม CSS, UI และ DevTools นั่งล้อมโต๊ะในห้องประชุม Una ยืนอยู่ที่ไวท์บอร์ดที่มีโน้ตติดหนึบเต็มไปหมด สมาชิกคนอื่นๆ ในทีมนั่งล้อมรอบโต๊ะพร้อมเครื่องดื่มและแล็ปท็อป

เช่น การทดสอบหนังสือในอุปกรณ์ทำให้เกิดปัญหาการแสดงผล หนังสือของเราแสดงผลในอุปกรณ์ iOS ไม่ได้ตามที่คาดไว้ หน่วยวิวพอร์ตจะกำหนดขนาดหน้าเว็บ แต่เมื่อมีรอยบาก รอยบากก็จะส่งผลต่อหนังสือ วิธีการแก้ปัญหาคือการใช้ viewport-fit=cover ในวิวพอร์ต meta

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

เซสชันนี้ยังทำให้เกิดปัญหาบางอย่างเกี่ยวกับ polyfill ของ API ด้วย Bramus ได้แจ้งปัญหาเหล่านี้ในที่เก็บข้อมูล polyfill หลังจากนั้นเขาพบวิธีแก้ปัญหาเหล่านั้นและผสานเข้ากับ polyfill ตัวอย่างเช่น คำขอดึงข้อมูลนี้ทำให้ประสิทธิภาพเพิ่มขึ้นด้วยการเพิ่มการแคชไปยังส่วนหนึ่งของ polyfill

ภาพหน้าจอของเดโมที่เปิดใน Chrome เครื่องมือสําหรับนักพัฒนาซอฟต์แวร์จะเปิดขึ้นและแสดงการวัดประสิทธิภาพพื้นฐาน

ภาพหน้าจอของเดโมที่เปิดใน Chrome เครื่องมือสําหรับนักพัฒนาซอฟต์แวร์จะเปิดขึ้นและแสดงการวัดประสิทธิภาพที่ปรับปรุงแล้ว

เท่านี้ก็เรียบร้อย

นี่เป็นโปรเจ็กต์ที่สนุกมากที่ได้ทํางานด้วย ส่งผลให้เราได้รับประสบการณ์การเลื่อนดูที่แปลกใหม่ซึ่งไฮไลต์เนื้อหาที่น่าทึ่งจากชุมชน ไม่เพียงเท่านั้น แต่ยังเหมาะสําหรับการทดสอบ polyfill รวมถึงการให้ความคิดเห็นแก่ทีมวิศวกรเพื่อช่วยปรับปรุง polyfill ด้วย

Chrometober 2022 จบลงแล้ว

หวังว่าคุณจะชอบ ฟีเจอร์ที่คุณชอบคืออะไร โปรดทวีตถึงเราและแจ้งให้เราทราบ

Jhey ถือแผ่นสติกเกอร์ของตัวละครจาก Chrometober

คุณอาจขอรับสติกเกอร์จากสมาชิกในทีมได้ด้วยหากพบเราในงานกิจกรรม

รูปภาพหลักโดย David Menidrey ใน Unsplash