feat: Migrate landing page to Vue 3 + TypeScript
- 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
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user