Files
mai-bot/dashboard/src/components/waves-background.tsx
2026-01-13 06:24:35 +08:00

383 lines
9.1 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
// 生成一个固定的随机种子(在模块加载时生成一次)
const NOISE_SEED = (() => {
// 使用时间戳的一部分作为种子,但在开发环境中使用固定值以保持一致性
if (import.meta.env.DEV) {
return 42 // 开发环境使用固定种子
}
return Date.now() % 1000000
})()
// Perlin Noise implementation
class Noise {
private grad3: number[][]
private p: number[]
private perm: number[]
constructor(seed = 0) {
// Use seed to ensure deterministic noise (seed is used implicitly in shuffle)
void seed
this.grad3 = [
[1, 1, 0],
[-1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[1, 0, 1],
[-1, 0, 1],
[1, 0, -1],
[-1, 0, -1],
[0, 1, 1],
[0, -1, 1],
[0, 1, -1],
[0, -1, -1],
]
this.p = []
for (let i = 0; i < 256; i++) {
this.p[i] = Math.floor(Math.random() * 256)
}
this.perm = []
for (let i = 0; i < 512; i++) {
this.perm[i] = this.p[i & 255]
}
}
dot(g: number[], x: number, y: number) {
return g[0] * x + g[1] * y
}
mix(a: number, b: number, t: number) {
return (1 - t) * a + t * b
}
fade(t: number) {
return t * t * t * (t * (t * 6 - 15) + 10)
}
perlin2(x: number, y: number) {
const X = Math.floor(x) & 255
const Y = Math.floor(y) & 255
x -= Math.floor(x)
y -= Math.floor(y)
const u = this.fade(x)
const v = this.fade(y)
const A = this.perm[X] + Y
const AA = this.perm[A]
const AB = this.perm[A + 1]
const B = this.perm[X + 1] + Y
const BA = this.perm[B]
const BB = this.perm[B + 1]
return this.mix(
this.mix(
this.dot(this.grad3[AA % 12], x, y),
this.dot(this.grad3[BA % 12], x - 1, y),
u
),
this.mix(
this.dot(this.grad3[AB % 12], x, y - 1),
this.dot(this.grad3[BB % 12], x - 1, y - 1),
u
),
v
)
}
}
interface Point {
x: number
y: number
wave: { x: number; y: number }
cursor: { x: number; y: number; vx: number; vy: number }
}
export function WavesBackground() {
const svgRef = useRef<SVGSVGElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const animationRef = useRef<number | undefined>(undefined)
const [noiseInstance] = useState(() => new Noise(NOISE_SEED))
const dataRef = useRef<{
mouse: {
x: number
y: number
lx: number
ly: number
sx: number
sy: number
v: number
vs: number
a: number
set: boolean
}
lines: Point[][]
paths: SVGPathElement[]
noise: Noise
bounding: DOMRect | null
}>({
mouse: {
x: -10,
y: 0,
lx: 0,
ly: 0,
sx: 0,
sy: 0,
v: 0,
vs: 0,
a: 0,
set: false,
},
lines: [],
paths: [],
noise: noiseInstance,
bounding: null,
})
useEffect(() => {
const container = containerRef.current
const svg = svgRef.current
if (!container || !svg) return
const data = dataRef.current
// 将 noiseInstance 赋值给 dataRef
data.noise = noiseInstance
// Set size
const setSize = () => {
const bounding = container.getBoundingClientRect()
data.bounding = bounding
svg.style.width = `${bounding.width}px`
svg.style.height = `${bounding.height}px`
}
// Set lines
const setLines = () => {
if (!data.bounding) return
const { width, height } = data.bounding
data.lines = []
data.paths.forEach((path) => path.remove())
data.paths = []
const xGap = 10
const yGap = 32
const oWidth = width + 200
const oHeight = height + 30
const totalLines = Math.ceil(oWidth / xGap)
const totalPoints = Math.ceil(oHeight / yGap)
const xStart = (width - xGap * totalLines) / 2
const yStart = (height - yGap * totalPoints) / 2
for (let i = 0; i <= totalLines; i++) {
const points: Point[] = []
for (let j = 0; j <= totalPoints; j++) {
const point: Point = {
x: xStart + xGap * i,
y: yStart + yGap * j,
wave: { x: 0, y: 0 },
cursor: { x: 0, y: 0, vx: 0, vy: 0 },
}
points.push(point)
}
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
svg.appendChild(path)
data.paths.push(path)
data.lines.push(points)
}
}
// Move points
const movePoints = (time: number) => {
const { lines, mouse, noise } = data
lines.forEach((points) => {
points.forEach((p) => {
// Wave movement
const move =
noise.perlin2((p.x + time * 0.0125) * 0.002, (p.y + time * 0.005) * 0.0015) * 12
p.wave.x = Math.cos(move) * 32
p.wave.y = Math.sin(move) * 16
// Mouse effect
const dx = p.x - mouse.sx
const dy = p.y - mouse.sy
const d = Math.hypot(dx, dy)
const l = Math.max(175, mouse.vs)
if (d < l) {
const s = 1 - d / l
const f = Math.cos(d * 0.001) * s
p.cursor.vx += Math.cos(mouse.a) * f * l * mouse.vs * 0.00065
p.cursor.vy += Math.sin(mouse.a) * f * l * mouse.vs * 0.00065
}
p.cursor.vx += (0 - p.cursor.x) * 0.005
p.cursor.vy += (0 - p.cursor.y) * 0.005
p.cursor.vx *= 0.925
p.cursor.vy *= 0.925
p.cursor.x += p.cursor.vx * 2
p.cursor.y += p.cursor.vy * 2
p.cursor.x = Math.min(100, Math.max(-100, p.cursor.x))
p.cursor.y = Math.min(100, Math.max(-100, p.cursor.y))
})
})
}
// Get moved point
const moved = (point: Point, withCursorForce = true) => {
const coords = {
x: point.x + point.wave.x + (withCursorForce ? point.cursor.x : 0),
y: point.y + point.wave.y + (withCursorForce ? point.cursor.y : 0),
}
coords.x = Math.round(coords.x * 10) / 10
coords.y = Math.round(coords.y * 10) / 10
return coords
}
// Draw lines
const drawLines = () => {
const { lines, paths } = data
lines.forEach((points, lIndex) => {
let p1 = moved(points[0], false)
let d = `M ${p1.x} ${p1.y}`
points.forEach((point, pIndex) => {
const isLast = pIndex === points.length - 1
p1 = moved(point, !isLast)
d += `L ${p1.x} ${p1.y}`
})
paths[lIndex].setAttribute('d', d)
})
}
// Tick
const tick = (time: number) => {
const { mouse } = data
mouse.sx += (mouse.x - mouse.sx) * 0.1
mouse.sy += (mouse.y - mouse.sy) * 0.1
const dx = mouse.x - mouse.lx
const dy = mouse.y - mouse.ly
const d = Math.hypot(dx, dy)
mouse.v = d
mouse.vs += (d - mouse.vs) * 0.1
mouse.vs = Math.min(100, mouse.vs)
mouse.lx = mouse.x
mouse.ly = mouse.y
mouse.a = Math.atan2(dy, dx)
if (container) {
container.style.setProperty('--x', `${mouse.sx}px`)
container.style.setProperty('--y', `${mouse.sy}px`)
}
movePoints(time)
drawLines()
animationRef.current = requestAnimationFrame(tick)
}
// Event handlers
const handleMouseMove = (e: MouseEvent) => {
if (!data.bounding) return
const { mouse } = data
mouse.x = e.pageX - data.bounding.left
mouse.y = e.pageY - data.bounding.top + window.scrollY
if (!mouse.set) {
mouse.sx = mouse.x
mouse.sy = mouse.y
mouse.lx = mouse.x
mouse.ly = mouse.y
mouse.set = true
}
}
const handleResize = () => {
setSize()
setLines()
}
// Init
setSize()
setLines()
window.addEventListener('resize', handleResize)
window.addEventListener('mousemove', handleMouseMove)
animationRef.current = requestAnimationFrame(tick)
return () => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('mousemove', handleMouseMove)
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [noiseInstance])
return (
<div
ref={containerRef}
className="waves-background"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
overflow: 'hidden',
pointerEvents: 'none',
}}
>
<div
className="waves-cursor"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '0.5rem',
height: '0.5rem',
background: 'hsl(var(--primary) / 0.3)',
borderRadius: '50%',
transform: 'translate3d(calc(var(--x, -0.5rem) - 50%), calc(var(--y, 50%) - 50%), 0)',
willChange: 'transform',
pointerEvents: 'none',
}}
/>
<svg
ref={svgRef}
style={{
display: 'block',
width: '100%',
height: '100%',
}}
>
<style>{`
path {
fill: none;
stroke: hsl(var(--primary) / 0.20);
stroke-width: 1px;
}
`}</style>
</svg>
</div>
)
}