트랙위를 달리는 자동차 만들기 (w/ canvas & requestAnimaitionFrame)

에구마·2024년 3월 4일

1. 캔버스

useCanvas 훅 만들기

생성을 또 하게될 수도 있으니까
useHover 훅 만들었던 것처럼 ref를 이용한다
캔버스를 생성하고 생성된 캔버스 요소의 ref를 반환하자

import { useEffect, useRef } from 'react';

interface UseCanvasProps {
  setCanvas: (canvas: HTMLCanvasElement) => void;
}
const useCanvas = ({ setCanvas }: UseCanvasProps) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

export default useCanvas;

이렇게 호출한다

const canvasRef = useCanvas({
    **setCanvas:** (canvas: HTMLCanvasElement) => {
      canvas.width = 200;
      canvas.height = 200;
      canvas.style.background = 'pink';
    },
  });


화면이 뿅 등장~

사용될 타입과 참조변수들을 먼저 만들어보자!

  • 자동차가 위치할 좌표에 대한 타입지정
    interface ICarCoord {
      x: number;
      y: number;
    }
  • 자동차 이미지에 대한 레퍼런스 참조변수를 만든다.
    const carImageRef = useRef<HTMLImageElement | **null**>(null);
    [(useRef) Cannot assign to 'current' because it is read-only property
    타입 선언으로 null을 줘야 위의 current 이슈 해결!
  • 자동차 좌표를 가질 참조변수를 만든다.
    const car1Ref = useRef<ICarCoord>({ x: 0, y: 0 });
    우선, 차 한대에 대해서만 생각해보자!! (추후엔 최대 8대가 되야함 !)


2. 자동차

자동차를 그려보자

이제 본격적으로 자동차를 불러오고 캔버스에서 움직이는 걸 구현한다!

자동차의 위치(서버로부터 받을 데이터에 의함)에 따라 변경될 값이니까 모두 useEffect 안에서 관리한다!!!

** 이 글에선 useEffect로 감싸지 않고 내부 코드만 작성해둘 것이다.

  • 이미지를 불러오자
if (car1) {
  const img = new Image();
  img.src = car1;
  carImageRef.current = img;
  car1Ref.current = { x: 20, y: 20 }; // 자동차 초기 좌표 배정
}
  • 애니메이션을 이용하기 위해 requestAnimationFrame을 사용해보자

    • requestAnimationFrame ??

      참고
      MDN
      inpa티스토리

      자동차 이동에 대한 구현도 css를 활용하여 transition, animation등으로도 가능하다. 하지만 실시간 기반의 이동이고 성능을 생각하여 최적화할 필요가 있다! requestAnimationFrame은 이럴 때 사용할 수 있는 애니메이션 관련 최적화 API라고 한다.

      setTimeout 처럼 재귀를 사용할 수 있다. 하지만 별도 타이머 지정은 필요 없다.

  const carImageRef = useRef<HTMLImageElement | null>(null);
  const car1Ref = useRef<ICarCoord>({ x: 0, y: 0 });

  const loadImage = useCallback(
    (src: string) =>
      new Promise<HTMLImageElement>((resolve) => {
        const img = new Image();
        img.src = src;
        img.sizes = '2';
        img.onload = () => resolve(img);
      }),
    []
  );

  useEffect(() => {
    loadImage(car1).then((img) => {
      carImageRef.current = img;
      car1Ref.current = { x: 20, y: 20 };
    });
    let rafTimer: number | undefined;

    const cvs = canvasRef.current;
    const ctx = cvs?.getContext('2d');
    if (!ctx) {
      return;
    }
    const animate = () => {
      const car = carImageRef.current;
      if (car) {
        ctx.drawImage(car, 100, 10); // 임의 지정 위치! 
      }
      rafTimer = requestAnimationFrame(animate);
    };
    requestAnimationFrame(animate);
  });

작소 자동차 등장!!!!

☄️ 불러온 자동차 이미지의 사이즈를 조정하고 싶다.

  • 현 상황 : 불러온 이미지 사이즈 변경이 적용되지 않는다. 애초에 조그마한 이미지를 불러와야 한다.
    • 문제: 불러온 이미지에 대해 따로 사이즈 조정을 어떻게 할까?
    • 해결 ! drawImage의 네번째, 다섯번째 인자로 사이즈를 조정한다.
        drawImage(image, dx, dy)
        drawImage(image, dx, dy, dWidth, dHeight)
        drawImage(image, sx, sy, sWidth, sHeight, dx, dy, **dWidth, dHeight)**

☄️ ctx.drawImage(car, 100, 10); 를 수정해서 위치를 업데이트하면, 자동차가 계속 쌓인다 ?!

→ 이전 작업을 지워줘야한다!

ctx?.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // 이전 값 삭제

자동차를 이동시키자

자동차 좌표 x,y 값을 바꾸어야 한다.

자동차의 x,y 좌표를 각각 30씩 이동시키는 함수를 만든다.

const updateCarCoord = useCallback(
  (carCoord: ICarCoord) => {
    carCoord.x += 30;
    carCoord.y += 30;

    blockOverflowPos(carCoord);
  },
  [blockOverflowPos]
);

바뀐 좌표가 canvas 영역을 벗어나는지 확인하는 함수도 적용한다.

const blockOverflowPos = useCallback((pos: ICarCoord) => {
    pos.x = pos.x >= 1100 ? 1000 : pos.x < 0 ? 0 : pos.x;
    pos.y = pos.y >= 450 ? 400 : pos.y < 0 ? 0 : pos.y;
  }, []);

이들을 사용해서 애니메이션을 그리는 useEffect 내부를 수정해본다.

**const coord = car1Ref.current; ///<<- 차 좌표에 대한 참조변수의 현재 값을 가져오고**

const animate = () => {
    const car = carImageRef.current;
    if (car) {
      **updateCarCoord(coord); // <<<- 그 좌표를 갱신한다.**
      ctx.drawImage(car, coord.x, coord.y); // 위치!px단위
    }
    rafTimer = requestAnimationFrame(animate); // <<<- 이로인해 그려짐!!
  };
  requestAnimationFrame(animate);

좌표가 바뀌어 차가 이동하는 것을 그리기 위해 테스트용 setInterval을 추가했다.

setInterval(() => {
    updateCarCoord(car1Ref.current);
  }, 5000);
  • requestAnimationFrame의 콜백으로 재귀호출한다

animate 코드를 보면

**const animate = () => {**
      const car = carImageRef.current;
      console.log('render..', coord);
      if (car) {
        // updateCarCoord(coord);
        ctx.drawImage(car, coord.x, coord.y); // 위치!px단위
      }
      **rafTimer = requestAnimationFrame(animate); ////////**
    };
    **requestAnimationFrame(animate);**

이렇게 animate를 재귀호출한다!!

그래서 콘솔에 위의 ‘render..’가 계~~속 찍힌다.

useEffect의 return으로 클린업하기

return () => {
    rafTimer && cancelAnimationFrame(rafTimer);
    rafTimer = undefined;
  };

useEffect 내의 코드를 정리(clean-up)하기 위함이다!

deps가 비어있다면 컴포넌트가 사라질 때 호출된다.

특정 값이 있다면, 그 값이 바뀌여서 이전 effect가 필요없을때! 즉 새로운값으로 바뀌면서 그 때 그전거를 청소

profile
코딩하는 고구마 🍠 Life begins at the end of your comfort zone

0개의 댓글