このページの本文へ

SVGをもっと使いこなすために知っておきたい、HTML DOMとSVG座標の相互変換

2016年09月21日 04時09分更新

文●Craig Buckler

  • この記事をはてなブックマークに追加
本文印刷
Web制作でもよく使うようになったSVG。インタラクションなどを作るときにハマるのが座標に関する理解です。HTMLと組み合わせて使うときの座標の変換方法について解説します。

クールな人はもれなくSVGを使います。ただ、すばらしいSVGも、DOMやベクターインタラクションと一緒に使おうとすると複雑になります。

SVGは独自の座標系を持っており、viewbox属性を介して定義されます。たとえば、viewbox="0 0 800 600"とすると、幅800ユニット、高さ600ユニット、初期位置(0, 0)と設定されます。このSVGを800 x 600ピクセルの領域に置く場合、各SVGユニットは画面のピクセルに直接マッピングされます。

しかし、ベクター画像は美しさを保ったまま任意のサイズに拡大縮小できます。SVGは400 x 300の領域にも縮小でき、100 x 1200の領域に大きく形を変えて引き延ばすことだってできます。さらに要素を追加したときに領域を把握していないとSVGは難しくなります。

(SVG座標系が招く混乱については、Sara Soueidanの記事『viewport, viewBox and preserveAspectRatio』で触れています)

シンプルに分離したSVGの相乗効果

座標系全体の変換をせずに済む場合があります。

(画像やCSSの背景ではない)ページに埋め込まれたSVGはDOMの1部になり、ほかの要素と同様に操作されます。たとえば、以下のような1つの円を持つ基本的なSVGがあった場合、

<svg id="mysvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
  <circle id="mycircle" cx="400" cy="300" r="50" />
<svg>

以下のようなCSSエフェクトを適用できます。

circle {
  stroke-width: 5;
  stroke: #f00;
  fill: #ff0;
}

circle:hover {
  stroke: #090;
  fill: #fff;
}

さらにイベントハンドラーにアタッチして、属性を修正します。

var mycircle = document.getElementById('mycircle');

mycircle.addEventListener('click', function(e) {
  console.log('circle clicked - enlarging');
  mycircle.setAttributeNS(null, 'r', 60);
}, false);

次のデモでは、30個の円を1つのSVG画像に追加し、ホバーエフェクトをCSSに適用し、さらにJavaScriptを使って10個のユニットで半径を増加させます。

SVGからDOMへの座標変換

SVGの上にDOM要素を重ねたい場合、たとえば地図の上にメニューや情報ボックスを重ねたい場合はどうすればよいでしょうか? 再度、HTMLに組み込まれたSVG要素はDOMの一部を形成するため、優秀なgetBoundingClientRect()メソッドを使用して1回の呼び出しですべての寸法を返します。上のデモのコンソールを開くと、クリックされた円の新しい属性は半径の増加と連動することが分かります。

Element.getBoundingClientRect()はすべてのブラウザーでサポートされ、以下に示すピクセル寸法のプロパティとともにDOMrectオブジェクトを返します。

  • .xおよび.left – 表示領域の原点に対する、要素の左端のx座標
  • .right – 表示領域の原点に対する、要素の右端のx座標
  • .yおよび.top – 表示領域の原点に対する、要素の上端のy座標
  • .bottom – 表示領域の原点に対する、要素の下端のy座標
  • .width – 要素の幅(IE8およびそれ以前のバージョンではサポートされていないが、.rightから.leftを引いた値と同じ)
  • .height – 要素の高さ(IE8およびそれ以前のバージョンではサポートされていないが、.bottomから.topを引いた値と同じ)

すべての座標はブラウザーの表示領域に対するものなので、ページのスクロールに応じて変化します。ページの絶対位置は.leftwindow.scrollXを加え、.topwindow.scrollYを加えると計算できます。

DOMからSVGへの座標変換

ここは注意が必要な箇所です。SVGをクリックされたら、その地点にSVG要素を作成または設置したいとします。イベントハンドラーオブジェクトからDOMの.clientXおよび.clientYのピクセル座標が得られますが、これらをSVGユニットに変換しなければなりません。

ピクセル位置へ増倍係数を適用すると、SVGの点のx座標やy座標を計算できそうです。たとえば、1000ユニット幅のSVGが500px幅のコンテナー内にある場合、任意のカーソルのx座標を2倍にすればSVGの位置が取得できます。…が、これはほとんど機能しません!

  • SVGがコンテナーにぴったりフィットする保証がない
  • ページや要素の寸法が変化した場合、たとえばユーザーがブラウザーをリサイズするたびに、x要素やy要素を再計算しなければならない
  • SVGは2次元または3次元のいずれかの空間で変換できる
  • 上記の課題を乗り越えたとしても、決して期待通りには動作しない

幸い、SVGでは座標変換のために独自の行列式の因数分解の仕組みが提供されます。最初に、createSVGPoint()メソッドを使ってSVG上の点を生成し、 画面上のx座標およびy座標を渡します。

var svg = document.getElementById('mysvg'),
    pt = svg.createSVGPoint();

pt.x = 100;
pt.y = 200;

そのあと、行列変換を適用できます。その行列はSVGの逆行列から生成されます(まだきちんとドキュメント化されていません!)。.getScreenCTM()メソッドは以下のように画面座標にSVGユニットをマッピングします。

var svgP = pt.matrixTransform(svg.getScreenCTM().inverse());

svgPはすでにSVGの座標位置を規定する.xおよび.yプロパティを保持しています。

そして以下のように、SVGキャンバス上でクリックされた点に円を描けます。

var svg = document.getElementById('mysvg'),
    NS = svg.getAttribute('xmlns');

svg.addEventListener('click', function(e) {
  var pt = svg.createSVGPoint(), svgP, circle;
  
  pt.x = e.clientX;
  pt.y = e.clientY;
  svgP = pt.matrixTransform(svg.getScreenCTM().inverse());

  circle = document.createElementNS(NS, 'circle');
  circle.setAttributeNS(null, 'cx', svgP.x);
  circle.setAttributeNS(null, 'cy', svgP.y);
  circle.setAttributeNS(null, 'r', 10);
  svg.appendChild(circle);
}, false);

createElementNS()setAttributeNS()メソッドはXML名前空間URIを指定することを除けば、標準DOMのcreateElement()setAttribute()メソッドとまったく同じです。言い換えると、createElementNS()setAttributeNS()はHTMLではなくSVG上で動作します。setAttributeNS()は直接SVG要素を操作するので、空の名前空間URLが渡されることがあります。

DOMから変換されたSVG要素座標

ここからさらに複雑になります。なんらかの方法で変換されたSVG要素をクリックしたらどうなるでしょうか? 拡大縮小、回転、傾斜の処理が施され、結果的にSVG座標へ影響を及ぼします。たとえばこの<g>レイヤーが標準ユニットよりも4倍大きければ、それに含まれるSVGの座標は4分の1になります。

<g id="local" transform="scale(4)">
  <rect x="50" y="50" width="100" height="100" />
</g>

結果的に、長方形が400ユニットの大きさで位置(200, 200)に表示されます。

幸い、.getScreenCTM()は任意のSVG要素上のみで使用され、結果の行列によってすべての変換が考慮されます。そして、次のようなシンプルなsvgPoint変換関数を生成できます。

var svg = document.getElementById('mysvg'),
    local = svg.getElementById('local');

console.log( svgPoint(svg, 10, 10) ); // returns x, y
console.log( svgPoint(local, 10, 10) ); // = x/4, y/4

// translate page to SVG co-ordinate
function svgPoint(element, x, y) {
  var pt = svg.createSVGPoint();

  pt.x = x;
  pt.y = y;

  return pt.matrixTransform(element.getScreenCTM().inverse());
}

以下のデモは、すべてのブラウザーで動作します。グレーのSVG領域がクリックされると、カーソル位置に円が1つ追加されます。緑のボックスをクリックするとさらに円が1つ追加されますが、レイヤーは4倍に拡大されているので円は4倍の大きさで表示されます。

ページ座標に直接すべてのSVGユニットをマッピングすればシンプルになりますが、あるとき、クライアントにその画像を「数ピクセルだけ大きくしたい」と頼まれるとシステムが破たんしてしまいます。すぐに座標系を変換して、心配事が残らないようにしてください!

(原文:How to Translate from DOM to SVG Coordinates and Back Again

[翻訳:市川千枝/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事