4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SVG の座標系を理解する

Last updated at Posted at 2023-11-10

概要

HTML の中に SVG をインラインで記述して、SVG 内の要素を動的に配置したい時があります。
そんな時、SVG の座標系を正しく理解することが重要です。

この記事では、SVG の座標系について調べたことをまとめます。
また関連する話題として、以下の作業をした時のメモも載せておきます。

  • PDF で書いていたものを SVG にした
  • SVG で書いていたものを HTML にした

HTML の座標系と SVG の座標系

まず、HTML の座標系と SVG の座標系をはっきりと分けて考える必要があります。

SVG の座標系

SVG 内の各要素の配置は、SVG の座標系に対する配置になります。
下の図で、いちばん左の「SVG の世界」に対して配置するイメージです。

01_svg_viewBox_html.png

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 要素の属性のうち widthheight は 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 要素の属性である widthheight は上書きされます。

<html>
  <style>
    svg {
      width: auto;
      height: 100%;
      aspect-ratio: 50 / 80;
    }
  </style>
  <svg width="50px" height="80px" viewBox="10 10 50 80">
    <!-- 中略 -->
  </svg>
</html>

02_aspect-ratio_CSS.png

preserveAspectRatio 属性

実は CSS の aspect-ratio を使わずに widthheight を両方とも "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 の中心が拡張の基準となります。
03_aspect-ratio_DEFAULT.png

例2 viewBox の幅または高さを縮小する

<svg width="100%" height="100%" viewBox="10 10 50 80" preserveAspectRatio="xMinYMin slice">
  <!-- 中略 -->
</svg>

slice: svg 要素のアスペクト比に合わせて viewBox の幅または高さを縮小します。
xMinYMin: viewBox の左上が縮小の基準となります。
04_aspect-ratio_xMinYMin_slice.png

例3 viewBox を変えずにコンテンツを変形する

<svg width="100%" height="100%" viewBox="10 10 50 80" preserveAspectRatio="none">
  <!-- 中略 -->
</svg>

none: viewBox はそのままで、コンテンツを svg 要素のサイズに合わせて変形します。
05_aspect-ratio_NONE.png

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 要素 を使えば要素の高さを気にする必要はありません。

06_from_pdf_y_coordinate.png

画像の配置

画像の配置には SVG の image 要素 を使いました。

HTML の img 要素とは違うので注意が必要です。
画像ファイルの指定は src ではなく href 属性になります。

image 要素にも preserveAspectRatio 属性 があり、正しく設定しないと意図したとおりに表示されません。

改行のないテキスト

改行のないテキストを配置するには SVG の text 要素 を使いました。
SVG の座標系の単位を 'pt' に合わせたので、font-size は PDF で指定した値をそのまま使えます。
font-familyfont-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 のスタイルに paddingline-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」参照)。

  1. div の Y 座標は top ではなく bottom で指定して、値は svg 要素の高さから SVG での Y 座標を引いた値にする。
  2. div の幅と高さは内容 (下記 p) により決まる。
  3. div の中に p を作って、leftbottom を "0" にする (左下の角が div と一致する)。
  4. pline-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 だけ指定して、内側の pline-height を指定しました。

補足2
これでも厳密には SVG とまったく同じにはならず、僅かに位置がずれます。
座標の変換が行われている以上、しかたのない事と思っています。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?