Skip to content

Commit

Permalink
feat: Add draw label and animation (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
romantech authored Feb 17, 2024
1 parent 58746f4 commit fba3546
Show file tree
Hide file tree
Showing 13 changed files with 76 additions and 30 deletions.
Binary file modified public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
"src": "/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
"purpose": "maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/fade.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function Fade({

return (
<Box
className={clsx(className, durationClasses[duration], { 'animate-fadein': triggerFade })}
className={clsx(className, durationClasses[duration], { 'animate-fade-in': triggerFade })}
{...boxProps}
>
{children}
Expand Down
25 changes: 25 additions & 0 deletions src/components/game/draw-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { clsx } from 'clsx';

import { Box } from '@/components';

interface DrawLabelProps {
isDraw: boolean;
className?: string;
}

export default function DrawLabel({ isDraw, className }: DrawLabelProps) {
return (
<Box
className={clsx(
'absolute left-1/2 z-10 -translate-x-1/2 bg-slate-800 px-10 py-6 text-3xl font-bold uppercase italic text-slate-200 transition-all duration-500',
{
'invisible opacity-0 scale-0 translate-y-full': !isDraw,
'visible opacity-100 scale-100': isDraw,
},
className,
)}
>
draw
</Box>
);
}
1 change: 1 addition & 0 deletions src/components/game/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as TurnIndicator } from './turn-indicator';
export { default as UndoStatus } from './undo-status';
export { default as OrderToggle } from './order-toggle';
export { default as WinnerLabel } from './winner-label';
export { default as DrawLabel } from './draw-label';
15 changes: 11 additions & 4 deletions src/components/game/square.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,21 @@ export default function Square({
...buttonProps
}: SquareProps) {
const buttonClasses = clsx(
'relative transition-all duration-500',
highlight && 'bg-slate-600',
'relative transition-all duration-300',
{ 'bg-slate-600': highlight },
className,
);
const markClasses = clsx('inline-block', dim && 'opacity-50');
const markClasses = clsx('inline-block', {
'animate-blink-2': highlight && !buttonProps.disabled,
'opacity-50': dim,
});
const sequenceClasses = clsx(
'absolute right-0 top-0 grid size-5 place-content-center text-xs font-medium transition-all duration-300',
[highlight ? 'text-slate-400' : 'text-slate-500', hideSequence && 'invisible opacity-0'],
{
'text-slate-400': highlight,
'text-slate-500': !highlight,
'invisible opacity-0': hideSequence,
},
);

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/game/winner-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function WinnerLabel({ target, winner, className }: WinnerLabelPr
<span
className={clsx(
'text-3xl font-bold uppercase italic transition-all duration-300 md:text-4xl',
{ 'invisible opacity-0': target !== winner, 'animate-win': target === winner },
{ 'invisible opacity-0': target !== winner, 'animate-rotate-1': target === winner },
className,
)}
>
Expand Down
19 changes: 7 additions & 12 deletions src/hooks/use-game-sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,32 @@ type SoundPath = (typeof soundPath)[number];
export const useGameSound = () => {
const isMuted = useIsMuted();
const audioRef = useRef<Map<SoundPath, HTMLAudioElement>>(new Map());
// const currentPlaying = useRef<HTMLAudioElement | null>(null);
const loaded = useRef(false);

// 오디오 파일을 미리 로드
useEffect(() => {
if (loaded.current) return;

const preload = (soundPath: SoundPath) => {
const audio = new Audio(soundPath);
const audio = new Audio(soundPath); // 오디오 파일 미리 로드
audioRef.current.set(soundPath, audio);
};

soundPath.forEach(preload);
loaded.current = true;
}, []);

const playSound = (soundPath: SoundPath) => {
const audio = audioRef.current.get(soundPath);
if (isMuted || !audio) return;

// // 오디오가 재생중이면
// if (currentPlaying.current?.paused === false) {
// currentPlaying.current.pause();
// currentPlaying.current.currentTime = 0;
// }

audio.play().catch((error) => console.error('Failed to play sound', error));
// .finally(() => (currentPlaying.current = audio));
};

const mark = (identifier: BasePlayer) => playSound(identifier === BasePlayer.X ? soundX : soundO);

const end = (isTied: boolean) => {
const end = (isDraw: boolean) => {
// 마지막 마크 사운드와 안겹치도록 딜레이 추가
setTimeout(() => playSound(isTied ? gameOverTie : gameOver), 350);
setTimeout(() => playSound(isDraw ? gameOverTie : gameOver), 350);
};

return { mark, end };
Expand Down
11 changes: 6 additions & 5 deletions src/hooks/use-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ export const useGame = ({
newBoard[boardIdx] = createSquare(identifier, mark, updatedSequence.length, color);
setBoard(newBoard);

const isTied = updatedSequence.length === newBoard.length;
const isDraw = updatedSequence.length === newBoard.length;
const winIndices = checkWin(newBoard, size, winCondition, boardIdx, identifier);
if (winIndices) winner.current = { identifier, indices: winIndices, mark };

if (winIndices || isTied) {
if (winIndices || isDraw) {
addHistory(createHistory(newBoard, winner.current, size));
playSound.end(isTied);
playSound.end(isDraw);
} else togglePlayer();
},
[addHistory, board, playSound, playerConfigs, size, togglePlayer, winCondition],
Expand Down Expand Up @@ -112,12 +112,12 @@ export const useGame = ({
}, [board, isSinglePlay, onBoardClick, size, winCondition]);

const hasMark = sequence.current.length > 0;
const isTied = sequence.current.length === board.length;
const isDraw = sequence.current.length === board.length;
const hasWinner = Boolean(winner.current.identifier);
const hasUndoCount = undoControls.getUndoCountBy(currentPlayer.current) > 0;

const enabledReset = hasMark || isUndoUsed;
const enableUndo = !hasWinner && hasMark && hasUndoCount && !isTied;
const enableUndo = !hasWinner && hasMark && hasUndoCount && !isDraw;

return {
board,
Expand All @@ -126,5 +126,6 @@ export const useGame = ({
controlStates: { undo: enableUndo, reset: enabledReset },
winner: winner.current,
undoCounts,
isDraw,
};
};
2 changes: 1 addition & 1 deletion src/pages/game-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function GameHistory() {
const { toggle: toggleShowOrder, isOpen: showOrder } = useDisclosure(true);

return (
<Fade className="text- m-auto grid size-full max-w-screen-xl grid-cols-[repeat(auto-fill,_minmax(288px,320px))] place-content-center gap-8">
<Fade className="m-auto grid size-full max-w-screen-xl grid-cols-[repeat(auto-fill,_minmax(288px,320px))] place-content-center gap-8">
<div className="col-span-full flex gap-3">
<Button className="px-3 capitalize" onClick={clearHistory}>
clear
Expand Down
16 changes: 13 additions & 3 deletions src/pages/game.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Board, Box, Fade, GameController, TurnIndicator, WinnerLabel } from '@/components';
import {
Board,
Box,
DrawLabel,
Fade,
GameController,
TurnIndicator,
WinnerLabel,
} from '@/components';
import { useGameOption } from '@/context';
import { useGame } from '@/hooks';
import { BasePlayer, BoardType, getBoardConfig } from '@/lib';

export default function Game() {
const options = useGameOption();
const { board, currentPlayer, handlers, controlStates, undoCounts, winner } = useGame(options);
const { board, currentPlayer, handlers, controlStates, undoCounts, winner, isDraw } =
useGame(options);

return (
<Fade className="m-auto flex size-full flex-col items-center justify-center gap-8">
Expand All @@ -24,10 +33,11 @@ export default function Game() {
className={getBoardConfig(options.size, BoardType.Play)}
winner={winner}
/>
<Box className="flex items-center gap-5 md:gap-8">
<Box className="relative flex items-center gap-5 md:gap-8">
<WinnerLabel target={BasePlayer.X} winner={winner.identifier} />
<TurnIndicator currentPlayer={currentPlayer} playerConfigs={options.playerConfigs} />
<WinnerLabel target={BasePlayer.O} winner={winner.identifier} />
<DrawLabel isDraw={isDraw} />
</Box>
</Fade>
);
Expand Down
6 changes: 4 additions & 2 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ export default {
secondary: '#9CFFD9',
},
animation: {
fadein: 'fadeIn .5s ease-in-out',
win: 'rotateOnce .5s ease-in-out',
'fade-in': 'fadeIn .5s ease-in-out',
'rotate-1': 'rotateOnce .5s ease-in-out',
'blink-2': 'blinkTwoTimes .5s ease-in-out 2',
},
keyframes: {
fadeIn: { from: { opacity: 0 }, to: { opacity: 1 } },
rotateOnce: {
from: { transform: 'rotate(60deg)' },
to: { transform: 'rotate(360deg)' },
},
blinkTwoTimes: { '0%, 100%': { opacity: 1 }, '50%': { opacity: 0 } },
},
},
},
Expand Down

0 comments on commit fba3546

Please sign in to comment.