2017-03-23

tobijibu

バラバラな文字が集まってくるアニメーションを作成する

この前のCSSアニメーション作成が思った以上に面白かったので、別のアニメーションを作ってみました。

今回は、文章の文字をバラバラにして、特定のトリガーで文字が元の位置に戻ってくるといったアニメーションです。 今回はCSSだけではなく、JavaScriptをメインで実装しています。

以下がデモです。"ぐちゃぐちゃを戻す"をクリックすると元の文章に戻ります。WikipediaのJavaScriptの文章を使っています。

ぐちゃぐちゃを戻す

JavaScriptという言葉は狭義にはMozillaが仕様を策定し実装しているスクリプト言語を指す。このスクリプト言語はEcmaインターナショナルでECMAScript (ECMA-262) として標準化されており、多くのウェブブラウザ等はこの標準化されたECMAScriptを実装している。たとえば、マイクロソフトによる実装はJScriptと呼ぶ。

一般的にJavaScriptという言葉が使われるときはこのようなさまざまなECMAScriptの実装も含んだ幅広い意味でつかわれるので、どちらの意味でJavaScriptという言葉が使われているかは文脈で判断する必要がある。

ECMAScriptは仕様自体に独自の拡張を条件付きで認める記述があり、現在主要なブラウザが実装しているスクリプト言語はすべてECMAScriptに準拠していることになる。広義の意味でこれをJavaScriptと呼ぶ場合、主要なブラウザが実装しているスクリプト言語はマイクロソフトやGoogle、アップルの実装も含めてJavaScriptである。

なお、ウェブブラウザでよく実装されているAPIであるDOM (Document Object Model) はECMAScriptの仕様の一部ではないので、DOMの準拠の有無はECMAScriptの準拠の有無とは関係ない。

文章をぐちゃぐちゃにする

まずは文字をぐちゃぐちゃにしてしまいます。

setGuchax2() {
  let sentence  = document.querySelectorAll('.gucha'),
      character = new Object(),
      _sentence = '', _character = '',
      charType = 0, continueFlg = 0;
  for (const key of Object.keys(sentence)) {
    //文章内の特殊文字を16進数化して、文字を1文字ずつに分割する
    character[key] = this.replaceHexSpChar(sentence[key].innerHTML).split('');
    for (const k of Object.keys(character[key])) {
      //HTMLタグ、特殊文字は1塊にしたいので、「塊」かどうかの判定
      continueFlg += this.setContinueFlg(character[key][k]);
      if (continueFlg === 1) {
        //HTML、特殊文字は次の文字に繋げる
        _character += character[key][k];
        continue;
      } else if (continueFlg === 3) {
        //HTML、特殊文字の終わり文字
        _character += character[key][k];
      } else {
        //HTML、特殊文字どちらでもないので連結しない
        _character = character[key][k];
      }

      //文字を連結する
      //HTMLタグはそのまま連結、通常文字は位置を設定する
      _sentence += (this.setCharType(character[key][k]) === 2) ?
                    _character : this.setCharacterPosition(_character);

      //挿入対象文字、文字判定フラグをリセット
      _character = '';
      charType = continueFlg = 0;
    }
    //文章を原文の後ろの位置に挿入
    _sentence = '<div class="w_gucha">' + _sentence + '</div>';
    sentence[key].insertAdjacentHTML('afterend', _sentence);
    _sentence = '';
  }
  //原文を削除
  for (const key of Object.keys(sentence)) {
    sentence[key].parentNode.removeChild(sentence[key]);
  }
}

.guchaが設定されている要素を全て抽出し、さらにその要素の文字を1文字ずつ分割します。

分割する時にhtmlタグや、特殊文字が含まれている場合があるので、タグや特殊文字を判定する処理を入れておきます。 タグや特殊文字の抽出と設定については後述します。

分割した文字は1文字ずつ<span>で囲って、さらの文章全体を<div class="w_gucha">で囲みます。

生成した文章はinsertAdjacentHTMLを使って追加します。元の文章はもう使わないので、削除しています。

htmlタグ、特殊文字を判定する

先ほどの説明で飛ばした、htmlタグや特殊文字の抽出について説明します。

まず前提として、htmlタグや特殊文字はどんなパターンがあるかを考えました。そして、以下のように定義しました。

  • htmlタグは<で始まり、>で終わる。
  • <>で囲われた範囲をhtmlタグとする。
  • 特殊文字は&で始まり、;で終わる。
  • &;で囲われた範囲を特殊文字とする。

この定義を元に作成した処理が以下です。 1が開始文字、2が終端文字であることを返します。 これでhtmlタグ、特殊文字の開始と、終端判定ができました。

setContinueFlg(str) {
  return (str === '&' || str === '<') ? 1 :
         (str === ';' || str === '>') ? 2 : 0;
}

この処理を使って、「1つの塊であること」を判定し、htmlタグ、特殊文字であれば文字を連結していきます。

開始文字が出た場合は、continueFlg1になります。 その後、終点文字が出現するまでは1が続き、終点文字が出現した段階で、 setContinueFlgから2が返ってくるので、continueFlg3になると終端ということになります。

continueFlg += this.setContinueFlg(character[key][k]);
if (continueFlg === 1) {
  _character += character[key][k];
  continue;
} else if (continueFlg === 3) {
  _character += character[key][k];
} else {
  _character = character[key][k];
}

htmlタグの扱い

さて、ここまでに1文字ずつ分割し、htmlタグ、特殊文字を判定してきましたが、1つ気になる点があります。それは、htmlタグの扱いです。

特殊文字は複数の文字で表されますが、表示上1文字として扱いますが、htmlタグは事情が違います。 文字ではなく、タグそのものとして扱う必要がありますね。

そこで、htmlタグか、それ以外の文字かを判定する処理が必用になります。

判定は以下の処理で実現します。先ほどの処理とそっくりですが、1が特殊文字、2がhtmlタグ、それ以外は0を返すようにしています。

setCharType(str) {
  return (str === '&' || str === ';') ? 1 :
         (str === '<' || str === '>') ? 2 : 0;
}

この判定を元に、通常文字ならば<span>で囲う処理を入れて、htmlタグはそのまま連結します。

_sentence += (this.setCharType(character[key][k]) === 2) ?
              _character : this.setCharacterPosition(_character);

文字の位置を設定

先ほどから<span>で囲うと説明していました。何故でしょうか。それは文字の位置を設定するためです。

1文字ずつ抽出した文字を<span>で囲って、transformを指定することで、文字の位置を指定します。

文字のX軸、Y軸の位置と、角度をランダムに取得して、style属性で指定します。

setCharacterPosition(character) {
  if (character.trim() == '') return character;
  let y_position = this.randPercentage() + 'px',
      x_position = this.randPercentage() + 'px',
      rotate     = this.randRotate() + 'deg';
  return '<span class="char-position" ' +
              'style="transform: translate(' + x_position + ',' + y_position + ')'+
                  'rotate(' + rotate + '); display:inline-block">' +
          character +
         '</span>';
}

この処理ではcreateElementしてappendChildという処理を使っていません。 本来ならばcreateElementしてstyleを使い、appendChildした方が見やすく、処理も簡潔です。

しかし、ここでappendChildを使わない理由は、テキストの方がhtmlタグの挿入のしやすいためです。

appendChildを使ってDOMオブジェクトで操作してしまうと、htmlタグの開始、終了といった概念が無いので、htmlタグ内の文字を一括で追加しなければなりません。htmlタグが入れ子状態の場合、複雑な処理になってしまいます。

そのため、今回の場合はhtmlタグもテキストとして連結し、最後にまとめてinsertAdjacentHTMLを使って処理しています。

これで文字をバラバラにすることができました。

文字を戻す

文字を元の戻す時にはreturnPositionを使います。

この関数では.child-positionが指定されている要素のクラスを.posi-zeroに書き換えています。 クラスを書き換えるので、.posi-zeroのアニメーションが動きます。

また、transformにはnullを与えて、文字の位置と角度をリセットします。

JS
returnPosition() {
  let character = document.querySelectorAll('.char-position');
  for (const k of Object.keys(character)) {
    character[k].className = 'posi-zero';
    character[k].style.transform = null;
  }
}
CSS
.posi-zero {
    transition: 1.5s cubic-bezier(0.755, 0.05, 0.855, 0.06);
    animation: bright 1s ease-in-out 1.5s 1 alternate;
}
@keyframes bright {
    0% {
        color: rgba(0, 191, 165, 0.8);
        text-shadow: 0px 0px 5px rgba(0, 191, 165, 0.3);
    }
}

いかがでしょうか。少し難しいかなと思いましたが、何とか上手くできました。

今回のパターンを利用すれば、"1文字ずつ戻す"とか、"スクロールに合わせて1行ずつ戻す"といったことも出来ると思います。 色々イメージが膨らみそうですね。


今回説明で利用したソースはこちらにあります。