折り返しのあるテキストのアニメーション

「折り返しのあるテキスト」とは、具体的には以下のようなものを指す。

  • 複数行にわたる
  • brタグや改行がない
  • あらかじめwrap要素で囲まない
  • レスポンシブ

今回はこのしょうもないアニメーションに対応した。

line-gradientを使う

IEを考えなくていいなら、CSSのline-gradientが使える。

line-gradient()は、グラデーションを作成するCSSの関数。
これはbackground-image扱いになるため、background-sizeが効く。
文字をspanなどのinline要素で囲み、background-image: line-gradient()を設定し、background-sizeの横を0から100%に広げるようなtransitionを設定すれば、サンプルコードのようなアニメーションが作成できる。

line-gradient()は、カンマで区切ると2つ以上設置できる。(サンプルコードの一番下)
さらに凝ったものを作る場合は、repeating-linear-gradient()が使える。

IEには対応していない。background-sizeのtransitionが動かないので…。
それとこの方法では文字を直接操作することができない。

折り返しまでのテキストを取得してみる

改行コードやbrタグを取得する、いわゆるforced line breakのための方法はたくさんあるけど、
折り返しまでのテキストを取得する方法がなかなか見つからない。

GreenSockのプラグインに、SplitTextというのがある。
この中に"natural line break animation"というドンピシャな機能があるけど、
有料ライセンスなので気軽に使えない。

要素をはみ出るまで文字を取得して、はみ出したら1行のデータとして出力、という方法にした。
CanvasのmeasureText()のようなものがないので、空要素に1文字ずつ複製する必要がある。

そんなに難しくない、と思っていた。
てきとうな要素に文字突っ込んで幅を数えていくだけだし…。

試しにやった。
折り返しまでのテキスト(日本語)を取得して、要素で囲んでアニメーションさせる。
リサイズで再度出力する。

前提として、文字のカウントに使う要素はpaddingやmargin、borderを全て消した状態にする。
また、本文とカウント用の要素のフォントは合わせる。

まず、ベースとなる文字をどこかから取得して、配列としてreduceで回す。
そのreduceの中で、仮の要素に一文字ずつ入れていく。
禁則処理(後述)を挟み、仮要素がベースの幅からはみ出るまで繰り返す。
はみ出たところまでが1行となるので、それをspanで囲んで出力する。

スペースは、前後に文字がないと消えてしまうので、前に挿入させる。
brタグはUnicodeに変換し、reduceで拾った際に、はみ出た時と同じ処理を行う。

spanで囲むとか言いながら、サンプルではdivで囲んでた。
block要素の場合white-space: nowrap;をつけないと、最後の文字で折り返してしまうことがある。
重そう、など問題も色々あって結構つらい。

また、アプローチしやすいアニメーションも全く異なる。
何しろ、これで出力した要素に下線を引くアニメーションをつける場合、囲んだ要素をまたいで線を引く処理が必要になる。
linearならともかく、easingだと地獄。

禁則処理について

普通にテキストの幅をはかるだけでも問題ないけど、テキストには折り返しの規則があって、
この規則に従わないと、行を正しく区切ることができない。

W3Cの"CSS Text Module Level 3"では、折り返しの機会(soft-wrap-opportunity)についての指定がある。

CSS Text Module Level 3

長すぎるので要約すると、

  • UAはコンテンツの幅に収まるように強制的でない改行(折り返し、soft-wrap)をいれるべきである
  • 折り返しはスペースや句読点を区切りとして識別させることができる
  • 単語に区切りのない言語(日本語など)は、他の語彙リソースで正しく認識させる必要がある
  • 日本語は単語の途中でも区切れる、改行規則はJIS4051(JIS X 4051)にある
  • CSSで規則のゆるさを設定できる

JIS X 4051でググったら、wikipediaに書いてあった。

禁則処理 - Wikipedia

「行頭禁則文字」「行末禁則文字」が改行規則の対象になる。
行頭に「っ」とか「。」とかがあると、前の行の最後の文字が行頭に移るようになっている。

これを無理やり再現するには、まず文末と文頭の禁則文字を正規表現で用意する。

const re_bunmatsu = /[ゝゞーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇷ゚ㇺㇻㇼㇽㇾㇿ々〻‐゠–〜~?!‼⁇⁈⁉・:;/。.,)\]}、〕〉》」』】〙〗〟’”⦆»]/
const re_bunto = /[\(\[{〔〈《「『【〘〖〝‘“⦅«]/

次に、ループ処理内で、以下の条件を判定する。

  • 「次の次の文字」が「行頭禁則文字」
  • 「次の文字」が「行末禁則文字」

↑の時に「次の次の文字」を仮の要素に足して、
比較する幅を水増しする方法で対処。

if (!isBreakAll && re_bunto.test(texts[index + 2])) {
  tmp.innerText += texts[index + 2]
}
if (!isBreakAll && re_bunmatsu.test(text)) {
  tmp.innerText += texts[index + 2]
}

これだけだと空白+文字+禁則が再現しきれず、ちょっと怪しい…。
ただ、見た目はほとんど変わらないし、元のテキストを消せばまったくわからないのでOKにした。

CSSのword-breakがbreak-allの場合、禁則処理が適用されないので上記の処理は必要ない。

まとめ

自前だとなかなか面倒くさい。
SplitTextが、多分似たようなことをいい感じにやってくれるのではないかと信じている。
もし、折り返す文字のアニメーションに妥協できないようなプロジェクトがあれば、お金も妥協しないで出してもらうのがいいかもしれない。