ズーム可能なSVGを実装する方法の解説
ソフトウェアエンジニアの畠山です。今回は,SVGに対して"Pan"と"Zoom"を実装する方法を解説します。
PanとZoomのためのライブラリはsvg-pan-zoomなどがありますが,今回はこういったライブラリを使用しないで実装してみます。
※ Google Chrome 69でしか動作検証していません。
SVGのviewBox
まず最初に,svgタグのviewBox属性についておさらいしておきます。なぜならこのviewBoxを使ってPanとZoomを実現するからです.viewBox属性は”min-x”, “min-y”, “width”, “height” の4つの数値リストをスペース区切り(もしくはカンマ区切り)で指定します.
例えば,viewBox属性に[min-x min-y width height] = [0 0 100 100]
を指定すると以下のようになります。
<svg viewBox="0 0 100 100"></svg>
このSVGの真ん中に円を置いてみます.
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50"></circle>
</svg>
中心が\((x, y) = (50, 50)\),半径が\(50\)の円を書きました.ご覧の通り,円は画像のビューポートに内接しています.
viewBox
のmin-x
とmin-y
を変更して50 50 100 100
としてみます。
<svg viewBox="50 50 100 100">
<circle cx="50" cy="50" r="50"></circle>
</svg>
この操作は,ビューポートが\(x\)方向に\(50\),\(y\)方向に\(50\)平行移動したと考えることが出来ます。(\(y\)軸は下向きであることに注意。)
次に,viewBox
のwidth
とheight
を変更して 0 0 200 200
としてみます。
<svg viewBox="0 0 200 200">
<circle cx="50" cy="50" r="50"></circle>
</svg>
この操作は,元の円に対して\((x, y)=(100, 100)\)を中心にズームアウトを行なったと考えることができます.
このように,viewBox
属性のみを変更することで平行移動やズームの挙動を実現出来ることがわかります。
Panの実装
Panの実装はとても簡単です。viewBox
属性のmin-x
とmin-y
を変化させるだけで実装出来ます。
See the Pen SVG ViewBox Demo #1 by Sota Hatakeyama (@chooblarin) on CodePen.
上のデモでは,マウスドラッグの量に応じてmin-x
とmin-y
を変化させています。なお,マウスドラッグを処理するため,RxJSを使用しています。
Zoomの実装
Zoomの実装は,先に述べた通り,viewBox
の大きさを変化させることで実現可能です。簡単のため,まず最初にSVGの中心を基点にしたズームを説明し,そのあとにマウスカーソルの位置を基点にしたズームの実装を説明します。
SVGの中心でZoomする
中心でZoomするのはとても簡単です。例えば,SVGをscale
倍ズームしたい場合には次のようなコードになります。
// 現在のviewBoxを取得する
const [minX, minY, width, height] = svg.getAttribute('viewBox')
.split(' ')
.map(s => parseFloat(s));
// 大きさをscale倍する
const zoomedWidth = width * scale;
const zoomedHeight = height * scale;
// 中心の座標を計算する
const centerX = minX + width / 2.0;
const centerY = minY + height / 2.0;
// scale倍したあとのmin-xとmin-yを計算する
const zoomedMinX = centerX - zoomedWidth / 2.0;
const zoomedMinY = centerY - zoomedHeight / 2.0;
// viewBoxを更新
const zoomedViewBox = [zoomedMinX, zoomedMinY, zoomedWidth, zoomedHeight].join(' ');
svg.setAttribute('viewBox', zoomedViewBox);
以下は"+"ボタンでズームイン,"-"ボタンでズームアウトするデモです。いずれもSVGの中心を基点にズームを行なっています。Panも実装してあるのでそれも合わせて試してみてください。
See the Pen SVG ViewBox Demo #2 by Sota Hatakeyama (@chooblarin) on CodePen.
任意の点でZoomする
次は,Googleマップのように,ズームしたい箇所にマウスカーソルを移動してマウスホイールを動かすことでズームできるようにしてみます。
まず最初に,マウスイベントの位置を取得します。
const getEventPosition = (ev) => {
let x, y;
if (ev.offsetX) {
/*
Check the browser compatibility
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX
*/
x = ev.offsetX;
y = ev.offsetY;
} else {
const { left, top } = ev.srcElement.getBoundingClientRect();
x = ev.clientX - left;
y = ev.clientY - top;
}
return { x, y };
};
本記事執筆時点(2018年09月27日)現在ではMouseEvent#offsetX
はexperimentalでした。マウスの位置を取得する方法については,こちらなどを参考にして下さい。
次に,先ほど取得したマウスの位置から,SVGのビューポートにおける位置を計算します。理解を簡単にするために,位置の座標が0から1の間に収まるように正規化します。以下のようにSVGの画像サイズ(px)で割ることで正規化できます。
const point = getEventPosition(ev);
const sx = point.x / svg.clientWidth;
const sy = point.y / svg.clientHeight;
正規化した座標と,現在のviewBox
から,SVGのビューポートにおける位置を計算します。
const x = minX + width * sx;
const y = minY + height * sy;
最後に,ズーム後のviewBoxを計算します。
const zoomedWidth = width * scale;
const zoomedHeight = height * scale;
const zoomedMinX = x + scale * (minX - x);
const zoomedMinY = y + scale * (minY - y);
// viewBoxを更新
const zoomedViewBox = [zoomedMinX, zoomedMinY, zoomedWidth, zoomedHeight].join(' ');
svg.setAttribute('viewBox', zoomedViewBox);
完成したデモが以下になります。
See the Pen SVG ViewBox Demo #3 by Sota Hatakeyama (@chooblarin) on CodePen.
まとめ
今回は,SVGをZoomしたりPanしたりする方法を紹介しました。ライブラリを使わずに数十行のコードで実装できました。ブラウザの互換性に対応するためにライブラリを利用する場合にも,どのように実装するのかを知っておくと何かの役に立つかもしれません。
<br>
弊社では,一緒にWebの技術を楽しめるフロントエンドのエンジニアを募集しています。こちらまでお願いします。
参考
その他の記事
Other Articles
関連職種
Recruit