Animating our Grid | kirupa


Canvas Grid 애니메이션 구현 가이드

개요

이 문서는 Canvas 2D API를 사용하여 정적인 그리드를 부드럽게 움직이는 애니메이션 그리드로 변환하는 방법을 다룹니다. DPI 인식과 프레임 레이트 일관성을 유지하면서 전문적인 품질의 애니메이션을 구현하는 과정을 단계별로 설명합니다.

시작점 코드 구조

기본 HTML 구조

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>A Perfect Fullscreen Grid</title>
  
  <style>
    #myCanvas {
      outline: 2px solid #333;
      width: 100vw;
      height: 100vh;
    }
    
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 0;
      margin: 0;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas" width="500" height="500"> </canvas>
  <!-- JavaScript 코드 영역 -->
</body>
</html>

핵심 기능: DPI 처리 함수

function accountForDPI() {
  // 현재 디바이스 픽셀 비율 가져오기
  const dpr = window.devicePixelRatio || 1;
  
  // CSS에서 캔버스 크기 가져오기
  const rect = myCanvas.getBoundingClientRect();
  
  // DPI에 맞춰 캔버스 내부 치수 설정
  myCanvas.width = rect.width * dpr;
  myCanvas.height = rect.height * dpr;
  
  // DPI를 고려하여 모든 캔버스 연산 스케일링
  ctx.scale(dpr, dpr);
  
  // 캔버스 표시 크기 재설정
  myCanvas.style.width = `${rect.width}px`;
  myCanvas.style.height = `${rect.height}px`;
}

기본 그리드 그리기 함수

function drawGrid(lineWidth, cellWidth, cellHeight, color) {
  // 선 속성 설정
  ctx.strokeStyle = color;
  ctx.lineWidth = lineWidth;
  
  // 크기 가져오기
  let width = myCanvas.width;
  let height = myCanvas.height;
  
  // 수직선 그리기
  for (let x = 0; x <= width; x += cellWidth) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, height);
    ctx.stroke();
  }
  
  // 수평선 그리기
  for (let y = 0; y <= height; y += cellHeight) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(width, y);
    ctx.stroke();
  }
}

애니메이션 구현 과정

1단계: 애니메이션 루프 추가

중요: RequestAnimationFrame 사용

// 애니메이션 루프
function animate(currentTime) {
  drawGrid(1, 20, 20, "#000");
  
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

실용적 팁

  • requestAnimationFrame(animate)로 초기 호출하여 currentTime 인자가 처음부터 올바르게 전달되도록 함
  • 직접 animate()를 호출하는 것보다 타임스탬프 처리에 안전함

2단계: 그리드 이동 로직 구현

오프셋 변수 선언

let offset = 0;
let animationSpeed = 1;

애니메이션 함수 수정

function animate(currentTime) {
    offset += animationSpeed;
    
    drawGrid(1, 20, 20, "#000");
    
    requestAnimationFrame(animate);
}

핵심: 수평선 그리기 로직 변경

// 기존 코드
for (let y = 0; y <= height; y += cellHeight) {

// 변경된 코드  
for (let y = offset; y <= height; y += cellHeight) {

필수: 화면 클리어 추가

function animate(currentTime) {
  offset += animationSpeed;
  ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);

  drawGrid(1, 20, 20, "#000");

  requestAnimationFrame(animate);
}

3단계: 오프셋 초기화 문제 해결

주의사항: 첫 번째 접근법 (권장하지 않음)

if (offset > cellHeight) {
    offset = 0;
}

이 방법은 애니메이션 속도나 셀 높이 변경 시 불안정할 수 있음

권장: 개선된 접근법

function drawGrid(lineWidth, cellWidth, cellHeight, color) {
  // 선 속성 설정
  ctx.strokeStyle = color;
  ctx.lineWidth = lineWidth;

  // 크기 가져오기
  let width = myCanvas.width;
  let height = myCanvas.height;

  // 수직선 그리기
  for (let x = 0; x <= width; x += cellWidth) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, height);
    ctx.stroke();
  }

  // **개선된 수평선 그리기**
  let startCondition = -cellHeight + (offset % cellHeight);
  let endCondition = height + cellHeight;
  
  for (let y = startCondition; y <= endCondition; y += cellHeight) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(width, y);
    ctx.stroke();
  }
}

핵심 개념

  • startCondition: 그리기 영역을 셀 높이만큼 위로 확장
  • endCondition: 그리기 영역을 셀 높이만큼 아래로 확장
  • offset % cellHeight: 모듈로 연산으로 자동 순환 패턴 생성

4단계: 일관된 애니메이션 속도 보장

중요: Delta Time 변수 설정

let frames_per_second = 60;
let previousTime = performance.now();

let frame_interval = 1000 / frames_per_second;
let delta_time_multiplier = 1;
let delta_time = 0;

Delta Time 계산 로직

function animate(currentTime) {
  delta_time = currentTime - previousTime;
  delta_time_multiplier = delta_time / frame_interval;

  offset += (delta_time_multiplier * animationSpeed);
  ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);

  drawGrid(1, 20, 20, "#000");
  
  previousTime = currentTime;

  requestAnimationFrame(animate);
}

핵심 공식

  • delta_time = currentTime - previousTime: 프레임 간 실제 시간 차이
  • delta_time_multiplier = delta_time / frame_interval: 목표 프레임레이트 대비 실제 비율
  • offset += (delta_time_multiplier * animationSpeed): 일관된 속도 보장

완성된 코드 구조

실용적 팁: 창 크기 조정 처리

window.addEventListener("resize", () => {
  requestAnimationFrame(() => {
    myCanvas.style.width = window.innerWidth + "px";
    myCanvas.style.height = window.innerHeight + "px";

    accountForDPI();
    drawGrid(1, 20, 20, "#000");
  });
});

핵심 학습 내용

1. 애니메이션 루프 관리

  • requestAnimationFrame 사용의 중요성
  • 타임스탬프 처리 모범 사례

2. 성능 최적화

  • DPI 인식을 통한 선명한 렌더링
  • 효율적인 화면 클리어 및 재그리기

3. 크로스 플랫폼 호환성

  • Delta time을 통한 프레임레이트 독립적 애니메이션
  • 다양한 디스플레이 환경 지원

4. 수학적 접근

  • 모듈로 연산을 통한 순환 패턴 구현
  • 그리기 영역 확장을 통한 안정성 확보

주의사항

성능 관련

  • 고해상도 디스플레이에서 120fps 이상 지원 시 애니메이션 속도 조절 필요
  • Delta time multiplier 없이는 디바이스별 속도 차이 발생

코드 안정성

  • 단순한 조건문 기반 오프셋 리셋은 애니메이션 속도 변경 시 불안정
  • 확장된 그리기 영역과 모듈로 연산이 더 안정적

브라우저 호환성

  • performance.now() 사용으로 정확한 타이밍 측정
  • requestAnimationFrame의 브라우저별 구현 차이 고려

결론

이 구현은 단순한 그리드 애니메이션을 넘어서 전문적인 웹 애니메이션 개발에 필요한 핵심 기술들을 다룹니다. DPI 처리, 프레임레이트 일관성, 효율적인 렌더링 등은 모든 Canvas 기반 애니메이션 프로젝트에서 활용할 수 있는 범용적인 기술입니다.

1개의 좋아요