diff --git a/.gitignore b/.gitignore
index 2457088..5ad6d27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,4 +40,4 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
.idea
-.gemini
+.cli
diff --git a/src/app/flight/page.tsx b/src/app/flight/page.tsx
index 22a2a36..75ef5ca 100644
--- a/src/app/flight/page.tsx
+++ b/src/app/flight/page.tsx
@@ -79,7 +79,7 @@ export default function FlightPage() {
return (
-
+
{/* UI Element 1: Label */}
@@ -115,4 +115,4 @@ export default function FlightPage() {
);
-}
\ No newline at end of file
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 672fe9a..f6535c1 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -126,25 +126,119 @@
@layer utilities {
@keyframes twinkle {
- 0%, 100% { opacity: 0.8; transform: scale(1); }
- 50% { opacity: 0.3; transform: scale(0.8); }
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.6;
+ }
}
+
+ @keyframes star-core-pulse {
+ 0%,
+ 38%,
+ 100% {
+ opacity: var(--core-low, 0.25);
+ }
+ 52% {
+ opacity: var(--core-high, 0.55);
+ }
+ }
+
+ @keyframes star-glint-pulse {
+ 0%,
+ 35%,
+ 100% {
+ opacity: var(--glint-base, 0.02);
+ }
+ 50% {
+ opacity: var(--glint-peak, 0.82);
+ }
+ 60% {
+ opacity: calc(var(--glint-peak, 0.82) * 0.12);
+ }
+ }
+
+ @keyframes star-bloom-pulse {
+ 0%,
+ 35%,
+ 100% {
+ opacity: var(--bloom-base, 0.01);
+ }
+ 50% {
+ opacity: var(--bloom-peak, 0.18);
+ }
+ 60% {
+ opacity: calc(var(--bloom-peak, 0.18) * 0.2);
+ }
+ }
+
.animate-twinkle {
animation: twinkle 4s ease-in-out infinite;
}
- .animate-twinkle-delay-1 {
- animation: twinkle 5s ease-in-out infinite;
- animation-delay: 1s;
+
+ .star-core {
+ animation-name: star-core-pulse;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: infinite;
+ opacity: var(--core-low, 0.25);
+ fill: currentColor;
}
- .animate-twinkle-delay-2 {
- animation: twinkle 6s ease-in-out infinite;
- animation-delay: 2s;
+
+ .star-core-bloom {
+ animation-name: star-bloom-pulse;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: infinite;
+ opacity: var(--bloom-base, 0.01);
+ fill: currentColor;
+ filter: blur(0.7px);
+ }
+
+ .star-glint {
+ animation-name: star-glint-pulse;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: infinite;
+ opacity: var(--glint-base, 0.02);
+ stroke-width: 0.62;
+ stroke-linecap: round;
+ }
+
+ .star-glint-bloom {
+ animation-name: star-bloom-pulse;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: infinite;
+ opacity: var(--bloom-base, 0.01);
+ stroke-width: 0.9;
+ stroke-linecap: round;
+ filter: blur(0.65px);
}
@media (prefers-reduced-motion: reduce) {
- .animate-twinkle, .animate-twinkle-delay-1, .animate-twinkle-delay-2 {
+ .animate-twinkle {
animation: none;
+
opacity: 0.8;
}
+
+ .star-core {
+ animation: none;
+ opacity: var(--core-reduced, 0.34);
+ }
+
+ .star-core-bloom {
+ animation: none;
+ opacity: 0.08;
+ }
+
+ .star-glint {
+ animation: none;
+ opacity: 0.08;
+ }
+
+ .star-glint-bloom {
+ animation: none;
+ opacity: 0.04;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index ab4005f..9e90222 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,63 +1,370 @@
-'use client';
+"use client";
-import Link from "next/link";
import { ROUTES } from "@/lib/constants";
-import { useEffect, useState } from "react";
import { getCurrentVoyage } from "@/lib/store";
+import { Route } from "@/types";
+import Link from "next/link";
import { useRouter } from "next/navigation";
-import LobbyBackground from "@/components/LobbyBackground";
+import { CSSProperties, useEffect } from "react";
+
+type Star = {
+ cx: number;
+ cy: number;
+ r: number;
+ armScale?: number;
+};
+
+type Segment = {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+};
+
+type Constellation = {
+ key: "orion" | "auriga" | "ursaMajor";
+ className: string;
+ viewBox: string;
+ colorClass: string;
+ stars: Star[];
+ segments: Segment[];
+};
+
+const CONSTELLATIONS: Constellation[] = [
+ {
+ key: "orion",
+ className: "absolute bottom-10 left-5 w-72 h-72 opacity-30",
+ viewBox: "0 0 100 100",
+ colorClass: "text-indigo-200",
+ stars: [
+ { cx: 25, cy: 15, r: 0.82 },
+ { cx: 75, cy: 25, r: 0.96, armScale: 1.08 },
+ { cx: 45, cy: 48, r: 0.58 },
+ { cx: 50, cy: 50, r: 0.54 },
+ { cx: 55, cy: 52, r: 0.6 },
+ { cx: 30, cy: 85, r: 0.86 },
+ { cx: 70, cy: 80, r: 0.98, armScale: 1.08 },
+ ],
+ segments: [
+ { x1: 25, y1: 15, x2: 45, y2: 48 },
+ { x1: 75, y1: 25, x2: 55, y2: 52 },
+ { x1: 45, y1: 48, x2: 30, y2: 85 },
+ { x1: 55, y1: 52, x2: 70, y2: 80 },
+ ],
+ },
+ {
+ key: "auriga",
+ className: "absolute top-10 right-10 w-64 h-64 opacity-25",
+ viewBox: "0 0 100 100",
+ colorClass: "text-blue-200",
+ stars: [
+ { cx: 50, cy: 15, r: 1.06, armScale: 1.1 },
+ { cx: 20, cy: 35, r: 0.67 },
+ { cx: 25, cy: 75, r: 0.65 },
+ { cx: 75, cy: 75, r: 0.66 },
+ { cx: 85, cy: 35, r: 0.76 },
+ ],
+ segments: [
+ { x1: 50, y1: 15, x2: 20, y2: 35 },
+ { x1: 20, y1: 35, x2: 25, y2: 75 },
+ { x1: 25, y1: 75, x2: 75, y2: 75 },
+ { x1: 75, y1: 75, x2: 85, y2: 35 },
+ { x1: 85, y1: 35, x2: 50, y2: 15 },
+ ],
+ },
+ {
+ key: "ursaMajor",
+ className: "absolute top-20 left-10 w-80 h-48 opacity-25",
+ viewBox: "0 0 100 60",
+ colorClass: "text-slate-200",
+ stars: [
+ { cx: 10, cy: 20, r: 0.64 },
+ { cx: 25, cy: 25, r: 0.67 },
+ { cx: 40, cy: 35, r: 0.69 },
+ { cx: 55, cy: 45, r: 0.99 },
+ { cx: 75, cy: 45, r: 0.98 },
+ { cx: 80, cy: 15, r: 0.93 },
+ { cx: 60, cy: 10, r: 0.96 },
+ ],
+ segments: [
+ { x1: 10, y1: 20, x2: 25, y2: 25 },
+ { x1: 25, y1: 25, x2: 40, y2: 35 },
+ { x1: 40, y1: 35, x2: 55, y2: 45 },
+ { x1: 55, y1: 45, x2: 75, y2: 45 },
+ { x1: 75, y1: 45, x2: 80, y2: 15 },
+ { x1: 80, y1: 15, x2: 60, y2: 10 },
+ { x1: 60, y1: 10, x2: 55, y2: 45 },
+ ],
+ },
+];
+
+const STAR_STYLES = [
+ { duration: 2.3, delay: 0.1 },
+ { duration: 3.1, delay: 1.2 },
+ { duration: 4.8, delay: 0.8 },
+ { duration: 2.7, delay: 2.1 },
+ { duration: 5.2, delay: 1.7 },
+ { duration: 3.9, delay: 0.4 },
+ { duration: 4.4, delay: 2.6 },
+ { duration: 2.1, delay: 1.3 },
+ { duration: 5.8, delay: 0.2 },
+ { duration: 3.3, delay: 2.4 },
+ { duration: 4.0, delay: 1.1 },
+ { duration: 2.9, delay: 1.9 },
+] as const;
+
+function StarGlint({ starIndex, star }: { starIndex: number; star: Star }) {
+ const timing = STAR_STYLES[starIndex % STAR_STYLES.length];
+ const strengthTier =
+ star.r >= 0.95 ? "bright" : star.r >= 0.72 ? "mid" : "faint";
+ const glintPeak =
+ strengthTier === "bright" ? 0.7 : strengthTier === "mid" ? 0.61 : 0.52;
+ const bloomPeak =
+ strengthTier === "bright" ? 0.16 : strengthTier === "mid" ? 0.12 : 0.08;
+ const coreLow =
+ strengthTier === "bright" ? 0.9 : strengthTier === "mid" ? 0.73 : 0.6;
+ const coreHigh =
+ strengthTier === "bright" ? 1 : strengthTier === "mid" ? 0.9 : 0.82;
+ const coreReduced =
+ strengthTier === "bright" ? 0.9 : strengthTier === "mid" ? 0.76 : 0.66;
+ const coreStyle = {
+ animationDuration: `${timing.duration}s`,
+ animationDelay: `${timing.delay}s`,
+ "--core-low": `${coreLow}`,
+ "--core-high": `${coreHigh}`,
+ "--core-reduced": `${coreReduced}`,
+ } as CSSProperties;
+ const glintStyle = {
+ animationDuration: `${timing.duration}s`,
+ animationDelay: `${timing.delay + 0.12}s`,
+ "--glint-peak": `${glintPeak}`,
+ "--glint-base": "0.02",
+ "--bloom-peak": `${bloomPeak}`,
+ "--bloom-base": "0.01",
+ } as CSSProperties;
+ const glintLength = star.r * 4.4 * (star.armScale ?? 1);
+ const gradientXId = `glint-x-${starIndex}`;
+ const gradientYId = `glint-y-${starIndex}`;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function LobbyBackground() {
+ return (
+
+ {CONSTELLATIONS.map((constellation, constellationIndex) => (
+
+
+
+ ))}
+
+ );
+}
+
+function RouteCard({
+ route,
+ isCTA = false,
+}: {
+ route: Route;
+ isCTA?: boolean;
+}) {
+ return (
+
+
+
+
+ {route.name}
+
+
+ {route.tag}
+
+
+
+ {route.durationMinutes === 0 ? "∞" : route.durationMinutes}
+
+ {route.durationMinutes === 0 ? "" : "min"}
+
+
+
+
+ {!isCTA && (
+
+ {route.description}
+
+ )}
+
+ {isCTA && (
+
+ {route.description}
+
+ )}
+
+
+ {isCTA ? "정거장 진입 (대기)" : "바로 출항"}
+
+
+ );
+}
export default function Home() {
const router = useRouter();
- const [isMount, setIsMount] = useState(false);
useEffect(() => {
- setIsMount(true);
const current = getCurrentVoyage();
- if (current && current.status === 'in_progress') {
- router.replace('/flight');
+ if (current && current.status === "in_progress") {
+ router.replace("/flight");
}
}, [router]);
- if (!isMount) return null;
+ const stationRoute = ROUTES[0];
+ const normalRoutes = ROUTES.slice(1);
return (
- {/* Background Layer */}
- {/* Content Layer */}
-
어느 별자리로 출항할까요?
+
+ 어느 별자리로 출항할까요?
+
몰입하기 좋은 궤도입니다.
-
- {ROUTES.map((route) => (
-
-
-
-
{route.name}
-
- {route.tag}
-
-
-
{route.durationMinutes}min
-
-
{route.description}
-
- 바로 출항
-
-
- ))}
-
+
+
+
+
-
- 정거장에서 3명이 대기 중
+
+ {normalRoutes.map((route) => (
+
+ ))}
+
diff --git a/src/components/FlightBackground.tsx b/src/components/FlightBackground.tsx
index 982ae69..28f88c5 100644
--- a/src/components/FlightBackground.tsx
+++ b/src/components/FlightBackground.tsx
@@ -2,7 +2,31 @@
import { useEffect, useRef } from 'react';
-export default function FlightBackground() {
+type FlightBackgroundProps = {
+ vanishYOffset?: number;
+ centerProtectRadius?: number;
+};
+
+type Star = {
+ wx: number;
+ wy: number;
+ z: number;
+ speed: number;
+ radius: number;
+ alpha: number;
+ tailLength: number;
+};
+
+const clamp = (value: number, min: number, max: number) =>
+ Math.min(max, Math.max(min, value));
+
+const randomInRange = (min: number, max: number) =>
+ min + Math.random() * (max - min);
+
+export default function FlightBackground({
+ vanishYOffset = -64,
+ centerProtectRadius = 190,
+}: FlightBackgroundProps) {
const canvasRef = useRef
(null);
useEffect(() => {
@@ -14,9 +38,12 @@ export default function FlightBackground() {
let width = window.innerWidth;
let height = window.innerHeight;
+ let animationFrameId = 0;
- // Check prefers-reduced-motion
- const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+ let prefersReduced = motionQuery.matches;
+
+ const vanishXJitter = (Math.random() < 0.5 ? -1 : 1) * randomInRange(10, 25);
const setSize = () => {
width = window.innerWidth;
@@ -24,103 +51,262 @@ export default function FlightBackground() {
canvas.width = width;
canvas.height = height;
};
+
+ const getStarCount = () => {
+ const isMobile = width < 768;
+ const min = isMobile ? 12 : 18;
+ const max = isMobile ? 30 : 45;
+ const byArea = Math.round((width * height) / 42000);
+ return clamp(byArea, min, max);
+ };
+
+ const getVanishingPoint = () => ({
+ x: width / 2 + vanishXJitter,
+ y: height / 2 + vanishYOffset,
+ });
+
+ const createSpeed = () => {
+ const tier = Math.random();
+
+ if (tier < 0.9) return randomInRange(0.003, 0.007);
+ if (tier < 0.99) return randomInRange(0.007, 0.011);
+ return randomInRange(0.011, 0.014);
+ };
+
+ const createVisuals = () => {
+ const highlight = Math.random() < 0.16;
+ const tailRoll = Math.random();
+ const tailLength =
+ tailRoll < 0.82
+ ? randomInRange(0, 2.5)
+ : tailRoll < 0.86
+ ? randomInRange(2.5, 3.8)
+ : randomInRange(4, 10);
+
+ return {
+ radius: highlight ? randomInRange(1.2, 1.8) : randomInRange(0.7, 1.2),
+ alpha: highlight ? randomInRange(0.55, 0.85) : randomInRange(0.25, 0.55),
+ tailLength,
+ };
+ };
+
+ const createSpawnRadius = () => {
+ const roll = Math.random();
+ const maxWideRadius = Math.min(Math.max(width, height) * 0.7, 360);
+ const ringOuter = Math.min(320, maxWideRadius);
+
+ if (roll < 0.08) {
+ return randomInRange(0, 60);
+ }
+
+ if (roll < 0.8) {
+ return randomInRange(80, Math.max(80, ringOuter));
+ }
+
+ return randomInRange(120, Math.max(120, maxWideRadius));
+ };
+
+ const createStar = (isRespawn: boolean): Star => {
+ const radius = createSpawnRadius();
+ const angle = randomInRange(0, Math.PI * 2);
+ const visuals = createVisuals();
+
+ return {
+ wx: Math.cos(angle) * radius,
+ wy: Math.sin(angle) * radius,
+ z: isRespawn ? randomInRange(0.9, 1.55) : randomInRange(0.55, 1.6),
+ speed: createSpeed(),
+ radius: visuals.radius,
+ alpha: visuals.alpha,
+ tailLength: visuals.tailLength,
+ };
+ };
+
setSize();
- window.addEventListener('resize', setSize);
+ let stars: Star[] = Array.from({ length: getStarCount() }, () => createStar(false));
- // Star configuration
- const starCount = 150;
- const stars: { x: number; y: number; z: number; speed: number }[] = [];
-
- // Target point (slightly above center)
- // We will move stars away from this point.
- // Instead of full 3D, let's do a 2D split flow.
- // Stars spawn randomly.
- // If x < center, move left-down.
- // If x > center, move right-down.
-
- // Initial Spawn
- for (let i = 0; i < starCount; i++) {
- stars.push({
- x: Math.random() * width,
- y: Math.random() * height,
- z: Math.random() * 2 + 0.5, // depth/size
- speed: Math.random() * 2 + 0.5
- });
- }
+ const screenDistanceFromCenter = (x: number, y: number) => {
+ const cx = width / 2;
+ const cy = height / 2;
+ return Math.hypot(x - cx, y - cy);
+ };
- let animationFrameId: number;
+ const applyCenterProtection = (x: number, y: number, alpha: number) => {
+ const distance = screenDistanceFromCenter(x, y);
- const render = () => {
- if (!ctx) return;
- ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; // Trails
+ if (distance >= centerProtectRadius) return alpha;
+
+ const ratio = clamp(distance / centerProtectRadius, 0, 1);
+ const attenuation = 0.35 + ratio * 0.65;
+ return alpha * attenuation;
+ };
+
+ const project = (star: Star, zValue: number) => {
+ const vp = getVanishingPoint();
+ return {
+ x: vp.x + star.wx / zValue,
+ y: vp.y + star.wy / zValue,
+ };
+ };
+
+ const recycleStar = (index: number) => {
+ stars[index] = createStar(true);
+ };
+
+ const drawStar = (
+ star: Star,
+ fromX: number,
+ fromY: number,
+ toX: number,
+ toY: number,
+ ) => {
+ const visibleAlpha = applyCenterProtection(toX, toY, star.alpha);
+ const dx = toX - fromX;
+ const dy = toY - fromY;
+ const movementLength = Math.hypot(dx, dy);
+
+ if (star.tailLength < 1 || movementLength < 0.001) {
+ ctx.globalAlpha = visibleAlpha;
+ ctx.fillStyle = '#f8fbff';
+ ctx.beginPath();
+ ctx.arc(toX, toY, star.radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.globalAlpha = 1;
+ return;
+ }
+
+ const dirX = dx / movementLength;
+ const dirY = dy / movementLength;
+ const tailX = toX - dirX * star.tailLength;
+ const tailY = toY - dirY * star.tailLength;
+
+ const gradient = ctx.createLinearGradient(tailX, tailY, toX, toY);
+ gradient.addColorStop(0, 'rgba(248, 251, 255, 0)');
+ gradient.addColorStop(1, `rgba(248, 251, 255, ${visibleAlpha})`);
+
+ ctx.strokeStyle = gradient;
+ ctx.lineWidth = clamp(star.radius * 0.9, 0.65, 1.6);
+ ctx.beginPath();
+ ctx.moveTo(tailX, tailY);
+ ctx.lineTo(toX, toY);
+ ctx.stroke();
+
+ ctx.globalAlpha = Math.min(1, visibleAlpha + 0.08);
+ ctx.fillStyle = '#f8fbff';
+ ctx.beginPath();
+ ctx.arc(toX, toY, clamp(star.radius * 0.72, 0.6, 1.45), 0, Math.PI * 2);
+ ctx.fill();
+ ctx.globalAlpha = 1;
+ };
+
+ const drawCenterVeil = () => {
+ const vp = getVanishingPoint();
+ const veil = ctx.createRadialGradient(
+ vp.x,
+ vp.y,
+ centerProtectRadius * 0.18,
+ vp.x,
+ vp.y,
+ centerProtectRadius * 1.35,
+ );
+ veil.addColorStop(0, 'rgba(160, 185, 235, 0.08)');
+ veil.addColorStop(0.55, 'rgba(90, 114, 170, 0.03)');
+ veil.addColorStop(1, 'rgba(0, 0, 0, 0)');
+
+ ctx.fillStyle = veil;
ctx.fillRect(0, 0, width, height);
- // ctx.clearRect(0, 0, width, height); // Clear
+ };
- const targetX = width / 2;
- // targetY is virtual, above screen.
- // Movement vector:
- // Left side: dx = -speed, dy = speed
- // Right side: dx = speed, dy = speed
+ const drawVignette = () => {
+ const vignette = ctx.createRadialGradient(
+ width / 2,
+ height / 2,
+ Math.min(width, height) * 0.25,
+ width / 2,
+ height / 2,
+ Math.max(width, height) * 0.95,
+ );
+ vignette.addColorStop(0, 'rgba(0, 0, 0, 0)');
+ vignette.addColorStop(1, 'rgba(0, 0, 0, 0.82)');
+ ctx.fillStyle = vignette;
+ ctx.fillRect(0, 0, width, height);
+ };
- ctx.fillStyle = '#ffffff';
+ const drawFrame = (moveStars: boolean) => {
+ ctx.fillStyle = 'rgba(2, 5, 10, 0.3)';
+ ctx.fillRect(0, 0, width, height);
- stars.forEach((star) => {
- // Reduced motion: very slow or static
- const moveFactor = prefersReduced ? 0.1 : 1.0;
+ drawCenterVeil();
- // Determine direction based on side
- const isLeft = star.x < targetX;
-
- // Simple perspective-ish flow
- // The further from center X, the faster dx
- // The further down, the faster dy
-
- // Let's stick to the PRD requirements:
- // Left -> Bottom-Left
- // Right -> Bottom-Right
-
- const dx = (isLeft ? -1 : 1) * star.speed * 0.5 * moveFactor;
- const dy = star.speed * moveFactor;
+ stars.forEach((star, index) => {
+ const from = project(star, star.z);
- star.x += dx;
- star.y += dy;
-
- // Reset if out of bounds
- if (star.y > height || star.x < 0 || star.x > width) {
- star.y = -10;
- star.x = Math.random() * width;
- // Bias respawn near center-top for "flow" effect?
- // Or just random top. Random top is safer for uniform coverage.
+ if (moveStars) {
+ star.z -= star.speed;
}
- const size = Math.max(0.5, star.z * (star.y / height) * 2);
- const opacity = Math.min(1, star.y / 200); // Fade in from top
+ const to = project(star, star.z);
- ctx.globalAlpha = opacity;
- ctx.beginPath();
- ctx.arc(star.x, star.y, size, 0, Math.PI * 2);
- ctx.fill();
- ctx.globalAlpha = 1.0;
+ const outOfBounds =
+ to.x < -50 || to.x > width + 50 || to.y < -50 || to.y > height + 50;
+
+ if (star.z <= 0.22 || outOfBounds) {
+ recycleStar(index);
+ return;
+ }
+
+ drawStar(star, from.x, from.y, to.x, to.y);
});
- // Draw subtle cockpit frame or vignette
- // Gradient vignette
- const gradient = ctx.createRadialGradient(width/2, height/2, height/3, width/2, height/2, height);
- gradient.addColorStop(0, 'transparent');
- gradient.addColorStop(1, 'rgba(0,0,0,0.8)');
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, width, height);
+ drawVignette();
+ };
+ const render = () => {
+ drawFrame(true);
animationFrameId = requestAnimationFrame(render);
};
- render();
+ const renderStatic = () => {
+ ctx.clearRect(0, 0, width, height);
+ drawFrame(false);
+ };
+
+ const handleResize = () => {
+ setSize();
+ stars = Array.from({ length: getStarCount() }, () => createStar(false));
+
+ if (prefersReduced) {
+ renderStatic();
+ }
+ };
+
+ const handleMotionChange = (event: MediaQueryListEvent) => {
+ prefersReduced = event.matches;
+
+ if (prefersReduced) {
+ cancelAnimationFrame(animationFrameId);
+ renderStatic();
+ return;
+ }
+
+ render();
+ };
+
+ window.addEventListener('resize', handleResize);
+ motionQuery.addEventListener('change', handleMotionChange);
+
+ if (prefersReduced) {
+ renderStatic();
+ } else {
+ render();
+ }
return () => {
- window.removeEventListener('resize', setSize);
+ window.removeEventListener('resize', handleResize);
+ motionQuery.removeEventListener('change', handleMotionChange);
cancelAnimationFrame(animationFrameId);
};
- }, []);
+ }, [vanishYOffset, centerProtectRadius]);
return ;
}
diff --git a/src/components/LobbyBackground.tsx b/src/components/LobbyBackground.tsx
index e665728..e5e3df8 100644
--- a/src/components/LobbyBackground.tsx
+++ b/src/components/LobbyBackground.tsx
@@ -1,57 +1,330 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
export default function LobbyBackground() {
+ const [isMount, setIsMount] = useState(false);
+
+ useEffect(() => {
+ setIsMount(true);
+ }, []);
+
+ // Helper for random delay/duration style
+ const getRandomStyle = () => ({
+ animationDelay: `${Math.random() * 5}s`,
+ animationDuration: `${2 + Math.random() * 4}s`,
+ });
+
+ if (!isMount) return ;
+
return (
- {/* Orion (Approximate) - Bottom Left */}
-
-