3 //まず、最も多数のエッジを持つノードを探す。
6 function MGCanvas(canvasDOMObj){
9 this.initGraphicContext(canvasDOMObj);
10 this.tickPerSecond = 30;
12 this.tickTimer = window.setInterval(function(){ that.tick(); }, 1000 / this.tickPerSecond);
13 this.isFrozen = false;
14 this.nodeList = new Array();
15 this.edgeList = new Array();
16 this.selectedNode = null;
17 this.selectedEdge = null;
20 window.addEventListener('keydown', function(event){
21 switch(event.keyCode){
23 that.moveViewRelative(-10, 0);
26 that.moveViewRelative(10, 0);
29 that.moveViewRelative(0, -10);
32 that.moveViewRelative(0, 10);
37 this.isMouseDown = false;
38 this.mouseDownPosition = new Point2D(0, 0);
39 this.lastMousePosition = new Point2D(0, 0);
40 this.canvas.onmousemove = function (e){
46 that.lastMousePosition = that.getMousePositionOnElement(e);
49 this.canvas.onmousedown = function (e){
54 that.lastMousePosition = that.getMousePositionOnElement(e);
55 that.mouseDownPosition = that.lastMousePosition;
56 var p = that.convertPointToGraphLayerFromCanvasLayerP(that.lastMousePosition);
57 //console.log(p.x + "," + p.y);
58 var node = that.getNodeAtPointP(p);
59 that.selectNode(node);
60 var edge = that.getEdgeAtPointP(p);
61 that.selectEdge(edge);
62 that.isMouseDown = true;
64 this.canvas.onmouseup = function (e){
65 that.isMouseDown = false;
68 MGCanvas.prototype = {
69 setGraph: function(gArray){
70 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
73 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
75 for(var i = 0, iLen = p.length; i < iLen; i++){
76 this.nodeList.push(new MGNode(this, p[i]));
80 for(var i = 0, iLen = p.length; i < iLen; i++){
81 this.edgeList.push(new MGEdge(this, p[i][2], n(p[i][0]), n(p[i][1])));
84 bringToCenter: function(){
85 // 重心を求めて、それを表示オフセットに設定する
86 var g = new Point2D(0, 0);
89 for(var i = 0, iLen = p.length; i < iLen; i++){
90 g.x += p[i].position.x;
91 g.y += p[i].position.y;
96 this.positionOffset.x = -g.x;
97 this.positionOffset.y = -g.y;
100 this.context.scale(2, 2);
101 this.currentScale *= 2;
104 this.context.scale(0.5, 0.5);
105 this.currentScale *= 0.5;
107 moveViewRelative: function(x, y){
108 this.positionOffset.x += -x;
109 this.positionOffset.y += -y;
112 bringInScreen: function(){
114 var g = new Point2D(0, 0);
117 for(var i = 0, iLen = p.length; i < iLen; i++){
118 g.x += p[i].position.x;
119 g.y += p[i].position.y;
123 if( g.x < this.displayRect.origin.x / 2 ||
124 g.x > -this.displayRect.origin.x / 2 ||
125 g.y < this.displayRect.origin.y / 2 ||
126 g.y > -this.displayRect.origin.x / 2){
128 this.positionOffset.x = -g.x;
129 this.positionOffset.y = -g.y;
144 // View moving with mouse
146 if(this.isMouseDown){
147 this.moveViewRelative(
148 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
149 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
159 for(var i = 0, iLen = p.length; i < iLen; i++){
160 nTemp = this.getVectorLength(p[i].vector);
167 n.ignoreEdgeRepulsion = 10;
175 for(var i = 0, iLen = p.length; i < iLen; i++){
176 this.nodeList[i].tick();
179 for(var i = 0, iLen = p.length; i < iLen; i++){
180 this.edgeList[i].tick();
187 dr = this.displayRect;
189 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
190 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
191 this.context.scale(this.currentScale, this.currentScale);
193 this.context.translate(this.positionOffset.x, this.positionOffset.y);
196 for(var i = 0, iLen = p.length; i < iLen; i++){
197 this.nodeList[i].draw();
201 for(var i = 0, iLen = p.length; i < iLen; i++){
202 this.edgeList[i].draw();
205 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
208 getMousePositionOnElement: function(e){
209 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
211 var retv = new Object();
212 var rect = e.target.getBoundingClientRect();
213 retv.x = e.clientX - rect.left;
214 retv.y = e.clientY - rect.top;
217 fillRect: function(x, y, w, h){
218 var d = this.drawCoordinatesInInteger;
219 this.context.fillRect(d(x), d(y), d(w), d(h));
221 strokeRect: function(x, y, w, h){
222 var d = this.drawCoordinatesInInteger;
223 this.context.strokeRect(d(x), d(y), d(w), d(h));
225 drawCoordinatesInInteger: function(coordinateElement){
226 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
227 // With a bitwise or.
228 return ((0.5 + coordinateElement) | 0);
230 drawCircle: function(x, y, r){
231 var d = this.drawCoordinatesInInteger;
232 this.context.beginPath();
233 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
234 this.context.closePath();
236 this.context.stroke();
238 drawLineP: function(p, q){
239 var d = this.drawCoordinatesInInteger;
240 this.context.beginPath();
241 this.context.moveTo(d(p.x), d(p.y));
242 this.context.lineTo(d(q.x), d(q.y));
243 this.context.closePath();
244 this.context.stroke();
246 drawText: function(text, x, y){
248 //前景をstrokeStyleで塗りつぶした文字列を描画する
251 var textsize = this.context.measureText(text);
252 this.context.fillRect(x, y, textsize.width, 20);
254 this.context.fillStyle = this.context.strokeStyle;
255 //fillText引数の座標は文字列の左下!
256 this.context.fillText(text, x, y + 20 - 1);
257 this.context.restore();
259 getVectorLengthP: function(p, q){
260 return this.getVectorLength(this.getVectorP(p, q));
262 getVectorLength: function(a){
263 return Math.sqrt(a.x * a.x + a.y * a.y);
265 getVectorP: function(p, q){
266 return new Point2D(q.x - p.x, q.y - p.y);
268 getUnitVectorP: function(p, q){
269 var e = this.getVectorP(p, q);
270 return this.getUnitVector(e);
272 getUnitVector: function(a){
273 var l = Math.sqrt(a.x * a.x + a.y * a.y);
278 getNormalUnitVectorSideOfP: function(a, b, p){
279 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
280 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
282 getNormalVectorSideOfP: function(a, b, p){
283 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
284 //pがab上にある場合は零ベクトルとなる。
285 var n = this.getVectorP(a, b);
291 i = this.getInnerVector2D(n, this.getVectorP(a, p));
293 //この法線ベクトルとapの向きが逆なので反転する。
302 getExteriorVector2D: function(a, b){
303 return a.x * b.y - a.y * b.x;
305 getInnerVector2D: function(a, b){
306 return a.x * b.x + a.y * b.y;
308 getDistanceDotAndLineP: function(p, a, b){
309 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
316 ab = this.getVectorP(a, b);
317 ap = this.getVectorP(a, p);
319 s = Math.abs(this.getExteriorVector2D(ab, ap));
320 l = this.getVectorLengthP(a, b);
323 s = this.getInnerVector2D(ap, ab);
325 //線分の範囲外なので端点aからの距離に変換
328 d = Math.sqrt(d * d + l * l);
329 } else if(s > l * l){
332 d = Math.sqrt(d * d + l * l);
336 initGraphicContext: function(newCanvas){
337 this.canvas = newCanvas;
338 this.context = this.canvas.getContext('2d');
339 this.context.fillStyle = "rgba(255,255,255,0.5)";
340 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
341 this.context.font = "normal 20px sans-serif";
342 var w = this.canvas.width / 2;
343 var h = this.canvas.height / 2;
344 this.context.translate(w, h);
345 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
346 this.currentScale = 1;
347 this.positionOffset = new Point2D(0, 0);
349 convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
350 var p = new Point2D(pCanvas.x, pCanvas.y);
352 p.x -= this.canvas.width / 2;
353 p.y -= this.canvas.height / 2;
355 p.x /= this.currentScale;
356 p.y /= this.currentScale;
359 p.x -= this.positionOffset.x;
360 p.y -= this.positionOffset.y;
364 getNodeAtPointP: function(p){
365 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
367 var nl = this.nodeList;
368 for(var i = 0, iLen = nl.length; i < iLen; i++){
369 if(r.isIncludePointP(nl[i].position)){
375 getEdgeAtPointP: function(p){
376 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
378 var el = this.edgeList;
379 for(var i = 0, iLen = el.length; i < iLen; i++){
380 if(r.isIncludePointP(el[i].centerPoint)){
386 selectNode: function(node){
387 if(this.selectedNode){
388 this.selectedNode.isSelected = false;
391 node.isSelected = true;
393 this.selectedNode = node;
395 selectEdge: function(edge){
396 if(this.selectedEdge){
397 this.selectedEdge.isSelected = false;
400 edge.isSelected = true;
402 this.selectedEdge = edge;
404 setIdentifierForSelectedNode: function(str){
405 if(this.selectedNode){
406 this.selectedNode.identifier = str;
411 function MGNode(env, identifier){
413 this.identifier = identifier;
414 this.position = new Point2D(0, 0);
417 this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
418 this.friction = (100 - 8) / 100;
419 this.repulsionLengthNode = 90;
420 this.repulsionLengthEdge = 90;
421 this.ignoreEdgeRepulsion = 0;
423 this.strokeStyle = "rgba(0, 0, 0, 1)";
424 this.isSelected = false;
429 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
431 this.env.context.strokeStyle = this.strokeStyle;
433 this.env.drawCircle(this.position.x, this.position.y, this.size);
435 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
443 this.position.x += this.vector.x;
444 this.position.y += this.vector.y;
445 this.vector.x *= this.friction;
446 this.vector.y *= this.friction;
448 if(!this.ignoreEdgeRepulsion){
450 //距離の近い点同士には斥力が働くとする。
451 p = this.env.nodeList;
452 for(var i = 0, iLen = p.length; i < iLen; i++){
453 var q = this.env.nodeList[i];
455 l = this.env.getVectorLengthP(this.position, q.position);
456 if(l < this.repulsionLengthNode && l != 0){
457 e = this.env.getUnitVectorP(q.position, this.position);
458 e.x *= this.repulsionLengthNode / l;
459 e.y *= this.repulsionLengthNode / l;
460 this.vector.x += e.x;
461 this.vector.y += e.y;
466 //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
467 p = this.env.edgeList;
468 for(var i = 0, iLen = p.length; i < iLen; i++){
469 var q = this.env.edgeList[i];
470 if(q.node0 != this && q.node1 != this){
471 l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
472 if(l < this.repulsionLengthEdge && l != 0){
476 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
477 e.x *= this.repulsionLengthEdge / l;
478 e.y *= this.repulsionLengthEdge / l;
479 this.vector.x += e.x;
480 this.vector.y += e.y;
481 q.node0.vector.x -= e.x / 2;
482 q.node0.vector.y -= e.y / 2;
483 q.node1.vector.x -= e.x / 2;
484 q.node1.vector.y -= e.y / 2;
489 this.ignoreEdgeRepulsion--;
494 function MGEdge(env, identifier, node0, node1){
496 this.identifier = identifier;
499 this.freeLength = 250;
501 this.strokeStyle = "rgba(0, 0, 0, 1)";
502 this.isSelected = false;
504 this.centerPoint = new Point2D(0, 0);
509 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
511 this.env.context.strokeStyle = this.strokeStyle;
513 if(this.node0 && this.node1){
514 this.drawCurvedLineP(this.node0.position, this.node1.position);
515 this.env.strokeRect(this.centerPoint.x - 8, this.centerPoint.y - 8, 16, 16);
517 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
522 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
524 if(l > this.freeLength){
525 e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
526 e.x *= l / this.freeLength;
527 e.y *= l / this.freeLength;
528 this.node0.vector.x += e.x;
529 this.node0.vector.y += e.y;
530 this.node1.vector.x -= e.x;
531 this.node1.vector.y -= e.y;
533 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
535 drawCurvedLineP: function(p, q){
537 var d = function(x){ return that.env.drawCoordinatesInInteger(x); };
538 var v = this.env.getUnitVectorP(p, q);
539 var w = new Point2D(-(v.y * 50), v.x * 50);
540 this.env.context.beginPath();
541 this.env.context.moveTo(d(p.x), d(p.y));
542 this.env.context.bezierCurveTo(d(this.centerPoint.x + w.x), d(this.centerPoint.y + w.y), d(this.centerPoint.x - w.x), d(this.centerPoint.y - w.y), d(q.x), d(q.y));
543 //this.env.context.closePath();
544 this.env.context.stroke();
548 function Point2D(x, y){
552 Point2D.prototype = {
556 function Rectangle(x, y, width, height){
557 this.origin = new Point2D(x,y);
558 this.size = new Point2D(width,height);
560 Rectangle.prototype = {
561 isIncludePointP: function(p){
562 return (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
563 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);