Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/entities/mini-game/coin-toss/model/drawVideoToCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function drawVideoFrameToCanvas(
video: HTMLVideoElement,
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
) {
const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const cropWidth = videoWidth;
const cropHeight = videoHeight;
const centerX = videoWidth * 0.5;
const centerY = videoHeight * 1;
const sx = Math.max(0, centerX - cropWidth / 2);
const sy = Math.max(0, centerY - cropHeight / 2);
const sw = Math.min(cropWidth, videoWidth - sx);
const sh = Math.min(cropHeight, videoHeight - sy);

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);
}
80 changes: 80 additions & 0 deletions src/entities/mini-game/coin-toss/model/useVideoCanvasPlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { useEffect, useRef } from 'react';
import { drawVideoFrameToCanvas } from './drawVideoToCanvas';

interface UseVideoCanvasPlayerProps {
videoSource: string;
isPlaying: boolean;
onAnimationEnd?: () => void;
}

export function useVideoCanvasPlayer({
videoSource,
isPlaying,
onAnimationEnd,
}: UseVideoCanvasPlayerProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationFrameId = useRef<number>();

useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;

let isMounted = true;
const ctx = canvas.getContext('2d');
if (!ctx) return;

const drawVideoFrames = () => {
if (!video || !canvas || !ctx) return;
if (video.readyState < 2) return;

drawVideoFrameToCanvas(video, canvas, ctx);

if (isMounted && isPlaying) {
animationFrameId.current = requestAnimationFrame(drawVideoFrames);
}
};

const drawInitialFrame = () => {
if (!video || !canvas || !ctx) return;
if (video.readyState < 2) return;

drawVideoFrameToCanvas(video, canvas, ctx);
};

const handleLoadedData = () => {
if (isPlaying) {
video.play();
drawVideoFrames();
} else {
drawInitialFrame();
}
};

const handleEnded = () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
onAnimationEnd?.();
};

video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('ended', handleEnded);

video.src = videoSource;
video.load();

return () => {
isMounted = false;
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('ended', handleEnded);
if (animationFrameId.current)
cancelAnimationFrame(animationFrameId.current);
};
}, [videoSource, isPlaying, onAnimationEnd]);

return { videoRef, canvasRef };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, RefObject, useRef } from 'react';

interface Args {
videoRef: RefObject<HTMLVideoElement>;
canvasRef: RefObject<HTMLCanvasElement>;
videoSource: string;
isPlaying: boolean;
onAnimationEnd?: () => void;
}

export function useVideoCanvasRenderer({
videoRef,
canvasRef,
videoSource,
isPlaying,
onAnimationEnd,
}: Args) {
const frameId = useRef<number>();

useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;

let mounted = true;
const ctx = canvas.getContext('2d');
if (!ctx) return;

const drawLoop = () => {
if (!mounted || !isPlaying) return;
if (video.readyState < 2) {
frameId.current = requestAnimationFrame(drawLoop);
return;
}

const vw = video.videoWidth;
const vh = video.videoHeight;
const cw = canvas.width;
const ch = canvas.height;

ctx.clearRect(0, 0, cw, ch);
ctx.drawImage(video, 0, vh - vh, vw, vh, 0, 0, cw, ch);

frameId.current = requestAnimationFrame(drawLoop);
};

const handleLoadedData = () => {
if (isPlaying) {
video.play();
drawLoop();
} else {
if (video.readyState >= 2) {
const vw = video.videoWidth;
const vh = video.videoHeight;
const cw = canvas.width;
const ch = canvas.height;

ctx.clearRect(0, 0, cw, ch);
ctx.drawImage(video, 0, vh - vh, vw, vh, 0, 0, cw, ch);
}
}
};

const handleEnded = () => {
if (frameId.current) cancelAnimationFrame(frameId.current);
onAnimationEnd?.();
};

video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('ended', handleEnded);

video.src = videoSource;
video.load();

return () => {
mounted = false;
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('ended', handleEnded);
if (frameId.current) cancelAnimationFrame(frameId.current);
};
}, [videoSource, isPlaying, onAnimationEnd, videoRef, canvasRef]);
}
97 changes: 7 additions & 90 deletions src/entities/mini-game/coin-toss/ui/CoinTossAnimation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client';

import { useEffect, useRef } from 'react';
import React from 'react';
import { cn } from '@/shared/utils/cn';
import { useVideoCanvasPlayer } from '../../model/useVideoCanvasPlayer';

interface CoinTossAnimationProps {
isPlaying: boolean;
Expand All @@ -14,94 +15,11 @@ const CoinTossAnimation = ({
videoSource,
onAnimationEnd,
}: CoinTossAnimationProps) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationFrameId = useRef<number>();

useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;

const ctx = canvas.getContext('2d');
if (!ctx) return;

const drawToCanvas = () => {
if (!video || !canvas || !ctx) return;

const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const zoom = 1;
const cropWidth = videoWidth / zoom;
const cropHeight = videoHeight / zoom;
const centerX = videoWidth * 0.5;
const centerY = videoHeight * 1;

const sx = Math.max(0, centerX - cropWidth / 2);
const sy = Math.max(0, centerY - cropHeight / 2);
const sw = Math.min(cropWidth, videoWidth - sx);
const sh = Math.min(cropHeight, videoHeight - sy);

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);

animationFrameId.current = requestAnimationFrame(drawToCanvas);
};

const drawInitialFrame = () => {
if (!video || !canvas || !ctx) return;

const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const zoom = 1;
const cropWidth = videoWidth / zoom;
const cropHeight = videoHeight / zoom;
const centerX = videoWidth * 0.5;
const centerY = videoHeight * 1;

const sx = Math.max(0, centerX - cropWidth / 2);
const sy = Math.max(0, centerY - cropHeight / 2);
const sw = Math.min(cropWidth, videoWidth - sx);
const sh = Math.min(cropHeight, videoHeight - sy);

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);
};

const handleEnded = () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
onAnimationEnd?.();
};

const handleLoadedData = () => {
if (isPlaying) {
video.play();
drawToCanvas();
} else {
drawInitialFrame();
}
};

video.addEventListener('ended', handleEnded);
video.addEventListener('loadeddata', handleLoadedData);

// ✅ 무조건 load 호출해서 첫 프레임도 준비되게
video.load();

return () => {
video.removeEventListener('ended', handleEnded);
video.removeEventListener('loadeddata', handleLoadedData);
cancelAnimationFrame(animationFrameId.current!);
};
}, [isPlaying, videoSource, onAnimationEnd]);
const { videoRef, canvasRef } = useVideoCanvasPlayer({
videoSource,
isPlaying,
onAnimationEnd,
});

return (
<div className={cn('relative', 'rounded-lg')}>
Expand All @@ -120,7 +38,6 @@ const CoinTossAnimation = ({
<video
ref={videoRef}
className="hidden"
src={videoSource}
muted
playsInline
preload="auto"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@ interface CoinTossButtonProps {
selectedSide: string | undefined;
setValue: UseFormSetValue<CoinTossForm>;
isPlaying: boolean;
isPending: boolean;
}

const CoinTossButton = ({
side,
selectedSide,
setValue,
isPlaying,
isPending,
}: CoinTossButtonProps) => {
const isSelected = selectedSide === side;

return (
<Button
onClick={() => setValue('bet', side)}
bg={!isSelected ? 'bg-black-800' : undefined}
border={!isSelected && !isPlaying ? 'border-white' : undefined}
disabled={isPlaying}
border={
!isSelected && !isPlaying && !isPending ? 'border-white' : undefined
}
disabled={isPlaying || isPending}
>
{side === 'FRONT' ? '앞면' : '뒷면'}
</Button>
Expand Down
6 changes: 5 additions & 1 deletion src/entities/mini-game/yavarwee/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export { default as YavarweeAnimation } from './ui/YavarweeAnimation';
export { default as YavarweeButton } from './ui/YavarweeButton';
export { default as Round } from './ui/Round';
export { IdleOverlay } from './ui/IdleOverlay';
export { RoundOverlay } from './ui/RoundOverlay';
export { SelectingTimerProgress } from './ui/SelectingTimerProgress';
export { StatusText } from './ui/StatusText';
export { useCanvasAnimation } from './model/useCanvasAnimation';
13 changes: 13 additions & 0 deletions src/entities/mini-game/yavarwee/model/drawBall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BallState } from '@/shared/types/mini-game/yavarwee';

export function drawBall(
ctx: CanvasRenderingContext2D,
ball: BallState,
ballImage: HTMLImageElement,
) {
ctx.save();
ctx.translate(ball.x, ball.y + 30);
ctx.scale(ball.scale, ball.scale);
ctx.drawImage(ballImage, -37.5, -37.5, 75, 75);
ctx.restore();
}
13 changes: 13 additions & 0 deletions src/entities/mini-game/yavarwee/model/drawCups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CupState } from '@/shared/types/mini-game/yavarwee';

export function drawCups(
ctx: CanvasRenderingContext2D,
cups: CupState[],
cupImage: HTMLImageElement,
) {
const sortedCups = [...cups].sort((a, b) => a.zIndex - b.zIndex);

sortedCups.forEach((cup) => {
ctx.drawImage(cupImage, cup.x - 105, cup.y - 105, 210, 210);
});
}
20 changes: 20 additions & 0 deletions src/entities/mini-game/yavarwee/model/getStatusText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GameState, Result } from '@/shared/types/mini-game/yavarwee';

export function getStatusText(gameState: GameState, result: Result) {
switch (gameState) {
case 'betting':
return '포인트를 배팅해주세요!';
case 'showing':
return '공의 위치를 확인하세요!';
case 'hiding':
return '공을 숨기는 중...';
case 'shuffling':
return '컵을 섞는 중...';
case 'selecting':
return '컵을 선택해주세요!';
case 'result':
return result === 'correct' ? '정답입니다!' : '틀렸습니다!';
default:
return '';
}
}
Loading