Scrollytelling

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

วิธีการนำไปใช้งาน

หากต้องการสร้างประสบการณ์การเล่าเรื่องแบบ Scrollytelling คุณต้องมีองค์ประกอบ 2 ชุด ได้แก่ องค์ประกอบหนึ่งเพื่อติดตามตำแหน่งการเลื่อน และอีกองค์ประกอบหนึ่งเพื่อสร้างภาพเคลื่อนไหว

ก่อนอื่น ให้กำหนด view-timeline ที่มีชื่อในองค์ประกอบที่ต้องการติดตาม ซึ่งจะทำหน้าที่เป็นตัวขับเคลื่อนภาพเคลื่อนไหว

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

ถัดไป ให้ใช้ภาพเคลื่อนไหวกับองค์ประกอบที่ต้องการทำให้เคลื่อนไหว และลิงก์องค์ประกอบเหล่านั้นกับไทม์ไลน์ที่คุณเพิ่งสร้างโดยใช้พร็อพเพอร์ตี้ animation-timeline

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

เพื่อให้ animation-timeline สามารถอ้างอิงไทม์ไลน์ที่มีชื่อได้ ไทม์ไลน์เหล่านั้นจะต้องอยู่ในขอบเขตเดียวกัน คุณสามารถใช้พร็อพเพอร์ตี้ timeline-scope ในระดับบนสุดที่ใช้ร่วมกันเพื่อให้ไทม์ไลน์พร้อมใช้งานสำหรับองค์ประกอบทั้งหมดที่ต้องการ องค์ประกอบ :root มักเป็นตัวเลือกที่ดีสำหรับกรณีนี้

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

สุดท้าย คุณสามารถใช้พร็อพเพอร์ตี้ animation-range เพื่อระบุช่วงเวลาที่แน่นอนของไทม์ไลน์ที่ภาพเคลื่อนไหวควรทำงาน ซึ่งจะช่วยให้คุณควบคุมได้อย่างละเอียดว่าภาพเคลื่อนไหวจะเริ่มทำงานเมื่อใดและจะดำเนินไปอย่างไร

#animated section {
  animation-range: entry 25% cover 50%, exit 50% exit 75%;
}

โค้ดตัวอย่าง

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

@keyframes animate-in {
  from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
  to { transform: rotateY(0deg); }
}
@keyframes animate-out {
  to { translate: 100% 0; opacity: 0; }
}

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
    backface-visibility: hidden;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

แนวทางปฏิบัติแนะนำ

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

  • ควรรวมการตรวจหาฟีเจอร์: เบราว์เซอร์บางรายการไม่รองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน ใช้ @supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) เพื่อตรวจสอบการรองรับและระบุตัวเลือกสำรองสำหรับเบราว์เซอร์ที่ไม่รองรับ
    • ต้องรวมการตรวจสอบ (animation-range: 0% 100%) ไว้ที่นี่เพื่อกรองเบราว์เซอร์ที่รองรับเพียงบางส่วนออก
    • อย่าใช้แพ็กเกจ scroll-timeline-polyfill สำหรับกลยุทธ์สำรอง เนื่องจากฟีเจอร์ยังไม่สมบูรณ์และมีปัญหาที่ทราบอยู่แล้วจำนวนมาก
    • หากภาพเคลื่อนไหวถือเป็นเพียงการตกแต่ง ให้เลือกใช้การเพิ่มประสิทธิภาพแบบต่อเนื่อง และอย่าระบุทางเลือกสำรอง
  • ควรเคารพค่ากำหนดของผู้ใช้: ผู้ใช้บางรายต้องการให้มีการเคลื่อนไหวน้อยลงบนเว็บ ใช้คิวรีสื่อ prefers-reduced-motion เพื่อปิดใช้หรือลดภาพเคลื่อนไหวสำหรับผู้ใช้เหล่านี้
  • ควรพยายามทำให้เฉพาะพร็อพเพอร์ตี้ CSS ที่มีประสิทธิภาพเคลื่อนไหว: เพื่อให้ภาพเคลื่อนไหวราบรื่นที่สุด ให้ใช้พร็อพเพอร์ตี้ที่เคลื่อนไหวซึ่งจัดการได้โดยเทรด Compositor ของเบราว์เซอร์ เช่น transform และ opacity การทำให้พร็อพเพอร์ตี้อื่นๆ เช่น width หรือ height เคลื่อนไหวอาจทำให้เกิดปัญหาด้านประสิทธิภาพ
  • ควรใช้ลำดับการประกาศที่ถูกต้อง: เมื่อใช้พร็อพเพอร์ตี้ชวเลข animation ให้ประกาศ animation-timeline และ animation-range หลังจากพร็อพเพอร์ตี้ดังกล่าวเพื่อป้องกันไม่ให้ชวเลขรีเซ็ตไทม์ไลน์

เมื่อใช้พร็อพเพอร์ตี้ view-timeline เพื่อสร้างภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน ให้ทำดังนี้

  • ควรใช้ CSS <dashed-ident> สำหรับชื่อ
  • ไม่บังคับ ระบุแกนที่จะติดตามอย่างชัดเจน: เมื่อไม่ได้กำหนดเป้าหมายแกน block เริ่มต้น (เช่น ในแถบเลื่อนแนวนอน) ให้ระบุแกนที่จะติดตามด้วย view-timeline-axis อย่างชัดเจน
  • ควรตรวจสอบว่าขอบเขตของการค้นหาใช้งานได้: เมื่อองค์ประกอบที่ประกาศ view-timeline ไม่ใช่ระดับบนสุดแบบ Flat Tree ขององค์ประกอบที่เคลื่อนไหว ให้เพิ่มระดับการมองเห็นชื่อของ view-timeline โดยใช้ timeline-scope ในระดับบนสุดที่ใช้ร่วมกัน

กลยุทธ์สำรอง

สำหรับเบราว์เซอร์ที่ไม่รองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน คุณสามารถใช้โหมดสำรองเพื่อสร้างเอฟเฟกต์ภาพขึ้นมาใหม่ได้ โดยปกติแล้ว ฟังก์ชันสำรองจะสร้างขึ้นด้วย Listener การเลื่อน (สำหรับเอฟเฟกต์ ScrollTimeline) หรือ IntersectionObserver API (สำหรับเอฟเฟกต์ ViewTimeline)

ในเบราว์เซอร์ที่มีการรองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อนในตัว ให้ใช้การติดตั้งใช้งาน CSS แบบเนทีฟเสมอ เนื่องจากมีประสิทธิภาพมากกว่า

โปรดทราบว่าคุณไม่สามารถสร้างเอฟเฟกต์ทั้งหมดขึ้นมาใหม่ได้โดยใช้วิธีการสำรอง

สำหรับ Use Case นี้โดยเฉพาะ สคริปต์ต่อไปนี้จะใช้โหมดสำรองสำหรับเบราว์เซอร์ที่ไม่รองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน โดยใช้ IntersectionObserver เพื่อติดตามระดับการมองเห็นขององค์ประกอบ #tracked section แต่ละรายการ และอัปเดตพร็อพเพอร์ตี้ transform ของ #animated section ที่เกี่ยวข้องตามลำดับ

const animatedSections = document.querySelectorAll('#animated section');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
    if (sectionIndex !== -1) {
      const animatedSection = animatedSections[sectionIndex];
      const ratio = entry.intersectionRatio;

      // Animate-in
      animatedSection.style.opacity = ratio;
      animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;

      // Animate-out
      if (ratio < 0.5) {
        animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
      } else {
        animatedSection.style.translate = '0 0';
      }
    }
  });
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });

document.querySelectorAll('#tracked section').forEach(section => {
  observer.observe(section);
});

และ CSS ที่มาพร้อมกัน:

#animated section {
  opacity: 0;
  transform: scale(0.5)  rotateY(-180deg);
  backface-visibility: hidden;
}

/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    opacity: 1 !important;
    transform: none !important;
    translate: 0 0 !important;
  }
}

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