Canvas.tsx 5.19 KB
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import SocketContext from '../../contexts/SocketContext';
import { MessageType, RawMessage } from '../common/types';
import { BrushData, Vector } from './types';

// 참고 : https://basketdeveloper.tistory.com/79

interface CanvasProps {
  isDrawer: boolean;
}

const Canvas: React.FC<CanvasProps> = ({ isDrawer }) => {
  const socket = useContext(SocketContext);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  
  const [mousePosition, setMousePosition] = useState<Vector>({ x:0, y:0 });
  const [isPainting, setIsPainting] = useState(false);

  const getCoordinates = useCallback((event: MouseEvent): Vector | undefined => {
    if (!canvasRef.current) {
      return;
    } else {
      return {
        x: event.pageX - canvasRef.current.offsetLeft,
        y: event.pageY - canvasRef.current.offsetTop
      };
    }
  }, []);

  const drawLine = useCallback((prev: Vector, current: Vector) => {
    if (canvasRef.current) {
      const context = canvasRef.current!.getContext('2d');
      if (context) {
        context.strokeStyle = 'black';
        context.lineJoin = 'round';
        context.lineWidth = 5;

        context.beginPath();
        context.moveTo(prev.x, prev.y);
        context.lineTo(current.x, current.y);
        context.closePath();

        context.stroke();
      }
    }
  }, []);

  const clearCanvas = useCallback(() => {
    if (canvasRef.current) {
      const context = canvasRef.current.getContext('2d');
      if (context) {
        context.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
      }
    }
  }, []);

  const startPaint = useCallback((event: MouseEvent) => {
    const coordinates = getCoordinates(event);
    if (coordinates) {
      setIsPainting(true);
      setMousePosition(coordinates);

      const rawMessage: RawMessage = {
        type: MessageType.DRAW_MOVE,
        message: coordinates
      };
      socket.emit('msg', rawMessage, () => {});

      const nextRawMessage: RawMessage = {
        type: MessageType.DRAW_SET,
        message: {
          size: 5,
          color: '000000',
          drawing: true
        } as BrushData
      };
      socket.emit('msg', nextRawMessage, () => {});
    }
  }, []);

  const paint = useCallback(
    (event: MouseEvent) => {
      // 드래그 방지
      event.preventDefault();
      event.stopPropagation();

      if (isPainting) {
        const newMousePosition = getCoordinates(event);
        if (mousePosition && newMousePosition) {
          drawLine(mousePosition, newMousePosition);

          const rawMessage: RawMessage = {
            type: MessageType.DRAW_MOVE,
            message: newMousePosition
          };
          socket.emit('msg', rawMessage, () => {});

          setMousePosition(newMousePosition);
        }
      }
    },
    [isPainting, mousePosition]
  );

  const exitPaint = useCallback(() => {
    const rawMessage: RawMessage = {
      type: MessageType.DRAW_SET,
      message: {
        size: 5,
        color: '000000',
        drawing: false
      } as BrushData
    };
    socket.emit('msg', rawMessage, () => {});

    setIsPainting(false);
  }, []);

  const handleDrawSet = useCallback((rawMessage: RawMessage) => {
    if (rawMessage.type === MessageType.DRAW_SET) {
      const data = rawMessage.message as BrushData;
      setIsPainting(data.drawing);
    }
  }, []);

  const handleDrawMove = useCallback((rawMessage: RawMessage) => {
    if (rawMessage.type === MessageType.DRAW_MOVE) {
      const data = rawMessage.message as Vector;
      if (isPainting) {
        drawLine(mousePosition, data);
      }
      setMousePosition(data);
    }
  }, [isPainting, mousePosition])

  useEffect(() => {
    if (canvasRef.current) {
      if (isDrawer) {
        const canvas: HTMLCanvasElement = canvasRef.current;

        canvas.addEventListener('mousedown', startPaint);
        canvas.addEventListener('mousemove', paint);
        canvas.addEventListener('mouseup', exitPaint);
        canvas.addEventListener('mouseleave', exitPaint);
    
        return () => {
          canvas.removeEventListener('mousedown', startPaint);
          canvas.removeEventListener('mousemove', paint);
          canvas.removeEventListener('mouseup', exitPaint);
          canvas.removeEventListener('mouseleave', exitPaint);
        };
      }
    }
  }, [isDrawer, startPaint, paint, exitPaint]);

  useEffect(() => {
    if (!isDrawer) {
      socket.on('msg', handleDrawSet);
      socket.on('msg', handleDrawMove);

      return () => {
        socket.off('msg', handleDrawSet);
        socket.off('msg', handleDrawMove);
      }
    }
  }, [isDrawer, handleDrawMove]);

  const handleClearWhenStart = useCallback((rawMessage: RawMessage) => {
    if (rawMessage.type === MessageType.GAME_START) {
      clearCanvas();
      setIsPainting(false);
      // TODO: 펜 굵기, 색 설정하게 되면 여기에 초기화 넣기
    }
  }, []);

  useEffect(() => {
    socket.on('msg', handleClearWhenStart);
    return () => {
      socket.off('msg', handleClearWhenStart);
    }
  }, []);

  return (
    <div className='mx-3 px-2 py-1 rounded shadow'>
      <canvas ref={canvasRef} width='640' height='480' />
    </div>
  );
}

export default Canvas;