前回d3.jsで、①→②を描画してみたのですが、回したくなったので回してみました。
こんな感じになります。
この記事で行うこと
- d3.jsでアニメーションさせる
- そのアニメーションをgif化する
基礎知識
d3.js超初心者向け ①→②を表現してみるを参考にしてください。
準備
htmlとjavascriptファイルを分けて、javascriptファイル側だけ編集すればいいようにしておきます。
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="d3.v3.min.js"></script>
</head>
<body>
<div id='example'></div>
<script src="arrownode_move.js"></script>
</body>
</html>
以後、arrownode_move.jsを編集していきます。
アニメーションさせるための考え方
d3.jsは差分アニメーションの機能が備わっていて、今描画している状態と、最終的に描画したい状態を指示すると、その間を補完アニメーションしてくれます。
今回のアニメーションは、冒頭にあったように、①を中心にして②を周回させる、その時矢印線もいっしょに動かす、ということをします。
その際に、いちいち円軌道を計算して動かすのではなくて、svgで周回軌道のパスを作成して、そのパスの上を②が動いていく、というようにします。
円のパスを作る
調べ方が悪いだけかもしれないのですが、d3.jsで円型のpathの作り方がわかりませんでした。
どうもd3.svg.line.radialを使って点を50個ぐらい置いて、その点を滑らかに繋ぐという実装を見かけるのですが、どうもしっくりきません。
なぜかというと、svgでは、SVG(スケーラブルベクターグラフィックス)学習辞典等参考資料にあるように、円弧のpathが作れるのでsvgの機能を使った方が早そうだからです。
調べたところ、円のパスを作るのは、この記事の内容でいけそうです。Circle drawing with SVG's arc path
実装すると、こうなります。
var t = path.node().getTotalLength();
var t2 = t*2;
// pathとしての円を描くのに、d3.svg.lineで点を並べてつなぐ方法は遅いので、svgのpathのarcコマンドを使用
// 参考:http://stackoverflow.com/questions/5737975/circle-drawing-with-svgs-arc-path
var str = "M0,0 M-" + t + ",0 a" + t + ","+ t + " 0 1,1 " + t2 + ",0 a" + t + "," + t + " 0 1,1 -" + t2 + ",0";
var path3 = svg.append("path")
.attr({
'd': str,
'fill': "none",
'stroke': "lightgreen",
'transform': "translate("+c1[0]+","+c1[1]+")",
});
}
パスを定義する時は、原点は(0,0)としておき、半径を変数として直接path:dの文字列を作成します。
その後、transformで原点の位置を①の位置に移動させます。
実際に動かすとこのようになります。
パスが奇麗に描けました! このパス上を②が動けばよさそうです。
②を動かす
パス上を動かすというのは、言い方を変えると、パス上の長さ(円なので始点と終点がいっしょですが、ここでは円周をパスの長さとします)を1とした時に、0と1を与えるとその間を補完アニメーションするということです。
0から1までというパラメータの指定は、d3.ease()で行うことが出来ます。
とはいっても、自動では補完してくれないので、途中の座標の計算式を与えることで対応します。
また、座標を変更する際に、合わせて矢印線の座標更新を行います。一回のアニメーション処理で、同時に変更することでずれなくアニメーションができます。
そのあたりをふまえて、プログラムにしてみます。
createsvg();
function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});
var r1 = 30;
var r2 = 20;
var ref1 = 8;
var c1 = [150, 140, r1,"id1"];
var c2 = [250, 170, r2, "id2"];
var carray = [c1, c2];
// defs/markerという構造で、svgの下に矢印を定義します。
var marker = svg.append("defs").append("marker")
.attr({
'id': "arrowhead",
'refX': ref1,
'refY': 2,
'markerWidth': 4,
'markerHeight': 4,
'orient': "auto"
});
// 矢印の形をpathで定義します。
marker.append("path")
.attr({
d: "M 0,0 V 4 L4,2 Z",
fill: "steelblue"
});
// 10種類の色を返す関数を使う
var color = d3.scale.category10();
var g = svg.selectAll('g')
.data(carray).enter().append('g')
.attr({
// 座標設定を動的に行う
id: function(d) { return d[3]; },
transform: function(d) {
return "translate(" + d[0] + "," + d[1] + ")";
},
});
g.append('circle')
.attr({
'r': function(d) { return d[2]; },
'fill': function(d,i) { return color(i); },
});
g.append('text')
.attr({
'text-anchor': "middle",
'dy': ".35em",
'fill': 'white',
})
.text(function(d,i) { return i+1; });
var line = d3.svg.line()
.interpolate('basis')
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});
var path = svg.append('path')
.attr({
'd': line(carray),
'id': 'nodepath',
'stroke': 'lightgreen',
'stroke-width': 5,
'fill': 'none',
// pathのアトリビュートとして、上で定義した矢印を指定します
'marker-end':"url(#arrowhead)",
});
var t = path.node().getTotalLength();
var tdiff = t - (r1+r2+ref1);
path.attr({
'stroke-dasharray': "0 " + r1 + " " + tdiff + " " + r2,
'stroke-dashoffset': 0,
});
var t2 = t*2;
// pathとしての円を描くのに、d3.svg.lineで点を並べてつなぐ方法は遅いので、svgのpath:aコマンドを使用
// 参考:http://stackoverflow.com/questions/5737975/circle-drawing-with-svgs-arc-path
// パスは表示しないので、strokeを定義しない。
var str = "M0,0 M-" + t + ",0 a" + t + ","+ t + " 0 1,1 " + t2 + ",0 a" + t + "," + t + " 0 1,1 -" + t2 + ",0";
var path3 = svg.append("path")
.attr({
'd': str,
'fill': "none",
// 下のコメントを外すと、pathが表示できる
// 'stroke': "lightgreen",
'transform': "translate("+c1[0]+","+c1[1]+")",
});
// 動かしたい円を指定する(あらかじめidを設定している)
function movecircle(){
svg.selectAll('#id2')
.transition()
// 5秒かけて一周させる
.duration(5000)
// easeを指定すると、transitionで変化させるパラメータを[0->1]にすることができる。
.ease("linear")
.attrTween(
// 座標設定を動的に行う
'transform', function(d,i) {
// easeで設定したパラメータがtとなって渡ってくる
return function(t) {
// path(ここでは円)の座標を取得する
var p = path3.node().getPointAtLength(path3.node().getTotalLength()*t);
c2[0] = c1[0]+p.x;
c2[1] = c1[1]+p.y;
// 矢印線の座標も変更する。こちらもidを設定している
svg.selectAll('#nodepath').attr('d', line(carray));
return "translate(" + c2[0] + "," + c2[1] + ")";
};
}
)
// 次の行のコメントするとループしなくなる
.each("end", function() {movecircle()});
};
movecircle();
};
アニメーションをリピートさせる時は、transitionの最後でeach("end")として、もう一度同じ関数を呼ぶようにするのがよいようです。
アニメーションをgif化する
d3.jsとは関係ないのですが、せっかくアニメーションしているのにそれを静止画像で提供するとインパクトが薄くなります。
簡単にアニメーションgif作れないかなと思ったら、LICEcapなるアプリケーションがあるということがわかったので、紹介します。
使い方は本当に簡単で、枠だけのアプリが立ち上がるので、アニメーションgifにしたい部分に持っていってRecodeを押すと、自動的にアニメーションgifにしてくれます。
まとめ
d3.jsでアニメーションさせようとする時は、何をどう変化させたいのかを十分意識しないと、なかなか思うように動いてくれません。
うまく動いたら祝杯をあげましょう!