→ לכל המאגר

G40 · סצנה אופקית שכל אלמנט בה מתעורר בתורו

GSAP GSAP · ScrollTrigger · RTL תהליך ושלביםכרטיסיםניווט ומבנה ממתין

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

מתי משתמשים: סיפור אופקי עם תחנות: מסע לקוח, ציר זמן, שלבי תהליך. זה מה שהופך גלילה צידית מרצועה שזזה לסצנה שמתרחשת. ההבדל מ-MV:g01 הוא בדיוק זה.

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

הדמו

גלול למטה, הדמו מגיע ↓

ארבע תחנות, גלילה אחת

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

1
תחנה 01

הכירות

מבינים את המצב הקיים ואת מה שצריך לקרות.

2
תחנה 02

אפיון

מגדירים מבנה, תוכן והיררכיה לפני שנוגעים בעיצוב.

3
תחנה 03

בנייה

מרכיבים את העמוד ובודקים אותו בכל רוחב מסך.

4
תחנה 04

עלייה לאוויר

מפרסמים, מודדים ומתקנים לפי מה שקורה באמת.

עוד מסלול גלילה. נסה גם חזרה למעלה ↑
כיוון: הרצועה נשארת RTL, התחנה הראשונה בימין, ו-x חיובי. המצלמה נעה שמאלה בתוך התוכן וכל תחנה נכנסת דרך שפת המסך השמאלית, ככיוון הקריאה בעברית. באתר אנגלי הופכים את הסימן ומודדים את השפה השמאלית של הפריט במקום הימנית.
למה לא containerAnimation: זו הדרך הרשמית לתלות אנימציות במיקום אופקי, אבל היא מודדת נכון רק כשהרצועה מתחילה בקצה השמאלי. בתוך קונטיינר RTL ילד עם width:max-content שרחב מהמסך מתיישר לקצה הימני והגלישה יוצאת שמאלה, כלומר נקודת האפס שלו היא כבר סוף הרצועה, וכל המדידות יוצאות מוזזות באורך מסלול שלם. זו התנהגות פריסה של הדפדפן ולא באג ב-GSAP. לכן כאן כל פריט מקבל את מצבו מהמיקום החי שלו על המסך, שיטה שעובדת בשני הכיוונים ולא תלויה בפרשנות של left ו-right.

קוד להדבקה

הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --accent או --line משלו יורש אותם אוטומטית. הרכיב הזה מסתמך על dir="rtl".

פתח קובץ עצמאי ↗
הצג את הקוד כאן

CSS

/* מחלקות עזר של המאגר שהרכיב הזה משתמש בהן */
.ph{display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;border-radius:var(--r, 14px)}
.ph-a{background:linear-gradient(160deg,#3b5bdb,#748ffc)}
.ph-b{background:linear-gradient(160deg,#5f3dc4,#9775fa)}
.ph-c{background:linear-gradient(160deg,#0b7285,#3bc9db)}
.ph-d{background:linear-gradient(160deg,#2b8a3e,#69db7c)}
.ph-l{background:rgba(16,18,43,.72);color:#fff;padding:.18em .7em;border-radius:999px;line-height:1.3;font-size:max(.78em,12px)}

/* G40 · סצנה אופקית שכל אלמנט בה מתעורר בתורו */
html,body{overflow-x:clip}
.hz{overflow:hidden;background:#0f1020;color:#fff}
.hz-track{display:flex;align-items:center;gap:clamp(48px,11vw,220px);width:max-content;padding-inline:24vw 10vw;height:100vh}
.hz-item{flex:0 0 auto;text-align:center;will-change:transform}
.hz-num{font-size:13px;letter-spacing:.16em;color:#8d8fb0;margin-bottom:14px}
.hz-item h3{font-size:clamp(26px,2.6vw,46px);margin:0 0 10px;font-weight:800}
.hz-item p{margin:0 auto;max-width:32ch;color:#b9bad0;font-size:15px;line-height:1.6}
.hz-card{width:min(420px,74vw);aspect-ratio:4/3;margin-inline:auto;margin-bottom:22px;font-size:26px}
.hz-scrub{width:120px;height:120px;flex:0 0 auto;border-radius:26px;background:linear-gradient(140deg,#c6ff4a,var(--accent, #4a3aff));display:grid;place-items:center;font-size:34px;color:#0f1020}
.hz-lead{flex:0 0 min(760px,82vw)}
.hz-lead h2{font-size:clamp(34px,4vw,72px);margin:0 0 12px;font-weight:800;line-height:1.1}
.hz-lead p{color:#b9bad0;margin:0;font-size:16px}

HTML

<div class="hz"><div class="hz-track">
  <div class="hz-lead"><h2>ארבע תחנות, גלילה אחת</h2><p>גלול למטה. המסלול זז הצידה, וכל תחנה נכנסת ברגע שהיא מגיעה לאזור הצפייה.</p></div>
  <div class="hz-item"><div class="hz-card ph ph-a"><span class="ph-l">1</span></div><div class="hz-num">תחנה 01</div><h3>הכירות</h3><p>מבינים את המצב הקיים ואת מה שצריך לקרות.</p></div>
  <div class="hz-item"><div class="hz-card ph ph-b"><span class="ph-l">2</span></div><div class="hz-num">תחנה 02</div><h3>אפיון</h3><p>מגדירים מבנה, תוכן והיררכיה לפני שנוגעים בעיצוב.</p></div>
  <div class="hz-scrub" aria-hidden="true">✦</div>
  <div class="hz-item"><div class="hz-card ph ph-c"><span class="ph-l">3</span></div><div class="hz-num">תחנה 03</div><h3>בנייה</h3><p>מרכיבים את העמוד ובודקים אותו בכל רוחב מסך.</p></div>
  <div class="hz-item"><div class="hz-card ph ph-d"><span class="ph-l">4</span></div><div class="hz-num">תחנה 04</div><h3>עלייה לאוויר</h3><p>מפרסמים, מודדים ומתקנים לפי מה שקורה באמת.</p></div>
</div></div>

JS

gsap.registerPlugin(ScrollTrigger);

(function(){
  const track=document.querySelector(".hz-track");
  const items=gsap.utils.toArray(".hz-item");
  const spinner=document.querySelector(".hz-scrub");
  const dist=()=>Math.max(1,track.scrollWidth-window.innerWidth);
  const clamp=gsap.utils.clamp(0,1);

  // הרצועה יושבת ב-RTL: הפריט הראשון בימין, והגלישה יוצאת שמאלה.
  // לכן x חיובי, והמצלמה נעה שמאלה בתוך התוכן, ככיוון הקריאה בעברית.
  let origin=0; // המיקום של שפת הרצועה על המסך כשהיא בנקודת ההתחלה

  function measure(){
    origin=track.getBoundingClientRect().left-(gsap.getProperty(track,"x")||0);
  }

  // כל פריט מקבל את המצב שלו מהמיקום שלו על המסך ברגע הנתון.
  // בעברית הפריט נכנס דרך שפת המסך השמאלית, ולכן מודדים את השפה הימנית שלו.
  function paint(progress){
    const x=dist()*progress;
    gsap.set(track,{x:x});
    const vw=window.innerWidth;
    items.forEach(item=>{
      // כמה מהפריט כבר חצה את שפת המסך השמאלית. אחד = הוא כולו בפנים.
      const right=origin+item.offsetLeft+item.offsetWidth+x;
      const f=clamp(right/item.offsetWidth);
      gsap.set(item,{autoAlpha:f,y:70*(1-f)});
    });
    const sRight=origin+spinner.offsetLeft+spinner.offsetWidth+x;
    gsap.set(spinner,{rotation:270*clamp(sRight/(vw+spinner.offsetWidth))});
  }

  ScrollTrigger.create({
    trigger:track,pin:track.parentNode,start:"top top",end:()=>"+="+dist(),
    anticipatePin:1,invalidateOnRefresh:true,
    onRefresh:self=>{measure();paint(self.progress);},
    onUpdate:self=>paint(self.progress)
  });
  measure();paint(0);
})();