feat: refine preloader and hero entrance animations

- Add PreloaderView with staggered "Initializing the dark orbit" reveal
- Gate hero reveal animations behind active state so they start after
  the preloader hides, fixing the NOVERIA/OPERATIONS timing race
- Tighten title letter stagger for a more cohesive, elegant entrance
- Polish galaxy canvas, footer and section blocks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-17 23:30:44 +02:00
parent fb7d7c5b2d
commit 4a9c6024d9
9 changed files with 1025 additions and 288 deletions
+6 -2
View File
@@ -3,8 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bao — Architekt</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>" />
<title>Noveria Operations // Coming Soon</title>
<meta
name="description"
content="Noveria Operations coming soon."
/>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%2303040a'/%3E%3Ccircle cx='50' cy='50' r='33' fill='none' stroke='%237ed8ff' stroke-width='7'/%3E%3Cpath d='M31 61 50 20l19 41H57l-7-17-7 17z' fill='%239d83ff'/%3E%3C/svg%3E" />
</head>
<body>
<div id="app"></div>
+2 -1
View File
@@ -16,5 +16,6 @@
"typescript": "~5.7.3",
"vite": "^6.3.1",
"vue-tsc": "^2.2.8"
}
},
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620"
}
+98 -115
View File
@@ -1,120 +1,35 @@
<template>
<GalaxyCanvas />
<div class="container">
<HeroSection />
<SectionBlock
title="Über mich"
:items="aboutItems"
/>
<SectionBlock
title="Aktuelle Projekte"
layout="stack"
:items="projectItems"
/>
<SectionBlock
title="Tech-Stack"
:items="techItems"
/>
<SectionBlock
title="Prinzipien"
layout="stack"
:items="principleItems"
/>
<PreloaderView :visible="showPreloader" />
<div :class="['page-shell', { 'page-shell--ready': !showPreloader }]">
<main class="container">
<HeroSection :active="!showPreloader" />
</main>
<FooterView />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import GalaxyCanvas from './components/GalaxyCanvas.vue'
import HeroSection from './components/HeroSection.vue'
import SectionBlock from './components/SectionBlock.vue'
import FooterView from './components/FooterView.vue'
import PreloaderView from './components/PreloaderView.vue'
const aboutItems = [
{
icon: '📍',
title: 'Standort',
description: 'Regensburg-Burgweinting, Deutschland',
meta: 'Bayern · Zeitzone Europe/Berlin',
},
{
icon: '🎓',
title: 'Werdegang',
description: 'Fachinformatiker für Anwendungsentwicklung — staatlich geprüft, praxiserprobt.',
meta: 'Abgeschlossene Ausbildung',
},
{
icon: '⚙️',
title: 'Arbeitsweise',
description: 'Clean Code, durchdachte Architektur, ehrliche Kommunikation. Keine Eierlegende-Wollmilchsäue.',
meta: 'Qualität > Quantität',
},
]
const showPreloader = ref(true)
const projectItems = [
{
icon: '🚀',
title: 'Nexus Platform',
description: 'Multi-Agent-System · ASP.NET Core + Vue 3',
badge: 'Aktiv',
badgeClass: 'green',
},
{
icon: '🏗️',
title: 'OpenClaw Gateway',
description: 'Agent-Orchestrierung & Infrastruktur',
badge: 'Aktiv',
badgeClass: 'green',
},
{
icon: '🗄️',
title: 'Noveria Cloud',
description: 'Private Cloud & Self-Hosting',
badge: 'In Planung',
badgeClass: 'yellow',
},
]
let preloaderTimer = 0
const techItems = [
{
icon: '🔷',
title: 'Backend',
description: 'C# · ASP.NET Core 10 · Entity Framework · REST APIs · PostgreSQL',
},
{
icon: '🟢',
title: 'Frontend',
description: 'Vue 3 · TypeScript · Tailwind · Vite · Responsive Design',
},
{
icon: '🐳',
title: 'DevOps',
description: 'Docker · Compose · CI/CD · Linux · Self-Hosting · Gitea',
},
{
icon: '🤖',
title: 'KI & Agents',
description: 'OpenAI · Claude · OpenClaw · Multi-Agent Orchestrierung',
},
]
onMounted(() => {
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
preloaderTimer = window.setTimeout(() => {
showPreloader.value = false
}, reducedMotion ? 500 : 3200)
})
const principleItems = [
{
icon: '🎯',
title: 'Lieber ehrlich als bequem',
description: 'Keine Architektur um der Architektur willen. Jedes Bauteil muss seinen Wert beweisen.',
},
{
icon: '🔒',
title: 'Sicherheit zuerst',
description: 'Keine Secrets im Code. Keine offenen Türen. Produktionszugriff nur über Approval-Ketten.',
},
{
icon: '📐',
title: 'Durchdacht statt überladen',
description: 'Der sauberste Code ist der, den du nicht brauchst. YAGNI lebt.',
},
]
onUnmounted(() => {
window.clearTimeout(preloaderTimer)
})
</script>
<style>
@@ -127,28 +42,96 @@ const principleItems = [
}
:root {
--bg: #0f0f13;
--surface: #1a1a24;
--border: #2a2a3a;
--text: #e4e4ed;
--text-muted: #8888a0;
--accent: #6c5ce7;
color-scheme: dark;
--bg: #02030a;
--surface: rgba(16, 10, 35, 0.58);
--border: rgba(198, 174, 255, 0.2);
--border-bright: rgba(226, 208, 255, 0.38);
--text: #fbf7ff;
--text-muted: #c8b8dd;
--text-dim: #786989;
--accent: #a887ff;
--violet: #5d37b8;
--lilac: #dec9ff;
--deep-blue: #070d22;
}
html {
min-height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
background: var(--bg);
background:
radial-gradient(circle at 50% 36%, rgba(93, 55, 184, 0.28), transparent 31rem),
radial-gradient(circle at 76% 18%, rgba(222, 201, 255, 0.11), transparent 25rem),
radial-gradient(circle at 18% 78%, rgba(7, 13, 34, 0.92), transparent 24rem),
linear-gradient(180deg, #02030a 0%, #0a0618 45%, #070d22 74%, #010207 100%);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
overflow: hidden;
}
body::before,
body::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
}
body::before {
background:
linear-gradient(90deg, rgba(226, 208, 255, 0.03) 1px, transparent 1px),
linear-gradient(180deg, rgba(143, 122, 214, 0.026) 1px, transparent 1px);
background-size: 96px 96px;
mask-image: radial-gradient(circle at 50% 46%, black, transparent 68%);
opacity: 0.42;
}
body::after {
background:
radial-gradient(circle at 50% 48%, transparent 0 30%, rgba(1, 2, 7, 0.72) 100%),
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.022) 0 1px, transparent 1px 4px);
mix-blend-mode: soft-light;
opacity: 0.72;
}
#app {
min-height: 100vh;
position: relative;
}
.page-shell {
min-height: 100vh;
display: grid;
grid-template-rows: 1fr auto;
opacity: 0;
transition: opacity 900ms ease;
}
.page-shell--ready {
opacity: 1;
}
.container {
max-width: 1100px;
width: min(1180px, calc(100vw - 2rem));
margin: 0 auto;
padding: 2rem 1.5rem;
position: relative;
z-index: 1;
z-index: 2;
display: grid;
place-items: center;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
+36 -11
View File
@@ -1,26 +1,51 @@
<template>
<footer>
<p>Bao · Regensburg-Burgweinting · Deutschland</p>
<p class="footnote">Handcrafted with care · Keine KI-generierte Landing Page 😉</p>
<p>Minh Bao - 2026</p>
</footer>
</template>
<style scoped>
footer {
position: relative;
z-index: 2;
text-align: center;
padding: 3rem 0 1.5rem;
border-top: 1px solid #2a2a3a;
margin-top: 2rem;
padding: 0 1rem 1.7rem;
}
footer p {
font-size: 0.82rem;
color: #8888a0;
color: #a994bd;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
text-shadow: 0 0 24px rgba(222, 201, 255, 0.22);
opacity: 0;
transform: translateY(16px);
animation: footer-reveal 1.2s ease 8.45s forwards;
}
.footnote {
margin-top: 0.4rem;
font-size: 0.75rem;
opacity: 0.5;
@keyframes footer-reveal {
from {
opacity: 0;
transform: translateY(16px);
filter: blur(6px);
}
to {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
}
@media (max-width: 760px) {
footer {
padding-bottom: 1.45rem;
}
footer p {
color: #b8a6c8;
font-size: 0.74rem;
}
}
</style>
+181 -26
View File
@@ -7,6 +7,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
const canvasRef = ref<HTMLCanvasElement | null>(null)
let animationId = 0
let cleanup: (() => void) | undefined
interface Star {
x: number
@@ -15,6 +16,25 @@ interface Star {
size: number
opacity: number
speed: number
tint: string
}
interface DriftParticle {
x: number
y: number
radius: number
opacity: number
speed: number
phase: number
}
interface Comet {
x: number
y: number
length: number
speed: number
opacity: number
delay: number
}
onMounted(() => {
@@ -25,46 +45,179 @@ onMounted(() => {
if (!ctx) return
const stars: Star[] = []
const STAR_COUNT = 160
const dust: DriftParticle[] = []
const comets: Comet[] = []
const STAR_COUNT = 220
const DUST_COUNT = 42
const COMET_COUNT = 5
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const mouse = { x: 0, y: 0 }
const viewport = { width: 0, height: 0, ratio: 1 }
function resize() {
canvas!.width = window.innerWidth
canvas!.height = window.innerHeight
viewport.width = window.innerWidth
viewport.height = window.innerHeight
viewport.ratio = Math.min(window.devicePixelRatio || 1, 2)
canvas!.width = Math.floor(viewport.width * viewport.ratio)
canvas!.height = Math.floor(viewport.height * viewport.ratio)
canvas!.style.width = `${viewport.width}px`
canvas!.style.height = `${viewport.height}px`
ctx!.setTransform(viewport.ratio, 0, 0, viewport.ratio, 0, 0)
seedScene()
}
function initStars() {
for (let i = 0; i < STAR_COUNT; i++) {
function seedScene() {
stars.length = 0
dust.length = 0
comets.length = 0
for (let i = 0; i < STAR_COUNT; i += 1) {
const cool = Math.random() > 0.18
stars.push({
x: Math.random() * canvas!.width - canvas!.width / 2,
y: Math.random() * canvas!.height - canvas!.height / 2,
x: Math.random() * viewport.width - viewport.width / 2,
y: Math.random() * viewport.height - viewport.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,
size: Math.random() * 1.8 + 0.35,
opacity: Math.random() * 0.75 + 0.18,
speed: reducedMotion ? 0 : Math.random() * 0.22 + 0.035,
tint: cool ? '225, 207, 255' : '170, 142, 255',
})
}
for (let i = 0; i < DUST_COUNT; i += 1) {
dust.push({
x: Math.random() * viewport.width,
y: Math.random() * viewport.height,
radius: Math.random() * 260 + 80,
opacity: Math.random() * 0.055 + 0.018,
speed: reducedMotion ? 0 : Math.random() * 0.08 + 0.015,
phase: Math.random() * Math.PI * 2,
})
}
for (let i = 0; i < COMET_COUNT; i += 1) {
comets.push({
x: Math.random() * viewport.width,
y: Math.random() * viewport.height * 0.75,
length: Math.random() * 160 + 110,
speed: reducedMotion ? 0 : Math.random() * 2 + 1.2,
opacity: Math.random() * 0.24 + 0.14,
delay: Math.random() * 900,
})
}
}
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)
function drawNebula(time: number) {
const pulse = reducedMotion ? 0 : Math.sin(time * 0.00022) * 22
const cx = viewport.width * 0.5 + mouse.x * 42
const cy = viewport.height * 0.46 + mouse.y * 28
const drift = reducedMotion ? 0 : Math.sin(time * 0.00012) * viewport.width * 0.025
const cx = canvas!.width / 2 + mouse.x * 40
const cy = canvas!.height / 2 + mouse.y * 40
const core = ctx!.createRadialGradient(cx, cy, 0, cx, cy, viewport.width * 0.62)
core.addColorStop(0, 'rgba(222, 201, 255, 0.2)')
core.addColorStop(0.22, 'rgba(93, 55, 184, 0.14)')
core.addColorStop(0.52, 'rgba(24, 16, 58, 0.1)')
core.addColorStop(1, 'rgba(2, 3, 10, 0)')
ctx!.fillStyle = core
ctx!.fillRect(0, 0, viewport.width, viewport.height)
const veil = ctx!.createRadialGradient(
viewport.width * 0.72,
viewport.height * 0.18 + pulse,
0,
viewport.width * 0.72,
viewport.height * 0.18,
viewport.width * 0.48,
)
veil.addColorStop(0, 'rgba(184, 156, 255, 0.08)')
veil.addColorStop(0.45, 'rgba(30, 45, 101, 0.09)')
veil.addColorStop(1, 'rgba(3, 5, 16, 0)')
ctx!.fillStyle = veil
ctx!.fillRect(0, 0, viewport.width, viewport.height)
const rift = ctx!.createLinearGradient(
viewport.width * 0.12 + drift,
viewport.height * 0.12,
viewport.width * 0.86 + drift,
viewport.height * 0.86,
)
rift.addColorStop(0, 'rgba(2, 3, 10, 0)')
rift.addColorStop(0.28, 'rgba(7, 13, 34, 0.22)')
rift.addColorStop(0.5, 'rgba(222, 201, 255, 0.055)')
rift.addColorStop(0.72, 'rgba(7, 13, 34, 0.18)')
rift.addColorStop(1, 'rgba(2, 3, 10, 0)')
ctx!.save()
ctx!.globalCompositeOperation = 'screen'
ctx!.fillStyle = rift
ctx!.fillRect(0, 0, viewport.width, viewport.height)
ctx!.restore()
}
function drawDust(time: number) {
ctx!.globalCompositeOperation = 'lighter'
for (const particle of dust) {
particle.phase += particle.speed * 0.004
const x = particle.x + Math.sin(time * 0.00008 + particle.phase) * 34 + mouse.x * 10
const y = particle.y + Math.cos(time * 0.00006 + particle.phase) * 22 + mouse.y * 8
const gradient = ctx!.createRadialGradient(x, y, 0, x, y, particle.radius)
gradient.addColorStop(0, `rgba(222, 201, 255, ${particle.opacity * 1.12})`)
gradient.addColorStop(0.55, `rgba(83, 102, 184, ${particle.opacity * 0.34})`)
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)')
ctx!.fillStyle = gradient
ctx!.fillRect(x - particle.radius, y - particle.radius, particle.radius * 2, particle.radius * 2)
}
ctx!.globalCompositeOperation = 'source-over'
}
function drawComets() {
for (const comet of comets) {
comet.delay -= 1
if (comet.delay > 0) continue
comet.x += comet.speed * 1.9
comet.y += comet.speed * 0.78
if (comet.x > viewport.width + comet.length || comet.y > viewport.height + 80) {
comet.x = Math.random() * viewport.width * 0.6 - viewport.width * 0.22
comet.y = Math.random() * viewport.height * 0.35
comet.delay = Math.random() * 1000 + 420
}
const tail = ctx!.createLinearGradient(comet.x, comet.y, comet.x - comet.length, comet.y - comet.length * 0.38)
tail.addColorStop(0, `rgba(250, 244, 255, ${comet.opacity})`)
tail.addColorStop(0.16, `rgba(205, 184, 255, ${comet.opacity * 0.5})`)
tail.addColorStop(1, 'rgba(111, 125, 210, 0)')
ctx!.strokeStyle = tail
ctx!.lineWidth = 1
ctx!.beginPath()
ctx!.moveTo(comet.x, comet.y)
ctx!.lineTo(comet.x - comet.length, comet.y - comet.length * 0.38)
ctx!.stroke()
}
}
function animate() {
const time = performance.now()
ctx!.fillStyle = 'rgba(2, 3, 10, 0.32)'
ctx!.fillRect(0, 0, viewport.width, viewport.height)
drawNebula(time)
drawDust(time)
const cx = viewport.width / 2 + mouse.x * 48
const cy = viewport.height / 2 + mouse.y * 34
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.x = Math.random() * viewport.width - viewport.width / 2
star.y = Math.random() * viewport.height - viewport.height / 2
star.opacity = Math.random() * 0.8 + 0.2
}
@@ -73,35 +226,37 @@ onMounted(() => {
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
if (sx < 0 || sx > viewport.width || sy < 0 || sy > viewport.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!.fillStyle = `rgba(${star.tint}, ${alpha})`
ctx!.fill()
}
drawComets()
animationId = requestAnimationFrame(animate)
}
resize()
initStars()
// Fade in
ctx.fillStyle = '#0f0f13'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#02030a'
ctx.fillRect(0, 0, viewport.width, viewport.height)
setTimeout(animate, 100)
window.addEventListener('resize', resize)
window.addEventListener('mousemove', onMouseMove)
onUnmounted(() => {
cleanup = () => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', resize)
window.removeEventListener('mousemove', onMouseMove)
}
})
})
onUnmounted(() => cleanup?.())
</script>
<style scoped>
+314 -65
View File
@@ -1,96 +1,345 @@
<template>
<div class="hero">
<div class="hero-avatar">B</div>
<h1>Bao</h1>
<p class="subtitle">Architekt · Systemdenker · Entwickler</p>
<p class="tagline">
Fachinformatiker für Anwendungsentwicklung aus Regensburg-Burgweinting.
Ich entwerfe Systeme, die halten.
</p>
<div class="status-bar">
<span class="status-dot"></span>
<span class="status-label">Online Nexus Platform</span>
</div>
<section class="hero" :class="{ 'hero--active': active }" aria-labelledby="hero-title">
<div class="galaxy-aura" aria-hidden="true">
<span class="aura-cloud"></span>
<span class="aperture-arc arc-one"></span>
<span class="aperture-arc arc-two"></span>
<span class="aperture-arc arc-three"></span>
<span class="dust-ribbon"></span>
<span class="star-stream"></span>
</div>
<div class="cinema-mark" aria-hidden="true"></div>
<h1 id="hero-title" aria-label="NOVERIA OPERATIONS">
<span
v-for="word in titleWords"
:key="word.text"
class="title-word"
aria-hidden="true"
>
<span
v-for="letter in word.letters"
:key="`${word.text}-${letter.index}`"
class="title-letter"
:style="{ '--delay': `${letter.index * 0.11}s` }"
>
{{ letter.char }}
</span>
</span>
</h1>
<p class="coming-soon">COMING SOON</p>
</section>
</template>
<script setup lang="ts">
defineProps<{
active: boolean
}>()
const titleWords = ['NOVERIA', 'OPERATIONS'].map((text, wordIndex) => {
const offset = wordIndex === 0 ? 0 : 'NOVERIA'.length + 1
return {
text,
letters: Array.from(text).map((char, index) => ({
char,
index: offset + index,
})),
}
})
</script>
<style scoped>
.hero {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
min-height: calc(100vh - 5rem);
display: grid;
place-items: center;
align-content: center;
text-align: center;
padding: 5rem 0 4rem;
position: relative;
isolation: isolate;
}
.hero-avatar {
width: 100px;
height: 100px;
.hero::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
border-radius: 999px;
translate: -50% -50%;
z-index: -1;
}
.hero::before {
width: min(880px, 88vw);
height: min(880px, 88vw);
background:
radial-gradient(circle, rgba(205, 176, 255, 0.18), transparent 30%),
radial-gradient(circle, rgba(89, 55, 176, 0.14), transparent 57%);
filter: blur(28px);
}
.galaxy-aura {
position: absolute;
left: 50%;
top: 50%;
width: min(1060px, 96vw);
height: min(560px, 64vw);
translate: -50% -34%;
z-index: -1;
pointer-events: none;
opacity: 0;
}
.hero--active .galaxy-aura {
animation: aura-reveal 2.8s ease 0.1s forwards;
}
.aura-cloud,
.aperture-arc,
.dust-ribbon,
.star-stream {
position: absolute;
inset: 0;
display: block;
}
.aura-cloud {
border-radius: 50%;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
display: flex;
align-items: center;
justify-content: center;
font-size: 2.8rem;
font-weight: 700;
color: #fff;
margin-bottom: 1.5rem;
box-shadow: 0 0 40px rgba(108, 92, 231, 0.15);
user-select: none;
background:
radial-gradient(circle at 48% 48%, rgba(230, 214, 255, 0.08), transparent 20%),
radial-gradient(circle at 36% 58%, rgba(171, 128, 255, 0.055), transparent 34%),
radial-gradient(circle at 68% 34%, rgba(55, 73, 148, 0.08), transparent 44%);
filter: blur(34px);
opacity: 0.34;
transform: rotate(-7deg) scaleX(1.08);
}
.hero h1 {
font-size: 2.8rem;
font-weight: 700;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #fff 0%, #a29bfe 100%);
.aperture-arc {
border: 1px solid rgba(224, 202, 255, 0.24);
border-radius: 50%;
background: transparent;
filter: drop-shadow(0 0 18px rgba(168, 135, 255, 0.22));
mask-image: conic-gradient(
from 210deg,
transparent 0deg 32deg,
black 46deg 92deg,
transparent 112deg 218deg,
black 236deg 256deg,
transparent 278deg 360deg
);
}
.arc-one {
inset: 9% 3%;
transform: rotate(-12deg);
}
.arc-two {
inset: 19% 13%;
transform: rotate(10deg);
opacity: 0.72;
border-color: rgba(179, 150, 255, 0.18);
mask-image: conic-gradient(
from 24deg,
black 0deg 34deg,
transparent 58deg 198deg,
black 218deg 254deg,
transparent 278deg 360deg
);
}
.arc-three {
inset: 31% 23%;
transform: rotate(-31deg);
opacity: 0.42;
border-color: rgba(226, 208, 255, 0.2);
mask-image: conic-gradient(from 148deg, transparent 0deg 38deg, black 54deg 78deg, transparent 96deg 360deg);
}
.dust-ribbon {
inset: 39% 2% auto;
height: 18%;
border-radius: 50%;
background:
radial-gradient(circle at 24% 62%, rgba(225, 207, 255, 0.2), transparent 1.6px),
radial-gradient(circle at 38% 44%, rgba(225, 207, 255, 0.14), transparent 1px),
radial-gradient(circle at 56% 52%, rgba(143, 122, 214, 0.18), transparent 1.8px),
radial-gradient(circle at 72% 38%, rgba(225, 207, 255, 0.18), transparent 1px),
linear-gradient(104deg, transparent 22%, rgba(222, 201, 255, 0.035) 45%, transparent 75%);
filter: blur(2.5px);
opacity: 0.2;
transform: rotate(-9deg);
}
.star-stream {
inset: 42% -3% auto;
height: 2px;
background:
radial-gradient(circle, rgba(245, 236, 255, 0.82) 0 1px, transparent 2px) 0 0 / 96px 2px,
linear-gradient(90deg, transparent, rgba(225, 202, 255, 0.32), rgba(79, 97, 178, 0.18), transparent);
filter: blur(0.2px);
opacity: 0.72;
transform: rotate(-13deg);
}
.cinema-mark {
width: 1px;
height: 88px;
margin-bottom: 2.4rem;
background: linear-gradient(180deg, transparent, rgba(229, 211, 255, 0.82), transparent);
box-shadow: 0 0 42px rgba(165, 126, 255, 0.48);
opacity: 0;
}
.hero--active .cinema-mark {
animation: mark-reveal 1.45s ease 0.15s forwards;
}
h1 {
display: flex;
justify-content: center;
align-items: baseline;
flex-wrap: wrap;
gap: 0 0.28em;
max-width: 1120px;
font-size: clamp(3rem, 10.8vw, 9.7rem);
line-height: 0.9;
font-weight: 900;
letter-spacing: 0;
text-transform: uppercase;
}
.title-word {
display: inline-flex;
white-space: nowrap;
}
.title-letter {
display: inline-block;
min-width: 0.52em;
background: linear-gradient(180deg, #fffaff 0%, #e2ccff 43%, #9b73ff 76%, #5140b9 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
opacity: 0;
filter: blur(13px);
transform: translateY(0.22em) scale(0.985);
text-shadow:
0 0 38px rgba(224, 202, 255, 0.32),
0 0 90px rgba(109, 83, 214, 0.25);
}
.hero .subtitle {
font-size: 1.15rem;
color: #8888a0;
margin-top: 0.5rem;
font-weight: 400;
.hero--active .title-letter {
animation: letter-reveal 1.85s cubic-bezier(0.16, 1, 0.3, 1) calc(0.45s + var(--delay)) forwards;
}
.hero .tagline {
margin-top: 1.2rem;
max-width: 540px;
color: #8888a0;
font-size: 1rem;
.coming-soon {
margin-top: 2.2rem;
color: #e1ccff;
font-size: clamp(0.92rem, 2vw, 1.22rem);
font-weight: 800;
letter-spacing: 0.36em;
text-transform: uppercase;
opacity: 0;
text-shadow: 0 0 32px rgba(166, 126, 255, 0.5);
}
.status-bar {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
margin-top: 1.5rem;
.hero--active .coming-soon {
animation: subtitle-reveal 1.4s ease 2.95s forwards;
}
.status-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: #00d68f;
animation: pulse 2s ease-in-out infinite;
@keyframes aura-reveal {
0% {
opacity: 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 6px #00d68f; }
50% { opacity: 0.5; box-shadow: 0 0 10px #00d68f; }
68% {
opacity: 0.72;
}
.status-label {
font-size: 0.85rem;
color: #8888a0;
100% {
opacity: 1;
}
}
@media (max-width: 640px) {
.hero { padding: 3rem 0 2rem; }
.hero h1 { font-size: 2rem; }
@keyframes letter-reveal {
0% {
opacity: 0;
filter: blur(13px);
transform: translateY(0.22em) scale(0.985);
}
62% {
opacity: 1;
filter: blur(1.5px);
}
100% {
opacity: 1;
filter: blur(0);
transform: translateY(0) scale(1);
}
}
@keyframes subtitle-reveal {
from {
opacity: 0;
transform: translateY(14px);
filter: blur(8px);
}
to {
opacity: 0.86;
transform: translateY(0);
filter: blur(0);
}
}
@keyframes mark-reveal {
from {
opacity: 0;
transform: scaleY(0.25);
}
to {
opacity: 0.72;
transform: scaleY(1);
}
}
@media (max-width: 760px) {
.hero {
min-height: calc(100vh - 5rem);
}
h1 {
font-size: clamp(2.25rem, 11.8vw, 3.35rem);
max-width: min(360px, 100%);
gap: 0.04em 0.2em;
}
.title-letter {
min-width: 0.48em;
}
.galaxy-aura {
width: min(680px, 118vw);
height: min(420px, 84vw);
translate: -50% -37%;
}
.aura-cloud {
opacity: 0.72;
filter: blur(20px);
}
.aperture-arc {
opacity: 0.58;
}
.coming-soon {
letter-spacing: 0.24em;
}
}
</style>
+213
View File
@@ -0,0 +1,213 @@
<template>
<Transition name="preloader-fade">
<div v-if="visible" class="preloader" aria-hidden="true">
<div class="preloader-core">
<div class="preloader-aura">
<span class="pre-orbit pre-orbit-one"></span>
<span class="pre-orbit pre-orbit-two"></span>
<span class="pre-dust"></span>
<span class="pre-flare"></span>
</div>
<div class="pre-copy">
<p class="pre-status" aria-label="Initializing the dark orbit">
<span
v-for="(letter, index) in statusLetters"
:key="`${letter}-${index}`"
class="pre-status-letter"
:class="{ 'is-space': letter === ' ' }"
:style="{ '--delay': `${index * 0.04}s` }"
>{{ letter === ' ' ? ' ' : letter }}</span>
</p>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
defineProps<{
visible: boolean
}>()
const statusLetters = Array.from('Initializing the dark orbit')
</script>
<style scoped>
.preloader {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
background:
radial-gradient(circle at 50% 42%, rgba(222, 201, 255, 0.08), transparent 22rem),
radial-gradient(circle at 68% 26%, rgba(93, 55, 184, 0.18), transparent 24rem),
linear-gradient(180deg, rgba(2, 3, 10, 0.94), rgba(3, 5, 18, 0.97));
backdrop-filter: blur(10px);
}
.preloader-core {
width: min(560px, calc(100vw - 2rem));
display: grid;
justify-items: center;
gap: 0.65rem;
}
.preloader-aura {
position: relative;
width: min(260px, 54vw);
aspect-ratio: 1;
}
.preloader-aura::before {
content: '';
position: absolute;
inset: 14%;
border-radius: 50%;
background:
radial-gradient(circle at 50% 50%, rgba(225, 207, 255, 0.2), transparent 32%),
radial-gradient(circle at 52% 48%, rgba(70, 82, 158, 0.16), transparent 62%);
filter: blur(20px);
animation: preload-pulse 3.2s ease-in-out infinite;
}
.pre-orbit,
.pre-dust,
.pre-flare {
position: absolute;
inset: 0;
display: block;
}
.pre-orbit {
border-radius: 50%;
border: 1px solid rgba(226, 208, 255, 0.2);
filter: drop-shadow(0 0 14px rgba(168, 135, 255, 0.18));
}
.pre-orbit-one {
inset: 8%;
transform: rotate(-18deg);
mask-image: conic-gradient(from 214deg, transparent 0deg 38deg, black 56deg 130deg, transparent 164deg 236deg, black 258deg 306deg, transparent 336deg);
animation: preload-rotate 5.5s linear infinite;
}
.pre-orbit-two {
inset: 21%;
border-color: rgba(165, 144, 236, 0.18);
transform: rotate(11deg);
mask-image: conic-gradient(from 28deg, black 0deg 42deg, transparent 62deg 210deg, black 228deg 260deg, transparent 286deg);
animation: preload-rotate-reverse 7.4s linear infinite;
}
.pre-dust {
inset: 33% 10%;
height: 34%;
border-radius: 50%;
background:
radial-gradient(circle at 16% 58%, rgba(225, 207, 255, 0.26), transparent 1.8px),
radial-gradient(circle at 42% 46%, rgba(225, 207, 255, 0.16), transparent 1.1px),
radial-gradient(circle at 76% 40%, rgba(143, 122, 214, 0.22), transparent 1.8px),
linear-gradient(104deg, transparent 18%, rgba(222, 201, 255, 0.055) 46%, transparent 78%);
filter: blur(2.4px);
opacity: 0.42;
transform: rotate(-10deg);
}
.pre-flare {
inset: 48% -12% auto;
height: 2px;
background:
radial-gradient(circle, rgba(245, 236, 255, 0.8) 0 1px, transparent 2px) 0 0 / 74px 2px,
linear-gradient(90deg, transparent, rgba(225, 202, 255, 0.32), rgba(79, 97, 178, 0.18), transparent);
opacity: 0.72;
transform: rotate(-12deg);
}
.pre-copy {
display: grid;
justify-items: center;
gap: 0.8rem;
}
.pre-status {
display: flex;
flex-wrap: wrap;
justify-content: center;
color: #b7a5cd;
text-transform: uppercase;
letter-spacing: 0.26em;
font-size: 0.72rem;
}
.pre-status-letter {
display: inline-block;
opacity: 0;
filter: blur(10px);
transform: translateY(0.3em);
animation: pre-letter-reveal 0.9s cubic-bezier(0.16, 1, 0.3, 1) calc(0.18s + var(--delay)) forwards;
}
.pre-status-letter.is-space {
width: 0.4em;
}
.preloader-fade-enter-active,
.preloader-fade-leave-active {
transition: opacity 800ms ease;
}
.preloader-fade-enter-from,
.preloader-fade-leave-to {
opacity: 0;
}
@keyframes preload-pulse {
0%, 100% {
opacity: 0.68;
transform: scale(0.98);
}
50% {
opacity: 1;
transform: scale(1.02);
}
}
@keyframes preload-rotate {
to {
transform: rotate(342deg);
}
}
@keyframes preload-rotate-reverse {
to {
transform: rotate(-349deg);
}
}
@keyframes pre-letter-reveal {
0% {
opacity: 0;
filter: blur(10px);
transform: translateY(0.3em);
}
100% {
opacity: 1;
filter: blur(0);
transform: translateY(0);
}
}
@media (max-width: 760px) {
.preloader-core {
gap: 0.5rem;
}
.pre-status {
font-size: 0.66rem;
letter-spacing: 0.18em;
}
}
</style>
+157 -50
View File
@@ -1,18 +1,20 @@
<template>
<section class="section-block">
<div class="section-header">
<span class="signal"></span>
<h2>{{ title }}</h2>
<span class="line"></span>
</div>
<div v-if="layout === 'cards'" class="cards">
<div v-for="item in items" :key="item.title" class="card">
<article v-for="item in items" :key="item.title" class="card">
<div class="icon">{{ item.icon }}</div>
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<p v-if="item.meta" class="meta">{{ item.meta }}</p>
</div>
</article>
</div>
<div v-else class="stack">
<div v-for="item in items" :key="item.title" class="stack-item">
<article v-for="item in items" :key="item.title" class="stack-item">
<div class="left">
<span class="emoji">{{ item.icon }}</span>
<div class="info">
@@ -21,8 +23,9 @@
</div>
</div>
<span v-if="item.badge" :class="['badge', item.badgeClass || '']">{{ item.badge }}</span>
</article>
</div>
</div>
</section>
</template>
<script setup lang="ts">
@@ -41,24 +44,44 @@ defineProps<{
</script>
<style scoped>
.section-block {
position: relative;
padding: 1.2rem 0 2.8rem;
animation: reveal 720ms ease both;
animation-timeline: view();
animation-range: entry 0% cover 28%;
}
.section-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
padding-top: 2rem;
gap: 0.9rem;
margin-bottom: 1.25rem;
padding-top: 1rem;
}
.section-header .signal {
width: 0.58rem;
height: 0.58rem;
border-radius: 50%;
background: var(--cyan);
box-shadow:
0 0 18px rgba(126, 216, 255, 0.55),
0 0 42px rgba(157, 131, 255, 0.18);
}
.section-header h2 {
font-size: 1.4rem;
font-weight: 600;
color: #e4e4ed;
font-size: clamp(1.05rem, 1.8vw, 1.38rem);
font-weight: 700;
color: var(--text);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.section-header .line {
flex: 1;
height: 1px;
background: #2a2a3a;
background: linear-gradient(90deg, var(--border-bright), transparent);
}
.cards {
@@ -69,43 +92,77 @@ defineProps<{
}
.card {
background: #1a1a24;
border: 1px solid #2a2a3a;
border-radius: 12px;
min-height: 210px;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.045), transparent 32%),
var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
transition: all 200ms ease;
box-shadow:
0 26px 70px rgba(0, 0, 0, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
backdrop-filter: blur(18px);
transition: transform 220ms ease, border-color 220ms ease, background 220ms ease;
cursor: default;
position: relative;
overflow: hidden;
}
.card::after {
content: '';
position: absolute;
inset: auto 1rem 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(126, 216, 255, 0.42), transparent);
opacity: 0;
transition: opacity 220ms ease;
}
.card:hover {
background: #22222e;
border-color: #6c5ce7;
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
background:
linear-gradient(145deg, rgba(126, 216, 255, 0.08), transparent 36%),
var(--surface-strong);
border-color: rgba(126, 216, 255, 0.36);
transform: translateY(-4px);
}
.card:hover::after {
opacity: 1;
}
.card .icon {
font-size: 1.6rem;
margin-bottom: 0.75rem;
width: 2.6rem;
height: 2.6rem;
display: grid;
place-items: center;
border: 1px solid rgba(126, 216, 255, 0.2);
border-radius: 8px;
background: rgba(126, 216, 255, 0.055);
color: var(--cyan);
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.12em;
margin-bottom: 1.1rem;
}
.card h3 {
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 0.4rem;
color: #e4e4ed;
font-weight: 700;
margin-bottom: 0.55rem;
color: var(--text);
}
.card p {
font-size: 0.9rem;
color: #8888a0;
line-height: 1.55;
font-size: 0.92rem;
color: var(--text-muted);
line-height: 1.65;
}
.card .meta {
font-size: 0.8rem;
margin-top: 0.75rem;
opacity: 0.7;
margin-top: 1rem;
color: var(--text-dim);
}
.stack {
@@ -116,20 +173,28 @@ defineProps<{
}
.stack-item {
background: #1a1a24;
border: 1px solid #2a2a3a;
border-radius: 12px;
min-height: 88px;
background:
linear-gradient(90deg, rgba(126, 216, 255, 0.06), transparent 34%),
var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.25rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
transition: all 200ms ease;
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(18px);
transition: transform 220ms ease, border-color 220ms ease, background 220ms ease;
}
.stack-item:hover {
background: #22222e;
border-color: #6c5ce7;
background:
linear-gradient(90deg, rgba(157, 131, 255, 0.12), transparent 42%),
var(--surface-strong);
border-color: rgba(157, 131, 255, 0.36);
transform: translateX(5px);
}
.stack-item .left {
@@ -139,24 +204,35 @@ defineProps<{
}
.stack-item .left .emoji {
font-size: 1.3rem;
flex: 0 0 auto;
width: 2.35rem;
height: 2.35rem;
display: grid;
place-items: center;
border: 1px solid rgba(157, 131, 255, 0.22);
border-radius: 8px;
color: #c7bbff;
background: rgba(157, 131, 255, 0.075);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.08em;
}
.stack-item .left .info h4 {
font-size: 1rem;
font-weight: 500;
color: #e4e4ed;
font-weight: 700;
color: var(--text);
}
.stack-item .left .info span {
font-size: 0.82rem;
color: #8888a0;
font-size: 0.86rem;
color: var(--text-muted);
}
.badge {
background: rgba(108, 92, 231, 0.15);
color: #6c5ce7;
border: 1px solid rgba(108, 92, 231, 0.15);
background: rgba(157, 131, 255, 0.12);
color: #c7bbff;
border: 1px solid rgba(157, 131, 255, 0.2);
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.78rem;
@@ -165,19 +241,50 @@ defineProps<{
}
.badge.green {
background: rgba(0, 214, 143, 0.1);
color: #00d68f;
border-color: rgba(0, 214, 143, 0.15);
background: rgba(53, 231, 179, 0.1);
color: var(--green);
border-color: rgba(53, 231, 179, 0.2);
}
.badge.yellow {
background: rgba(255, 214, 0, 0.1);
color: #ffd600;
border-color: rgba(255, 214, 0, 0.15);
background: rgba(255, 209, 102, 0.1);
color: var(--amber);
border-color: rgba(255, 209, 102, 0.2);
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(28px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px) {
.cards { grid-template-columns: 1fr; }
.stack-item { flex-direction: column; align-items: flex-start; }
.stack-item {
flex-direction: column;
align-items: flex-start;
padding: 1.15rem;
}
.section-block {
padding-bottom: 2rem;
}
}
@media (prefers-reduced-motion: reduce) {
.section-block {
animation: none;
}
.card:hover,
.stack-item:hover {
transform: none;
}
}
</style>
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/env.d.ts","./src/main.ts","./src/App.vue","./src/components/FooterView.vue","./src/components/GalaxyCanvas.vue","./src/components/HeroSection.vue","./src/components/SectionBlock.vue"],"version":"5.7.3"}
{"root":["./src/env.d.ts","./src/main.ts","./src/app.vue","./src/components/footerview.vue","./src/components/galaxycanvas.vue","./src/components/herosection.vue","./src/components/preloaderview.vue","./src/components/sectionblock.vue"],"version":"5.7.3"}