jnobuyukiのブログ

研究していて困ったことやその解決に関するメモ。同じように困ったあなたのために。twitter ID: @j_nobuyuki

SVGオブジェクトの重なり統制が不完全なことへの対処

このブログではSVGオブジェクトをよく取り上げてきました。いわゆるペイントのようなソフトなしで図形を描けるので、とても便利です。しかし、不便なこともあります。その一つが重なり順序の統制の問題です。

SVGオブジェクトの重なり順

単一のSVGオブジェクト上に描かれた図形や画像の重なり順は、描き出した順番と完全に一致します。つまり、後に書いたものほど手前に来ます。SVGオブジェクト自体は純粋な2Dの概念で出来ているようなので、奥行きのような属性がありません。なので、少なくとも現状では、一度書きだした複数の図形の重なり順を後から変更できません。これが非常に不便な場合があります。例えば、D3.jsでたくさんの図形を一度に書きだした後で、transition()による操作を加えた後から、SVGオブジェクト上の背景画像を入れ替えるときを考えてみます。この場合、背景画像を入れ替えたつもりが、背景画像が最前列に来てしまうため、最初に書きだしておいた図形が隠れてしまいます。もう一度画面上に書きだすのでは、デバイスの描画の負担をむやみに上げてしまうような気がします。

解消策:SVGではなくDIVの奥行きを利用する

この問題に関する解消策をいろいろ考えてみましたが、結局、SVGの操作のみではどうにもなりません。そこで、奥行きの概念を持っているDIV要素を利用することにしました。

例題:3つの円の重なり順を次々に入れ替えてみよう

では例題として、3つの円を書きだして、ボタンを押すごとに重なり順を変えるようなデモを作ってみます。

CSSにDIVのstyleを定義しておく

3つのDIV要素の重なり順をz-indexで区別させます。ポイントは2つです。1つは、z-indexが大きくなるほど前景に来ること。それに、3つのDIV要素の位置を完全に一致させ置くことです。

div.front {
    position: absolute;
    z-index: 3;
    top: 50px;
    left:10px;
    width:600px;
    height:480px;
}
div.middle{
    position: absolute;
    z-index: 2;
    top: 50px;
    left:10px;
    width:600px;
    height:480px;

}
div.back {
    position: absolute;
    z-index: 1;
    top: 50px;
    left:10px;
    width:600px;
    height:480px;
}
HTMLでDIV要素とボタンを定義を作成する

DIV要素のクラスはあえてここに書きこまないでおきます。

<div id ="div1">
</div>
<div id ="div2">    
</div>
<div id = "div3">
</div>
<button type = "button" id = "ordershift" onclick = "shiftorder(event);">
    Order Change
</button>
JavaScriptで円を描き、重なり順を変える関数を作成する。

円の作成と重なり順の変更にはD3.jsを利用します。重なり順を動的に変更できるように、重なり順のクラス名のリストを作成し、ボタンを押すごとに、3つのDIV要素のクラスが変更されます。
重なり順の変更にはtransition()を利用しています。

var orderlist = ["back","middle","front"];

var order1 = 0;
var order2 = 1;
var order3 = 2;
var diff = 0;

var svg1 = d3.select("#div1").attr("class",orderlist[(order1 + diff)%3]).append("svg").attr("width",600).attr("height",480);
var svg2 = d3.select("#div2").attr("class",orderlist[(order2 + diff)%3]).append("svg").attr("width",600).attr("height",480);
var svg3 = d3.select("#div3").attr("class",orderlist[(order3 + diff)%3]).append("svg").attr("width",600).attr("height",480);

var circle1 = svg1.append("circle").attr("cx",300).attr("cy",140).attr("r",100).attr("fill","blue").attr("stroke","orange").style("stroke-width","5px");
                                                                                       
var circle2 = svg2.append("circle").attr("cx",250).attr("cy",240).attr("r",100).attr("fill","lightgreen").attr("stroke","pink").style("stroke-width","5px");

var circle3 = svg3.append("circle").attr("cx",350).attr("cy",240).attr("r",100).attr("fill","red").attr("stroke","lightblue").style("stroke-width","5px");

function shiftorder(event) {
    diff += 1;
    d3.select("#div1").transition().delay(0).duration(2000).attr("class",orderlist[(order1 + diff)%3]);
    d3.select("#div2").transition().delay(0).duration(2000).attr("class",orderlist[(order2 + diff)%3]);
    d3.select("#div3").transition().delay(0).duration(2000).attr("class",orderlist[(order3 + diff)%3]);
}


ここまでをまとめるとこうなります。



もしかしたら将来的にはSVGに奥行きの概念が加わるかもしれませんが、今のところ、このやり方が最も簡単そうです。冒頭で述べたような背景画像を使用する場合は、常に後景のDIV要素上に画像を張り付ければいいでしょう。