SVGでパス上の文字の均等配置をブラウザ(Win10のFirefox, Chrome, Edge)によらずに実現する方法について調べました。
SVGではtext要素にtextLength属性を設定することで文字間隔を調整できる。
textLengthの値はテキストの幅を表していてテキストがそれに合わせて自動的に配置されるから……多分。
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="20">
This is a pen.
</text>
<text x="0" y="60" textLength="300">
This is a pen.
</text>
</svg>
text要素はtextPath要素と組み合わせることでパスに沿ってテキストを配置することができる。
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<path d="M 30 160 Q 150 0 300 160" stroke="black" fill="none" id="path1"></path>
<text>
<textPath href="#path1">This is a pen.</textPath>
</text>
</svg>
本題はパスを使っているときの文字間隔の調整方法についてである。
普通に考えてtext要素のtextLength属性を使えばいいと思うが、なんとtextPath要素にもtextLengthがある。
どっちを使えばいいのかわからない。
わからないから試してみた。
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg" style="font-family: Times New Roman;font-size: 14px;">
<g>
<path d="M 30 160 Q 150 0 300 160" stroke="black" fill="none" id="path1"></path>
<text>
<textPath href="#path1">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 200 Q 150 40 300 200" stroke="black" fill="none" id="path2"></path>
<text textLength="300">
<textPath href="#path2">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 240 Q 150 80 300 240" stroke="black" fill="none" id="path3"></path>
<text>
<textPath href="#path3" textLength="300">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 280 Q 150 120 300 280" stroke="black" fill="none" id="path4"></path>
<text textLength="300">
<textPath href="#path4" textLength="300">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 320 Q 150 160 300 320" stroke="black" fill="none" id="path5"></path>
<text textLength="150">
<textPath href="#path5" textLength="300">This is a pen.</textPath>
</text>
</g>
</svg>
Firefoxではtext要素のtextLength属性の値が使われる。
ChromeではtextPath要素のtextLength属性の値が使われる。
なのでFirefoxとChromeで見た目を揃えたいなら、両方の属性に同じ値を設定すれば良さそうだ。
問題はEdgeである。どっちの値も使われないのか何一つ見た目が変わらない。やる気が感じられない。
3つのブラウザ間で見た目を揃えるにはtextLengthでは駄目そうだ。
そもそもSVGに詳しくないのでパス上の均等配置にtextLengthを使うのが真っ当な方法かどうかも知らない。
textLength以外にも文字間の間隔を設定する方法がある。
それはtext要素のletter-spacing属性。名前の通り文字間の間隔値を表している。
均等配置が目的だから先にこっちを試せば良かったな……。
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg" style="font-family: Times New Roman;font-size: 14px;">
<g>
<path d="M 30 160 Q 150 0 300 160" stroke="black" fill="none" id="path1"></path>
<text letter-spacing="20">
<textPath href="#path1">This is a pen.</textPath>
</text>
</g>
</svg>
あっちを立てればこっちが立たず。仲良くする気がない三人だ。
ならばtextLengthと組み合わせてみようか。
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg" style="font-family: Times New Roman;font-size: 14px;">
<g>
<path d="M 30 160 Q 150 0 300 160" stroke="black" fill="none" id="path1"></path>
<text letter-spacing="20">
<textPath href="#path1">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 200 Q 150 40 300 200" stroke="black" fill="none" id="path2"></path>
<text textLength="150" letter-spacing="20">
<textPath href="#path2">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 240 Q 150 80 300 240" stroke="black" fill="none" id="path3"></path>
<text letter-spacing="20">
<textPath href="#path3" textLength="250">This is a pen.</textPath>
</text>
</g>
<g>
<path d="M 30 280 Q 150 120 300 280" stroke="black" fill="none" id="path4"></path>
<text textLength="150" letter-spacing="20">
<textPath href="#path4" textLength="250">This is a pen.</textPath>
</text>
</g>
</svg>
答えが見えてきた。各ブラウザでの属性の採用規則は次のようになると思われる。
(◎:最優先、○:優先、×:使用されない)
- Firefox : ○text.textLength ×textPath.textLength ×text.letter-spacing
- Chrome : ×text.textLength ◎textPath.textLength ○text.letter-spacing
- Edge : ×text.textLength ×textPath.textLength ○text.letter-spacing
要するに、描画したい文字列の間隔がwでその間隔で描画したときの文字列の幅がyならば
text要素とtextPath要素のtextLength属性にyを、text要素のletter-spacing属性にwを設定すれば
3つのブラウザ上で見た目が揃うはずである。
今回はy=300に固定しているが、その場合のwは何だろう?
ちょっと動かしてみたところ、letter-spacingが設定されていないときはletter-spacing=0となるらしい。
ならばletter-spacingとtextLength属性が設定されていないときの文字列の幅を計算できれば
yとの差分からwの値を計算できそうだ。
次のコードはそのXを計算するコードである。
const textLength = 300;
const svgText = document.getElementById('text1');
const svgTextPath = document.getElementById('textPath1');
//letter-spacing=0のときのテキストの幅
const box = svgText.getBBox();
//元々のテキストの幅とtextLengthのギャップ
const diff = textLength - box.width;
//テキストの長さ
const number = svgTextPath.textContent.length;
//文字間の隙間の数はテキスト長-1だからギャップ/テキスト長-1で割れば大丈夫だろう…
const w = diff / (number - 1);
//Edge用属性
svgText.setAttribute("letter-spacing", `${w}`);
//Firefox用属性
svgText.setAttribute("textLength", `${textLength}`);
//Chrome用属性
svgTextPath.setAttribute("textLength", `${textLength}`);
いい感じに見た目が揃った。FirefoxとChromeでちょっとずれている気がするとか思ってはいけない。
それにしても2018年にもなってこんな仕様の違いに四苦八苦するのはどういうことだろう?
もしかしたらもっとシンプルで真っ当な方法があるのかも。いや、あってほしい。
以上です。