본문 바로가기
React/실험실

[React] React와 Canvas를 사용해서 움직이는 공 만들기

by 잉여개발자 2022. 10. 20.

지금까지 리액트 환경을 구성하는 부분은 많이 작성했기 때문에 리액트 환경 구성을

모두 다 했다는 조건으로 글을 작성합니다. 

 

Canvas 만들기 

//GlobalStyle.tsx

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
    body {
        margin: 0;
    }
`;

export default GlobalStyle;

우선 거슬리는 body의 margin을 styled-components의 createGlobalStyle을 사용해서

없애준다. 

 

import GlobalStyle from './GlobalStyle';
import Home from './pages/Home';

interface Props {}

const App = ({}: Props) => {
  return (
    <>
      <GlobalStyle />
      <Home />
    </>
  );
};

export default App;

App.tsx에 GlobalStyle을 적용하고, 메인 작업을 하게 될 Home 컴포넌트를 하나 만들어준다.

 

// HomePresenter.tsx

import React from 'react';
import styled from 'styled-components';

interface Props {
  canvasRef: React.RefObject<HTMLCanvasElement>;
}

const HomePresenter = ({ canvasRef }: Props) => {
  return <Canvas ref={canvasRef} />;
};

const Canvas = styled.canvas`
  display: block;
  background: #eee;
`;

export default HomePresenter;

이제 캔퍼스를 styled-components를 사용해서 만들어서 넣어준다. 

 

ref는 document ~ 를 사용해서 캔퍼스를 가져오는 것이 아닌 useRef를 사용해서 가져오기 위해서 

적용했다. 바로 다음에 해당 부분을 설명할 것이다. 

 

// HomeContainer.tsx

import { useEffect, useRef } from 'react';

import HomePresenter from './Presenter';

interface Props {}

const HomeContainer = ({}: Props) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (canvasRef.current) {
      const canvas = canvasRef.current;
      const context = canvasRef.current.getContext('2d') as CanvasRenderingContext2D;

      // Canvas FullScreen
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
  	}
  }, [canvasRef]);

  return <HomePresenter canvasRef={canvasRef} />;
};

export default HomeContainer;

useRef를 사용해서 canvasRef를 만들었고, 그걸 HomePresenter에게 넘겨줬다. 

 

  const handleCanvasResize = () => {
    const canvas = canvasRef.current as HTMLCanvasElement;

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
  };

  useEffect(() => {
    if (canvasRef.current) {
      const canvas = canvasRef.current;
      const context = canvasRef.current.getContext('2d') as CanvasRenderingContext2D;

      // Canvas FullScreen
      window.addEventListener('resize', handleCanvasResize);
      handleCanvasResize();
    }
    
    return function () {
      window.removeEventListener('resize', handleCanvasResize);
    };
  }, [canvasRef]);

canvasRef의 current가 있다는 뜻은 렌더링이 끝난 이후라서 

canvas의 width와 height 값을 innerWidth, innerHeight를 사용해서 적용했다. 

 

이걸로 전체 화면을 만들었다. 

 

공 만들기 

 useEffect(() => {
    if (canvasRef.current) {
      const canvas = canvasRef.current;
      const context = canvasRef.current.getContext('2d') as CanvasRenderingContext2D;

      // Canvas FullScreen
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      
      let x = canvas.width / 2;
      let y = canvas.height - 30;

      function draw() {
        context.beginPath();
        context.arc(x, y, 20, 0, Math.PI * 2);
        context.fillStyle = 'green';
        context.fill();
        context.closePath();
      }

      setInterval(draw, 10);
    }
  }, [canvasRef]);

이제 공튀기기의 주인공인 공을 만들어보겠다. 

 

context.beginPath();
context.arc(x, y, 20, 0, Math.PI * 2);
context.fillStyle = 'green';
context.fill();
context.closePath();

arc는 원을 그리는 함수로, 6개의 매개변수가 있다. 

첫 번째와 두 번째는 각각 x축과 y축을 나타낸다. 

세 번째는 반지름을 나타내고 네 번째와 다섯 번째는 각각 시작 각도와 끝 각도를 나타낸다. 

마지막 없는 여섯 번째는 시계방향, 반시계방향을 boolean 값으로 나타낸다. 

 

fillStyle은 fill을 하기 전 색상을 넣기위해서 사용하고, fill은 fillStyle의 색으로 채워주는 함수이다. 

 

공 움직이게 하기 

  useEffect(() => {
    if (canvasRef.current) {
      const canvas = canvasRef.current;
      const context = canvasRef.current.getContext('2d') as CanvasRenderingContext2D;

      // Canvas FullScreen
      window.addEventListener('resize', handleCanvasResize);
      handleCanvasResize();
      
      let x = canvas.width / 2;
      let y = canvas.height - 30;

      const dx = 2;
      const dy = -2;

      function draw() {
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.beginPath();
        context.arc(x, y, 20, 0, Math.PI * 2);
        context.fillStyle = 'green';
        context.fill();
        context.closePath();

        x += dx;
        y += dy;
      }

      setInterval(draw, 10);
    }
    
    return function () {
      window.removeEventListener('resize', handleCanvasResize);
    };
  }, [canvasRef]);

dx와 dy는 각각 x와 y의 가중치를 나타낸다. 

이것을 setInterval을 통해서 반복하면서 x와 y값을 증가시킨다. 

 

그리고 clearRect를 사용하면 이전 Canvas를 지워주면서 새로운 화면이 그려지게 된다. 

이것을 사용하지 않으면 잔상?!이 남으면서 선이 되어버린다. 

 

매개변수는 4개가 있는데, 시작 x 축, y축과 끝 x축, y축을 나타낸다. 

위 코드는 전체 화면을 다시 그려주는 셈이다.

 

벽에 튕기게 하기 ( 충돌감지 )

  useEffect(() => {
    if (canvasRef.current) {
      // ...

      function drawBall() {
        context.beginPath();
        context.arc(x, y, radius, 0, Math.PI * 2);
        context.fillStyle = 'green';
        context.fill();
        context.closePath();
      }

      function draw() {
        context.clearRect(0, 0, canvas.width, canvas.height);

        if (x + dx > canvas.width - radius || x + dx < radius) {
          dx = -dx;
        }
        if (y + dy > canvas.height - radius || y + dy < radius) {
          dy = -dy;
        }

        drawBall();

        x += dx;
        y += dy;
      }

      setInterval(draw, 10);
    }

   // ...
  }, [canvasRef]);

공을 그리는 부분을 따로 drawBall로 빼내고, draw에서는 전체적인 환경을 컨트롤하는 

함수로 변경했다. 

 

만약 x축이 canvas의 width보다 크면 화면 밖으로 나간 것이고, 0보다 작으면 마찬가지로 

화면 밖으로 나간 것이다.

 

그리고 y축도 canvas의 width보다 크거나 0보다 작으면 화면 밖으로 나간 것이다. 

 

그런데, 0이 아닌 radius로 변경한 이유는 공의 중심이 기준이 아닌 반지금 끝이 기준이기 때문에 

기준 변경을 위해서 0이 아닌 radius를 기준으로 잡았다. 

 

반응형

댓글