우리는 JavaScript를 사용해서 <canvas> 요소를 제어하기 때문에, (인터랙티브한) 애니메이션을 만드는 것도 매우 쉬워요. 이 장에서는 몇 가지 기본 애니메이션을 어떻게 만드는지 살펴볼 거예요.
아마도 가장 큰 제약은, 한번 도형이 그려지면 그 상태로 유지된다는 거예요. 만약 그것을 이동해야 한다면 그것과 그 전에 그려진 모든 것을 다시 그려야 해요. 복잡한 프레임을 다시 그리는 데는 많은 시간이 걸리고, 성능은 실행되는 컴퓨터의 속도에 크게 의존해요.
안녕하세요! 캔버스의 진면목을 확인할 수 있는 '기본 애니메이션(Basic animations)' 챕터에 오신 것을 환영합니다.
지금까지는 정적인 그림만 그렸다면, 이제부터는 시간에 따라 움직이는 역동적인 화면을 만들어 볼 거예요. 포트폴리오 프로필의 로딩 스피너나 배경에 떠다니는 파티클, 독후감 사이트의 스크롤에 반응하는 차트 등을 만들 때 이 애니메이션 지식이 완벽한 기초가 되어줄 겁니다. 강사의 팁을 가득 담아 번역해 드릴게요!
애니메이션의 하나의 프레임(frame, 화면)을 그리기 위해 우리가 거쳐야 하는 필수적인 4단계는 다음과 같습니다:
clearRect() 메서드를 사용하는 것입니다.restore())하여 캔버스를 원래의 깨끗한 상태로 되돌립니다.💡 강사의 핵심 팁:
이 4단계는 모든 캔버스 애니메이션의 절대적인 '루틴'입니다. 매 프레임마다 이 과정이 1초에 60번씩 반복되는 거예요. 만약 1번(지우기)을 빼먹으면 어떻게 될까요? 그림이 이동하는 궤적을 따라 잔상이 길게 남아서 마치 브러시로 화면을 문지른 것처럼 지저분해집니다!
지금까지 우리는 캔버스의 메서드를 직접 사용하거나 커스텀 함수를 호출하여 캔버스에 도형을 그렸습니다. 일반적인 상황에서는 스크립트의 실행이 모두 끝난 후에만 이 결과물들이 캔버스 위에 한꺼번에 짠 하고 나타나는 것을 볼 수 있었죠. 즉, 단순한 for 루프(반복문) 내부에서는 우리가 눈으로 볼 수 있는 애니메이션을 실행하는 것이 불가능합니다. (루프가 도는 동안에는 화면이 갱신되지 않으니까요.)
이 말은 즉, 우리가 만든 그리기 함수를 '일정한 시간 간격을 두고 반복해서 실행할 수 있는 방법'이 필요하다는 뜻입니다. 이렇게 애니메이션을 제어하는 데는 크게 두 가지 방식이 있습니다.
첫 번째로, 특정 함수를 정해진 시간 동안 지속적으로 호출하는 데 사용할 수 있는 setInterval(), setTimeout(), 그리고 requestAnimationFrame() 함수들이 있습니다.
setInterval()
지정된 function을 delay 밀리초(milliseconds)마다 무한히 반복해서 실행하기 시작합니다.
setTimeout()
지정된 function을 delay 밀리초 후에 딱 한 번 실행합니다. (이 함수 안에서 자기 자신을 다시 호출하면 애니메이션 루프를 만들 수 있습니다.)
requestAnimationFrame()
브라우저에게 "나 이제 애니메이션을 수행하고 싶어!"라고 알리고, 브라우저가 다음 번 리페인트(화면을 다시 그리는 작업)를 하기 직전에 애니메이션을 업데이트하는 특정 함수를 호출해 달라고 요청합니다.
사용자의 조작이 전혀 필요 없이 혼자서 돌아가는 애니메이션을 만들고 싶다면, 제공된 코드를 단순히 반복 실행해 주는 setInterval() 함수를 사용할 수 있습니다. 반면에 게임을 만든다고 가정해 볼까요? 우리는 사용자의 키보드나 마우스 이벤트에 반응하여 애니메이션을 제어해야 하므로, 이때는 addEventListener()로 리스너를 달아 사용자의 상호작용(interaction)을 포착하고 우리의 애니메이션 함수를 실행시키는 방식을 사용해야 합니다.
참고:
아래에 이어질 예제들에서는 애니메이션을 제어하기 위해Window.requestAnimationFrame()메서드를 사용할 것입니다.requestAnimationFrame메서드는 시스템(브라우저)이 프레임을 화면에 그릴 준비가 완벽하게 되었을 때 알아서 애니메이션 프레임을 호출해 주기 때문에, 훨씬 더 부드럽고 성능이 뛰어난(efficient) 애니메이션 방식을 제공합니다. 콜백 함수는 보통 초당 60회(60fps) 호출되지만, 브라우저의 탭이 백그라운드(숨겨진 상태)로 넘어가면 브라우저가 알아서 호출 빈도를 대폭 낮춰 배터리나 CPU 소모를 줄여줍니다. 게임 개발 등 애니메이션 루프에 대한 더 자세한 정보가 필요하다면, 게임 개발 존(Game development zone)에 있는 비디오 게임의 해부학(Anatomy of a video game) 아티클을 참고해 보세요.
💡 강사의 실무 팁:
최신 프론트엔드 환경에서 캔버스 애니메이션을 만들 때는 무조건requestAnimationFrame을 사용한다고 생각하시면 됩니다!setInterval은 브라우저의 렌더링 주기와 엇박자가 나서 화면이 찢어지거나 버벅거리는(jank) 현상이 발생할 확률이 아주 높거든요.
이 예제는 우리 태양계의 작은 모델(태양, 지구, 달)을 애니메이션으로 구현한 것입니다.
<canvas id="canvas" width="300" height="300"></canvas>
const sun = new Image();
const moon = new Image();
const earth = new Image();
const ctx = document.getElementById("canvas").getContext("2d");
function init() {
// 배경 투명한 이미지 파일들을 불러옵니다.
sun.src = "canvas_sun.png";
moon.src = "canvas_moon.png";
earth.src = "canvas_earth.png";
// 다음 렌더링 프레임에 draw 함수를 실행하도록 예약합니다.
window.requestAnimationFrame(draw);
}
function draw() {
// 새로운 그림이 항상 예전 그림 뒤로(배경으로) 그려지도록 설정합니다.
ctx.globalCompositeOperation = "destination-over";
// 1단계: 캔버스 지우기 (전체 영역 초기화)
ctx.clearRect(0, 0, 300, 300);
ctx.fillStyle = "rgb(0 0 0 / 40%)";
ctx.strokeStyle = "rgb(0 153 255 / 40%)";
// 2단계: 캔버스 원본 상태 저장
ctx.save();
ctx.translate(150, 150); // 원점을 캔버스 중앙으로 이동
// 지구 (Earth) 그리기
const time = new Date();
// 현재 시간에 비례해서 캔버스 전체를 회전시킵니다. (지구의 공전)
ctx.rotate(
((2 * Math.PI) / 60) * time.getSeconds() +
((2 * Math.PI) / 60000) * time.getMilliseconds(),
);
ctx.translate(105, 0); // 회전된 궤도를 따라 지구의 위치로 원점 이동
ctx.fillRect(0, -12, 40, 24); // 그림자(Shadow) 그리기
ctx.drawImage(earth, -12, -12); // 지구 이미지 그리기
// 달 (Moon) 그리기
ctx.save(); // 달의 위치를 기준으로 다시 한 번 상태 저장
// 현재 시간에 비례해서 캔버스를 더 빠르게 회전시킵니다. (달의 공전)
ctx.rotate(
((2 * Math.PI) / 6) * time.getSeconds() +
((2 * Math.PI) / 6000) * time.getMilliseconds(),
);
ctx.translate(0, 28.5); // 회전된 궤도를 따라 달의 위치로 원점 이동
ctx.drawImage(moon, -3.5, -3.5); // 달 이미지 그리기
ctx.restore(); // 달 그리기 종료 후, 지구 위치였던 상태로 복원
// 4단계: 캔버스 상태 복원 (처음의 캔버스 중앙 원점 상태로 복원)
ctx.restore();
// 지구 궤도 궤적 그리기
ctx.beginPath();
ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
ctx.stroke();
// 가장 가운데에 태양 그리기
ctx.drawImage(sun, 0, 0, 300, 300);
// 애니메이션 루프! 다음 프레임에 자신을 다시 호출합니다.
window.requestAnimationFrame(draw);
}
init(); // 최초 초기화 및 실행
위의 스크립트에서는, 브라우저가 화면을 그릴 준비가 될 때마다 requestAnimationFrame(draw)가 자신을 끊임없이 호출하면서 무한 루프를 돌게 됩니다. 이때 Date 객체를 사용해서 현실 세계의 시간(밀리초) 흐름에 따라 행성들의 각도(rotate)가 조금씩 변하게 되고, 그 결과 부드러운 애니메이션이 탄생하는 것입니다.
이 예제는 현재 시간을 보여주는 부드럽게 움직이는 아날로그 시계를 캔버스에 그립니다.
<canvas id="canvas" width="150" height="150">현재 시간</canvas>
function clock() {
const now = new Date();
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 전체 상태 저장
ctx.save();
ctx.clearRect(0, 0, 150, 150);
ctx.translate(75, 75); // 중앙으로 이동
ctx.scale(0.4, 0.4); // 시계 전체 크기 축소
// 캔버스를 기본적으로 -90도(위쪽, 12시 방향)로 회전시킵니다.
ctx.rotate(-Math.PI / 2);
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.lineWidth = 8;
ctx.lineCap = "round";
// 시간 표시 마크(눈금) 그리기
ctx.save();
for (let i = 0; i < 12; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 6); // 30도씩 12번 돌면서 눈금을 긋습니다.
ctx.moveTo(100, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.restore();
// 분 표시 마크(눈금) 그리기
ctx.save();
ctx.lineWidth = 5;
for (let i = 0; i < 60; i++) {
if (i % 5 !== 0) { // 시간 눈금과 겹치는 곳은 그리지 않습니다.
ctx.beginPath();
ctx.moveTo(117, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.rotate(Math.PI / 30); // 6도씩 돌면서 눈금을 긋습니다.
}
ctx.restore();
const sec = now.getSeconds();
// 부드럽게 물 흐르듯 움직이는(sweeping) 초침을 원한다면 아래 코드를 쓰세요:
// const sec = now.getSeconds() + now.getMilliseconds() / 1000;
const min = now.getMinutes();
const hr = now.getHours() % 12;
ctx.fillStyle = "black";
// 스크린 리더 등 접근성을 위해 시계의 내용(텍스트)을 업데이트 해줍니다.
canvas.innerText = `현재 시간: ${hr}:${min}`;
// 시침 그리기
ctx.save();
ctx.rotate(
(Math.PI / 6) * hr + (Math.PI / 360) * min + (Math.PI / 21600) * sec,
);
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-20, 0);
ctx.lineTo(80, 0);
ctx.stroke();
ctx.restore();
// 분침 그리기
ctx.save();
ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(-28, 0);
ctx.lineTo(112, 0);
ctx.stroke();
ctx.restore();
// 초침 그리기
ctx.save();
ctx.rotate((sec * Math.PI) / 30);
ctx.strokeStyle = "#D40000"; // 빨간색
ctx.fillStyle = "#D40000";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-30, 0);
ctx.lineTo(83, 0);
ctx.stroke();
// 초침의 둥근 중앙 장식
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
ctx.fill();
// 초침의 둥근 꼬리 장식
ctx.beginPath();
ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
ctx.stroke();
// 중앙의 작은 검은색 점
ctx.fillStyle = "transparent";
ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
// 시계 바깥쪽 파란색 두꺼운 테두리 그리기
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = "#325FA2";
ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
ctx.stroke();
// 초기 상태로 싹 복원
ctx.restore();
// 애니메이션 루프 재호출
window.requestAnimationFrame(clock);
}
// 최초 애니메이션 시작
window.requestAnimationFrame(clock);
이 예제에서도 핵심은 같습니다! 매 프레임마다 clearRect로 화면을 싹 지우고, save와 restore를 층층이 감싸면서 캔버스 원점을 회전시켜 눈금과 바늘들을 그려냅니다. translate와 rotate를 적절히 활용하면, 모든 바늘을 무조건 (0, 0)(현재 원점인 중앙)에서부터 출발시켜 그릴 수 있어서 삼각함수로 끝점 좌표를 일일이 계산할 필요 없이 코드가 아주 깔끔해진답니다!
참고:
시계의 시간 자체는 1초에 한 번씩만 업데이트되지만, 실제로 화면을 그리는 애니메이션은 초당 60프레임(혹은 여러분의 웹 브라우저 디스플레이 주사율)의 속도로 쉴 새 없이 업데이트되고 있습니다.
초침이 딱딱 끊어지지 않고 부드럽게 스르륵 움직이는(sweeping) 아날로그시계를 보고 싶으시다면, 위 코드에서 const sec 변수의 정의 부분을 주석 처리된 다른 버전의 코드로 교체해 보세요!
(시계 애니메이션 데모 화면)
이번 예제에서는 파노라마 이미지가 왼쪽에서 오른쪽으로 끊임없이 스크롤되는 애니메이션을 만들어 볼 겁니다. 우리는 위키피디아에서 가져온 요세미티 국립공원(Yosemite National Park)의 멋진 풍경 사진을 사용하겠지만, 캔버스보다 가로 길이가 긴 이미지라면 어떤 이미지든 자유롭게 가져다 쓰셔도 좋습니다.
HTML 코드는 파노라마 이미지가 스크롤될 <canvas> 요소를 포함합니다. 여기서 지정한 width와 height 속성의 값은 자바스크립트 코드 내의 canvasXSize와 canvasYSize 변수 값과 반드시 똑같이 맞춰야 한다는 점을 주의하세요.
<canvas id="canvas" width="800" height="200"
>Yosemite National Park, meadow at the base of El Capitan</canvas
>
자, 코드를 한번 살펴볼까요?
const img = new Image();
// 사용자 변수 - 이 값들을 수정해서 스크롤될 이미지, 방향, 속도 등을 자유롭게 커스텀할 수 있습니다.
img.src = "capitan_meadows_yosemite_national_park.jpg";
const canvasXSize = 800;
const canvasYSize = 200;
const speed = 30; // 값이 작을수록 애니메이션이 빨라집니다.
const scale = 1.05; // 이미지 확대/축소 비율
const y = -4.5; // 세로 방향 오프셋 (위아래 위치 조정)
// 메인 프로그램 변수들
const dx = 0.75; // 한 프레임당 이동할 픽셀 수 (속도 조절)
let imgW;
let imgH;
let x = 0;
let clearX;
let clearY;
let ctx;
img.onload = () => {
// 스케일이 적용된 실제 이미지 크기를 계산합니다.
imgW = img.width * scale;
imgH = img.height * scale;
if (imgW > canvasXSize) {
// 만약 이미지가 캔버스보다 더 넓다면, 이미지의 오른쪽 끝에서부터 시작하도록 x 시작 좌표를 맞춥니다.
x = canvasXSize - imgW;
}
// 캔버스를 지울 때 사용할(clear) 최대 영역 크기를 계산합니다.
clearX = Math.max(imgW, canvasXSize);
clearY = Math.max(imgH, canvasYSize);
// 캔버스 컨텍스트를 가져옵니다.
ctx = document.getElementById("canvas").getContext("2d");
// 애니메이션 루프 시작! (speed 값마다 draw 함수를 실행합니다.)
return setInterval(draw, speed);
};
function draw() {
// 1. 그리기 전에 캔버스를 깨끗하게 지웁니다.
ctx.clearRect(0, 0, clearX, clearY);
// 이미지가 캔버스 크기보다 작거나 같은 경우의 처리
if (imgW <= canvasXSize) {
// 캔버스 끝을 지나가면 초기화해서 다시 처음부터 그리게 합니다.
if (x > canvasXSize) {
x = -imgW + x;
}
// 빈 공간을 채우기 위해 복제된 이미지(image1)를 그립니다.
if (x > 0) {
ctx.drawImage(img, -imgW + x, y, imgW, imgH);
}
// 빈 공간을 채우기 위해 또 다른 복제된 이미지(image2)를 그립니다.
if (x - imgW > 0) {
ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
}
} else {
// 이미지가 캔버스보다 큰 경우의 처리
// 끝을 지나가면 초기화해서 다시 처음부터 그리게 합니다.
if (x > canvasXSize) {
x = canvasXSize - imgW;
}
// 이미지가 지나가고 남은 빈틈을 채우기 위해 복제된 이미지를 꼬리에 붙여 그립니다.
if (x > canvasXSize - imgW) {
ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
}
}
// 메인 이미지를 화면에 그립니다.
ctx.drawImage(img, x, y, imgW, imgH);
// 다음 프레임을 위해 x 좌표를 이동시킵니다.
x += dx;
}
이 로직의 핵심은 "이미지가 스크롤되면서 화면 밖으로 벗어났을 때, 어떻게 꼬리를 물고 다시 자연스럽게 나타나게 만들 것인가?" 입니다. 원본 이미지를 계속 이동(x += dx)시키면서, 캔버스 뒤쪽에 빈 공간이 생기기 시작하면 똑같은 이미지를 꼬리에 덧붙여 그려(ctx.drawImage) 마치 끊임없이 이어지는 긴 배경처럼 속이는(trick) 것이죠. 이 기법은 횡스크롤 게임(런닝 게임 등)의 배경을 만들 때 100% 필수적으로 들어가는 아주 중요한 알고리즘입니다!
💡 강사의 팁: 여기서도
setInterval을 사용해 애니메이션을 구현하고 있네요! 초보자분들이 이해하기엔setInterval이 쉽지만, 실무에서 이런 스크롤 배경을 만드실 땐 무조건requestAnimationFrame을 쓰셔야 합니다. 그래야 모니터 주사율에 맞춰 아주 부드럽고 뚝뚝 끊기지 않는 찰진 스크롤을 구현하실 수 있어요.
(요세미티 국립공원 파노라마 루프 애니메이션 데모 화면)
이번엔 조금 더 화려하고 상호작용(Interactive)이 들어간 애니메이션을 만들어 볼까요? 마우스 커서가 이미지를 가로질러 움직일 때, 커서를 따라 화려한 색상의 빛줄기 파티클(Particles)들이 생겨나고 사라지는 멋진 효과를 구현해 보겠습니다.
HTML은 이전에 했던 것처럼 아주 단순하게 캔버스 하나만 덜렁 있으면 됩니다.
<canvas id="cw"
>마우스가 이미지 위를 지날 때 커서를 따라다니며 사라지는 다채로운 빛줄기를 만드는 애니메이션</canvas
>
CSS 설정도 간단합니다. 캔버스를 화면 전체를 꽉 채우는 배경처럼 띄워버리기 위해 position: fixed와 꽉 찬 크기를 부여했습니다.
#cw {
position: fixed;
z-index: -1;
}
body {
margin: 0;
padding: 0;
background-color: rgb(0 0 0 / 5%);
}
이제 마우스(그리고 터치) 입력을 추적하고 수학적 계산을 통해 100개가 넘는 파티클들을 회전시키는, 꽤나 복잡해 보이지만 재밌는 자바스크립트 코드를 살펴봅시다.
const canvas = document.getElementById("cw");
const context = canvas.getContext("2d");
context.globalAlpha = 0.5; // 빛줄기가 약간 투명하게 보이도록 알파값을 줍니다.
// 마우스 커서의 현재 위치를 저장할 객체입니다. 시작 위치는 화면 정중앙입니다.
const cursor = {
x: innerWidth / 2,
y: innerHeight / 2,
};
// 생성된 파티클 객체들을 모아둘 배열입니다.
let particlesArray = [];
// 파티클 101개를 생성하고, 캔버스 크기를 맞추고, 애니메이션 루프를 시작합니다!
generateParticles(101);
setSize();
anim();
// 마우스가 움직일 때마다 cursor 객체의 x, y 좌표를 갱신합니다.
addEventListener("mousemove", (e) => {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
// 모바일 기기 등을 위해 화면 터치로 움직일 때도 좌표를 갱신해 줍니다.
addEventListener(
"touchmove",
(e) => {
e.preventDefault(); // 스크롤 등 기본 터치 동작을 막습니다.
cursor.x = e.touches[0].clientX;
cursor.y = e.touches[0].clientY;
},
{ passive: false },
);
// 창 크기가 바뀔 때마다 캔버스 크기도 다시 맞춰줍니다.
addEventListener("resize", () => setSize());
// 지정된 수(amount)만큼 파티클 객체를 찍어내서 배열에 넣는 함수
function generateParticles(amount) {
for (let i = 0; i < amount; i++) {
particlesArray[i] = new Particle(
innerWidth / 2,
innerHeight / 2,
4, // 선의 굵기
generateColor(), // 선의 색상
0.02, // 회전하는 속도
);
}
}
// 헥스(HEX) 코드를 무작위로 뽑아서 랜덤한 색상 문자열(예: "#F2A19B")을 반환하는 함수
function generateColor() {
let hexSet = "0123456789ABCDEF";
let finalHexString = "#";
for (let i = 0; i < 6; i++) {
finalHexString += hexSet[Math.ceil(Math.random() * 15)];
}
return finalHexString;
}
// 캔버스 크기를 브라우저 창(window) 크기에 딱 맞추는 함수
function setSize() {
canvas.height = innerHeight;
canvas.width = innerWidth;
}
// 파티클 하나하나를 정의하는 생성자 함수(클래스)입니다.
function Particle(x, y, particleTrailWidth, strokeColor, rotateSpeed) {
this.x = x;
this.y = y;
this.particleTrailWidth = particleTrailWidth;
this.strokeColor = strokeColor;
this.theta = Math.random() * Math.PI * 2; // 랜덤한 시작 각도(라디안)
this.rotateSpeed = rotateSpeed;
this.t = Math.random() * 150; // 커서로부터 떨어져서 회전할 반지름 거리
// 매 프레임마다 파티클을 움직이고 캔버스에 그리는 메서드
this.rotate = () => {
// 이동하기 전의 '현재' 위치를 먼저 저장해 둡니다 (선 긋기 용도).
const ls = {
x: this.x,
y: this.y,
};
// 각도(theta)를 증가시켜서 빙글빙글 돌게 만듭니다.
this.theta += this.rotateSpeed;
// 마우스 커서 위치(cursor)를 중심으로 원운동을 하도록 x, y 좌표를 수학적으로 갱신합니다!
this.x = cursor.x + Math.cos(this.theta) * this.t;
this.y = cursor.y + Math.sin(this.theta) * this.t;
// 드디어 그리기 시작! 이전 위치(ls)에서 새 위치(this)로 선을 하나 긋습니다.
context.beginPath();
context.lineWidth = this.particleTrailWidth;
context.strokeStyle = this.strokeColor;
context.moveTo(ls.x, ls.y);
context.lineTo(this.x, this.y);
context.stroke();
};
}
// 드디어! 진정한 애니메이션 루프 함수입니다.
function anim() {
requestAnimationFrame(anim); // 다음 프레임이 그릴 준비가 되면 자기를 다시 호출해 달라고 브라우저에 예약합니다.
// 여기서 핵심 트릭!
// clearRect()로 화면을 아예 다 지워버리는 대신, 투명도가 5%인 검은색 상자로 화면을 살짝만 덮어버립니다.
// 이렇게 하면 이전에 그렸던 선들의 흔적이 천천히 어두워지면서 사라지는, 아름다운 '꼬리(Trail)' 잔상 효과가 생겨납니다!
context.fillStyle = "rgb(0 0 0 / 5%)";
context.fillRect(0, 0, canvas.width, canvas.height);
// 모든 파티클을 순회하면서 각각 한 스텝씩 회전하고 그리라고 명령합니다.
particlesArray.forEach((particle) => particle.rotate());
}
💡 강사의 팁: 방금
anim()함수 안에 들어있는 트릭(fillRect에rgba를 줘서 잔상을 남기는 꼼수)은 파티클 애니메이션이나 우주 배경 효과 등을 만들 때 캔버스 장인들이 영혼까지 끌어다 쓰는 "Alpha Trail (알파 잔상)"이라는 전설적인 기법입니다. 이거 하나만 알아두셔도 정말 화려하고 있어 보이는 효과를 수백 가지는 만들어내실 수 있어요. 꼭 기억하세요! 그리고 드디어 이 예제에서setInterval대신requestAnimationFrame이 등장했네요. 아주 칭찬합니다!
(마우스 커서를 따라 수많은 빛줄기들이 빙글빙글 돌며 잔상을 남기는 인터랙티브 데모 화면)
이번 장에서는 캔버스로 할 수 있는 애니메이션의 기초를 맛보았습니다. 더 깊이 파고들고 싶으신가요?
고급 애니메이션 (Advanced animations)다음 챕터에서는 중력, 마찰력 같은 간단한 물리 법칙(physics)을 더하고 더 복잡한 애니메이션 기법들을 적용해 보는 시간을 가져보겠습니다.
정말 수고 많으셨습니다! 오늘 배운 파노라마와 파티클 기법만 잘 섞어 써도, 웬만한 웹사이트 메인 비주얼은 여러분 손으로 뚝딱 만들어 내실 수 있을 겁니다!