지난 장에서 우리는 몇 가지 기본 애니메이션을 만들었고 움직이게 하는 방법들을 알게 됐어요. 이 파트에서는 움직임 자체를 더 자세히 살펴보고, 애니메이션을 더 고급스럽게 만들기 위해 물리 효과를 추가할 거예요.
안녕하세요! 캔버스 튜토리얼을 열심히 파고 계시군요. 이전 장에서 배웠던 기본 애니메이션 뼈대에 살을 붙여, 이번에는 물리 법칙을 적용한 '고급 애니메이션(Advanced animations)' 챕터에 돌입하셨습니다.
단순히 x, y 좌표만 1씩 더해서 일직선으로 움직이는 게 아니라, 가속도와 중력, 그리고 벽에 부딪혀 튕기는 효과(바운스)를 주어서 훨씬 현실감 있는 '움직이는 공'을 만들어 볼 텐데요. 아주 재미있는 예제가 될 겁니다. 바로 시작해 볼까요!
우리는 애니메이션 학습을 위해 '공(ball)'을 하나 사용할 것입니다. 그러니 먼저 캔버스 위에 그 공을 그려보죠. 아래 코드가 그 기본 설정(setup)을 해줄 것입니다.
<canvas id="canvas" width="600" height="300"></canvas>
늘 그랬듯이, 먼저 드로잉 컨텍스트(drawing context)가 필요합니다. 공을 그리기 위해 우리는 여러 속성들(properties)과 캔버스 위에 자신을 그리는 draw() 메서드를 포함하고 있는 ball이라는 자바스크립트 객체(object)를 하나 만들 것입니다.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const ball = {
x: 100, // 공의 중심 x 좌표
y: 100, // 공의 중심 y 좌표
radius: 25, // 공의 반지름
color: "blue", // 공의 색상
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
};
ball.draw(); // 공 그리기 실행!
코드에 특별히 어려운 부분은 없습니다. 공은 사실 단순한 원(circle)일 뿐이며, 앞서 배웠던 arc() 메서드의 도움을 받아 그려집니다.
자, 이제 공이 하나 생겼으니 이 튜토리얼의 이전 챕터(기본 애니메이션)에서 배웠던 것과 같은 기본적인 애니메이션을 추가할 준비가 되었습니다. 이번에도 역시 애니메이션 제어를 위해 window.requestAnimationFrame()을 사용할 것입니다.
공의 현재 위치 좌표(x, y)에 속도 벡터(vx, vy) 값을 더해줌으로써 공을 움직이게 만듭니다. 또한 이전 프레임에서 그려졌던 낡은 원(공의 잔상)들을 지워버리기 위해 매 프레임마다 캔버스를 지우는(clearRect()) 작업도 잊지 않고 해줍니다.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf; // requestAnimationFrame의 ID를 저장할 변수
const ball = {
x: 100,
y: 100,
vx: 5, // x축 방향 속도 (Velocity X)
vy: 2, // y축 방향 속도 (Velocity Y)
radius: 25,
color: "blue",
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
};
function draw() {
// 1. 캔버스 싹 지우기 (잔상 제거)
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 공 그리기
ball.draw();
// 3. 공의 다음 위치 계산 (현재 위치 + 속도)
ball.x += ball.vx;
ball.y += ball.vy;
// 4. 다음 프레임 예약
raf = window.requestAnimationFrame(draw);
}
// 마우스를 캔버스 위에 올리면 애니메이션 시작
canvas.addEventListener("mouseover", (e) => {
raf = window.requestAnimationFrame(draw);
});
// 마우스가 캔버스를 벗어나면 애니메이션 정지
canvas.addEventListener("mouseout", (e) => {
window.cancelAnimationFrame(raf);
});
// 화면이 처음 로드될 때 정지된 상태의 공을 한 번 그려줍니다.
ball.draw();
💡 강사의 실무 팁:
cancelAnimationFrame을 기억해 두세요! 보통 리액트 컴포넌트 안에서 캔버스 애니메이션을 실행시킬 때, 컴포넌트가 화면에서 사라질 때(unmount) 애니메이션 루프를 정지시켜주지 않으면 보이지도 않는 애니메이션이 뒤에서 계속 돌아가면서 메모리 누수(Memory Leak)가 발생합니다.useEffect의 return 함수(클린업 함수) 안에서cancelAnimationFrame을 꼭 호출해 주세요.
위 코드까지만 작성하면 경계면 충돌 테스트 로직이 전혀 없기 때문에, 마우스를 올리는 순간 공이 캔버스 밖으로 영원히 도망가 버립니다. 공이 캔버스 화면 밖으로 나가지 못하게 하려면, 공의 x와 y 위치가 캔버스의 가로/세로 크기를 벗어났는지 확인(check)하고, 만약 벗어났다면 속도 벡터의 방향을 반대로 뒤집어(invert) 주어야 합니다.
이 기능을 구현하기 위해 draw 메서드(함수) 안쪽에 다음의 체크 로직을 추가해 줍니다.
// Y축(상하) 경계 충돌 체크
if (
ball.y + ball.vy > canvas.height - ball.radius || // 바닥에 부딪혔거나
ball.y + ball.vy < ball.radius // 천장에 부딪혔다면
) {
ball.vy = -ball.vy; // Y축 이동 방향을 반대로 바꿈 (튕김!)
}
// X축(좌우) 경계 충돌 체크
if (
ball.x + ball.vx > canvas.width - ball.radius || // 오른쪽 벽에 부딪혔거나
ball.x + ball.vx < ball.radius // 왼쪽 벽에 부딪혔다면
) {
ball.vx = -ball.vx; // X축 이동 방향을 반대로 바꿈 (튕김!)
}
💡 강사의 핵심 보충 설명:
코드를 자세히 보시면 그냥ball.y > canvas.height라고 체크하지 않고ball.y + ball.vy > canvas.height - ball.radius라고 계산하고 있습니다.
1.+ ball.vy: "다음에 이동할 위치"를 미리 계산해서 확인하는 겁니다.
2.- ball.radius: 공의 좌표(x,y)는 원의 '중심점'입니다. 공의 가장자리(표면)가 벽에 닿았을 때 튕기게 하려면 중심점에서 반지름(radius)만큼 뺀 거리를 기준으로 계산해야 벽에 반쯤 파묻힌 채로 튕기는 불상사를 막을 수 있습니다!
지금까지 작성한 코드가 어떻게 작동하는지 결과를 확인해 볼까요?
<canvas id="canvas" width="600" height="300"></canvas>
#canvas {
border: 1px solid black;
}
(마우스를 캔버스 영역 안으로 올리면 파란색 공이 이리저리 화면의 벽을 치며 튕겨 다니는 애니메이션이 실행됩니다. 마우스를 빼면 그 자리에 멈춥니다.)
마치 무중력 우주 공간에서 튕기는 것 같던 공의 움직임에 가속도(중력이나 마찰력 등)를 부여해서 모션을 훨씬 더 현실감 있게 만들어 보겠습니다. 속도 값(vy)을 이런 식으로 조작해 볼 수 있죠.
ball.vy *= 0.99; // 마찰력: 매 프레임마다 y축 속도가 1%씩 줄어듭니다.
ball.vy += 0.25; // 중력: 매 프레임마다 y축 방향으로 밑으로 떨어지는 힘(0.25)이 추가됩니다.
이 코드는 매 프레임마다 공의 수직(상하) 속도를 미세하게 줄이면서 동시에 아래로 당기는 힘을 더합니다. 그 결과 공은 땅에 튕길 때마다 점점 낮게 뛰어오르다가, 결국엔 바닥에 통통거리며 멈추게(bounce on the floor in the end) 됩니다.
중력과 마찰력이 추가된 두 번째 데모의 전체 코드입니다.
<canvas id="canvas" width="600" height="300"></canvas>
#canvas {
border: 1px solid black;
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;
const ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: "blue",
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
};
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ball.draw();
// 위치 이동
ball.x += ball.vx;
ball.y += ball.vy;
// 가속도(마찰력과 중력) 적용
ball.vy *= 0.99;
ball.vy += 0.25;
// 바닥/천장 충돌 (튕김)
if (
ball.y + ball.vy > canvas.height - ball.radius ||
ball.y + ball.vy < ball.radius
) {
ball.vy = -ball.vy;
}
// 좌/우 벽 충돌 (튕김)
if (
ball.x + ball.vx > canvas.width - ball.radius ||
ball.x + ball.vx < ball.radius
) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener("mouseover", (e) => {
raf = window.requestAnimationFrame(draw);
});
canvas.addEventListener("mouseout", (e) => {
window.cancelAnimationFrame(raf);
});
ball.draw();
마우스를 캔버스 안으로 옮기면 애니메이션이 시작됩니다. 공이 바닥에 튕길 때마다 중력과 마찰력의 영향을 받아 점점 튕기는 높이가 줄어들다가 바닥에 데굴데굴 굴러가는 매우 현실적인 모션을 볼 수 있습니다.
어떠신가요! vx, vy 변수 두 개와 덧셈, 곱셈 몇 번만으로 캔버스 위에 아주 훌륭한 물리 엔진을 만들어냈습니다. 이 원리만 잘 이해하시면 공뿐만 아니라 눈 내리는 효과나 파티클 폭발 효과 등 상상하는 모든 움직임을 만들어내실 수 있을 거예요.
직접 변수 값(0.99나 0.25)을 요리조리 바꿔가면서 어떻게 움직임이 변하는지 테스트해 보세요!
안녕하세요! 이번 시간에는 캔버스의 진짜 묘미인 고급 애니메이션 기법에 대해 알아보겠습니다. 이전 튜토리얼에서 배운 기초를 응용하면, 잔상이 남는 유성 효과나 마우스를 졸졸 따라다니는 인터랙티브한 공 애니메이션 같은 화려한 결과물을 만들 수 있습니다.
이전 프레임의 그림을 지울 때 우리는 항상 clearRect 메서드를 사용해서 캔버스를 깨끗하게 닦아냈습니다. 하지만 이 clearRect 대신에 약간 투명한(semi-transparent) 색상으로 캔버스를 덮어버리는 fillRect를 사용하면, 지나간 자리에 스르륵 사라지는 아주 멋진 꼬리(Trailing) 효과를 손쉽게 만들어낼 수 있습니다.
ctx.fillStyle = "rgb(255 255 255 / 30%)"; // 투명도가 30%인 흰색
ctx.fillRect(0, 0, canvas.width, canvas.height); // 캔버스 전체를 살짝 덮어줍니다!
위의 꼬리 효과 트릭을 적용한 데모를 만들어 보겠습니다.
<canvas id="canvas" width="600" height="300"></canvas>
#canvas {
border: 1px solid black;
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;
// 공 객체를 정의합니다.
const ball = {
x: 100, // 시작 위치 x
y: 100, // 시작 위치 y
vx: 5, // x축 이동 속도
vy: 2, // y축 이동 속도
radius: 25, // 공의 반지름
color: "blue",
draw() { // 공을 그리는 메서드
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
};
function draw() {
// clearRect 대신 알파 값이 있는 fillRect를 사용해 잔상 효과를 만듭니다!
ctx.fillStyle = "rgb(255 255 255 / 30%)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ball.draw();
// 속도만큼 공을 이동시킵니다.
ball.x += ball.vx;
ball.y += ball.vy;
// 간단한 물리 법칙 추가: 마찰력과 중력
ball.vy *= 0.99; // 1. 공기 저항(마찰력) 때문에 y축 속도가 점점 줄어듭니다.
ball.vy += 0.25; // 2. 중력 때문에 아래로 당겨지는 힘이 더해집니다.
// 공이 캔버스 위/아래 벽에 부딪히면 튕겨 나옵니다.
if (
ball.y + ball.vy > canvas.height - ball.radius ||
ball.y + ball.vy < ball.radius
) {
ball.vy = -ball.vy;
}
// 공이 캔버스 좌/우 벽에 부딪히면 튕겨 나옵니다.
if (
ball.x + ball.vx > canvas.width - ball.radius ||
ball.x + ball.vx < ball.radius
) {
ball.vx = -ball.vx;
}
// 다음 프레임을 예약합니다.
raf = window.requestAnimationFrame(draw);
}
// 캔버스에 마우스를 올리면 애니메이션이 시작됩니다.
canvas.addEventListener("mouseover", (e) => {
raf = window.requestAnimationFrame(draw);
});
// 마우스가 캔버스를 벗어나면 애니메이션을 멈춥니다.
canvas.addEventListener("mouseout", (e) => {
window.cancelAnimationFrame(raf);
});
// 시작할 때 화면에 멈춰있는 공을 한 번 그려줍니다.
ball.draw();
💡 강사의 팁: 캔버스로 게임이나 물리 시뮬레이션을 만들 때, 위 코드에 있는
vy *= 0.99(마찰력/공기저항)와vy += 0.25(중력 가속도) 패턴은 정말 밥 먹듯이 쓰이는 기초 물리 엔진 수학입니다. 이 두 줄의 코드만으로도 공이 훨씬 자연스럽고 묵직하게 통통 튀는 느낌을 줄 수 있어요!
(마우스를 캔버스 위에 올리면 파란 공이 꼬리를 남기며 중력과 마찰력에 의해 통통 튀는 데모 화면)
우리가 공을 조금 더 마음대로 가지고 놀 수 있도록, 마우스 이벤트(mousemove)를 이용해 마우스 커서가 가는 대로 공이 졸졸 따라오게 만들어 볼까요? 그리고 클릭(click) 이벤트를 주면 공이 다시 제멋대로 튕기도록 놓아주는 기능도 추가해 보겠습니다.
<canvas id="canvas" width="600" height="300"></canvas>
#canvas {
border: 1px solid black;
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;
// 공이 스스로 움직이고 있는지(true), 마우스에 잡혀있는지(false) 체크하는 플래그
let running = false;
const ball = {
x: 100,
y: 100,
vx: 5,
vy: 1,
radius: 25,
color: "blue",
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
};
function clear() {
ctx.fillStyle = "rgb(255 255 255 / 30%)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function draw() {
clear();
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
// 벽에 부딪히면 튕겨 나오는 로직
if (
ball.y + ball.vy > canvas.height - ball.radius ||
ball.y + ball.vy < ball.radius
) {
ball.vy = -ball.vy;
}
if (
ball.x + ball.vx > canvas.width - ball.radius ||
ball.x + ball.vx < ball.radius
) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
// 1. 마우스를 움직일 때
canvas.addEventListener("mousemove", (e) => {
// 공이 자유롭게 움직이는 상태가 아닐 때(마우스가 잡고 있을 때)만
if (!running) {
clear();
// 공의 위치를 현재 마우스 포인터의 위치로 순간이동시킵니다.
ball.x = e.clientX;
ball.y = e.clientY;
ball.draw();
}
});
// 2. 캔버스를 클릭할 때
canvas.addEventListener("click", (e) => {
// 공이 멈춰있었다면, 애니메이션 루프를 실행하고 플래그를 true로 바꿉니다.
if (!running) {
raf = window.requestAnimationFrame(draw);
running = true;
}
});
// 3. 마우스가 캔버스를 벗어날 때
canvas.addEventListener("mouseout", (e) => {
// 애니메이션을 멈추고 다시 공을 잡을 수 있는 상태로 만듭니다.
window.cancelAnimationFrame(raf);
running = false;
});
// 초기 공 그리기
ball.draw();
마우스를 움직여 공을 이리저리 끌고 다니다가, 클릭해서 화면에 던져보세요!
(마우스를 따라다니다 클릭 시 벽을 치며 튕겨나가는 인터랙티브 공 데모 화면)
이 짧은 챕터에서는 캔버스로 구현할 수 있는 아주 기초적인 형태의 고급 애니메이션 기법들을 다루어 보았습니다. 하지만 이건 빙산의 일각일 뿐이죠!
지금까지 배운 공 튀기기 로직에다가 마우스로 움직이는 직사각형 패들(paddle)을 하나 추가하고, 천장에 벽돌(bricks)들을 배열해 본다면 어떨까요? 이 데모를 그 유명한 고전 게임인 벽돌 깨기(Breakout) 게임으로 완벽하게 진화시킬 수 있습니다!
웹 게임 제작에 관심이 있으시다면 MDN의 게임 개발(Game development) 영역에서 더 다양하고 흥미로운 아티클들을 찾아보시는 것을 추천합니다.
window.requestAnimationFrame()수고하셨습니다! 기초적인 애니메이션 로직에 물리 법칙과 사용자 상호작용(Interaction)까지 버무리는 방법을 마스터하셨네요. 이제 여러분도 캔버스로 멋진 미니 게임 하나쯤은 거뜬히 만들어 낼 수 있는 실력을 갖추게 되었습니다! 🎮✨