Skip to content

Commit

Permalink
feat: adding lobbies to the drawing game
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasgriffintn committed Dec 6, 2024
1 parent 7de34f2 commit 8bbb6c4
Show file tree
Hide file tree
Showing 8 changed files with 834 additions and 390 deletions.
540 changes: 350 additions & 190 deletions apps/multiplayer/src/multiplayer.ts

Large diffs are not rendered by default.

27 changes: 24 additions & 3 deletions apps/multiplayer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface GameState {
guess: string;
timestamp: number;
playerId: string;
playerName: string;
correct: boolean;
}>;
hasWon: boolean;
currentDrawer?: string;
Expand All @@ -35,10 +37,11 @@ export interface GameState {
type: 'success' | 'failure';
message: string;
};
isLobby: boolean;
}

// Request types
export interface JoinRequest {
gameId: string;
playerId: string;
playerName: string;
}
Expand All @@ -48,19 +51,21 @@ export interface LeaveRequest {
}

export interface StartGameRequest {
gameId: string;
playerId: string;
}

export interface GuessRequest {
gameId: string;
playerId: string;
guess: string;
}

export interface DrawingUpdateRequest {
drawingData: string;
gameId: string;
drawingData: any;
}

// Response types
export interface BaseResponse {
ok: boolean;
success: boolean;
Expand All @@ -76,3 +81,19 @@ export interface GameStateResponse extends BaseResponse {
score: number;
}>;
}

export interface Game {
name: string;
users: Map<string, { name: string; score: number }>;
gameState: GameState;
timerInterval: number | null;
lastAIGuessTime: number;
}

export interface GameListItem {
id: string;
name: string;
playerCount: number;
isLobby: boolean;
isActive: boolean;
}
3 changes: 0 additions & 3 deletions apps/web/components/AnyoneCanDraw/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { onGenerateDrawing } from '@/components/ChatInterface/actions';
export function AnyoneCanDraw() {
const [result, setResult] = useState<string | null>(null);
// TODO: make this dynamic
const gameId = 'everyone';
// TODO: make this dynamic
const playerId = 'anonymous';
const playerName = 'Anonymous';

Expand All @@ -29,7 +27,6 @@ export function AnyoneCanDraw() {
onSubmit={handleSubmit}
result={result}
gameMode={true}
gameId={gameId}
playerId={playerId}
playerName={playerName}
/>
Expand Down
190 changes: 104 additions & 86 deletions apps/web/components/DrawingCanvas/Components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { useState, useRef, useEffect } from 'react';

import { floodFill } from '../utils';
import { useEffect, useRef, RefObject } from 'react';

interface CanvasProps {
canvasRef: React.RefObject<HTMLCanvasElement>;
canvasRef: RefObject<HTMLCanvasElement>;
isFillMode: boolean;
currentColor: string;
lineWidth: number;
saveToHistory: () => void;
onDrawingComplete?: () => void;
onDrawingUpdate?: () => void;
updateInterval?: number;
onDrawingComplete: () => void;
isReadOnly?: boolean;
drawingData?: string;
}

export function Canvas({
Expand All @@ -20,118 +18,138 @@ export function Canvas({
lineWidth,
saveToHistory,
onDrawingComplete,
onDrawingUpdate,
updateInterval = 2000,
isReadOnly = false,
drawingData,
}: CanvasProps) {
const [isDrawing, setIsDrawing] = useState(false);
const [lastX, setLastX] = useState(0);
const [lastY, setLastY] = useState(0);
const lastUpdateRef = useRef<number>(0);
const updateIntervalRef = useRef<NodeJS.Timeout>();

const startUpdateInterval = () => {
if (onDrawingUpdate) {
updateIntervalRef.current = setInterval(() => {
onDrawingUpdate();
}, updateInterval);
}
};
const isDrawing = useRef(false);
const lastX = useRef(0);
const lastY = useRef(0);

const clearUpdateInterval = () => {
if (updateIntervalRef.current) {
clearInterval(updateIntervalRef.current);
updateIntervalRef.current = undefined;
}
};
useEffect(() => {
if (!drawingData || !canvasRef.current) return;

const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const image = new Image();
image.onload = () => {
const ctx = canvasRef.current?.getContext('2d');
if (!ctx) return;

const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(image, 0, 0);
};
image.src = drawingData;
}, [drawingData]);

const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const draw = (e: MouseEvent | TouchEvent) => {
if (!isDrawing.current || !canvasRef.current) return;

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

const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const rect = canvasRef.current.getBoundingClientRect();
const scaleX = canvasRef.current.width / rect.width;
const scaleY = canvasRef.current.height / rect.height;

const x =
(('touches' in e ? e.touches[0].clientX : e.clientX) - rect.left) *
scaleX;
const y =
(('touches' in e ? e.touches[0].clientY : e.clientY) - rect.top) * scaleY;

ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.moveTo(lastX.current, lastY.current);
ctx.lineTo(x, y);
ctx.strokeStyle = currentColor;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();

setLastX(x);
setLastY(y);
lastX.current = x;
lastY.current = y;
};

const now = Date.now();
if (now - lastUpdateRef.current >= updateInterval) {
onDrawingUpdate?.();
lastUpdateRef.current = now;
}
const fill = () => {
if (!canvasRef.current) return;
const ctx = canvasRef.current.getContext('2d');
if (!ctx) return;

ctx.fillStyle = currentColor;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
saveToHistory();
onDrawingComplete();
};

const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
const startDrawing = (e: React.MouseEvent | React.TouchEvent) => {
if (isReadOnly) return;

const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
if (!canvas) return;

isDrawing.current = true;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;

if (isFillMode) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
floodFill(imageData, Math.round(x), Math.round(y), currentColor);
ctx.putImageData(imageData, 0, 0);
saveToHistory();
onDrawingUpdate?.();
return;
if ('touches' in e) {
lastX.current = (e.touches[0].clientX - rect.left) * scaleX;
lastY.current = (e.touches[0].clientY - rect.top) * scaleY;
} else {
lastX.current = (e.clientX - rect.left) * scaleX;
lastY.current = (e.clientY - rect.top) * scaleY;
}

setIsDrawing(true);
setLastX(x);
setLastY(y);
lastUpdateRef.current = Date.now();
startUpdateInterval();

ctx.beginPath();
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = currentColor;
ctx.moveTo(x, y);
if (isFillMode) {
fill();
isDrawing.current = false;
}
};

const handleMouseUp = () => {
setIsDrawing(false);
clearUpdateInterval();
saveToHistory();
onDrawingComplete?.();
const stopDrawing = () => {
if (isDrawing.current) {
isDrawing.current = false;
saveToHistory();
onDrawingComplete();
}
};

useEffect(() => {
return () => {
clearUpdateInterval();
};
}, []);
const canvas = canvasRef.current;
if (!canvas) return;

if (!isReadOnly) {
const handleMouseMove = (e: MouseEvent) => draw(e);
const handleTouchMove = (e: TouchEvent) => {
e.preventDefault();
draw(e);
};

canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('touchmove', handleTouchMove);

return () => {
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('touchmove', handleTouchMove);
};
}
}, [isReadOnly, currentColor, lineWidth, isFillMode]);

return (
<div className="relative w-full aspect-square">
<>
<span className="sr-only">
Drawing Canvas {isReadOnly ? 'ReadOnly' : 'Editable'}
</span>
<canvas
ref={canvasRef}
width={800}
height={800}
className="absolute top-0 left-0 w-full h-full border border-gray-200 rounded-lg cursor-crosshair bg-[#f9fafb] shadow-sm"
height={650}
className={`bg-[#f9fafb] top-0 left-0 w-full h-full max-h-[650px] border border-gray-200 rounded-lg touch-none ${
isReadOnly ? 'cursor-default' : 'cursor-crosshair'
}`}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onMouseUp={stopDrawing}
onMouseOut={stopDrawing}
onTouchStart={startDrawing}
onTouchEnd={stopDrawing}
/>
</div>
</>
);
}
Loading

0 comments on commit 8bbb6c4

Please sign in to comment.