fb7d7c5b2d
- Replace static HTML with Vue 3 SPA (Vite + TypeScript) - Add GalaxyCanvas component (interactive particle animation) - Component-based sections (HeroSection, SectionBlock, FooterView) - Add nginx.conf with SPA routing + security headers - Update compose.yaml to mount nginx config - Build output in html/ directory
117 lines
2.8 KiB
Vue
117 lines
2.8 KiB
Vue
<template>
|
|
<canvas ref="canvasRef" class="galaxy-canvas"></canvas>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
let animationId = 0
|
|
|
|
interface Star {
|
|
x: number
|
|
y: number
|
|
z: number
|
|
size: number
|
|
opacity: number
|
|
speed: number
|
|
}
|
|
|
|
onMounted(() => {
|
|
const canvas = canvasRef.value
|
|
if (!canvas) return
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
|
|
const stars: Star[] = []
|
|
const STAR_COUNT = 160
|
|
|
|
function resize() {
|
|
canvas!.width = window.innerWidth
|
|
canvas!.height = window.innerHeight
|
|
}
|
|
|
|
function initStars() {
|
|
for (let i = 0; i < STAR_COUNT; i++) {
|
|
stars.push({
|
|
x: Math.random() * canvas!.width - canvas!.width / 2,
|
|
y: Math.random() * canvas!.height - canvas!.height / 2,
|
|
z: Math.random() * 1000,
|
|
size: Math.random() * 2 + 0.5,
|
|
opacity: Math.random() * 0.8 + 0.2,
|
|
speed: Math.random() * 0.3 + 0.05,
|
|
})
|
|
}
|
|
}
|
|
|
|
const mouse = { x: 0, y: 0 }
|
|
function onMouseMove(e: MouseEvent) {
|
|
mouse.x = (e.clientX / window.innerWidth - 0.5) * 2
|
|
mouse.y = (e.clientY / window.innerHeight - 0.5) * 2
|
|
}
|
|
|
|
function animate() {
|
|
ctx!.fillStyle = 'rgba(15, 15, 19, 0.12)'
|
|
ctx!.fillRect(0, 0, canvas!.width, canvas!.height)
|
|
|
|
const cx = canvas!.width / 2 + mouse.x * 40
|
|
const cy = canvas!.height / 2 + mouse.y * 40
|
|
|
|
for (const star of stars) {
|
|
star.z -= star.speed * 2
|
|
|
|
if (star.z <= 0) {
|
|
star.z = 1000
|
|
star.x = Math.random() * canvas!.width - canvas!.width / 2
|
|
star.y = Math.random() * canvas!.height - canvas!.height / 2
|
|
star.opacity = Math.random() * 0.8 + 0.2
|
|
}
|
|
|
|
const scale = 800 / star.z
|
|
const sx = star.x * scale + cx
|
|
const sy = star.y * scale + cy
|
|
const size = star.size * scale * 0.5
|
|
|
|
if (sx < 0 || sx > canvas!.width || sy < 0 || sy > canvas!.height) continue
|
|
|
|
const alpha = Math.max(0, Math.min(1, (1 - star.z / 1000) * star.opacity))
|
|
ctx!.beginPath()
|
|
ctx!.arc(sx, sy, Math.max(0.5, size), 0, Math.PI * 2)
|
|
ctx!.fillStyle = `rgba(200, 190, 255, ${alpha})`
|
|
ctx!.fill()
|
|
}
|
|
|
|
animationId = requestAnimationFrame(animate)
|
|
}
|
|
|
|
resize()
|
|
initStars()
|
|
|
|
// Fade in
|
|
ctx.fillStyle = '#0f0f13'
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
setTimeout(animate, 100)
|
|
|
|
window.addEventListener('resize', resize)
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
|
|
onUnmounted(() => {
|
|
cancelAnimationFrame(animationId)
|
|
window.removeEventListener('resize', resize)
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.galaxy-canvas {
|
|
position: fixed;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|