Vueでアニメーションする方法いろいろ


いろんなやり方があってわけ分からなくなったので整理の為に書きます


# 1. 素のCSS, Javascript+Vueでアニメーションする


Vueでなくてもできる方法ですが、ここではVueで書いています


## 1-1. SMILでアニメーションする


htmlのsvg内の要素の内側にanimate, animateTransform, animateMotion要素を配置すると、記載の指示通りにアニメーションするというものです。他の2つと違ってhtmlに書くので素のJavaScriptで操作しやすいのがメリットだと思います。比較的新しいものでIEは対応していないようです


### animate


汎用のアニメーション要素です
attributeを指定して変化を指示するという書き方で重ね掛けも出来るのでだいたい何でも書けます


```vue:hoge.vue
<script setup>
  import { ref, computed } from 'vue'

  const y = ref(40)
  const values1 = computed(() => { return (y.value - 70) + ";" + (y.value - 30) })
  const values2 = computed(() => { return (y.value - 50) + ";" + (y.value - 10) })
  
  const fuga = () => {
    if (y.value >= 200) {
      y.value = 90
    } else {
      y.value += 50
    }
  }
</script>

<template>
  <svg xmlns="http://www.w3.org/2000/svg" width="520" height="300">
    <rect :x="10" :y="y-20" width="400" height="27">
      <animate
        attributeName="y"
        :values="values1"
        begin="hoge.click"
        dur="1s"
        repeatCount="1" 
        fill="freeze"
      />
    </rect>
    <text x="15" :y="y">
      <animate
        attributeName="y"
        :values="values2"
        begin="hoge.click"
        dur="1s"
        repeatCount="1" 
        fill="freeze"
      />
      y:{{ y }} values1:{{ values1 }} values2:{{ values2 }}
    </text>
  </svg>
  <div>
    <button @click="fuga" id="hoge">クリックで1つ進めます</button>
  </div>
</template>
  
<style>
  svg {
    background-color: #ccc;
  }
  rect {
    stroke: #000044;
    fill: #fff;
  }
  circle {
    stroke: #000044;
    fill: #fff;
  }
  text {
    font-size: 18px;
    fill: #000044;
  }
  button {
    height: 30px;
    width: 200px;
    font-size: 16px;
  }
</style>
```


繰り返し表示するアニメーションの場合は簡単ですが、ボタンクリック後に1回アニメーションさせるような場合にはbegin属性にきっかけになるイベントを渡すなどして開始時間をリセットされるようにする必要があります


### animateTransform


拡大縮小や回転などの変形に特化したアニメーション要素です
typeにscaleやrotateを指定してvalueに値を与えれば変形させることができます

同じことをanimateでやろうと思うと複数のattributeをちょうどよく動かす必要があるので、該当する変形を行う際にはこれを使うのが合理的だと思います


### animateMotion


移動に特化したアニメーション要素です
移動させたいpathを作って与えればpathに沿って移動してくれるので、複雑な経路を動かしたいときに便利です


## 1-2. CSSのanimation+keyframesでアニメーションする


CSSにanimation-name, animation-durationなどの条件を書いておいて、＠keyframeにアニメーションの動作を書くやり方です


```vue:hoge.vue
<script setup>
  import { ref, computed } from 'vue'

  const statusList = [
    { y: 30,
      from: 'translateY(0px)',
      to: 'translateY(0px)', 
      id: 'animationOFF', 
    },
    { y: 30,
      from: 'translateY(0px)',
      to: 'translateY(50px)', 
      id: 'animation1', 
    },
    { y: 30,
      from: 'translateY(50px)',
      to: 'translateY(100px)', 
      id: 'animation2', 
    },
    { y: 30,
      from: 'translateY(100px)',
      to: 'translateY(200px)', 
      id: 'animation1', 
    },
    { y: 30,
      from: 'translateY(200px)',
      to: 'translateY(0px)', 
      id: 'animation2', 
    },
  ]
  const k = ref(0)
  const status = computed(() => { console.log(k.value, statusList[k.value]);return statusList[k.value] })

  const fuga = () => {
    if (k.value >= 4) {
      k.value = 1
    } else {
      k.value += 1
    }
  }
</script>

<template>
  <svg xmlns="http://www.w3.org/2000/svg" width="520" height="300">
    <rect :x="10-5" :y="status.y-20" width="495" height="27" :id="status.id"></rect>
    <text x="10" :y="status.y" :id="status.id" class="animationText">k: {{ k }} y:{{ status.y }} from:{{ status.from }} to:{{ status.to }}</text>
  </svg>
  <div>
    <button @click="fuga">クリックで1つ進めます</button>
  </div>
</template>
  
<style>
  svg {
    background-color: #ccc;
    transition:all 5s;
  }
  rect {
    stroke: #000044;
    fill: #fff;
  }
  text.animationText {
    font-size: 18px;
    fill: #000044;
  }
  #animation1 {
    animation-name: aaa;  /* ここの名前の@keyframesにアニメーションの内容を書く */
    animation-duration: 1s; /* アニメーション1回分の時間 */
    animation-timing-function: ease;
    animation-iteration-count: 1; /* 繰り返し回数 (止めないで繰り返したいときはinfiniteにする)  */
    animation-fill-mode: forwards;  /* アニメーション終了位置で止める */
  }
  @keyframes aaa {
    from {
      transform: v-bind('status.from');
    }
    to {
      transform: v-bind('status.to');
    }
  }
  #animation2 {
    animation-name: bbb;
    animation-duration: 1s;
    animation-timing-function: ease;
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
  }
    @keyframes bbb {
    from {
      transform: v-bind('status.from');
    }
    to {
      transform: v-bind('status.to');
    }
  }
  button {
    height: 30px;
    width: 200px;
    font-size: 16px;
  }
</style>
```


Vueから＠keyframeだけ書き換えれば連続してアニメーションさせられるのではないかと思ったんですが、animation-nameが同じままだと経過時間がリセットされないのか終点に移動するだけになってしまいますから、idを切り替えて適用する＠keyframeを変えるようにしないといけないようです


## 1-3. CSSのtransitionでアニメーションする


CSSにtransition属性を書いておくと値を変更したときに変化を中割りしたようなアニメーションをやってくれます。＠keyframeより書く量が減るので楽ですが、変化を中割りするだけなので繰り返しアニメーションするような動作は書けない、svgのtext要素には適用できない、適用されている要素についてはすべての変化をモーフィングしてしまうので色は素早く変えるけど移動はゆっくりアニメーションさせるというのもできないなど制約があるようです。ある表示からある表示への変化をアニメーションとして見せたいケースは多いので、その場合はCSSに1行書けば適用できるというメリットを享受できると思います


```vue:hoge.vue
<script setup>
  import { ref } from 'vue'

  const y = ref(30)
  
  const fuga = () => {
    if (y.value >= 250) {
      y.value = 30
    } else {
      y.value += 50
    }
  }
</script>

<template>
  <svg xmlns="http://www.w3.org/2000/svg" width="520" height="300">
    <rect :x="10-5" :y="y-20" width="65" height="27" id="animate"></rect>
    <text x="10" :y="y" id="animate">y:{{ y }}</text>
  </svg>
  <div>
    <button @click="fuga">クリックで1つ進めます</button>
  </div>
</template>
  
<style>
  svg {
    background-color: #ccc;
    transition:all 5s;
  }
  rect {
    stroke: #000044;
    fill: #fff;
    transition: all 1s cubic-bezier(.96,.04,.04,.96);
  }
  text {
    font-size: 18px;
    fill: #000044;
    transition: all 1s cubic-bezier(.96,.04,.04,.96);
  }
  button {
    height: 30px;
    width: 200px;
    font-size: 16px;
  }
</style>
```


上記のコードではtextにtransitionが当たらないのでtextだけすぐ移動してrectが遅れて動くような表示になっていると思います


# 2. Vueの機能を使ってアニメーションする


Vueにもアニメーション用に準備されている機能があります
https://ja.vuejs.org/guide/built-ins/transition.html#dynamic-transitions

実際のアニメーションはCSSやJavascriptで実行されるものですが、簡潔にいろいろ指定できるようになっていて便利です


## 2-1. transition要素


Vueが用意しているtransition要素で囲んだ要素にVueのtransitionアニメーションを追加できます
CSSのtransitionは表示の変化を与えたときに一息に移動するのでなく指定の時間をかけてモーフィングするというものですが、Vueのtransition要素では表示する際と非表示にする際のそれぞれについてtransition開始、transition中、transition終了時点のCSSを指定できるようにして表現力を高めてくれています

hoge-enter-active: 表示するtransitionの間ずっと適用されるCSS
hoge-enter-from: 表示するtransitionの開始時点のCSS
hoge-enter-to: 表示するtransitionの終了時点のCSS
hoge-leave-active: 非表示にするtransitionの間ずっと適用されるCSS
hoge-leave-from: 非表示にするtransitionの開始時点のCSS
hoge-leave-to: 非表示にするtransitionの終了時点のCSS

以下ではv-ifで表示/非表示を切り替える際にtransitionの間だけ色を変更しています


```vue:hoge.vue
<script setup>
  import { ref } from 'vue'
  const show = ref(true)
</script>

<template>
  <p>
    <button @click="show = !show">Toggle</button>
    <Transition name="hoge">
      <p v-if="show" style="text-align: center;">
        Hello here is some bouncy text!
      </p>
    </Transition>
  </p>
</template>
  
<style scoped>
  p {
    font-size: 36px;
    color: #00f;
  }
  .hoge-enter-active {
    animation: fuga 0.5s;
    color: #f00
  }
  .hoge-leave-active {
    animation: fuga 0.5s reverse;
    color: #0f0
  }
  @keyframes fuga {
    0% {
      transform: scale(0.7);
    }
    50% {
      transform: scale(1.25);
    }
    100% {
      transform: scale(1);
    }
  }
</style>
```


transitionの直接の子要素は1つしか含められませんが、divやgで囲んでやれば複数含めることが出来ます


```vue:ダメな例
<template>
  <p>hoge</p>
  <p>fuga</p>
</template>
```


```vue:OKな例
<template>
  <div>
    <p>hoge</p>
    <p>fuga</p>
  </div>
</template>
```



## 2-2. 連続したtransition


transition内に要素をネストして書くと連続したtransitionを簡潔に書くことができます
外から内の順で実行されますが初期ではdelayがゼロなので同時実行になります。delayを設定することで順番に表示されるようにすることが出来ます


```vue:hoge.vue
<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">Toggle</button>
  <Transition duration="550" name="nested">
    <div v-if="show" class="outer">
      <div class="inner">
   			Hello
      </div>
    </div>
  </Transition>
</template>

<style>
.outer, .inner {
  background: #eee;
  padding: 30px;
  min-height: 100px;
}
  
.inner { 
  background: #ccc;
}
  
.nested-enter-from,
.nested-leave-to {
  transform: translateY(60px);
  opacity: 0;
}

.nested-enter-from .inner,
.nested-leave-to .inner {
  transform: translateX(60px);
  /*
  	Hack around a Chrome 96 bug in handling nested opacity transitions.
    This is not needed in other browsers or Chrome 99+ where the bug
    has been fixed.
  */
  opacity: 0.001;
}

.nested-enter-active {
  transition: all 0.3s ease-in-out;
}
/* delay enter of nested element */
.nested-enter-active .inner {
    transition: all 0.3s ease-in-out;
	transition-delay: 0.2s;
}

/* we can also transition nested elements using nested selectors */
.nested-leave-active .inner { 
  transition: all 0.3s ease-in-out;
}
/* delay leave of parent element */
.nested-leave-active {
  transition: all 0.3s ease-in-out;
  transition-delay: 0.2s;
}
</style>
```


## 2-3. アニメーションの終了をイベントとして受け取る


transition要素からは以下のイベントを受け取ることが出来ます

v-on:before-enter
v-on:enter
v-on:after-enter
v-on:enter-cancelled
v-on:before-leave
v-on:leave
v-on:after-leave
v-on:leave-cancelled

これを使えばアニメーションが終わったら次に何かするというのが簡単に実装できそうです


```vue:hoge.vue
<script setup>
import { ref } from 'vue'

const show = ref(true)
const beforeEnter = () => { console.log('beforeEnter()') }
const afterEnter = () => { console.log('afterEnter()') }
</script>

<template>
  <button @click="show = !show">Toggle</button>
  <Transition @before-enter="beforeEnter" @after-enter="afterEnter">
    <p v-if="show">hello</p>
  </Transition>
</template>

<style>
.v-enter-active,
.v-leave-active {
  transition: opacity 3s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>
```


# まとめ


1つのやり方で全部解決とはいかないようなので、用途によって適切なものを使えるように一通りマスターしておく必要がありそうです。レッツトライ