UNORTHODOX WORKBOOK

BLOG TOPFront-EndSVGでハンドライティングアニメーションを実装する

SVGでハンドライティングアニメーションを実装する

catch-svg-handwriting-animation

文字を書き順通りに徐々にアニメーションさせて描画する「ハンドライティングアニメーション」をSVGで実装する方法。

以前に AfterEffects で同じようなことをやっていて、それのSVGバージョン。AfterEffects の場合は、当然ながら動画として出力しvideo要素で動画を埋め込んで表示させるわけだけど、SVGの場合は、(主に)インラインSVGを設置してJavaScriptでアニメーションさせて実現する。

少し前までは、一部のブラウザでうまく表示できなかった(気がした)が、今は全く問題なく表示させることができるようになっていた。古いIE(10以前)ではさすがに無理だけど、もう考慮する必要はないと思われるので、十分に使える表現だと思う。

SVGの作成が意外に面倒だったので作成時のポイントとか、HTMLへ設置する方法、JS実装ではまった点等を覚書として書いておく。

ハンドライティングアニメーションとは

正直、正式な名称はよく分からないんだけど、以下のようなアニメーションを「ハンドライティングアニメーション」と勝手に呼んでいる。

svg-handwriting-animation-1

これをSVGで表現する場合、SVGのマスキング(mask要素)を用いる方法とクリッピング(clipPath要素)を用いる方法の2通りの方法がある。両方とも試してみて、簡単なデモも作ってみた。

DEMO

Chrome、Safari、Firefoxでは、どちらも問題なく表示できているけど、EdgeとIE11ではクリッピングを用いた方法がうまく表示できていない。理由はよく分からなかった(SVGの書き方がよくないのかもしれない)が、記述的にもマスキングの方がシンプルなので、実際に使用する際はmask要素を用いた方法でやるのがいいと思う。ちなみにJSはどちらも同じ。

デモのコード一式はGitHubにアップしてある。

svg-handwriting-animation

大まかな仕組みや必要なもの

ざっくりと解説すると以下のような感じになる。

必要なSVG画像

前述の通り、この実装には2通りの方法があるが、どちらの場合もそのものの形をあらわすベースとなるSVG画像(以下「ベース画像」という)とベース画像に沿ってアニメーションするSVG画像(以下「アニメーション画像」という)の2つのSVGが必要となる。

ベース画像はそのものの形をあらわす外枠となる画像なので、必然的にクローズドパスとなるが、アニメーション画像は一方向に進むだけの画像なので、基本的にはオープンパスとなる(絶対ではない)。

今回は、JSの実装上、アニメーション画像のSVGはpath要素で構成されている必要があるので、その点だけ注意が必要。

なお、クローズドパスでも複雑な形であればpath要素で出力されるし、オープンパスでもpath要素で出力されない場合もある。また、設定でも変わってくると思うので、出力されたSVGコードの確認は必須。この辺りは後述する。

余談だけど、ベース画像をSVG画像ではなく、SVGのtext要素で代用することもできた。ただ、位置を合わせるのが非常にめんどくさいのと、レスポンシブ化がほぼほぼ不可能なのでやらない方がいい。

仕組み

簡単な仕組みを説明すると、

  1. アニメーション画像の strokeDasharraystrokeDashoffset にその pathlength 値をセットし、アニメーション画像の stroke が全く表示されない状態にする
  2. マスキングの場合にはマスクの効果によって、クリッピングの場合にはクリップマスクの効果によって、ベース画像の範囲を超えて描画されないようにSVGで配置( mask 属性または clip-path 属性を用いてベース画像とアニメーション画像をリンク)する
  3. JSでアニメーション画像の strokeDashoffset 値を徐々に減らしていくことで、アニメーション画像の stroke が徐々に表示される( strokeDashoffset が 0 になると全長が表示される)。それに伴いベース画像が塗りつぶされていくように見える
  4. 手で書いたような(ハンドライティング)アニメーションになる

という感じ。

SVGのstrokeDasharraystrokeDashoffsetを利用したアニメーションは前からよくあるけど、これとマスク(またはクリッピング)の効果を利用しているという点が肝になってる。

SVG画像の作成

まず、mask要素を用いる方法の場合、ベース画像は外部SVGファイル、アニメーション画像はインラインSVGが必要になる。ベース画像はSVG形式でなく、透過のPNG形式でも大丈夫だけど、Retinaディスプレイ等で閲覧した場合、ぼやけることがあるのでSVG形式がオススメ。

clipPath要素を用いる方法の場合には、ベース画像、アニメーション画像ともにインラインSVGとなる。

作成のポイント

SVG画像を作成する際のポイントは以下の通り。

  • ベース画像をアニメーション画像ので確実、かつ重ならないようにギリギリで覆う
  • アニメーション画像は path要素で出力されるよう考慮する
  • ベース画像、アニメーション画像ともに同じ大きさ(viewBox)となるようにする

作成方法

SVGを作成するツールはなんでもいい。以下はイラストレーターを使用した場合の記載となる。また、文字(テキスト)をアニメーションさせる想定で進める。なお、同じアートボード上にベース画像、アニメーション画像を同時に作成していく。

新規作成

まず、カラーコードをRGBにして新規ドキュメントを作成する。大きさは適当でいいけど、小さすぎるとSVGに出力した際に数値がおかしくなるので、なるべく大きめで作っておく。

ベース画像の作成

ベース画像は、文字ツールを使って好きなテキストをタイプし、アウトライン化する。カーニングとかは適当に。ベース画像はこれだけで完了。

svg-handwriting-animation-2
テキストをアウトライン化

アニメーション画像の作成

アニメーション画像は、ベース画像に合せて描画させたい順(アニメーション順)にペーンツールでアンカーポイントを打っていき、線(の太さ)で覆っていく。

svg-handwriting-animation-3
赤がアニメーション画像(色はなんでもいい)。分かりやすいように不透明度を下げている

パスは、文字ごとに分けても、繋げられるだけ繋げてしまってもいい。ただ、文字ごとに分けた方が線の太さを途中で変えられたり、pathごとに色を変えることができたりするので柔軟性が高くなる。

オープンパスが基本だけど、SVGに出力された時にpath要素として出力されればいいので、閉じてしまってもpath要素で出力されていれば問題はない。

パスが直線の場合は、オープンパスであってもline要素として出力されてしまうので、もし一直線のパスがある場合には「複合パス」にしておくことでpath要素で出力されるようになる(ここはイラストレーターの設定によって変わるかもしれないが念のため)。

svg-handwriting-animation-4
一直線のパスは「複合パス」にしておく

ベース画像を全てパス(線)で綺麗に覆うことができたらアニメーション画像は完了。

svg-handwriting-animation-5
ベース画像がアニメーション画像に完全に覆われていればOK

viewBox について

ベース画像とアニメーション画像は別々に使用するわけだけど、個別にSVGとして保存してしまうとviewBoxの値が異なってしまうので、一旦、それぞれでグループ化だけしておいて(レイヤー名を半角英数でつけておくと分かりやすい)一緒にSVG化してしまうのがいい。

また、viewBoxの値はアートボードの大きさと同じになるため、(大きい方の)アニメーション画像に合わせたサイズに調整しておく必要がある。

なお、アニメーション画像は「線」だけでできているので、そのままだと「線」を含めた正確な大きさが分からない。もし正確な大きさを割り出したければ、一旦「オブジェクト」→「パス」→「パスのアウトライン」でパスをアウトライン化すれば正確な値を知ることができる(小数点は繰上げでおk)。

SVGで保存

SVGにする方法はいくつかあるが、「別名で保存」からのファイル形式を「SVG(svg)」にして「保存」すると、「SVGオプション」→「詳細オプション」でいろいろと設定できて分かりやすかった。

後にSVG化したファイル自体を編集する必要があるので、詳細オプションの「CSS プロパティ」を「スタイル要素」としておくと色々と捗る。

「小数点以下の桁数」は、1〜3ぐらいにして、「エンコーディング」は「UTF-8」。「レスポンシブ」にチェックを入れるとwidthheightが省略されるようなので、チェックを入れた場合には、CSS側で設定する必要がある。

svg-handwriting-animation-6
「詳細オプション」画面

設定の問題なのかもしれないけど「書き出し」でSVG化するとワンライン(改行が削除された状態)で出力されてしまい、その後の加工が大変になる。「別名で保存」の場合には、適度に整形されていて扱いやすかった。

これでSVGの作成は完了。

HTMLへ設置

作成したSVGをエディタで開き、ベース画像とアニメーション画像に分離する。イラストレーターでそれぞれグループ化してあれば、ベース画像とアニメーション画像はg要素で分離され、id属性にレイヤー名がセットされているはず。

マスキングの場合

アニメーション画像の部分をg要素ごと切り取り、HTMLの任意の場所へ貼り付ける。g要素はmask要素へ変更しid属性を付加、defs要素でラップした上でsvg要素でマークアップする。

そして、各path要素にclass属性をセットし、各path要素にあったstroke-width値をCSSで指定し、fill:none;stroke:#fff;を全path要素へCSSであてる。

アニメーション画像を切り取って残った部分は、ベース画像として利用するため、不要な部分(style要素とか、g要素もいらない)を削除した上で、svg要素にfill属性を付加し描画させたい色を指定する(ここ重要)。path要素ごとにfill属性をセットすれば、それぞれで色を切り替えることもできる。

マスキングの場合、ベース画像は外部ファイルとするので、これを保存して、HTMLのsvg要素にimage要素を追加し、xlink:hrefmask属性でアニメーション画像とベース画像をリンクさせる。

あとはコードを見れば分かる。

index.html

<svg class="p-svg__mask" viewBox="0 0 892 135" id="js-mask">
  <defs>
    <!-- アニメーション画像 -->
    <mask id="clipMask">
      <path class="p-svg__path sw15" d="...<!-- 省略 -->..."/>
      <!-- 省略 -->
      <path class="p-svg__path sw20" d="...<!-- 省略 -->..."/>
    </mask>
  </defs>
  <!-- ベース画像 -->
  <image xlink:href="./images/base.svg" width="100%" height="100%" mask="url(#clipMask)"></image>
</svg>

_svg.scss

.p-svg__path {
  fill: none;
  stroke: #fff;
  &.sw13 {
    stroke-width: 13px;
  }
  &.sw14 {
    stroke-width: 14px;
  }
  &.sw15 {
    stroke-width: 15px;
  }
  &.sw16 {
    stroke-width: 16px;
  }
  &.sw19 {
    stroke-width: 19px;
  }
  &.sw20 {
    stroke-width: 20px;
  }
}

マスク効果の黒を透明にし白を描画色で表示するという性質を利用しているので、アニメーション画像を白くして(stroke:#fff;)、ベース画像自体に表示させたい色を指定している。

クリッピングの場合

クリッピングの場合は、ベース画像、アニメーション画像ともにインラインSVGとしてHTMLへ貼付する。ベース画像部分をclipPath要素で囲み、色はアニメーション画像で指定する。関連づけが必要になるので、アニメーション画像はg要素で囲んでおく。

クリッピングに関しては、コードを見ればだいたい分かると思う。CSSは上記と同じなので省略。

index.html

<svg class="p-svg__clip" viewBox="0 0 892 135">
  <defs>
    <!-- ベース画像 -->
    <clipPath id="clipPath">
      <path d="...<!-- 省略 -->..."/>
      <!-- 省略 -->
      <path d="...<!-- 省略 -->..."/>
    </clipPath>
  </defs>
  <!-- アニメーション画像 -->
  <g id="js-clip" clip-path="url(#clipPath)">
    <path class="p-svg__path sw15" d="...<!-- 省略 -->..."/>
    <!-- 省略 -->
    <path class="p-svg__path sw20" d="...<!-- 省略 -->..."/>
  </g>
</svg>

JavaScriptでアニメーション

はじめに、JSについては以下を大いに参考にさせてもらいました。

alphabet-svg

使い方など

SVGのアニメーションは、JSのrequestAnimationFrameで行っている。ライブラリを利用するのが手っ取り早いが、requestAnimationFrameを使いたかったのでライブラリは使用しなかった。

使い方は、querySelectorAllでアニメーションさせたいSVGのpathを取得して、newしてplayAnimation()を実行すれば動く。SVGの構成なりが間違っていなければ、配置したpath順に次々自動でアニメーションを開始する。

JavaScript

const target = Array.from(document.querySelectorAll('#id-name path'));
const phw = new PlayHandwriting(target, speed, interval); // 第一引数に要素(path要素を指定)、第二引数にスピード(数が小さい方が早い)、第三引数にpath間のインターバル(ミリ秒)
phw.playAnimation();

EdgeとIE11では、querySelectorAllで取得した要素(NodeList)をforEachで配列として渡せない(Arrayではないから?)ようなので、Array.fromでNodeListからArrayを生成すればいけた。

アニメーションのスピードに関して、一貫して(全長で)同じ速度でアニメーションさせたかったため、一般的なアニメーションにおける時間単位の指定ではなく、ただの数値(小さいほど早い)を第二引数に指定することでアニメーションスピードを変更できるようにした。「全長で何秒」みたいな指定が理想だったんだけど、速さ・時間・距離みたいなのが苦手すぎて無理だった(もう考えたくなかった)。

個人的に、newを配列に突っ込んでforEachで回してインスタンスを生成するっていうのが新たな発見だった。また、forEach便利すぎて使いまくってる。

JavaScriptに関しては、ほぼ全ての部分ではまったし、自信ないので変なところがあったら教えてもらいたいです。

終わり

これ読んで実装できる人は、これ読まなくても実装できるんじゃないかってくらいの内容ですが、あくまでも自分用のメモなのでご容赦ください。

もし読んでいただいて間違いとかあったらお気軽に指摘していただけれると嬉しいです。

ABOUT

it's me

長野県北部を拠点にフリーランスとして活動しています。
Webサイトの制作をメインに、グラフィックデザインなどの制作も行っています。 Twitter / GitHub / About

PAGE TOP