<!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>
<canvas id="canvas"></canvas>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #03030e;
height: 100vh;
}
canvas {
display: block;
}
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);