רקע פריימים בגלילה

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

רכיב · גלילה · קנבס · בלי ספריות

<!-- רקע פריימים לפי גלילה — להדביק לפני </body> -->
<div id="klix-scrub" aria-hidden="true">
  <canvas></canvas>
  <span class="scrim"></span>
</div>

<style>
  #klix-scrub{position:fixed;inset:0;z-index:-10}
  #klix-scrub canvas{width:100%;height:100%;display:block}
  /* חושך מדורג לקריאות הטקסט שמעל */
  #klix-scrub .scrim{position:absolute;inset:0;
    background:linear-gradient(180deg,rgba(2,6,23,.7),rgba(2,6,23,.35) 45%,rgba(2,6,23,.75))}
</style>

<script>
(function () {
  // ═══ ערכו כאן ═══
  var FRAME_COUNT = 96;
  var POSTER = '/frames/poster.webp';
  function frameUrl(i) {
    return '/frames/hero_' + String(i + 1).padStart(4, '0') + '.webp';
  }
  // ═════════════════

  var wrap = document.getElementById('klix-scrub');
  if (!wrap) return;
  var canvas = wrap.querySelector('canvas');
  var ctx = canvas.getContext('2d');

  var images = new Array(FRAME_COUNT);
  var rendered = -1, current = 0, target = 0, raf = 0, animating = false, disposed = false;

  // הגלילה היא תנועה של המשתמש עצמו, ולכן ה-scrub נשאר פעיל גם במצב תנועה
  // מופחתת. רק ההחלקה מבוטלת, כדי שהתמונה תיצמד ישירות לגלגלת.
  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  var poster = new Image();
  poster.src = POSTER;
  poster.onload = function () { if (rendered < 0) drawCover(poster); };

  var dpr = Math.min(window.devicePixelRatio || 1, 2);

  function resize() {
    canvas.width = Math.round(window.innerWidth * dpr);
    canvas.height = Math.round(window.innerHeight * dpr);
    rendered = -2;
  }

  function drawCover(img) {
    var cw = canvas.width, ch = canvas.height;
    var scale = Math.max(cw / img.naturalWidth, ch / img.naturalHeight);
    var w = img.naturalWidth * scale, h = img.naturalHeight * scale;
    ctx.drawImage(img, (cw - w) / 2, (ch - h) / 2, w, h);
  }

  // הפריים הטעון הקרוב ביותר לאינדקס המבוקש, כדי שלא יהיו הבהובים
  // בזמן שהשאר עוד בדרך
  function nearestLoaded(idx) {
    for (var d = 0; d < FRAME_COUNT; d++) {
      var before = images[idx - d];
      if (before && before.complete && before.naturalWidth) return before;
      var after = images[idx + d];
      if (after && after.complete && after.naturalWidth) return after;
    }
    return poster.complete && poster.naturalWidth ? poster : null;
  }

  // ה-rAF רץ רק כשיש מה לצייר, ונרגע כשהפריים מתייצב — חוסך סוללה
  function tick() {
    if (disposed) return;
    current = reduced ? target : current + (target - current) * 0.16;
    if (Math.abs(target - current) < 0.02) current = target;
    var idx = Math.round(current);
    if (idx !== rendered) {
      var img = nearestLoaded(idx);
      if (img) { drawCover(img); rendered = idx; }
    }
    if (current === target && rendered === idx) { animating = false; return; }
    raf = requestAnimationFrame(tick);
  }

  function ensureTick() {
    if (!animating && !disposed) { animating = true; raf = requestAnimationFrame(tick); }
  }

  function readScroll() {
    var max = document.documentElement.scrollHeight - window.innerHeight;
    var p = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
    target = p * (FRAME_COUNT - 1);
    ensureTick();
  }

  function load(i) {
    return new Promise(function (resolve) {
      var img = new Image();
      img.onload = function () {
        images[i] = img;
        if (Math.round(current) === i || rendered < 0) { rendered = -2; ensureTick(); }
        resolve();
      };
      img.onerror = function () { resolve(); };
      img.src = frameUrl(i);
    });
  }

  // טעינה דו-שלבית: קודם כל פריים שמיני, כדי שהרקע יגיב תוך שניות,
  // ואז השאר בקבוצות כדי לא לחנוק את הרשת
  function loadAll() {
    var coarse = [];
    for (var i = 0; i < FRAME_COUNT; i += 8) coarse.push(i);
    coarse.push(FRAME_COUNT - 1);
    Promise.all(coarse.map(load)).then(function () {
      var rest = [];
      for (var j = 0; j < FRAME_COUNT; j++) if (!images[j]) rest.push(j);
      (function next(c) {
        if (c >= rest.length || disposed) return;
        Promise.all(rest.slice(c, c + 12).map(load)).then(function () { next(c + 12); });
      })(0);
    });
  }

  resize();
  readScroll();
  current = target;
  window.addEventListener('resize', function () { resize(); ensureTick(); });
  window.addEventListener('scroll', readScroll, { passive: true });
  ensureTick();
  loadAll();
})();
</script>