12일 9월 2020

CSS 애니메이션

CSS 애니메이션을 사용하면 자바스크립트 없이도 간단한 애니메이션을 만들 수 있습니다.

자바스크립트를 사용하면 CSS 애니메이션을 제어할 수 있고 짧은 코드로 훨씬 더 효과적인 애니메이션을 만들 수 있습니다.

CSS 트랜지션

CSS 트랜지션의 원리는 간단합니다. 애니메이션 관련 프로퍼티와 값을 정의해 변화 효과(애니메이션 효과)를 정의할 수 있다는 것이 CSS 트랜지션의 핵심입니다. 브라우저는 애니메이션 관련 프로퍼티가 변하면 자동으로 그 효과를 화면에 보여줍니다.

결론은 이렇습니다. 프로퍼티 값을 변경시키면 브라우저가 알아서 자연스럽게 트랜지션(전환) 효과를 주는 것이죠.

예시를 살펴봅시다. 아래 CSS를 적용하면 3초 동안 background-color가 서서히 변합니다.

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

이 CSS를 적용하면 animated 클래스 속성이 있는 요소의 background-color가 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이라는 공통 프로퍼티를 사용해 이 네 프로퍼티를 한 번에 선언할 수 있다는 사실 정도만 알아둡시다. transition 프로퍼티에 값을 넣어주면 이 값들은 property duration timing-function delay 순으로 위 네 개의 프로퍼티에 대응하게 됩니다.

아래와 같이 transition 프로퍼티를 정의하면 colorfont-size에 애니메이션 효과가 나타납니다.

<button id="growing">클릭하기</button>

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

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

이제 본격적으로 각 프로퍼티를 살펴봅시다.

‘transition-property’ 프로퍼티

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

모든 프로퍼티에 애니메이션 효과를 적용할 수 없지만, 상당수의 프로퍼티에 애니메이션 효과를 적용할 수 있습니다. 값에 all이 있으면 '모든 프로퍼티에 애니메이션 효과를 적용하겠다’라는 것을 의미합니다.

‘transition-duration’ 프로퍼티

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

‘transition-delay’ 프로퍼티

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

transition-delay엔 음수 값도 넣을 수 있습니다. 값이 음수일 땐 애니메이션 효과가 중간부터 나타납니다. transition-duration2s, 지연 시간을 -1s로 설정하면 애니메이션 효과는 1초가 지난 후 1초 동안 지속됩니다.

아래 예시에선 CSS translate 프로퍼티를 사용해 화면에 숫자 0부터 9까지 자연스럽게 나타나도록 해보았습니다…

결과
script.js
style.css
index.html
stripe.onclick = function() {
  stripe.classList.add('animate');
};
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: linear;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  아래 숫자를 클릭하세요.

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script src="script.js"></script>
</body>

</html>

transform 프로퍼티 값에 translate(-90%)를 입력하면 왼쪽으로 해당 요소가 이동합니다.

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
}

숫자를 클릭하면 자바스크립트가 동작해 숫자가 들어 있는 요소(stripe)에 animate 클래스가 추가되고 애니메이션 효과가 나타납니다.

stripe.classList.add('animate');

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

직접 숫자를 클릭해보세요. 현재 날짜가 2020년 9월 12일 오후 12시 17분 8초라면 숫자 8부터 스르륵 이동합니다.

결과
script.js
style.css
index.html
stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
};
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: linear;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  아래 숫자를 클릭하세요.
  <div id="digit"><div id="stripe">0123456789</div></div>

  <script src="script.js"></script>
</body>
</html>

바로 아래 코드가 이런 효과를 만들어낸 것이죠.

stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  // sec가 3이라면 transitionDelay 값이 -3s가 되어 3부터 애니메이션 효과가 적용됩니다.
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
};

transition-timing-function

Timing function describes how the animation process is distributed along the time. Will it start slowly and then go fast or vise versa.

That’s the most complicated property from the first sight. But it becomes very simple if we devote a bit time to it.

That property accepts two kinds of values: a Bezier curve or steps. Let’s start from the curve, as it’s used more often.

Bezier curve

The timing function can be set as a Bezier curve with 4 control points that satisfies the conditions:

  1. First control point: (0,0).
  2. Last control point: (1,1).
  3. For intermediate points values of x must be in the interval 0..1, y can be anything.

The syntax for a Bezier curve in CSS: cubic-bezier(x2, y2, x3, y3). Here we need to specify only 2nd and 3rd control points, because the 1st one is fixed to (0,0) and the 4th one is (1,1).

The timing function describes how fast the animation process goes in time.

  • The x axis is the time: 0 – the starting moment, 1 – the last moment of transition-duration.
  • The y axis specifies the completion of the process: 0 – the starting value of the property, 1 – the final value.

The simplest variant is when the animation goes uniformly, with the same linear speed. That can be specified by the curve cubic-bezier(0, 0, 1, 1).

Here’s how that curve looks:

…As we can see, it’s just a straight line. As the time (x) passes, the completion (y) of the animation steadily goes from 0 to 1.

The train in the example below goes from left to right with the permanent speed (click it):

결과
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">

</body>

</html>

The CSS transition is based on that curve:

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
  /* JavaScript sets left to 450px */
}

…And how can we show a train slowing down?

We can use another Bezier curve: cubic-bezier(0.0, 0.5, 0.5 ,1.0).

The graph:

As we can see, the process starts fast: the curve soars up high, and then slower and slower.

Here’s the timing function in action (click the train):

결과
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 0px;
  transition: left 5s cubic-bezier(0.0, 0.5, 0.5, 1.0);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">

</body>

</html>

CSS:

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, .5, .5, 1);
  /* JavaScript sets left to 450px */
}

There are several built-in curves: linear, ease, ease-in, ease-out and ease-in-out.

The linear is a shorthand for cubic-bezier(0, 0, 1, 1) – a straight line, we saw it already.

Other names are shorthands for the following cubic-bezier:

ease* ease-in ease-out ease-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)

* – by default, if there’s no timing function, ease is used.

So we could use ease-out for our slowing down train:

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

But it looks a bit differently.

A Bezier curve can make the animation “jump out” of its range.

The control points on the curve can have any y coordinates: even negative or huge. Then the Bezier curve would also jump very low or high, making the animation go beyond its normal range.

In the example below the animation code is:

.train {
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
  /* JavaScript sets left to 400px */
}

The property left should animate from 100px to 400px.

But if you click the train, you’ll see that:

  • First, the train goes back: left becomes less than 100px.
  • Then it goes forward, a little bit farther than 400px.
  • And then back again – to 400px.
결과
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='400px'">

</body>

</html>

Why it happens – pretty obvious if we look at the graph of the given Bezier curve:

We moved the y coordinate of the 2nd point below zero, and for the 3rd point we made put it over 1, so the curve goes out of the “regular” quadrant. The y is out of the “standard” range 0..1.

As we know, y measures “the completion of the animation process”. The value y = 0 corresponds to the starting property value and y = 1 – the ending value. So values y<0 move the property lower than the starting left and y>1 – over the final left.

That’s a “soft” variant for sure. If we put y values like -99 and 99 then the train would jump out of the range much more.

But how to make the Bezier curve for a specific task? There are many tools. For instance, we can do it on the site http://cubic-bezier.com/.

Steps

Timing function steps(number of steps[, start/end]) allows to split animation into steps.

Let’s see that in an example with digits.

Here’s a list of digits, without any animations, just as a source:

결과
style.css
index.html
#digit {
  border: 1px solid red;
  width: 1.2em;
}

#stripe {
  display: inline-block;
  font: 32px monospace;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="digit"><div id="stripe">0123456789</div></div>

</body>
</html>

We’ll make the digits appear in a discrete way by making the part of the list outside of the red “window” invisible and shifting the list to the left with each step.

There will be 9 steps, a step-move for each digit:

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

In action:

결과
style.css
index.html
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: steps(9, start);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  아래 숫자를 클릭하세요.

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script>
    digit.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>


</body>

</html>

The first argument of steps(9, start) is the number of steps. The transform will be split into 9 parts (10% each). The time interval is automatically divided into 9 parts as well, so transition: 9s gives us 9 seconds for the whole animation – 1 second per digit.

The second argument is one of two words: start or end.

The start means that in the beginning of animation we need to do make the first step immediately.

We can observe that during the animation: when we click on the digit it changes to 1 (the first step) immediately, and then changes in the beginning of the next second.

The process is progressing like this:

  • 0s-10% (first change in the beginning of the 1st second, immediately)
  • 1s-20%
  • 8s-80%
  • (the last second shows the final value).

The alternative value end would mean that the change should be applied not in the beginning, but at the end of each second.

So the process would go like this:

  • 0s0
  • 1s-10% (first change at the end of the 1st second)
  • 2s-20%
  • 9s-90%

Here’s steps(9, end) in action (note the pause between the first digit change):

결과
style.css
index.html
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: steps(9, end);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  아래 숫자를 클릭하세요.

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script>
    digit.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>


</body>

</html>

There are also shorthand values:

  • step-start – is the same as steps(1, start). That is, the animation starts immediately and takes 1 step. So it starts and finishes immediately, as if there were no animation.
  • step-end – the same as steps(1, end): make the animation in a single step at the end of transition-duration.

These values are rarely used, because that’s not really animation, but rather a single-step change.

Event transitionend

When the CSS animation finishes the transitionend event triggers.

It is widely used to do an action after the animation is done. Also we can join animations.

For instance, the ship in the example below starts to swim there and back on click, each time farther and farther to the right:

The animation is initiated by the function go that re-runs each time when the transition finishes and flips the direction:

boat.onclick = function() {
  //...
  let times = 1;

  function go() {
    if (times % 2) {
      // swim to the right
      boat.classList.remove('back');
      boat.style.marginLeft = 100 * times + 200 + 'px';
    } else {
      // swim to the left
      boat.classList.add('back');
      boat.style.marginLeft = 100 * times - 200 + 'px';
    }

  }

  go();

  boat.addEventListener('transitionend', function() {
    times++;
    go();
  });
};

The event object for transitionend has few specific properties:

event.propertyName
The property that has finished animating. Can be good if we animate multiple properties simultaneously.
event.elapsedTime
The time (in seconds) that the animation took, without transition-delay.

Keyframes

We can join multiple simple animations together using the @keyframes CSS rule.

It specifies the “name” of the animation and rules: what, when and where to animate. Then using the animation property we attach the animation to the element and specify additional parameters for it.

Here’s an example with explanations:

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

<style>
  @keyframes go-left-right {        /* give it a name: "go-left-right" */
    from { left: 0px; }             /* animate from left: 0px */
    to { left: calc(100% - 50px); } /* animate to left: 100%-50px */
  }

  .progress {
    animation: go-left-right 3s infinite alternate;
    /* apply the animation "go-left-right" to the element
       duration 3 seconds
       number of times: infinite
       alternate direction every time
    */

    position: relative;
    border: 2px solid green;
    width: 50px;
    height: 20px;
    background: lime;
  }
</style>

There are many articles about @keyframes and a detailed specification.

Probably you won’t need @keyframes often, unless everything is in the constant move on your sites.

Summary

CSS animations allow to smoothly (or not) animate changes of one or multiple CSS properties.

They are good for most animation tasks. We’re also able to use JavaScript for animations, the next chapter is devoted to that.

Limitations of CSS animations compared to JavaScript animations:

장점
  • Simple things done simply.
  • Fast and lightweight for CPU.
단점
  • JavaScript animations are flexible. They can implement any animation logic, like an “explosion” of an element.
  • Not just property changes. We can create new elements in JavaScript for purposes of animation.

The majority of animations can be implemented using CSS as described in this chapter. And transitionend event allows to run JavaScript after the animation, so it integrates fine with the code.

But in the next chapter we’ll do some JavaScript animations to cover more complex cases.

과제

아래 사진의 애니메이션 효과 보기 (비행기를 클릭해보세요):

  • 사진을 클릭하면 40x24px 에서 400x240px로 확대됩니다. (10배 확대)
  • 해당 효과는 3초가 소요됩니다.
  • 다음 메시지가 출력됩니다.: “Done!”
  • 애니메이션이 실행되는 동안에는 사진을 클릭해도 애니메이션이 중단되지 않습니다.

샌드박스를 열어 정답을 작성해보세요.

CSS to animate both width and height:

/* original class */

#flyjet {
  transition: all 3s;
}

/* JS adds .growing */
#flyjet.growing {
  width: 400px;
  height: 240px;
}

Please note that transitionend triggers two times – once for every property. So if we don’t perform an additional check then the message would show up 2 times.

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

Modify the solution of the previous task 비행기에 애니메이션 효과주기 (CSS) to make the plane grow more than its original size 400x240px (jump out), and then return to that size.

Here’s how it should look (click on the plane):

Take the solution of the previous task as the source.

We need to choose the right Bezier curve for that animation. It should have y>1 somewhere for the plane to “jump out”.

For instance, we can take both control points with y>1, like: cubic-bezier(0.25, 1.5, 0.75, 1.5).

The graph:

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

Create a function showCircle(cx, cy, radius) that shows an animated growing circle.

  • cx,cy are window-relative coordinates of the center of the circle,
  • radius is the radius of the circle.

Click the button below to see how it should look like:

The source document has an example of a circle with right styles, so the task is precisely to do the animation right.

샌드박스를 열어 정답을 작성해보세요.

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.