PerfectVector
著者: Irene Kim2 分で読める

SVGの境界ボックス:適切な座標系で測定する

変換したSVGでgetBBoxとgetBoundingClientRectを比較します。枠を正しく配置し、ストローク境界を検証し、明示した余白でviewBoxを調整します。

目次

SVG要素自身のユーザー座標で境界を知りたいときはgetBBox()を使います。ブラウザーのビューポートを基準に、CSSピクセルの矩形が必要なときはgetBoundingClientRect()を使います。どちらの結果も、オーバーレイやviewBoxにコピーする前に、配置先の座標系を確認してください。

回転したエンブレムでは、ローカルのボックスは小さく、画面の軸に沿ったボックスはずっと大きくなる場合があります。どちらも正しい測定値になり得ます。ただし、ストローク、マーカー、クリッピング、フィルターがある場合など、どちらもすべての描画ピクセルを囲む万能の矩形として扱うべきではありません。

配置先に合った測定方法を選ぶ

配置先最初の測定方法必要な注意
同じSVG座標系内の矩形getBBox()矩形に一致する変換の環境を与えます
固定配置のHTML選択枠getBoundingClientRect()ビューポート座標を維持し、レイアウト変更時に更新します
変換済みグループを囲むルートSVGのviewBoxローカルボックスと座標変換収める前にルートのユーザー単位へ変換します
ストロークや効果を含む切り抜き別途検証した境界計測手順基本的な形状ボックスにすべての描画内容が入ると想定しません

MDNのgetBBox()ドキュメントでは、ローカル形状の境界と、ストローク、マーカー、クリッピングを指定するオプションを説明しています。測定要素自身と祖先の変換は、そのローカル結果に含まれません。グループの子孫は、グループ内の配置も含めてグループの形状に寄与します。

MDNのgetBoundingClientRect()リファレンスでは、ビューポートを基準とする座標を説明しています。スクロールするとその座標関係が変わります。CSSピクセルの値が、自動的にSVGのユーザー単位や印刷の実寸になるわけではありません。

傾いたエンブレムとそれに沿う傾いたローカル矩形を、水平な画面矩形が囲む図
2つの座標系の概念図です。ローカル矩形は画像の軸に従い、ビューポートの矩形は画面の軸に沿ったままです。

独自の変換済みエンブレムを測定する

この素材には、矩形の穴がある青緑のリングと、別の金色の三角形があります。両方に、結合部を丸くした太いストロークがあります。グループには移動、回転、拡大縮小を適用しています。

<svg id="scene" width="600" height="300" viewBox="0 0 300 150"
     xmlns="http://www.w3.org/2000/svg" style="overflow:visible">
  <g id="art" transform="translate(100 35) rotate(25) scale(1.2)"
     stroke="#17364c" stroke-width="10" stroke-linejoin="round">
    <path fill="#147d78" fill-rule="evenodd"
          d="M0 0H80V60H0Z M20 20H60V40H20Z" />
    <path fill="#e5a340" d="M100 50L120 10L140 50Z" />
  </g>
</svg>
<script>
  const art = document.querySelector("#art");
  const local = art.getBBox();
  const screen = art.getBoundingClientRect();
</script>

ローカル座標は、水平方向に0から140、垂直方向に0から60です。穴は形状の内部に影響しますが、外側の範囲を小さくしません。

Chromeでこの形状を開き、小さなテストツールで測定しました。600 × 300 CSSピクセルの表示サイズでは、getBBox()はx=0、y=0、width=140、height=60を返しました。クライアント矩形は約365.376 × 272.508 CSSピクセルでした。画面上の位置は、ページ内のSVGの配置場所によって変わりました。

表示サイズを300 × 150にすると、ローカルボックスは140 × 60のままでした。クライアント矩形は約182.688 × 136.254となり、以前の半分の寸法でした。元の形状は変わらず、ページへの対応付けが変わったためです。

これらは、テストしたブラウザーでこの素材を測定した値です。ブラウザーエンジンの比較でも、すべてのエディターの選択枠を規定するものでもありません。

各枠をそれぞれの座標系に描く

ローカルの枠には、返された座標と測定グループと同じ変換を使います。次回の測定対象に含まれないよう、兄弟要素として配置します。

const scene = document.querySelector("#scene");
const box = art.getBBox();
const outline = document.createElementNS("http://www.w3.org/2000/svg", "rect");
for (const key of ["x", "y", "width", "height"]) {
  outline.setAttribute(key, box[key]);
}
outline.setAttribute("transform", art.getAttribute("transform"));
outline.setAttribute("fill", "none");
outline.setAttribute("stroke", "olive");
scene.append(outline);

この素材では、画像と枠が同じ親を持つため、変換を直接コピーする方法が適切です。別の場所に挿入する矩形には、2つの座標系間を対応させる行列が必要です。

HTMLの枠では、divをbodyの直下に置き、固定配置にします。包含する祖先に変換がない状態にします。

const box = art.getBoundingClientRect();
Object.assign(overlay.style, {
  position: "fixed",
  boxSizing: "border-box",
  pointerEvents: "none",
  border: "2px dashed coral",
  left: `${box.x}px`,
  top: `${box.y}px`,
  width: `${box.width}px`,
  height: `${box.height}px`,
});

ここでoverlayは既存の枠要素です。スクロール、リサイズ、画像のレイアウトや変換の変更時に再測定します。文書基準で配置するならスクロール量を考慮し、位置を指定したコンテナーを使うなら、そのコンテナーの座標系を考慮します。これらの配置規則を混ぜると、矩形の測定自体は正しくても、ずれて見える原因になります。

テストツールでは両方の枠を表示しました。ローカル矩形はリングと三角形とともに傾き、破線のHTML矩形は水平と垂直を保ちました。太いストロークは、形状の枠の一部より外側にはみ出しました。

ストロークのオプションは対応を想定せずテストする

既定のローカルボックスにはストローク幅が含まれません。MDNではgetBBox({ stroke: true })を説明していますが、オプション引数の互換性は、長く使われてきた引数なしのメソッドとは別に検討する必要があります。

Chromeでの今回の素材では、{ stroke: true }を渡しても、引数なしの場合と同じ140 × 60のボックスが返されました。10単位のストロークを取り除いても、クライアント矩形は変わりませんでした。見えている暗い縁は明らかに変化したので、このテストではどちらの結果も描画全体の範囲を表していませんでした。

例外を出さずにオプションオブジェクトを受け取るだけでは、オプションが反映されたとは証明できません。対象ブラウザーで既知のストローク付き形状を測り、返された寸法を確認します。SVGの境界ボックスの定義を参照して、オブジェクト、ストローク、装飾を含む境界を区別してください。

ストローク幅の半分だけ余白を追加する規則を、あらゆる形状に一般化しないでください。マイター結合、マーカー、拡大縮小しないストローク、影、フィルターは、はみ出し方が異なります。自分で管理する単純な丸い結合の素材なら、明示的な余白と目視確認で十分な場合もあります。自動書き出しの切り抜きツールでは、これらを意図的に処理する必要があります。

ルートのviewBoxを調整する前にローカルボックスを変換する

クライアント矩形のx、y、width、heightを、そのままSVGのviewBoxに貼り付けないでください。ページ上の位置が含まれ、単位系も異なります。

この素材では、ローカルボックスの4隅をルートSVGのユーザー座標へ変換します。MDNのgetScreenCTM()リファレンスは、SVG座標を文書のビューポートへ対応させる行列を説明しています。画像の行列とルート行列の逆行列を組み合わせると、共通する画面への対応付けを取り除けます。

const box = art.getBBox();
const rootMatrix = scene.getScreenCTM();
const artMatrix = art.getScreenCTM();
if (!rootMatrix || !artMatrix) throw new Error("SVG is not measurable");
 
const toRoot = rootMatrix.inverse().multiply(artMatrix);
const corners = [
  [box.x, box.y],
  [box.x + box.width, box.y],
  [box.x + box.width, box.y + box.height],
  [box.x, box.y + box.height],
].map(([x, y]) => new DOMPoint(x, y).matrixTransform(toRoot));
 
const xs = corners.map(p => p.x);
const ys = corners.map(p => p.y);
const x = Math.min(...xs);
const y = Math.min(...ys);
const width = Math.max(...xs) - x;
const height = Math.max(...ys) - y;
const padding = 12; // Chosen for this simple fixture, in root user units.
scene.setAttribute("viewBox", [
  x - padding, y - padding,
  width + 2 * padding, height + 2 * padding,
].join(" "));

これは、描画済みのSVGで、2次元の変換が逆変換可能であることを前提にしています。ローカル矩形の隅を変換すると、軸に沿った安全側の包絡矩形が得られますが、変換後の各曲線に最もぴったり合うボックスとは限りません。

テストツールでは、その余白を含むviewBoxは約57.571 23 206.688 160.254でした。結果を確認すると、この素材の丸いストロークは表示領域内に収まっていました。調整したviewBoxの縦横比が異なるため、横長の表示には左右の余白が残りました。収めることは、引き伸ばすことを意味しません。

切り抜きではなく回転中心が必要なら、transform-originガイドで参照ボックスが回転へ与える影響を確認できます。読み込み時の実寸には、SVGサイズの問題を調べる手順を使ってください。

編集したい形状を測定する

元データがPNGやJPGしかなく、編集可能な形状が必要なら、測定前にPerfectVectorでSVGを復元できます。パス、開口部、ストローク、残っているラスター画像を確認します。境界ボックスは形状がどこまで広がるかを示しますが、トレースが正確であることや、見える細部がすべて編集可能であることを証明しません。

既存のSVGは直接測定できます。SVGの編集ガイドは、実際に含まれるものを確認するのに役立ちます。画像のベクター化の概要では、その前段階の変換を説明しています。

ラスターしかない画像では、PerfectVectorで元画像を試し、復元した輪郭を確認してから、配置や書き出しに使うグループを測定します。余白を確保し、納品用の切り抜きとして使う前に、最終的に見える端を確認してください。

よくある質問

2つの方法で幅が異なるのはなぜですか? 座標系が異なるためです。SVGのローカルボックスはユーザー単位で形状を表し、クライアント矩形はページへの対応付け後のビューポート基準の境界をCSSピクセルで表します。拡大縮小や回転によって、その寸法は変化します。

境界ボックスで画像に穴があるか分かりますか? いいえ。矩形が記録するのは外側の範囲で、内部の構造ではありません。外側の端が同じなら、塗りつぶした矩形とリングは同じ境界ボックスになり得ます。開口部は、パスと塗りの規則を調べて確認します。

クライアント矩形をviewBoxとして使えますか? 直接は使えません。まず対象の形状を、ルートSVGのユーザー座標へ変換します。画面位置とCSSピクセルの寸法を、元の座標系の矩形へコピーするべきではありません。

参考資料

  1. MDN getBBox — SVGのローカル境界と測定用のオプションを定義しています。
  2. MDN getBoundingClientRect — ビューポート基準の矩形とスクロールを説明しています。
  3. SVGの境界ボックスの定義 — オブジェクト、ストローク、装飾を含む形状の境界を区別しています。
  4. MDN getScreenCTM — SVG座標とビューポート座標を結ぶ行列を説明しています。

ブログのその他の記事

編集しやすい
よりきれいなSVGから始めましょう