feat(privat/CV): sync to latest upstream (cv-upstream/main, 30f9608b)

This commit is contained in:
2026-07-07 15:20:55 +02:00
parent 8ac664d33d
commit a4efabbc60
417 changed files with 48564 additions and 712 deletions
@@ -0,0 +1,114 @@
import React, { useRef, useEffect } from 'react'
import './BackgroundPattern.css'
/**
* Full-page particle background — subtle floating dots that react to mouse.
* Replaces the old SVG pattern drift animation.
*/
export default function BackgroundPattern() {
const canvasRef = useRef(null)
const animRef = useRef(null)
const mouseRef = useRef({ x: -1000, y: -1000 })
const particlesRef = useRef([])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
function resize() {
const dpr = window.devicePixelRatio || 1
canvas.width = window.innerWidth * dpr
canvas.height = window.innerHeight * dpr
ctx.scale(dpr, dpr)
canvas.style.width = window.innerWidth + 'px'
canvas.style.height = window.innerHeight + 'px'
initParticles()
}
function initParticles() {
const w = window.innerWidth
const h = window.innerHeight
const count = Math.floor((w * h) / 4000) // ~500 on 1920x1080
particlesRef.current = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
baseX: 0,
baseY: 0,
vx: 0,
vy: 0,
radius: 0.8 + Math.random() * 1.2,
drift: Math.random() * Math.PI * 2,
driftSpeed: 0.002 + Math.random() * 0.003,
}))
particlesRef.current.forEach(p => {
p.baseX = p.x
p.baseY = p.y
})
}
function handleMouseMove(e) {
mouseRef.current.x = e.clientX
mouseRef.current.y = e.clientY
}
resize()
window.addEventListener('resize', resize)
window.addEventListener('mousemove', handleMouseMove)
function animate() {
const w = window.innerWidth
const h = window.innerHeight
ctx.clearRect(0, 0, w, h)
const style = getComputedStyle(document.documentElement)
const accentColor = style.getPropertyValue('--accent-color').trim() || '#4a9eff'
const mouse = mouseRef.current
const repelRadius = 80
particlesRef.current.forEach(p => {
// Subtle autonomous drift
p.drift += p.driftSpeed
const driftX = Math.sin(p.drift) * 0.3
const driftY = Math.cos(p.drift * 0.7) * 0.3
// Mouse repulsion
const dx = p.x - mouse.x
const dy = p.y - mouse.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < repelRadius && dist > 0) {
const force = (repelRadius - dist) / repelRadius
p.vx += (dx / dist) * force * 1.5
p.vy += (dy / dist) * force * 1.5
}
// Spring back
p.vx += (p.baseX - p.x) * 0.02 + driftX * 0.1
p.vy += (p.baseY - p.y) * 0.02 + driftY * 0.1
p.vx *= 0.92
p.vy *= 0.92
p.x += p.vx
p.y += p.vy
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2)
ctx.fillStyle = accentColor
ctx.globalAlpha = 0.25
ctx.fill()
ctx.globalAlpha = 1
})
animRef.current = requestAnimationFrame(animate)
}
animate()
return () => {
cancelAnimationFrame(animRef.current)
window.removeEventListener('resize', resize)
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return <canvas ref={canvasRef} className="bg-particle-canvas" />
}