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;
15 this.srcMemoryDB = null;
16 this.srcMemoryDBSyncPerTick = 60;
17 this.srcMemoryDBSyncCount = this.srcMemoryDBSyncPerTick;
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;
63 that.isEnabledAutomaticTracking = false;
65 this.canvas.onmouseup = function (e){
66 that.isMouseDown = false;
69 MGCanvas.prototype = {
70 setGraph: function(gArray){
71 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
74 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
76 for(var i = 0, iLen = p.length; i < iLen; i++){
77 this.nodeList.push(new MGNode(this, p[i]));
81 for(var i = 0, iLen = p.length; i < iLen; i++){
82 this.edgeList.push(new MGEdge(this, p[i][2], n(p[i][0]), n(p[i][1])));
85 setSourceMemoryDB: function(mdb){
87 this.srcMemoryDB = mdb;
88 mdb.callback_refreshedNode = function(t){
90 n = new MGNode(that, t.identifier);
91 that.nodeList.push(n);
94 bringToCenter: function(){
95 // 重心を求めて、それを表示オフセットに設定する
96 var g = new Point2D(0, 0);
99 for(var i = 0, iLen = p.length; i < iLen; i++){
100 g.x += p[i].position.x;
101 g.y += p[i].position.y;
106 this.positionOffset.x = -g.x;
107 this.positionOffset.y = -g.y;
109 this.isEnabledAutomaticTracking = true;
112 this.context.scale(2, 2);
113 this.currentScale *= 2;
116 this.context.scale(0.5, 0.5);
117 this.currentScale *= 0.5;
119 moveViewRelative: function(x, y){
120 this.positionOffset.x += -x;
121 this.positionOffset.y += -y;
123 bringInScreen: function(){
125 var g = new Point2D(0, 0);
126 var f = new Point2D(0, 0);
129 for(var i = 0, iLen = p.length; i < iLen; i++){
130 g.x += p[i].position.x;
131 g.y += p[i].position.y;
135 g.x += this.positionOffset.x;
136 g.y += this.positionOffset.y;
137 if( g.x < this.displayRect.origin.x / 2 ||
138 g.x > -this.displayRect.origin.x / 2 ||
139 g.y < this.displayRect.origin.y / 2 ||
140 g.y > -this.displayRect.origin.x / 2){
142 this.positionOffset.x += -g.x;
143 this.positionOffset.y += -g.y;
155 this.srcMemoryDBSyncCount++;
160 if(this.srcMemoryDB && this.srcMemoryDBSyncCount > this.srcMemoryDBSyncPerTick){
161 this.srcMemoryDB.syncDB();
162 this.srcMemoryDBSyncCount = 0;
168 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
169 this.bringInScreen();
174 // View moving with mouse
176 if(this.isMouseDown){
177 this.moveViewRelative(
178 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
179 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
189 for(var i = 0, iLen = p.length; i < iLen; i++){
190 nTemp = this.getVectorLength(p[i].vector);
197 n.ignoreEdgeRepulsion = 10;
205 for(var i = 0, iLen = p.length; i < iLen; i++){
206 this.nodeList[i].tick();
209 for(var i = 0, iLen = p.length; i < iLen; i++){
210 this.edgeList[i].tick();
217 dr = this.displayRect;
219 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
220 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
221 this.context.scale(this.currentScale, this.currentScale);
223 this.context.translate(this.positionOffset.x, this.positionOffset.y);
226 for(var i = 0, iLen = p.length; i < iLen; i++){
227 this.nodeList[i].draw();
231 for(var i = 0, iLen = p.length; i < iLen; i++){
232 this.edgeList[i].draw();
235 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
238 getMousePositionOnElement: function(e){
239 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
241 var retv = new Object();
242 var rect = e.target.getBoundingClientRect();
243 retv.x = e.clientX - rect.left;
244 retv.y = e.clientY - rect.top;
247 fillRect: function(x, y, w, h){
248 var d = this.drawCoordinatesInInteger;
249 this.context.fillRect(d(x), d(y), d(w), d(h));
251 strokeRect: function(x, y, w, h){
252 var d = this.drawCoordinatesInInteger;
253 this.context.strokeRect(d(x), d(y), d(w), d(h));
255 drawCoordinatesInInteger: function(coordinateElement){
256 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
257 // With a bitwise or.
258 return ((0.5 + coordinateElement) | 0);
260 drawCircle: function(x, y, r){
261 var d = this.drawCoordinatesInInteger;
262 this.context.beginPath();
263 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
264 this.context.closePath();
266 this.context.stroke();
268 drawLineP: function(p, q){
269 var d = this.drawCoordinatesInInteger;
270 this.context.beginPath();
271 this.context.moveTo(d(p.x), d(p.y));
272 this.context.lineTo(d(q.x), d(q.y));
273 this.context.closePath();
274 this.context.stroke();
276 drawText: function(text, x, y){
278 //前景をstrokeStyleで塗りつぶした文字列を描画する
281 var textsize = this.context.measureText(text);
282 this.context.fillRect(x, y, textsize.width, 20);
284 this.context.fillStyle = this.context.strokeStyle;
285 //fillText引数の座標は文字列の左下!
286 this.context.fillText(text, x, y + 20 - 1);
287 this.context.restore();
289 getVectorLengthP: function(p, q){
290 return this.getVectorLength(this.getVectorP(p, q));
292 getVectorLength: function(a){
293 return Math.sqrt(a.x * a.x + a.y * a.y);
295 getVectorP: function(p, q){
296 return new Point2D(q.x - p.x, q.y - p.y);
298 getUnitVectorP: function(p, q){
299 var e = this.getVectorP(p, q);
300 return this.getUnitVector(e);
302 getUnitVector: function(a){
303 var l = Math.sqrt(a.x * a.x + a.y * a.y);
308 getNormalUnitVectorSideOfP: function(a, b, p){
309 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
310 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
312 getNormalVectorSideOfP: function(a, b, p){
313 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
314 //pがab上にある場合は零ベクトルとなる。
315 var n = this.getVectorP(a, b);
321 i = this.getInnerVector2D(n, this.getVectorP(a, p));
323 //この法線ベクトルとapの向きが逆なので反転する。
332 getExteriorVector2D: function(a, b){
333 return a.x * b.y - a.y * b.x;
335 getInnerVector2D: function(a, b){
336 return a.x * b.x + a.y * b.y;
338 getDistanceDotAndLineP: function(p, a, b){
339 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
346 ab = this.getVectorP(a, b);
347 ap = this.getVectorP(a, p);
349 s = Math.abs(this.getExteriorVector2D(ab, ap));
350 l = this.getVectorLengthP(a, b);
353 s = this.getInnerVector2D(ap, ab);
355 //線分の範囲外なので端点aからの距離に変換
358 d = Math.sqrt(d * d + l * l);
359 } else if(s > l * l){
362 d = Math.sqrt(d * d + l * l);
366 initGraphicContext: function(newCanvas){
367 this.canvas = newCanvas;
368 this.context = this.canvas.getContext('2d');
369 this.context.fillStyle = "rgba(255,255,255,0.5)";
370 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
371 this.context.font = "normal 20px sans-serif";
372 var w = this.canvas.width / 2;
373 var h = this.canvas.height / 2;
374 this.context.translate(w, h);
375 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
376 this.currentScale = 1;
378 this.positionOffset = new Point2D(0, 0);
380 convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
381 var p = new Point2D(pCanvas.x, pCanvas.y);
383 p.x -= this.canvas.width / 2;
384 p.y -= this.canvas.height / 2;
386 p.x /= this.currentScale;
387 p.y /= this.currentScale;
390 p.x -= this.positionOffset.x;
391 p.y -= this.positionOffset.y;
395 getNodeAtPointP: function(p){
396 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
398 var nl = this.nodeList;
399 for(var i = 0, iLen = nl.length; i < iLen; i++){
400 if(r.isIncludePointP(nl[i].position)){
406 getEdgeAtPointP: function(p){
407 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
409 var el = this.edgeList;
410 for(var i = 0, iLen = el.length; i < iLen; i++){
411 if(r.isIncludePointP(el[i].centerPoint)){
417 selectNode: function(node){
418 if(this.selectedNode){
419 this.selectedNode.isSelected = false;
422 node.isSelected = true;
424 this.selectedNode = node;
426 selectEdge: function(edge){
427 if(this.selectedEdge){
428 this.selectedEdge.isSelected = false;
431 edge.isSelected = true;
433 this.selectedEdge = edge;
435 setIdentifierForSelectedNode: function(str){
436 if(this.selectedNode){
437 this.selectedNode.identifier = str;
442 function MGNode(env, identifier){
444 this.identifier = identifier;
445 this.position = new Point2D(Math.random() * 32 - 16, Math.random() * 32 - 16);
448 this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
449 this.friction = 50 / 100;
450 this.repulsionLengthNode = 90;
451 this.repulsionLengthEdge = 90;
452 this.ignoreEdgeRepulsion = 0;
454 this.strokeStyle = "rgba(0, 0, 0, 1)";
455 this.isSelected = false;
460 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
462 this.env.context.strokeStyle = this.strokeStyle;
464 this.env.drawCircle(this.position.x, this.position.y, this.size);
466 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
474 this.position.x += this.vector.x;
475 this.position.y += this.vector.y;
476 this.vector.x *= this.friction;
477 this.vector.y *= this.friction;
479 if(!this.ignoreEdgeRepulsion){
481 //距離の近い点同士には斥力が働くとする。
482 p = this.env.nodeList;
483 for(var i = 0, iLen = p.length; i < iLen; i++){
484 var q = this.env.nodeList[i];
486 l = this.env.getVectorLengthP(this.position, q.position);
487 if(l < this.repulsionLengthNode && l != 0){
488 e = this.env.getUnitVectorP(q.position, this.position);
489 e.x *= this.repulsionLengthNode / l;
490 e.y *= this.repulsionLengthNode / l;
491 this.vector.x += e.x;
492 this.vector.y += e.y;
498 //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
500 p = this.env.edgeList;
501 for(var i = 0, iLen = p.length; i < iLen; i++){
502 var q = this.env.edgeList[i];
503 if(q.node0 != this && q.node1 != this){
504 l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
505 if(l < this.repulsionLengthEdge && l != 0){
509 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
510 e.x *= this.repulsionLengthEdge / l;
511 e.y *= this.repulsionLengthEdge / l;
512 this.vector.x += e.x;
513 this.vector.y += e.y;
514 q.node0.vector.x -= e.x / 2;
515 q.node0.vector.y -= e.y / 2;
516 q.node1.vector.x -= e.x / 2;
517 q.node1.vector.y -= e.y / 2;
523 this.ignoreEdgeRepulsion--;
529 function MGEdge(env, identifier, node0, node1){
531 this.identifier = identifier;
534 this.freeLength = 250;
536 this.strokeStyle = "rgba(0, 0, 0, 0.5)";
537 this.isSelected = false;
539 this.centerPoint = new Point2D(0, 0);
544 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
546 this.env.context.strokeStyle = this.strokeStyle;
548 if(this.node0 && this.node1){
549 this.drawCurvedLineP(this.node0.position, this.node1.position);
550 this.env.strokeRect(this.centerPoint.x - 8, this.centerPoint.y - 8, 16, 16);
552 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
557 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
559 if(l > this.freeLength){
560 e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
561 e.x *= l / this.freeLength;
562 e.y *= l / this.freeLength;
563 this.node0.vector.x += e.x;
564 this.node0.vector.y += e.y;
565 this.node1.vector.x -= e.x;
566 this.node1.vector.y -= e.y;
568 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
570 drawCurvedLineP: function(p, q){
572 var d = function(x){ return that.env.drawCoordinatesInInteger(x); };
573 var v = this.env.getUnitVectorP(p, q);
574 var w = new Point2D(-(v.y * 50), v.x * 50);
575 this.env.context.beginPath();
576 this.env.context.moveTo(d(p.x), d(p.y));
577 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));
578 //this.env.context.closePath();
579 this.env.context.stroke();
583 function Point2D(x, y){
587 Point2D.prototype = {
591 function Rectangle(x, y, width, height){
592 this.origin = new Point2D(x,y);
593 this.size = new Point2D(width,height);
595 Rectangle.prototype = {
596 isIncludePointP: function(p){
597 return (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
598 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);