Initial commit baby!
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Point { x: number; y: number }
|
||||
interface Seg { from: Point; to: Point }
|
||||
interface Trace { segs: Seg[]; bright: boolean }
|
||||
interface Pulse {
|
||||
trace: Trace;
|
||||
segIdx: number;
|
||||
t: number; // [0,1] within current segment
|
||||
speed: number; // segments per second
|
||||
hue: number; // HSL hue
|
||||
trail: Point[];
|
||||
}
|
||||
|
||||
interface ThemePalette {
|
||||
bg: string;
|
||||
traceBright: string;
|
||||
traceDim: string;
|
||||
padBright: string;
|
||||
padDim: string;
|
||||
viaStroke: string;
|
||||
viaFill: string;
|
||||
pulseHues: number[];
|
||||
trailLightness: number; // % lightness for trail hsla
|
||||
glowLightness: number; // % lightness for glow center
|
||||
coreLightness: number; // % lightness for core dot
|
||||
coreSaturation: number; // % saturation for core dot
|
||||
}
|
||||
|
||||
// ─── Theme palettes ───────────────────────────────────────────────────────────
|
||||
|
||||
const DARK_PALETTE: ThemePalette = {
|
||||
bg: '#040c07',
|
||||
traceBright: '#112b1a',
|
||||
traceDim: '#0a1f10',
|
||||
padBright: '#183322',
|
||||
padDim: '#0f2219',
|
||||
viaStroke: '#1a3d25',
|
||||
viaFill: '#0d2218',
|
||||
pulseHues: [140, 150, 155, 160, 165, 170, 175, 180, 185],
|
||||
trailLightness: 65,
|
||||
glowLightness: 80,
|
||||
coreLightness: 96,
|
||||
coreSaturation: 60,
|
||||
};
|
||||
|
||||
const LIGHT_PALETTE: ThemePalette = {
|
||||
bg: '#e8f2f7',
|
||||
traceBright: '#7aafc8',
|
||||
traceDim: '#9dc4d8',
|
||||
padBright: '#689fbc',
|
||||
padDim: '#88b8cc',
|
||||
viaStroke: '#5590ac',
|
||||
viaFill: '#c2dce8',
|
||||
pulseHues: [185, 190, 195, 200, 205, 210, 215, 220, 170],
|
||||
trailLightness: 38,
|
||||
glowLightness: 28,
|
||||
coreLightness: 22,
|
||||
coreSaturation: 75,
|
||||
};
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const CELL = 56;
|
||||
const TRAIL = 28;
|
||||
const PULSE_COUNT = 22;
|
||||
const DX = [1, 0, -1, 0];
|
||||
const DY = [0, 1, 0, -1];
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function pt(c: number, r: number): Point {
|
||||
return { x: c * CELL, y: r * CELL };
|
||||
}
|
||||
|
||||
function buildTraces(w: number, h: number): Trace[] {
|
||||
const cols = Math.floor(w / CELL) + 2;
|
||||
const rows = Math.floor(h / CELL) + 2;
|
||||
const count = 40 + Math.floor((w * h) / 40000);
|
||||
const traces: Trace[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let c = Math.floor(Math.random() * cols);
|
||||
let r = Math.floor(Math.random() * rows);
|
||||
let dir = Math.floor(Math.random() * 4);
|
||||
const bright = Math.random() < 0.35;
|
||||
const maxLen = bright
|
||||
? 6 + Math.floor(Math.random() * 14)
|
||||
: 3 + Math.floor(Math.random() * 8);
|
||||
const segs: Seg[] = [];
|
||||
|
||||
for (let step = 0; step < maxLen; step++) {
|
||||
const nc = c + DX[dir];
|
||||
const nr = r + DY[dir];
|
||||
if (nc < 0 || nc >= cols || nr < 0 || nr >= rows) break;
|
||||
segs.push({ from: pt(c, r), to: pt(nc, nr) });
|
||||
c = nc;
|
||||
r = nr;
|
||||
if (Math.random() < 0.28) {
|
||||
dir = (dir + (Math.random() < 0.5 ? 1 : 3)) % 4;
|
||||
}
|
||||
if (step >= 2 && Math.random() < 0.06) break;
|
||||
}
|
||||
|
||||
if (segs.length >= 2) traces.push({ segs, bright });
|
||||
}
|
||||
|
||||
return traces;
|
||||
}
|
||||
|
||||
function buildPulses(traces: Trace[], palette: ThemePalette): Pulse[] {
|
||||
const candidates = traces.filter((t) => t.segs.length >= 3 && t.bright);
|
||||
const pool = candidates.length > 0 ? candidates : traces.filter((t) => t.segs.length >= 3);
|
||||
const pulses: Pulse[] = [];
|
||||
|
||||
for (let i = 0; i < PULSE_COUNT; i++) {
|
||||
const trace = pool[Math.floor(Math.random() * pool.length)];
|
||||
if (!trace) continue;
|
||||
pulses.push({
|
||||
trace,
|
||||
segIdx: Math.floor(Math.random() * trace.segs.length),
|
||||
t: Math.random(),
|
||||
speed: 0.45 + Math.random() * 0.85,
|
||||
hue: palette.pulseHues[Math.floor(Math.random() * palette.pulseHues.length)],
|
||||
trail: [],
|
||||
});
|
||||
}
|
||||
return pulses;
|
||||
}
|
||||
|
||||
function lerp(a: Point, b: Point, t: number): Point {
|
||||
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface Props {
|
||||
theme?: 'dark' | 'light';
|
||||
}
|
||||
|
||||
export default function CircuitBackground({ theme = 'dark' }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const stateRef = useRef<{ traces: Trace[]; pulses: Pulse[]; raf: number }>({
|
||||
traces: [], pulses: [], raf: 0,
|
||||
});
|
||||
|
||||
const palette = theme === 'light' ? LIGHT_PALETTE : DARK_PALETTE;
|
||||
|
||||
const init = useCallback((w: number, h: number) => {
|
||||
const traces = buildTraces(w, h);
|
||||
const pulses = buildPulses(traces, palette);
|
||||
stateRef.current.traces = traces;
|
||||
stateRef.current.pulses = pulses;
|
||||
}, [palette]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
if (!ctx) return;
|
||||
|
||||
let lastTime = performance.now();
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
init(canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// ── Draw helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function drawTraces(traces: Trace[]) {
|
||||
for (const trace of traces) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = trace.bright ? palette.traceBright : palette.traceDim;
|
||||
ctx.lineWidth = trace.bright ? 1.5 : 1;
|
||||
ctx.lineCap = 'square';
|
||||
ctx.moveTo(trace.segs[0].from.x, trace.segs[0].from.y);
|
||||
for (const seg of trace.segs) {
|
||||
ctx.lineTo(seg.to.x, seg.to.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
const padCol = trace.bright ? palette.padBright : palette.padDim;
|
||||
for (const seg of trace.segs) {
|
||||
ctx.fillStyle = padCol;
|
||||
ctx.beginPath();
|
||||
ctx.arc(seg.to.x, seg.to.y, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if (trace.bright && Math.random() < 0.4) {
|
||||
const last = trace.segs[trace.segs.length - 1].to;
|
||||
ctx.strokeStyle = palette.viaStroke;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(last.x, last.y, 5, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = palette.viaFill;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawPulse(pulse: Pulse) {
|
||||
const len = pulse.trail.length;
|
||||
if (len < 2) return;
|
||||
|
||||
const { trailLightness, glowLightness, coreLightness, coreSaturation } = palette;
|
||||
|
||||
for (let i = 1; i < len; i++) {
|
||||
const alpha = Math.pow(i / len, 1.6) * 0.75;
|
||||
const width = (i / len) * 2.5;
|
||||
ctx.strokeStyle = `hsla(${pulse.hue}, 90%, ${trailLightness}%, ${alpha})`;
|
||||
ctx.lineWidth = width;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pulse.trail[i - 1].x, pulse.trail[i - 1].y);
|
||||
ctx.lineTo(pulse.trail[i].x, pulse.trail[i].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const head = pulse.trail[len - 1];
|
||||
const glow = ctx.createRadialGradient(head.x, head.y, 0, head.x, head.y, 16);
|
||||
glow.addColorStop(0, `hsla(${pulse.hue}, 90%, ${glowLightness}%, 0.7)`);
|
||||
glow.addColorStop(0.35, `hsla(${pulse.hue}, 90%, ${trailLightness}%, 0.25)`);
|
||||
glow.addColorStop(1, `hsla(${pulse.hue}, 90%, ${trailLightness}%, 0)`);
|
||||
ctx.fillStyle = glow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(head.x, head.y, 16, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = `hsl(${pulse.hue}, ${coreSaturation}%, ${coreLightness}%)`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(head.x, head.y, 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ── Animation loop ────────────────────────────────────────────────────────
|
||||
|
||||
const animate = (now: number) => {
|
||||
const dt = Math.min((now - lastTime) / 1000, 0.05);
|
||||
lastTime = now;
|
||||
|
||||
const { traces, pulses } = stateRef.current;
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
ctx.fillStyle = palette.bg;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
drawTraces(traces);
|
||||
|
||||
for (const pulse of pulses) {
|
||||
pulse.t += pulse.speed * dt;
|
||||
while (pulse.t >= 1) {
|
||||
pulse.t -= 1;
|
||||
pulse.segIdx = (pulse.segIdx + 1) % pulse.trace.segs.length;
|
||||
}
|
||||
|
||||
const seg = pulse.trace.segs[pulse.segIdx];
|
||||
const pos = lerp(seg.from, seg.to, pulse.t);
|
||||
pulse.trail.push(pos);
|
||||
if (pulse.trail.length > TRAIL) pulse.trail.shift();
|
||||
|
||||
drawPulse(pulse);
|
||||
}
|
||||
|
||||
stateRef.current.raf = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
stateRef.current.raf = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
cancelAnimationFrame(stateRef.current.raf);
|
||||
};
|
||||
}, [init, palette]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="fixed inset-0 pointer-events-none"
|
||||
style={{ zIndex: 0 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user