OSDN Git Service

9f4fc3ae28b10ee12e6d1581985b96f9c5123744
[chnosproject/AI004.git] / mgcanvas / mgcanvas.js
1 //文字列をキーとする。
2
3 //まず、最も多数のエッジを持つノードを探す。
4 //それを起点にして、エッジをたどる。
5
6 function MGCanvas(canvasDOMObj){
7         var that = this;
8
9         this.initGraphicContext(canvasDOMObj);
10         this.tickPerSecond = 30;
11         this.tickCount = 0;
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.selectedEdge = null;
17         
18         var that = this;
19         window.addEventListener('keydown', function(event){
20                 switch(event.keyCode){
21                         case 37:        //左カーソル
22                                 that.moveViewRelative(-10, 0);
23                                 break;
24                         case 39:        //右カーソル
25                                 that.moveViewRelative(10, 0);
26                                 break;
27                         case 38:        //上カーソル
28                                 that.moveViewRelative(0, -10);
29                                 break;
30                         case 40:        //下カーソル
31                                 that.moveViewRelative(0, 10);
32                                 break;
33                 }
34         }, true);
35         
36         this.isMouseDown = false;
37         this.mouseDownPosition = new Point2D(0, 0);
38         this.lastMousePosition = new Point2D(0, 0);
39         this.canvas.onmousemove = function (e){
40                 if(that.isMouseDown){
41                         if(!e){
42                                 //for IE
43                                 e = window.event;
44                         }
45                         that.lastMousePosition = that.getMousePositionOnElement(e);
46                 }
47         };
48         this.canvas.onmousedown = function (e){
49                 if(!e){
50                         //for IE
51                         e = window.event;
52                 }
53                 that.lastMousePosition = that.getMousePositionOnElement(e);
54                 that.mouseDownPosition = that.lastMousePosition;
55                 var p = that.convertPointToGraphLayerFromCanvasLayerP(that.lastMousePosition);
56                 //console.log(p.x + "," + p.y);
57                 var node = that.getNodeAtPointP(p);
58                 that.selectNode(node);
59                 that.isMouseDown = true;
60         };
61         this.canvas.onmouseup = function (e){
62                 that.isMouseDown = false;
63         };
64 }
65 MGCanvas.prototype = {
66         setGraph: function(gArray){
67                 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
68                 var that = this;
69                 var p = gArray[0];
70                 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
71                 
72                 for(var i = 0, iLen = p.length; i < iLen; i++){
73                         this.nodeList.push(new MGNode(this, p[i]));
74                 }
75                 
76                 p = gArray[1];
77                 for(var i = 0, iLen = p.length; i < iLen; i++){
78                         this.edgeList.push(new MGEdge(this, p[i][2], n(p[i][0]), n(p[i][1])));
79                 }
80         },
81         bringToCenter: function(){
82                 // 重心を求めて、それを表示オフセットに設定する
83                 var g = new Point2D(0, 0);
84                 var p;
85                 p = this.nodeList;
86                 for(var i = 0, iLen = p.length; i < iLen; i++){
87                         g.x += p[i].position.x;
88                         g.y += p[i].position.y;
89                 }
90                 g.x /= p.length;
91                 g.y /= p.length;
92                 
93                 this.positionOffset.x = -g.x;
94                 this.positionOffset.y = -g.y;
95         },
96         zoomIn: function(){
97                 this.context.scale(2, 2);
98                 this.currentScale *= 2;
99         },
100         zoomOut: function(){
101                 this.context.scale(0.5, 0.5);
102                 this.currentScale *= 0.5;
103         },
104         moveViewRelative: function(x, y){
105                 this.positionOffset.x += -x;
106                 this.positionOffset.y += -y;
107         },
108         /*
109         bringInScreen: function(){
110                 //大きく外れていないときには動かさない
111                 var g = new Point2D(0, 0);
112                 var p;
113                 p = this.nodeList;
114                 for(var i = 0, iLen = p.length; i < iLen; i++){
115                         g.x += p[i].position.x;
116                         g.y += p[i].position.y;
117                 }
118                 g.x /= p.length;
119                 g.y /= p.length;
120                 if(     g.x < this.displayRect.origin.x / 2 || 
121                         g.x > -this.displayRect.origin.x / 2 || 
122                         g.y < this.displayRect.origin.y / 2 || 
123                         g.y > -this.displayRect.origin.x / 2){
124                         
125                         this.positionOffset.x = -g.x;
126                         this.positionOffset.y = -g.y;
127                 }
128         },
129         */
130         tick: function(){
131                 var p;
132                 var t;
133                 var dr;
134                 var n = null;
135                 var nMax = 0;
136                 var nTemp;
137                 
138                 this.tickCount++;
139                 
140                 //
141                 // View moving with mouse
142                 //
143                 if(this.isMouseDown){
144                         this.moveViewRelative(
145                                 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
146                                 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
147                         );
148                 }
149                 
150                 if(!this.isFrozen){
151                         //
152                         // Check
153                         //
154                         
155                         p = this.nodeList;
156                         for(var i = 0, iLen = p.length; i < iLen; i++){
157                                 nTemp = this.getVectorLength(p[i].vector);
158                                 if(nMax < nTemp){
159                                         n = p[i];
160                                         nMax = nTemp;
161                                 }
162                         }
163                         if(n){
164                                 n.ignoreEdgeRepulsion = 10;
165                         }
166                         
167                         
168                         //
169                         // Move
170                         //
171                         p = this.nodeList;
172                         for(var i = 0, iLen = p.length; i < iLen; i++){
173                                 this.nodeList[i].tick();
174                         }
175                         p = this.edgeList;
176                         for(var i = 0, iLen = p.length; i < iLen; i++){
177                                 this.edgeList[i].tick();
178                         }
179                 }
180                 
181                 //
182                 // Refresh
183                 //
184                 dr = this.displayRect;
185                 
186                 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
187                 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
188                 this.context.scale(this.currentScale, this.currentScale);
189                 
190                 this.context.translate(this.positionOffset.x, this.positionOffset.y);
191                 
192                 p = this.nodeList;
193                 for(var i = 0, iLen = p.length; i < iLen; i++){
194                         this.nodeList[i].draw();
195                 }
196                 
197                 p = this.edgeList;
198                 for(var i = 0, iLen = p.length; i < iLen; i++){
199                         this.edgeList[i].draw();
200                 }
201                 
202                 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
203                 
204         },
205         getMousePositionOnElement: function(e){
206                 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
207                 // retv:
208                 var retv = new Object();
209                 var rect = e.target.getBoundingClientRect();
210                 retv.x = e.clientX - rect.left;
211                 retv.y = e.clientY - rect.top;
212                 return retv;
213         },
214         fillRect: function(x, y, w, h){
215                 var d = this.drawCoordinatesInInteger;
216                 this.context.fillRect(d(x), d(y), d(w), d(h));
217         },
218         strokeRect: function(x, y, w, h){
219                 var d = this.drawCoordinatesInInteger;
220                 this.context.strokeRect(d(x), d(y), d(w), d(h));
221         },
222         drawCoordinatesInInteger: function(coordinateElement){
223                 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
224                 // With a bitwise or.
225                 return ((0.5 + coordinateElement) | 0);
226         },
227         drawCircle: function(x, y, r){
228                 var d = this.drawCoordinatesInInteger;
229                 this.context.beginPath();
230                 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
231                 this.context.closePath();
232                 this.context.fill();
233                 this.context.stroke();
234         },
235         drawLineP: function(p, q){
236                 var d = this.drawCoordinatesInInteger;
237                 this.context.beginPath();
238                 this.context.moveTo(d(p.x), d(p.y));
239                 this.context.lineTo(d(q.x), d(q.y));
240                 this.context.closePath();
241                 this.context.stroke();
242         },
243         drawText: function(text, x, y){
244                 //背景をfillStyle
245                 //前景をstrokeStyleで塗りつぶした文字列を描画する
246                 //塗りつぶし高さは20px固定
247                 //座標は文字列の左上の座標となる
248                 var textsize = this.context.measureText(text);
249                 this.context.fillRect(x, y, textsize.width, 20);
250                 this.context.save();
251                 this.context.fillStyle = this.context.strokeStyle;
252                 //fillText引数の座標は文字列の左下!
253                 this.context.fillText(text, x, y + 20 - 1);
254                 this.context.restore();
255         },
256         getVectorLengthP: function(p, q){
257                 return this.getVectorLength(this.getVectorP(p, q));
258         },
259         getVectorLength: function(a){
260                 return Math.sqrt(a.x * a.x + a.y * a.y);
261         },
262         getVectorP: function(p, q){
263                 return new Point2D(q.x - p.x, q.y - p.y);
264         },
265         getUnitVectorP: function(p, q){
266                 var e = this.getVectorP(p, q);
267                 return this.getUnitVector(e);
268         },
269         getUnitVector: function(a){
270                 var l = Math.sqrt(a.x * a.x + a.y * a.y);
271                 a.x /= l;
272                 a.y /= l;
273                 return a;
274         },
275         getNormalUnitVectorSideOfP: function(a, b, p){
276                 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
277                 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
278         },
279         getNormalVectorSideOfP: function(a, b, p){
280                 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
281                 //pがab上にある場合は零ベクトルとなる。
282                 var n = this.getVectorP(a, b);
283                 var t = n.x;
284                 var i;
285                 n.x = -n.y;
286                 n.y = t;
287                 
288                 i = this.getInnerVector2D(n, this.getVectorP(a, p));
289                 if(i < 0){
290                         //この法線ベクトルとapの向きが逆なので反転する。
291                         n.x = -n.x;
292                         n.y = -n.y;
293                 } else if(i == 0){
294                         n.x = 0;
295                         n.y = 0;
296                 }
297                 return n;
298         },
299         getExteriorVector2D: function(a, b){
300                 return a.x * b.y - a.y * b.x;
301         },
302         getInnerVector2D: function(a, b){
303                 return a.x * b.x + a.y * b.y;
304         },
305         getDistanceDotAndLineP: function(p, a, b){
306                 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
307                 var ab;
308                 var ap;
309                 var s;
310                 var l;
311                 var d;
312                 
313                 ab = this.getVectorP(a, b);
314                 ap = this.getVectorP(a, p);
315                 
316                 s = Math.abs(this.getExteriorVector2D(ab, ap));
317                 l = this.getVectorLengthP(a, b);
318                 d = (s / l);
319                 
320                 s = this.getInnerVector2D(ap, ab);
321                 if(s < 0){
322                         //線分の範囲外なので端点aからの距離に変換
323                         //端点から垂線の足までの距離
324                         l = - (s / l);
325                         d = Math.sqrt(d * d + l * l);
326                 } else if(s > l * l){
327                         //同様に端点bからの距離に変換
328                         l = s / l;
329                         d = Math.sqrt(d * d + l * l);
330                 }
331                 return d;
332         },
333         initGraphicContext: function(newCanvas){
334                 this.canvas = newCanvas;
335                 this.context = this.canvas.getContext('2d');
336                 this.context.fillStyle = "rgba(255,255,255,0.5)";
337                 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
338                 this.context.font = "normal 20px sans-serif";
339                 var w = this.canvas.width / 2;
340                 var h = this.canvas.height / 2;
341                 this.context.translate(w, h);
342                 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
343                 this.currentScale = 1;
344                 this.positionOffset = new Point2D(0, 0);
345         },
346         convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
347                 var p = new Point2D(pCanvas.x, pCanvas.y);
348                 // Canvasの中心が原点
349                 p.x -= this.canvas.width / 2;
350                 p.y -= this.canvas.height / 2;
351                 // スケール変換
352                 p.x /= this.currentScale;
353                 p.y /= this.currentScale;
354                 
355                 // オフセット平行移動
356                 p.x -= this.positionOffset.x;
357                 p.y -= this.positionOffset.y;
358                 
359                 return p;
360         },
361         getNodeAtPointP: function(p){
362                 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
363                 
364                 var nl = this.nodeList;
365                 for(var i = 0, iLen = nl.length; i < iLen; i++){
366                         if(r.isIncludePointP(nl[i].position)){
367                                 return nl[i];
368                         }
369                 }
370                 return null;
371         },
372         selectNode: function(node){
373                 if(this.selectedNode){
374                         this.selectedNode.isSelected = false;
375                 }
376                 if(node){
377                         node.isSelected = true;
378                 }
379                 this.selectedNode = node;
380         },
381         setIdentifierForSelectedNode: function(str){
382                 if(this.selectedNode){
383                         this.selectedNode.identifier = str;
384                 }
385         },
386 }
387
388 function MGNode(env, identifier){
389         this.env = env;
390         this.identifier = identifier;
391         this.position = new Point2D(0, 0);
392         this.size = 10;
393         //ランダムな初期ベクトルをもつ。
394         this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
395         this.friction = (100 - 8) / 100;
396         this.repulsionLengthNode = 90;
397         this.repulsionLengthEdge = 90;
398         this.ignoreEdgeRepulsion = 0;
399         //
400         this.strokeStyle = "rgba(0, 0, 0, 1)";
401         this.isSelected = false;
402 }
403 MGNode.prototype = {
404         draw: function(){
405                 if(this.isSelected){
406                         this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
407                 } else{
408                         this.env.context.strokeStyle = this.strokeStyle;
409                 }
410                 this.env.drawCircle(this.position.x, this.position.y, this.size);
411                 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
412         },
413         tick: function(){
414                 var e;
415                 var p;
416                 var l;
417                 var q;
418                 this.position.x += this.vector.x;
419                 this.position.y += this.vector.y;
420                 this.vector.x *= this.friction;
421                 this.vector.y *= this.friction;
422                 
423                 if(!this.ignoreEdgeRepulsion){
424                         //node
425                         //距離の近い点同士には斥力が働くとする。
426                         p = this.env.nodeList;
427                         for(var i = 0, iLen = p.length; i < iLen; i++){
428                                 var q = this.env.nodeList[i];
429                                 if(q != this){
430                                         l = this.env.getVectorLengthP(this.position, q.position);
431                                         if(l < this.repulsionLengthNode && l != 0){
432                                                 e = this.env.getUnitVectorP(q.position, this.position);
433                                                 e.x *= this.repulsionLengthNode / l;
434                                                 e.y *= this.repulsionLengthNode / l;
435                                                 this.vector.x += e.x;
436                                                 this.vector.y += e.y;
437                                         }
438                                 }
439                         }
440                         //edge
441                         //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
442                         p = this.env.edgeList;
443                         for(var i = 0, iLen = p.length; i < iLen; i++){
444                                 var q = this.env.edgeList[i];
445                                 if(q.node0 != this && q.node1 != this){
446                                         l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
447                                         if(l < this.repulsionLengthEdge && l != 0){
448                                                 if(l < 1){
449                                                         l = 1;
450                                                 }
451                                                 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
452                                                 e.x *= this.repulsionLengthEdge / l;
453                                                 e.y *= this.repulsionLengthEdge / l;
454                                                 this.vector.x += e.x;
455                                                 this.vector.y += e.y;
456                                                 q.node0.vector.x -= e.x / 2;
457                                                 q.node0.vector.y -= e.y / 2;
458                                                 q.node1.vector.x -= e.x / 2;
459                                                 q.node1.vector.y -= e.y / 2;
460                                         }
461                                 }
462                         }
463                 } else{
464                         this.ignoreEdgeRepulsion--;
465                 }
466         },
467 }
468
469 function MGEdge(env, identifier, node0, node1){
470         this.env = env;
471         this.identifier = identifier;
472         this.node0 = node0;
473         this.node1 = node1;
474         this.freeLength = 250;
475         //
476         this.strokeStyle = "rgba(0, 0, 0, 1)";
477         this.isSelected = false;
478 }
479 MGEdge.prototype = {
480         draw: function(){
481                 if(this.isSelected){
482                         this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
483                 } else{
484                         this.env.context.strokeStyle = this.strokeStyle;
485                 }
486                 if(this.node0 && this.node1){
487                         this.env.drawLineP(this.node0.position, this.node1.position);
488                 }
489         },
490         tick: function(){
491                 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
492                 var e;
493                 if(l > this.freeLength){
494                         e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
495                         e.x *= l / this.freeLength;
496                         e.y *= l / this.freeLength;
497                         this.node0.vector.x += e.x;
498                         this.node0.vector.y += e.y;
499                         this.node1.vector.x -= e.x;
500                         this.node1.vector.y -= e.y;
501                 }
502         },
503 }
504
505 function Point2D(x, y){
506         this.x = x;
507         this.y = y;
508 }
509 Point2D.prototype = {
510         
511 }
512
513 function Rectangle(x, y, width, height){
514         this.origin = new Point2D(x,y);
515         this.size = new Point2D(width,height);
516 }
517 Rectangle.prototype = {
518         isIncludePointP: function(p){
519                 return  (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
520                                 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);
521         },
522 }
523