OSDN Git Service

自動追尾を再実装。
[chnosproject/AI004.git] / mgcanvas / mgcanvas.js
1
2 function MGCanvas(canvasDOMObj){
3         var that = this;
4
5         this.initGraphicContext(canvasDOMObj);
6         this.tickPerSecond = 30;
7         this.tickCount = 0;
8         this.tickTimer = window.setInterval(function(){ that.tick(); }, 1000 / this.tickPerSecond);
9         this.isPaused = false;
10         this.isEnabledAutomaticTracking = true;
11         this.nodeList = new Array();
12         this.edgeList = new Array();
13         this.selectedNode = null;
14         this.selectedEdge = null;
15         
16         var that = this;
17         window.addEventListener('keydown', function(event){
18                 switch(event.keyCode){
19                         case 37:        //左カーソル
20                                 that.moveViewRelative(-10, 0);
21                                 break;
22                         case 39:        //右カーソル
23                                 that.moveViewRelative(10, 0);
24                                 break;
25                         case 38:        //上カーソル
26                                 that.moveViewRelative(0, -10);
27                                 break;
28                         case 40:        //下カーソル
29                                 that.moveViewRelative(0, 10);
30                                 break;
31                 }
32         }, true);
33         
34         this.isMouseDown = false;
35         this.mouseDownPosition = new Point2D(0, 0);
36         this.lastMousePosition = new Point2D(0, 0);
37         this.canvas.onmousemove = function (e){
38                 if(that.isMouseDown){
39                         if(!e){
40                                 //for IE
41                                 e = window.event;
42                         }
43                         that.lastMousePosition = that.getMousePositionOnElement(e);
44                 }
45         };
46         this.canvas.onmousedown = function (e){
47                 if(!e){
48                         //for IE
49                         e = window.event;
50                 }
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;
61         };
62         this.canvas.onmouseup = function (e){
63                 that.isMouseDown = false;
64         };
65 }
66 MGCanvas.prototype = {
67         setGraph: function(gArray){
68                 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
69                 var that = this;
70                 var p = gArray[0];
71                 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
72                 
73                 for(var i = 0, iLen = p.length; i < iLen; i++){
74                         this.nodeList.push(new MGNode(this, p[i]));
75                 }
76                 
77                 p = gArray[1];
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])));
80                 }
81         },
82         addNode: function(identifier, uuid){
83                 
84         },
85         addEdge: function(identifier, uuid, nodeid){
86                 
87         },
88         bringToCenter: function(){
89                 // 重心を求めて、それを表示オフセットに設定する
90                 var g = new Point2D(0, 0);
91                 var p;
92                 p = this.nodeList;
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;
96                 }
97                 g.x /= p.length;
98                 g.y /= p.length;
99                 
100                 this.positionOffset.x = -g.x;
101                 this.positionOffset.y = -g.y;
102                 
103                 this.isEnabledAutomaticTracking = true;
104         },
105         zoomIn: function(){
106                 this.context.scale(2, 2);
107                 this.currentScale *= 2;
108         },
109         zoomOut: function(){
110                 this.context.scale(0.5, 0.5);
111                 this.currentScale *= 0.5;
112         },
113         moveViewRelative: function(x, y){
114                 this.positionOffset.x += -x;
115                 this.positionOffset.y += -y;
116         },
117         bringInScreen: function(){
118                 //大きく外れていないときには動かさない
119                 var g = new Point2D(0, 0);
120                 var f = new Point2D(0, 0);
121                 var p;
122                 p = this.nodeList;
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;
126                 }
127                 g.x /= p.length;
128                 g.y /= p.length;
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){
135                         
136                         this.positionOffset.x += -g.x;
137                         this.positionOffset.y += -g.y;
138                 }
139         },
140         tick: function(){
141                 var p;
142                 var t;
143                 var dr;
144                 var n = null;
145                 var nMax = 0;
146                 var nTemp;
147                 
148                 this.tickCount++;
149                 
150                 //
151                 // AutomaticTracking
152                 //
153                 
154                 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
155                         this.bringInScreen();
156                 }
157
158                 
159                 //
160                 // View moving with mouse
161                 //
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
166                         );
167                 }
168                 
169                 if(!this.isPaused){
170                         //
171                         // Check
172                         //
173                         
174                         p = this.nodeList;
175                         for(var i = 0, iLen = p.length; i < iLen; i++){
176                                 nTemp = this.getVectorLength(p[i].vector);
177                                 if(nMax < nTemp){
178                                         n = p[i];
179                                         nMax = nTemp;
180                                 }
181                         }
182                         if(n){
183                                 n.ignoreEdgeRepulsion = 10;
184                         }
185                         
186                         
187                         //
188                         // Move
189                         //
190                         p = this.nodeList;
191                         for(var i = 0, iLen = p.length; i < iLen; i++){
192                                 this.nodeList[i].tick();
193                         }
194                         p = this.edgeList;
195                         for(var i = 0, iLen = p.length; i < iLen; i++){
196                                 this.edgeList[i].tick();
197                         }
198                 }
199                 
200                 //
201                 // Refresh
202                 //
203                 dr = this.displayRect;
204                 
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);
208                 
209                 this.context.translate(this.positionOffset.x, this.positionOffset.y);
210                 
211                 p = this.nodeList;
212                 for(var i = 0, iLen = p.length; i < iLen; i++){
213                         this.nodeList[i].draw();
214                 }
215                 
216                 p = this.edgeList;
217                 for(var i = 0, iLen = p.length; i < iLen; i++){
218                         this.edgeList[i].draw();
219                 }
220                 
221                 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
222                 
223         },
224         getMousePositionOnElement: function(e){
225                 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
226                 // retv:
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;
231                 return retv;
232         },
233         fillRect: function(x, y, w, h){
234                 var d = this.drawCoordinatesInInteger;
235                 this.context.fillRect(d(x), d(y), d(w), d(h));
236         },
237         strokeRect: function(x, y, w, h){
238                 var d = this.drawCoordinatesInInteger;
239                 this.context.strokeRect(d(x), d(y), d(w), d(h));
240         },
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);
245         },
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();
251                 this.context.fill();
252                 this.context.stroke();
253         },
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();
261         },
262         drawText: function(text, x, y){
263                 //背景をfillStyle
264                 //前景をstrokeStyleで塗りつぶした文字列を描画する
265                 //塗りつぶし高さは20px固定
266                 //座標は文字列の左上の座標となる
267                 var textsize = this.context.measureText(text);
268                 this.context.fillRect(x, y, textsize.width, 20);
269                 this.context.save();
270                 this.context.fillStyle = this.context.strokeStyle;
271                 //fillText引数の座標は文字列の左下!
272                 this.context.fillText(text, x, y + 20 - 1);
273                 this.context.restore();
274         },
275         getVectorLengthP: function(p, q){
276                 return this.getVectorLength(this.getVectorP(p, q));
277         },
278         getVectorLength: function(a){
279                 return Math.sqrt(a.x * a.x + a.y * a.y);
280         },
281         getVectorP: function(p, q){
282                 return new Point2D(q.x - p.x, q.y - p.y);
283         },
284         getUnitVectorP: function(p, q){
285                 var e = this.getVectorP(p, q);
286                 return this.getUnitVector(e);
287         },
288         getUnitVector: function(a){
289                 var l = Math.sqrt(a.x * a.x + a.y * a.y);
290                 a.x /= l;
291                 a.y /= l;
292                 return a;
293         },
294         getNormalUnitVectorSideOfP: function(a, b, p){
295                 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
296                 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
297         },
298         getNormalVectorSideOfP: function(a, b, p){
299                 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
300                 //pがab上にある場合は零ベクトルとなる。
301                 var n = this.getVectorP(a, b);
302                 var t = n.x;
303                 var i;
304                 n.x = -n.y;
305                 n.y = t;
306                 
307                 i = this.getInnerVector2D(n, this.getVectorP(a, p));
308                 if(i < 0){
309                         //この法線ベクトルとapの向きが逆なので反転する。
310                         n.x = -n.x;
311                         n.y = -n.y;
312                 } else if(i == 0){
313                         n.x = 0;
314                         n.y = 0;
315                 }
316                 return n;
317         },
318         getExteriorVector2D: function(a, b){
319                 return a.x * b.y - a.y * b.x;
320         },
321         getInnerVector2D: function(a, b){
322                 return a.x * b.x + a.y * b.y;
323         },
324         getDistanceDotAndLineP: function(p, a, b){
325                 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
326                 var ab;
327                 var ap;
328                 var s;
329                 var l;
330                 var d;
331                 
332                 ab = this.getVectorP(a, b);
333                 ap = this.getVectorP(a, p);
334                 
335                 s = Math.abs(this.getExteriorVector2D(ab, ap));
336                 l = this.getVectorLengthP(a, b);
337                 d = (s / l);
338                 
339                 s = this.getInnerVector2D(ap, ab);
340                 if(s < 0){
341                         //線分の範囲外なので端点aからの距離に変換
342                         //端点から垂線の足までの距離
343                         l = - (s / l);
344                         d = Math.sqrt(d * d + l * l);
345                 } else if(s > l * l){
346                         //同様に端点bからの距離に変換
347                         l = s / l;
348                         d = Math.sqrt(d * d + l * l);
349                 }
350                 return d;
351         },
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);
364         },
365         convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
366                 var p = new Point2D(pCanvas.x, pCanvas.y);
367                 // Canvasの中心が原点
368                 p.x -= this.canvas.width / 2;
369                 p.y -= this.canvas.height / 2;
370                 // スケール変換
371                 p.x /= this.currentScale;
372                 p.y /= this.currentScale;
373                 
374                 // オフセット平行移動
375                 p.x -= this.positionOffset.x;
376                 p.y -= this.positionOffset.y;
377                 
378                 return p;
379         },
380         getNodeAtPointP: function(p){
381                 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
382                 
383                 var nl = this.nodeList;
384                 for(var i = 0, iLen = nl.length; i < iLen; i++){
385                         if(r.isIncludePointP(nl[i].position)){
386                                 return nl[i];
387                         }
388                 }
389                 return null;
390         },
391         getEdgeAtPointP: function(p){
392                 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
393                 
394                 var el = this.edgeList;
395                 for(var i = 0, iLen = el.length; i < iLen; i++){
396                         if(r.isIncludePointP(el[i].centerPoint)){
397                                 return el[i];
398                         }
399                 }
400                 return null;
401         },
402         selectNode: function(node){
403                 if(this.selectedNode){
404                         this.selectedNode.isSelected = false;
405                 }
406                 if(node){
407                         node.isSelected = true;
408                 }
409                 this.selectedNode = node;
410         },
411         selectEdge: function(edge){
412                 if(this.selectedEdge){
413                         this.selectedEdge.isSelected = false;
414                 }
415                 if(edge){
416                         edge.isSelected = true;
417                 }
418                 this.selectedEdge = edge;
419         },
420         setIdentifierForSelectedNode: function(str){
421                 if(this.selectedNode){
422                         this.selectedNode.identifier = str;
423                 }
424         },
425 }
426
427 function MGNode(env, identifier){
428         this.env = env;
429         this.identifier = identifier;
430         this.position = new Point2D(0, 0);
431         this.size = 10;
432         //ランダムな初期ベクトルをもつ。
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;
438         //
439         this.strokeStyle = "rgba(0, 0, 0, 1)";
440         this.isSelected = false;
441 }
442 MGNode.prototype = {
443         draw: function(){
444                 if(this.isSelected){
445                         this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
446                 } else{
447                         this.env.context.strokeStyle = this.strokeStyle;
448                 }
449                 this.env.drawCircle(this.position.x, this.position.y, this.size);
450                 if(this.identifier){
451                         this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
452                 }
453         },
454         tick: function(){
455                 var e;
456                 var p;
457                 var l;
458                 var q;
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;
463                 
464                 if(!this.ignoreEdgeRepulsion){
465                         //node
466                         //距離の近い点同士には斥力が働くとする。
467                         p = this.env.nodeList;
468                         for(var i = 0, iLen = p.length; i < iLen; i++){
469                                 var q = this.env.nodeList[i];
470                                 if(q != this){
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;
478                                         }
479                                 }
480                         }
481                         //edge
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){
489                                                 if(l < 1){
490                                                         l = 1;
491                                                 }
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;
501                                         }
502                                 }
503                         }
504                 } else{
505                         this.ignoreEdgeRepulsion--;
506                 }
507         },
508 }
509
510 function MGEdge(env, identifier, node0, node1){
511         this.env = env;
512         this.identifier = identifier;
513         this.node0 = node0;
514         this.node1 = node1;
515         this.freeLength = 250;
516         //
517         this.strokeStyle = "rgba(0, 0, 0, 1)";
518         this.isSelected = false;
519         //
520         this.centerPoint = new Point2D(0, 0);
521 }
522 MGEdge.prototype = {
523         draw: function(){
524                 if(this.isSelected){
525                         this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
526                 } else{
527                         this.env.context.strokeStyle = this.strokeStyle;
528                 }
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);
532                         if(this.identifier){
533                                 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
534                         }
535                 }
536         },
537         tick: function(){
538                 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
539                 var e;
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;
548                 }
549                 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
550         },
551         drawCurvedLineP: function(p, q){
552                 var that = this;
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();
561         },
562 }
563
564 function Point2D(x, y){
565         this.x = x;
566         this.y = y;
567 }
568 Point2D.prototype = {
569         
570 }
571
572 function Rectangle(x, y, width, height){
573         this.origin = new Point2D(x,y);
574         this.size = new Point2D(width,height);
575 }
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);
580         },
581 }
582