SVG の path 要素のみを用いたくり抜き図形について
react-share を LINE に対応させるプルリクエストを作成する際に SVG アイコンを用意する必要があり、いろいろと慣れない作業をしました。 SVG をエディタで開いて手作業で編集する機会はなかなかないと思うので、半分作業メモのような形で記録を残しておきます。
react-share のアイコンについて
react-share は Facebook や Twitter などのソーシャルシェアボタンの React コンポーネントライブラリです。 LINE には対応していなかったので、対応させるプルリクエストを送りました。
react-share ではアイコンに SVG を使っています。
色付きの正方形 rect
または円 circle
の上に、白色の塗りつぶしパス path
を描写することでアイコンを形成しています。
LINE のアイコンを用意するときに悩んだのは、path
要素が 1 つしか使われていないことです1。
1 つの path
要素の d
属性 と fill
属性のみを使って、文字の部分をくり抜いたような図形(以下、くり抜き図形と呼ぶ)を作るのに、どうすればいいのか当初はさっぱりわかりませんでした。
デモを見ると、Reddit のアイコンは明らかにくり抜き図形なので、何らかの方法でできるはずだと思っていろいろと調査しました。
path
要素を使って塗りつぶし図形を作るには
まず SVG のパス(path
要素)を用いて塗りつぶし図形をつくるのに簡単に解説をしておきます。
path
要素の図形は d
属性によって定義されて、d
要素には一連のコマンドによって定義されるパスを複数設定することができます。簡単のため、ここでは直線のみのパスを考えることにします。一連のコマンドとは、以下のようなコマンドを並べたものです。
M x y
: パスの始点を座標(x, y)
に設定するH y
: 現在の位置から Y 座標y
への垂直な直線を描画するV x
: 現在の位置から X 座標x
への水平な直線を描画するZ
: 現在の位置からパスの始点へ戻る直線を描画する
そして fill
属性に色を指定することでパスで囲まれた部分が塗りつぶされます。例えば正方形を描画する path
は次のようになります。わかりやすさのためにパス自体にも stroke
属性を利用して色を付けています。
<svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg>
コマンドの詳細は Paths - SVG: Scalable Vector Graphics | MDN などを見てください。
path
要素の fill-rule
属性について
単純な図形の塗りつぶしなら問題はありませんが、くり抜き図形を塗りつぶそうとするとうまくいかない場合があります。そこで用いられるのが fill-rule
属性です。
fill-rule
属性は複数のパスで囲まれる部分がパスの内側かどうかを判断して塗りつぶしを制御するための属性で、値としては主に nonzero
(省略した場合のデフォルト値) と evenodd
があります。
fill-rule="nonzero"
(省略時のデフォルト値)
ある視点を図形の外から内へ向けて進めていくと仮定します。カウント 0 から始めて、右向きのパスを横切るときにカウントに 1 を加え、左向きのパスを横切るときにカウントから 1 を引きます。そしてカウントが 0 でない部分を塗りつぶします。
これを利用してくり抜き図形を作るときは、外側のパスと内側のパスを逆向きにする必要があります。
<!-- 左: 外側パスが反時計回り、内側パスが半時計回り。くり抜けない。 --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 H 70 V 70 H 30 Z" fill-rule="nonzero" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg> <!-- 右: 外側パスが反時計回り、内側パスが時計周り --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 V 70 H 70 V 30 Z" fill-rule="nonzero" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg>
fill-rule="evenodd"
同じくある視点を図形の外から内へ向けて進めていくと仮定します。カウント 0 から始めて、パスの向きは考慮せずパスを横切るときにカウントに 1 を加えます。そしてカウントが奇数の部分を塗りつぶします。
これを利用してくり抜き図形を作るときは、外側のパスと内側のパスの向きが合っているかどうかは無関係になります。
<!-- 左: 外側パスが反時計回り、内側パスが反時計回り --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 H 70 V 70 H 30 Z" fill-rule="evenodd" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg> <!-- 右: 外側パスが反時計回り、内側パスが時計周り --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 V 70 H 70 V 30 Z" fill-rule="evenodd" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg>
fill-rule="evenodd"
な path
要素を fill-rule="nonzero"
に変換する
こちらが作業メモになります。
ベクタ画像の編集に Gravit Designer を使っているのですが、これでエクスポートした SVG には fill-rule="evenodd"
属性が付いていました。
まず fill-rule
属性を削除して、図形の様子を確認しました。
案の定どの文字もくり抜かれていなかったので、パスの向きを逆にする必要がありました。
何か良いツールはないかと探したら、svg-path-reverse を発見し、これを使ってパスの向きを逆にすることができました。 使い方はこのような感じです。
const { reverse } = require('svg-path-reverse'); const path = 'ここに d 要素の中身'; console.log(reverse(path, 0));
reverse
関数の第 2 引数にサブパスのインデックス(0 始まり)を指定することで、指定したサブパスだけを逆向きにできます。
今回は外側のパスが最初のサブパスであり、外側のパスだけ逆向きにすればよかったので、第 2 引数に 0
を指定しています。
パスの向き変更とは関係が薄いですが、svg-path-reverse が出力したパスは冗長になりがちだったので、svgo という SVG を最適化するツールも併用しておきました。
まとめ
内部がくり抜かれた塗りつぶし図形を SVG の path
要素のみを用いて描画する方法を調査し、簡単にまとめました。
fill-rule="nonzero"
(もしくは省略時)の場合に外側のパスの向きと内側のパスの向きを逆にする方法と、fill-rule="evenodd"
を使う方法の 2 通りを紹介しました。