האפקט שגורם לדף להרגיש מופק. 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>