const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT = cs.getPropertyValue('--accent').trim() || '#5b5bf7';
let w, h;
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
w = innerWidth; h = innerHeight;
c.width = w * dpr; c.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();
const N = 150, VISION = 46, MAXSPD = 2.6;
const boids = Array.from({ length: N }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2 }));
function step() {
for (const b of boids) {
let sx = 0, sy = 0, ax = 0, ay = 0, cx = 0, cy = 0, n = 0;
for (const o of boids) {
if (o === b) continue;
const dx = o.x - b.x, dy = o.y - b.y, d2 = dx * dx + dy * dy;
if (d2 < VISION * VISION) {
n++; ax += o.vx; ay += o.vy; cx += o.x; cy += o.y;
if (d2 < 320) { sx -= dx; sy -= dy; }
}
}
if (n) {
b.vx += (ax / n - b.vx) * 0.04 + (cx / n - b.x) * 0.0006 + sx * 0.004;
b.vy += (ay / n - b.vy) * 0.04 + (cy / n - b.y) * 0.0006 + sy * 0.004;
}
const spd = Math.hypot(b.vx, b.vy) || 1;
if (spd > MAXSPD) { b.vx = (b.vx / spd) * MAXSPD; b.vy = (b.vy / spd) * MAXSPD; }
b.x += b.vx; b.y += b.vy;
if (b.x < 0) b.x += w; if (b.x > w) b.x -= w;
if (b.y < 0) b.y += h; if (b.y > h) b.y -= h;
}
}
function draw() {
ctx.fillStyle = 'rgba(13,13,18,0.28)';
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = ACCENT;
for (const b of boids) {
const ang = Math.atan2(b.vy, b.vx);
ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(ang);
ctx.beginPath(); ctx.moveTo(6, 0); ctx.lineTo(-5, 3.5); ctx.lineTo(-5, -3.5); ctx.closePath(); ctx.fill();
ctx.restore();
}
}
for (let i = 0; i < 40; i++) step(); // 무리가 형성된 상태로 시작
draw();
(function loop() { step(); draw(); requestAnimationFrame(loop); })();