תמונות שמפוזרות בחלל, מסובבות וקטנות, ומתכנסות לגריד מסודר בקצב הגלילה. גוללים אחורה והן מתפזרות בחזרה.
מתי משתמשים: פתיחת סקשן עבודות, קיר לקוחות, גלריית מוצרים, מעבר בין פרק לפרק. רגע אחד שאומר "יש כאן מישהו ששולט בפרטים", ואחריו התוכן מתנהג רגיל.
הדמו כאן עיצובי-ניטרלי בכוונה. כשהמהלך נכנס לפרויקט, מיובאת רק ההתנהגות: הצבעים, הרדיוסים, הפונטים והצללים יורשים את העיצוב של אותו פרויקט.
גלול לאט. הכרטיסים מגיעים מהחלל ומתיישבים לגריד, ובגלילה אחורה חוזרים להתפזר.
הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --accent או --line משלו יורש אותם אוטומטית. הרכיב הזה מסתמך על dir="rtl".
/* מחלקות עזר של המאגר שהרכיב הזה משתמש בהן */
.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)}}
<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>
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);});
})();