→ לכל המאגר

G54 · גריד מפוזר שמתכנס בגלילה

GSAP GSAP · ScrollTrigger scrub תמונות וגלריותכרטיסיםהירו ופתיחה ממתין

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

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

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

הדמו

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

קיר העבודות

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

עוד מסלול גלילה. נסה גם חזרה למעלה ↑
ההבדל בין פיזור שנראה מכוון לפיזור שנראה כמו תקלה הוא שהמרחק לא אקראי: כל כרטיס מגיע מהכיוון שאליו הוא ממילא שייך בגריד, וזה מחושב מהמיקום שלו ביחס למרכז. לכן העין קוראת את זה כהתכנסות ולא כבלגן. הערכים ביחידות אחוז (`xPercent`) ולא בפיקסלים, כך שהאפקט מתכווץ מעצמו במסכים קטנים. `rotateY` קטן עם `perspective` על הגריד מוסיף עומק בלי להפוך את זה לאטרקציה. חובה לחשב מחדש בשינוי רוחב, כי מספר העמודות משתנה וכל הכיוונים איתו.

קוד להדבקה

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

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

CSS

/* מחלקות עזר של המאגר שהרכיב הזה משתמש בהן */
.stage{padding:var(--sec, clamp(120px, 12vw, 260px)) var(--gutter, clamp(24px, 5vw, 96px));position:relative}
.stage.tight{padding-block:clamp(48px,5vw,90px)}
.stage.full{padding-inline:0}
.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-e{background:linear-gradient(160deg,#e8590c,#ffa94d)}
.ph-f{background:linear-gradient(160deg,#c92a2a,#ff8787)}

/* G54 · גריד מפוזר שמתכנס בגלילה */
.cv-head{text-align:center;max-width:44ch;margin:0 auto clamp(30px,5vw,60px)}
.cv-head h3{font-size:clamp(26px,3.6vw,48px);margin:0 0 10px}
.cv-head p{color:var(--muted, #6a6d85);font-size:17px;line-height:1.8;margin:0}
.cv{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(10px,1.4vw,22px);
  max-width:min(1080px,94vw);margin-inline:auto;perspective:1200px}
/* align-self:start קריטי. בלעדיו פריט הגריד נמתח בשני הצירים, aspect-ratio גוזר את
   הרוחב מגובה השורה במקום להפך, והעמודות יוצאות ברוחבים שונים. אז הפיזור שמחושב
   באחוזים מרוחב הפריט יוצא לא סימטרי והענן נסחף הצידה מול הכותרת. */
.cv-item{aspect-ratio:1;align-self:start;border-radius:16px;font-size:0;will-change:transform,opacity}
.cv-item:nth-child(4n+1){aspect-ratio:3/4}
.cv-item:nth-child(4n+4){aspect-ratio:4/5}
@media(max-width:820px){.cv{grid-template-columns:repeat(3,1fr)}}
@media(max-width:540px){.cv{grid-template-columns:repeat(2,1fr)}}

HTML

<div class="stage"><div class="cv-head"><h3>קיר העבודות</h3>
  <p>גלול לאט. הכרטיסים מגיעים מהחלל ומתיישבים לגריד, ובגלילה אחורה חוזרים להתפזר.</p></div>
<div class="cv">
  <div class="ph cv-item ph-a"></div><div class="ph cv-item ph-c"></div><div class="ph cv-item ph-d"></div><div class="ph cv-item ph-e"></div>
  <div class="ph cv-item ph-b"></div><div class="ph cv-item ph-f"></div><div class="ph cv-item ph-a"></div><div class="ph cv-item ph-c"></div>
  <div class="ph cv-item ph-d"></div><div class="ph cv-item ph-e"></div><div class="ph cv-item ph-b"></div><div class="ph cv-item ph-f"></div>
</div></div>

JS

gsap.registerPlugin(ScrollTrigger);

(function(){
  const grid=document.querySelector(".cv");
  const items=gsap.utils.toArray(".cv-item");
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  if(reduce){gsap.set(items,{opacity:1});return;}
  let tl;
  function build(){
    if(tl){tl.scrollTrigger&&tl.scrollTrigger.kill();tl.kill();gsap.set(items,{clearProps:"all"});}
    const g=grid.getBoundingClientRect();
    const cx=g.left+g.width/2, cy=g.top+g.height/2;
    tl=gsap.timeline({scrollTrigger:{trigger:grid,start:"top 92%",end:"top 22%",scrub:.8,invalidateOnRefresh:true}});
    items.forEach(el=>{
      const r=el.getBoundingClientRect();
      // הכיוון נגזר מהמיקום בגריד: כל כרטיס בא מהצד שאליו הוא ממילא שייך
      const dx=(r.left+r.width/2-cx)/g.width;      // בין מינוס חצי לחצי
      const dy=(r.top+r.height/2-cy)/g.height;
      // duration מפורש. בלעדיו הטווין מקבל חצי שנייה, וכל ההתכנסות נגמרת בשליש הראשון של הגלילה.
      tl.fromTo(el,{
        xPercent:dx*190, yPercent:dy*150,
        scale:.42, rotate:dx*22, rotateY:dx*-26, opacity:0
      },{
        xPercent:0,yPercent:0,scale:1,rotate:0,rotateY:0,opacity:1,duration:1,ease:"power2.out"
      },0);
    });
  }
  build();
  // הפיזור מחושב ממידות אמיתיות, ולכן חייבים לחשב מחדש כשהגריד משנה מספר עמודות
  let t;addEventListener("resize",()=>{clearTimeout(t);t=setTimeout(()=>{build();ScrollTrigger.refresh();},220);});
})();