プログラミング兼読書日記

プログラミングをしていて、はまってしまった事を中心に記事にしています。

d3.jsを用いた連続アニメーション

d3ではtransitionを用いて簡単にアニメーションを実現できます。しかし、d3を用いてデータに基づく連続したアニメーションを実装することは面倒です。ここでは、連続アニメーションを若干汚い方法で実装します。

 

アニメーションするオブジェクトが一つの場合:

  1. 各時間に対応するオブジェクトの属性を持った配列を作成する。
  2. オブジェクトを次の属性に変化させる再帰的な関数animateを作成する。

2.に関しては、d3のeachを用いて、animateの中でアニメーション完了後に呼ばれる関数をセットする。セットした関数の中でanimateを呼び出し、再帰的にアニメーションを実現させる。

 

以下にアニメーションのサンプルとソースを記載しておきます。

ソース:

<div id="sample"><button id="startbutton">start</button>
<div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<svg id="canpas" width="300" height="300"> </svg></div>
<script>// <![CDATA[
//初期化
  initialize();
  function initialize(){
    //ボタンの初期化。
    var startbutton = d3.select("#startbutton");
    startbutton.attr("disabled",null).on("click",startAnimation);
    //svgにcircle追加
    var svg = d3.select("#canpas");
    svg.append("circle")
       .attr({id:"circle0",
             cx:100,
             cy:100,
             r:10});
  }
  //startbuttonから呼ばれる
  function startAnimation(){
    //startbuttonを押せないようにする
    var startbutton = d3.select("#startbutton");
    startbutton.attr("disabled","disabled");
    //circleの位置の初期化
    var svg = d3.select("#canpas");
    circle=svg.select("#circle0")
      .attr({cx:100,
             cy:100,
             r:10});
    //順番に、半径、x座標、y座標のデータ
    var datas=[{r:5,x:10,y:100},
               {r:10,x:40,y:58},
               {r:20,x:100,y:200},
               {r:14,x:20,y:90},
               {r:10,x:100,y:100}];
    //どこまでアニメーションしたかを表すインデックス
    var index=0;
    //以下でanimateを再帰的に呼び出す
    animate();
    function animate(){
      if(index == datas.length){
        startbutton.attr("disabled",null);
        return;
      }
      var d=datas[index];
      index+=1;
      circle.transition()
        .duration(1000)
        .attr("r",d.r)
        .attr("cx",d.x)
        .attr("cy",d.y)
        .each("end",animate);
    }
  }
// ]]></script>
</div>

アニメーションするオブジェクトが複数の場合:

  1. 各時間に対応するオブジェクトすべての属性を要素としてもつ配列を作成する。
  2. すべてのオブジェクトを次の属性に変化させる再帰的な関数animateを作成する。

2.に関して、上記のオブジェクトが一つの場合と違う点はすべてのオブジェクトがアニメーションを終えた時にanimate関数を呼びださなければならない点です。d3では残念ながら、selectAllなどで複数に分割したすべてのオブジェクトのアニメーションが終了した時点で呼ばれる関数をセットするものはありません。そこで、d3のeach関数が何回呼ばれたかを記録しておき、オブジェクトの数だけ呼ばれたらanimate関数を呼び出すようにしました。

 

以下にアニメーションのサンプルとソースを記載しておきます。

ソース:

<div id="sample1">
    <button id="startbutton1">start</button>
    <div>
      <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
      <svg id="canpas1" width="300" height="300">
      </svg>
    </div>
    <script>
  //順番に、半径、x座標、y座標のデータ
  var datas=[[{r:5,cx:10,cy:100},
	       {r:10,cx:40,cy:58},
	       {r:20,cx:100,cy:200},
	       {r:14,cx:120,cy:90}
	       ],
	       [{r:14,cx:20,cy:90},
	       {r:5,cx:80,cy:100},
	       {r:10,cx:40,cy:58},
	       {r:20,cx:100,cy:200}
	       ],
	       [{r:20,cx:100,cy:200},
	       {r:14,cx:20,cy:90},
	       {r:5,cx:80,cy:100},
	       {r:10,cx:50,cy:58}
	       ],
	       [{r:10,cx:40,cy:58},
	       {r:20,cx:100,cy:200},
	       {r:14,cx:40,cy:90},
	       {r:5,cx:90,cy:80}
	       ],
	       [{r:5,cx:60,cy:100},
	       {r:10,cx:40,cy:78},
	       {r:20,cx:100,cy:200},
	       {r:14,cx:50,cy:90}
	       ]];    
  //初期化
  initialize1();
  function initialize1(){
    //ボタンの初期化。
    var startbutton = d3.select("#startbutton1");
    startbutton.attr("disabled",null);
    startbutton.on("click",startAnimation1);
  //circleの位置の初期化
    var svg = d3.select("#canpas1");
    var circles=svg.selectAll("circle").data(datas[0])
       .enter()
       .append("circle")
       .attr("r",(d)=>d.r)
       .attr("cx",(d)=>d.cx)
       .attr("cy",(d)=>d.cy)

  }
  //startbuttonから呼ばれる
  function startAnimation1(){
    //startbuttonを押せないようにする
    var startbutton = d3.select("#startbutton1");
    startbutton.attr("disabled","disabled");
    //circleの位置の初期化
    var svg = d3.select("#canpas1");
    var circles=svg.selectAll("circle")
       .data(datas[0])
       .attr("r",(d)=>d.r)
       .attr("cx",(d)=>d.cx)
       .attr("cy",(d)=>d.cy)
    //どこまでアニメーションしたかを表すインデックス
    var index=1;
    //何番目の点までセットしたendfunctionが呼ばれたか
    var endIndex=0;
    //以下でanimateを再帰的に呼び出す
    animate();
    function animate(){	     
      if(index == datas.length){
	startbutton.attr("disabled",null);
	return;
      }
      var ds=datas[index];
      index+=1;
      circles.data(ds)
	.transition()
	.duration(1000)
	.attr("r",(d)=>d.r)
	.attr("cx",(d)=>d.cx)
	.attr("cy",(d)=>d.cy)
	.each("end",function(){
	  endIndex++;
	  if(endIndex==ds.length){
	    endIndex=0;
	    animate();
	  }
	});
    }
  }
    </script>