[Canvas] #05. Movement

undefcat·2020년 3월 21일
0

canvas

목록 보기
5/6
post-thumbnail

Flip Animation

본격적으로 움직임을 표현하기에 앞서, 애니메이션이 무엇인지부터 이해해보자. 애니메이션을 쉽게 이해하는 것 중 하나가 플립 애니메이션이라고 생각한다.

유튜브에 Flip Animation을 검색한 결과 중 아무 영상 하나를 살펴보도록 하자. 찾기가 귀찮은 분들은 이 링크를 클릭해보시길 바란다.

애니메이션이란 매우 간단하다. 정지된 그림을 연속적으로 보여주면 그게 애니메이션이다.

캔버스 애니메이션도 똑같다. 정지된 이미지를 조금씩 변화를 주면서 매번 새로 그리면, 그게 바로 애니메이션이다.

이전 챕터에서 속도 벡터를 배웠는데, 속도 벡터는 위치의 변화량이라고 했다. 매 단위시간마다 도형의 위치값을 바꾸면서 그리면 그게 움직임이 되는 것이다.

Circle

이제 움직이는 원을 한 번 구현해보자.

우선, 원을 나타내는 클래스를 하나 정의한다.

class Circle {
  constructor(x, y, radius) {
    this.x = x
    this.y = y
    this.radius = radius
  }
}

캔버스에서 원을 그렸던 arc메서드를 생각해보면, 원이라는 것은 x y 좌표에 반지름 radius를 그리면 되는 것이었다. 그래서 이런 원의 속성을 가진 클래스를 하나 정의했다.

이제 이 원을 그리는 메서드인 draw를 정의해보자. draw 메서드는 캔버스에 현재 Circle 객체의 프로퍼티 값을 이용해 원을 표현한다.

class Circle {
  // constructor
  
  draw() {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2)
    ctx.fill()
  }
}

매우 간단하다. 여기서 ctxclass가 정의된 스코프에 있는 ctx를 뜻한다.

지금까지의 코드를 합쳐보면 아래와 같아야 한다.

const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')

canvas.width = window.innerWidth
canvas.height = window.innerHeight

class Circle {
  constructor(x, y, radius) {
    this.x = x
    this.y = y
    this.radius = radius
  }
  
  draw() {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2)
    ctx.fill()
  }
}

그 다음으로, 원을 캔버스에 그려보자.

const c = new Circle(200, 200, 50)
c.draw()

참으로 쉽다.

원의 속성을 가진 객체를 하나 생성하고, 그 객체를 그리는 메서드 draw를 정의했으므로 그냥 메서드를 호출만 해주면 된다.

애니메이션 입문

플립 애니메이션에서 봤듯이, 애니메이션을 표현하려면, 단위시간동안 매번 새로운 도화지에 이전에 그린 그림보다 미세하기 바뀐 그림을 새로 그리면 된다.

플립 애니메이션에서의 단위시간은 한 장, 한 장을 넘기는 시간일 것이고 그 한 장, 한 장이 바로 새로운 도화지다.

마찬가지로 캔버스에서 단위시간새로운 도화지를 어떻게 구현할지를 생각해보면 된다.

clearRect

우선 새로운 도화지부터 구현해보자. 캔버스의 clearRect 메서드가 바로 이 역할을 해줄 수 있다.

이 메서드의 인터페이스는 아래와 같다.

void ctx.clearRect(x, y, width, height)

x y부터 width height에 이르는 캔버스의 영역을 지워버린다. 매 번 새로운 도화지를 그리려면, 캔버스 모든 영역을 지워야 한다. 따라서

ctx.clearRect(0, 0, canvas.width, canvas.height)

위와 같이 호출하면 도화지가 지워질 것이다.

c.draw()
ctx.clearRect(0, 0, canvas.width, canvas.height)

위와 같이 호출해보면, 아주 잠깐 원이 그려졌다가 금새 사라지는 것을 볼 수 있다.

requestAnimationFrame

이제 단위시간을 표현하면 되는데, 알다시피 브라우저에서 setInterval을 이용하면 매 ms마다 호출되는 함수를 구현할 수 있다. 물론 이 setInterval을 사용할 수도 있겠지만, 렌더링에 특화된 requestAnimationFrame 메서드를 사용하도록 하자.

간단히 설명하자면, 이 메서드는 60fps에 맞춰서 동작하는 특징이 있다. 즉, 1초당 60번 호출된다고 보면 된다. 물론 최대한 60fps에 동작하는 것이고, 브라우저가 어떤 작업을 하느냐에 따라 덜 호출될 수도 있다. (fps는 Frames Per second로, 1초당 프레임의 수를 나타낸다. 60fps는 1초당 60프레임이라는 뜻이다.)

requestAnimationFrame을 이용해서 움직임을 표현하려면, 우리는 이를 재귀 함수의 형태로 구현을 해야 한다.

1초에 60번을 호출하고자 한다. 이를 만약 setInterval로 구현한다고 하자. 그러면

setInterval(function () {
  console.log('called!')
}, 1000/60)

위와 같이 구현하면 콘솔창에 called가 1초에 60회 정도 찍힐 것이라고 생각할 수 있다.

requestAnimationFrame은 브라우저에서 requestAnimationFrame의 매개변수로 넘겨진 함수를 1초에 60회의 빈도로 조절해서 호출하게 해준다. 즉, requestAnimationFrame을 1초 안에 1000번 호출한다고 하더라도 브라우저는 이를 60fps에 맞게 호출해준다는 것이다.

따라서, setInterval과는 약간 다른 방식으로 구현을 해야한다.

우선 렌더링하는 함수를 하나 정의하고, 이 함수가 requestAnimationFrame을 호출하면서 자기 자신을 호출하도록 두면 된다. 그러면 1초에 60번 호출될 것이다!

function render() {
  // ...
  // ctx를 이용해 캔버스에 그리는 작업들
  
  // 다음 렌더링 주기에 자기 자신을 호출하도록 함
  requestAnimationFrame(render)
}

render() // 최초 호출이 필요하다.

위와 같이 구현하면, 우선 render가 호출되면서 ctx를 이용해 캔버스에 그림을 그릴 것이다.

그 뒤, requestAnimationFrame에 자기 자신을 또 호출하도록 둔다. 그러면 브라우저는 60fps에 맞게 그 다음 시간에 render를 호출할 것이다. 그러면 render는 또 다시 requestAnimationFrame에 자기 자신을 다음 렌더링 주기에 호출하도록 할 것이다. 이렇게 재귀적인 구조로 구현해야 한다.

구현

필요한 준비물은 다 갖췄다. 이제 구현만 하면 된다. 우리는 대략 아래와 같은 단계가 필요하다.

  1. 원을 생성한다.
  2. 그린다.
  3. 도화지를 지운다.
  4. 1에서 생성한 원의 위치를 바꾼다.
  5. 그린다.
  6. 도화지를 지운다.
  7. 1에서 생성한 원의 위치를 바꾼다.
  8. 그린다.
  9. 도화지를 지운다.
  10. 1에서 생성한 ...

즉, 원을 생성한 후, 그리기 -> 위치조절 -> 지우기 -> 그리기 -> ...

위와 같은 작업을 반복만 하면 된다.

이제 코드를 차근차근 구현해보자.

const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')

// 화면의 너비
const W = canvas.width = window.innerWidth

// 화면의 높이
const H = canvas.height = window.innerHeight

class Circle {
  constructor(x, y, radius) {
    this.x = x
    this.y = y
    this.radius = radius
  }
  
  draw() {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2)
    ctx.fill()
  }
}

// 화면의 중간 왼쪽 끝에 있는 원을 하나 정의한다.
const c = new Circle(0, H/2, 50)

// 그리는 함수
function render() {
  // 1. 원의 위치를 조절한다.
  c.x += 1
  
  // 2. 도화지를 지운다.
  ctx.clearRect(0, 0, W, H)

  // 3. 그린다.
  c.draw()
  
  // 4. 1부터 다시 반복하도록 raf 함수 호출
  requestAnimationFrame(render)
}

// 최초 호출!
render()

결과물은 직접 확인할 수 있을 것이다.

(구현의 순서가 약간 달라졌는데, 그 이유는 만약 위치를 조절하는 계산값이 시간이 오래 걸리는 작업이라면 도화지를 지우고 -> 계산하고 -> 그릴 때, 계산작업이 오래걸리게 되어 반짝거릴 수도 있기 때문이다.)

다음은?

드디어 도형을 움직이게 구현해보았다. 사실 여기까지 했다면, 이제 다른 내용은 큰 필요가 없다. 캔버스에서 도형을 움직이게 하기 위해서는 이전 챕터에서 배웠던 벡터를 통해 움직임을 구현하기만 하면 된다. 이런 움직임을 물리, 수학적으로 표현하는 방법만 안다면 나머지는 스스로의 몫이다.

다음 챕터부터는 이전 챕터에서 배웠던 속도벡터를 이용해 원이 특정한 방향으로 움직이도록 구현해 볼 것이다.

profile
undefined cat

0개의 댓글