feat(v2): design tokens, fonts (Manrope/Space Grotesk/JetBrains Mono), galaxy background
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user