JavaScriptを使い、CSS3を使ったanimationの内容を変える方法





## はじめに

CSSでは、 ainmation プロパティと keyframes を使ってアニメーションができます。

1つのオブジェクトに対して1つのアニメーション設定をするだけなら、それほど難しいことはありません。
しかし、ボタンの入力内容などに応じてアニメーション内容を切り替えたいという事もあります。

このページでは JavaScriptを使い、CSSのアニメーションを切り替える方法を書いてみました。

## まずは1回だけのCSSアニメーション

まずは、ボタンが押されたらアニメーションを1回だけおこなうサンプルを作ってみます。
ここでは、div要素の中にあるspanで作った赤色の箱(id="box")を、transform:translateX で横移動をさせてみます。

```html

<div class="parent">
 <span class="child" id="box"></span>
</div>

<p>
<input type="button" onclick="move();" value=" move! " />
</p>
```
```css

div.parent {
	position: relative;
	border: 1px solid black;
	width : 400px;
	height: 100px;
}
span.child {
	position: absolute;
	left: 100px;
	top: 25px;
	width : 50px;
	height: 50px;
	background-color: red;
}
@keyframes move {
	to {
		transform:translateX(200px);
	}
}
.moveToRight {
	animation-name: move;
	animation-duration: 1s;
	animation-iteration-count: 1;
	animation-fill-mode: forwards;
}
```
```Javascript
function move() {
	const objBox = document.getElementById("box");
	objBox.classList.add('moveToRight');
}
```

keyframes として 200px 右に動く動きを move という名前で定義しておき、この move を1秒間で1回だけアニメーションさせる定義が .moveToRight です。ボタンが押されたら JavaScript で moveToRight を box の classList に追加してやることでアニメーションが動き出します。

CSSで「animation-fill-mode: forwards」としているので、box は移動終了した場所で止まっています。

## アニメーション内容を変更する方法

上のサンプルでは、アニメーションは1回だけしか動作しません(ボタンは何回でも押せますが、押しても何も変化しません)。
もし何回も同じアニメーションをおこなわせたければ、アニメーション終了時に classList から前回のアニメーションを取り除いてやる必要があります。

では、ボタンが押されたら、今度は元の位置までアニメーションで戻るようにしたい場合はどうすれば良いでしょうか？
この場合もアニメーション終了時に classList から前回のアニメーションを取り除いた上で、今度は逆向きに移動する transform の keyframes を使ったアニメーションを定義し、それを classList に追加してやれば良さそうですね。
しかし、それだけでは上手くいきません。classList から取り除いた時点で transform の影響が消えるので、box は瞬間的に元の位置に戻ってしまいます。このため、left 位置に transform 後の値を加味した値を書いておく必要があります。left や transform の値は JavaScript からは読めないので、自分で計算して値を書かなければいけません(~~レンダリングエンジンが知っているはずの値を自分でも別途管理しなければいけないというのはイマイチな感じがしますね。何か良い方法はありますか？~~ window.getComputedStyle()を使えば transform の値を読めます。コメント参照)。

あと、classList にアニメーションを追加したり削除したりするっていうのは意外に面倒くさいですね。動かしたいオブジェクトが沢山あると、だんだんと管理できなくなってきます。
この問題は、実は簡単に解決できます。classListは使わず、単にオブジェクトの style.animationName を書けば良いだけなのです。

ボタンを押す度に、box (span.child) が右に行ったり左に戻ったりするサンプルを以下に示します。(html は最初のサンプルと一緒です)

```css
div.parent {
	position: relative;
	border: 1px solid black;
	width : 400px;
	height: 100px;
}
span.child {
	position: absolute;
	left: 100px;
	top: 25px;
	width : 50px;
	height: 50px;
	background-color: red;
	animation-duration: 1s;
	animation-iteration-count: 1;
	animation-fill-mode: forwards;
}
@keyframes moveRight {
	to {
		transform:translateX(200px);
	}
}
@keyframes moveLeft {
	to {
		transform:translateX(-200px);
	}
}
```
```javascript
let direct = 0;	// 移動方向。0以外なら右へ、0なら左へ

function move() {
	const objBox = document.getElementById("box");
	objBox.style.transform = '';	// 前回移動アニメのtransform を消す

	direct ^= 1;
	if (direct !== 0) {
		objBox.style.left = '100px';	// 移動開始位置を設定する必要がある
		objBox.style.animationName = 'moveRight';
	} else {
		objBox.style.left = '300px';
		objBox.style.animationName = 'moveLeft';
	}
}
```

最初のサンプルと違い、アニメーション設定は予め span の中に書いてしまっています(アニメーション設定だけの CSS 記述 .moveToRight が無くなりました)。
こうすることで、classList を使う事無く、ボタンが押されたら style.animationName を書くだけでアニメーションの切り替えが出来ます(もちろん、duration とかを変化させても良いです)。
また、classList の削除に相当する transform の初期化は、ボタンが押されてからおこなえばOKです。これは空文字をセットするだけで実現できます。

今回の例では横移動だけのアニメーションですが、色や透明度を変化させる場合も考え方は一緒です。

## 応用例

応用例として、上下左右のボタンに対して、押された方向へアニメーションするサンプルを作ってみました。
(2020/7/27 : window.getComputedStyle() を使って位置やtransformを取得する方法に変更しました)
<p class="codepen" data-height="300" data-theme-id="light" data-default-tab="js,result" data-user="phoophiang" data-slug-hash="dyGLZOK" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="cssAnimationMove">
  <span>See the Pen <a href="https://codepen.io/phoophiang/pen/dyGLZOK">
  cssAnimationMove</a> by Hiroaki Okabe (<a href="https://codepen.io/phoophiang">@phoophiang</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
今までのサンプルとちょっと違うのは、同じ方向に何回も移動できるようになっている事です。これを実現するために、アニメーション終了時に style.animationName をクリアしています。style.animationName は、値の変化があった時だけアニメーションが開始します。同じ方向へのアニメでは同じ名前をセットしてしまうので、一度クリアしないとアニメが動かないのです。

このことから、style.animationName への代入は、代入に見せかけた演算子のオーバーロードでは無いことが分かります。style.animationName に限らず、left とか transform の値は、JavaScript でセットしてもすぐに状態が反映される訳では無く、描画直前に前回との差分を見て一気に反映しているらしい事が想像できます。

あと余談ぽくなりますが、moveRight と moveLeft の keyframes を作らずに、1つの keyframes でanimationDirection を normal と reverse を JavaScriptで切り替える方法も可能です。ただし reverse では加速の仕方も逆になってしまうので、animationTimingFunction が linear の時以外は使いにくいです。

## 最後に

ここまでやるんなら、CSS を使ったアニメーションではなくて、全て JavaScript でやった方が制御しやすいのでは？ とも思いますね。
しかし、JavaScript ではインターバル割り込みによって処理する関係上、描画フレーム毎に処理してくれる CSS に比べると、どうしても動きがカクカク見えてしまいます。
当たり判定が不要な演出とかは、このページの内容を参考にして動かしてみるのも良いかもしれません。