이번 포스팅에서는 Canvas
와 requestAnimationFrame()
을 사용해 애니메이션을 적용한 그림그리기 (Canvas 사용)을 알아보도록 하겠다.
해당 메서드는 자바스크립트에서 반복적인 혹은 이번 포스팅의 내용과 같이 Canvas를 이용한 애니메이션 / 그리기에 사용 할 수 있다.
기본적인 구동원리는 반복문의 사용이다.
function animate(){
requestAnimationFrame(animate);
...
}
requestAnimationFrame
의 파라미터로 바깥 함수(animate)를 넣어주면 해당함수(animate)를 무한반복 하게 된다. requestAnimationFrame
하단에 작업내용을 작성해도 정상적으로 작동한다.
requestAnimationFrame
브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출하게 합니다. 이 메소드는 리페인트 이전에 실행할 콜백을 인자로 받습니다.
- 참고: 노트: 다음 리페인트에서 그 다음 프레임을 애니메이트하려면 콜백 루틴이 반드시 스스로 requestAnimationFrame()을 호출해야합니다.
let xpoint = 0;
function animate() {
requestAnimationFrame(animate);
const ctx = ref.current?.getContext('2d');
if (ctx) {
ctx.beginPath();
ctx.arc(xpoint, 100, 30, 0, Math.PI * 2, false);
ctx.strokeStyle = 'blue';
ctx.stroke();
}
xpoint++;
}
위 코드를 실행해 보면,
(이미지1) 의 형태가 나오게 된다.
여기서 ctx?.clearRect(x,y,w,h)
를 ctx.beginPath();
이전에 넣어주게되면 원통형 같은 모형이 아닌 단 하나의 원이 오른쪽으로 이동하는 것을 볼 수 있다.
이러한 방식을 이용해 그래프를 그려보려고 한다.
원래 곡선을 그리려고 했지만 계산식이 많기에, 먼저 연습삼아 직선 그래프를 그려보려고 한다.
📝 문제
🔍 이유
💡 해결방안
먼저 (0,0)
에서 (20,60)
으로 그려지는 직선을 그려본다.
let y = 0;
const loopAnimate = () => {
requestAnimationFrame(loopAnimate);
const ctx = ref.current?.getContext('2d');
if (ctx) {
ctx?.clearRect(0, 0, 300, 200);
ctx.beginPath();
ctx.moveTo(0, 0);
if (ctx !== undefined && ctx !== null) {
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
}
ctx.lineTo(20, y);
ctx.stroke();
y++;
}
}
위 코드는 반복적으로 실행되기 때문에 y가 지속적으로 상승하지만 문제는
(이미지2) 와 같이 대각선이 변화하기 때문에 (0,0)
부터 (20,60)
까지 직선적으로의 움직임이 그려지지는 않는다.
이를 해결하기 위해 피타고라스의 정리를 이용해 애니메이션이 직선적으로 그려지도록 한다.
const pointY = Math.sqrt(to ** 2 + 20 ** 2) / 10; // 추가 계수
먼저 num ** e
를 사용하여 num의 e제곱 을 계산하고, 루트메소드 Math.sqrt(num)
를 사용하여 대각선의 길이를 계산한다. 그리고 이를 10으로 나눔으로 함수가 10번 실행되면 point
에 도달 하도록 했다.
여기서 추가할 부분은 데이터에 대한 그리기가 다 끝났을 경우 requestAnimationFrame(loopAnimate);
를 실행하지 않을 코드 추가한다.
let y = 0;
let bool = false;
const loopAnimate = () => {
if(!bool){
requestAnimationFrame(loopAnimate);
}
...//생략
if(y>60){
bool=true;/
}
}
}
각 포인트 데이터를 그대로 사용하게 된다면, 60 => 20 으로 내려갈시 40의 높이 변화 차이가 있어야하는데 정작 실제로는 20의 높이 변화만의 차이가 생기게 된다.
즉, 이전 포인트와 현재 포인트의 차이 (ex. 60-20 = 40)의 계산을 통해 정상적인 그래프가 그려지도록 한다.
다만, 배열 첫 인덱스는 이전 데이터가 존재 하지 않기 때문에 처음만 제외 해주면 된다.
let to = point[i];
if (i > 0) {
to = Math.abs(point[i - 1] - point[i]);
//각 포인트 사이의 차이(y)를 계산 (하단 1번 붙임 참고)
}
const pointY = Math.sqrt(to ** 2 + 20 ** 2) / 10; //to를 사용해 대각선 길이 도출
또한 상승 및 하강에 따라 + / - 를 구분 및 포인트에 도달시 i(인덱스)++
if (dok === 'up') {
// 상승
y += pointY; // 계수 추가
if (y >= point[i]) {
//목표지점 도달시 => 다음 데이터 i 인덱스 ++
dok = 'down';
i++;
}
}
if (dok === 'down') {
// 하강
y -= pointY; // 계수 빼기
if (y <= point[i]) {
//목표지점 도달시 => 다음 데이터 i 인덱스 ++
dok = 'up';
i++;
}
}
이제 그래프에 표시될 데이터가 들어있는 배열 point
를 사용할 수 있는 형태로 함수를 수정한다.
let x = 0;
let y = 0;
let i = 0;//인텍스
let first = true;
let dok = 'up';
const loopAnimate = () => {
if (i !== point.length) {
//point배열의 인덱스가 길이와 같게 된다면 정지.
requestAnimationFrame(loopAnimate);
}
const ctx = ref.current?.getContext('2d');
if (ctx) {
ctx?.clearRect(0, 0, 300, 200);
if (first) {
//함수가 반복되기 때문에 첫번째에 한번만 설정,
//이렇게 하지 않으면 모든 데이터 표시가 (0,0)으로 부터 시작되게 된다.
ctx.beginPath();
ctx.moveTo(0, 0);
first = false;
}
if (ctx !== undefined && ctx !== null) {
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
}
let to = point[i];
if (i > 0) {
to = Math.abs(point[i - 1] - point[i]);
//각 포인트 사이의 차이(y)를 계산
}
const pointY = Math.sqrt(to ** 2 + 20 ** 2) / 10;
//각 포인트 to까지 대각선 길이 계산
ctx?.lineTo(x, y);
ctx?.stroke();
x += 2;
//데이터탕 넓이는 20, 위 Math.sqrt()에서 10으로 나눈 이유.
if (dok === 'up') {
// 상승
y += pointY; // 계수 추가
if (y >= point[i]) {
//목표지점 도달시 => 다음 데이터 i 인덱스 ++
dok = 'down';
i++;
}
}
if (dok === 'down') {
// 하강
y -= pointY; // 계수 빼기
if (y <= point[i]) {
//목표지점 도달시 => 다음 데이터 i 인덱스 ++
dok = 'up';
i++;
}
}
}
};
위 코드는 이전 포스트에서 잠깐 설명했던 seperatePoint를 사용하여 상승 / 하강지점의 데이터를 기반으로 사용이 가능한 코드이다.