SVGのビューポートとビューボックスの関係について

はじめに

SVGを使い始めて最初に「あれっ?」と思うのは、ビューポート (viewport) とビューボックス (viewBox) ではないでしょうか。例えば「一部しか表示されないぞ」と思ったら、viewportのサイズを設定していなかったり、設定しても原点からしか表示できないことに戸惑ったり、そんな経験はないでしょうか。

ここで重要なのが、viewportとviewBoxの働きを理解することですが、その関係は意外と複雑です。そこでviewportとviewBoxの関係について調べたのでその結果を報告します。

本稿の最後にviewportとviewBoxの関係を可視化できる簡易ツールを提供しています。百聞は一見にしかず、本稿を読んで「?」と思われたら、実際に設定を変えながら結果を目で確認してみてください。

<svg>要素

<svg>要素はSVGの描画要素を書き込むためのキャンバスを提供します。このキャンバスに書き込まれた描画要素は、viewportとviewBoxという二つのウィンドウを介して画面に表示されます。

viewportとviewBox

一般的にviewportは、表示対象の一部を覗き込むための「窓」のようなもの、と説明されることが多いです。SVGの場合は、これにviewBoxが加わるため、単純に「窓」とは言い難いところがあります。

筆者は、viewBoxは表示対象を写すカメラ、viewportはカメラから送られてきた表示対象を映すディスプレイのようなものと例えています。カメラで写された映像はそのままディスプレイに映される訳ではありません。拡大・縮小が行われたり、縦横比の変形が行われたりします。またカメラの撮影範囲の一部が切り取られたり、逆に撮影範囲を超えて取り込まれる場合すらあります (これは実際のカメラとは違いますね)。SVGを使いこなすには、このようなviewBoxとviewportの間で行われるマッピングを理解する必要があります。(この説明もかなり苦し紛れですが …)

まず単純なケースでviewportとviewBoxの働き見てみます。

<svg width=<width> height=<height> 
    viewBox="<min-x> <min-y> <width> <height>"
    xmlns="http://www.w3.org/2000/svg">
    <!-- 描画要素 -->
    <circle cx="X" cy="Y" r="R" />
</svg>

この例では、キャンバスに円が描画されており、その周りをviewBoxが取り囲んでいます。このviewBoxをviewportにマッピングします。このviewportの内容が我々が画面上で見ることのできる描画内容です。

単純化のためにこの例ではviewportとviewBoxのwidth, heightは同じに定義しています。この場合はviewBoxの範囲がそのままviewportに表示されます。

実際にはviewportとviewBoxの大きさや縦横比 (aspect ratio) は同じである必要はありません。異なる大きさや縦横比を指定することで、様々な変形を伴うマッピングが可能です。これについては後で詳述します。

<svg>要素の定義はviewBoxを省略することもできます。この場合は、viewportと同じ大きさのviewBoxが原点に定義されていると見なせます。

<svg width=<width> height=<height> 
    xmlns="http://www.w3.org/2000/svg">
    <!-- 描画要素 -->
</svg>

同じくwidth, heightを省略することもできます。この場合viewportの大きさは、ブラウザのデフォルトサイズに従います。これは一般的にwidth by height = 300px by 150pxとなります。ただしviewBoxが設定されている場合は、その値と親要素 (外側の<svg>要素や<div>要素など) との関係でviewportの大きさが決まります。

座標系

viewportとviewBoxの間ではさまざまなマッピング (変換) が行われます。これはviewportとviewBoxはそれぞれ独立した座標系に存在していて、両者の間で座標変換が行われると考えることができます。

  • ビューポート座標系 (Viewport Coordinate System)
    • viewportはこの座標系に存在する
    • (0, 0) を原点とし、width (x軸), height (y軸) の大きさを持つ (つまりviewportそのもの)
    • ユーザー座標系に描画されたSVG要素がここにマッピングされる
  • ユーザー座標系 (User Coordinate System)
    • viewBoxはこの座標系に存在する
    • (0, 0) を原点とし、x軸、y軸の大きさは無限 (実際には実装ごとの制約がある)
    • SVG要素はここに書き込まれる (キャンバスとはこの座標空間のこと)

先ほどのマッピングの図に座標軸を加えると次のようになります。

クリッピング

viewport

width, heightに従って描画範囲をクリップします。width, heightの外にある描画要素は描画されません。

viewBox

描画範囲をクリッピングしません。例えば次の例ではviewBoxは円の半分のみをカバーしています。しかしviewport上は円の全体が表示されます。どの範囲がviewportに表示されるかは、次の章に詳述するマッピングの形態によります。

本稿の最後で紹介している可視化ツールを開発した際、筆者はviewBoxがクリッピングしないことに気が付かず、表示が想定通りにならないことに何日も悩んでしまいました。

viewportとviewBoxのマッピング

ここではユーザー座標系に描画されたSVG要素が、どのようにビューポート座標系にマッピングされるのかを説明します。

マッピングの形態

viewportとviewBoxの大きさと縦横比はそれぞれ異なる値を取れます。このときviewBoxからviewportへのマッピングはいくつかの形態があります。その形態を決めるのがpreserveAspectRatio (アスペクト比の保存) です。これはviewBoxをviewportにマッピングする際に、

  • どのように位置を合わせるか
  • どのようにスケールするか

を指定するものです。

preserveAspectRatio="<align> [meet | slice]"

alignは次の何れかの値をとります。

[none | xMinYMin | xMidYMin | xMaxYMin | xMinYMid | xMidYMid | xMaxYMid |
xMinYMax | xMidYMax | xMaxYMax]

位置合わせ (align)

xMinYMin – xMaxYMaxは、viewBoxとviewportのどの点を合わせるかを指定します。この位置はviewBoxをスケールする際の起点となります。

位置合わせの場所

noneは位置合わせに関してはxMinYMinと同じですが、次に説明するスケールの仕方が異なります。

スケール

none

x軸、y軸それぞれ独立して、viewportのwidth, heightに合わせてスケールされます。つまりアスペクト比は保存されません。

[meet | slice]

alignがnone以外のときに効力があります。省略した場合はmeetとなります。

  • meet
    viewBoxのアスペクト比を保ったまま、viewport内に全て収まるようにスケールを調整します。
    viewportとviewBoxのアスペクト比が異なる場合、viewBoxはviewportよりも小さな領域を占めます。
  • slice
    viewBoxのアスペクト比を保ったまま、viewport内が全て覆われるようにスケールを調整します。
    viewportとviewBoxのアスペクト比が異なる場合、viewBoxはviewportよりも大きな領域をしめます。この結果viewportを通して表示されるのは、viewBoxの一部となります。

マッピングの例

viewportとviewBoxのアスペクト比が同じ場合

viewBoxの内容がアスペクト比を保ったままviewportに表示されます。これはpreserveAspectRatioの設定値による影響を受けません。次の例ではx軸、y軸ともに1/2のスケールになります。

viewportとviewBoxのアスペクト比が異なる場合

マッピング結果はpreserveAspectRatioの設定の影響を受けます。次のような例について検討します。

none

alignにnoneが指定されると、viewBoxはviewportに一致するように、x軸、y軸それぞれにスケールされます。結果としてviewBox内の円はviewport内で楕円として表示されます。

meet

meetが指定されると、viewBoxはviewport内に全てが表示されるように、アスペクト比を保ったまま、x軸、y軸ともに等倍でスケールされます。結果としてviewBoxはviewportの一部のみを占めます。このときviewBoxの外側はクリッピングされないことに留意してください。viewBoxの外側に描画された要素があれば、それはviewportにも描画されます。

slice

sliceが指定されると、viewBoxはviewportの全体を覆うように、アスペクト比を保ったまま、x軸、y軸ともに等倍でスケールされます。結果としてviewBoxの一部のみがviewportに表示されます。

入れ子になった<svg>要素

<svg>要素は入れ子にすることができます。子<svg>要素は、親<svg>要素からみれば、<rect>要素などと同じく描画要素のひとつとなります。子<svg>要素の座標系やviewBoxについては隠蔽され、子<svg>要素のviewport上に表示された内容のみが、親<svg>要素からは見えることになります。

例として次のように二つの<svg>要素が入れ子になったものを見てみます。

<!doctype html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>SVG ViewBox Test</title>
</head>

<body>
    <svg id="svgA" width="200" height="100" viewBox="150 200 100 100" 
        preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg">
        <svg id="svgB" x="100" y="150" width="200" height="100" viewBox="300 300 100 100"
            preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg">
            <circle cx="350" cy="350" r="50" fill="pink" />
        </svg>
    </svg>
</body>

このとき、svgAのviewportとviewBox, svgBのviewportとviewBoxの関係は次のようになります。svgBのx, yは、svgAのユーザー座標系でsvgBを何処に表示すべきかを示すものです。svgBからは黒い四角の部分が最終的にsvgAのviewportの中に表示されます。

ブラウザに表示した結果は次のようになります。

入れ子になった<svg>要素の表示

<svg>要素以外にviewportやviewBoxを持つ要素

<svg>要素意外にもviewportやviewBoxを持つ要素があります。

要素viewportviewBoxpreserve
AspectRatio
<image>
<marker>
<pattern>
<symbol>

<image>要素以外では、<svg>要素と同じくviewBoxとviewportを使って切り出された描画内容が、それぞれの要素の描画出力として使われます。viewBoxからviewportへのマッピングは<svg>要素のそれと同じです。

<image>要素にはviewBoxがありません。viewBoxは読み込むイメージの表示領域そのものになります。JPEG, PNGの場合は、読み込むイメージ全体がviewBox内に存在すると見なします。SVGの場合は、読み込まれるSVGファイルに定義された<svg>要素のviewportが、<image>要素のviewBoxに相当します。

次の例では<svg>要素の子要素である<image>要素に、ファイルから<circle>要素を読み込んでいます。

<!doctype html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>SVG ViewBox Test</title>
</head>

<body>
    <svg id="svgA" width="300" height="300" xmlns="http://www.w3.org/2000/svg">
        <rect x="50" y="50" width="100" height="200" fill="green" />
        <image href="image-as-svg.svg" x="50" y="50" width="200" height="100" 
            preserveAspectRatio="none" />
    </svg>
</body>

読み込まれるイメージファイルを次に示します。

<svg id="svgB" width="100" height="100" xmlns="http://www.w3.org/2000/svg">
    <circle cx="50" cy="50" r="50" fill="pink" preserveAspectRatio="none" />
</svg>

このとき<image>要素と読み込まれる”image-as-svg.svg”ファイル内の<circle>要素の関係は次のようになります。

ブラウザに表示した結果は次のようになります。

可視化ツール

viewportとviewBoxの関係を可視化できる簡易ツールを作成しました。次のリンクをクリックするとブラウザにツールが開きます。

可視化ツール

右側の矩形に表示されているモザイク模様が元となる表示内容になります。その上の矩形はviewBoxを表します。四隅の丸いハンドルをドラッグすることで、viewBoxの位置、大きさ、縦横比を変更することができます。

左側の矩形は、viewportとviewBoxを指定してマッピングした結果を表示しています。丸いハンドルをドラッグすることで、viewportの大きさと縦横比を変更することができます。

上部にあるのはpreserveAspectRatioの設定です。align, meet, sliceの設定を変更することができます。

viewportとviewBox、preserveAspectRatioの設定を変更して、マッピング結果がどのように変化するか確かめてください。

終わりに

<svg>要素を中心にviewportとviewBoxの働きについて解説しました。viewportとviewBoxを組み合わせることで、縦横比、スケーリング、クリッピングを柔軟に変更できます。ただその関係は複雑であまり直感的ではないところもあります。本稿がその理解の助けになれば幸いです。

変更履歴

日付内容
2024/08/24viewportとviewBoxの説明を改訂
2024/08/19初版リリース