実践! Aurelia アニメーション

aurelia-animator-css というプラグインを使えば、CSS アニメーションとアニメーションさせたい要素の CSS クラスを定義し、それをHTMLで使うだけで自動的にアニメーションさせることができます。

この記事では事前準備の方法と、自動的なアニメーションを設定する方法、任意のタイミングでアニメーションさせる方法を紹介します。

# 事前準備

## `aurelia-animator-css` プラグインを追加する

### Aurelia プロジェクトを作成

Aurelia は自由度が高くプロジェクトの構成も多岐にわたります。ここでは [Aurelia CLI](http://aurelia.io/hub.html#/doc/article/aurelia/framework/latest/the-aurelia-cli) を使ってプロジェクトを作るところから説明します。

Aurelia CLI は npm でインストールできます。

```sh
$ sudo npm install aurelia-cli -g
```

```bash:プロジェクト作成開始
$ au new
                      _ _          ____ _     ___
  __ _ _   _ _ __ ___| (_) __ _   / ___| |   |_ _|
 / _` | | | | '__/ _ \ | |/ _` | | |   | |    | |
| (_| | |_| | | |  __/ | | (_| | | |___| |___ | |
 \__,_|\__,_|_|  \___|_|_|\__,_|  \____|_____|___|

```

```bash:プロジェクト名を指定
Please enter a name for your new project below.

[aurelia-app]> practice-au-animations[Enter]
```

```bash:構成を選択

Would you like to use the default setup or customize your choices?

1. Default ESNext (Default)
   A basic web-oriented setup with Babel for modern JavaScript development.
2. Default TypeScript
   A basic web-oriented setup with TypeScript for modern JavaScript development.
3. Custom
   Select transpilers, CSS pre-processors and more.

[Default ESNext]>[Enter] (LESS, SASS などを選択する場合は 3)

Project Configuration

    Name: practice-au-animations
    Platform: Web
    Transpiler: Babel
    CSS Processor: None
    Unit Test Runner: Karma
    Editor: Visual Studio Code

```

```bash:生成
Would you like to create this project?

1. Yes (Default)
   Creates the project structure based on your selections.
2. Restart
   Restarts the wizard, allowing you to make different selections.
3. Abort
   Aborts the new project wizard.

[Yes]>[Enter]
Project structure created and configured.
```

```bash:ライブラリインストール

Would you like to install the project dependencies?

1. Yes (Default)
   Installs all server, client and tooling dependencies needed to build the project.
2. No
   Completes the new project wizard without installing dependencies.

[Yes]>[Enter]
Installing project dependencies.
... ライブラリ群のインストールが始まる
```

インストールが終わったら起動します。

```bash
$ au run --watch
...
Finished 'writeBundles'
Application Available At: http://localhost:9000
BrowserSync Available At: http://localhost:3001
Starting 'watch'...
```

http://localhost:9000 にアクセスしてみましょう。

### `aurelia-animator-css` を追加する

ライブラリのインストールが終わったらアニメーションのためのプラグインを追加します。

`aurelia-animator-css` をとってきて...

```bash
$ npm install aurelia-animator-css --save-dev
```

プロジェクトファイル aurelia.json `dependencies` に追加します。

```json:追加するコード
{
  "name": "aurelia-animator-css",
  "path": "../node_modules/aurelia-animator-css/dist/amd",
  "main": "aurelia-animator-css"
}
```

```json:aurelia_project/aurelia.json(追加後)
{
  "name": "practice-au-animations",

  ...
  "build": {
    
    ...
    "bundles": [
      
      ...
      {

        ...
        "dependencies": [
          "aurelia-binding",
          "aurelia-bootstrapper",
          "aurelia-dependency-injection",
          "aurelia-event-aggregator",
          "aurelia-framework",
          "aurelia-history",
          "aurelia-history-browser",
          "aurelia-loader",
          "aurelia-loader-default",
          "aurelia-logging",
          "aurelia-logging-console",
          "aurelia-metadata",
          "aurelia-pal",
          "aurelia-pal-browser",
          "aurelia-path",
          "aurelia-polyfills",
          "aurelia-route-recognizer",
          "aurelia-router",
          "aurelia-task-queue",
          "aurelia-templating",
          "aurelia-templating-binding",
          {
            "name": "text",
            "path": "../scripts/text"
          },
          {
            "name": "aurelia-templating-resources",
            "path": "../node_modules/aurelia-templating-resources/dist/amd",
            "main": "aurelia-templating-resources"
          },
          {
            "name": "aurelia-templating-router",
            "path": "../node_modules/aurelia-templating-router/dist/amd",
            "main": "aurelia-templating-router"
          },
          {
            "name": "aurelia-testing",
            "path": "../node_modules/aurelia-testing/dist/amd",
            "main": "aurelia-testing",
            "env": "dev"
          },
          {
            "name": "aurelia-animator-css",
            "path": "../node_modules/aurelia-animator-css/dist/amd",
            "main": "aurelia-animator-css"
          }
        ]
      }
    ]
  }
}
```

一旦 `au run --watch` を止めて(Ctrl+C)、再起動してください。
これで準備完了です。

# 自動アニメーションの手順

DOM 要素が追加されたり削除されるタイミングで自動的にアニメーションさせるための手順は次の2つです。

- ステップごとに CSS アニメーションを記述する
- View (HTML) のアニメーションさせたい要素に CSS クラスを設定する

## ステップごとに CSS アニメーションを記述する

こちらがフェードイン・フェードアウトの例です。
要素が表示された時、具体的には if バインディングや repeat バインディングで DOM に要素が追加された時、その要素にフェードインエフェクトをかけます。
またその逆に DOM から要素が取り除かれる前にフェードアウトエフェクトをかけます。

```css:src/app.css

/* アニメーションに必要な `@keyframes` の定義(-webkit プレフィクスあり) */
@-webkit-keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

/* アニメーションに必要な `@keyframes` の定義(-webkit プレフィクスなし) */
@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

/* 要素がDOMに追加された時（初期化） */
.fade-in.au-enter {
  opacity: 0;
}

/* 要素がDOMに追加された時(アニメーション) */
.fade-in.au-enter-active {
  -webkit-animation: fadeIn 400ms;
  -o-animation: fadeIn 400ms;
  animation: fadeIn 400ms;
}

/* 要素がDOMから除かれる時(アニメーション) */
.fade-out.au-leave-active {
  -webkit-animation: fadeIn 400ms;
  -o-animation: fadeIn 400ms;
  animation: fadeIn 400ms;
  /* fadeIn アニメーションを逆再生すれば fadeOut になる */
  -webkit-animation-direction: reverse;
  -moz-animation-direction: reverse;
  -o-animation-direction: reverse;
  animation-direction: reverse;
}
```

※タイミングについての詳細は後述

## View (HTML) のアニメーションさせたい要素に CSS クラスを設定する

早速定義したアニメーションを要素に適用させてみましょう。

```html:src/app.html
<template>
  <require from="./app.css"></require>

  <h1>${message}</h1>

  <div>
    <label>
      <input type="checkbox" checked.bind="appeared">
      表示
    </label>
  </div>
  
  <h2 if.bind="appeared" class="au-animate fade-in fade-out">
    IFバインディングによる自動アニメーション
  </h2>

  <div>
    <button click.trigger="append()">追加</button>
  </div>

  <div>
    <div repeat.for="item of items" class="au-animate fade-in fade-out">
      <a href="#" click.trigger="$parent.remove(item)">${item.kao}</a>
    </div>
  </div>

</template>
```

この画面を動作させるために、ViewModel に幾つか追記する必要があります。

```js:src/app.js
export class App {
  appeared = false;
  items = [
    {kao:'( ๑>ω<)۶”'},
    {kao:'(*´罒`*)'},
    {kao:'(＿´Д｀)'},
    {kao:'(´･ω･`)'},
    {kao:'(　･`ω･´)'},
    {kao:'( ๑>ω<)۶”'},
    {kao:'(*´罒`*)'},
    {kao:'(＿´Д｀)'},
    {kao:'(´･ω･`)'},
    {kao:'(　･`ω･´)'},
  ];

  constructor() {
    this.message = 'Hello Animation World!';
  }

  append() {
    let kaos = "( ๑>ω<)۶”  (*´罒`*)  (＿´Д｀)  (´･ω･`)  (　･`ω･´)";
    let kao = kaos.split('  ')[Math.floor( Math.random() * 5 )];
    this.items.push({kao});
  }

  remove(item) {
    this.items.splice(this.items.indexOf(item), 1);
  }
}
```

以下のように表示され、表示チェックボックスを操作すると「IFバインディングによる自動アニメーション」がフェードイン・フェードアウトします。また追加ボタンをクリックすると顔文字がフェードインで追加され、顔文字自体をクリックするとフェードアウトで消えてゆきます。

![スクリーンショット 2016-11-18 20.16.04.png](https://qiita-image-store.s3.amazonaws.com/0/25145/e918cf16-f85e-138d-e4ca-774b87ce7c39.png)

### それぞれのタイミング

今回のアニメーションのために定義した CSS クラスは以下の３つでした。

- `.fade-in.au-enter`
- `.fade-in.au-enter-active`
- `.fade-out.au-leave-active`

実際に HTML に付与したクラスは `au-animate` `fade-in` `fade-out` の３つでした。つまり、アニメーションを実行させたいクラス `fade-in` `fade-out` に対して、タイミングごとに定義することで自動アニメーションを実現しています。

- `au-enter` ...要素が追加された直後に付与される。初期化。
- `au-enter-active` ...au-enter の直後に付与される。出現アニメーション本体。
- `au-leave` ...要素が削除される前に付与される。初期化。
- `au-leave-active` ...au-leave の直後に付与される。消滅アニメーション本体。

例では `fade-in` `fade-out` を分けて定義しましたが、これは必須ではなく HTML で使うクラスごとにアニメーションをまとめて定義することができます。

```css:まとめて定義する例
.kao.au-enter { ... }
.kao.au-enter-active { ... }
.kao.au-leave-active { ... }
```

### リストアイテムの出現・消滅タイミングをずらすには

今の所、最初からリストに存在するアイテムが表示される際、すべて同時にフェードインしています。もう少しカッコよく見せるために、一つ目から順にフェードインするようにしてみましょう。

```css:src/app.css
/* 省略 */

/* リストアイテムの出現にディレイをかける */
.au-stagger {
  -webkit-animation-delay: 100ms;
  animation-delay: 100ms;
}
```

```html:src/app.html
  <!-- リスト部分だけ抜粋 親要素に定義した au-stagger クラスを指定する -->
  <div class="au-stagger">
    <div repeat.for="item of items" class="au-animate fade-in fade-out">
      <a href="#" click.trigger="$parent.remove(item)">${item.kao}</a>
    </div>
  </div>
```

`au-stagger` というクラスを定義してリストの親要素に指定しました。ブラウザを再読み込みして、顔文字の一覧がどう表示されるかを見てみてください。

この `au-stagger` というクラス名には特別な意味があり、この機能を使うには `au-stagger` という名前にする必要があります。ところで別の場所でディレイの長さを調整したい時はどうしたらいいでしょう。名前を変えることができないので、複数定義したくてもできません。

そういう場合は、このように書くことができます。

```css:src/app.css
/* 省略 */

/* リストアイテムの出現にディレイをかける(100ms) */
.au-stagger.d100 {
  -webkit-animation-delay: 100ms;
  animation-delay: 100ms;
}

/* リストアイテムの出現にディレイをかける(200ms) */
.au-stagger.d200 {
  -webkit-animation-delay: 200ms;
  animation-delay: 200ms;
}
```

```html:src/app.html
  <!-- ディレイ 100ms -->
  <div class="au-stagger d100">
    <div repeat.for="item of items" class="au-animate fade-in fade-out">
      <a href="#" click.trigger="$parent.remove(item)">${item.kao}</a>
    </div>
  </div>
  <!-- ディレイ 200ms -->
  <div class="au-stagger d200">
    <div repeat.for="item of items" class="au-animate fade-in fade-out">
      <a href="#" click.trigger="$parent.remove(item)">${item.kao}</a>
    </div>
  </div>
```

# 任意のタイミングでアニメーションを発動させる手順

プログラムから任意のタイミングでアニメーションを発動させるための手順は次の２つです。

- アニメーションする CSS クラスを一つ定義する
- Animator を使って任意のタイミングで CSS クラスを除去・付与する

## アニメーションする CSS クラスを一つ定義する

```css:app.css
/* 省略 */

/* アニメーションに必要な `@keyframes` の定義(-webkit プレフィクスあり) */
@-webkit-keyframes flash {
  0%, 50%, 100% {
    opacity: 1;
  }
  25%, 75% {
    opacity: 0.2;
  }
}

/* アニメーションに必要な `@keyframes` の定義(プレフィクスなし) */
@keyframes flash {
  0%, 50%, 100% {
    opacity: 1;
  }
  25%, 75% {
    opacity: 0.2;
  }
}

/* 実際にアニメーションするクラスの定義 */
.au-attention {
  -webkit-animation: 0.5s flash;
  animation: 0.5s flash;
}
```

`au-attention` クラスを定義しました。付与されると flash キーフレームによって明滅します。

## Animator を使って任意のタイミングで CSS クラスを除去・付与する

今回は HTML では CSS クラスを直接設定しません。その代わりアニメーションさせたい要素に ViewModel からアクセスするため、`ref="titleElm"` のように参照を設定してください。最後にアニメーションを発動するためのボタンを用意します。

```html:src/app.html
<template>
  <require from="./app.css"></require>

  <h1 ref="titleElm">${message}</h1>
  <button click.trigger="invokeAnimation()">アニメーション発動</button>

  <!-- 省略 -->

</template>
```

ViewModel 側では、新たに `inject` `CssAnimator` をインポートします。`inject` を使って `CssAnimator` のインスタンスを注入します。`constructor(animator)` で受け取って `this.animator` にセットしておきましょう。

```js:src/app.js
import {inject} from 'aurelia-framework'
import {CssAnimator} from 'aurelia-animator-css'

@inject(CssAnimator)
export class App {
  appeared = false;
  items = [
    /* 省略 */
  ];

  constructor(animator) {
    this.message = 'Hello Animation World!';
    this.animator = animator;
  }

  /* 省略 */

  invokeAnimation() {
    // CSS クラスを除去してから付与することで、毎回必ずアニメーションするようにする
    this.animator.removeClass(this.titleElm, 'au-attention')
      .then(() => this.animator.addClass(this.titleElm, 'au-attention'));
  }
}
```

アニメーション発動ボタンをクリックするたびにタイトルが明滅するようになれば成功です！

# 出典・参考情報

[Animating Aurelia](http://diydeveloper.io/tech/2015/04/21/animating-aurelia) from DIY Developer
[Aurelia element animation with custom attribute](http://mikhail.io/2015/07/aurelia-element-animation-with-custom-attribute/) by Mikhail Shilkov
[<i class="fa fa-github"></i> aurelia/skeleton-navigation](https://github.com/aurelia/skeleton-navigation)　([styles.css アニメーション定義](https://github.com/aurelia/skeleton-navigation/blob/master/skeleton-esnext-webpack/styles/styles.css#L61))