概要
HTML の中に SVG をインラインで記述して、SVG 内の要素を動的に配置したい時があります。
そんな時、SVG の座標系を正しく理解することが重要です。
この記事では、SVG の座標系について調べたことをまとめます。
また関連する話題として、以下の作業をした時のメモも載せておきます。
- PDF で書いていたものを SVG にした
- SVG で書いていたものを HTML にした
HTML の座標系と SVG の座標系
まず、HTML の座標系と SVG の座標系をはっきりと分けて考える必要があります。
SVG の座標系
SVG 内の各要素の配置は、SVG の座標系に対する配置になります。
下の図で、いちばん左の「SVG の世界」に対して配置するイメージです。
viewBox は、svg 要素の属性です。
これは端的にいうと「SVG の世界のうち、どの部分を描画するか」を決める矩形です。
SVG の座標系に単位はないので、viewBox
も4つの数字のみで指定します。
<svg viewBox="10 10 50 80">
<circle cx="50" cy="30" r="5" style="fill:red;" />
<circle cx="20" cy="80" r="5" style="fill:blue;" />
<circle cx="75" cy="60" r="5" style="fill:yellow;" />
<circle cx="60" cy="105" r="5" style="fill:lightgreen;" />
</svg>
HTML の座標系
HTML の座標系には単位があるので、svg 要素のサイズには単位が必要です。
さきほどの viewBox の内容を 80px * 100px で表示するなら、以下のようになります。
<svg width="80px" height="100px" viewBox="10 10 50 80">
<!-- 中略 -->
</svg>
つまり、svg 要素の属性のうち width
と height
は HTML の世界の話で、viewBox
は SVG の世界の話になります。
width
属性と height
属性はパーセントで指定することもできます。
コツ
仮想の座標単位を決める
SVG の中の要素と SVG の外の要素で、サイズを合わせたい時があります。
たとえばフォントサイズ等は「普通に表示したら普通の大きさになってほしい」と思います。
そんな時、SVG 内の座標系においても仮想の単位を決めておくと間違いが起きにくいです。
SVG 内の座標に単位はありませんが、svg 要素のサイズから単位を取ったものが viewBox のサイズになっていれば、SVG 内の座標も同じ単位であると考えることができます。
<svg width="80px" height="100px" viewBox="0 0 80 100">
<!-- SVG 内の座標の単位が px であると仮定して要素を配置できる -->
</svg>
アスペクト比を固定する
レスポンシブデザイン等で svg 要素のサイズを変化させ、かつアスペクト比を固定したい時があります。
そんな時は CSS の aspect-ratio を使うと上手くいきます。
svg 要素の属性である width
と height
は上書きされます。
<html>
<style>
svg {
width: auto;
height: 100%;
aspect-ratio: 50 / 80;
}
</style>
<svg width="50px" height="80px" viewBox="10 10 50 80">
<!-- 中略 -->
</svg>
</html>
preserveAspectRatio 属性
実は CSS の aspect-ratio
を使わずに width
と height
を両方とも "100%" 等としても、デフォルトでは SVG のコンテンツのアスペクト比は保たれます。
ただし、コンテンツのアスペクト比を保ったまま viewBox を svg 要素に合わせて変形する ので、SVG 内の座標での描画される範囲が変わります。
この振る舞いは svg 要素の preserveAspectRatio
属性 の値によって変わります。
いくつか例を上げますが、詳しくは MDN のドキュメント 等をご覧ください。
例1 viewBox の幅または高さを拡張する (デフォルト)
<svg width="100%" height="100%" viewBox="10 10 50 80" preserveAspectRatio="xMidYMid meet">
<!-- 中略 -->
</svg>
meet
: svg 要素のアスペクト比に合わせて viewBox の幅または高さを拡張します。
xMidYMid
: viewBox の中心が拡張の基準となります。
例2 viewBox の幅または高さを縮小する
<svg width="100%" height="100%" viewBox="10 10 50 80" preserveAspectRatio="xMinYMin slice">
<!-- 中略 -->
</svg>
slice
: svg 要素のアスペクト比に合わせて viewBox の幅または高さを縮小します。
xMinYMin
: viewBox の左上が縮小の基準となります。
例3 viewBox を変えずにコンテンツを変形する
<svg width="100%" height="100%" viewBox="10 10 50 80" preserveAspectRatio="none">
<!-- 中略 -->
</svg>
none
: viewBox はそのままで、コンテンツを svg 要素のサイズに合わせて変形します。
PDF から SVG へ
PDF で書いていたものを SVG にした時のメモです。
PDF 書類を変換するのではなく、「PDF へ出力するロジック」から「SVG にして表示するロジック」に変えることが目的でした。
PDF へ出力する処理は Python の reportlab を使っていました。
座標単位
PDF では座標単位が 'pt' だったので、SVG 内の座標系の単位が 'pt' に対応するようにしました。
PDF で A4 サイズで出していたものを SVG で画面に表示する場合は以下のような感じです。
<svg xmlns="http://www.w3.org/2000/svg" width="841.9pt" height="595.3pt" viewBox="0 0 841.9 595.3">
<!-- 中略 -->
</svg>
これで、PDF 出力していた時の寸法が SVG 内の要素の配置にそのまま使えます。
ただし PDF では原点がページの左下なので、その変換は必要になります。
Y 座標の変換
PDF では Y 座標がページの下からになっているので、これを変換する必要があります。
画像やフレームなら、ページの高さから PDF での Y 座標とその要素の高さを引けば良いです。
改行のないテキストなら SVG の text
要素 を使えば要素の高さを気にする必要はありません。
画像の配置
画像の配置には SVG の image
要素 を使いました。
HTML の img
要素とは違うので注意が必要です。
画像ファイルの指定は src
ではなく href
属性になります。
image
要素にも preserveAspectRatio
属性 があり、正しく設定しないと意図したとおりに表示されません。
改行のないテキスト
改行のないテキストを配置するには SVG の text
要素 を使いました。
SVG の座標系の単位を 'pt' に合わせたので、font-size
は PDF で指定した値をそのまま使えます。
font-family
や font-size
はスタイルではなく text
要素の属性であることに注意してください。
改行のあるテキスト
改行のあるテキストを配置するには foreignObject
要素 を使いました。
foreignObject
は HTML を埋め込むことができるので、div
要素を使って改行のあるテキストを再現できます。
ただし、フォントサイズに注意が必要です (後述)。
<svg>
<foreignObject x="10" y="10" width="100" height="80">
<div style="width: 100%; height: 100%; font-size: 12px;">
(改行されるくらい長いテキスト)
</div>
</foreignObject>
</svg>
要素のサイズ
foreignObject
要素を PDF でのフレームと同じサイズにしておいて、その中に1個の div
を隙間なく収めるのが分かりやすいです。
必要なら div
のスタイルに padding
や line-height
を指定します。
ただし、div
にスタイルを指定する際にはフォントサイズ (下記) と同様に単位に注意が必要です。
フォントサイズ
フォントサイズも div
のスタイルになりますが、単位に注意が必要です。
上の例のように "12px" とした場合、SVG の座標系での12になります。
つまり、SVG の座標系の単位を 'pt' に対応させたなら、"12px" は実際には "12pt" で描画されます。
紛らわしいので単位をつけずに "12" としても良いのですが、環境によっては「単位がない」とエラーや警告が出るかも知れません。
line-height
は、PDF 側でフォントサイズを基準に決めていたなら、単位を 'em' にするのが良いです。
印刷できるようにする
もともと PDF で出力していたのは印刷の目的もあったので、SVG も印刷できるようにしたいです。
ブラウザの印刷機能で SVG 部分を印刷できるようにするには、スタイルを設定します。
<style>
@media print {
body {
margin: 0;
padding: 0;
border: none;
}
nav, footer {
display: none;
}
svg {
width: 841.9pt;
height: 585.3pt; /* 10pt ほど小さくする */
}
}
@page {
size: A4 landscape;
margin: 0;
}
</style>
印刷時のスタイルには print
メディアクエリ を使います。
-
body
など、印刷したい SVG を含む要素から余計なmargin
,padding
,border
をなくす。 -
nav
,footer
などの UI は非表示にする。 - 印刷したい SVG を印刷したい用紙サイズにする。
- この時、高さを少し小さくしておかないと2ページになってしまうようです。
@page
というルール で用紙を指定しておくとブラウザの印刷機能から参照されるようです。
SVG から HTML へ
厳密には SVG で書いていたものを HTML にした訳ではないのですが、SVG 内の要素の配置を HTML で再現したので、その時のメモです。
配置を再現するには position: absolute;
の div
要素を使いました。
余談ですが、なぜそんな事をしたのかというと、SVG 内の各要素の配置を視覚化したかったからです。
SVG を書き換えて中の要素に枠を描画する方法もありますが、将来的に枠をインタラクティブにするかも知れないので、HTML の div
で描画しておくことにしました。
座標単位
前述したように SVG の座標系の単位を 'pt' に対応させていたので、SVG 要素の x
, y
, width
, height
といった属性値に 'pt' をつけて div
のスタイル (left
, top
, width
, height
) にすれば良いです。
フォントサイズ等
もうお分かりかと思いますが、text
要素のフォントサイズは font-size
属性の値に 'pt' をつければ良くて、foreignObject
内の div
のフォントサイズは単位を 'px' -> 'pt' に変えれば良いです (前述・「PDF から SVG へ」の「改行のあるテキスト」参照)。
foreignObject
内の div
については font-size
以外についても同様です。
text 要素を div に
SVG の text
要素はサイズの概念がありません。
改行ができないので「幅」がなく、高さも自動的に1行分になるからです。
これを div
で表現するには以下のようにしました。
ただし p
は必要ないかも知れません (下記「補足1」参照)。
-
div
の Y 座標はtop
ではなくbottom
で指定して、値は svg 要素の高さから SVG での Y 座標を引いた値にする。 -
div
の幅と高さは内容 (下記p
) により決まる。 -
div
の中にp
を作って、left
とbottom
を "0" にする (左下の角がdiv
と一致する)。 -
p
のline-height
をフォントサイズと同じにして、改行しないように (white-space: nowrap;
に) する。
<div style="position: absolute; font-size: 12pt; left: 50pt; bottom: 350pt;">
<p style="position: absolute; margin: 0; padding: 0; left: 0; bottom: 0; line-height: 12pt; white-space: nowrap;">
(改行のないテキスト)
</p>
</div>
補足1
もしかすると p
を使わずに div
に直接テキストを書いても良いかもしれません。
その場合は div
の高さを正しく指定する必要があり、おそらく line-height
と同じにすれば良いように思います。
ただ実際にそうすると SVG で表示した時と位置がずれたことがあったので、div
の高さは指定せずに bottom
だけ指定して、内側の p
で line-height
を指定しました。
補足2
これでも厳密には SVG とまったく同じにはならず、僅かに位置がずれます。
座標の変換が行われている以上、しかたのない事と思っています。