딱히 canvas에서만 사용되지는 않는데, canvas에서 애니메이션을 그릴 때 주로 사용되긴 한다.
아래는 MDN에서 가져온 코드인데, timestamp가 찍힐 때마다 progress를 증가시키는 애니메이션이라 가정할 때, 분명 매우 많은 수의 이벤트가 일어날 것이다.
많은 수의 이벤트가 발생하는 경우에 취하는 일반적인 조치에선 (소켓, 리사이징, 폴링 이벤트 같은 경우가 있겠다) 의도에 따라 debounceTime, thrtottlingTime 등을 지정해주면 되는데 애니메이션의 경우 이런 식의 핸들링을 할 경우 '부드러움'이 부족하다.
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
이럴 때 requestAnimationFrame을 사용해보면 된다.
requestAnimationFrame
https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame
cf) react에서 requestAnimationFrame을 사용하고 싶다면
(https://css-tricks.com/using-requestanimationframe-with-react-hooks/) 여기를 참고합시다.
window.requestAnimationFrame()은 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출하게 합니다. 이 메소드는 리페인트 이전에 실행할 콜백을 인자로 받습니다.
window.requestAnimationFrame(callback);
이게 무슨 말이냐면,
브라우저 화면 상에 무언가를 렌더할 때, 위치나 색을 계산하는 과정을 '리플로우'라고 하며 이 계산을 실제로 수행하여 브라우저상에 그리는 것을 '리페인트'라고 합니다.
그런데 애니메이션의 경우, 리페인트 과정이 끝나지도 않았는데 다음 좌표로 이동하라고 애니메이션을 수행하는 경우, 애니메이션이 의도한 대로 부드럽게 움직이지 않게 됩니다. requestAnimationFrame은 이러한 문제를 해결해줍니다.
리페인트 과정이 끝난 후 적용할 애니메이션을 requestAnimationFrame의 콜백으로 넣어주시면 됩니다.
다음과 같이 실행하면, 무한으로 draw를 실행하게 됩니다. 60fps로 지원합니다.
초당 60번 호출한다는 거죠.
const canvas = document.querySelector(".canvas");
const context = canvas.getContext("2d");
function draw() {
context.arc(10, 150, 10, 0, Math.PI * 2, false);
context.fill();
console.log("그렸다!");
requestAnimationFrame(draw);
}
draw();
애니메이션
이런 식으로 선을 그릴 수 있습니다.
속도를 조절하고 싶으면 x값을 키우거나 줄이면 되겠죠?
let x = 10;
function draw() {
context.beginPath();
context.arc(x, 150, 10, 0, Math.PI * 2, false);
context.closePath();
context.fill();
x += 2;
requestAnimationFrame(draw);
}
draw();
원 자체를 움직이고 싶다면 다음 도형을 그리기 전에 canvas를 전부 싹 지워주시면 됩니다.
clearRect로 전체 캔버스 크기를 주면 되겠죠.
let x = 10;
function draw() {
// canvas 지우기
context.clearRect(0, 0, canvas.width, canvas.height);
// path 그리기
context.beginPath();
context.arc(x, 150, 10, 0, Math.PI * 2, false);
context.closePath();
context.fill();
x += 2;
requestAnimationFrame(draw);
}
draw();
캔버스의 마지막에 닿았을 때 멈추게 하려면
x 좌표를 계산해서 멈추게 하거나
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(xPos, 150, 10, 0, Math.PI * 2, false);
context.fill();
xPos += 3;
// (캔버스 길이 - 반지름)에 도달하면 멈춤
if (xPos >= canvas.width - 10) {
return;
}
requestAnimationFrame(draw);
}
requestAnimationFrame이 반환하는 값을 알아내서 cancelAnimationFrame을 사용할 수도 있습니다.
timerId = requestAnimationFrame(draw);
if (xPos >= canvas.width - 10) {
cancelAnimationFrame(timerId);
}
cancelAnimationFrame을 활용하면, 캔버스를 클릭하거나 특정 버튼을 눌렀을 때 멈추게하는 등 응용할 수 있습니다.
애니메이션 렌더 끊기 (cancelAnimationFrame)
setInterval을 사용하시면 됩니다. 이게 쉽긴 하지만 requestAnimationFrame의 장점을 취할 수 없습니다.
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x, 150, 10, 0, Math.PI * 2, false);
context.closePath();
context.fill();
x += 2;
requestAnimationFrame(draw);
}
setInterval(draw, 1000);
카운터를 두고 다음과 같이 끊기도록 애니메이션을 하는 방식이 좋습니다.
let x = 10;
let count = 0;
function draw() {
if (count % 30 === 0) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x, 150, 10, 0, Math.PI * 2, false);
context.closePath();
context.fill();
x += 2;
}
count++;
requestAnimationFrame(draw);
}
draw();
아니라면 cancelAnimationFrame를 사용해도 됩니다.
requestAnimationFrame을 취소하는 함수인 cancelAnimationFrame을 활용해도 됩니다.
https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame
잘 쓸 수 있는 보일러 플레이트 같은 코드는 없나?
클래스로 관리하면, 아래와 같은 코드 베이스로 시작하면 편리합니다.
class App {
constructor() {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
document.body.appendChild(this.canvas);
// your shape instance generated here
// this.resize를 인스턴스에 바인딩하고, 이벤트 캡쳐링 차단.
window.addEventListener('resize', this.resize.bind(this), false);
this.resize();
// 리페인트되기 전에 애니메이션 업데이트해서 부드러운 애니메이션 구현. 60fps 목표.
requestAnimationFrame(this.animate.bind(this));
}
// 브라우저 크기에 따라 canvas 크기 수정
resize() {
this.stageWidth = document.body.clientWidth;
this.stageHeight = document.body.clientHeight;
this.canvas.width = this.stageWidth * 2;
this.canvas.height = this.stageHeight * 2;
// 고해상도(레티나) 화면에서의 선명도를 위해 캔버스 크기의 2배를 html상의 가로, 세로 길이에 줍니다.
// 4k 촬영 => fhd로 압축하는 원리와 비슷
this.ctx.scale(2, 2);
// your shape resize here
}
animate() {
this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight); // 전에 있던 그림 지워야죠
requestAnimationFrame(this.animate.bind(this));
// your animation here
}
}
// Domcontents가 로드되었을 때 로드해도 되고, window가 load되었을 때 해도 되고 맘대로 하셈
window.onload = () => {
new App();
};
'📈 js 그래픽 > 🎨 Canvas' 카테고리의 다른 글
Canvas 이미지 사용하기 (0) | 2020.07.18 |
---|---|
HTML5 Canvas 생성 및 그리기 (0) | 2020.07.18 |