上传完整的WebUI前端仓库
This commit is contained in:
382
dashboard/src/components/waves-background.tsx
Normal file
382
dashboard/src/components/waves-background.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user