דילוג לתוכן הראשי
[attribute]While scrollingsticky-basedconceptירידת דור במובייל

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

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

Puzzle on Scroll

הרעיון: המצב בלי JS הוא המצב הסופי של העימוד. בונים את הסצנה כסקשן רגיל, כאן גריד של שלוש עמודות, וזה מה שמוצג בכל רוחב. כל חלק אומר רק מאיפה הוא מגיע (data-from) ומתי (data-at), ו-GSAP מזיז אותו לשם בזמן שהמסילה נגללת. ככה האפקט גם מגניב וגם רספונסיבי מעצמו: בטלפון הגריד הוא עדיין שלוש עמודות, רק קטן יותר, ולכן המסילה רצה כאן גם שם (data-puzzle-mobile; בלעדיו, מתחת ל-768 החלקים פשוט יושבים במקום).

זה לא עימוד מוכן אלא דרך לחשוב על סצנות: שלושה טלפונים (הדוגמה כאן), תפוז בארבעה פלחים שמתחברים, מחשב וטאבלט וטלפון, חלקי מוצר בתצוגה מפוצצת, אותיות של לוגו, חפיסת כרטיסים שנפרשת. לכל סצנה בונים קודם את המצב הסופי ב-CSS, ורק אז מוסיפים מאיפה כל חלק מגיע. הפאנל שומר אזור בטוח מעל ומתחת (--uk-puzzle-safe, 40px) והוא size container: סצנה שחייבת להיגמר בתוך הפולד, גם עם ניווט דביק, קובעת את הרוחב שלה גם לפי הגובה שלו (cqh; הטלפונים: width: min(…, 125cqh - 120px)). סקריפט: es-scripts/GSAP/puzzle_on_scroll.js.

<section data-puzzle-track="" style={{ '--uk-puzzle-distance': '150svh' }}>   {/* the track */}
  <div data-puzzle-pane="">                                                      {/* sticky while it plays */}
    <h2 data-puzzle-piece="" data-from="opacity:0 y:40px" data-at="0.45">…</h2>
    <div className="grid *:min-w-0 gap-(--uk-grid-gutter) max-uk-s:gap-[var(--uk-grid-gutter-mobile,20px)] grid-cols-3">                                           {/* the FINAL state: plain CSS */}
      <div data-puzzle-piece="" data-from="x:100% opacity:0" data-at="0.4">…</div>
      <div data-puzzle-piece="" data-from="scale:7.2 opacity:0">…</div>
      <div data-puzzle-piece="" data-from="x:-100% opacity:0" data-at="0.4">…</div>
    </div>
  </div>
</section>

data-from   x / y (% of the piece, or px), scale, rotate, opacity, any other GSAP key (no space inside a value)
data-to     the same values, the other way: starts in the layout, the scroll takes it apart
data-at     0..1 on the track (default 0)     data-for   0..1 (default: to the end)
            at + for = where it lands (0.6 + 0.3 = 90% of the track), past 1 it is cut at the end
data-puzzle-mobile   on the track: play under 768px too (default: the pieces sit in place there)
--uk-puzzle-safe     the pane's safe zone above and below (40px); size the scene in cqh to end inside the fold

איך זה בנוי מבפנים: How it works.

האפליקציה, בשלושה מסכים

מלון בוטיק
מסעדה
ספא

דוגמה שנייה: תמונה אחת שמתפרקת לחלקים

אותו מנגנון, בלי אף שורת קוד נוספת: המצב הסופי הוא תמונה אחת רגילה של אופנוע. כל חלק הוא עותק של אותה תמונה, חתוך ב-clip-path לאזור שלו (גלגל, מנוע, מיכל ומושב, מזלג), וכולם יושבים באותה קופסה. כך הם נוחתים בדיוק אחד על השני בכל רוחב, בלי ליישר כלום ביד, ורק ה-data-from אומר מאיפה כל אחד מגיע. הגישה הזו מתאימה לכל מוצר שיש לו תמונת צד נקייה: אופנוע, מכונית, נעל, אופניים.

<div className="pz-bike">                                     {/* one box, the picture's ratio */}
  <img src={bike} className="pz-bike__part pz-bike__part--rear"   data-puzzle-piece="" data-from="x:-60% y:30% rotate:-25" data-at="0" />
  <img src={bike} className="pz-bike__part pz-bike__part--front"  data-puzzle-piece="" data-from="x:60% y:30% rotate:25"   data-at="0.05" />
  <img src={bike} className="pz-bike__part pz-bike__part--engine" data-puzzle-piece="" data-from="y:80% opacity:0"          data-at="0.2" />
  <img src={bike} className="pz-bike__part pz-bike__part--tank"   data-puzzle-piece="" data-from="y:-70% opacity:0"         data-at="0.3" />
  <img src={bike} className="pz-bike__part pz-bike__part--fork"   data-puzzle-piece="" data-from="x:40% y:-50% opacity:0"   data-at="0.4" />
</div>

/* every part: the same picture in the same box, clipped to its region */
.pz-bike__part { position: absolute; inset: 0; width: 100%; height: 100%; }
.pz-bike__part--rear  { clip-path: polygon(0 40%, 36% 40%, 36% 100%, 0 100%); }
.pz-bike__part--tank  { clip-path: polygon(14% 0, 72% 0, 72% 42%, 14% 42%); }
…

חלק אחרי חלק

דוגמה שלישית: חפצים נפרדים בגריד

הסוג השלישי: לא גריד של כרטיסים ולא תמונה אחת שנחתכת, אלא כמה חפצים שכל אחד תמונה משלו (flat-lay על לבן), והמצב הסופי הוא גריד CSS רגיל שמסדר אותם על שולחן. המחשב יורד מלמעלה, המחברת מגיעה באלכסון, הספל מתגלגל מהצד, הצמח צומח מלמטה. בטלפון הגריד הוא שתי עמודות, אז הסצנה פשוט מסתדרת אחרת ואותם data-from עדיין נכונים. הרקע הלבן של כל חפץ נעלם על המסילה הלבנה עם mix-blend-mode: multiply.

שולחן העבודה שלכם, מסודר

דוגמה רביעית: המילים הן החלקים

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

אתרשנבנהמילהאחרימילה.

כל מילה מגיעה מכיוון אחר, ונוחתת בשורה.

דוגמה חמישית: חפיסה שנפרשת

הפעם החלקים לא מגיעים מבחוץ אלא מאותה נקודה. המצב הסופי הוא שורה של חמש תמונות מודפסות, כל אחת מוטה קצת, על קשת. כל אחת מתחילה באמצע עם ההטיה שלה מבוטלת, אז הסצנה נפתחת כחפיסה סגורה אחת ורואים רק את התמונה העליונה. ה-x של כל כרטיס הוא כמה צעדים הוא רחוק מהאמצע (צעד הוא 86% מרוחב הכרטיס, כי הכרטיסים חופפים 7% מכל צד), וה-rotate שלו הוא ההטיה הפוכה. ההטיה והקשת יושבות על כרטיס פנימי ב-CSS (rotate ו-translate כתכונות נפרדות), ו-GSAP מזיז רק את העטיפה, כך שהשניים לא דורכים אחד על השני. מתאים לתיק עבודות של צלם, לחדרים של מלון, למנות בתפריט.

<div className="pz-deck">                                   {/* a row, 5 slots, 7% overlap each side */}
  <div className="pz-deck__slot" data-puzzle-piece="" data-from="x:172% y:-8% rotate:12">     {/* 2 steps right, tilt undone */}
    <figure className="pz-deck__card">…</figure>             {/* the tilt and the arc: rotate:-12deg, translate:0 8% */}
  </div>
  <div className="pz-deck__slot" data-puzzle-piece="" data-from="x:86% y:-2% rotate:6">…</div>
  <div className="pz-deck__slot" data-puzzle-piece="" data-from="scale:1.06">…</div>         {/* the top card */}
  <div className="pz-deck__slot" data-puzzle-piece="" data-from="x:-86% y:-2% rotate:-6">…</div>
  <div className="pz-deck__slot" data-puzzle-piece="" data-from="x:-172% y:-8% rotate:-12">…</div>
</div>
זוג על שפת המכתש
מכתש רמון
זוג בין שורות הגפנים
כרם בגליל
זוג בצללית מול השקיעה
שקיעה
שתי טבעות על ענף זית
הטבעות
זוג הולך על קו המים
חוף הים

חמש תמונות, ערב אחד

דוגמה שישית: תמונה שמתחברת ממשבצות

הפאזל במובן המילולי: תמונה אחת, גריד של 8 על 5 משבצות, וכל משבצת היא אותה תמונה כרקע, מוזזת לפרוסה שלה (background-size: 800% 500%, וה-position לפי העמודה והשורה). בניגוד לאופנוע, לא קוראים אזורים מהתמונה ולא חותכים ביד: הגריד חותך את עצמו, אז זה עובד עם כל תמונה, גם תמונת ההירו של לקוח. כל משבצת מגיעה מהצד שלה של האמצע, וככל שהיא נוחתת רחוק יותר ממנו כך היא מגיעה מרחוק יותר, מסובבת ובגודל אחר, בזמן קצת אחר. המספרים פסאודו-אקראיים אבל קבועים (mulberry32, חשבון שלמים), כך שהשרת והדפדפן מקבלים את אותם ערכים. בסוף התמונה המקורית נכנסת מתחת למשבצות, כדי שלא יישאר תפר דק ביניהן.

<div className="pz-tiles">                                   {/* grid: repeat(8, 1fr), the picture's ratio */}
  <img className="pz-tiles__whole" src={pic} data-puzzle-piece="" data-from="opacity:0" data-at="0.82" data-for="0.08" />
  {TILES.map((t) => (                                       {/* 40 cells, the same picture */}
    <div className="pz-tiles__t" style={{ '--c': t.c, '--r': t.r }}
         data-puzzle-piece="" data-from={t.from} data-at={t.at} data-for="0.45" />
  ))}
</div>

.pz-tiles__t {
  background-image: url(pic);
  background-size: 800% 500%;                                /* the picture, 8 x 5 cells big */
  background-position: calc(var(--c) * 100% / 7) calc(var(--r) * 100% / 4);
}

גל אחד, ארבעים חלקים

גולש על גל מול קו הרקיע של תל אביב

דוגמה שביעית: אתר שנבנה בלוק אחרי בלוק

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

<div className="pz-site" data-puzzle-piece="" data-from="scale:0.9">               {/* the frame: on screen, empty, from the start */}
  <nav  data-puzzle-piece="" data-from="y:-250% opacity:0" data-at="0.12">…</nav>
  <h3   data-puzzle-piece="" data-from="x:80% opacity:0"   data-at="0.2">…</h3>    {/* the copy from its side */}
  <img  data-puzzle-piece="" data-from="x:-70% rotate:-8 opacity:0" data-at="0.18" /> {/* the picture from the other */}
  <figure data-puzzle-piece="" data-from="y:120% rotate:-6 opacity:0" data-at="0.4">…</figure>   {/* cards, one by one */}
  <footer data-puzzle-piece="" data-from="y:200% opacity:0" data-at="0.6">…</footer>
</div>

.pz-site { container-type: inline-size; }       /* everything inside in cqw: it scales like a picture */

רגעים שנשארים

צילום חתונות ואירועים, בלי להעמיד אף אחד.

לתיאום פגישה
מכתש רמון
כרם בגליל
חוף הים
אור · צילום077-4444-4444

ככה נבנה אתר

דוגמה שמינית: עומק, תמונות שעפות אליך

אותו פאזל על הציר השלישי. x הוא ימינה ושמאלה, y למעלה ולמטה, ו-z הוא אליך והלאה ממך. עם perspective על הסצנה (מצלמה במרחק), תמונה שמתקרבת גם גדלה וגם זזה החוצה מהמרכז, ולכן נראה שעוברים דרך המרחב, לא שהיא סתם גדלה כמו ב-data-scale. לכל תמונה z משלה, והן מסביב לקצוות, אז כשהן גדלות הן זזות החוצה ופונות את המרכז. הן לא עוברות את המצלמה: עד בערך פי 2.5, כדי שיישארו חדות, גם הכותרת עפה באותו כיוון: מגיעה מהעומק, עוברת אותך ונעלמת. ומתחתיה מסר נוסף: הסקשן הבא, על אותו שחור, עולה מעל הסצנה בזמן שהתמונות עוד עפות (One Ground), אז אין פריים של מסך שחור שנעצר. מסלול של מסך גלילה אחד. מסך שנעצר, אז לאתר שבנוי לזה: תיק עבודות.

<section data-puzzle-track="" className="pz-track-depth">       {/* --uk-puzzle-distance: 100svh, one screen as the pen */}
  <div data-puzzle-pane="">
    <div className="pz-depth"><div className="pz-depth__layer">       {/* perspective: 1000px */}
      <img data-puzzle-piece="" data-to="z:400" data-at="0" data-for="1" … />
      <img data-puzzle-piece="" data-to="z:600" data-at="0" data-for="1" … />   {/* comes closest, drawn on top */}
    </div></div>
  </div>
</section>
<section className="pz-depth-after">…</section>                 {/* same black, margin-top: -60vh: rises over the end */}

כל רגע, מקרוב

צילום חתונות ואירועים

מהטקס ועד הריקוד האחרון, בלי להעמיד אף אחד.

סוף הדמו. הסקשן הבא מתחיל כאן.