המראה שמבדיל אתר פרימיום מתבנית. הקנבס מרנדר ב-dpr אחד ונמתח ב-CSS, מה שחוסך פי ארבעה עבודה במסכי retina בלי הבדל נראה לעין. אם אין WebGL2, נשאר הגרדיאנט שמוגדר כרקע הקנבס. רקע שיידר הוא רכיב מתוך רכיבים של כליקס. מה שמורידים הוא רכיב שלם, גם כ-HTML עצמאי וגם כרכיב React. אין ספרייה חיצונית, אין שלב בנייה, ואם כליקס ייעלם מחר הקוד ימשיך לעבוד.
רכיב · WebGL · בלי ספריות · רקע
<!-- רקע שיידר — הקנבס נמתח על ההורה, שצריך להיות position:relative -->
<canvas id="klix-shader" aria-hidden="true"></canvas>
<style>
/* הגרדיאנט הזה הוא מה שנשאר אם אין WebGL2, ולכן הסקשן לא יוצא ריק */
#klix-shader{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;
background:linear-gradient(160deg,#0a1626,#050b14)}
</style>
<script>
(function () {
var canvas = document.getElementById('klix-shader');
if (!canvas) return;
// אפשר להחליף את השיידר בכל fragment shader שמשתמש
// ב-u_res, u_time ו-u_mouse
var FRAGMENT = "#version 300 es\nprecision highp float;\nout vec4 outColor;\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec2 u_mouse;\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\n\nfloat noise(vec2 p){\n vec2 i = floor(p); vec2 f = fract(p);\n float a = hash(i), b = hash(i + vec2(1.0, 0.0));\n float c = hash(i + vec2(0.0, 1.0)), d = hash(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;\n}\n\n// ארבע אוקטבות ולא שש. ההבדל הוויזואלי ברקע מטושטש זניח,\n// והחיסכון משמעותי כששיידר רץ על מסך מלא.\nfloat fbm(vec2 p){\n float v = 0.0; float amp = 0.5;\n for (int i = 0; i < 4; i++) { v += amp * noise(p); p *= 2.02; amp *= 0.5; }\n return v;\n}\n\nvoid main(){\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_res.xy) / u_res.y;\n float t = u_time * 0.05;\n\n vec2 m = u_mouse;\n float grip = 1.0 - smoothstep(0.0, 0.9, length(uv - m));\n vec2 push = m * 0.35 + (uv - m) * grip * 0.25;\n\n vec2 q = vec2(fbm(uv * 1.5 + push + t), fbm(uv * 1.5 - push - t + 3.7));\n vec2 r = vec2(fbm(uv * 2.0 + q + t * 1.3), fbm(uv * 2.0 + q - t * 1.1));\n float n = fbm(uv * 2.5 + r);\n\n vec3 deep = vec3(0.019, 0.043, 0.078);\n vec3 mid = vec3(0.106, 0.227, 0.419);\n vec3 hi = vec3(0.784, 0.643, 0.082);\n\n vec3 col = mix(deep, mid, smoothstep(0.15, 0.58, n));\n col = mix(col, hi, smoothstep(0.66, 0.98, n) * 0.7);\n col += hi * grip * 0.16;\n\n float v = smoothstep(1.25, 0.15, length(uv));\n col *= mix(0.45, 1.0, v);\n\n outColor = vec4(col, 1.0);\n}";
mountShader(canvas, FRAGMENT);
// מריץ fragment shader על מלבן שמכסה את הקנבס. מחזיר פונקציית עצירה,
// או null אם אין WebGL2 — ואז נשאר רקע ה-CSS שמוגדר על הקנבס.
function mountShader(canvas, fragment, extra, onDown) {
var gl = canvas.getContext('webgl2', { antialias: false, alpha: true });
if (!gl) return null; // אין WebGL2 — נשארים עם רקע ה-CSS
function compile(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error('shader compile failed:', gl.getShaderInfoLog(s));
return null;
}
return s;
}
var vs = compile('#version 300 es\nin vec2 a_pos;void main(){gl_Position=vec4(a_pos,0.0,1.0);}',
gl.VERTEX_SHADER);
var fs = compile(fragment, gl.FRAGMENT_SHADER);
if (!vs || !fs) return null;
var prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error('program link failed:', gl.getProgramInfoLog(prog));
return null;
}
gl.useProgram(prog);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
var loc = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
var cache = {};
function uloc(name) {
if (!(name in cache)) cache[name] = gl.getUniformLocation(prog, name);
return cache[name];
}
// שעון שמתקדם רק כשהתנועה דלוקה, ונבדק בכל פריים כדי שמתג יגיב מיד.
// הסמן נשאר חי תמיד, כי הוא מונע ישירות על ידי המשתמש.
var clock = 0;
var smooth = { x: 0, y: 0 };
return mountStage(canvas, {
// רקעים ערפיליים נראים זהים ב-dpr 1 ומתיחה ב-CSS, וזה חוסך
// פי ארבעה עבודה במסכים צפופים.
maxDpr: 1,
onResize: function (w, h) {
gl.viewport(0, 0, w, h);
gl.uniform2f(uloc('u_res'), w, h);
},
onPointerDown: onDown,
onFrame: function (f) {
if (motionEnabled()) clock += f.dt;
smooth.x += (f.pointer.nx - smooth.x) * 0.06;
smooth.y += (f.pointer.ny - smooth.y) * 0.06;
gl.uniform1f(uloc('u_time'), clock);
gl.uniform2f(uloc('u_mouse'), smooth.x, smooth.y);
if (extra) extra(f, gl, uloc);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
});
}
// תשתית הקנבס: מדידת גודל, השהיה מחוץ למסך, מעקב אחרי המצביע ושעון dt חסום
function mountStage(canvas, opts) {
var dpr = Math.min(window.devicePixelRatio || 1, opts.maxDpr || 2);
var pointer = { x: 0, y: 0, nx: 0, ny: 0, inside: false };
var w = 0, h = 0, onScreen = true, painted = false, running = true, raf = 0;
// ה-CSS לא בהכרח נטען כשהפונקציה רצה, ואז הקנבס נתקע בגודל 0.
// לכן מודדים בכל פריים ולא רק כשמשנים גודל חלון.
function syncSize() {
var cw = Math.round(canvas.clientWidth * dpr);
var ch = Math.round(canvas.clientHeight * dpr);
if (!cw || !ch || (canvas.width === cw && canvas.height === ch)) return false;
canvas.width = cw; canvas.height = ch; w = cw; h = ch;
if (opts.onResize) opts.onResize(cw, ch, dpr);
return true;
}
function readPointer(e) {
var r = canvas.getBoundingClientRect();
if (!r.width || !r.height) return;
pointer.x = (e.clientX - r.left) * dpr;
pointer.y = (e.clientY - r.top) * dpr;
pointer.nx = (e.clientX - r.left - r.width / 2) / r.height;
pointer.ny = (r.height / 2 - (e.clientY - r.top)) / r.height;
pointer.inside = e.clientX >= r.left && e.clientX <= r.right &&
e.clientY >= r.top && e.clientY <= r.bottom;
if (onScreen) painted = false; // קלט מהמשתמש תמיד מצדיק ציור מחדש
}
function onMove(e) { readPointer(e); }
function onDown(e) {
readPointer(e);
if (pointer.inside && opts.onPointerDown) opts.onPointerDown(pointer);
}
window.addEventListener('pointermove', onMove, { passive: true });
window.addEventListener('pointerdown', onDown, { passive: true });
// רץ רק כשהקנבס על המסך. בלי זה כמה אפקטים בדף אחד מתחרים על המעבד.
var io = new IntersectionObserver(function (entries) { onScreen = entries[0].isIntersecting; });
io.observe(canvas);
var last = performance.now(), start = last;
function loop() {
if (!running) return;
raf = requestAnimationFrame(loop);
if (syncSize()) painted = false;
if (!w || !h) return;
// מחוץ למסך: מציירים פריים אחד כדי שלא יישאר ריק, ואז נעצרים
var idle = document.hidden || !onScreen;
if (idle && painted) return;
var now = performance.now();
var dt = Math.min((now - last) / 1000, 1 / 20);
last = now;
opts.onFrame({ t: (now - start) / 1000, dt: dt, w: w, h: h, dpr: dpr, pointer: pointer });
painted = idle;
}
loop();
return function () {
running = false;
cancelAnimationFrame(raf);
io.disconnect();
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerdown', onDown);
};
}
// התנועה האוטומטית נתלית בתכונה על אלמנט השורש, כדי שמתג באתר יוכל לגבור
// על הגדרת מערכת ההפעלה. תנועה שהמשתמש מייצר בעצמו פועלת תמיד.
function motionEnabled() {
if (document.documentElement.dataset.motion) return document.documentElement.dataset.motion !== 'off';
return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
})();
</script>