163 lines
4.6 KiB
Vue
163 lines
4.6 KiB
Vue
<template>
|
|
<canvas
|
|
ref="el"
|
|
class="pointer-events-none absolute inset-0 h-full w-full"
|
|
aria-hidden="true"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
type Particle = { x: number; y: number; vx: number; vy: number; r: number; a: number };
|
|
|
|
const el = ref<HTMLCanvasElement | null>(null);
|
|
|
|
onMounted(() => {
|
|
const canvas = el.value;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
let particles: Particle[] = [];
|
|
let raf = 0;
|
|
let w = 0;
|
|
let h = 0;
|
|
let dpr = 1;
|
|
const mouse = { x: -9999, y: -9999, tx: -9999, ty: -9999 };
|
|
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
|
|
const resize = () => {
|
|
const parent = canvas.parentElement;
|
|
if (!parent) return;
|
|
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
w = parent.clientWidth;
|
|
h = parent.clientHeight;
|
|
canvas.width = Math.floor(w * dpr);
|
|
canvas.height = Math.floor(h * dpr);
|
|
canvas.style.width = `${w}px`;
|
|
canvas.style.height = `${h}px`;
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
const count = Math.min(110, Math.max(40, Math.floor((w * h) / 12000)));
|
|
particles = Array.from({ length: count }, () => ({
|
|
x: Math.random() * w,
|
|
y: Math.random() * h,
|
|
vx: (Math.random() - 0.5) * 0.45,
|
|
vy: (Math.random() - 0.5) * 0.45,
|
|
r: Math.random() * 1.8 + 0.5,
|
|
a: Math.random() * 0.5 + 0.18,
|
|
}));
|
|
};
|
|
|
|
const onMove = (e: PointerEvent) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
mouse.tx = e.clientX - rect.left;
|
|
mouse.ty = e.clientY - rect.top;
|
|
};
|
|
const onLeave = () => {
|
|
mouse.tx = -9999;
|
|
mouse.ty = -9999;
|
|
};
|
|
|
|
const draw = () => {
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// smooth mouse follow
|
|
if (mouse.tx > -9000) {
|
|
mouse.x += (mouse.tx - mouse.x) * 0.12;
|
|
mouse.y += (mouse.ty - mouse.y) * 0.12;
|
|
} else {
|
|
mouse.x += (-9999 - mouse.x) * 0.05;
|
|
mouse.y += (-9999 - mouse.y) * 0.05;
|
|
}
|
|
|
|
const hasMouse = mouse.x > -5000;
|
|
|
|
for (const p of particles) {
|
|
if (!reduced && hasMouse) {
|
|
const dx = mouse.x - p.x;
|
|
const dy = mouse.y - p.y;
|
|
const dist = Math.hypot(dx, dy) || 1;
|
|
if (dist < 220) {
|
|
const force = (1 - dist / 220) * 0.065;
|
|
p.vx += (dx / dist) * force;
|
|
p.vy += (dy / dist) * force;
|
|
}
|
|
// soft orbital swirl near cursor
|
|
if (dist < 160) {
|
|
p.vx += (-dy / dist) * 0.02;
|
|
p.vy += (dx / dist) * 0.02;
|
|
}
|
|
}
|
|
|
|
p.vx *= 0.98;
|
|
p.vy *= 0.98;
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
|
|
if (p.x < 0) p.x = w;
|
|
if (p.x > w) p.x = 0;
|
|
if (p.y < 0) p.y = h;
|
|
if (p.y > h) p.y = 0;
|
|
|
|
// keep a tiny base drift so field never freezes
|
|
if (Math.abs(p.vx) < 0.05) p.vx += (Math.random() - 0.5) * 0.04;
|
|
if (Math.abs(p.vy) < 0.05) p.vy += (Math.random() - 0.5) * 0.04;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(200, 255, 46, ${p.a})`;
|
|
ctx.fill();
|
|
}
|
|
|
|
for (let i = 0; i < particles.length; i++) {
|
|
for (let j = i + 1; j < particles.length; j++) {
|
|
const a = particles[i];
|
|
const b = particles[j];
|
|
const dx = a.x - b.x;
|
|
const dy = a.y - b.y;
|
|
const dist = Math.hypot(dx, dy);
|
|
if (dist > 120) continue;
|
|
let alpha = (1 - dist / 120) * 0.2;
|
|
if (hasMouse) {
|
|
const midX = (a.x + b.x) / 2;
|
|
const midY = (a.y + b.y) / 2;
|
|
const toMouse = Math.hypot(midX - mouse.x, midY - mouse.y);
|
|
if (toMouse < 180) alpha += (1 - toMouse / 180) * 0.25;
|
|
}
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.strokeStyle = `rgba(200, 255, 46, ${Math.min(alpha, 0.55)})`;
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
if (hasMouse) {
|
|
const g = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, 140);
|
|
g.addColorStop(0, "rgba(200, 255, 46, 0.12)");
|
|
g.addColorStop(1, "rgba(200, 255, 46, 0)");
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath();
|
|
ctx.arc(mouse.x, mouse.y, 140, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
raf = requestAnimationFrame(draw);
|
|
};
|
|
|
|
resize();
|
|
draw();
|
|
window.addEventListener("resize", resize);
|
|
window.addEventListener("pointermove", onMove, { passive: true });
|
|
document.addEventListener("pointerleave", onLeave);
|
|
|
|
onUnmounted(() => {
|
|
cancelAnimationFrame(raf);
|
|
window.removeEventListener("resize", resize);
|
|
window.removeEventListener("pointermove", onMove);
|
|
document.removeEventListener("pointerleave", onLeave);
|
|
});
|
|
});
|
|
</script>
|