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();