diff --git a/README.md b/README.md index bb71330..5036d57 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # ばーちゃる夏祭り frontend -https://virtual-natsumatsuri.web.app/ \ No newline at end of file +https://virtual-natsumatsuri.web.app/ + +## topazLink +https://topaz.dev/projects/4505516cee6f487ac360 \ No newline at end of file diff --git a/index.html b/index.html index 434434f..dc39bba 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,14 @@ - - + + - - - + + + VIRTUAL_NATSUMATSURI diff --git a/src/App.tsx b/src/App.tsx index 40406a9..0faef9c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ const AppRoutes = () => { } /> } /> - } /> + } /> } /> ); diff --git a/src/components/Gallery/index.module.css b/src/components/Gallery/index.module.css index b9df346..81a4051 100644 --- a/src/components/Gallery/index.module.css +++ b/src/components/Gallery/index.module.css @@ -1,61 +1,62 @@ -h1{ - font-size: 40px; +h1 { + font-size: 40px; } -.title{ - position: absolute; - top: 0; - left: 0; - z-index: 1; - padding: 16px; - margin: 0; -} -.nobori img{ - position: absolute; - bottom: 0; - left: -240px; - height: 80vh; - aspect-ratio: 1 / 1; - z-index: -1; +.title { + position: absolute; + top: 0; + left: 0; + z-index: 1; + padding: 16px; + margin: 0; + cursor: pointer; +} +.nobori img { + position: absolute; + bottom: 0; + left: -240px; + height: 80vh; + aspect-ratio: 1 / 1; + z-index: -1; } -.light img{ - position: absolute; - top: 0; - left: 0px; - height: 80vh; - aspect-ratio: 1 / 1; - z-index: -1; +.light img { + position: absolute; + top: 0; + left: 0px; + height: 80vh; + aspect-ratio: 1 / 1; + z-index: -1; } .container { - display: flex; - align-items: flex-end; - position: absolute; - bottom: 16px; - left: 4vw; - z-index: 1; + display: flex; + align-items: flex-end; + position: absolute; + bottom: 16px; + left: 4vw; + z-index: 1; } .logo img { - height: 16vh; - aspect-ratio: 1 / 1; - margin-right: 2vw; + height: 16vh; + aspect-ratio: 1 / 1; + margin-right: 2vw; } .shop { - color: var(--white); - font-size: 4vw; - padding: 16px; - margin: 0; + color: var(--white); + font-size: 4vw; + padding: 16px; + margin: 0; } -.light-right img{ - position: absolute; - top: 0; - right: 0px; - height: 80vh; - aspect-ratio: 1 / 1; - z-index: -1; - transform: scale(-1, 1); -} \ No newline at end of file +.light-right img { + position: absolute; + top: 0; + right: 0px; + height: 80vh; + aspect-ratio: 1 / 1; + z-index: -1; + transform: scale(-1, 1); +} diff --git a/src/components/Gallery/index.tsx b/src/components/Gallery/index.tsx index af3ee94..7db986c 100644 --- a/src/components/Gallery/index.tsx +++ b/src/components/Gallery/index.tsx @@ -1,6 +1,16 @@ +import { useNavigate } from "react-router-dom"; +import { useRoomIdStore } from "../../store"; import styles from "./index.module.css"; function Gallery() { + const navigate = useNavigate(); + const updateUUID = useRoomIdStore((state) => state.updateUUID); + + const handleClick = () => { + updateUUID(); + navigate("/"); + }; + return (
{/* biome-ignore lint/a11y/useMediaCaption: 夏祭りの音を再生します。 */} @@ -10,7 +20,15 @@ function Gallery() { loop aria-label="夏祭りの音" /> -

VIRTUAL_NATSUMATSURI

+

{ + handleClick(); + }} + onKeyDown={() => {}} + className={styles.title} + > + VIRTUAL_NATSUMATSURI +

夏祭り_のぼり
diff --git a/src/components/Yatai/TargetOverlay.module.css b/src/components/Yatai/TargetOverlay.module.css index c285a00..d22ce22 100644 --- a/src/components/Yatai/TargetOverlay.module.css +++ b/src/components/Yatai/TargetOverlay.module.css @@ -1,12 +1,13 @@ .target { - position: absolute; - z-index: 1; - width: 100px; - height: 100px; - transform: translate(calc(-50%+50px), calc(-50%+50px)); + position: absolute; + z-index: 1; + width: 100px; + height: 100px; + transform: translate(calc(-50%+50px), calc(-50%+50px)); + transition: all 0.1s linear; } .image { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } diff --git a/src/components/Yatai/TargetOverlay.tsx b/src/components/Yatai/TargetOverlay.tsx index f5ffe29..2598d34 100644 --- a/src/components/Yatai/TargetOverlay.tsx +++ b/src/components/Yatai/TargetOverlay.tsx @@ -1,10 +1,7 @@ import { useEffect, useState } from "react"; import { useSocketReceiver } from "../../hooks/useSocketReceiver"; -import { - type ActionSchema, - MessageType, - type Target, -} from "../../type/shooting"; +import type { ActionSchema, PointerSchema, Target } from "../../type/shooting"; +import { MessageType } from "../../type/shooting"; import styles from "./TargetOverlay.module.css"; export const TargetOverlay = () => { @@ -12,29 +9,21 @@ export const TargetOverlay = () => { useEffect(() => { onMessage((data) => { - // ここも本来はPointerSchemaになる - if (data.message_type === MessageType.Action) { - shotTarget(data); + if ( + data.message_type === MessageType.Action || + data.message_type === MessageType.Pointer + ) { + aimTarget(data); } }); }, [onMessage]); // TODO: これらは一人用,いつかマルチプレイヤー対応する const [aim, setAim] = useState(undefined); - // TODO: エイム照準の実装 - // const aimTarget = (data: PointerSchema) => { - // const x = window.innerWidth * data.target.x + window.innerWidth / 2; - // const y = window.innerHeight * data.target.y + window.innerHeight / 2; - // setAim({ x, y }); - // }; - - // const [target, setTarget] = useState(undefined); - const shotTarget = (data: ActionSchema) => { + const aimTarget = (data: PointerSchema | ActionSchema) => { const x = window.innerWidth / 2 + data.target.x * 1200; const y = window.innerHeight / 2 + data.target.y * 1200; - // TODO: エイム実装ができたらここのsetAimは削除する setAim({ x, y }); - // setTarget({ x, y }); }; return ( diff --git a/src/hooks/useSocketSender.ts b/src/hooks/useSocketSender.ts index ca6eb51..306690b 100644 --- a/src/hooks/useSocketSender.ts +++ b/src/hooks/useSocketSender.ts @@ -31,7 +31,6 @@ export const useSocketSender = () => { message_type: mes_type, event_type: event_type.shooter, }; - console.log(data); socketRef?.current?.send(JSON.stringify(data)); }, [socketRef], diff --git a/src/pages/result/index.module.css b/src/pages/result/index.module.css index ce483cf..1a6e17f 100644 --- a/src/pages/result/index.module.css +++ b/src/pages/result/index.module.css @@ -1,36 +1,36 @@ .result-text { - position: absolute; - left: 50%; - transform: translateX(-50%); - font-size: 40px; + position: absolute; + left: 50%; + transform: translateX(-50%); + font-size: 40px; + z-index: 1; } -.background-logo{ - opacity: 0.6; - position: absolute; - top: 40%; - left: 50%; - transform: translate(-50%, -50%); - justify-content: center; - align-items: center; - +.background-logo { + opacity: 0.6; + position: absolute; + top: 40%; + left: 50%; + transform: translate(-50%, -50%); + justify-content: center; + align-items: center; } .get-text { - position: absolute; - top: 60%; - left: 50%; - transform: translateX(-50%); - font-size: 40px; - white-space: nowrap; + position: absolute; + top: 60%; + left: 50%; + transform: translateX(-50%); + font-size: 40px; + white-space: nowrap; } -.share-btn{ - position: absolute; - top: 80%; - left: 50%; - transform: translateX(-50%); - white-space: nowrap; +.share-btn { + position: absolute; + top: 80%; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; } .replay-text { @@ -39,30 +39,29 @@ left: 50%; transform: translateX(-50%); white-space: nowrap; - font-family: 'Yuji Syuku', serif, sans-serif; + font-family: "Yuji Syuku", serif, sans-serif; line-height: 1.5; font-weight: 400; font-style: normal; } @keyframes rotate { - from { - transform: translate(-50%, -50%) rotate(0deg); - } - to { - transform: translate(-50%, -50%) rotate(360deg); - } + from { + transform: translate(-50%, -50%) rotate(0deg); + } + to { + transform: translate(-50%, -50%) rotate(360deg); + } } .get-image-container { - position: absolute; - top: 40%; - left: 50%; - transform: translate(-50%, -50%); - display: flex; - justify-content: center; - align-items: center; - z-index: 1; - animation: rotate 16s linear infinite; + position: absolute; + top: 40%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + animation: rotate 16s linear infinite; } - diff --git a/src/pages/result/index.tsx b/src/pages/result/index.tsx index 7c04145..e9e9266 100644 --- a/src/pages/result/index.tsx +++ b/src/pages/result/index.tsx @@ -1,12 +1,10 @@ import GetImage from "../../components/GetImage"; import { DefaultButton } from "../../components/ui/Button"; +import { useScoreStore } from "../../store/useScoreStore"; import styles from "./index.module.css"; -type ResultProps = { - score: number; -}; - -const Result = ({ score }: ResultProps) => { +const Result = () => { + const score = useScoreStore((state) => state.score); const image = score >= 0 && score <= 3 ? `/drink/bottle${score}.webp` diff --git a/src/pages/shooter/index.tsx b/src/pages/shooter/index.tsx index e19d598..7464ab6 100644 --- a/src/pages/shooter/index.tsx +++ b/src/pages/shooter/index.tsx @@ -1,25 +1,27 @@ -import { type KeyboardEventHandler, useEffect, useState } from "react"; +import { + type KeyboardEventHandler, + useCallback, + useEffect, + useState, +} from "react"; import { useNavigate } from "react-router-dom"; import { DefaultButton } from "../../components/ui/Button"; import { Modal } from "../../components/ui/Modal"; import { ShooterButton } from "../../components/ui/ShooterButton"; -import { useOrientation } from "../../hooks/useOrientation"; +import type { Orientation } from "../../hooks/useOrientation"; import { useSocketReceiver } from "../../hooks/useSocketReceiver"; import { useSocketSender } from "../../hooks/useSocketSender"; import { useUUIDStore } from "../../store"; import { useScoreStore } from "../../store/useScoreStore"; import { message_type } from "../../type/schema"; import { MessageType } from "../../type/shooting"; -import style from "./index.module.css"; +import styles from "./index.module.css"; const Shooter = () => { const [isOpen, setIsOpen] = useState(true); - const { orientationDiff } = useOrientation(); const { sendData } = useSocketSender(); const { onMessage } = useSocketReceiver(); - const uuid = useUUIDStore((state) => state.uuid); const navigate = useNavigate(); - const score = useScoreStore((state) => state.score); const addOneScore = useScoreStore((state) => state.addOneScore); const initialImages = [ @@ -28,55 +30,95 @@ const Shooter = () => { "/2D_material/cork.webp", ]; const [images, setImages] = useState(initialImages); + const uuid = useUUIDStore((state) => state.uuid); + const [initialOrientation, setInitialOrientation] = useState({ + alpha: 0, + beta: 0, + gamma: 0, + }); + const send = useCallback( + (event: DeviceOrientationEvent, msg_type: message_type) => { + if (!event.alpha || !event.beta || !event.gamma) { + return; + } + sendData(msg_type, uuid, { + alpha: initialOrientation + ? (event.gamma - initialOrientation.gamma) * 2 + : event.gamma, + beta: initialOrientation + ? event.beta - initialOrientation.beta + : event.beta, + }); + }, + [sendData, uuid, initialOrientation], + ); useEffect(() => { - let intervalId: number | null = null; - - intervalId = window.setInterval(() => { - sendData(message_type.status, uuid, orientationDiff); + const intervalId = setInterval(() => { + window.addEventListener( + "deviceorientation", + (event) => send(event, message_type.status), + { once: true }, + ); }, 100); - return () => clearInterval(intervalId); - }, [uuid, orientationDiff, sendData]); + return () => { + if (intervalId !== null) { + clearInterval(intervalId); + } + }; + }, [send]); + // biome-ignore lint/correctness/useExhaustiveDependencies: 初期化処理だけでいいため依存配列は空配列にしている useEffect(() => { onMessage((data) => { if (data.message_type === MessageType.Hit && data.id === uuid) { addOneScore(); } }); - }, [onMessage, uuid, addOneScore]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { if (images.length === 0) { - navigate("/result", { state: { score } }); + navigate("/result"); } - }, [images, navigate, score]); + }, [images, navigate]); - const handleClick = () => { + const handleClick = async () => { + window.addEventListener( + "deviceorientation", + (event) => send(event, message_type.action), + { once: true }, + ); const audio = new Audio("/sound/cork_sound.mp3"); - audio.play().catch((error) => { - console.error("オーディオの音が出なかった", error); - }); - sendData(message_type.action, uuid, orientationDiff); + audio + .play() + .then(() => {}) + .catch((error) => { + console.error("オーディオの音が出なかった", error); + }); setImages((prevImages) => prevImages.slice(1)); }; const handleKeyUp: KeyboardEventHandler = (event) => { if (event.key === "Enter" || event.key === " ") { - handleClick(); + // handleClick(); } }; return (
setIsOpen(false)}> - + -
+
-
+
{images.length > 0 ? ( images.map((src, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: @@ -92,34 +134,48 @@ const Shooter = () => { type ModalContentProps = { setIsOpen: (isOpen: boolean) => void; + setInitialOrientation: (orientation: Orientation) => void; }; -const ModalContent: React.FC = ({ setIsOpen }) => { - const { reset } = useOrientation(); +const ModalContent: React.FC = ({ + setIsOpen, + setInitialOrientation, +}) => { + const handleClick = () => { + window.addEventListener( + "deviceorientation", + (event) => { + setInitialOrientation({ + alpha: event.alpha || 0, + beta: event.beta || 0, + gamma: event.gamma || 0, + }); + }, + { once: true }, + ); + setIsOpen(false); + }; return ( -
+
スマホを画面に向かって垂直におく図 -
-

+

+

スマホを画面に向かって
垂直に机の上に置いてね

-
+
{ - reset(); - setIsOpen(false); - }} + onClick={handleClick} > 置いたよ! diff --git a/src/pages/yatai/index.tsx b/src/pages/yatai/index.tsx index 596ebe5..a92dd9d 100644 --- a/src/pages/yatai/index.tsx +++ b/src/pages/yatai/index.tsx @@ -1,9 +1,20 @@ +import { useEffect } from "react"; import Gallery from "../../components/Gallery"; import { TargetOverlay } from "../../components/Yatai/TargetOverlay"; import { YataiStage } from "../../components/Yatai/YataiStage"; +import { useTargetStatusStore } from "../../store"; import styles from "./index.module.css"; function Yatai() { + const resetTargetStatus = useTargetStatusStore( + (state) => state.resetTargetStatus, + ); + + // init target status + useEffect(() => { + resetTargetStatus(); + }, [resetTargetStatus]); + return (
diff --git a/src/store/useScoreStore.ts b/src/store/useScoreStore.ts index c493a24..baf99a2 100644 --- a/src/store/useScoreStore.ts +++ b/src/store/useScoreStore.ts @@ -1,6 +1,6 @@ -import create from "zustand"; +import { create } from "zustand"; -type Store = { +type State = { score: number; }; @@ -9,8 +9,14 @@ type Action = { addOneScore: () => void; }; -export const useScoreStore = create((set) => ({ +export const useScoreStore = create()((set) => ({ score: 0, setScore: (score) => set(() => ({ score: score })), - addOneScore: () => set((state) => ({ score: state.score + 1 })), + addOneScore: () => + set((state) => { + if (state.score > 3) { + return { score: 3 }; + } + return { score: state.score + 1 }; + }), })); diff --git a/src/store/useTargetStatusStore.ts b/src/store/useTargetStatusStore.ts index dcda4c1..96dc6e9 100644 --- a/src/store/useTargetStatusStore.ts +++ b/src/store/useTargetStatusStore.ts @@ -7,6 +7,7 @@ type State = { type Action = { updateTargetStatus: (index: number, status: TargetStatus) => void; + resetTargetStatus: () => void; }; export const useTargetStatusStore = create((set) => ({ @@ -18,4 +19,8 @@ export const useTargetStatusStore = create((set) => ({ targetStatus[index] = status; return { targetStatus }; }), + resetTargetStatus: () => + set(() => ({ + targetStatus: [TargetStatus.Live, TargetStatus.Live, TargetStatus.Live], + })), })); diff --git a/src/utils/copyClipBoard.ts b/src/utils/copyClipBoard.ts index 1652119..1a77e0c 100644 --- a/src/utils/copyClipBoard.ts +++ b/src/utils/copyClipBoard.ts @@ -1,8 +1,6 @@ export const copyStringToClipboard = (text: string) => { navigator.clipboard.writeText(text).then( - () => { - console.log("Async: Copying to clipboard was successful!"); - }, + () => {}, (err) => { console.error("Async: Could not copy text: ", err); },