45fda56fb8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
152 lines
3.9 KiB
Vue
152 lines
3.9 KiB
Vue
<script setup lang="ts">
|
|
const canvasRef = ref<HTMLCanvasElement>()
|
|
let ctx: CanvasRenderingContext2D | null = null
|
|
let animId = 0
|
|
let stars: Star[] = []
|
|
let shootingStars: ShootingStar[] = []
|
|
let nextShootTime = 0
|
|
|
|
interface Star {
|
|
x: number
|
|
y: number
|
|
size: number
|
|
baseAlpha: number
|
|
twinkleSpeed: number
|
|
twinklePhase: number
|
|
}
|
|
|
|
interface ShootingStar {
|
|
x: number
|
|
y: number
|
|
speed: number
|
|
length: number
|
|
alpha: number
|
|
angle: number
|
|
}
|
|
|
|
function init() {
|
|
const canvas = canvasRef.value
|
|
if (!canvas || ctx) return
|
|
canvas.width = window.innerWidth
|
|
canvas.height = window.innerHeight
|
|
ctx = canvas.getContext('2d')
|
|
|
|
const count = Math.floor((canvas.width * canvas.height) / 2000)
|
|
stars = Array.from({ length: count }, () => ({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
size: 0.3 + Math.random() * 2.0,
|
|
baseAlpha: 0.3 + Math.random() * 0.7,
|
|
twinkleSpeed: 0.2 + Math.random() * 0.8,
|
|
twinklePhase: Math.random() * Math.PI * 2,
|
|
}))
|
|
}
|
|
|
|
function scheduleShootingStar() {
|
|
nextShootTime = Date.now() + 2000 + Math.random() * 8000
|
|
}
|
|
|
|
function spawnShootingStar(w: number, h: number) {
|
|
const angle = Math.PI / 4 + (Math.random() - 0.5) * 0.3
|
|
shootingStars.push({
|
|
x: 0,
|
|
y: Math.random() * h * 0.4,
|
|
speed: 6 + Math.random() * 8,
|
|
length: 60 + Math.random() * 100,
|
|
alpha: 1,
|
|
angle,
|
|
})
|
|
}
|
|
|
|
function isDark(): boolean {
|
|
return document.documentElement.classList.contains('dark')
|
|
}
|
|
|
|
function animate() {
|
|
const canvas = canvasRef.value
|
|
if (!canvas || !ctx) return
|
|
|
|
if (!isDark()) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
animId = requestAnimationFrame(animate)
|
|
return
|
|
}
|
|
|
|
const { width, height } = canvas
|
|
ctx.clearRect(0, 0, width, height)
|
|
const now = Date.now()
|
|
|
|
// Stars
|
|
for (const s of stars) {
|
|
const alpha = s.baseAlpha * (0.5 + 0.5 * Math.sin(now * s.twinkleSpeed * 0.001 + s.twinklePhase))
|
|
ctx.beginPath()
|
|
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2)
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`
|
|
ctx.fill()
|
|
}
|
|
|
|
// Shooting stars
|
|
if (now > nextShootTime && shootingStars.length < 4) {
|
|
spawnShootingStar(width, height)
|
|
scheduleShootingStar()
|
|
}
|
|
|
|
for (let i = shootingStars.length - 1; i >= 0; i--) {
|
|
const ss = shootingStars[i]
|
|
ss.x += Math.cos(ss.angle) * ss.speed
|
|
ss.y += Math.sin(ss.angle) * ss.speed
|
|
ss.alpha -= 0.005
|
|
|
|
if (ss.alpha <= 0 || ss.x > width + ss.length || ss.y > height) {
|
|
shootingStars.splice(i, 1)
|
|
continue
|
|
}
|
|
|
|
const tailX = ss.x - Math.cos(ss.angle) * ss.length
|
|
const tailY = ss.y - Math.sin(ss.angle) * ss.length
|
|
const gradient = ctx.createLinearGradient(tailX, tailY, ss.x, ss.y)
|
|
gradient.addColorStop(0, `rgba(255, 255, 255, 0)`)
|
|
gradient.addColorStop(1, `rgba(255, 255, 255, ${ss.alpha})`)
|
|
ctx.beginPath()
|
|
ctx.moveTo(tailX, tailY)
|
|
ctx.lineTo(ss.x, ss.y)
|
|
ctx.strokeStyle = gradient
|
|
ctx.lineWidth = 1.5
|
|
ctx.stroke()
|
|
}
|
|
|
|
animId = requestAnimationFrame(animate)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (typeof window === 'undefined' || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
|
|
await nextTick()
|
|
init()
|
|
animate()
|
|
|
|
const onResize = () => {
|
|
const canvas = canvasRef.value
|
|
if (!canvas) return
|
|
canvas.width = window.innerWidth
|
|
canvas.height = window.innerHeight
|
|
const count = Math.floor((canvas.width * canvas.height) / 2000)
|
|
stars = Array.from({ length: count }, () => ({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
size: 0.3 + Math.random() * 2.0,
|
|
baseAlpha: 0.3 + Math.random() * 0.7,
|
|
twinkleSpeed: 0.2 + Math.random() * 0.8,
|
|
twinklePhase: Math.random() * Math.PI * 2,
|
|
}))
|
|
}
|
|
window.addEventListener('resize', onResize)
|
|
})
|
|
|
|
const style = `position: fixed; inset: 0; z-index: 0; pointer-events: none;`
|
|
</script>
|
|
|
|
<template>
|
|
<canvas ref="canvasRef" :style="style" />
|
|
</template>
|