Interactive Physics Toys

KINETIC PARTICLE VORTEX

1,000+ interactive glowing physics particles responding to mouse movement. Move cursor to attract, click to explode shockwaves.

Particle Count 800
Gravity Force 1x
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

const canvas = document.getElementById('vortex-canvas');
    const ctx = canvas.getContext('2d');
    const wrap = document.getElementById('canvas-wrap');

    const themeSelect = document.getElementById('select-theme');
    const countInput = document.getElementById('input-count');
    const gravInput = document.getElementById('input-grav');
    const toast = document.getElementById('toast');

    let particles = [];
    let mouse = { x: 0, y: 0, active: false };

    const THEMES = {
      fire: ['#ff2200', '#ff7700', '#ffcc00', '#ff0055'],
      neon: ['#00e5ff', '#b537f2', '#ff0077', '#00ff66'],
      ocean: ['#00b4d8', '#0077b6', '#90e0ef', '#03045e'],
      emerald: ['#00c896', '#059669', '#34d399', '#a7f3d0']
    };

    function resize() {
      canvas.width = wrap.clientWidth;
      canvas.height = wrap.clientHeight;
      mouse.x = canvas.width / 2;
      mouse.y = canvas.height / 2;
    }

    function initParticles() {
      const count = parseInt(countInput.value);
      document.getElementById('val-count').textContent = count;
      particles = [];
      for (let i = 0; i < count; i++) {
        particles.push({
          x: Math.random() * canvas.width,
          y: Math.random() * canvas.height,
          vx: (Math.random() - 0.5) * 2,
          vy: (Math.random() - 0.5) * 2,
          radius: Math.random() * 2 + 1,
          color: THEMES[themeSelect.value][Math.floor(Math.random() * 4)]
        });
      }
    }

    function drawVortex() {
      ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      const grav = parseFloat(gravInput.value);
      document.getElementById('val-grav').textContent = grav + 'x';

      particles.forEach(p => {
        const dx = mouse.x - p.x;
        const dy = mouse.y - p.y;
        const dist = Math.sqrt(dx * dx + dy * dy) || 1;

        if (dist < 400) {
          const force = (400 - dist) / 400 * 0.2 * grav;
          p.vx += (dx / dist) * force;
          p.vy += (dy / dist) * force;
        }

        p.vx *= 0.96;
        p.vy *= 0.96;

        p.x += p.vx;
        p.y += p.vy;

        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
        ctx.fill();
      });

      requestAnimationFrame(drawVortex);
    }

    wrap.addEventListener('mousemove', e => {
      const rect = canvas.getBoundingClientRect();
      mouse.x = e.clientX - rect.left;
      mouse.y = e.clientY - rect.top;
    });

    wrap.addEventListener('click', e => {
      const rect = canvas.getBoundingClientRect();
      const clickX = e.clientX - rect.left;
      const clickY = e.clientY - rect.top;

      particles.forEach(p => {
        const dx = p.x - clickX;
        const dy = p.y - clickY;
        const dist = Math.sqrt(dx * dx + dy * dy) || 1;
        if (dist < 200) {
          const force = (200 - dist) * 0.1;
          p.vx += (dx / dist) * force;
          p.vy += (dy / dist) * force;
        }
      });
    });

    countInput.addEventListener('input', initParticles);
    themeSelect.addEventListener('change', initParticles);

    document.getElementById('btn-fullscreen').addEventListener('click', () => {
      if (!document.fullscreenElement) {
        wrap.requestFullscreen();
      } else {
        document.exitFullscreen();
      }
    });

    document.getElementById('btn-screenshot').addEventListener('click', () => {
      const a = document.createElement('a');
      a.href = canvas.toDataURL('image/png');
      a.download = 'particle-vortex.png';
      a.click();
    });

    window.addEventListener('resize', resize);
    resize();
    initParticles();
    drawVortex();
Screenshot Saved!