SVG の座標系を理解する

# 概要

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

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

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

# HTML の座標系と SVG の座標系

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

## SVG の座標系

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

![01_svg_viewBox_html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/ad6e3197-80ce-46df-9a7f-821d7b96c690.png)

[viewBox](https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/viewBox) は、svg 要素の属性です。  
これは端的にいうと「SVG の世界のうち、どの部分を描画するか」を決める矩形です。  
SVG の座標系に単位はないので、`viewBox` も4つの数字のみで指定します。

```html
<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 で表示するなら、以下のようになります。

```html
<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 内の座標も同じ単位であると考えることができます。

```html
<svg width="80px" height="100px" viewBox="0 0 80 100">
  <!-- SVG 内の座標の単位が px であると仮定して要素を配置できる -->
</svg>
```

## アスペクト比を固定する

レスポンシブデザイン等で svg 要素のサイズを変化させ、かつアスペクト比を固定したい時があります。  
そんな時は CSS の [aspect-ratio](https://developer.mozilla.org/ja/docs/Web/CSS/aspect-ratio) を使うと上手くいきます。  
svg 要素の属性である `width` と `height` は上書きされます。

```html
<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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/021391b0-4113-9be3-7241-8bc86aa69d85.png)

### preserveAspectRatio 属性

実は CSS の `aspect-ratio` を使わずに `width` と `height` を両方とも "100%" 等としても、デフォルトでは SVG のコンテンツのアスペクト比は保たれます。  
ただし、コンテンツのアスペクト比を保ったまま **viewBox を svg 要素に合わせて変形する** ので、SVG 内の座標での描画される範囲が変わります。

この振る舞いは svg 要素の [`preserveAspectRatio` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio) の値によって変わります。  
いくつか例を上げますが、詳しくは [MDN のドキュメント](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio) 等をご覧ください。

#### 例1 viewBox の幅または高さを拡張する (デフォルト)

```html
<svg width="100%" height="100%" viewBox="10 10 50 80" preserveAspectRatio="xMidYMid meet">
  <!-- 中略 -->
</svg>
```

`meet`: svg 要素のアスペクト比に合わせて viewBox の幅または高さを拡張します。  
`xMidYMid`: viewBox の中心が拡張の基準となります。
![03_aspect-ratio_DEFAULT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/cb05e023-e5c4-176c-3fc5-43025e324d17.png)

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

```html
<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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/1e7097f3-1e72-5f02-5984-cfcffd9a6c5c.png)

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

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

`none`: viewBox はそのままで、コンテンツを svg 要素のサイズに合わせて変形します。
![05_aspect-ratio_NONE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/5ed11765-e930-f1b3-283e-c2b7eab54d6e.png)

# PDF から SVG へ

PDF で書いていたものを SVG にした時のメモです。  
PDF 書類を変換するのではなく、「PDF へ出力するロジック」から「SVG にして表示するロジック」に変えることが目的でした。  
PDF へ出力する処理は Python の [reportlab](https://pypi.org/project/reportlab/) を使っていました。

## 座標単位

PDF では座標単位が 'pt' だったので、SVG 内の座標系の単位が 'pt' に対応するようにしました。  
PDF で A4 サイズで出していたものを SVG で画面に表示する場合は以下のような感じです。

```html
<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` 要素](https://developer.mozilla.org/ja/docs/Web/SVG/Element/text) を使えば要素の高さを気にする必要はありません。

![06_from_pdf_y_coordinate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/c8734d79-ff6e-0212-7270-07c20f96c55f.png)

## 画像の配置

画像の配置には SVG の [`image` 要素](https://developer.mozilla.org/ja/docs/Web/SVG/Element/image) を使いました。

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

`image` 要素にも [`preserveAspectRatio` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio) があり、正しく設定しないと意図したとおりに表示されません。

## 改行のないテキスト

改行のないテキストを配置するには SVG の [`text` 要素](https://developer.mozilla.org/ja/docs/Web/SVG/Element/text) を使いました。  
SVG の座標系の単位を 'pt' に合わせたので、`font-size` は PDF で指定した値をそのまま使えます。  
`font-family` や `font-size` はスタイルではなく `text` 要素の属性であることに注意してください。

## 改行のあるテキスト

改行のあるテキストを配置するには [`foreignObject` 要素](https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject) を使いました。  
`foreignObject` は HTML を埋め込むことができるので、`div` 要素を使って改行のあるテキストを再現できます。  
ただし、フォントサイズに注意が必要です (後述)。

```html
<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 部分を印刷できるようにするには、スタイルを設定します。

```html
<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` [メディアクエリ](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_media_queries/Using_media_queries) を使います。

- `body` など、印刷したい SVG を含む要素から余計な `margin`, `padding`, `border` をなくす。
- `nav`, `footer` などの UI は非表示にする。
- 印刷したい SVG を印刷したい用紙サイズにする。
    - この時、高さを少し小さくしておかないと2ページになってしまうようです。

[`@page` というルール](https://developer.mozilla.org/ja/docs/Web/CSS/@page) で用紙を指定しておくとブラウザの印刷機能から参照されるようです。

# SVG から HTML へ

厳密には SVG で書いていたものを HTML にした訳ではないのですが、SVG 内の要素の配置を HTML で再現したので、その時のメモです。  
配置を再現するには [`position: absolute;`](https://developer.mozilla.org/ja/docs/Web/CSS/position#absolute) の `div` 要素を使いました。

:::note
余談ですが、なぜそんな事をしたのかというと、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 座標を引いた値にする。
1. `div` の幅と高さは内容 (下記 `p`) により決まる。
1. `div` の中に `p` を作って、`left` と `bottom` を "0" にする (左下の角が `div` と一致する)。
1. `p` の `line-height` をフォントサイズと同じにして、改行しないように (`white-space: nowrap;` に) する。

```html
<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>
```

:::note
補足1
もしかすると `p` を使わずに `div` に直接テキストを書いても良いかもしれません。
その場合は `div` の高さを正しく指定する必要があり、おそらく　`line-height` と同じにすれば良いように思います。
ただ実際にそうすると SVG で表示した時と位置がずれたことがあったので、`div` の高さは指定せずに `bottom` だけ指定して、内側の `p` で `line-height` を指定しました。
:::

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