diff --git a/client/src/components/canvas/CanvasUI.tsx b/client/src/components/canvas/CanvasUI.tsx index 3647e8c05..1defcb44c 100644 --- a/client/src/components/canvas/CanvasUI.tsx +++ b/client/src/components/canvas/CanvasUI.tsx @@ -80,6 +80,7 @@ interface ColorButton { interface CanvasProps extends HTMLAttributes { canvasRef: RefObject; + cursorCanvasRef: RefObject; isDrawable: boolean; colors: ColorButton[]; canUndo: boolean; @@ -102,6 +103,7 @@ const Canvas = forwardRef( { className, canvasRef, + cursorCanvasRef, isDrawable = true, colors = [], onUndo, @@ -142,6 +144,16 @@ const Canvas = forwardRef( isDrawable ? 'touch-none' : 'pointer-events-none', )} aria-label={isDrawable ? '그림판' : '그림 보기'} + /> + {isDrawable && ( diff --git a/client/src/components/canvas/GameCanvas.tsx b/client/src/components/canvas/GameCanvas.tsx index e19b57de5..9988a0cdf 100644 --- a/client/src/components/canvas/GameCanvas.tsx +++ b/client/src/components/canvas/GameCanvas.tsx @@ -2,6 +2,7 @@ import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useCallba import { PlayerRole, RoomStatus } from '@troublepainter/core'; import { Canvas } from '@/components/canvas/CanvasUI'; import { COLORS_INFO, DEFAULT_MAX_PIXELS, MAINCANVAS_RESOLUTION_WIDTH } from '@/constants/canvasConstants'; +import { handleInCanvas, handleOutCanvas } from '@/handlers/canvas/cursorInOutHandler'; import { drawingSocketHandlers } from '@/handlers/socket/drawingSocket.handler'; import { gameSocketHandlers } from '@/handlers/socket/gameSocket.handler'; import { useDrawing } from '@/hooks/canvas/useDrawing'; @@ -50,6 +51,7 @@ interface GameCanvasProps { */ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomStatus, isHidden }: GameCanvasProps) => { const canvasRef = useRef(null); + const cursorCanvasRef = useRef(null); const { convertCoordinate } = useCoordinateScale(MAINCANVAS_RESOLUTION_WIDTH, canvasRef); const { @@ -122,6 +124,8 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt const point = getDrawPoint(e, canvas); const convertPoint = convertCoordinate(point); + handleInCanvas(cursorCanvasRef, convertPoint, brushSize); + const crdtDrawingData = continueDrawing(convertPoint); if (crdtDrawingData) { void drawingSocketHandlers.sendDrawing(crdtDrawingData); @@ -130,6 +134,23 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt [continueDrawing, convertCoordinate, isConnected], ); + const handleDrawLeave = useCallback( + (e: ReactMouseEvent | ReactTouchEvent) => { + const { canvas } = getCanvasContext(canvasRef); + const point = getDrawPoint(e, canvas); + const convertPoint = convertCoordinate(point); + + const crdtDrawingData = continueDrawing(convertPoint); + if (crdtDrawingData) { + void drawingSocketHandlers.sendDrawing(crdtDrawingData); + } + + handleOutCanvas(cursorCanvasRef); + stopDrawing(); + }, + [continueDrawing, handleOutCanvas, stopDrawing], + ); + const handleDrawEnd = useCallback(() => { stopDrawing(); }, [stopDrawing]); @@ -156,7 +177,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt onMouseDown: handleDrawStart, onMouseMove: handleDrawMove, onMouseUp: handleDrawEnd, - onMouseLeave: handleDrawEnd, + onMouseLeave: handleDrawLeave, onTouchStart: handleDrawStart, onTouchMove: handleDrawMove, onTouchEnd: handleDrawEnd, @@ -166,6 +187,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt return ( , point: Point, brushSize: number) => { + const { canvas, ctx } = getCanvasContext(canvasRef); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.arc(point.x, point.y, brushSize / 2, 0, 2 * Math.PI); + ctx.stroke(); +}; + +const handleOutCanvas = (canvasRef: RefObject) => { + const { canvas, ctx } = getCanvasContext(canvasRef); + ctx.clearRect(0, 0, canvas.width, canvas.height); +}; + +export { handleInCanvas, handleOutCanvas }; diff --git a/client/src/hooks/canvas/useDrawingOperation.ts b/client/src/hooks/canvas/useDrawingOperation.ts index 66fa87c81..1c77390a2 100644 --- a/client/src/hooks/canvas/useDrawingOperation.ts +++ b/client/src/hooks/canvas/useDrawingOperation.ts @@ -1,6 +1,7 @@ import { RefObject, useCallback } from 'react'; import { DrawingData, Point, StrokeStyle } from '@troublepainter/core'; import { useDrawingState } from './useDrawingState'; +import { MAINCANVAS_RESOLUTION_HEIGHT, MAINCANVAS_RESOLUTION_WIDTH } from '@/constants/canvasConstants'; import { RGBA } from '@/types/canvas.types'; import { getCanvasContext } from '@/utils/getCanvasContext'; import { hexToRGBA } from '@/utils/hexToRGBA'; @@ -12,15 +13,23 @@ const fillTargetColor = (pos: number, fillColor: RGBA, pixelArray: Uint8ClampedA pixelArray[pos + 3] = fillColor.a; }; -const checkColorisNotEqual = (pos: number, startColor: RGBA, pixelArray: Uint8ClampedArray) => { +const checkColorisEqual = (pos: number, startColor: RGBA, pixelArray: Uint8ClampedArray) => { return ( - pixelArray[pos] !== startColor.r || - pixelArray[pos + 1] !== startColor.g || - pixelArray[pos + 2] !== startColor.b || - pixelArray[pos + 3] !== startColor.a + pixelArray[pos] === startColor.r && + pixelArray[pos + 1] === startColor.g && + pixelArray[pos + 2] === startColor.b && + pixelArray[pos + 3] === startColor.a ); }; +/* +const checkOutsidePoint = (canvas: HTMLCanvasElement, point: Point) => { + const { width, height } = canvas.getBoundingClientRect(); + if (point.x >= 0 && point.x <= width && point.y >= 0 && point.y <= height) return false; + else return true; +}; +*/ + /** * 캔버스의 실제 드로잉 작업을 수행하는 Hook입니다. * @@ -105,11 +114,8 @@ export const useDrawingOperation = ( const activeStrokes = state.crdtRef.current.getActiveStrokes(); for (const { stroke } of activeStrokes) { - if (stroke.points.length > 2) { - applyFill(stroke); - } else { - drawStroke(stroke); - } + if (stroke.points.length > 2) applyFill(stroke); + else drawStroke(stroke); } }, [drawStroke]); @@ -148,26 +154,40 @@ export const useDrawingOperation = ( }; const pixelsToCheck = [[startX, startY]]; - let pixelCount = 0; - const filledPoints: Point[] = []; + const checkArray = new Array(MAINCANVAS_RESOLUTION_HEIGHT) + .fill(null) + .map(() => new Array(MAINCANVAS_RESOLUTION_WIDTH).fill(false)); + let pixelCount = 1; + const filledPoints: Point[] = [{ x: startX, y: startY }]; while (pixelsToCheck.length > 0 && pixelCount <= inkRemaining) { - const [x, y] = pixelsToCheck.shift()!; - const pos = (y * canvas.width + x) * 4; - - if ( - x < 0 || - x >= canvas.width || - y < 0 || - y >= canvas.height || - checkColorisNotEqual(pos, startColor, pixelArray) - ) - continue; - - fillTargetColor(pos, fillColor, pixelArray); - filledPoints.push({ x, y }); - pixelsToCheck.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]); - pixelCount++; + const [currentX, currentY] = pixelsToCheck.shift()!; + for (const move of [ + [1, 0], + [0, -1], + [-1, 0], + [0, 1], + ]) { + const [nextX, nextY] = [currentX + move[0], currentY + move[1]]; + if ( + nextX < 0 || + nextX >= MAINCANVAS_RESOLUTION_WIDTH || + nextY < 0 || + nextY >= MAINCANVAS_RESOLUTION_HEIGHT || + checkArray[nextY][nextX] + ) + continue; + + const nextArrayIndex = (nextY * MAINCANVAS_RESOLUTION_WIDTH + nextX) * 4; + + if (!checkColorisEqual(nextArrayIndex, startColor, pixelArray)) continue; + + checkArray[nextY][nextX] = true; + fillTargetColor(nextArrayIndex, fillColor, pixelArray); + pixelsToCheck.push([nextX, nextY]); + filledPoints.push({ x: nextX, y: nextY }); + pixelCount++; + } } ctx.putImageData(imageData, 0, 0);