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

JavaScript, CSS

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

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

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

line-gradientを使う

IE を考えなくていいなら、CSS の line-gradient が使える。 https://codepen.io/dkrk/pen/OrgZea

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-sizetransition が動かないので…。
それとこの方法では文字を直接操作できない。

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

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

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

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

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

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

前提として、文字のカウントに使う要素は paddingmargin, 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 が、多分似たようなことをいい感じにやってくれるのではないかと信じている。
もし、折り返す文字のアニメーションに妥協できないようなプロジェクトがあれば、お金も妥協しないで出してもらうのがいいかもしれない。