→ לכל המאגר

G49 · וידאו שמתקדם עם הגלילה

GSAP GSAP · ScrollTrigger · currentTime תמונות וגלריותתהליך ושלביםהירו ופתיחה ממתין

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

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

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

הדמו

הגלילה היא הנגןגלול למטה ולמעלה. הסרטון עוקב.

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

הטכניקה עצמה קצרה, וכל ההצלחה תלויה בקובץ. **הכלל**: מקודדים את הסרטון עם כל פריים כ-keyframe (`-g 1 -keyint_min 1 -sc_threshold 0`), אחרת כל קפיצה בזמן מחייבת את הדפדפן לפענח מהמפתח הקודם והתנועה נתקעת. הסרטון כאן נוצר בדיוק כך, ולכן הוא כבד יחסית לאורכו. שני דברים נוספים: לא מציבים `currentTime` ישירות מה-onUpdate אלא מתקרבים אליו ב-ticker, אחרת רואים קפיצות; ובאייפון אין הרשאה לקפוץ בזמן עד שהווידאו התנגן פעם אחת, ומכאן ה-play שמיד עוצר. `muted` ו-`playsinline` הם תנאי סף. **ומלכודת שנתפסה כאן באימות**: השרת חייב לתמוך ב-Range requests (תשובה 206). בלעדיה הדפדפן לא יכול לדלג בזמן, `seeking` נתקע על true והסרטון קפוא על הפריים הראשון בלי שום הודעת שגיאה. GitHub Pages ורוב האחסונים תומכים, אבל שווה לבדוק אחרי העלאה לשרת של לקוח.

קוד להדבקה

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

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

CSS

/* G49 · וידאו שמתקדם עם הגלילה */
.vs{position:relative;height:320vh}
.vs-stick{position:sticky;top:0;height:100vh;display:grid;place-items:center;overflow:hidden;background:#0e0f16}
.vs-video{width:min(92vw,960px);border-radius:var(--r, 14px);display:block}
.vs-cap{position:absolute;inset-inline:0;bottom:11vh;text-align:center;color:#fff;pointer-events:none}
.vs-cap b{display:block;font-size:clamp(22px,3.4vw,42px);margin-bottom:6px}
.vs-cap span{font-size:15px;opacity:.75}
/* מוטבע ומעוגל. רצועה בגובה 4 פיקסלים שנמתחת מקצה לקצה המסך נקראת כקו מפריד תועה
   ולא כמד התקדמות, במיוחד כשהיא ריקה בתחילת הגלילה. */
.vs-bar{position:absolute;inset-inline:clamp(16px,6vw,80px);bottom:clamp(14px,2.5vh,26px);
  height:5px;border-radius:999px;overflow:hidden;background:rgba(255,255,255,.14)}
.vs-bar i{display:block;height:100%;width:0;border-radius:999px;background:var(--accent, #4a3aff)}
.vs-after{padding:14vh var(--gutter, clamp(24px, 5vw, 96px));max-width:min(680px,92vw);margin-inline:auto;text-align:center;color:var(--muted, #6a6d85);font-size:17px;line-height:1.9}

HTML

<div class="vs">
  <div class="vs-stick">
    <video class="vs-video" src="https://liavwebdesign-spec.github.io/motion-vault/assets/media/scrub.mp4" poster="https://liavwebdesign-spec.github.io/motion-vault/assets/media/scrub.jpg"
           muted playsinline preload="auto" aria-label="הדגמה: וידאו שמתקדם עם הגלילה"></video>
    <div class="vs-cap"><b>הגלילה היא הנגן</b><span>גלול למטה ולמעלה. הסרטון עוקב.</span></div>
    <div class="vs-bar"><i></i></div>
  </div>
</div>
<p class="vs-after">בסרטון אמיתי זה יהיה מוצר שמסתובב, מכונה שעובדת או בית שנבנה. הקובץ חייב להיות מקודד לחיפוש מהיר, אחרת התנועה מקרטעת.</p>

JS

gsap.registerPlugin(ScrollTrigger);

(function(){
  const v=document.querySelector(".vs-video"),bar=document.querySelector(".vs-bar i");
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  let target=0,current=0,dur=0,ready=false;
  function start(){
    dur=v.duration||0; if(!dur)return;
    ready=true;
    ScrollTrigger.create({
      trigger:".vs",start:"top top",end:"bottom bottom",scrub:true,
      onUpdate:self=>{target=self.progress*dur;bar.style.width=(self.progress*100).toFixed(1)+"%";}
    });
    ScrollTrigger.refresh();
  }
  v.addEventListener("loadedmetadata",start);
  if(v.readyState>=1)start();
  // iOS לא מרשה קפיצה בזמן לפני שהווידאו התנגן פעם אחת, ולכן דוחפים play ומיד pause
  const kick=()=>{const p=v.play();if(p&&p.then)p.then(()=>v.pause()).catch(()=>{});};
  addEventListener("touchstart",kick,{once:true,passive:true});
  addEventListener("click",kick,{once:true});
  // החלקה: מתקרבים אל היעד בהדרגה במקום להציב currentTime בכל אירוע גלילה
  gsap.ticker.add(()=>{
    if(!ready)return;
    if(reduce){v.currentTime=target;return;}
    current+=(target-current)*.18;
    if(Math.abs(target-current)<.004)current=target;
    if(v.seeking)return;
    v.currentTime=current;
  });
})();