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 loadGraphFromMemoryDB: function(mdb){
83 var a, l, t, u, m, e, f;
87 t = new MGNode(this, "Node");
88 this.nodeList.push(t);
90 for(var i = 0; i < l; i++){
91 u = new MGNode(this, a[i].identifier);
92 this.nodeList.push(u);
93 //e = new MGEdge(this, null, t, u);
94 //this.edgeList.push(e);
100 bringToCenter: function(){
101 // 重心を求めて、それを表示オフセットに設定する
102 var g = new Point2D(0, 0);
105 for(var i = 0, iLen = p.length; i < iLen; i++){
106 g.x += p[i].position.x;
107 g.y += p[i].position.y;
112 this.positionOffset.x = -g.x;
113 this.positionOffset.y = -g.y;
115 this.isEnabledAutomaticTracking = true;
118 this.context.scale(2, 2);
119 this.currentScale *= 2;
122 this.context.scale(0.5, 0.5);
123 this.currentScale *= 0.5;
125 moveViewRelative: function(x, y){
126 this.positionOffset.x += -x;
127 this.positionOffset.y += -y;
129 bringInScreen: function(){
131 var g = new Point2D(0, 0);
132 var f = new Point2D(0, 0);
135 for(var i = 0, iLen = p.length; i < iLen; i++){
136 g.x += p[i].position.x;
137 g.y += p[i].position.y;
141 g.x += this.positionOffset.x;
142 g.y += this.positionOffset.y;
143 if( g.x < this.displayRect.origin.x / 2 ||
144 g.x > -this.displayRect.origin.x / 2 ||
145 g.y < this.displayRect.origin.y / 2 ||
146 g.y > -this.displayRect.origin.x / 2){
148 this.positionOffset.x += -g.x;
149 this.positionOffset.y += -g.y;
166 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
167 this.bringInScreen();
172 // View moving with mouse
174 if(this.isMouseDown){
175 this.moveViewRelative(
176 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
177 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
187 for(var i = 0, iLen = p.length; i < iLen; i++){
188 nTemp = this.getVectorLength(p[i].vector);
195 n.ignoreEdgeRepulsion = 10;
203 for(var i = 0, iLen = p.length; i < iLen; i++){
204 this.nodeList[i].tick();
207 for(var i = 0, iLen = p.length; i < iLen; i++){
208 this.edgeList[i].tick();
215 dr = this.displayRect;
217 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
218 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
219 this.context.scale(this.currentScale, this.currentScale);
221 this.context.translate(this.positionOffset.x, this.positionOffset.y);
224 for(var i = 0, iLen = p.length; i < iLen; i++){
225 this.nodeList[i].draw();
229 for(var i = 0, iLen = p.length; i < iLen; i++){
230 this.edgeList[i].draw();
233 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
236 getMousePositionOnElement: function(e){
237 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
239 var retv = new Object();
240 var rect = e.target.getBoundingClientRect();
241 retv.x = e.clientX - rect.left;
242 retv.y = e.clientY - rect.top;
245 fillRect: function(x, y, w, h){
246 var d = this.drawCoordinatesInInteger;
247 this.context.fillRect(d(x), d(y), d(w), d(h));
249 strokeRect: function(x, y, w, h){
250 var d = this.drawCoordinatesInInteger;
251 this.context.strokeRect(d(x), d(y), d(w), d(h));
253 drawCoordinatesInInteger: function(coordinateElement){
254 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
255 // With a bitwise or.
256 return ((0.5 + coordinateElement) | 0);
258 drawCircle: function(x, y, r){
259 var d = this.drawCoordinatesInInteger;
260 this.context.beginPath();
261 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
262 this.context.closePath();
264 this.context.stroke();
266 drawLineP: function(p, q){
267 var d = this.drawCoordinatesInInteger;
268 this.context.beginPath();
269 this.context.moveTo(d(p.x), d(p.y));
270 this.context.lineTo(d(q.x), d(q.y));
271 this.context.closePath();
272 this.context.stroke();
274 drawText: function(text, x, y){
276 //前景をstrokeStyleで塗りつぶした文字列を描画する
279 var textsize = this.context.measureText(text);
280 this.context.fillRect(x, y, textsize.width, 20);
282 this.context.fillStyle = this.context.strokeStyle;
283 //fillText引数の座標は文字列の左下!
284 this.context.fillText(text, x, y + 20 - 1);
285 this.context.restore();
287 getVectorLengthP: function(p, q){
288 return this.getVectorLength(this.getVectorP(p, q));
290 getVectorLength: function(a){
291 return Math.sqrt(a.x * a.x + a.y * a.y);
293 getVectorP: function(p, q){
294 return new Point2D(q.x - p.x, q.y - p.y);
296 getUnitVectorP: function(p, q){
297 var e = this.getVectorP(p, q);
298 return this.getUnitVector(e);
300 getUnitVector: function(a){
301 var l = Math.sqrt(a.x * a.x + a.y * a.y);
306 getNormalUnitVectorSideOfP: function(a, b, p){
307 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
308 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
310 getNormalVectorSideOfP: function(a, b, p){
311 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
312 //pがab上にある場合は零ベクトルとなる。
313 var n = this.getVectorP(a, b);
319 i = this.getInnerVector2D(n, this.getVectorP(a, p));
321 //この法線ベクトルとapの向きが逆なので反転する。
330 getExteriorVector2D: function(a, b){
331 return a.x * b.y - a.y * b.x;
333 getInnerVector2D: function(a, b){
334 return a.x * b.x + a.y * b.y;
336 getDistanceDotAndLineP: function(p, a, b){
337 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
344 ab = this.getVectorP(a, b);
345 ap = this.getVectorP(a, p);
347 s = Math.abs(this.getExteriorVector2D(ab, ap));
348 l = this.getVectorLengthP(a, b);
351 s = this.getInnerVector2D(ap, ab);
353 //線分の範囲外なので端点aからの距離に変換
356 d = Math.sqrt(d * d + l * l);
357 } else if(s > l * l){
360 d = Math.sqrt(d * d + l * l);
364 initGraphicContext: function(newCanvas){
365 this.canvas = newCanvas;
366 this.context = this.canvas.getContext('2d');
367 this.context.fillStyle = "rgba(255,255,255,0.5)";
368 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
369 this.context.font = "normal 20px sans-serif";
370 var w = this.canvas.width / 2;
371 var h = this.canvas.height / 2;
372 this.context.translate(w, h);
373 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
374 this.currentScale = 1;
376 this.positionOffset = new Point2D(0, 0);
378 convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
379 var p = new Point2D(pCanvas.x, pCanvas.y);
381 p.x -= this.canvas.width / 2;
382 p.y -= this.canvas.height / 2;
384 p.x /= this.currentScale;
385 p.y /= this.currentScale;
388 p.x -= this.positionOffset.x;
389 p.y -= this.positionOffset.y;
393 getNodeAtPointP: function(p){
394 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
396 var nl = this.nodeList;
397 for(var i = 0, iLen = nl.length; i < iLen; i++){
398 if(r.isIncludePointP(nl[i].position)){
404 getEdgeAtPointP: function(p){
405 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
407 var el = this.edgeList;
408 for(var i = 0, iLen = el.length; i < iLen; i++){
409 if(r.isIncludePointP(el[i].centerPoint)){
415 selectNode: function(node){
416 if(this.selectedNode){
417 this.selectedNode.isSelected = false;
420 node.isSelected = true;
422 this.selectedNode = node;
424 selectEdge: function(edge){
425 if(this.selectedEdge){
426 this.selectedEdge.isSelected = false;
429 edge.isSelected = true;
431 this.selectedEdge = edge;
433 setIdentifierForSelectedNode: function(str){
434 if(this.selectedNode){
435 this.selectedNode.identifier = str;
440 function MGNode(env, identifier){
442 this.identifier = identifier;
443 this.position = new Point2D(Math.random() * 32 - 16, Math.random() * 32 - 16);
446 this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
447 this.friction = 50 / 100;
448 this.repulsionLengthNode = 90;
449 this.repulsionLengthEdge = 90;
450 this.ignoreEdgeRepulsion = 0;
452 this.strokeStyle = "rgba(0, 0, 0, 1)";
453 this.isSelected = false;
458 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
460 this.env.context.strokeStyle = this.strokeStyle;
462 this.env.drawCircle(this.position.x, this.position.y, this.size);
464 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
472 this.position.x += this.vector.x;
473 this.position.y += this.vector.y;
474 this.vector.x *= this.friction;
475 this.vector.y *= this.friction;
477 if(!this.ignoreEdgeRepulsion){
479 //距離の近い点同士には斥力が働くとする。
480 p = this.env.nodeList;
481 for(var i = 0, iLen = p.length; i < iLen; i++){
482 var q = this.env.nodeList[i];
484 l = this.env.getVectorLengthP(this.position, q.position);
485 if(l < this.repulsionLengthNode && l != 0){
486 e = this.env.getUnitVectorP(q.position, this.position);
487 e.x *= this.repulsionLengthNode / l;
488 e.y *= this.repulsionLengthNode / l;
489 this.vector.x += e.x;
490 this.vector.y += e.y;
496 //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
498 p = this.env.edgeList;
499 for(var i = 0, iLen = p.length; i < iLen; i++){
500 var q = this.env.edgeList[i];
501 if(q.node0 != this && q.node1 != this){
502 l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
503 if(l < this.repulsionLengthEdge && l != 0){
507 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
508 e.x *= this.repulsionLengthEdge / l;
509 e.y *= this.repulsionLengthEdge / l;
510 this.vector.x += e.x;
511 this.vector.y += e.y;
512 q.node0.vector.x -= e.x / 2;
513 q.node0.vector.y -= e.y / 2;
514 q.node1.vector.x -= e.x / 2;
515 q.node1.vector.y -= e.y / 2;
521 this.ignoreEdgeRepulsion--;
527 function MGEdge(env, identifier, node0, node1){
529 this.identifier = identifier;
532 this.freeLength = 250;
534 this.strokeStyle = "rgba(0, 0, 0, 0.5)";
535 this.isSelected = false;
537 this.centerPoint = new Point2D(0, 0);
542 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
544 this.env.context.strokeStyle = this.strokeStyle;
546 if(this.node0 && this.node1){
547 this.drawCurvedLineP(this.node0.position, this.node1.position);
548 this.env.strokeRect(this.centerPoint.x - 8, this.centerPoint.y - 8, 16, 16);
550 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
555 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
557 if(l > this.freeLength){
558 e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
559 e.x *= l / this.freeLength;
560 e.y *= l / this.freeLength;
561 this.node0.vector.x += e.x;
562 this.node0.vector.y += e.y;
563 this.node1.vector.x -= e.x;
564 this.node1.vector.y -= e.y;
566 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
568 drawCurvedLineP: function(p, q){
570 var d = function(x){ return that.env.drawCoordinatesInInteger(x); };
571 var v = this.env.getUnitVectorP(p, q);
572 var w = new Point2D(-(v.y * 50), v.x * 50);
573 this.env.context.beginPath();
574 this.env.context.moveTo(d(p.x), d(p.y));
575 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));
576 //this.env.context.closePath();
577 this.env.context.stroke();
581 function Point2D(x, y){
585 Point2D.prototype = {
589 function Rectangle(x, y, width, height){
590 this.origin = new Point2D(x,y);
591 this.size = new Point2D(width,height);
593 Rectangle.prototype = {
594 isIncludePointP: function(p){
595 return (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
596 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);