สร้างเอฟเฟกต์พารัลแลกซ์เมื่อเลื่อน

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

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

วิธีสร้างเอฟเฟกต์พารัลแลกซ์พื้นฐานมีดังนี้

  1. สร้างองค์ประกอบ Wrapper: องค์ประกอบนี้จะจัดกลุ่มเลเยอร์ทั้งหมดของเอฟเฟกต์พารัลแลกซ์เข้าด้วยกัน องค์ประกอบนี้ไม่ใช่องค์ประกอบที่เลื่อนได้ ดังนั้นจึงควรตัดส่วนที่เกินออก และกำหนด height ที่ตรงกับความสูงของเลเยอร์ใดเลเยอร์หนึ่งของเอฟเฟกต์พารัลแลกซ์

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. ประกาศเลเยอร์: เพิ่มเลเยอร์แต่ละรายการที่จะเคลื่อนที่ด้วยความเร็วที่ต่างกันภายใน Wrapper

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. เพิ่มภาพเคลื่อนไหวแบบแปล: กำหนดภาพเคลื่อนไหว CSS ที่เปลี่ยนพร็อพเพอร์ตี้ transform ของเลเยอร์ สำหรับเอฟเฟกต์พารัลแลกซ์ โดยทั่วไปคุณจะใช้ translateY เพื่อย้ายเลเยอร์ในแนวตั้ง

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. ตั้งค่า view-timeline: หากต้องการลิงก์ภาพเคลื่อนไหวกับตำแหน่งการเลื่อน ให้สร้าง view-timeline ในองค์ประกอบ Wrapper แล้วนำไปใช้กับเลเยอร์

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. สลับเวลาของภาพเคลื่อนไหว: หากต้องการให้เลเยอร์เคลื่อนไหวด้วยความเร็วที่ต่างกัน คุณสามารถใช้วิธีหลัก 2 วิธี ได้แก่ สลับเวลาในคีย์เฟรม หรือสลับเวลาของ animation-range

    ทั้ง 2 แนวทางนี้สามารถใช้ค่าที่ฮาร์ดโค้ดหรือใช้การติดตั้งใช้งาน sibling-index()/sibling-count() ก็ได้ ค่าที่ฮาร์ดโค้ดเป็นค่าที่ง่ายที่สุดและยังมีประโยชน์เมื่อมีเลเยอร์จำนวนจำกัดเท่านั้น การติดตั้งใช้งาน sibling-index()/sibling-count() จะมีประโยชน์เมื่อคุณมีหลายเลเยอร์

    • การจัดเรียงคีย์เฟรมแบบเหลื่อมกัน

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

      .layer:nth-child(1) { --offset: 100px; }
      .layer:nth-child(2) { --offset: 200px; }
      .layer:nth-child(3) { --offset: 300px; }
      
      @keyframes parallax {
        from {
          transform: translateY(var(--offset));
        }
      }
      

      เมื่อใช้ sibling-index() ให้ฟังก์ชัน sibling-index() แสดงผลดัชนีขององค์ประกอบย่อยในบรรดาองค์ประกอบระดับเดียวกันเพื่อคำนวณเอฟเฟกต์แบบสลับกันโดยอัตโนมัติ

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • การจัดเรียง animation-range:

      เมื่อใช้ค่าที่ฮาร์ดโค้ด คุณจะกำหนดขอบเขตของ animation-range ในแต่ละเลเยอร์แยกกันได้อย่างชัดเจน

      .layer:nth-child(1) { animation-range: entry 25% exit 50%; }
      .layer:nth-child(2) { animation-range: entry 25% exit 75%; }
      .layer:nth-child(3) { animation-range: entry 25% exit 100%; }
      

      เมื่อใช้ sibling-index() และ sibling-count() คุณจะคำนวณช่วงทางคณิตศาสตร์ได้โดยอิงตามจำนวนเลเยอร์ทั้งหมด (sibling-count())

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

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

@keyframes parallax {
  from {
    transform: translateY(calc(100px * sibling-index()));
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

หรือจะใช้พร็อพเพอร์ตี้ animation-range เพื่อให้ได้ผลลัพธ์ที่คล้ายกันก็ได้ ดังนี้

@keyframes parallax {
  from {
    transform: translateY(700px);
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
  animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

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

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

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

สำหรับการตั้งค่า animation-range ให้ทำดังนี้

  • ควรกำหนดให้เลเยอร์ทั้งหมดมีออฟเซ็ตเริ่มต้นเดียวกัน เช่น entry 25%
  • ควรกำหนดค่าออฟเซ็ตสิ้นสุดที่แตกต่างกันให้กับเลเยอร์ทั้งหมดที่ใช้ sibling-count() และ sibling-index() เพื่อกระจายค่าออฟเซ็ต เช่น exit calc(100% / sibling-count() * sibling-index())

การรองรับเบราว์เซอร์และกลยุทธ์สำรอง

ดังนั้น โดยทั่วไปจึงต้องมีกลยุทธ์สำรอง

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

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

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

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

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const wrapper = document.querySelector('.wrapper');
  const layers = document.querySelectorAll('.layer');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        window.addEventListener('scroll', onScroll);
      } else {
        window.removeEventListener('scroll', onScroll);
      }
    });
  }, { threshold: 0 });

  observer.observe(wrapper);

  function onScroll() {
    const scrollY = window.scrollY;
    const wrapperRect = wrapper.getBoundingClientRect();
    const wrapperTop = wrapperRect.top + scrollY;
    const wrapperHeight = wrapperRect.height;
    const windowHeight = window.innerHeight;

    if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
      const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
      
      layers.forEach((layer, index) => {
        // This matches the effect as defined in the CSS example above.
        // Customize this further if needed.
        const initialTranslateY = 100 * index;
        const translateY = initialTranslateY * (1 - scrollPercent);
        layer.style.transform = `translateY(${translateY}px)`;
      });
    }
  }

  // Trigger onScroll once to set initial positions
  onScroll();
}