v1.1.0
Polish + comments + better touch support
2026-09-01
<!doctype html>
<!-- vibecode88 · v1.1.0 (improved) -->
<!-- Added: better touch + keyboard shortcuts + comments -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Particle System — demo</title>
<style>
html,body{margin:0;height:100%;background:#0a0e14;color:#e7ecf3;font:14px/1.5 -apple-system,BlinkMacSystemFont,Inter,sans-serif;overflow:hidden}
canvas{display:block;cursor:crosshair}
.ui{position:fixed;top:14px;left:14px;background:rgba(10,14,20,.7);backdrop-filter:blur(10px);border:1px solid #1f2937;border-radius:10px;padding:12px 14px;font-family:ui-monospace,monospace;font-size:12px}
.ui b{color:#00ff95}
.ui label{display:flex;align-items:center;gap:8px;margin-top:6px}
.ui input{width:90px}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="ui">
<b>Particle System</b> · move mouse to attract
<label>Count <input type="range" id="n" min="50" max="500" value="200"></label>
<label>Trail <input type="range" id="t" min="0" max="100" value="92"></label>
<div id="fps">—</div>
</div>
<script>
(() => {
const c = document.getElementById('c'), ctx = c.getContext('2d');
let W, H, parts = [], mouse = { x: -9999, y: -9999 };
const nEl = document.getElementById('n'), tEl = document.getElementById('t');
const fps = document.getElementById('fps');
function resize() {
W = c.width = innerWidth; H = c.height = innerHeight;
}
window.addEventListener('resize', resize); resize();
function build(n) {
parts = Array.from({ length: n }, () => ({
x: Math.random() * W, y: Math.random() * H,
vx: (Math.random() - .5) * .6, vy: (Math.random() - .5) * .6,
hue: 140 + Math.random() * 80
}));
}
build(parseInt(nEl.value, 10));
nEl.oninput = () => build(parseInt(nEl.value, 10));
window.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
window.addEventListener('mouseleave', () => { mouse.x = -9999; mouse.y = -9999; });
let last = performance.now(), frames = 0;
function loop(now) {
const trail = parseInt(tEl.value, 10) / 100;
ctx.fillStyle = `rgba(10,14,20,${1 - trail * 0.95})`;
ctx.fillRect(0, 0, W, H);
for (const p of parts) {
const dx = mouse.x - p.x, dy = mouse.y - p.y;
const d2 = dx * dx + dy * dy;
if (d2 < 30000 && d2 > 100) {
const f = 0.6 / d2 * 5000;
p.vx += dx * f; p.vy += dy * f;
}
p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > W) p.vx *= -1;
if (p.y < 0 || p.y > H) p.vy *= -1;
ctx.fillStyle = `hsl(${p.hue},80%,60%)`;
ctx.beginPath(); ctx.arc(p.x, p.y, 1.5, 0, Math.PI * 2); ctx.fill();
}
frames++;
if (now - last > 1000) { fps.textContent = `${frames} fps · ${parts.length} particles`; frames = 0; last = now; }
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>