feat(v2): design tokens, fonts (Manrope/Space Grotesk/JetBrains Mono), galaxy background
CI - Build & Test / Backend (.NET) (push) Failing after 23s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 15s
CI - Build & Test / Security Check (push) Successful in 3s

This commit is contained in:
2026-06-12 00:16:01 +02:00
parent 1a7bf8ca11
commit f378d7aed4
5 changed files with 410 additions and 1 deletions
@@ -0,0 +1,249 @@
<script setup lang="ts">
/**
* GalaxyBackground Canvas Starfield + Aurora Blobs
*
* Ported from assets/galaxy.js (design_handoff_nexus_v2).
* Auto-mounts onMounted; cleans up onUnmounted.
* Fixed overlay with z-index:0 and pointer-events:none.
*/
import { onMounted, onUnmounted, ref } from 'vue'
const rootRef = ref<HTMLElement | null>(null)
interface Star {
x: number
y: number
r: number
a: number
tw: number
ph: number
hue: number
dx: number
dy: number
}
interface Shoot {
x: number
y: number
len: number
sp: number
ang: number
life: number
}
let resizeObserver: ResizeObserver | null = null
let animFrameId = 0
let canvas: HTMLCanvasElement | null = null
let ctx: CanvasRenderingContext2D | null = null
function mount(root: HTMLElement) {
canvas = document.createElement('canvas')
root.appendChild(canvas)
ctx = canvas.getContext('2d')
if (!ctx) return
const dpr = Math.min(window.devicePixelRatio || 1, 2)
let stars: Star[] = []
let shoots: Shoot[] = []
let W = 0
let H = 0
let t = 0
function resize() {
const r = root.getBoundingClientRect()
W = r.width
H = r.height
canvas!.width = W * dpr
canvas!.height = H * dpr
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0)
const count = Math.round((W * H) / 5200)
stars = []
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.3 + 0.25,
a: Math.random() * 0.6 + 0.2,
tw: Math.random() * 0.025 + 0.004,
ph: Math.random() * Math.PI * 2,
hue: Math.random() < 0.5 ? 230 : 270,
dx: (Math.random() - 0.5) * 0.04,
dy: (Math.random() - 0.5) * 0.04,
})
}
}
function spawnShoot() {
const fromLeft = Math.random() < 0.6
shoots.push({
x: fromLeft ? -40 : W * (0.4 + Math.random() * 0.5),
y: Math.random() * H * 0.5,
len: 90 + Math.random() * 120,
sp: 6 + Math.random() * 5,
ang: Math.random() * 0.3 + 0.15,
life: 1,
})
}
function frame() {
ctx!.clearRect(0, 0, W, H)
t += 1
// Draw stars
for (let i = 0; i < stars.length; i++) {
const s = stars[i]
s.ph += s.tw
const a = s.a * (0.55 + 0.45 * Math.sin(s.ph))
s.x += s.dx
s.y += s.dy
if (s.x < 0) s.x = W
if (s.x > W) s.x = 0
if (s.y < 0) s.y = H
if (s.y > H) s.y = 0
ctx!.beginPath()
ctx!.fillStyle = `hsla(${s.hue},90%,82%,${a})`
ctx!.arc(s.x, s.y, s.r, 0, Math.PI * 2)
ctx!.fill()
// Glow for larger stars
if (s.r > 1) {
ctx!.beginPath()
ctx!.fillStyle = `hsla(${s.hue},95%,80%,${a * 0.12})`
ctx!.arc(s.x, s.y, s.r * 3.5, 0, Math.PI * 2)
ctx!.fill()
}
}
// Shooting stars
if (Math.random() < 0.004 && shoots.length < 2) spawnShoot()
for (let j = shoots.length - 1; j >= 0; j--) {
const sh = shoots[j]
sh.x += Math.cos(sh.ang) * sh.sp
sh.y += Math.sin(sh.ang) * sh.sp
sh.life -= 0.006
const ex = sh.x - Math.cos(sh.ang) * sh.len
const ey = sh.y - Math.sin(sh.ang) * sh.len
const g = ctx!.createLinearGradient(sh.x, sh.y, ex, ey)
g.addColorStop(0, `rgba(200,210,255,${0.9 * sh.life})`)
g.addColorStop(1, 'rgba(160,120,255,0)')
ctx!.strokeStyle = g
ctx!.lineWidth = 2
ctx!.lineCap = 'round'
ctx!.beginPath()
ctx!.moveTo(sh.x, sh.y)
ctx!.lineTo(ex, ey)
ctx!.stroke()
if (sh.life <= 0 || sh.x > W + 60 || sh.y > H + 60) shoots.splice(j, 1)
}
animFrameId = requestAnimationFrame(frame)
}
resizeObserver = new ResizeObserver(() => resize())
resizeObserver.observe(root)
resize()
frame()
}
function cleanup() {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
if (animFrameId) {
cancelAnimationFrame(animFrameId)
animFrameId = 0
}
if (canvas && canvas.parentNode) {
canvas.parentNode.removeChild(canvas)
}
canvas = null
ctx = null
}
onMounted(() => {
if (rootRef.value) mount(rootRef.value)
})
onUnmounted(() => {
cleanup()
})
</script>
<template>
<div ref="rootRef" class="galaxy-bg">
<div class="aurora a1"></div>
<div class="aurora a2"></div>
<div class="aurora a3"></div>
</div>
</template>
<style scoped>
.galaxy-bg {
position: fixed;
inset: 0;
z-index: 0;
overflow: hidden;
background:
radial-gradient(1200px 800px at 18% -8%, rgba(79,124,255,.20), transparent 60%),
radial-gradient(1000px 760px at 92% 8%, rgba(181,87,246,.18), transparent 60%),
radial-gradient(900px 700px at 60% 110%, rgba(70,60,180,.20), transparent 60%),
linear-gradient(180deg, #070512, #0a0818 60%, #060410);
pointer-events: none;
}
.galaxy-bg canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.aurora {
position: absolute;
border-radius: 50%;
filter: blur(70px);
opacity: 0.5;
mix-blend-mode: screen;
will-change: transform;
}
.a1 {
width: 46vw;
height: 46vw;
left: -8vw;
top: -14vw;
background: radial-gradient(circle, rgba(79,124,255,.55), transparent 65%);
animation: drift1 26s ease-in-out infinite;
}
.a2 {
width: 40vw;
height: 40vw;
right: -10vw;
top: 2vw;
background: radial-gradient(circle, rgba(181,87,246,.5), transparent 65%);
animation: drift2 32s ease-in-out infinite;
}
.a3 {
width: 42vw;
height: 42vw;
left: 34vw;
bottom: -18vw;
background: radial-gradient(circle, rgba(95,80,220,.45), transparent 65%);
animation: drift3 30s ease-in-out infinite;
}
@keyframes drift1 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(8vw, 6vw) scale(1.12); }
}
@keyframes drift2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-7vw, 5vw) scale(1.1); }
}
@keyframes drift3 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(4vw, -6vw) scale(1.15); }
}
</style>