From 4a9c6024d938c855be46b7de443d6e51920326b1 Mon Sep 17 00:00:00 2001 From: AzuTear Date: Wed, 17 Jun 2026 23:30:44 +0200 Subject: [PATCH] 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 --- index.html | 8 +- package.json | 3 +- src/App.vue | 213 ++++++++--------- src/components/FooterView.vue | 47 +++- src/components/GalaxyCanvas.vue | 207 ++++++++++++++--- src/components/HeroSection.vue | 379 +++++++++++++++++++++++++------ src/components/PreloaderView.vue | 213 +++++++++++++++++ src/components/SectionBlock.vue | 241 ++++++++++++++------ tsconfig.app.tsbuildinfo | 2 +- 9 files changed, 1025 insertions(+), 288 deletions(-) create mode 100644 src/components/PreloaderView.vue diff --git a/index.html b/index.html index 6533601..7c34da5 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,12 @@ - Bao — Architekt - + Noveria Operations // Coming Soon + +
diff --git a/package.json b/package.json index 313eac6..d79677d 100644 --- a/package.json +++ b/package.json @@ -16,5 +16,6 @@ "typescript": "~5.7.3", "vite": "^6.3.1", "vue-tsc": "^2.2.8" - } + }, + "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620" } diff --git a/src/App.vue b/src/App.vue index df47dcf..7413a57 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,120 +1,35 @@ diff --git a/src/components/FooterView.vue b/src/components/FooterView.vue index fdf9f02..790cf9a 100644 --- a/src/components/FooterView.vue +++ b/src/components/FooterView.vue @@ -1,26 +1,51 @@ diff --git a/src/components/GalaxyCanvas.vue b/src/components/GalaxyCanvas.vue index 0b9589b..cd2239e 100644 --- a/src/components/GalaxyCanvas.vue +++ b/src/components/GalaxyCanvas.vue @@ -7,6 +7,7 @@ import { ref, onMounted, onUnmounted } from 'vue' const canvasRef = ref(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?.()) diff --git a/src/components/PreloaderView.vue b/src/components/PreloaderView.vue new file mode 100644 index 0000000..1696d17 --- /dev/null +++ b/src/components/PreloaderView.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/components/SectionBlock.vue b/src/components/SectionBlock.vue index ff4b824..ab4cd2d 100644 --- a/src/components/SectionBlock.vue +++ b/src/components/SectionBlock.vue @@ -1,28 +1,31 @@ diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index 051c648..32a3cbc 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file