# テキストアニメーション

# デモ

# 使用している主な機能

クラスとスタイルのデータバインディング 62ページ

リストトランジション 205ページ

算出プロパティ computed 120ページ

ウォッチャ watch 128ページ

# ソースコード

index.vue
<template>
  <div class="example">
    <h3>TextAnime1 <button @click="anime1=!anime1">切り替え</button></h3>
    <TextAnime1 v-if="anime1"/>
    <p>SCSS でディレイを付与</p>
    <hr>
    <h3>TextAnime2 <button @click="anime2=!anime2">切り替え</button></h3>
    <TextAnime2 v-if="anime2"/>
    <p>バインディングでディレイを付与</p>
    <hr>
    <h3>TextAnime3</h3>
    <p><label><input type="checkbox" v-model="autoplay"> 5秒ごとに自動で文字を更新</label></p>
    <TextAnime3 :autoplay="autoplay"/>
    <p><code>transition-group</code><code>v-move</code> を利用してゴニョゴニョしている</p>
  </div>
</template>

<script>
import TextAnime1 from './TextAnime1'
import TextAnime2 from './TextAnime2'
import TextAnime3 from './TextAnime3'
export default {
  components: {
    TextAnime1,
    TextAnime2,
    TextAnime3
  },
  data() {
    return {
      anime1: true,
      anime2: true,
      autoplay: true
    }
  }
}
</script>

# TextAnime1

文字数が少ない&スタイルを組み合わせて、アニメーションを作成する場合はこちらがオススメ。

TextAnime1.vue
<template>
  <div class="TextAnime1">
    <span v-for="(t, index) in text" :key="index" v-text="t" class="item delay-anime"/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: '基礎から学ぶ Vue.js'
    }
  }
}
</script>

<style lang="stylus" scoped>
@keyframes text-in {
  0% {
    transform: translate(0, -20px);
    opacity: 0;
  }
}

.item {
  display: inline-block;
  min-width: 0.3em;
  font-size: 2rem;
  animation: text-in 0.8s cubic-bezier(0.22, 0.15, 0.25, 1.43) 0s backwards;
}

for co in 0 .. 12 {
  .delay-anime:nth-child({co + 1}) {
    animation-delay: co * 100ms + 200ms;
  }
}
</style>

固定の文字列なら、もちろん v-for を使わずに静的コンテンツとして埋め込んでしまうのが一番コスパが良いです。

# TextAnime2

文字数が多い&可変の場合はこちらがオススメ。

TextAnime2.vue
<template>
  <div class="TextAnime1">
    <span
      v-for="(t, index) in text"
      :key="index"
      class="item"
      :style="{animationDelay: index*100+'ms'}"
      v-text="t"
      />
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: '基礎から学ぶ Vue.js'
    }
  }
}
</script>

<style scoped>
@keyframes text-in {
  0% {
    transform: translate(0, -20px);
    opacity: 0;
  }
}
.item {
  display: inline-block;
  min-width: 0.3em;
  font-size: 2rem;
  animation: text-in .8s cubic-bezier(0.22, 0.15, 0.25, 1.43) 0s backwards;
}
</style>

# TextAnime3

キーが同じなら v-move が適用されるのを利用して、文字+文字ごとのインデックスを組み合わせたキーを生成しています。 文字数が多いと若干コストが高くなるため、インスタンス初期化とメッセージが編集されたとき、あらかじめ文字列を分解&キーを生成しています。

TextAnime3.vue
<template>
  <div class="TextAnime1">
    <textarea v-model.lazy="editor" style="width:80%;height:40px;"></textarea>
    <transition-group tag="div" class="title">
      <span v-for="el in text" :key="el.id" class="item" v-text="el.text"/>
    </transition-group>
  </div>
</template>

<script>
export default {
  props: {
    autoplay: Boolean
  },
  data() {
    return {
      timer: null,
      index: 0,
      // オリジナルメッセージ
      original: [
        '機能ごとに解説している Vue.js 入門書です。これからはじめる方にも、すでに Vue.js をお使いの方にも、楽しんでいただける内容になっています。',
        'Vue.js は直感的に使える機能が多く、雰囲気で使ってしまいがちです。どんなメリット&デメリットがあるかも解説しているため、しっかりと学習できます。',
        '各チャプターやセクションは、基本的に独立した解説になっています。そのため、知りたい機能をピックアップして学習できます。'
      ],
      // 分解したメッセージ
      messages: [],
      text: ''
    }
  },
  computed: {
    editor: {
      get() { return this.text.map(e => e.text).join('') },
      set(text) { this.text = this.convText(text) }
    }
  },
  watch: {
    autoplay(val) {
      clearTimeout(this.timer)
      if (val) {
        this.ticker()
      }
    }
  },
  methods: {
    // デモ用のオートタイマー
    ticker() {
      this.timer = setTimeout(() => {
        if (this.autoplay) {
          this.index = this.index < this.messages.length-1 ? this.index + 1 : 0
          this.text = this.messages[this.index]
          this.ticker()
        }
      }, 5000)
    },
    // テキストを分解してオブジェクトに
    convText(text) {
      const alms = {}
      const result = text.split('').map(el => {
        alms[el] = alms[el] ? ++alms[el] : 1
        return { id: `${el}_${alms[el]}`, text: el }
      })
      return Object.freeze(result) // 監視しない
    }
  },
  created() {
    this.messages = this.original.map(el => this.convText(el))
    this.text = this.messages[0]
    this.ticker()
  }
}
</script>

<style scoped>
.title {
  font-size: 2rem;
}
.item {
  display: inline-block;
  min-width: 0.3em;
}
/* トランジション用スタイル */
.v-enter-active,
.v-leave-active,
.v-move {
  transition: all 1s;
}
.v-leave-active {
  position: absolute;
}
.v-enter,
.v-leave-to {
  opacity: 0;
  transform: translateY(-30px);
}
</style>