diff --git a/public/assets/intro.png b/public/assets/intro.png new file mode 100644 index 0000000..454a61a Binary files /dev/null and b/public/assets/intro.png differ diff --git a/src/app/globals.css b/src/app/globals.css index a1db609..3f1ab78 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -230,3 +230,37 @@ body { animation: tut-pulse 1.1s ease-in-out infinite; border-radius: 14px; } + +/* ── 인트로: 구름 걷힘 연출 ── */ +.intro-cloud { + position: absolute; + top: -14%; + bottom: -14%; + width: 68%; + transition: transform 1.9s cubic-bezier(0.65, 0.03, 0.35, 1) 0.25s; +} +.intro-cloud-left { + left: -6%; +} +.intro-cloud-right { + right: -6%; +} +.intro-leaving .intro-cloud-left { + transform: translateX(-120%); +} +.intro-leaving .intro-cloud-right { + transform: translateX(120%); +} +.cloud-puff { + position: absolute; + border-radius: 9999px; + background: #fff; + filter: blur(16px); +} +@keyframes bounce-slow { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-7px); } +} +.animate-bounce-slow { + animation: bounce-slow 1.6s ease-in-out infinite; +} diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 1494aa2..d4d1f92 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -7,6 +7,7 @@ import DbStatus from "@/components/DbStatus"; import NicknameModal from "@/components/game/NicknameModal"; import ChatTicker from "@/components/game/ChatTicker"; import ChatPanel from "@/components/game/ChatPanel"; +import IntroScreen from "@/components/game/IntroScreen"; import { engine, effectiveRate, @@ -119,6 +120,7 @@ type Panel = export default function Game() { const { state, dispatch } = useGame(); const [panel, setPanel] = useState(null); + const [showIntro, setShowIntro] = useState(true); const [candidates, setCandidates] = useState(null); const [placing, setPlacing] = useState(null); const [message, setMessage] = useState(null); @@ -211,6 +213,8 @@ export default function Game() { return (
+ {/* ── 인트로 (시작하기 → 구름 걷힘) ── */} + {showIntro && setShowIntro(false)} />} { if (id === "hall") setPanel({ type: "hall" }); diff --git a/src/components/game/IntroScreen.tsx b/src/components/game/IntroScreen.tsx new file mode 100644 index 0000000..8907671 --- /dev/null +++ b/src/components/game/IntroScreen.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useState } from "react"; + +const LEAVE_MS = 2300; // 연출 총 길이 (인트로 페이드 + 구름 걷힘) + +// 구름 안쪽 가장자리의 뭉게 퍼프 (top%, size(vh), edgeOffset(%)) +const PUFFS: Array<[number, number, number]> = [ + [-4, 26, -8], + [12, 34, -3], + [30, 28, -10], + [46, 38, -2], + [63, 30, -9], + [78, 36, -4], + [92, 27, -8], +]; + +function CloudHalf({ side }: { side: "left" | "right" }) { + return ( +
+ {/* 구름 본체 */} +
+ {/* 안쪽 가장자리 뭉게뭉게 */} + {PUFFS.map(([top, size, off], i) => ( +
+ ))} +
+ ); +} + +/** 시작 화면: 타이틀 일러스트 + 시작하기 → 구름이 걷히며 마을 진입 */ +export default function IntroScreen({ onDone }: { onDone: () => void }) { + const [leaving, setLeaving] = useState(false); + + const start = () => { + if (leaving) return; + setLeaving(true); + setTimeout(onDone, LEAVE_MS); + }; + + return ( +
+ {/* 구름 레이어 (인트로 아래) — 인트로가 투명해지면 드러나고, 양옆으로 걷힘 */} + + + + {/* 타이틀 일러스트 */} + {/* eslint-disable-next-line @next/next/no-img-element */} + 고양이 마을 + + {/* 시작하기 버튼 */} +
+ +
+
+ ); +}