library / particles & systems / neon-particle-network
live - 60fps 1920 x 1080
neon-particle-network.html - self-contained
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Neon Particle Network</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            background: #03030e;
            height: 100vh;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');

        let W = canvas.width = window.innerWidth;
        let H = canvas.height = window.innerHeight;

        window.addEventListener('resize', () => {
            W = canvas.width = window.innerWidth;
            H = canvas.height = window.innerHeight;
            initGrid();
        });

        // Config
        const PARTICLE_COUNT = 180;
        const CONNECT_DIST = 140;
        const MOUSE_RADIUS = 180;
        const REPEL_FORCE = 3.2;
        const CELL_SIZE = CONNECT_DIST;

        let mouse = { x: -9999, y: -9999 };
        let grid = {};

        window.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
        window.addEventListener('mouseleave', () => { mouse.x = -9999; mouse.y = -9999; });

        class Particle {
            constructor() { this.reset(true); }

            reset(init = false) {
                this.x = Math.random() * W;
                this.y = Math.random() * H;
                this.vx = (Math.random() - 0.5) * 0.9;
                this.vy = (Math.random() - 0.5) * 0.9;
                this.baseSize = Math.random() * 1.8 + 0.8;
                this.hue = Math.random() * 120 + 180; // 180-300: cyan → violet
                this.hueSpeed = (Math.random() - 0.5) * 0.3;
                this.phase = Math.random() * Math.PI * 2;
                this.phaseSpeed = Math.random() * 0.02 + 0.01;
            }

            update(t) {
                // Smooth mouse repulsion
                const dx = this.x - mouse.x;
                const dy = this.y - mouse.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                if (dist < MOUSE_RADIUS && dist > 0) {
                    const force = ((MOUSE_RADIUS - dist) / MOUSE_RADIUS) ** 2 * REPEL_FORCE;
                    this.vx += (dx / dist) * force * 0.12;
                    this.vy += (dy / dist) * force * 0.12;
                }

                // Velocity friction & speed cap
                this.vx *= 0.98;
                this.vy *= 0.98;
                const spd = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
                if (spd > 2.5) { this.vx = (this.vx / spd) * 2.5; this.vy = (this.vy / spd) * 2.5; }

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

                // Wrap edges
                if (this.x < -20) this.x = W + 20;
                if (this.x > W + 20) this.x = -20;
                if (this.y < -20) this.y = H + 20;
                if (this.y > H + 20) this.y = -20;

                // Color & size pulse
                this.hue = (this.hue + this.hueSpeed + 360) % 360;
                this.phase += this.phaseSpeed;
            }

            get size() { return this.baseSize + Math.sin(this.phase) * 0.6; }
            get color() { return `hsl(${this.hue}, 100%, 65%)`; }

            draw() {
                const s = this.size;
                // Glow
                ctx.shadowBlur = 12 + Math.sin(this.phase) * 6;
                ctx.shadowColor = this.color;
                ctx.beginPath();
                ctx.arc(this.x, this.y, s, 0, Math.PI * 2);
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.shadowBlur = 0;
            }
        }

        // Spatial grid helpers
        function cellKey(x, y) {
            return `${Math.floor(x / CELL_SIZE)},${Math.floor(y / CELL_SIZE)}`;
        }

        function initGrid() { grid = {}; }

        function buildGrid(particles) {
            grid = {};
            for (const p of particles) {
                const k = cellKey(p.x, p.y);
                if (!grid[k]) grid[k] = [];
                grid[k].push(p);
            }
        }

        function getNeighbors(p) {
            const cx = Math.floor(p.x / CELL_SIZE);
            const cy = Math.floor(p.y / CELL_SIZE);
            const result = [];
            for (let dx = -1; dx <= 1; dx++) {
                for (let dy = -1; dy <= 1; dy++) {
                    const k = `${cx + dx},${cy + dy}`;
                    if (grid[k]) result.push(...grid[k]);
                }
            }
            return result;
        }

        // Init particles
        const particles = Array.from({ length: PARTICLE_COUNT }, () => new Particle());

        function animate(t) {
            // Subtle trail fade
            ctx.fillStyle = 'rgba(3, 3, 14, 0.18)';
            ctx.fillRect(0, 0, W, H);

            buildGrid(particles);

            for (let i = 0; i < particles.length; i++) {
                particles[i].update(t);
            }

            // Connections via spatial grid (O(n) avg)
            const drawn = new Set();
            for (let i = 0; i < particles.length; i++) {
                const p = particles[i];
                const neighbors = getNeighbors(p);
                for (const q of neighbors) {
                    if (q === p) continue;
                    const id = i < particles.indexOf(q) ? `${i}-${particles.indexOf(q)}` : `${particles.indexOf(q)}-${i}`;
                    if (drawn.has(id)) continue;
                    drawn.add(id);

                    const dx = p.x - q.x;
                    const dy = p.y - q.y;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    if (dist < CONNECT_DIST) {
                        const alpha = (1 - dist / CONNECT_DIST) * 0.55;
                        const grad = ctx.createLinearGradient(p.x, p.y, q.x, q.y);
                        grad.addColorStop(0, `hsla(${p.hue}, 100%, 65%, ${alpha})`);
                        grad.addColorStop(1, `hsla(${q.hue}, 100%, 65%, ${alpha})`);
                        ctx.beginPath();
                        ctx.strokeStyle = grad;
                        ctx.lineWidth = alpha * 1.5;
                        ctx.moveTo(p.x, p.y);
                        ctx.lineTo(q.x, q.y);
                        ctx.stroke();
                    }
                }
                p.draw();
            }

            requestAnimationFrame(animate);
        }

        requestAnimationFrame(animate);
    </script>
</body>

</html>

About this animation

A dynamic network of HSL-cycling neon particles with spatial-grid optimized connection lines and mouse repulsion.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger