Katashin .info

アニメーションの実装が劇的に簡単になるFLIPテクニック

アニメーションの実装はややこしいからCSS Transitionでできなければ実装したくない、そう思っていませんか?FLIPというテクニックを使うことで、CSS Transitionだけでは実装できないケースでも簡単にアニメーションを実装することができます。

FLIPとは #

FLIP とは First, Last, Invert, Play の頭文字から取られた用語であり、アニメーションをこの4つのステップに分割して行うテクニックです。例えば、以下のようなリストの項目に追加、削除、並べかえができるUIに対して、簡単にアニメーションを付けることができます。

上のデモで追加、削除、並べかえを行った時に、項目の座標移動がアニメーションすることがわかると思います。これは CSS Transition だけでは実装することができず、FLIPを活用することで簡単に実装することができます。

FLIPアニメーションの実装 #

ここからはFLIPの実装をしていきます。まずはFLIPを実装する前の挙動を確認しましょう。

追加ボタンを押すごとに項目が追加されますが、それによって下に押し出される他の項目はアニメーションせず、即座に次の位置へと配置されます。この実装のコードは以下のとおりです。

<button id="add" type="button">追加</button>

<ul id="list">
  <li>項目 1</li>
  <li>項目 2</li>
  <li>項目 3</li>
</ul>
// 追加ボタンをクリックされた時に add 関数を呼ぶ
document.getElementById('add').addEventListener('click', add)

function add() {
  // すべてのリスト項目を取得
  const items = Array.from(document.getElementById('list').children)

  // First
  const firstRect = first(items)

  // リストへの項目の追加
  prependItem()

  // Last
  const lastRect = last(items)

  // Invert
  invert(items, firstRect, lastRect)

  // Play
  play(items)
}

let maxId = 3
// リストの一番最初に項目を追加する
function prependItem() {
  const id = ++maxId
  const item = document.createElement('li')
  item.innerText = `項目 ${id}`
  document.getElementById('list').prepend(item)
}

上部にある <button> 要素をクリックすることで、<ul> 要素の一番上に <li> 項目が追加されます。JavaScript 内ではボタンがクリックされると add 関数が呼ばれ、prependItem 関数内で項目の追加を行います。FLIPに対応する各ステップの関数 firstlastinvertplay も呼び出しています。以降でそれぞれの関数の実装を見ていきましょう。

まず、first 関数の中では新しい項目を追加する前の各項目の座標を取得します。ここではすべての項目に対して getBoundingClientRect を呼び出し、座標を取得しています。

function first(items) {
  return items.map((item) => {
    return item.getBoundingClientRect()
  })
}

次に項目の追加を行い(prependItem 関数)、last 関数の中では追加後の各項目の座標を取得します。ここでやっていることは、追加を行った後であること以外は first 関数と同じことをしています。

function last(items) {
  return items.map((item) => {
    return item.getBoundingClientRect()
  })
}

firstlast の実行結果を invert 関数に渡し、どれだけ座標が移動したかを計算します。そして、translate によって項目が移動した分を以前の座標まで巻き戻します。

function invert(items, firstRect, lastRect) {
  items.forEach((item, index) => {
    // First と Last で取得した座標
    const first = firstRect[index]
    const last = lastRect[index]

    // 座標の差分を計算
    const deltaX = first.left - last.left
    const deltaY = first.top - last.top

    // 差分を translate にし、追加が行われる前の座標に移動させる
    item.style.transform = `translate(${deltaX}px, ${deltaY}px)`
  })
}

play 関数の中では、requestAnimationFrameinvert の巻き戻しが描画されるまで待ち、その後、transition を設定した後に translate を削除することで CSS Transition を発動させます。

function play(items) {
  // Invert の座標更新を反映させるために requestAnimationFrame の後に実行する
  requestAnimationFrame(() => {
    items.forEach((item) => {
      // translate を消すことで追加が行われた後の座標に移動する
      item.style.transform = ''
      // ここで transition をセットすることでアニメーションが行われる
      item.style.transition = 'transform 0.2s ease-out'

      item.addEventListener(
        'transitionend',
        () => {
          // アニメーション後にセットしたスタイルをもとに戻しておく
          item.style.transition = ''
        },
        { once: true },
      )
    })
  })
}

これまでの実装を反映させたデモは以下のとおりです。項目の追加のたびに、既存の項目が下に押し出されるアニメーションが行われます。

FLIPはVueやSvelteでは標準機能として実装されています。Vueでは<TransitionGroup>で描画しているリスト項目が移動する時にFLIPが使用されますし、Svelteはflip関数を標準のモジュールで提供しています。

FLIPの利点 #

FLIPの実装は簡単です。アニメーションフレームごとに座標計算をしなくていいし、移動先の座標を複雑な計算で求める必要もありません。First、Last、Invertのステップを同期的に実行しているのがポイントで、この3ステップの間、座標の変化はユーザーから見えません。これによって、項目の追加前後の座標を要素から直接取得することができ、座標の差分をCSS Transitionするだけで良くなるのです。

さらに、どのような操作に対してもFLIPの各ステップは使い回せます。上の例では、Firstステップの後に prependItem を呼び出して項目の追加を行いましたが、これを削除や、並べかえに変えたとしてもアニメーションは正しく実行されます。

また、FLIPのアニメーション処理はCSS Transitionなのでパフォーマンスが良いです。FirstとLastで行う座標の取得は比較的重い処理ですが、アニメーション前の一度だけなので、これがボトルネックとなることは経験上ありません。

どのようなアニメーションに向いているか #

これまで見てきたように、FLIPはリスト内の項目を追加、削除、並べかえする時に簡単にアニメーションを付けたい時に役に立ちます。それぞれの項目の座標は固定値ではなく、アニメーションのたびに取得する必要があるため、FLIPと相性が良いです。

すこし違ったパターンでは、アコーディオンの開閉アニメーションの実装にもFLIPが向いていると考えます。アコーディオンは座標ではなく高さを変更するアニメーションですが、開閉部分の高さが auto の場合、CSS Transitionだけではアニメーションさせることができないため、getBoundingClientRectなどで実際の高さを取得する必要があります。この高さ取得部分をFirstやLastのステップに組み込むと、簡単にアコーディオンが実装できます。

逆に、以下のようなボタンにマウスホバーされたら、背景のバーを伸ばしてボタンの色を変えるというようなアニメーションはFLIPには向いていません。アニメーションの開始と終了のスタイルがそれぞれ width: 0width: 100% とすでにわかっているからです。ホバー前と後の座標を取り(First, Last)、アニメーションのために巻き戻す(Invert)ステップが不要で、単に最初から CSS Transition を実行してしまえばよいでしょう。

おわりに #

FLIPは一見難しそうなアニメーションを簡単に実装することができる強力なテクニックです。リストに対してアニメーションを付けるのが一般的な使い方だと考えられますが、応用次第ではさまざまなパターンのアニメーションに使えるポテンシャルがあります。いくつかのUIライブラリでは標準で実装されるので、まずはそのような既存の実装を試してみるのがよいでしょう。

参考文献 #

Aerotwist - FLIP Your Animations
https://aerotwist.com/blog/flip-your-animations/