2 function MGCanvas(canvasDOMObj){
5 this.initGraphicContext(canvasDOMObj);
6 this.tickPerSecond = 30;
8 this.tickTimer = window.setInterval(function(){ that.tick(); }, 1000 / this.tickPerSecond);
10 this.isEnabledAutomaticTracking = true;
11 this.nodeList = new Array();
12 this.edgeList = new Array();
13 this.selectedNode = null;
14 this.selectedEdge = null;
17 window.addEventListener('keydown', function(event){
18 switch(event.keyCode){
20 that.moveViewRelative(-10, 0);
23 that.moveViewRelative(10, 0);
26 that.moveViewRelative(0, -10);
29 that.moveViewRelative(0, 10);
34 this.isMouseDown = false;
35 this.mouseDownPosition = new Point2D(0, 0);
36 this.lastMousePosition = new Point2D(0, 0);
37 this.canvas.onmousemove = function (e){
43 that.lastMousePosition = that.getMousePositionOnElement(e);
46 this.canvas.onmousedown = function (e){
51 that.lastMousePosition = that.getMousePositionOnElement(e);
52 that.mouseDownPosition = that.lastMousePosition;
53 var p = that.convertPointToGraphLayerFromCanvasLayerP(that.lastMousePosition);
54 //console.log(p.x + "," + p.y);
55 var node = that.getNodeAtPointP(p);
56 that.selectNode(node);
57 var edge = that.getEdgeAtPointP(p);
58 that.selectEdge(edge);
59 that.isMouseDown = true;
60 that.isEnabledAutomaticTracking = false;
62 this.canvas.onmouseup = function (e){
63 that.isMouseDown = false;
66 MGCanvas.prototype = {
67 setGraph: function(gArray){
68 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
71 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
73 for(var i = 0, iLen = p.length; i < iLen; i++){
74 this.nodeList.push(new MGNode(this, p[i]));
78 for(var i = 0, iLen = p.length; i < iLen; i++){
79 this.edgeList.push(new MGEdge(this, p[i][2], n(p[i][0]), n(p[i][1])));
82 addNode: function(identifier, uuid){
85 addEdge: function(identifier, uuid, nodeid){
88 bringToCenter: function(){
89 // 重心を求めて、それを表示オフセットに設定する
90 var g = new Point2D(0, 0);
93 for(var i = 0, iLen = p.length; i < iLen; i++){
94 g.x += p[i].position.x;
95 g.y += p[i].position.y;
100 this.positionOffset.x = -g.x;
101 this.positionOffset.y = -g.y;
103 this.isEnabledAutomaticTracking = true;
106 this.context.scale(2, 2);
107 this.currentScale *= 2;
110 this.context.scale(0.5, 0.5);
111 this.currentScale *= 0.5;
113 moveViewRelative: function(x, y){
114 this.positionOffset.x += -x;
115 this.positionOffset.y += -y;
117 bringInScreen: function(){
119 var g = new Point2D(0, 0);
120 var f = new Point2D(0, 0);
123 for(var i = 0, iLen = p.length; i < iLen; i++){
124 g.x += p[i].position.x;
125 g.y += p[i].position.y;
129 g.x += this.positionOffset.x;
130 g.y += this.positionOffset.y;
131 if( g.x < this.displayRect.origin.x / 2 ||
132 g.x > -this.displayRect.origin.x / 2 ||
133 g.y < this.displayRect.origin.y / 2 ||
134 g.y > -this.displayRect.origin.x / 2){
136 this.positionOffset.x += -g.x;
137 this.positionOffset.y += -g.y;
154 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
155 this.bringInScreen();
160 // View moving with mouse
162 if(this.isMouseDown){
163 this.moveViewRelative(
164 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
165 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
175 for(var i = 0, iLen = p.length; i < iLen; i++){
176 nTemp = this.getVectorLength(p[i].vector);
183 n.ignoreEdgeRepulsion = 10;
191 for(var i = 0, iLen = p.length; i < iLen; i++){
192 this.nodeList[i].tick();
195 for(var i = 0, iLen = p.length; i < iLen; i++){
196 this.edgeList[i].tick();
203 dr = this.displayRect;
205 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
206 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
207 this.context.scale(this.currentScale, this.currentScale);
209 this.context.translate(this.positionOffset.x, this.positionOffset.y);
212 for(var i = 0, iLen = p.length; i < iLen; i++){
213 this.nodeList[i].draw();
217 for(var i = 0, iLen = p.length; i < iLen; i++){
218 this.edgeList[i].draw();
221 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
224 getMousePositionOnElement: function(e){
225 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
227 var retv = new Object();
228 var rect = e.target.getBoundingClientRect();
229 retv.x = e.clientX - rect.left;
230 retv.y = e.clientY - rect.top;
233 fillRect: function(x, y, w, h){
234 var d = this.drawCoordinatesInInteger;
235 this.context.fillRect(d(x), d(y), d(w), d(h));
237 strokeRect: function(x, y, w, h){
238 var d = this.drawCoordinatesInInteger;
239 this.context.strokeRect(d(x), d(y), d(w), d(h));
241 drawCoordinatesInInteger: function(coordinateElement){
242 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
243 // With a bitwise or.
244 return ((0.5 + coordinateElement) | 0);
246 drawCircle: function(x, y, r){
247 var d = this.drawCoordinatesInInteger;
248 this.context.beginPath();
249 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
250 this.context.closePath();
252 this.context.stroke();
254 drawLineP: function(p, q){
255 var d = this.drawCoordinatesInInteger;
256 this.context.beginPath();
257 this.context.moveTo(d(p.x), d(p.y));
258 this.context.lineTo(d(q.x), d(q.y));
259 this.context.closePath();
260 this.context.stroke();
262 drawText: function(text, x, y){
264 //前景をstrokeStyleで塗りつぶした文字列を描画する
267 var textsize = this.context.measureText(text);
268 this.context.fillRect(x, y, textsize.width, 20);
270 this.context.fillStyle = this.context.strokeStyle;
271 //fillText引数の座標は文字列の左下!
272 this.context.fillText(text, x, y + 20 - 1);
273 this.context.restore();
275 getVectorLengthP: function(p, q){
276 return this.getVectorLength(this.getVectorP(p, q));
278 getVectorLength: function(a){
279 return Math.sqrt(a.x * a.x + a.y * a.y);
281 getVectorP: function(p, q){
282 return new Point2D(q.x - p.x, q.y - p.y);
284 getUnitVectorP: function(p, q){
285 var e = this.getVectorP(p, q);
286 return this.getUnitVector(e);
288 getUnitVector: function(a){
289 var l = Math.sqrt(a.x * a.x + a.y * a.y);
294 getNormalUnitVectorSideOfP: function(a, b, p){
295 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
296 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
298 getNormalVectorSideOfP: function(a, b, p){
299 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
300 //pがab上にある場合は零ベクトルとなる。
301 var n = this.getVectorP(a, b);
307 i = this.getInnerVector2D(n, this.getVectorP(a, p));
309 //この法線ベクトルとapの向きが逆なので反転する。
318 getExteriorVector2D: function(a, b){
319 return a.x * b.y - a.y * b.x;
321 getInnerVector2D: function(a, b){
322 return a.x * b.x + a.y * b.y;
324 getDistanceDotAndLineP: function(p, a, b){
325 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
332 ab = this.getVectorP(a, b);
333 ap = this.getVectorP(a, p);
335 s = Math.abs(this.getExteriorVector2D(ab, ap));
336 l = this.getVectorLengthP(a, b);
339 s = this.getInnerVector2D(ap, ab);
341 //線分の範囲外なので端点aからの距離に変換
344 d = Math.sqrt(d * d + l * l);
345 } else if(s > l * l){
348 d = Math.sqrt(d * d + l * l);
352 initGraphicContext: function(newCanvas){
353 this.canvas = newCanvas;
354 this.context = this.canvas.getContext('2d');
355 this.context.fillStyle = "rgba(255,255,255,0.5)";
356 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
357 this.context.font = "normal 20px sans-serif";
358 var w = this.canvas.width / 2;
359 var h = this.canvas.height / 2;
360 this.context.translate(w, h);
361 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
362 this.currentScale = 1;
363 this.positionOffset = new Point2D(0, 0);
365 convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
366 var p = new Point2D(pCanvas.x, pCanvas.y);
368 p.x -= this.canvas.width / 2;
369 p.y -= this.canvas.height / 2;
371 p.x /= this.currentScale;
372 p.y /= this.currentScale;
375 p.x -= this.positionOffset.x;
376 p.y -= this.positionOffset.y;
380 getNodeAtPointP: function(p){
381 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
383 var nl = this.nodeList;
384 for(var i = 0, iLen = nl.length; i < iLen; i++){
385 if(r.isIncludePointP(nl[i].position)){
391 getEdgeAtPointP: function(p){
392 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
394 var el = this.edgeList;
395 for(var i = 0, iLen = el.length; i < iLen; i++){
396 if(r.isIncludePointP(el[i].centerPoint)){
402 selectNode: function(node){
403 if(this.selectedNode){
404 this.selectedNode.isSelected = false;
407 node.isSelected = true;
409 this.selectedNode = node;
411 selectEdge: function(edge){
412 if(this.selectedEdge){
413 this.selectedEdge.isSelected = false;
416 edge.isSelected = true;
418 this.selectedEdge = edge;
420 setIdentifierForSelectedNode: function(str){
421 if(this.selectedNode){
422 this.selectedNode.identifier = str;
427 function MGNode(env, identifier){
429 this.identifier = identifier;
430 this.position = new Point2D(0, 0);
433 this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
434 this.friction = (100 - 8) / 100;
435 this.repulsionLengthNode = 90;
436 this.repulsionLengthEdge = 90;
437 this.ignoreEdgeRepulsion = 0;
439 this.strokeStyle = "rgba(0, 0, 0, 1)";
440 this.isSelected = false;
445 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
447 this.env.context.strokeStyle = this.strokeStyle;
449 this.env.drawCircle(this.position.x, this.position.y, this.size);
451 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
459 this.position.x += this.vector.x;
460 this.position.y += this.vector.y;
461 this.vector.x *= this.friction;
462 this.vector.y *= this.friction;
464 if(!this.ignoreEdgeRepulsion){
466 //距離の近い点同士には斥力が働くとする。
467 p = this.env.nodeList;
468 for(var i = 0, iLen = p.length; i < iLen; i++){
469 var q = this.env.nodeList[i];
471 l = this.env.getVectorLengthP(this.position, q.position);
472 if(l < this.repulsionLengthNode && l != 0){
473 e = this.env.getUnitVectorP(q.position, this.position);
474 e.x *= this.repulsionLengthNode / l;
475 e.y *= this.repulsionLengthNode / l;
476 this.vector.x += e.x;
477 this.vector.y += e.y;
482 //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
483 p = this.env.edgeList;
484 for(var i = 0, iLen = p.length; i < iLen; i++){
485 var q = this.env.edgeList[i];
486 if(q.node0 != this && q.node1 != this){
487 l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
488 if(l < this.repulsionLengthEdge && l != 0){
492 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
493 e.x *= this.repulsionLengthEdge / l;
494 e.y *= this.repulsionLengthEdge / l;
495 this.vector.x += e.x;
496 this.vector.y += e.y;
497 q.node0.vector.x -= e.x / 2;
498 q.node0.vector.y -= e.y / 2;
499 q.node1.vector.x -= e.x / 2;
500 q.node1.vector.y -= e.y / 2;
505 this.ignoreEdgeRepulsion--;
510 function MGEdge(env, identifier, node0, node1){
512 this.identifier = identifier;
515 this.freeLength = 250;
517 this.strokeStyle = "rgba(0, 0, 0, 1)";
518 this.isSelected = false;
520 this.centerPoint = new Point2D(0, 0);
525 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
527 this.env.context.strokeStyle = this.strokeStyle;
529 if(this.node0 && this.node1){
530 this.drawCurvedLineP(this.node0.position, this.node1.position);
531 this.env.strokeRect(this.centerPoint.x - 8, this.centerPoint.y - 8, 16, 16);
533 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
538 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
540 if(l > this.freeLength){
541 e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
542 e.x *= l / this.freeLength;
543 e.y *= l / this.freeLength;
544 this.node0.vector.x += e.x;
545 this.node0.vector.y += e.y;
546 this.node1.vector.x -= e.x;
547 this.node1.vector.y -= e.y;
549 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
551 drawCurvedLineP: function(p, q){
553 var d = function(x){ return that.env.drawCoordinatesInInteger(x); };
554 var v = this.env.getUnitVectorP(p, q);
555 var w = new Point2D(-(v.y * 50), v.x * 50);
556 this.env.context.beginPath();
557 this.env.context.moveTo(d(p.x), d(p.y));
558 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));
559 //this.env.context.closePath();
560 this.env.context.stroke();
564 function Point2D(x, y){
568 Point2D.prototype = {
572 function Rectangle(x, y, width, height){
573 this.origin = new Point2D(x,y);
574 this.size = new Point2D(width,height);
576 Rectangle.prototype = {
577 isIncludePointP: function(p){
578 return (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
579 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);