יצירת אפקטים של שקפים בקרוסלה

אפקטים של מעבר בין שקפים בקרוסלה הם דרך מצוינת להוסיף עניין חזותי לקרוסלה. כשהמשתמש גולל בין השקפים, כל שקף יכול להציג אנימציה כשהוא נכנס, מתמרכז ויוצא מאזור הגלילה. לדוגמה, השקפים יכולים להופיע ולהיעלם בהדרגה, להסתובב או לשנות את הגודל שלהם. כך נוצרת חוויית משתמש דינמית ומעניינת. בניגוד לאנימציות פשוטות של כניסה/יציאה, האפקט הזה משתמש באנימציה אחת רציפה כדי לשלוט במראה של השקף בכל אזור הגלילה.

איך מטמיעים

כך יוצרים אפקטים של שקפים בקרוסלה:

  1. יצירת רכיב גלילה: הרכיב הזה ישמש כמאגר לשקפים בקרוסלה. בדוגמה הזו נעשה שימוש ב-overflow-x: scroll כדי לאפשר גלילה אופקית.

    <ul class="scroller">
      <li class="entry">1</li>
      <li class="entry">2</li>
      <li class="entry">3</li>
      …
    </ul>
    
    .scroller {
      overflow-x: scroll;
    }
    
  2. הגדרת האנימציה: יוצרים אנימציית CSS שמגדירה את המצבים השונים של השקפים כשהם עוברים דרך אזור הגלילה. אפשר להגדיר קיפריימים לכל חלק באנימציה. לדוגמה, אפשר להגדיר מצב שבו השקפים נמצאים במרכז של אזור הגלילה על ידי הכללת פריים מפתח 50%. בדוגמה הזו, המאפיין scale גורם לשקפים לגדול כשהם מתקרבים למרכז ולהתכווץ כשהם מתרחקים.

    @keyframes animate {
      0% {
        scale: 0.5;
      }
      50% {
        scale: 1;
      }
      100% {
        scale: 0.5;
      }
    }
    
  3. מחילים את האנימציה וview-timeline: מצרפים את האנימציה לשקפים של הקרוסלה ומקשרים אותה ל-view-timeline שעוקב אחרי הרכיב בזמן הגלילה במאגר.

    .scroller > * {
      animation: animate auto linear both;
      animation-timeline: view(inline);
    }
    

    כברירת מחדל, view() עוקב אחרי הרכיב בציר block. אם צריך לעקוב אחריו בציר inline, אפשר להשתמש ב-view(inline).

קוד לדוגמה

הקוד הזה מוסיף אנימציה לפריטים בקרוסלה של סרגל גלילה אופקי בזמן הגלילה, באמצעות ציר זמן אנונימי של תצוגה:

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

  50% {
    scale: 1;
  }

  100% {
    scale: 0.5;
  }
}

.scroller > * {
  /* Applies the animation using an `auto` duration */
  animation: animate auto linear both;
  /* Sets the animation timeline to use an anonymous view progress timeline, tracking the element's progress through the scroller on the inline axis */
  animation-timeline: view(inline);
}

הקוד הזה מנפיש את הפריטים בקרוסלה של סרגל גלילה אופקי בזמן הגלילה באמצעות ציר זמן של תצוגה עם שם:

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

  50% {
    scale: 1;
  }

  100% {
    scale: 0.5;
  }
}

/* This creates a named view-timeline on each carousel item. The timeline is used to drive the animation that is applied on the same element. */
.scroller > * {
  /* Applies the animation using an `auto` duration */
  animation: animate auto linear both;
  /* Defines a named view progress timeline, tracking the element's progress through the scroller on the inline axis */
  view-timeline: --item inline;
  /* Sets the animation timeline to use the named view progress timeline defined above */
  animation-timeline: --item;
}

שיטות מומלצות

כשמשתמשים באנימציות מבוססות-גלילה, חשוב לפעול לפי כמה שיטות מומלצות כדי להבטיח חוויה חלקה ונגישה:

  • מומלץ לכלול זיהוי תכונות: לא כל הדפדפנים תומכים באנימציות מבוססות-גלילה. כדאי להשתמש ב-@supports ((animation-timeline: view()) and (animation-range: entry)) כדי לבדוק אם יש תמיכה, ולספק חלופה לדפדפנים שלא תומכים בזה.
    • חובה לכלול כאן את הבדיקה של (animation-range: entry) כדי לסנן דפדפנים עם תמיכה חלקית בלבד.
    • אל תשתמשו בחבילה scroll-timeline-polyfill כאסטרטגיית גיבוי, כי היא לא כוללת את כל התכונות ויש בה הרבה בעיות ידועות.
    • אם האנימציה נחשבת רק לקישוט, כדאי לבחור בשיפור הדרגתי (progressive enhancement) ולא לספק חלופה.
  • חשוב לכבד את העדפות המשתמשים: חלק מהמשתמשים מעדיפים פחות תנועה באינטרנט. כדי להשבית או לצמצם את האנימציות למשתמשים האלה, צריך להשתמש בשאילתת המדיה prefers-reduced-motion.
  • מומלץ לנסות להנפיש רק מאפייני CSS עם ביצועים גבוהים: כדי שהאנימציות יהיו חלקות ככל האפשר, כדאי להנפיש רק מאפיינים שאפשר לטפל בהם בשרשור ה-compositor של הדפדפן, כמו transform ו-opacity. הנפשה של מאפיינים אחרים כמו width או height עלולה לגרום לבעיות בביצועים.
  • כן: משתמשים בסדר ההצהרה הנכון. כשמשתמשים במאפיין המקוצר animation, מצהירים על animation-timeline ו-animation-range אחריו כדי למנוע מהמאפיין המקוצר לאפס את ציר הזמן.

עדיף להשתמש ב-view-timeline עם שם כשצריך להוסיף אנימציה לכמה רכיבי DOM על סמך אותו ציר זמן, או כשצריך להוסיף אנימציה לרכיבים הצאצאים של הרכיב שבו מוגדר ה-view-timeline. אם הרכיב שאתם מוסיפים לו אנימציה הוא גם הרכיב שמגדיר את view-timeline, אתם יכולים להשתמש בציר זמן אנונימי של תצוגה באמצעות view().

כשמשתמשים בפונקציה view() כדי ליצור אנימציה שמופעלת בגלילה:

  • אופציונלי: ציינו במפורש את הציר שצריך לעקוב אחריו. אם אתם לא מטרגטים את הציר block שמוגדר כברירת מחדל (למשל, בסרגל גלילה אופקי), ציינו במפורש את הציר שצריך לעקוב אחריו באמצעות view(block) או view(inline).

כשמשתמשים במאפיין view-timeline כדי ליצור אנימציה שמופעלת בגלילה:

  • מומלץ להשתמש ב-<dashed-ident> של CSS בשביל השם (למשל view-timeline: --my-custom-name)
  • אופציונלי: מציינים במפורש את הציר למעקב: כשלא מטרגטים את ציר ברירת המחדל block (למשל, בסרגל גלילה אופקי), מציינים במפורש את הציר למעקב באמצעות view-timeline-axis.
  • חשוב לוודא שהיקף החיפוש פועל: כשהרכיב שמצהיר על view-timeline הוא לא רכיב אב בעץ שטוח של הרכיב המונפש, צריך להגדיל את החשיפה של השם של view-timeline באמצעות timeline-scope ברכיב אב משותף.

תמיכה בדפדפנים ושיטות חלופיות

לכן, בדרך כלל נדרשת אסטרטגיית גיבוי.

בדפדפנים שלא תומכים באנימציות מבוססות-גלילה, אפשר להשתמש ב-fallback כדי ליצור מחדש את האפקטים החזותיים. החלופות בדרך כלל נוצרות באמצעות מאזין גלילה (לאפקטים של ScrollTimeline) או IntersectionObserver API (לאפקטים של ViewTimeline).

בדפדפנים עם תמיכה מובנית באנימציות מבוססות-גלילה, תמיד צריך להשתמש בהטמעה המקורית של CSS כי היא יעילה יותר.

חשוב לזכור שלא כל אפקט אפשר ליצור מחדש באמצעות גישת החלופות.

במקרה הספציפי הזה, הסקריפט הבא מחיל את החלופה לדפדפנים שלא תומכים באנימציות מבוססות-גלילה. הוא משתמש ב-Web Animations API‏ (Element.animate()) כדי ליצור אנימציה מושהית לכל פריט בקרוסלה. לאחר מכן, הוא מאזין לאירוע scroll ברכיב הגלילה ומעדכן את currentTime של כל אנימציה על סמך התקדמות הגלילה של הפריט בתוך רכיב הגלילה.

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

  // Create a map to store animations
  const animations = new Map();

  entries.forEach(entry => {
    const animation = entry.animate(
      {
        scale: ['0.5', '1', '0.5']
      },
      {
        duration: 1, // We'll control the time ourselves
        fill: 'both'
      }
    );
    animation.pause();
    animations.set(entry, animation);
  });

  // Update animations on scroll
  const tick = () => {
    const scrollerRect = scroller.getBoundingClientRect();

    entries.forEach(entry => {
      const animation = animations.get(entry);
      if (!animation) return;

      const entryRect = entry.getBoundingClientRect();
      const progress = (entryRect.left + entryRect.width / 2 - scrollerRect.left) / scrollerRect.width;

      animation.currentTime = progress;
    });
  };
    
  scroller.addEventListener('scroll', tick);
  tick();
}