Canvas API를 이용한 2D 그래픽 그리기

2025년 02월 13일

TOC

Javascript

웹에서 2D 그래픽을 그릴 수 있는 대표적인 방법으로 Canvas API가 있다. HTML5부터 표준에 포함된 Canvas는 <canvas>라는 HTML 태그를 사용하여 픽셀 단위의 2D 그래픽을 다룰 수 있게 해준다. 간단한 그림부터 복잡한 게임, 데이터 시각화, 애니메이션 구현까지 폭넓게 활용할 수 있다.

Canvas 기초

Canvas 요소 삽입

Canvas를 사용하기 위해서는 HTML 문서에 <canvas> 태그를 배치해야 한다. 이때 id, width, height 등의 속성을 설정할 수 있다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <title>Canvas 기초</title>
  <style>
    /* Canvas를 눈에 잘 보이도록 테두리를 추가 */
    #myCanvas {
      border: 1px solid #ccc;
      background-color: #f9f9f9;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas" width="600" height="400"></canvas>
  <script src="main.js"></script>
</body>
</html>
  • width, height: 캔버스의 해상도를 의미하며, 기본값은 300×150이다. CSS로 사이즈를 조절할 수도 있지만, 실제 픽셀 해상도를 관리하려면 widthheight 속성을 직접 지정하는 편이 좋다.
  • CSS에서 width, height를 변경하면 요소의 표시 크기는 달라지지만 내부 해상도가 달라지지 않으므로 그림이 왜곡될 수 있다.

렌더링 컨텍스트(Context)

JavaScript에서 Canvas를 다루기 위해서는 Rendering Context를 가져와야 한다. 2D 그래픽을 그릴 때는 "2d"라는 문자열로 지정한다.

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 여기서부터 추가적인 예제 코드 작성
// ctx(렌더링 컨텍스트)를 이용해 도형이나 텍스트, 이미지 등을 그릴 수 있다.
  • getContext('2d'): 2D 그리기용 컨텍스트 객체를 반환한다.
  • WebGL 같은 3D 그래픽을 사용하려면 getContext('webgl') 또는 getContext('experimental-webgl')을 사용할 수 있다.

기본 도형

2D 컨텍스트를 이용하면 선(line), 사각형(rect), 원(arc), 텍스트(text) 등을 손쉽게 그릴 수 있다. 마치 그림판(Paint)처럼 그려나가지만, 프로그래밍적으로 제어한다는 점이 다르다.

사각형(Rectangle)

Canvas API에는 사각형을 그리는 몇 가지 편의 메서드가 있다.

  1. fillRect(x, y, width, height): 사각형 내부를 색으로 채운다.
  2. strokeRect(x, y, width, height): 사각형 윤곽선만 그린다.
  3. clearRect(x, y, width, height): 지정된 영역을 지워 투명하게 만든다.
// 사각형 채우기
ctx.fillStyle = 'skyblue'; // 사각형을 채울 색상
ctx.fillRect(20, 20, 100, 100); // (x=20, y=20) 위치에 100×100 크기 사각형

// 사각형 윤곽선 그리기
ctx.strokeStyle = 'red'; // 윤곽선 색상
ctx.lineWidth = 2; // 선 굵기
ctx.strokeRect(150, 20, 100, 100);

// 사각형 부분 지우기
ctx.clearRect(30, 30, 20, 20);
// (30, 30) ~ (50, 50) 영역을 지워 투명하게 만든다.
canvas1

선(Line) 그리기

선, 경로, 도형 등을 세밀하게 그리려면 경로(Path) 개념을 알아야 한다.
beginPath()로 경로를 시작하고, moveTo(x, y)로 시작점을 옮긴 뒤 lineTo(x, y)로 직선을 긋는다. 모든 경로 명령을 마친 후 stroke()fill()을 호출하면 실제로 캔버스에 그려진다.

ctx.beginPath();
ctx.moveTo(50, 200);   // 시작점
ctx.lineTo(150, 250);  // 경로 1
ctx.lineTo(50, 300);   // 경로 2
ctx.closePath();       // 시작점과 마지막 점을 연결
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.stroke();
  • beginPath(): 새로운 경로를 시작한다(이전에 사용하던 경로가 있다면 초기화한다).
  • moveTo(): 펜을 특정 위치로 옮긴다(이 시점에서는 선이 그려지지 않는다).
  • lineTo(): 현재 펜 위치에서 지정 위치까지 선을 그린다.
  • stroke(): 윤곽선을 그리는 메서드다.
  • fill(): 내부를 채우는 메서드다.
canvas2

원(Arc) 그리기

원이나 호(arc)를 그리려면 arc(x, y, radius, startAngle, endAngle, counterclockwise?)를 사용한다. 각도라디안 값으로 전달해야 한다.

ctx.beginPath();
ctx.arc(300, 100, 50, 0, Math.PI * 2); // 360도 원
ctx.fillStyle = 'green';
ctx.fill();

// 호(arc)만 그려보기: 0 ~ 180도(π 라디안)
ctx.beginPath();
ctx.arc(450, 100, 50, 0, Math.PI);
ctx.strokeStyle = 'orange';
ctx.lineWidth = 4;
ctx.stroke();
  • arc(x, y, radius, startAngle, endAngle, counterclockwise?)

    • x, y: 원의 중심 좌표
    • radius: 반지름
    • startAngle, endAngle: 호를 그릴 시작 각도와 끝 각도(라디안)
    • counterclockwise: true면 반시계 방향, false면 시계 방향(기본값)
canvas3

Color, Gradient

Canvas에서는 다양한 방법으로 색상을 지정하고, 그라디언트나 패턴을 적용할 수 있다.

  • 색상 지정: fillStyle, strokeStyle
  • 그라디언트: 선형, 방사형
  • 패턴: 이미지를 반복해 도형 내부 채우기

색상 지정

그리기 연산 시 fillStylestrokeStyle을 통해 그릴 색을 지정할 수 있다. 색상은 문자열 형태로 다양한 표현 방식(RGB, RGBA, HEX, HSL 등)을 지원한다.

ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.strokeStyle = '#00ff00';

그라디언트(Gradient)

Canvas API는 선형(Linear)과 방사(Radial) 그라디언트를 지원한다. 예를 들어, 선형 그라디언트는 다음과 같이 생성할 수 있다.

const gradient = ctx.createLinearGradient(0, 0, 200, 0);
// 시작점(0,0) -> 끝점(200,0)에 걸쳐 색상 변화를 만든다.
gradient.addColorStop(0, 'blue');
gradient.addColorStop(0.5, 'yellow');
gradient.addColorStop(1, 'red');

ctx.fillStyle = gradient;
ctx.fillRect(20, 100, 200, 100);
  • createLinearGradient(x0, y0, x1, y1): 시작점과 끝점을 설정해 그라디언트 객체를 만든다.
  • addColorStop(offset, color): 0~1 사이의 offset 지점에 color를 배치한다.
canvas4

텍스트 그리기

Canvas 컨텍스트에 텍스트를 그릴 때는 fillText()strokeText()를 사용한다.

ctx.font = 'bold 24px sans-serif';
ctx.fillStyle = '#000';
ctx.fillText('Canvas Text Example', 20, 50);

// 윤곽선 텍스트
ctx.strokeStyle = 'blue';
ctx.lineWidth = 1;
ctx.strokeText('Stroke Text', 20, 80);
  • ctx.font: CSS 문법과 유사한 형태로 글꼴, 크기, 스타일 등을 지정한다.
  • fillText(text, x, y): 텍스트 내부를 채워 그린다.
  • strokeText(text, x, y): 텍스트 윤곽선을 그린다.
canvas5

이미지 그리기

외부 이미지를 불러와 캔버스에 그릴 수도 있다. 이미지는 비동기 로드이므로 img.onload 내에서 그려야 에러 없이 표시할 수 있다. 예전 포스팅에서 사용했던 이미지를 불러와 보았다.

const myImage = new Image();
myImage.src = 'https://raw.githubusercontent.com/yhuj79/blog-assets/refs/heads/main/240713/car/car-1.webp';
myImage.onload = function() {
  ctx.drawImage(myImage, 10, 10, 500, 300);
};
  • drawImage(image, dx, dy): 이미지를 (dx, dy) 위치에 원본 크기로 그린다.
  • drawImage(image, dx, dy, dWidth, dHeight): 지정된 크기로 축소하거나 확대해 그린다.
  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight): 원본 이미지의 특정 영역(sx, sy, sWidth, sHeight)을 잘라서 (dx, dy) 위치에 (dWidth, dHeight) 크기로 그린다.
canvas6

변형(Transformations)

Canvas에서는 translate, rotate, scale 등을 통해 좌표계 자체를 변형할 수 있다. 변형을 적용하면 그 이후에 그려지는 도형, 텍스트, 이미지 등이 모두 이동, 회전, 확대/축소되어 표현된다.

// 1) translate (평행 이동)
ctx.translate(300, 200);
// (300,200) 좌표가 새로운 (0,0)이 된다.

// 2) rotate (회전)
ctx.rotate((Math.PI / 180) * 45);
// 45도 회전 (라디안)

// 3) scale (배율)
ctx.scale(1.5, 0.5);
// x축은 1.5배, y축은 0.5배

ctx.fillStyle = 'violet';
ctx.fillRect(0, 0, 100, 100);

// 변형 복원을 위해서는 save(), restore()를 사용한다.
  • save(): 현재 컨텍스트 상태(변환, 스타일 등)를 스택에 저장한다.
  • restore(): 스택에서 마지막으로 저장한 컨텍스트 상태를 불러온다(변환 이전 상태로 되돌린다).
canvas7

애니메이션 만들기

Canvas는 매 프레임마다 화면을 지우고(clearRect), 새로운 상태를 그려나가는 방식으로 애니메이션을 구현한다.

requestAnimationFrame

애니메이션을 매끄럽게 구현하려면 setInterval 대신 requestAnimationFrame()을 사용하는 것이 일반적이다. requestAnimationFrame은 브라우저가 최적의 타이밍에 콜백 함수를 호출해주므로 CPU 사용률을 조절하며 부드러운 애니메이션을 가능하게 해준다.

let xPos = 0;

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 도형을 그리고 위치를 갱신한다.
  ctx.fillStyle = 'blue';
  ctx.fillRect(xPos, 100, 50, 50);

  xPos += 2;
  if (xPos > canvas.width) {
    xPos = -50; // 화면 밖으로 나가면 다시 시작하도록 한다.
  }

  requestAnimationFrame(animate); // 다음 프레임을 요청한다.
}

animate(); // 애니메이션을 시작한다.

주요 흐름

  1. 매 프레임마다 캔버스를 초기화(clearRect)한다.
  2. 물체의 위치 혹은 상태를 변경한다.
  3. 변경된 상태를 다시 그린다.
  4. requestAnimationFrame(animate)로 다음 프레임을 예약한다.
canvas8

고해상도(레티나 디스플레이) 대응

레티나(HiDPI) 디스플레이에서 캔버스를 선명하게 그리려면, CSS 크기와 width, height 속성을 다르게 설정해 픽셀 농도를 조정해야 한다.

예를 들어, 2배 해상도가 필요한 경우 다음과 같이 설정할 수 있다.

const dpr = window.devicePixelRatio || 1;
canvas.width = 600 * dpr;
canvas.height = 400 * dpr;
canvas.style.width = '600px';
canvas.style.height = '400px';

ctx.scale(dpr, dpr);
// 캔버스 스케일을 조정한다.

이렇게 하면 CSS 상의 크기는 600×400이지만 실제 픽셀 해상도는 1200×800이므로, 레티나 환경에서도 선명한 화면을 제공할 수 있다.

기타 관리 기능

  1. save() / restore()

    • 도형을 그리면서 스타일(색상, 선 두께)을 자주 바꾸거나 복잡한 변환을 여러 번 적용한다면, save(), restore()를 통해 컨텍스트 상태를 기억하고 복구할 수 있다.
  2. 캔버스 지우기

    • 매 프레임이나 필요할 때 전체 화면을 지울 때는 ctx.clearRect(0, 0, canvas.width, canvas.height)를 사용한다.
  3. 성능 최적화

    • 더블 버퍼링처럼 보조 캔버스를 활용하거나, 최소 영역만 재렌더링하는 기법을 사용하면 성능을 개선할 수 있다.
    • 복잡한 애니메이션은 CPU/GPU 사용량이 높으므로 필요하다면 WebGL로 전환하거나 GPU 가속이 가능한 라이브러리를 고려해볼 수 있다.
  4. 유틸 함수 작성

    • 예: drawCircle(x, y, r, color)처럼 자주 쓰이는 도형을 함수화하거나, 애니메이션 루프용 클래스(혹은 모듈)를 만들어 재사용성을 높이면 편리하다.

마무리 종합 예제

지금까지 살펴본 도형, 색상, 애니메이션을 간단히 결합한 예시 코드이다. Html로 작성하였다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8"/>
  <title>Canvas 종합 예제</title>
  <style>
    #canvasExample {
      border: 1px solid #999;
    }
  </style>
</head>
<body>
  <canvas id="canvasExample" width="600" height="400"></canvas>

  <script>
    const canvas = document.getElementById('canvasExample');
    const ctx = canvas.getContext('2d');

    let xPos = 50;
    let yPos = 50;
    let xSpeed = 2;
    let ySpeed = 2;

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

      // 사각형을 그린다.
      ctx.fillStyle = 'skyblue';
      ctx.fillRect(0, 0, canvas.width, canvas.height / 2);

      // 텍스트를 그린다.
      ctx.font = 'bold 20px sans-serif';
      ctx.fillStyle = '#fff';
      ctx.fillText('Canvas 2D Demo', 20, 40);

      // 원(움직이는 공)을 그린다.
      ctx.beginPath();
      ctx.arc(xPos, yPos, 20, 0, Math.PI * 2);
      ctx.fillStyle = 'red';
      ctx.fill();

      // 위치를 업데이트한다.
      xPos += xSpeed;
      yPos += ySpeed;

      // 범위를 벗어나면 속도 방향을 반전시킨다.
      if (xPos < 20 || xPos > canvas.width - 20) xSpeed *= -1;
      if (yPos < 20 || yPos > canvas.height - 20) ySpeed *= -1;

      requestAnimationFrame(draw);
    }

    draw();
  </script>
</body>
</html>
canvas9

텍스트가 그려져 있고, 화면의 절반은 파란색 사각형으로 채우고, 움직이는 원이 화면의 경계에 닿으면 방향을 반전시킨다.


Reference

Canvas API - Web API | MDN

HTML Standard

Fabric.js Javascript Library

Konva - JavaScript 2d canvas library

p5.js

Written by

yhuj79

🌱 Junior Developer