This commit is contained in:
186
frontend/app/components/parts/AnimatedBackground.vue
Normal file
186
frontend/app/components/parts/AnimatedBackground.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<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 = ''
|
||||
|
||||
interface Particle {
|
||||
x: number
|
||||
y: number
|
||||
vx: number
|
||||
vy: number
|
||||
size: 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 createParticles(w: number, h: number): Particle[] {
|
||||
const count = Math.floor((w * h) / 40000)
|
||||
const capped = Math.min(count, 80)
|
||||
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: Math.random() * 2 + 1,
|
||||
})
|
||||
}
|
||||
|
||||
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 dx = a.x - b2.x
|
||||
const dy = a.y - b2.y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
if (dist < 100) {
|
||||
const connAlpha = (1 - dist / 100) * 0.25
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(a.x, a.y)
|
||||
ctx.lineTo(b2.x, b2.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
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
drawTuiParticle(p.x, p.y, p.size, r, g, b)
|
||||
}
|
||||
|
||||
drawConnections(w, h)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
if (ctx && canvas.value) {
|
||||
animate()
|
||||
}
|
||||
})
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user