🐶

わんコメのCUIタイピング風テーマ作ってみた

2023/12/10に公開

わんコメ(YouTube Live などで使うコメントビューワー)用にタイピングアニメーションっぽいテーマを作ったので、ざっくり解説とおすそ分けです。

↓テンプレートは、ここから雑に落として使ってみてください
https://github.com/tamayura-souki/live-layouts/tree/main/onecomment-template

わんコメ
https://onecomme.com/

この記事は VTuber・バーチャルプログラマ・技術者 Advent Calendar 2023 の10日目の記事です。
https://qiita.com/advent-calendar/2023/virtual-programmers-network

動作デモ

https://youtu.be/PFpZmrU2XMs

やったこと

  • チャンネル名とコメント本文の間に:~$または:~#の文字列を挿入
  • タイピングアニメーション
    • 文字を1文字ずつ順に表示
    • 文字の右端にカーソル的な縦棒を表示

:~$ の挿入

index.htmlに直接挿入したい文字列を入れる

index.html
{{comment.data.displayName.trim()}}:~<span class="normal-user">$</span><span class="root-user">#</span>

$#の切り替えは、JavaScript じゃなくて CSS でやる
実は CSS で、特定の属性値を持つ要素にだけスタイルを当てる、みたいな条件分けができる
わんコメでは、チャンネル主のコメントはdata-owner="true"、それ以外だとdata-owner="false"が設定される

style.css
div[data-owner="true"] .normal-user {
  display: none;
}

div[data-owner="false"] .root-user {
  display: none;
}

CSS の属性セレクター
https://developer.mozilla.org/ja/docs/Learn/CSS/Building_blocks/Selectors/Attribute_selectors

タイピングアニメーション

1文字毎に CSS アニメーションを付与

各文字毎に、スイッチ的に表れるアニメーションを付与
これは1文字毎にループを回したいので JavaScript でやる

mounted() の中でコメント取得時の callback を定義している場所があるので、そこの中にコメントの編集処理を挿入する(今回ドキュメント読まずに勘で編集したので細かいことは調べてください。すいません)

script.js
    OneSDK.subscribe({
      action: 'comments',
      callback: (comments) => {
        const newCache = new Map()
        comments.forEach(comment => {
          const index = cache.get(comment.data.id)
          if (isNaN(index)) {
            comment.commentIndex = commentIndex
            newCache.set(comment.data.id, commentIndex)
            ++commentIndex
            /* ↓ ここ ↓ */
            comment.data.comment = attachTypingAnim(comment.data.comment)
          } else {
            comment.commentIndex = index
            newCache.set(comment.data.id, index)
          }
        })
        cache = newCache
        this.comments = comments
      }
    })

comment.data.comment にコメントテキストが入ってるのでそれを編集する
1文字1文字を <div class='char'></div>で囲んでアニメーションをあてる

const htmlToTypeChar = (html, index) => {
  return `<div class='char'>${html}</div>`
}

const attachTypingAnim = (comment) => {
  comment = Array.prototype.map.call(comment, htmlToTypeChar).join('')
  return comment;
}

CSS のアニメーション
アニメーションが再生された瞬間に瞬時に、透明度が0から1に変化する

.char{
  opacity: 0;
  animation: type 0ms step-start forwards;
}

@keyframes type {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

アニメーションの開始時間を animation-delay プロパティで遅らせる

文字毎にアニメーションを当てただけだと、全部の文字が同じタイミングで一斉に表示されてタイピングにならない
animation-delay でアニメーションの開始を少しずつ遅らせて、それっぽくする(1文字目は0.1秒後表示、2文字目は0.2秒後表示...)
さっきの div 要素に直接プロパティを書く

const TYPING_INTERVAL = 100
const TYPING_DELAY = 200

const htmlToTypeChar = (html, index) => {
  const delayMiliSecond = index * TYPING_INTERVAL+TYPING_DELAY
  return `<div class='char' style='animation-delay:${delayMiliSecond}ms'>${html}</div>`
}

カーソルっぽいバーは border プロパティでそれっぽくする

縦棒の表示は border プロパティでやる
文字の表示とは逆に、アニメーション終了時には消えてないといけない(文字と文字の間に毎回縦棒が入る)ので、border プロパティについてもアニメーションを入れる

.char{
  border-right: 0.1em solid var(--cui-main-color);
  animation: bar 100ms step-end forwards;
}

@keyframes bar {
  from {
    border-right: 0.1em solid var(--cui-main-color);
  }
  to {
    border: none;
  }
}

絵文字に対応する

わんコメは入力された絵文字を html の img タグで流してくる
(反対にコメ欄に入力された<>はエスケープされる)

さしあたって、img タグだけを1文字単位で扱えればいいので

  • img タグをなんか特殊な文字で置き換え
  • アニメーション付与の処理
  • 特殊な文字を img タグに戻す
    で、対応する

img タグに対応したのは↓
コメント欄に\tが入ることはないので、img タグをいったん\tに置き換えて最後に戻す

const attachTypingAnim = (comment) => {
  const imgReg = /<img [^<>]+>/g
  const imgTags = [...comment.matchAll(imgReg)]
  const placeHolderToken = '\t'
  comment = comment.replaceAll(imgReg, placeHolderToken)

  comment = Array.prototype.map.call(comment, htmlToTypeChar).join('')

  imgTags.forEach((value) => {
    comment = comment.replace(placeHolderToken, value[0])
  })

  return comment;
}

animation-delay を大量に使うとアニメーションがなんかバグる

最後に、長文コメントを入れると、縦棒が消えるアニメーションが実行されたりされなかったりするバグが出た
chrome だと起こって firefox だと起こらない
ググるとそれっぽい記事は出るけども、イマイチ原因が分からない

色々いじった結果、縦棒のアニメーションに中間点を入れるとなぜか解決した
これに関しては本当に謎なので、分かる人教えてください🙇

@keyframes bar {
  from {
    border-right: 0.1em solid var(--cui-main-color);
  }
  80% {
    border: none;
  }
  to {
    border: none;
  }
}

Discussion