diff --git a/frontend/app/components/parts/AnimatedBackground.vue b/frontend/app/components/parts/AnimatedBackground.vue index e03578d..94290a1 100644 --- a/frontend/app/components/parts/AnimatedBackground.vue +++ b/frontend/app/components/parts/AnimatedBackground.vue @@ -6,6 +6,10 @@ let ctx: CanvasRenderingContext2D | null = null let animId: number let particles: Particle[] = [] let accentColor: string = '' +let scrollY: number = 0 +let mouseX: number = -9999 +let mouseY: number = -9999 +let bgParticles: Particle[] = [] interface Particle { x: number @@ -13,6 +17,7 @@ interface Particle { vx: number vy: number size: number + parallax: number } function parseAccent(hex: string): [number, number, number] { @@ -22,9 +27,16 @@ function parseAccent(hex: string): [number, number, number] { return [r, g, b] } +function parseBgAccent(hex: string): [number, number, number] { + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + return [r, g, b] +} + function createParticles(w: number, h: number): Particle[] { - const count = Math.floor((w * h) / 40000) - const capped = Math.min(count, 80) + const count = Math.floor((w * h) / 30000) + const capped = Math.min(count, 70) const particlesArr: Particle[] = [] for (let i = 0; i < capped; i++) { @@ -33,7 +45,27 @@ function createParticles(w: number, h: number): Particle[] { y: Math.random() * h, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, - size: Math.random() * 2 + 1, + size: 5, + parallax: 0.15, + }) + } + + return particlesArr +} + +function createBgParticles(w: number, h: number): Particle[] { + const count = Math.floor((w * h) / 8000) + const capped = Math.min(count, 400) + const particlesArr: Particle[] = [] + + for (let i = 0; i < capped; i++) { + particlesArr.push({ + x: Math.random() * w, + y: Math.random() * h, + vx: (Math.random() - 0.5) * 0.15, + vy: (Math.random() - 0.5) * 0.15, + size: 3, + parallax: 0.03, }) } @@ -48,14 +80,22 @@ function drawConnections(w: number, h: number) { const a = particles[i] for (let j = i + 1; j < particles.length; j++) { const b2 = particles[j] + const ay = (a as any)._dy ?? a.y + const by = (b2 as any)._dy ?? b2.y const dx = a.x - b2.x - const dy = a.y - b2.y + const dy = ay - by const dist = Math.sqrt(dx * dx + dy * dy) if (dist < 100) { const connAlpha = (1 - dist / 100) * 0.25 + const offset = Math.ceil(Math.max(a.size, b2.size)) + const angle = Math.atan2(dy, dx) + const edgeA_x = a.x + Math.cos(angle) * offset + const edgeA_y = ay + Math.sin(angle) * offset + const edgeB_x = b2.x - Math.cos(angle) * offset + const edgeB_y = by - Math.sin(angle) * offset ctx.beginPath() - ctx.moveTo(a.x, a.y) - ctx.lineTo(b2.x, b2.y) + ctx.moveTo(edgeA_x, edgeA_y) + ctx.lineTo(edgeB_x, edgeB_y) ctx.strokeStyle = `rgba(${r},${g},${b},${connAlpha})` ctx.lineWidth = 1 ctx.stroke() @@ -66,15 +106,17 @@ function drawConnections(w: number, h: number) { function drawTuiParticle(x: number, y: number, size: number, r: number, g: number, b: number) { if (!ctx) return - const half = Math.ceil(size) // Draw plus/cross shape (TUI cursor style) ctx.fillStyle = `rgba(${r},${g},${b},0.85)` - ctx.fillRect(Math.round(x - half), y - 1, size, 1) - ctx.fillRect(x - 1, Math.round(y - half), 1, size) - - // Center dot - ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 2, 2) + ctx.fillRect(Math.round(x - size / 2), Math.round(y - size / 2), size, size) +} + +function drawBgParticle(x: number, y: number, size: number, r: number, g: number, b: number) { + if (!ctx) return + + ctx.fillStyle = `rgba(${r},${g},${b},0.3)` + ctx.fillRect(Math.round(x - size / 2), Math.round(y - size / 2), size, size) } function animate(): void { @@ -87,6 +129,28 @@ function animate(): void { for (const p of particles) { + const drawY = p.y - scrollY * p.parallax + const dx = p.x - mouseX + const dy = drawY - mouseY + const dist2 = dx * dx + dy * dy + if (dist2 < 6400 && dist2 > 1) { + const dist = Math.sqrt(dist2) + const force = 0.8 / dist + p.vx += (dx / dist) * force + p.vy += (dy / dist) * force + } + + p.vx *= 0.98 + p.vy *= 0.98 + + const minSpeed = 0.15 + const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy) + if (speed < minSpeed) { + const angle = Math.atan2((Math.random() - 0.5) * 0.3, (Math.random() - 0.5) * 0.3) + p.vx = Math.cos(angle) * minSpeed + p.vy = Math.sin(angle) * minSpeed + } + p.x += p.vx p.y += p.vy if (p.x < -20) p.x = w + 20 @@ -94,10 +158,51 @@ function animate(): void { if (p.y < -20) p.y = h + 20 if (p.y > h + 20) p.y = -20 - drawTuiParticle(p.x, p.y, p.size, r, g, b) + p._dy = drawY + drawTuiParticle(p.x, p._dy, p.size, r, g, b) } drawConnections(w, h) + + const bgColor = getComputedStyle(document.documentElement) + .getPropertyValue('--color-border-color') + .trim() + if (bgColor && bgColor.startsWith('#')) { + const [br, bg, bb] = parseBgAccent(bgColor) + for (const p of bgParticles) { + const dy = (p as any)._dy ?? p.y - scrollY * p.parallax + const dx = p.x - mouseX + const dist2 = dx * dx + dy * dy + if (dist2 < 6400 && dist2 > 1) { + const dist = Math.sqrt(dist2) + const force = 0.3 / dist + p.vx += (dx / dist) * force + p.vy += (dy / dist) * force + } + + p.vx *= 0.99 + p.vy *= 0.99 + + const minSpeed = 0.1 + const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy) + if (speed < minSpeed) { + const angle = Math.atan2((Math.random() - 0.5) * 0.15, (Math.random() - 0.5) * 0.15) + p.vx = Math.cos(angle) * minSpeed + p.vy = Math.sin(angle) * minSpeed + } + + p.x += p.vx + p.y += p.vy + if (p.x < -10) p.x = w + 10 + if (p.x > w + 10) p.x = -10 + if (p.y < -10) p.y = h + 10 + if (p.y > h + 10) p.y = -10 + + const drawY = p.y - scrollY * p.parallax + drawBgParticle(p.x, drawY, p.size, br, bg, bb) + } + } + animId = requestAnimationFrame(animate) } @@ -122,6 +227,7 @@ function initCanvas() { if (ctx) ctx.scale(dpr, dpr) particles = createParticles(w, h) + bgParticles = createBgParticles(w, h) } onMounted(() => { @@ -155,11 +261,21 @@ onMounted(() => { ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.scale(dpr, dpr) particles = createParticles(w, h) + bgParticles = createBgParticles(w, h) }) if (ctx && canvas.value) { animate() } + + window.addEventListener('scroll', () => { + scrollY = window.scrollY + }, { passive: true }) + + window.addEventListener('mousemove', (e) => { + mouseX = e.clientX + mouseY = e.clientY + }, { passive: true }) }) onBeforeUnmount(() => {