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:
2026-06-16 17:42:23 +02:00
parent f7353ed111
commit fb7d7c5b2d
20 changed files with 1670 additions and 444 deletions
+116
View File
@@ -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>