צורות מרחפות

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

אפקט קנבס חי · גיאומטרי · רקע · עדין · קנבס

<!-- צורות מרחפות — להדביק לפני </body> -->
<canvas id="klix-float-shapes" aria-hidden="true"></canvas>

<style>
  #klix-float-shapes { position: fixed; inset: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 60 }
</style>

<script>
(function () {
  var canvas = document.getElementById('klix-float-shapes');
  if (!canvas) return;

  var cfg = {
    count: 22,
    spawn: function (w, h, first, dpr) {
      return { x: Math.random() * w, y: first ? Math.random() * h : h + 30 * dpr,
               size: (8 + Math.random() * 16) * dpr, s: (14 + Math.random() * 26) * dpr,
               rot: Math.random() * 6.283, vr: (Math.random() - .5) * .8,
               sway: Math.random() * 6.283, a: .18 + Math.random() * .3,
               kind: (Math.random() * 3) | 0 };
    },
    step: function (p, dt) {
      p.y -= p.s * dt; p.rot += p.vr * dt; p.sway += dt * .9;
      p.x += Math.sin(p.sway) * 10 * dt;
      return p.y > -40;
    },
    draw: function (ctx, p, t, dpr) {
      ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot);
      ctx.strokeStyle = 'rgba(148,163,184,' + p.a + ')';
      ctx.lineWidth = 1.4 * dpr;
      ctx.beginPath();
      if (p.kind === 0) {
        ctx.arc(0, 0, p.size, 0, 6.283);
      } else if (p.kind === 1) {
        ctx.moveTo(-p.size, -p.size);
        ctx.lineTo(p.size, -p.size);
        ctx.lineTo(p.size, p.size);
        ctx.lineTo(-p.size, p.size);
        ctx.closePath();
      } else {
        ctx.moveTo(0, -p.size);
        ctx.lineTo(p.size * .87, p.size * .5);
        ctx.lineTo(-p.size * .87, p.size * .5);
        ctx.closePath();
      }
      ctx.stroke(); ctx.restore();
    }
  };

  klixParticles(canvas, cfg);

  // מנוע חלקיקים קטן. cfg: { count, spawn, step, draw, before, onDown, userDriven }
  function klixParticles(canvas, cfg) {
    var ctx = canvas.getContext('2d');
    if (!ctx) return function () {};
  
    var dpr = Math.min(window.devicePixelRatio || 1, cfg.maxDpr || 1.5);
    var ps = [], w = 0, h = 0, raf = 0, running = true, onScreen = true;
    var last = performance.now(), start = last;
    var pointer = { x: -9999, y: -9999, inside: false, dx: 0, dy: 0 };
  
    // התנועה האוטומטית נתלית בתכונה על אלמנט השורש, כדי שמתג באתר יוכל לגבור
    // על הגדרת מערכת ההפעלה. אפקט שהמשתמש מפעיל בעצמו ממשיך לרוץ תמיד.
    function motionEnabled() {
      if (document.documentElement.dataset.motion) return document.documentElement.dataset.motion !== 'off';
      return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    }
  
    function fill() {
      ps = [];
      for (var i = 0; i < cfg.count; i++) ps.push(cfg.spawn(w, h, true, dpr));
    }
  
    // ה-CSS לא בהכרח נטען כשהפונקציה רצה, ואז הקנבס נתקע בגודל 0.
    // לכן מודדים בכל פריים ולא רק בשינוי גודל חלון.
    function size() {
      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;
      fill();
      return true;
    }
  
    function readPointer(e) {
      var r = canvas.getBoundingClientRect();
      if (!r.width) return;
      var nx = (e.clientX - r.left) * dpr, ny = (e.clientY - r.top) * dpr;
      pointer.dx = nx - pointer.x; pointer.dy = ny - pointer.y;
      pointer.x = nx; pointer.y = ny;
      pointer.inside = e.clientX >= r.left && e.clientX <= r.right &&
                       e.clientY >= r.top && e.clientY <= r.bottom;
    }
  
    function onMove(e) { readPointer(e); }
    function onDown(e) {
      readPointer(e);
      if (pointer.inside && cfg.onDown) cfg.onDown(pointer, ps, w, h, dpr);
    }
    window.addEventListener('pointermove', onMove, { passive: true });
    window.addEventListener('pointerdown', onDown, { passive: true });
  
    // רץ רק כשהקנבס על המסך. בלי זה כמה אפקטים בדף אחד מתחרים על המעבד.
    var io = new IntersectionObserver(function (en) { onScreen = en[0].isIntersecting; });
    io.observe(canvas);
  
    function loop() {
      if (!running) return;
      raf = requestAnimationFrame(loop);
      size();
      if (!w || !h) return;
      if (document.hidden || !onScreen) return;
  
      var now = performance.now();
      var dt = Math.min((now - last) / 1000, 1 / 20);
      last = now;
      var t = (now - start) / 1000;
      // אפקט דקורטיבי קופא כשהמשתמש ביקש פחות תנועה. אפקט שהוא מפעיל בעצמו
      // (סמן, לחיצה) ממשיך, כי זו תגובה ישירה לפעולה שלו.
      if (!cfg.userDriven && !motionEnabled()) dt = 0;
  
      if (cfg.before) cfg.before(ctx, w, h, dpr);
      else ctx.clearRect(0, 0, w, h);
  
      for (var i = 0; i < ps.length; i++) {
        if (cfg.step(ps[i], dt, w, h, t, pointer, dpr) === false) ps[i] = cfg.spawn(w, h, false, dpr);
        cfg.draw(ctx, ps[i], t, dpr);
      }
      // ps מועבר החוצה כדי שאפקטים שמציירים קשרים בין חלקיקים (רשת נקודות)
      // יוכלו לגשת לכולם, ולא רק לאחד בכל קריאה
      if (cfg.after) cfg.after(ctx, w, h, t, dpr, pointer, ps);
    }
    loop();
  
    return function () {
      running = false;
      cancelAnimationFrame(raf);
      io.disconnect();
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerdown', onDown);
    };
  }
})();
</script>