企业官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

253 lines
6.2 KiB

<template>
<div :class="cn('relative h-full w-full', props.containerClass)">
<Motion
ref="containerRef"
as="div"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent"
>
<canvas ref="canvasRef"></canvas>
</Motion>
<div :class="cn('relative z-10', props.class)">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { createNoise3D } from 'simplex-noise'
import { onMounted, onUnmounted } from 'vue'
import { templateRef, useDebounceFn } from '@vueuse/core'
import { cn } from '@/lib/utils'
const TAU = 2 * Math.PI
const BASE_TTL = 50
const RANGE_TTL = 150
const PARTICLE_PROP_COUNT = 9
const RANGE_HUE = 100
const NOISE_STEPS = 3
const X_OFF = 0.00125
const Y_OFF = 0.00125
const Z_OFF = 0.0005
interface VortexProps {
class?: string
containerClass?: string
particleCount?: number
rangeY?: number
baseHue?: number
baseSpeed?: number
rangeSpeed?: number
baseRadius?: number
rangeRadius?: number
backgroundColor?: string
}
const props = withDefaults(defineProps<VortexProps>(), {
particleCount: 700,
rangeY: 100,
baseSpeed: 0.0,
rangeSpeed: 1.5,
baseRadius: 1,
rangeRadius: 2,
baseHue: 220,
backgroundColor: '#000000',
})
const tick = ref<number>(0)
const animationFrame = ref<number | null>(null)
const particleProps = shallowRef<Float32Array | null>(null)
const center = ref<[number, number]>([0, 0])
const ctx = shallowRef<CanvasRenderingContext2D | null>(null)
const canvasRef = templateRef<HTMLCanvasElement | null>('canvasRef')
const containerRef = templateRef<HTMLElement | null>('containerRef')
const particleCache = {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
ttl: 0,
speed: 0,
radius: 0,
hue: 0,
}
const noise3D = createNoise3D()
function rand(n: number) {
return n * Math.random()
}
function randRange(n: number): number {
return n - rand(2 * n)
}
function fadeInOut(t: number, m: number): number {
const hm = 0.5 * m
return Math.abs(((t + hm) % m) - hm) / hm
}
function lerp(n1: number, n2: number, speed: number): number {
return (1 - speed) * n1 + speed * n2
}
function initParticle(i: number) {
if (!particleProps.value || !canvasRef.value) return
const canvas = canvasRef.value
particleCache.x = rand(canvas.width)
particleCache.y = center.value[1] + randRange(props.rangeY)
particleCache.vx = 0
particleCache.vy = 0
particleCache.life = 0
particleCache.ttl = BASE_TTL + rand(RANGE_TTL)
particleCache.speed = props.baseSpeed + rand(props.rangeSpeed)
particleCache.radius = props.baseRadius + rand(props.rangeRadius)
particleCache.hue = props.baseHue + rand(RANGE_HUE)
particleProps.value.set(
[
particleCache.x,
particleCache.y,
particleCache.vx,
particleCache.vy,
particleCache.life,
particleCache.ttl,
particleCache.speed,
particleCache.radius,
particleCache.hue,
],
i,
)
}
function updateParticle(i: number) {
if (!particleProps.value || !canvasRef.value || !ctx.value) return
const canvas = canvasRef.value
const props = particleProps.value
const context = ctx.value
particleCache.x = props[i]
particleCache.y = props[i + 1]
particleCache.vx = props[i + 2]
particleCache.vy = props[i + 3]
particleCache.life = props[i + 4]
particleCache.ttl = props[i + 5]
particleCache.speed = props[i + 6]
particleCache.radius = props[i + 7]
particleCache.hue = props[i + 8]
const n =
noise3D(particleCache.x * X_OFF, particleCache.y * Y_OFF, tick.value * Z_OFF) *
NOISE_STEPS *
TAU
const nextVx = lerp(particleCache.vx, Math.cos(n), 0.5)
const nextVy = lerp(particleCache.vy, Math.sin(n), 0.5)
const nextX = particleCache.x + nextVx * particleCache.speed
const nextY = particleCache.y + nextVy * particleCache.speed
context.save()
context.lineCap = 'round'
context.lineWidth = particleCache.radius
context.strokeStyle = `hsla(${particleCache.hue},100%,60%,${fadeInOut(
particleCache.life,
particleCache.ttl,
)})`
context.beginPath()
context.moveTo(particleCache.x, particleCache.y)
context.lineTo(nextX, nextY)
context.stroke()
context.restore()
props[i] = nextX
props[i + 1] = nextY
props[i + 2] = nextVx
props[i + 3] = nextVy
props[i + 4] = particleCache.life + 1
if (
nextX > canvas.width ||
nextX < 0 ||
nextY > canvas.height ||
nextY < 0 ||
particleCache.life > particleCache.ttl
) {
initParticle(i)
}
}
function draw() {
if (!canvasRef.value || !ctx.value || !particleProps.value) return
const canvas = canvasRef.value
const context = ctx.value
tick.value++
context.fillStyle = props.backgroundColor
context.fillRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < particleProps.value.length; i += PARTICLE_PROP_COUNT) {
updateParticle(i)
}
context.save()
context.filter = 'blur(8px) brightness(200%)'
context.globalCompositeOperation = 'lighter'
context.drawImage(canvas, 0, 0)
context.restore()
context.save()
context.filter = 'blur(4px) brightness(200%)'
context.globalCompositeOperation = 'lighter'
context.drawImage(canvas, 0, 0)
context.restore()
animationFrame.value = requestAnimationFrame(draw)
}
const handleResize = useDebounceFn(() => {
if (!canvasRef.value) return
const canvas = canvasRef.value
const { innerWidth, innerHeight } = window
canvas.width = innerWidth
canvas.height = innerHeight
center.value = [0.5 * canvas.width, 0.5 * canvas.height]
}, 150)
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
ctx.value = canvas.getContext('2d')
if (!ctx.value) return
canvas.width = window.innerWidth
canvas.height = window.innerHeight
center.value = [0.5 * canvas.width, 0.5 * canvas.height]
const particlePropsLength = props.particleCount * PARTICLE_PROP_COUNT
particleProps.value = new Float32Array(particlePropsLength)
for (let i = 0; i < particlePropsLength; i += PARTICLE_PROP_COUNT) {
initParticle(i)
}
draw()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
}
window.removeEventListener('resize', handleResize)
ctx.value = null
particleProps.value = null
})
</script>