feat: 일시정지 버튼을 눌렀을 때 별의 움직임이 함께 멈춤

This commit is contained in:
2026-02-14 02:19:34 +09:00
parent 6640962573
commit f32b7ee615
8 changed files with 67 additions and 162 deletions

View File

@@ -1,13 +1,16 @@
'use client';
import { useFlightSession } from '@/features/flight-session/model/useFlightSession';
import { FlightBackgroundWidget } from '@/widgets/flight-background';
import { FlightHudWidget } from '@/widgets/flight-hud';
export default function FlightPage() {
const session = useFlightSession();
return (
<div className="relative flex min-h-[calc(100vh-64px)] flex-1 flex-col items-center justify-center overflow-hidden p-6 text-white">
<FlightBackgroundWidget />
<FlightHudWidget />
<FlightBackgroundWidget isPaused={session.isPaused} />
<FlightHudWidget {...session} />
</div>
);
}

View File

@@ -16,11 +16,15 @@ import { getPrefersReducedMotionMediaQuery } from '@/shared/lib/motion/prefersRe
export function FlightStarfieldCanvas({
vanishYOffset = -68,
centerProtectRadius = 200,
isPaused = false,
}: {
vanishYOffset?: number;
centerProtectRadius?: number;
isPaused?: boolean;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const starsRef = useRef<FlightStar[]>([]);
const vanishXJitterRef = useRef<number | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
@@ -35,7 +39,10 @@ export function FlightStarfieldCanvas({
const motionQuery = getPrefersReducedMotionMediaQuery();
let prefersReducedMotion = motionQuery.matches;
const vanishXJitter = createFlightVanishXJitter();
if (vanishXJitterRef.current === null) {
vanishXJitterRef.current = createFlightVanishXJitter();
}
const vanishXJitter = vanishXJitterRef.current ?? 0;
const setCanvasSize = () => {
width = window.innerWidth;
@@ -58,7 +65,10 @@ export function FlightStarfieldCanvas({
);
setCanvasSize();
let stars: FlightStar[] = createStars();
if (starsRef.current.length === 0) {
starsRef.current = createStars();
}
let stars = starsRef.current;
const applyCenterProtection = (x: number, y: number, alpha: number) => {
const centerX = width / 2;
@@ -183,6 +193,7 @@ export function FlightStarfieldCanvas({
})
) {
stars[index] = createFlightStar({ width, height, isRespawn: true });
starsRef.current = stars;
return;
}
@@ -198,7 +209,18 @@ export function FlightStarfieldCanvas({
drawVignette();
};
const stopAnimation = () => {
if (!animationFrameId) return;
cancelAnimationFrame(animationFrameId);
animationFrameId = 0;
};
const render = () => {
if (prefersReducedMotion || isPaused) {
animationFrameId = 0;
return;
}
drawFrame(true);
animationFrameId = requestAnimationFrame(render);
};
@@ -211,8 +233,9 @@ export function FlightStarfieldCanvas({
const handleResize = () => {
setCanvasSize();
stars = createStars();
starsRef.current = stars;
if (prefersReducedMotion) {
if (prefersReducedMotion || isPaused) {
renderStatic();
}
};
@@ -221,18 +244,20 @@ export function FlightStarfieldCanvas({
prefersReducedMotion = event.matches;
if (prefersReducedMotion) {
cancelAnimationFrame(animationFrameId);
stopAnimation();
renderStatic();
return;
}
render();
if (!isPaused && !animationFrameId) {
render();
}
};
window.addEventListener('resize', handleResize);
motionQuery.addEventListener('change', handleMotionChange);
if (prefersReducedMotion) {
if (prefersReducedMotion || isPaused) {
renderStatic();
} else {
render();
@@ -241,9 +266,9 @@ export function FlightStarfieldCanvas({
return () => {
window.removeEventListener('resize', handleResize);
motionQuery.removeEventListener('change', handleMotionChange);
cancelAnimationFrame(animationFrameId);
stopAnimation();
};
}, [vanishYOffset, centerProtectRadius]);
}, [vanishYOffset, centerProtectRadius, isPaused]);
return <canvas ref={canvasRef} className="fixed inset-0 z-0 bg-black pointer-events-none" />;
}

View File

@@ -1,5 +1,11 @@
import { FlightStarfieldCanvas } from '@/features/flight-starfield';
export function FlightBackgroundWidget() {
return <FlightStarfieldCanvas vanishYOffset={-68} centerProtectRadius={200} />;
export function FlightBackgroundWidget({ isPaused }: { isPaused: boolean }) {
return (
<FlightStarfieldCanvas
vanishYOffset={-68}
centerProtectRadius={200}
isPaused={isPaused}
/>
);
}

View File

@@ -8,7 +8,6 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useFlightSession } from "@/features/flight-session/model/useFlightSession";
import { saveCurrentVoyage, saveToHistory } from "@/shared/lib/store";
import { Voyage, VoyageStatus } from "@/shared/types";
@@ -23,16 +22,24 @@ const statusOptions: { value: VoyageStatus; label: string; desc: string }[] = [
];
const FINISH_HOLD_MS = 1000;
export function FlightHudWidget() {
type FlightHudWidgetProps = {
voyage: Voyage | null;
isPaused: boolean;
formattedTime: string;
isCountdownCompleted: boolean;
handlePauseToggle: () => void;
handleFinish: () => Voyage | null;
};
export function FlightHudWidget({
voyage,
isPaused,
formattedTime,
isCountdownCompleted,
handlePauseToggle,
handleFinish,
}: FlightHudWidgetProps) {
const router = useRouter();
const {
voyage,
isPaused,
formattedTime,
isCountdownCompleted,
handlePauseToggle,
handleFinish,
} = useFlightSession();
const [isDebriefOpen, setIsDebriefOpen] = useState(false);
const [finishedVoyage, setFinishedVoyage] = useState<Voyage | null>(null);
const [status, setStatus] = useState<VoyageStatus | null>(null);