[모던JS: 심화] 애니메이션

KG·2021년 7월 4일
1

모던JS

목록 보기
45/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

베지어 곡선

베지어 곡선(Bezier Curve)은 컴퓨터 그래픽스에서 사용되는 특별한 형태의 곡선으로 CSS 애니메이션 등에서 도형을 그릴 때도 사용할 수 있다. 곡선 자체는 단순하지만, 이를 기반으로 벡터 그래픽스나 고급 애니메이션에서도 많이 응용되고 있다.

1) 조절점

베지어 곡선은 조절점(Control Point)을 사용해 정의한다. 조절점의 개수는 2개나 3개, 4개가 될 수 있으며 이 보다 더 많을 수도 있다. 정확히는 개수 제한이 존재하지 않는다. 조절점이 2개인 경우는 우리가 흔히 아는 직선이 그어진다. 이름은 여전히 베지어 곡선이지만 조절점이 두 개인 경우엔 직선 형태라는 점에 주의하자.

  • 조절점이 2개인 베지어 곡선

해당 형태만 본다면 조절점은 선분을 잇는 꼭지점의 역할로 착각할 수 있다. 그러나 조절점은 꼭지점이 아니다. 정확히는 어떤 특정 위치에서 선분을 잡아당기는 역할을 한다고 보면 쉽다. 조절점이 3개일때와 4개일때를 각각 살펴보자.

  • 조절점이 3개인 베지어 곡선

  • 조절점이 4개인 베지어 곡선

이 곡선들을 자세히 살펴보면 베지어 곡선은 다음의 특징을 가진다는 것을 알 수 있다.

  1. 조절점이 항상 곡선위에 위치하지 않는다. 이는 앞서 말했듯이 조절점은 꼭지점의 개념이 아니기 때문이다. 따라서 이는 아주 자연스러운 현상인데, 아래쪽에서 어떻게 곡선이 만들어지는지 살펴보도록 하자.

  2. 곡선의 차수(degree)는 조절점의 개수에서 1을 뺀 값이다. 즉 조절점이 두 개일땐 직선, 세 개일땐 2차 곡선, 네 개일땐 3차 곡선으로 분류할 수 있다.

  3. 곡선은 항상 조절점의 컨벡스 헐(Convex Hull)안에 위치한다.

컨벡스 헐(Convex Hull)은 볼록 껍질이라고 표현하기도 하는데, 2차원 평면에 N개의 점이 주어졌을 때 이들 중 몇 개의 점을 골라 볼록 다각형을 만들 때 나머지 모든 점을 내부에 포함하도록 하는 다각형을 의미한다. 베지어 곡선에서의 컨벡스 헐은 가까운 조절점끼리 모두 이은 형태가 된다. 컨벡스 헐과 관련된 알고리즘도 종류가 많은데 흥미가 있다면 찾아보는 것을 추천한다.

3번 특성 때문에 컴퓨터 그래픽스 분야에서 교차 검사(Intersection test) 최적화가 가능하다. 컨벡스 헐이 교차하지 않는다면 곡선 역시 서로 교차하지 않기 때문이다. 보통 곡선의 형태보다 컨벡스 헐의 형태가 더 단순하기 때문에, 컨벡스 헐의 교차 여부로 매우 빠르게 곡선 교차 여부를 확인할 수 있다.

조절점을 움직이면 베지어 곡선은 직관적으로 보았을 때, 아주 당연한 방식으로 다시 그려진다. 일단은 지금 당장 어떤 내부 메커니즘에 의해 조절점이 곡선 경로를 결정하는지 이해하기 힘들더라도, 직접 조절점을 움직여보며 곡선 경로의 변화가 생기는 것을 살펴보자.

아래 등장하는 모든 svg 형식의 이미지는 모두 https://ko.javascript.info/bezier-curve에서의 실제 데모의 캡처본이다. 실제 움직이는 기능을 벨로그 마크다운 기능만으로 가져올 수 없기 때문에, 데모 시연은 위 사이트로 직접 이동하여 하는 것을 추천한다.

위 예시를 통해 조절점이 곡선 경로에 정확히 어떤 계산을 통해 영향을 주는지 파악하기 힘들지만, 베지어 곡선이 조절점을 이은 선 1-23-4를 향해 뻗어나간다는 점을 확인할 수 있다. 이런 특징을 염두에 두고 연습하다보면 원하는 형태의 곡선을 만들 때 어디에 조절점을 두어야 할 지 쉽게 파악할 수 있을 것이다.

2) 카스텔조 알고리즘

베지어 곡선을 정의하는 수학 공식은 나중에 알아보고, 먼저 베지어 곡선을 정의하는 공식과 정확히 일치하며 곡선이 만들어지는 과정을 시각화하는데 도움을 주는 카스텔조 알고리즘(De Casteljau's Algorithm)에 대해 알아보자.

먼저 조절점이 3개인 예시를 살펴보자. 다음 예시에서는 조절점 1, 2, 3을 직접 움직여가면서 재생버튼을 통해 어떻게 곡선이 만들어지는지 가시적으로 관찰할 수 있다. (링크에서 실제로 확인하도록 하자)

조절점이 3개인 베지어 곡선을 만드는 카스텔조 알고리즘은 다음과 같다.

  1. 조절점을 먼저 좌표에 찍는다. 위 예시에서 각 조절점은 1, 2, 3으로 표현되고 있다.

  2. 조절점 12, 3을 각각 잇는 선분(갈색선)을 그린다. 이는 베지어 곡선의 컨벡스 헐을 의미한다.

  3. 매개변수 t의 값을 0부터 시작해 1이 되도록 증가한다. 데모에서는 재생 버튼을 누르면 0.05씩 증가하게 되어있다. t값을 이용해 다음과 같은 추가 작업이 가능하다.

    • 갈색으로 표시한 각 선분의 시작점에서 t와 비례하는 위치에 점을 찍는다. 선분이 두 개이기 때문에 점 두개가 만들어진다.
    • 추가작업을 통해 만든 두 점을 연결한다. 그림에서는 파란선에 해당한다.
    • 예를 들어 t0일 땐 두 점이 선분의 시작인 조절점 12에 위치하게 된다. t0.25일 땐 조절점 1에서 조절점 12를 이은 선분 길이에 25%에 해당하는 지점에 점이 위치한다. t0.5라면 선분의 중간, 1이라면 선분 끝에 점이 위치하게 될 것이다. 이를 그림으로 나타내면 아래와 같다.
t=0.25 t=0.5
  1. 이제 파란색 선분에서 t값과 비례하는 위치에 점 하나를 찍는다. t0.25일 때(왼쪽 그림)는 파란 선분의 1/4 지점에 빨간점이 위치하고, t0.5일 때(오른쪽 그림)는 선분 가운데에 빨간점이 위치한다.

  2. t0에서 1로 증가하면서 점이 계속 추가될 것이다. 이때 이 점들이 모여 만들어진 곡선이 바로 베지어 곡선(빨간색 포물선)이 된다.

카스텔조 알고리즘은 다양한 차수의 베지어 곡선에 모두 적용할 수 있다. 카스텔조 알고리즘은 재귀적이기 때문에 조절점 개수에 상관없이 일반화가 가능하다. 만약 N개의 조절점이 있다면 알고리즘은 다음과 같이 설명할 수 있다.

  1. N개의 조절점을 연결하여 N-1개의 선분을 만든다.
  2. 0부터 시작해 1이 될 때까지 t를 조금씩 증가시키면서 각 선분에서 t만큼 비례한 곳에 점을 찍고, 그 선을 연결한다. 이때 N-2개의 선분이 만들어 질 것이다.
  3. 점이 하나만 남을때까지 2번을 반복한다. (점이 마지막 조절점에 당도하는 순간)

다음은 조절점이 4개인 경우에 카스텔조 알고리즘을 적용하여 베지어 곡선이 만들어지는 과정을 살펴보자.

  • 조절점 12, 23 그리고 34를 연결하는 선분을 만든다. 총 3개의 갈색 선분이 만들어진다.

  • t0부터 시작해 1까지 증가시키면서 각 선분에 다음 작업을 수행한다.

    • 선분 시작점에서 t에 비례하는 위치에 점을 찍는다. 이 점들을 연결해 두 개의 녹색선을 만든다.
    • 두 개의 녹색 선분 위에 t에 비례하는 위치에 점을 찍는다. 이 두 점을 연결하는 파란색 선을 그린다.
    • 파란색 선에서 t에 비례하는 위치에 빨간점을 찍는다.
  • 빨간점이 여러개가 모여 곡선을 형성한다.

해당 알고리즘을 잘 상기하며 다음 예시들을 살펴보자. 위의 순서로 모든 예시를 설명할 수 있다. 이해가 잘 가지 않는다면 중간중간 예시를 정지시켜 가며 변화과정을 하나씩 직접 계산해보는 것도 좋다. 링크에서 다음 목록의 곡선들의 데모를 실제로 확인해 볼 수 있다.

  • y=1/t 형태를 띠는 곡선

  • 갈 지()자 형태의 조절점

  • 고리 모양 베지어 곡선

  • 뾰족한 베지어 곡선

조절점이 몇 개이든 카스텔조 알고리즘은 재귀적이기 때문에 모두 적용이 가능하다. 그러나 실무에서는 실제로 여러개의 조절점을 사용하여 베지어 곡선을 만들지는 않는다. 조절점이 많을 수록 곡선의 형태를 예측하기 복잡해질 수 있기 때문이다. 따라서 보통 2-3개의 조절점을 사용해 곡선을 만들고, 해당 곡선들을 여러개 이어 붙여 복잡한 형태의 곡선을 형성하는 경우가 많다. 이를 통해 개발과 계산을 모두 간단하게 만들 수 있다.

3) 수학

베지어 곡선은 하나의 수학 공식을 사용해 설명할 수 있다. 물론 해당 공식을 모르더라도 마우스로 조절점을 움직여가며 원하는 형태의 곡선을 만드는데 아무 지장이 없다. 따라서 흥미가 있는 사람들만 해당 챕터를 간단하게 읽어보기를 추천한다.

조절점 의 좌표에 대하여 첫 번째 조절점의 좌표를 P₁ = (x₁, y₁), 두 번째 조절점의 좌표를 P₂ = (x₂, y₂)라고 정의했을 때, 베지어 곡선을 이루는 점의 좌표들은 [0, 1] 사이의 매개변수 t 값에 의해 결정된다.

  • 조절점이 2개 일 때

    P = (1-t)P₁ + tP₂

  • 조절점이 3개 일 때

    P = (1-t)²P₁ + 2(1-t)tP₂+ t²P₃

위 식들은 벡터 방정식으로, 곡선을 이루는 점의 좌표를 얻고자 할 땐, P 대신 xy를 넣으면 된다. 조절점이 3개인 경우, 곡선은 각 점의 좌표 (x, y)를 사용해 만들어진다.

  • x = (1-t)²x₁ + 2(1-t)tx₂ + t²x₃
  • y = (1-t)²y₁ + 2(1-t)tx₂ + t²y₃

0부터 시작해 1이 될 때까지 t를 증가시켜 무수히 많은 (x, y) 조합을 만들고 이를 연결해 곡선을 만들수 있다.

더 자세한 사항은 그만 알아보자....

베지어 곡선의 사용처는 매우 다양하다. 컴퓨터 그래픽스, 모델링, 벡터 그래픽 에디터, 폰트 제작과 같이 그래픽을 다루는 전문적인 분야에서부터 canvas 요소나 SVG 포맷을 이용해 웹 개발을 할 때도 사용할 수 있다. 또한 이어서 살펴볼 CSS 애니메이션의 경로와 속도를 조절할 때도 사용할 수 있다.

CSS 애니메이션

CSS 애니메이션을 사용하면 자바스크립트 없이도 간단한 애니메이션을 만들 수 있다. 여기에 자바스크립트를 더하면 CSS 애니메이션을 더 세밀하게 조정할 수 있고, 짧은 코드로도 훨씬더 효과적인 애니메이션을 만들 수 있다. 먼저 CSS 속성을 활용해 애니메이션을 만드는 방법부터 살펴보도록 하자.

1) CSS 트랜지션

CSS 트랜지션이 등장하게 된 배경은 단순하다. 애니메이션 관련 프로퍼티를 만들고, 프로퍼티 값을 통해 애니메이션 효과를 정의해 보자는 데에서 출발했다. 프로퍼티가 변하면 브라우저가 이를 자동으로 감지하고 프로퍼티 값에 해당하는 애니메이션을 화면에 보여주는 메커니즘을 가지고 있다.

때문에 개발자는 단지 CSS 속성의 값만 변경하는 것으로 브라우저에게 관련 애니메이션 동작을 위임할 수 있다. 값이 변경되면 자연스러운 트랜지션 효과는 브라우저가 알아서 처리하게 된다. 예를 들어 다음 예시에서는, 3초 동안 background-color가 서서히 변하게 된다.

.animated {
  transition-property: background-color;
  transition-duration: 3s;
}

해당 CSS를 어떤 요소에 적용하면 해당 요소의 background-color가 3초간 변하게 될 것이다. 다음은 HTML을 이용해 버튼 클릭 시 해당 요소의 배경색이 3초간 전환 효과를 보이는 예시이다.

<button id='color'>클릭</button>

<style>
  #color {
    transition-property: background-color;
    transition-duration: 3s;
  }
</style>

<script>
  color.onclick = function() {
    this.style.backgroundColor = 'red';
  }
</script>

CSS 트랜지션에 사용되는 프로퍼티는 다음과 같이 네 가지가 있다.

  • transition-property
  • transition-duration
  • transition-timing-function
  • transition-delay

각 프로퍼티에 대해서는 아래에서 자세히 다루어보자. 트랜지션 효과는 각각의 프로퍼티를 따로 선언해서 지정할 수도 있지만 transition 프로퍼티를 통해 한 번에 위 속성을 모두 적용할 수도 있다. transiton 프로퍼티에 넣는 값은 앞에서부터 순서대로 property, duration, timing-function, delay에 해당되며, 지정하고 싶지 않은 옵션은 생략이 가능하다.

아래와 같이 transition 프로퍼티를 정의하면 font-size는 3초 동안, color는 2초 동안 변화하는 것을 확인할 수 있다.

<button id='growing'>클릭</button>

<style>
  #growing {
    transition: font-size 3s, color 2s;
  }
</style>

<script>
  growing.onclick = fucntion() {
    this.style.fontSize = '36px';
    this.style.color = 'red';
  }
</script>

2) transition-property

transition-property 프로퍼티엔 left, margin-left, height, color 같이 애니메이션 효과를 적용할 프로퍼티 목록을 정의한다.

이때 주의할 점은 모든 프로퍼티에 애니메이션을 적용할 수 없다는 것이다. 만약 내가 어떤 애니메이션 효과를 지정하고자 했는데, 브라우저가 이를 제대로 인식하지 못한다면 이는 지원되지 않는 프로퍼티일 확률이 높다. 흔히 사용되는 프로퍼티 대다수에는 애니메이션 효과를 적용할 수 있다. 전체 목록은 다음 링크에서 확인할 수 있다.

3) transition-duration

transition-duration 프로퍼티엔 애니메이션 효과를 얼마 동안 줄지 설정한다. 시간은 CSS 시간 형식을 따라야 하는데, 보통 초 단위(s)나 밀리초 단위(ms)를 많이 사용한다.

4) transition-delay

transition-delay 프로퍼티엔 애니메이션 효과가 시작되기 전에 얼마만큼의 지연시간을 줄 지 설정한다. transition-delay 값을 1s로 설정하면 애니메이션 효과는 1초후에 나타난다.

transition-delay 값에는 음수도 넣을 수 있는데, 값이 음수일 땐 애니메이션 효과가 중간부터 나타난다. transition-duration2s, 지연시간을 -1s로 설정한다면, 애니메이션 효과는 1초가 지난 후 1초 동안 지속되게 될 것 이다.

CSS의 translate 프로퍼티에 트랜지션 효과를 부여해, 0부터 9까지 화면에 나타나도록 애니메이션을 설정해보자.

<body>

  아래 숫자를 클릭하세요.

  <div id="digit"><div id="stripe">0123456789</div></div>
  
  <script>
    stripe.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>
  
</body>
#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
}
                       

숫자를 클릭하면 자바스크립트가 실행되면서 숫자가 들어있는 요소에 stripe 클래스가 추가됨으로써 애니메이션 효과가 적용된다.

이번엔 transition-delay에 음수를 써서 예시를 약간 변형해보자. 현재 시각을 기준으로 초를 추출하고, 이 값에 마이너스 기호를 붙여 transition-dealy 값으로 지정하면 현재 초를 기준으로 숫자가 나타나고 애니메이션 효과가 적용되는 것을 확인할 수 있다.

만약 버튼을 클릭했을 때 시간이 16시 45분 18초라면 숫자 8부터 트랜지션 효과가 적용되어 애니메이션이 나타날 것이다.

stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
}

5) transition-timing-function

transition-timing-function 프로퍼티를 사용해 timing 함수를 만들면 시간에 따라 애니메이션 효과를 어떻게 분배할 지 설정할 수 있다. 애니메이션 효과가 초반에는 천천히 나타나다가 중간에 빠르게 나타난다던지, 이와 반대되는 효과 등을 지정할 수 있다.

해당 프로퍼티는 transition을 지원하는 프로퍼티 중에 가장 복잡하다고 볼 수 있는데, 관련 내용을 차근차근 하나씩 같이 살펴보도록 하자.

transition-timing-function 프로퍼티 값에는 이전 챕터에서 다룬 베지어 곡선(bezier curve)이나 단계(step)이 올 수 있다. 먼저 사용빈도가 높은 베지어 곡선을 사용하는 예시부터 살펴보자.

1) 베지어 곡선

transition-timing-function엔 조절점이 4개 이면서 아래 조건을 만족하는 베지어 곡선을 지정할 수 있다.

  1. 첫 번째 조절점: (0, 0)
  2. 마지막 조절점: (1, 1)
  3. 중간 조절점들: x01 사이에 있어야 하나, y에는 제약이 없음

CSS에서는 베지어 곡선을 cubic-bezier(x2, y2, x3, y3) 형태로 정의한다. 규칙에 따라 첫 번째 조절점은 (0, 0), 네 번째 조절점은 (1, 1)로 고정되므로 두 번째와 세 번째 조절점만 개발자가 설정하면 된다.

조절점을 변경해 만든 베지어 곡선을 사용해 정의한 timing 함수는 시간이 지남에 따라 얼마나 빠르게 애니메이션 효과가 나타나게 할 지 보여준다.

  • x 축은 시간이 된다. 0은 애니메이션을 시작하는 시간이고, 1은 끝나는 시간을 나타낸다.
  • y 축은 프로세스 완성 정도를 나타낸다. 0은 프로퍼티 시작값, 1은 최종값을 나타낸다.

가장 간단한 timing 함수는 linear 형태로 일정한 속도의 애니메이션 효과를 나타낸다. 이는 cubic-bezier(0, 0, 1, 1)을 사용해 나타내는데, 해당 의미는 결국 기울기가 1인 직선을 나타내는 것과 같다.

시간(x)이 지나면서 애니메이션의 완성도(y)는 0에서 1로 꾸준히 올라간다. 다음은 요소를 클릭했을 때 해당 요소가 일정한 속도로 왼쪽에서 오른쪽으로 이동하게 하는 예제이다. (실제 작동 사례는 링크에서 확인할 수 있다)

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
}

반대로 기차를 점점 느리게 이동하도록 만들어보자. 이는 다른 베지어 곡선을 이용해 만들 수 있는데, cubic-bezier(0.0, 0.5, 0.5, 1.0)을 통해 해당 효과를 지정할 수 있다. 이때 만들어지는 베지어 곡선의 모양은 다음과 같다.

.train {
  left: 0;
  // 소수를 적을때 앞의 0은 생략이 가능하다 eg. 0.5 -> .5
  transition: left 5s cubic-bezier(0, .5, .5, 1);
}

그래프에서 볼 수 있듯이 기차는 초반에 빠르게 진행되다가 점점 느리게 이동할 것이다. x축은 시간진행도, y축은 애니메이션 완성도를 나타내기 때문에, 위의 기울기가 1인 그래프에 비해 더 짧은 시간에 더 높은 지점(= 완성도)에 도달하는 것을 볼 수 있다. cubic-bezier(0.0, 0.5, 0.5, 1.0) 속성으로 설명하면 처음 0초에 이미 애니메이션 진행도는 0.5로 지정이 되었기에 초반 진행이 더 빠르게 시작한다는 것을 알 수 있다. 반대로 세 번째 조절점은 0.5초 시간대에 이미 완성된 애니메이션 값이 지정되었기 때문에 진행이 1을 향해 천천히 나아갈 것이라는 것을 알 수 있다.

때로는 일일이 베지어 곡선을 이용해 그래프를 그려가며 애니메이션의 속도를 설정하는 것이 번거롭게 느껴질 수 있다. 때문에 CSS에는 이미 완성된 형태의 내장 곡선을 제공한다. 내장 곡선 프로퍼티 값은 다음과 같은 것들이 있다.

  • linear
  • ease-in
  • ease-out
  • ease-in-out

여기서 linear는 가장 처음에 살펴본 cubic-bezier(0, 0, 1, 1)과 동일한 의미를 가진다. 각 내장 곡선에 대응하는 베지어 곡선을 나타내면 다음 표와 같다.

ease*ease-inease-outease-in-out
(0.25, 0.1, 0.25, 1.0)(0.42, 0, 1.0, 1.0) (0, 0, 0.58, 1.0)(0.42, 0, 0.58, 1.0)

*로 표시한 내장곡선 easetiming 함수가 없을 때 기본값으로 사용되는 값이다. ease-out을 사용해서도 기차가 점점 느려지게 하는 애니메이션 효과를 만들 수 있다. 다만 위에서 cubic-bezier를 이용해 만들었을 때와 조절점이 다르기 때문에 당연히 어느정도 차이는 존재한다.

.train {
  left: 0;
  transition: left 5s ease-out;
}

한편 베지어 곡선을 사용하면 애니메이션이 지정한 범위를 넘어서 적용되게 할 수도 있다. 이는 베지어 곡선에서 중간 조절점인 y는 아무런 제약이 없기 때문에 가능한 것인데, 음수나 아주 큰 값을 지정하게 되면 베지어 곡선이 매우 낮거나 높게 그려질 수 있다. 이때 애니메이션은 그래프 상에서 정상 범위를 벗어나게 되기에 조금 더 풍부한 애니메이션 효과를 만들어줄 수 있다.

예를 들어 기차 예시에 다음과 같은 CSS를 적용했다고 가정해보자.

.train {
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
}

해당 CSS가 적용되면 left 프로퍼티가 100px에서 400px로 점차 변해야 한다. 그런데 기차는 베지어 곡선에 의해 다음과 같이 동작할 것이다.

  • 먼저 기차가 뒤로 이동한다. 이때 left 값은 100px 보다 조금 안 되게 설정된다.
  • 그리고 나서 다시 기차는 앞으로 이동한다. 이때 left 값은 400px보다 조금 크다.
  • 그리고 최종적으로 left400px일 때 위치로 후진한다.

이는 위에서 만든 베지어 곡선이 아래와 같은 그래프를 만들기 때문이다.

두 번째 조절점의 y 좌표가 0 보다 작아지고, 세 번째 조절점의 좌표가 1 보다 커지면서 범주를 벗어난 움직임을 보인 것이다. timing 함수의 y 축은 애니메이션 프로세스의 완성도를 나타내기 때문에 이 값이 0 보다 작아지면 left 프로퍼티가 시작 값인 100px 보다 낮게 설정되고, 반대로 1 보다 커지면 left 프로퍼티가 끝값인 400px 보다 커지는 현상이 생기게 된 것이다.

위 예시에서는 y의 값이 정상 범위를 아주 크게 벗어난 경우가 아니기 때문에 부드럽게 전환이 일어난다. 그러나 만약 의도치 않게 y 값이 정상 범위를 너무 크게 벗어나는 경우에는 매우 큰 움직임이 발생할 것이고 따라서 부자연스러운 애니메이션 효과가 될 수 있다.

사실 개발자가 일일이 베지어 곡선 그래프를 그려가며 원하는 효과를 설정하는 것은 매우 번거롭고 까다로운 일이다. 때문에 내장 곡선을 제공해주기도 하지만, 조금 더 세밀하게 효과를 조정하고 싶을 수 있다. 그럴땐 이를 도와주는 여러 사이트를 이용하는 것도 좋은 대안이 될 수 있다. 이를 도와주는 툴은 매우 다양한데 https://cubic-bezier.com 에서 이러한 작업이 가능하다.

2) 단계 (step)

timing 함수 steps(number of steps[, start/end])를 사용하면 애니메이션을 여러 단계로 나눌 수 있다. 처음에 살펴본 숫자 출력 애니메이션을 다시 들여다보자.

일단 애니메이션을 적용하기 전 상태를 생각해보자. 애니메이션을 적용하기 전에는 아래 이미지와 같이 빨간 박스 외부에 있는 숫자들은 overflow: hidden 등의 CSS 속성으로 숨김처리를 해주고, 단계 별로 숫자 목록을 왼쪽으로 이동시켜 사용자에게 숫자가 하나씩 증가하는 듯한 애니메이션을 부여할 것이다. 이때 숫자는 총 9까지 증가시켜야 하므로 단계는 모두 9개가 된다.

#stripe.animate {
  transform: translate(-90%);
  transition: transform 9s steps(9, start);
}

timing 함수 steps(9, start) 에서 첫 번째 인수는 단계의 수이므로 전체 프로세스가 9단계로 나눠 적용되어 10%씩 왼쪽으로 이동하는 것을 확인할 수 있다. 이때 시간 간격 역시 자동으로 9단계로 나뉘기 때문에, 1초 간격으로 숫자가 증가한다. (총 9s 시간을 transition-duration으로 지정했으므로)

이때 두 번째 인수는 start 또는 end를 지정할 수 있다.

두 번째 인수가 start인 경우엔 애니메이션이 첫 번째 단계부터 바로 시작된다. 숫자를 클릭할 때 애니메이션이 부여된다고 하면, 클릭 즉시 숫자가 1로 바뀌고(첫 단계), 1초 후에는 다음 숫자(다음 단계)로 바뀌는 것을 확인할 수 있을 것이다. 초별로 프로세스 진행도를 나타내면 다음과 같다.

  • 0s : 10% (애니메이션이 시작하자마자 바로 첫 단계 수행)
  • 1s : 20%
  • 2s : 30%
  • ...
  • 8s : 80%
  • 마지막 초에 최종값이 나타남

반면 두 번째 인수가 end인 경우엔 애니메이션이 바로 시작하지 않고 각 초가 끝날 때 시작된다. 이때 초별 프로세스는 아래와 같다.

  • 0s : 0%
  • 1s : 10%
  • 2s : 20%
  • ...
  • 9s : 90%

참고로 베지어 곡선의 내장 곡선과 같이 다음과 같은 내장 값을 사용해 timing 함수를 지정할 수도 있다.

  • step-start : steps(1, start)와 동일. 따라서 애니메이션의 첫 단계가 바로 시작되고 첫 단계만 실행. 때문에 애니메이션 효과가 없는 것처럼 보임

  • step-end : steps(1, end)와 동일. transition-duration의 끝에 애니메이션이 한 단계만 진행

그러나 베지어 곡선의 내장 곡선과 달리, 위의 내장 값들은 움직이는 효과를 크게 볼 수 없기 때문에 거의 사용하지 않는다.

6) transitionend 이벤트

CSS 애니메이션이 끝나면 transitionend 이벤트가 자동으로 트리거된다. transitionend 이벤트는 애니메이션이 끝났을 때 무언가를 하고 싶은 경우 유용하게 사용할 수 있다. 또는 애니메이션을 여러 개 조합할 때도 자주 사용한다.

예를 들어 아래 예시에서는 클릭된 요소가 오른쪽, 왼쪽으로 번갈아 움직이는데, 한 번 왕복할 때마다 오른쪽으로 더 멀리 움직이도록 할 수 있다. 이는 트랜지션이 종료될 때 transitionend 이벤트로 이를 캐치하고 방향을 뒤집는 함수 go를 실행시켜 새로운 애니메이션 효과를 계속 부여하기 때문이다.

boat.onclick = function() {
  // ...
  let times = 1;
  
  function go() {
    if (times % 2) {
      // 오른쪽으로 이동
      boat.classList.remove('back');
      boat.style.marginLeft = 100 * times + 200 + 'px';
    } else {
      // 왼쪽으로 이동
      boat.classList.add('back');
      boat.style.marginLeft = 100 * times - 200 + 'px';
    }
  }
  
  go();
  
  boat.addEventListener('transitionend', function() {
    times++;
    go();
  });
};

트랜지션과 관련된 이벤트는 몇 가지 특수 프로퍼티를 지원한다.

  • event.propertyName : 애니메이션이 완료된 프로퍼티로 동시에 여러 개의 프로퍼티에 애니메이션 효과를 줄 때 사용할 수 있다.

  • event.elapsedTime : transition-delay가 없다는 가정하에 애니메이션 효과에 걸린 시간(초)를 나타낸다.

7) keyframes

CSS 문법 중 @keyframes를 사용하면 간단한 애니메이션 여러 개를 한꺼번에 실행시킬 수 있다.

@keyframes엔 애니메이션 이름과 무엇을, 언제, 어디서, 어떻게 움직일 지 설정할 수 있다. @keyframes에 적절한 값을 넣은 후엔 animation 프로퍼티를 사용해 원하는 요소에 커스텀 애니메이션 적용이 가능하다. 이때 무한 반복 등의 애니메이션도 쉽게 적용할 수 있기 때문에 로딩 인디케이터를 만들때 보다 사용하기 편리하다.

<div class="progress"></div>

<style>
  @keyframes go-left-right {	/* 애니메이션 이름 지정 */
    from { left: 0px; }		/* left: 0px 부터 애니메이션 시작 */
    to { left: calc(100% - 50px); } /* left값이 100%-50px이 될때까지 애니메이션 적용 */
  }
  
  .progress {
    animation: go-left-right 3s infinite alternate;
    /* 해당 요소에 커스텀 애니메이션 go-left-right 적용
       지속 시간은 3초,
       반복 횟수는 무한(infinite),
       방향은 매번 전환(alternate)
    */
    position: relative;
    border: 2px solid green;
    width: 50px;
    height: 20px;
    background: lime;
  }
</style>

반면 특정 요소를 계속 정적인 형태로 움직이는 형태라면 트랜지션을 사용하는 경우가 많다.

8) 요약

CSS 애니메이션을 사용하면 하나 또는 여러 CSS 프로퍼티를 부드럽게 변화시킬 수 있다. 이를 적절히 적재적소에 사용한다면 UI 및 UX를 크게 향상시킬 수 있다.

CSS 애니메이션은 전환이 필요한 대다수의 경우에 큰 도움을 준다. 자바스크립트를 이용해서도 전환 효과를 동일하게 줄 수 있지만 이와 관련된 이야기는 다음 챕터에서 자세히 다루어보자. 그 전에 동일한 효과를 CSS와 자바스크립트로 모두 줄 수 있다면 왜 따로 존재하는지 의문이 들 수 있다. 이는 각각의 케이스가 장단점을 가지고 있기 때문인데, 먼저 CSS 애니메이션을 자바스크립트 애니메이션과 비교해보고 다음 챕터에서 자바스크립트 애니메이션에 대해 알아보자.

  • CSS 애니메이션의 장점 (비교: 자바스크립트 애니메이션)
장점단점
간단한 애니메이션을 간단히 수행자바스크립트 애니메이션보다 덜 유연. 요소의 폭발과 같은 특수 애니메이션은 CSS 애니메이션만으로 처리가 어려움
빠르고 CPU를 많이 소모하지 않음자바스크립트를 사용하면 프로퍼티의 변화뿐만 아니라, 애니메이션에 필요한 새로운 요소를 만들 수 있지만 CSS 애니메이션으로는 불가

자바스크립트 애니메이션

자바스크립트 애니메이션을 이용하면 CSS로 건들지 못하는 영역까지 세밀한 처리를 해줄 수 있다. 예를 들어 베지어 곡선과는 다른 timing 함수로 조금 더 복잡한 경로를 따라 움직이는 효과라던가 캔버스 위에 애니메이션을 그리는 등의 처리가 가능하다. 이번 챕터에서는 자바스크립트를 이용해 애니메이션을 부여하는 방법을 살펴보자.

1) setInterval

사실 애니메이션과 같이 어떤 움직이는 효과는 연속된 프레임의 나열이라고 볼 수 있다. 연속되는 프레임은 HTML/CSS 상에서 약간의 변화만을 가지고 쭉 나열되어 있을 것이다. 예를 들어 어떤 요소의style.left0px에서 100px로 변경하는 경우 이 값을 매 순간마다 2px 만큼 이동시킨다면 50개의 연속된 프레임이 발생할 것이고, 이를 나열하면 꽤나 자연스러운 움직임 효과를 부여할 수 있다. 매순간 주기적으로 움직임을 부여하기 때문에 우리는 앞서 배운 스케줄링 함수 중 setInterval을 이용해 관련 동작을 처리할 수 있다. 사실 이는 영화와 같은 매체에서 영상을 기록하는 방법과도 유사한 방식이다. 의사코드로 이를 나타내면 아래와 같다.

let timer = setInterval(function() {
  if (animation complete) clearInterval(timer);
  else increase style.left by 2px;
}, 20);

실제로 자연스럽게 어떤 요소가 왼쪽에서부터 오른쪽으로 이동하는 애니메이션 효과를 setInterval을 사용해 구현해보자.

let start = Date.now();	// 시작 시간을 저장

let timer = setInterval(function() {
  let timePassed = Date.now() - start;
  
  // 시작 시간으로부터 2초가 경과했다면 애니메이션 종료
  if (timePassed >= 2000) {
    clearInterval(timer);
    return;
  }
  
  // 애니메이션을 부여하는 함수
  draw(timePassed);
}, 20);

// 매 주기마다 증가하는 timePassed를 인수로 받아
// 특정 요소의 left 값을 점차 증가
function draw(timePassed) {
  train.style.left = timePassed / 5 + 'px';
}

2) requestAnimationFrame

만약 동시에 여러 애니메이션이 돌아가고 있는 상황이라고 가정해보자. 각각의 애니메이션을 모두 setInterval(..., 20)을 사용해 처리해 주었다면, 브라우저는 매 20ms 주기마다 여러개의 애니메이션 효과 처리를 위해 여러 번의 repaint 처리를 수행해야 한다. repaint는 요소의 스타일 변화로 인해, 브라우저가 해당 요소를 화면 상에 다시 출력하는 작업을 말한다.

그런데 이때 repaint 작업은 단순이 20ms 주기로 반복되는 것 보다 더 자주 발생할 수 있다. 왜냐하면 각각의 애니메이션의 시작 시간이 서로 다를 수 있기 때문이다. 따라서 서로의 간격은 setInterval로 일정하게 관리될 수 없다. 이는 브라우저가 너무 짧은 주기에 다량의 repaint를 처리하도록 요구할 수 있고, 이는 꽤나 부담스러운 작업이다. 브라우저의 부담을 덜어주기 위해 각각 독립적인 애니메이션을 하나의 주기로 모두 엮어서 실행하는 방법이 있다.

setInterval(function() {
  animate1();
  animate2();
  animate3();
}, 20);

하지만 setInterval20ms라는 주기를 항상 일정하게 보장하기 힘들 수 있다. 만약 CPU가 다른 자바스크립트 작업으로 인해 매우 바쁘게 돌아가고 있는 상황이라던가, 아니면 브라우저 탭이 숨겨져 있어 애니메이션 처리를 할 필요가 없는 상황과 같은 이유도 있기 때문에 setInterval을 통해 애니메이션 처리를 하는 것은 사실 유용하지 못하다.

이를 위해 자바스크립트는 비동기 함수 requestAnimationFrame을 제공한다. 이는 자바스크립트의 WebAPI에 속해있는 기능인데, 이를 이용해서 CSS의 트랜지션으로 처리하기 어려운 애니메이션이나 캔버스, SVG 등의 애니메이션 구현을 할 수 있음은 물론, 위에서 살펴본 setInterval의 문제 역시 모두 해결이 가능하다. requestAnimationFrame의 기본적인 문법은 아래와 같다.

let requestId = requestAnimationFrame(callback);

requestAnimationFrame을 통해 애니메이션 처리를 하게 되면, 인수로 전달되는 콜백은 브라우저가 애니메이션을 수행하려는 가장 가까운 시간에 실행된다. 또한 여러개의 애니메이션 처리를 수행하더라도 CSS 트랜지션 애니메이션과 더불어 하나의 그룹핑 된 애니메이션으로 처리되기 때문에 재계산이나 repaint가 훨씬 덜 발생하는 이점도 있다. requestAnimationFrame를 통해 애니메이션 처리후 반환되는 requestId 값은 다른 스케줄링 비동기 함수와 동일하게 애니메이션을 취소하고자 할 때 인수로 전달할 수 있다.

cancelAnimationFrame(requestId);

requestAnimationFrame에 전달되는 callback은 한 개의 인수를 받는다. 해당 인수는 일종의 timestamp 값으로 페이지 로드 시작부터 경과된 시간이 마이크로초 형태로 담겨있다. 이 시간은 performace.now()를 실행한 시간값과도 유사하기 때문에 이를 통해서도 해당 시간값을 얻을 수 있다.

전달된 callback은 CPU가 과부화 상태가 아니거나 노트북 배터리가 절전모드가 아니거나 또는 다른 특별한 이유가 없는 이상 아주 빨리 실행된다. 아래 예시는 requestAnimationFrame이 실행되는 처음 10회 실행 사이의 시간을 나타낸다. 일반적으로 10ms-20ms 사이에 콜백이 실행되는 것을 볼 수 있다.

정확히는 모니터에 주사율에 맞추어 함수를 실행하도록 내부적으로 구현되어 있다. 예를 들어 사용하는 모니터의 평균 주사율이 60FPS라면 1초에 60번, 140FPS라면 1초에 140번의 프레임 함수를 실행하게 된다. 때문에 실행환경에 따라 실행 시기는 조금씩 다를 수 있다.

let prev = performance.now();
let times = 0;

requestAnimationFrame(function measure(time) {
  document.body.insertAdjacentHTML('beforeEnd', Math.floor(time - prev) + " ");
  prev = time;
  
  if (times++ < 10) requestAnimationFrame(measure);
});

requestAnimationFrame는 앞서 브라우저가 실행 시기를 결정한다고 했다. 즉 처음에 살펴본 setInterval 과는 다르게 스스로 반복해서 호출하지 않는다. 스스로를 반복해서 호출하지 않기 때문에, requestAnimationFrame 함수로 다음 동작을 계속 반복하려면 내부에서 재귀적으로 requestAnimationFrame를 스스로 호출해주어야 한다. 때문에 위에 코드에서는 times가 10이 되기 전까지 계속 재귀호출을 하고 있는 것을 볼 수 있다.

브라우저가 실행 시기를 결정한다는 것은 또 하나의 강점이 있다. 만약 브라우저 탭이 숨김처리 되어 현재 유저가 사용하고 있지 않다면, requestAnimationFrame은 즉각 애니메이션을 잠시 보류한다. 따라서 setInerval과 같이 유저가 보고 있지 않더라도 계속 애니메이션을 처리하지 않기 때문에 repaint 측면에서 매우 효율적이다.

3) 구조화 애니메이션

이제 requestAnimationFrame을 사용해서 조금 더 보편적인 애니메이션 처리를 만들어보자. 이를 위한 보일러 플레이트(Boiler Plate)는 아래와 같다.

function animate({ timing, draw, duration }) {
  let start = performace.now();
  
  requestAnimationFrame(function animate(time) {
    // timeFraction은 0부터 1 사이의 값으로 1은 애니메이션 실행 종료
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;
    
    let progress = timing(timeFraction);
    
    draw(progress);
    
    // 애니메이션 실행이 종료되지 않았다면 재귀 호출을 통해
    // 다음 동작을 계속 부여
    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }
  });
}

animate 함수는 아래 3개의 인수를 전달받는다.

duration

애니메이션 효과가 동작하는 전체 시간을 의미한다.

timing(timeFraction)

CSS의 transition-timing-function 프로퍼티와 같이 경과한 시간의 일부(시작 시 0, 종료 시 1)를 가져오고 애니메이션의 진행결과를 반환한다. 진행결과는 베지어 곡선에서 y와 동일한 값을 의미한다.

예를 들어 linear 내장 곡선의 효과를 동일하게 적용하기 위해서는 다음과 같이 선언할 수 있다.

function linear(timeFraction) {
  return timeFraction;
}

위의 timing 함수는 다음과 같은 그래프를 형성한다. 우리는 이 함수를 이용해서 조금 더 복잡하고 세밀한 처리를 해줄 수 있는데, 자세한 사항은 잠시 후에 다시 살펴보자.

draw(progress)

애니메이션의 진행상태를 인수로 전달받아 이를 화면에 그리는 역할을 수행한다. 짧은 주기로 수행되기 때문에 애니메이션이 그려지는 듯한 효과를 나타내게 된다. progress=0인 상태는 애니메이션이 시작하는 것을 말하고, progress=1인 상태는 애니메이션이 종료된 상태라는 것을 의미한다. 해당 함수를 통해 실제로 애니메이션이 어떻게 작동할 지 정의할 수 있다. 예를 들어 다음은 왼쪽에서 오른쪽으로 이동하는 애니메이션 예시이다.

function draw(progress) {
  train.style.left = progress + 'px';
}

물론 이 보다 더 복잡하고 화려한 처리도 가능하다. 다음 코드는 요소의 width0에서 100%까지 늘리는 동작을 부여하는 애니메이션이다.

animate({
  duration: 1000,
  timing(timeFraction) {
    return timeFraction;
  },
  draw(progress) {
    elem.style.width = progress * 100 + '%';
  }
});

CSS 애니메이션과 달리 자바스크립트를 통한 애니메이션 처리는 정해진 형식에 구애받지 않고 개발자 마음대로 timing 함수와 draw 함수를 구현할 수 있다. timing 함수는 베지어 곡선에 제한받지 않고, draw는 새로운 요소를 자바스크립트를 통해 생성하여 폭죽과 같은 효과를 만들때 유용하게 사용할 수 있다.

4) timing 함수

위에서는 가장 간단한 예제인 linear한 형태의 timing 함수 예시만 살펴보았다. 이번에는 조금 더 복잡한 형태를 가진 timing 함수를 살펴보자.

1) n 제곱

만약 애니메이션의 속도를 빠르게 올리고 싶다면, timeFraction의 값을 제곱하여 리턴하면 된다.

function quad(timeFraction) {
  return Math.pow(timeFraction, 2);
}

이때 만들어지는 그래프는 다음과 같은 모습을 띠게 될 것이다.

제곱수를 높일 수록 점점 더 빨라지는 애니메이션 효과를 볼 수 있다. 예를 들어 다음은 5 제곱을 했을 때의 그래프 모양을 나타낸다.

2) 호 (arc)

function circ(timeFraction) {
  return 1 - Math.sin(Math.acos(timeFraction));
}

해당 timing 함수가 만들어내는 그래프는 아래와 같다. 이는 느리게 시작하다 중간지점부터 빨라지는 효과와 같다.

3) 활 시위 (bow shooting)

다음 timing 함수는 활 시위를 당기는 듯한 모습과 유사하다고 하여 bow shooting이라는 용어로 부른다. 활 시위를 당겼다 놓을때 생기는 파급효과와 비슷한 모양새를 보인다. 따라서 애니메이션이 부여되는 요소는 초반에는 일정량 역방향으로 움직이다가, 활 시위를 놓았을 때 화살이 날라가는 것처럼 빠른 속도로 정방향으로 애니메이션을 수행하게 될 것이다.

이전 timing 함수들과는 달리 추가적으로 x 인수를 전달받는데 이는 탄성 계수(elasticity coefficient)역할을 수행한다. 활 시위를 어느 정도 당기느냐는 해당 값에 따라 결정된다. 코드는 아래와 같다.

function back(x, timeFraction) {
  return Math.pow(timeFraction, 2) * ((x+1) * timeFraction - x);
}

x = 1.5 일때 형성되는 그래프 모양은 다음과 같다.

4) 바운스 (bounce)

공을 떨어뜨렸을 때 공이 지면에 닿아 통통 튀기는 듯한 동작을 상상해보자. 다음 timing 함수로 이러한 효과를 동일하게 부여할 수 있다. 그렇지만 지금의 timing 함수는 위의 공과 달리 바운스가 역으로 적용된다. 즉 바운스가 먼저 시작되면서 애니메이션이 진행된다. 이는 다음과 같이 구현할 수 있다.

function bounce(timeFraction) {
  for (let a = 0, b = 1, result; 1; a += b, b /= 2) {
    if (timeFraction >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
    }
  }
}

5) 탄력 애니메이션 (elastic animation)

x를 추가 인수로 입력받는데, 이 값은 초기 범위를 결정하는데 쓰인다.

function elastic(x, timeFraction) {
  return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction);
}

x = 1.5 일때 형성되는 그래프 모양은 다음과 같다.

5) timing 함수 반전 (ease*)

위에서 살펴본 timing 함수들은 내장 곡선으로 분류해보면 easeIn에 해당하는 효과로 볼 수 있다. 애니메이션이 점차 빨라지는 속도를 보이기 때문이다.

때때로 반대의 효과가 필요할 수 있다. 점점 느려지는 효과 말이다. 다음 두 케이스의 timing 함수를 통해 코드를 살펴보자.

1) easeOut

easeOut 모드를 위해 일일이 해당 효과를 다시 구현하는 것은 매우 번거로운 작업이다. 단순히 방향이 다르다는 점을 이용해서 기존해 구현했던 timing 함수를 timingEaseOut 래퍼로 감싸줄 수 있다.

timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction);

또는 makeEaseOut 형태로 easeOut 모드가 적용된 timing 함수를 반환하도록 만들어 줄 수 있다.

function makeEaseOut(timing) {
  return function(timeFraction) {
    return 1 - timing(1 - timeFraction);
  }
}

이를 통해 위에서 구현한 timing 함수들의 방향을 역으로 바꾸어 줄 수 있다. 예를 들어 바운스를 구현한 timing 함수에 이를 적용해보자.

let bounceEaseOut = makeEaseOut(bounce);

이때 만들어지는 그래프는 파란색 그래프와 같다. 비교를 위해 기존 bounce 함수는 빨간색 그래프로 표시했다.

파란색 그래프를 보면 알 수 있듯이 이젠 처음에 우리가 상상했던 공 튀기기와 같이 애니메이션 진행이 완료되고 바운스가 동작하는 것을 볼 수 있다.

2) easeInOut

또한 애니메이션의 시작과 끝 모두에서 어떤 효과를 부여할 수 있다. 이는 CSS transition-timing-function 프로퍼티에서 easeInOut 이라는 값으로 표현되는 내장 곡선과 유사한 효과이다. 역시 easeOut 모드와 동일하게 해당 효과를 부여하기 위한 래퍼 함수를 만들어주자.

먼저 easeInOut 효과를 부여하기 위한 계산은 다음과 같다.

if (timeFraction <= 0.5) {	// 처음부터 절반까지
  return timing(2 * timeFraction) / 2;
} else {			// 절반부터 끝까지
  return (2 - timing(2 * (1 - timeFraction))) / 2;
}

이를 적용해서 래퍼함수를 아래와 같이 만들 수 있다.

function makeEaseInOut(timing) {
  return function(timeFraction) {
    if (timeFraction <= 0.5) {	// 처음부터 절반까지
      return timing(2 * timeFraction) / 2;
    } else {			// 절반부터 끝까지
      return (2 - timing(2 * (1 - timeFraction))) / 2;
    }
  }
}

bounceEaseInOut = makeEaseInOut(bounce);
    

easeInOut을 그래프로 나타내면 정방향의 timing 함수 그래프와 여기에 easeOut 모드를 적용한 그래프의 중간에 위치하는 그래프가 그려진다. 다음 그림에서 파란색 그래프가 easeInOut 그래프를 보여준다.

easeInOut은 첫 시작부터 절반까지는 easeIn 모드로 작동하고, 그 이후부터 종료까지는 easeOut 모드로 작동하는 것임을 알 수 있다.

6) 다른 애니메이션 효과 예시

단순히 요소의 위치를 이동하는 애니메이션 외에도 자바스크립트는 매우 다양한 애니메이션 처리를 해줄 수 있다. 그 구현은 개발자에게 달려있지만 이번에는 간단하면서도 CSS 애니메이션으로는 구현할 수 없는 텍스트 타이핑 애니메이션을 구현해보자. 해당 애니메이션은 바운스 timing 함수를 적용해서 구현했다.

<textarea id="textExample" rows="5" cols="60">
  He took his vorpal sword in hand:
  Long time the manxome foe he sought—
  So rested he by the Tumtum tree,
  And stood awhile in thought.
</textarea>

<button onclick="animateText(textExample)">동작</button>
function animateText(textArea) {
  let text = textArea.value;
  let to = text.length;
  let from = 0;
  
  animate({
    duration: 5000,
    timing: bounce,
    draw: function(progress) {
      let result = (to - from) * progress + from;
      textArea.value = text.substr(0, Math.ceil(result));
    }
  });
}

function bounce(timeFraction) {
  for (let a = 0, b = 1, result; 1; a += b, b /= 2) {
    if (timeFraction >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
    }
  }
}

자바스크립트로 requestAnimationFrame을 사용해 애니메이션을 구현하는 것은 강력하지만, 꽤나 난이도가 있는 작업이다. 왜냐하면 모든 애니메이션을 직접 프레임 단위로 계산해주어야 할 일이 많기 때문이다. 또한 위에서 예시를 든 timing 함수를 보면 알 수 있듯이 물리학에서 사용하는 공식이나 기하학적인 계산이 필요한 경우도 많다.

그렇지만 대부분 자주 사용하는 계산은 이미 공식화가 되어 있기 때문에 가져다 쓰거나, 약간의 변형을 통해 입맛대로 조절이 가능하기도 하다. CSS 애니메이션과 자바스크립트 애니메이션을 잘 조합해서 사용한다면 강력한 애니메이션 효과를 만들 수 있을 것이다.

References

  1. https://ko.javascript.info/animation
  2. https://sohee-dev.tistory.com/92
  3. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties
  4. https://blog.eunsatio.io/develop/JavaScript-window.requestAnimationFrame-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC
profile
개발잘하고싶다

0개의 댓글