Files
aranroig.com/frontend/app/components/parts/AnimatedBackground.vue
BinarySandia04 ab0db1ab17
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 29s
particles
2026-06-10 20:47:42 +02:00

303 lines
7.9 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const canvas = ref<HTMLCanvasElement | null>(null)
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
y: number
vx: number
vy: number
size: number
parallax: number
}
function parseAccent(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 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) / 30000)
const capped = Math.min(count, 70)
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.3,
vy: (Math.random() - 0.5) * 0.3,
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,
})
}
return particlesArr
}
function drawConnections(w: number, h: number) {
if (!ctx) return
const [r, g, b] = parseAccent(accentColor)
for (let i = 0; i < particles.length; i++) {
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 = 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(edgeA_x, edgeA_y)
ctx.lineTo(edgeB_x, edgeB_y)
ctx.strokeStyle = `rgba(${r},${g},${b},${connAlpha})`
ctx.lineWidth = 1
ctx.stroke()
}
}
}
}
function drawTuiParticle(x: number, y: number, size: number, r: number, g: number, b: number) {
if (!ctx) return
// Draw plus/cross shape (TUI cursor style)
ctx.fillStyle = `rgba(${r},${g},${b},0.85)`
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 {
if (!ctx || !canvas.value) return
const w = canvas.value.width
const h = canvas.value.height
const [r, g, b] = parseAccent(accentColor)
ctx.clearRect(0, 0, w, h)
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
if (p.x > w + 20) p.x = -20
if (p.y < -20) p.y = h + 20
if (p.y > h + 20) p.y = -20
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)
}
function initCanvas() {
const c = canvas.value
if (!c) return
accentColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-link')
.trim()
if (!accentColor || !accentColor.startsWith('#')) return
const w = window.innerWidth
const h = window.innerHeight
const dpr = window.devicePixelRatio || 1
c.width = w * dpr
c.height = h * dpr
c.style.width = `${w}px`
c.style.height = `${h}px`
if (ctx) ctx.scale(dpr, dpr)
particles = createParticles(w, h)
bgParticles = createBgParticles(w, h)
}
onMounted(() => {
if (!canvas.value) return
initCanvas()
ctx = canvas.value.getContext('2d')
const observer = new MutationObserver(() => {
accentColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-link')
.trim()
if (canvas.value) {
const w = window.innerWidth
const h = window.innerHeight
canvas.value.style.width = `${w}px`
canvas.value.style.height = `${h}px`
}
})
observer.observe(document.documentElement, { attributes: true })
window.addEventListener('resize', () => {
if (!canvas.value || !ctx) return
const w = window.innerWidth
const h = window.innerHeight
const dpr = window.devicePixelRatio || 1
canvas.value.width = w * dpr
canvas.value.height = h * dpr
canvas.value.style.width = `${w}px`
canvas.value.style.height = `${h}px`
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(() => {
cancelAnimationFrame(animId)
})
</script>
<template>
<canvas ref="canvas" class="animated-bg" aria-hidden="true" />
</template>
<style lang="scss">
.animated-bg {
position: fixed !important;
top: 0 !important;
right: 0 !important;
bottom: 0 !important;
left: 0 !important;
z-index: -1 !important;
pointer-events: none !important;
width: 100vw !important;
height: 100vh !important;
}
</style>