GSAP/Flip が無料になった!! ので紹介!


概要

アニメーションをいい感じに書けるようになる GSAP 便利ですよね。
これの Flip という超便利なモジュールが 最近(去年12/16)のバージョンアップ で無料になったんですよ!
(クリスマスプレゼントとのこと。)
超うれしかったので一部機能を紹介します。

GSAP って何?

要するに Dom 移動とかのアニメーションをいい感じに書けるようになるライブラリ。
Dom 移動などを解りやすく、気持ちよくやろうと本気で取り込んでると、移動の処理ってどんどん複雑になって、そのほかのビジネスロジックと混ざってトラブルになりがちなんですけど、 GSAP は歴史も長く、たぶんいまやデファクトスタンダードとなってるアニメーションのライブラリで、本当に痒い所に手が届く素晴らしいライブラリ。

アニメーションって、普通のソフトウェア作る分には単純なもの(CSSのTransitionとかAnimationとか)があれば十分なんですけど、人の心をつかもうというものを作っている場合は結構重要で、もう少しエッジを攻める必要がある。それに答えてくれるライブラリ。

GSAP 自体に気になった人は、ショーケース とか見てみてください。実際使ってる例が並んでて刺激的です。

GSAP の基本をざっと学びたいなら、まずは Tips for Writing Animation Code Efficiently が良いです。ほかにもさまざまなドキュメントや動画が公式にあります。

本題の Flip とは?

このページ の動画やサンプル見てもらうのが解りやすいんだけど、動的な抽出条件の変化とかでアイテムがババっと動くときありますよね。
それをいい感じに元の位置から目的地に動いてくれるというもの。
これが、div をまたごうが、ページを遷移していようが、ちゃんと動くようにできる。
これ自作すると、すごい調整工数かかるし、コードが散らかる。
でも、GSAP/Flip 使えば簡単!

今回は

ページ間で共通のアイテムがサイズと位置が変化しながら移動するサンプルを紹介します。
みんな(私が?)大好き Vue.JS 3 で gsap/Flip を使います。
コード汚染しないようにプラグインとしてアニメーションコードを入れ込みます。

完成イメージ

Demoページ

↓ ページ遷移の際に、同じ v-gsap-flip-id を持ってはいる異なるオブジェクトが、前の位置から遷移後の位置に移動する。

作成方法(具体的なソース)

ソース全体

1. vue create

Vue 3 のプロジェクトは vue-clivue create xxx で作成している前提。

2. GSAP インストール

npm install --save-dev gsap

3. main.ts で GSAP 有効化

src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import { gsap } from 'gsap'
import { Flip } from 'gsap/Flip'
gsap.registerPlugin(Flip)
import GsapFlipId from '@/plugins/GsapFlipId'

createApp(App)
  .use(store)
  .use(router)
  .use(GsapFlipId)
  .mount('#app')

プラグイン本体追加

これがキモ。これのおかげで画面ごとにアニメーション処理を散らかすことなく実現できます。

src/plugins/GsapFlipId.ts
import { Plugin } from 'vue'
import { Flip } from 'gsap/Flip'

const _stateByIds: { [id: string]: Flip.FlipState } = {}

export default {
  install: (app) => {
    /**
     * v-gsap-flip-id="共通のID" という属性で指定したオブジェクト同士が Flip するようになる。
     */
    app.directive('gsap-flip-id', {
      /**
       * アイテムが消えた場合に状態を覚えておく。
       */
      beforeUnmount: function (el, binding) {
        const id: string = binding.value
        _stateByIds[id] = Flip.getState(el)
      },
      /**
       * アイテムがマウントされる前に data-flip-id という
       * gsap/Flip 標準でオブジェクト同士が同一であることを示す属性を追加しておく。
       */
      beforeMount: function (el, binding) {
        const id = binding.value
        el.setAttribute('data-flip-id', id)
      },
      /**
       * マウントされたらFlipを実行。
       */
      mounted: function (el, binding) {
        const id = binding.value
        // 前回の状態を取得
        const oldState = _stateByIds[id]
        if (oldState) {
          // Flip
          Flip.from(oldState, {
            targets: el,
            duration: 1,
            ease: 'power4.inOut',
            scale: true,
          })
        }
      },
    })
  }
} as Plugin

実験用のページ追加

src/App.vue
<template>
  <router-view/>
</template>
src/views/Home.vue
<template>
  <div class="home">
    <router-link to="/next">Next</router-link>
    <div v-gsap-flip-id="'hoge'" style="border: blue 1px solid; width: 100px; height: 100px;">
      <span style="font-size: 10pt;">あいうえお</span>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Home',
});
</script>
src/views/Next.vue
<template>
  <div class="home">
    <router-link to="/">Prev</router-link>
    <div style="height: 200px;"></div>
    <div v-gsap-flip-id="'hoge'" style="border: blue 1px solid; width: 200px; height: 200px;">
      <span style="font-size: 20pt;">あいうえお</span>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Next',
});
</script>
src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import Next from '../views/Next.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/next',
    name: 'Next',
    component: Next
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

以上です!