亮色、暗色模式适配
This commit is contained in:
@@ -1,26 +1,32 @@
|
||||
<template>
|
||||
<canvas ref="canvasRef" class="fixed inset-0 w-full h-full z-0 pointer-events-none"></canvas>
|
||||
<canvas ref="canvasRef" class="ambient-bg-canvas fixed inset-0 w-full h-full z-0 pointer-events-none"></canvas>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
// --- 配置参数 ---
|
||||
const config = {
|
||||
starCount: 150, // 恢复到经典数量,避免过于密集
|
||||
meteorChance: 0.003, // 自然流星概率
|
||||
starCount: 150,
|
||||
dustCount: 80,
|
||||
meteorChance: 0.003,
|
||||
orbCount: 3,
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
let running = false
|
||||
let reduceMotion = false
|
||||
let time = 0
|
||||
|
||||
interface Star {
|
||||
x: number
|
||||
y: number
|
||||
radius: number
|
||||
alpha: number
|
||||
fadingOut: boolean
|
||||
speed: number // 闪烁速度
|
||||
speed: number
|
||||
}
|
||||
|
||||
interface Meteor {
|
||||
@@ -52,19 +58,54 @@ interface Particle {
|
||||
size: number
|
||||
}
|
||||
|
||||
// 状态
|
||||
interface Dust {
|
||||
x: number
|
||||
y: number
|
||||
radius: number
|
||||
alpha: number
|
||||
fadingOut: boolean
|
||||
speed: number
|
||||
baseAlpha: number
|
||||
}
|
||||
|
||||
interface Orb {
|
||||
baseX: number
|
||||
baseY: number
|
||||
radius: number
|
||||
ampX: number
|
||||
ampY: number
|
||||
phaseX: number
|
||||
phaseY: number
|
||||
speed: number
|
||||
alpha: number
|
||||
color: string
|
||||
}
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let animationFrameId: number
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
// 对象池
|
||||
const stars: Star[] = []
|
||||
const meteors: Meteor[] = []
|
||||
const waves: Wave[] = []
|
||||
const particles: Particle[] = []
|
||||
const dust: Dust[] = []
|
||||
const orbs: Orb[] = []
|
||||
|
||||
const prefersReducedMotion = () =>
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
const clearPools = () => {
|
||||
stars.length = 0
|
||||
meteors.length = 0
|
||||
waves.length = 0
|
||||
particles.length = 0
|
||||
dust.length = 0
|
||||
orbs.length = 0
|
||||
}
|
||||
|
||||
// 初始化星星 (经典随机分布)
|
||||
const initStars = () => {
|
||||
stars.length = 0
|
||||
for (let i = 0; i < config.starCount; i++) {
|
||||
@@ -74,55 +115,77 @@ const initStars = () => {
|
||||
radius: Math.random() * 1.5,
|
||||
alpha: Math.random(),
|
||||
fadingOut: Math.random() > 0.5,
|
||||
speed: Math.random() * 0.02 + 0.005
|
||||
speed: Math.random() * 0.02 + 0.005,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染循环
|
||||
const render = () => {
|
||||
if (!ctx || !canvasRef.value) return
|
||||
|
||||
// 1. 清空画布
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 2. 绘制星星 (经典呼吸闪烁)
|
||||
ctx.fillStyle = '#FFF'
|
||||
stars.forEach(star => {
|
||||
if (star.fadingOut) {
|
||||
star.alpha -= star.speed
|
||||
if (star.alpha < 0) {
|
||||
star.alpha = 0
|
||||
star.fadingOut = false
|
||||
}
|
||||
} else {
|
||||
star.alpha += star.speed
|
||||
if (star.alpha > 1) {
|
||||
star.alpha = 1
|
||||
star.fadingOut = true
|
||||
}
|
||||
}
|
||||
|
||||
ctx!.globalAlpha = star.alpha
|
||||
ctx!.beginPath()
|
||||
ctx!.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
ctx!.fill()
|
||||
})
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
// 3. 绘制流星
|
||||
updateAndDrawMeteors()
|
||||
|
||||
// 4. 绘制波纹 (Shockwave - 随机颜色)
|
||||
updateAndDrawWaves()
|
||||
|
||||
// 5. 绘制粒子 (Sparks - 物理效果)
|
||||
updateAndDrawParticles()
|
||||
|
||||
animationFrameId = requestAnimationFrame(render)
|
||||
const initDust = () => {
|
||||
dust.length = 0
|
||||
for (let i = 0; i < config.dustCount; i++) {
|
||||
const baseAlpha = Math.random() * 0.35 + 0.15
|
||||
dust.push({
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
radius: Math.random() * 1.8 + 0.4,
|
||||
alpha: baseAlpha,
|
||||
fadingOut: Math.random() > 0.5,
|
||||
speed: Math.random() * 0.008 + 0.003,
|
||||
baseAlpha,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- 辅助绘制函数 ---
|
||||
const initOrbs = () => {
|
||||
orbs.length = 0
|
||||
const palette = [
|
||||
'rgba(212, 179, 131, 0.22)',
|
||||
'rgba(196, 150, 90, 0.16)',
|
||||
'rgba(232, 200, 150, 0.18)',
|
||||
]
|
||||
const placements = [
|
||||
{ bx: 0.15, by: 0.2 },
|
||||
{ bx: 0.82, by: 0.35 },
|
||||
{ bx: 0.45, by: 0.78 },
|
||||
]
|
||||
for (let i = 0; i < config.orbCount; i++) {
|
||||
const p = placements[i] || { bx: Math.random(), by: Math.random() }
|
||||
orbs.push({
|
||||
baseX: p.bx * width,
|
||||
baseY: p.by * height,
|
||||
radius: Math.min(width, height) * (0.28 + Math.random() * 0.12),
|
||||
ampX: 40 + Math.random() * 60,
|
||||
ampY: 30 + Math.random() * 50,
|
||||
phaseX: Math.random() * Math.PI * 2,
|
||||
phaseY: Math.random() * Math.PI * 2,
|
||||
speed: 0.00025 + Math.random() * 0.0002,
|
||||
alpha: 0.55 + Math.random() * 0.25,
|
||||
color: palette[i % palette.length],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const initForTheme = () => {
|
||||
clearPools()
|
||||
if (resolvedTheme.value === 'dark') {
|
||||
initStars()
|
||||
} else {
|
||||
initOrbs()
|
||||
initDust()
|
||||
}
|
||||
}
|
||||
|
||||
const createMeteor = () => {
|
||||
meteors.push({
|
||||
x: Math.random() * width,
|
||||
y: 0,
|
||||
length: Math.random() * 80 + 20,
|
||||
speed: Math.random() * 10 + 5,
|
||||
angle: Math.PI / 4,
|
||||
alpha: 1,
|
||||
width: Math.random() * 1.5 + 0.5,
|
||||
})
|
||||
}
|
||||
|
||||
const updateAndDrawMeteors = () => {
|
||||
if (Math.random() < config.meteorChance) createMeteor()
|
||||
@@ -134,7 +197,6 @@ const updateAndDrawMeteors = () => {
|
||||
const endX = m.x - m.length * Math.cos(m.angle)
|
||||
const endY = m.y - m.length * Math.sin(m.angle)
|
||||
|
||||
// 流星尾巴渐变
|
||||
const gradient = ctx.createLinearGradient(m.x, m.y, endX, endY)
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${m.alpha})`)
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||||
@@ -155,19 +217,20 @@ const updateAndDrawMeteors = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateAndDrawWaves = () => {
|
||||
const updateAndDrawWaves = (isLight: boolean) => {
|
||||
for (let i = waves.length - 1; i >= 0; i--) {
|
||||
const w = waves[i]
|
||||
if (!ctx) break
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(w.x, w.y, w.radius, 0, Math.PI * 2)
|
||||
// 使用 HSL 随机色
|
||||
ctx.strokeStyle = `hsla(${w.hue}, 70%, 60%, ${w.alpha})`
|
||||
ctx.strokeStyle = isLight
|
||||
? `hsla(${w.hue}, 45%, 48%, ${w.alpha * 0.85})`
|
||||
: `hsla(${w.hue}, 70%, 60%, ${w.alpha})`
|
||||
ctx.lineWidth = w.lineWidth
|
||||
ctx.stroke()
|
||||
|
||||
w.radius += 2
|
||||
w.radius += isLight ? 1.6 : 2
|
||||
w.alpha -= 0.02
|
||||
w.lineWidth *= 0.95
|
||||
|
||||
@@ -187,10 +250,9 @@ const updateAndDrawParticles = () => {
|
||||
|
||||
p.x += p.vx
|
||||
p.y += p.vy
|
||||
p.vy += 0.05 // 重力
|
||||
p.vy += 0.05
|
||||
p.vx *= 0.96
|
||||
p.vy *= 0.96
|
||||
|
||||
p.life -= 0.02
|
||||
p.size *= 0.96
|
||||
|
||||
@@ -198,38 +260,130 @@ const updateAndDrawParticles = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const createMeteor = () => {
|
||||
const startX = Math.random() * width
|
||||
const angle = Math.PI / 4 // 45度
|
||||
meteors.push({
|
||||
x: startX,
|
||||
y: 0,
|
||||
length: Math.random() * 80 + 20,
|
||||
speed: Math.random() * 10 + 5,
|
||||
angle: angle,
|
||||
alpha: 1,
|
||||
width: Math.random() * 1.5 + 0.5
|
||||
const drawDark = () => {
|
||||
if (!ctx) return
|
||||
|
||||
ctx.fillStyle = '#FFF'
|
||||
stars.forEach((star) => {
|
||||
if (!reduceMotion) {
|
||||
if (star.fadingOut) {
|
||||
star.alpha -= star.speed
|
||||
if (star.alpha < 0) {
|
||||
star.alpha = 0
|
||||
star.fadingOut = false
|
||||
}
|
||||
} else {
|
||||
star.alpha += star.speed
|
||||
if (star.alpha > 1) {
|
||||
star.alpha = 1
|
||||
star.fadingOut = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx!.globalAlpha = star.alpha
|
||||
ctx!.beginPath()
|
||||
ctx!.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
ctx!.fill()
|
||||
})
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (!reduceMotion) {
|
||||
updateAndDrawMeteors()
|
||||
}
|
||||
updateAndDrawWaves(false)
|
||||
updateAndDrawParticles()
|
||||
}
|
||||
|
||||
// 交互处理
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const drawLight = () => {
|
||||
if (!ctx) return
|
||||
|
||||
// Aurora orbs
|
||||
orbs.forEach((orb) => {
|
||||
const x = reduceMotion
|
||||
? orb.baseX
|
||||
: orb.baseX + Math.sin(time * orb.speed + orb.phaseX) * orb.ampX
|
||||
const y = reduceMotion
|
||||
? orb.baseY
|
||||
: orb.baseY + Math.cos(time * orb.speed * 0.85 + orb.phaseY) * orb.ampY
|
||||
|
||||
const mid = orb.color.replace(/,\s*[\d.]+\)$/, ', 0.08)')
|
||||
const gradient = ctx!.createRadialGradient(x, y, 0, x, y, orb.radius)
|
||||
gradient.addColorStop(0, orb.color)
|
||||
gradient.addColorStop(0.45, mid)
|
||||
gradient.addColorStop(1, 'rgba(212, 179, 131, 0)')
|
||||
|
||||
ctx!.globalAlpha = orb.alpha
|
||||
ctx!.fillStyle = gradient
|
||||
ctx!.beginPath()
|
||||
ctx!.arc(x, y, orb.radius, 0, Math.PI * 2)
|
||||
ctx!.fill()
|
||||
})
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
// Gold dust
|
||||
dust.forEach((d) => {
|
||||
if (!reduceMotion) {
|
||||
if (d.fadingOut) {
|
||||
d.alpha -= d.speed
|
||||
if (d.alpha < d.baseAlpha * 0.25) {
|
||||
d.alpha = d.baseAlpha * 0.25
|
||||
d.fadingOut = false
|
||||
}
|
||||
} else {
|
||||
d.alpha += d.speed
|
||||
if (d.alpha > d.baseAlpha) {
|
||||
d.alpha = d.baseAlpha
|
||||
d.fadingOut = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx!.globalAlpha = d.alpha
|
||||
ctx!.fillStyle = 'rgb(196, 155, 95)'
|
||||
ctx!.beginPath()
|
||||
ctx!.arc(d.x, d.y, d.radius, 0, Math.PI * 2)
|
||||
ctx!.fill()
|
||||
})
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
updateAndDrawWaves(true)
|
||||
updateAndDrawParticles()
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
if (!running || !ctx || !canvasRef.value) return
|
||||
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
if (!reduceMotion) time += 16
|
||||
|
||||
if (resolvedTheme.value === 'dark') {
|
||||
drawDark()
|
||||
} else {
|
||||
drawLight()
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (!running || reduceMotion) return
|
||||
|
||||
const isLight = resolvedTheme.value === 'light'
|
||||
const hue = isLight ? 35 + Math.floor(Math.random() * 12) : Math.floor(Math.random() * 360)
|
||||
|
||||
// 1. 波纹
|
||||
waves.push({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
radius: 0,
|
||||
alpha: 1,
|
||||
lineWidth: 3,
|
||||
hue: hue
|
||||
alpha: isLight ? 0.9 : 1,
|
||||
lineWidth: isLight ? 2.5 : 3,
|
||||
hue,
|
||||
})
|
||||
|
||||
// 2. 粒子
|
||||
const particleCount = 10
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const angle = (Math.PI * 2 / particleCount) * i
|
||||
const angle = ((Math.PI * 2) / particleCount) * i
|
||||
const speed = Math.random() * 3 + 1
|
||||
particles.push({
|
||||
x: e.clientX,
|
||||
@@ -237,41 +391,75 @@ const handleClick = (e: MouseEvent) => {
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
life: 1.0,
|
||||
color: `hsla(${hue}, 80%, 70%, 1)`, // 粒子跟随波纹颜色
|
||||
size: Math.random() * 2 + 1
|
||||
color: isLight
|
||||
? `hsla(${hue}, 50%, 52%, 0.9)`
|
||||
: `hsla(${hue}, 80%, 70%, 1)`,
|
||||
size: Math.random() * 2 + 1,
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 点击触发流星
|
||||
createMeteor()
|
||||
if (!isLight) createMeteor()
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (canvasRef.value) {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
canvasRef.value.width = width
|
||||
canvasRef.value.height = height
|
||||
initStars()
|
||||
if (!canvasRef.value) return
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
canvasRef.value.width = width
|
||||
canvasRef.value.height = height
|
||||
initForTheme()
|
||||
}
|
||||
|
||||
const startLoop = () => {
|
||||
if (running) return
|
||||
running = true
|
||||
render()
|
||||
}
|
||||
|
||||
const stopLoop = () => {
|
||||
running = false
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
if (ctx && canvasRef.value) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
const syncMotionPreference = () => {
|
||||
reduceMotion = prefersReducedMotion()
|
||||
}
|
||||
|
||||
let motionQuery: MediaQueryList | null = null
|
||||
const onMotionChange = () => {
|
||||
syncMotionPreference()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canvasRef.value) {
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
handleResize()
|
||||
if (!canvasRef.value) return
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
syncMotionPreference()
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('click', handleClick)
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('click', handleClick)
|
||||
|
||||
render()
|
||||
}
|
||||
motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
motionQuery.addEventListener('change', onMotionChange)
|
||||
|
||||
// Always run ambient layer for both themes (static draw when reduced motion)
|
||||
startLoop()
|
||||
})
|
||||
|
||||
watch(resolvedTheme, () => {
|
||||
clearPools()
|
||||
initForTheme()
|
||||
if (!running) startLoop()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
stopLoop()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('click', handleClick)
|
||||
motionQuery?.removeEventListener('change', onMotionChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -279,4 +467,4 @@ onUnmounted(() => {
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user