Skip to content

Commit

Permalink
feat: User Experience Optimization Code Merge about Drawing (#143)
Browse files Browse the repository at this point in the history
* feat: Add useCursor hook

* feat: Fix cursor offset and improve canvas edge interpolation

* feat: Change flood fill logic and fix lazy issue with fill operation on local

* feat: Add requestAnimationFrame about draw rendering

* feat: Delete requestAnimationFrame

* feat: Add drawSmoothLine method

* feat: Edge drawing re-improvement

* feat: Modified so that the lines are drawn roundly

* feat: Apply early return to flood fill

* feat: Merge if and else if

* feat: Move 'cursor correction' work file to handle folder

* feat: Recover accidentally deleted types

* feat: Save current state to change branch

* feat: Delete stroke interpolation
  • Loading branch information
dbjoung authored Dec 5, 2024
1 parent 18b942e commit 8b24e38
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 29 deletions.
12 changes: 12 additions & 0 deletions client/src/components/canvas/CanvasUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ interface ColorButton {

interface CanvasProps extends HTMLAttributes<HTMLDivElement> {
canvasRef: RefObject<HTMLCanvasElement>;
cursorCanvasRef: RefObject<HTMLCanvasElement>;
isDrawable: boolean;
colors: ColorButton[];
canUndo: boolean;
Expand All @@ -102,6 +103,7 @@ const Canvas = forwardRef<HTMLDivElement, CanvasProps>(
{
className,
canvasRef,
cursorCanvasRef,
isDrawable = true,
colors = [],
onUndo,
Expand Down Expand Up @@ -142,6 +144,16 @@ const Canvas = forwardRef<HTMLDivElement, CanvasProps>(
isDrawable ? 'touch-none' : 'pointer-events-none',
)}
aria-label={isDrawable ? '그림판' : '그림 보기'}
/>
<canvas
ref={cursorCanvasRef}
width={MAINCANVAS_RESOLUTION_WIDTH}
height={MAINCANVAS_RESOLUTION_HEIGHT}
className={cn(
'absolute left-0 top-0 h-full w-full cursor-none object-contain',
isDrawable ? 'touch-none' : 'pointer-events-none',
)}
aria-label={isDrawable ? '그림판' : '그림 보기'}
{...canvasEvents}
/>
{isDrawable && (
Expand Down
24 changes: 23 additions & 1 deletion client/src/components/canvas/GameCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,6 +51,7 @@ interface GameCanvasProps {
*/
const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomStatus, isHidden }: GameCanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const cursorCanvasRef = useRef<HTMLCanvasElement>(null);
const { convertCoordinate } = useCoordinateScale(MAINCANVAS_RESOLUTION_WIDTH, canvasRef);

const {
Expand Down Expand Up @@ -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);
Expand All @@ -130,6 +134,23 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
[continueDrawing, convertCoordinate, isConnected],
);

const handleDrawLeave = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement> | ReactTouchEvent<HTMLCanvasElement>) => {
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]);
Expand All @@ -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,
Expand All @@ -166,6 +187,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
return (
<Canvas
canvasRef={canvasRef}
cursorCanvasRef={cursorCanvasRef}
isDrawable={(role === 'PAINTER' || role === 'DEVIL') && roomStatus === 'DRAWING'}
isHidden={isHidden}
colors={colorsWithSelect}
Expand Down
20 changes: 20 additions & 0 deletions client/src/handlers/canvas/cursorInOutHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { RefObject } from 'react';
import { Point } from '@troublepainter/core';
import { getCanvasContext } from '@/utils/getCanvasContext';

const handleInCanvas = (canvasRef: RefObject<HTMLCanvasElement>, 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<HTMLCanvasElement>) => {
const { canvas, ctx } = getCanvasContext(canvasRef);
ctx.clearRect(0, 0, canvas.width, canvas.height);
};

export { handleInCanvas, handleOutCanvas };
76 changes: 48 additions & 28 deletions client/src/hooks/canvas/useDrawingOperation.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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입니다.
*
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 8b24e38

Please sign in to comment.