1 (function(global){ // BEGIN CLOSURE 2 3 var 4 math = global.Math, 5 cos = math.cos, 6 sin = math.sin, 7 sqrt = math.sqrt, 8 mmin = math.min, 9 mmax = math.max, 10 atan2 = math.atan2, 11 acos = math.acos, 12 PI = math.PI; 13 14 var enqueue = function(fnc){ 15 setTimeout(fnc, 0); 16 }; 17 18 var isArray = Array.isArray || function (obj) { 19 return Object.prototype.toString.call(obj) === '[object Array]'; 20 }; 21 22 var isObject = function (obj) { 23 return Object.prototype.toString.call(obj) === '[object Object]'; 24 }; 25 26 if (!global.console){ 27 global.console = { 28 log: function(){}, 29 warn: function(){}, 30 error: function(){}, 31 debug: function(){} 32 }; 33 } 34 35 if (!Array.indexOf){ 36 /** 37 * Array.indexOf is missing in IE 8. 38 * @private 39 */ 40 Array.prototype.indexOf = function (obj, start){ 41 for (var i = (start || 0), len = this.length; i < len; i++){ 42 if (this[i] == obj){ 43 return i; 44 } 45 } 46 return -1; 47 }; 48 } 49 50 /** 51 * Copies all the properties to the first argument from the following arguments. 52 * All the properties will be overwritten by the properties from the following 53 * arguments. Inherited properties are ignored. 54 * @private 55 */ 56 var Mixin = function() { 57 var target = arguments[0]; 58 for (var i = 1, l = arguments.length; i < l; i++){ 59 var extension = arguments[i]; 60 for (var key in extension){ 61 if (!extension.hasOwnProperty(key)){ 62 continue; 63 } 64 var copy = extension[key]; 65 if (copy === target[key]){ 66 continue; 67 } 68 // copying super with the name base if it does'nt has one already 69 if (typeof copy == "function" && typeof target[key] == "function" && !copy.base){ 70 copy.base = target[key]; 71 } 72 target[key] = copy; 73 } 74 } 75 return target; 76 }; 77 78 /** 79 * Copies all properties to the first argument from the following 80 * arguments only in case if they don't exists in the first argument. 81 * All the function propererties in the first argument will get 82 * additional property base pointing to the extenders same named 83 * property function's call method. 84 * @example 85 * // usage of base 86 * Bar.extend({ 87 * // function should have name 88 * foo: function foo(digit) { 89 * return foo.base(this, parseInt(digit)) 90 * } 91 * }); 92 * @private 93 */ 94 var Supplement = function() { 95 var target = arguments[0]; 96 for (var i = 1, l = arguments.length; i < l; i++){ 97 var extension = arguments[i]; 98 for (var key in extension) { 99 var copy = extension[key]; 100 if (copy === target[key]){ 101 continue; 102 } 103 // copying super with the name base if it does'nt has one already 104 if (typeof copy == "function" && typeof target[key] == "function" && !target[key].base){ 105 target[key].base = copy; 106 } 107 // target doesn't has propery that is owned by extension copying it 108 if (!target.hasOwnProperty(key) && extension.hasOwnProperty(key)){ 109 target[key] = copy; 110 } 111 } 112 } 113 return target; 114 }; 115 116 /** 117 * Copies all the properties to the first argument from the following arguments. 118 * All the properties will be overwritten by the properties from the following 119 * arguments. Inherited properties are ignored. 120 * Deep version. 121 * @private 122 */ 123 var DeepMixin = function() { 124 var target = arguments[0]; 125 for (var i = 1, l = arguments.length; i < l; i++) { 126 var extension = arguments[i]; 127 for (var key in extension) { 128 var copy = extension[key]; 129 if (copy === target[key]) continue; 130 if (isObject(copy)) DeepMixin((target[key] || (target[key] = {})), copy); 131 // copying super with the name base if it does'nt has one already 132 if (typeof copy == 'function' && typeof target[key] == 'function' && !target[key].base) { 133 target[key].base = copy; 134 } 135 target[key] = copy; 136 } 137 } 138 return target; 139 }; 140 141 /** 142 * Copies all properties to the first argument from the following 143 * arguments only in case if they don't exists in the first argument. 144 * All the function propererties in the first argument will get 145 * additional property base pointing to the extenders same named 146 * property function's call method. 147 * @example 148 * // usage of base 149 * Bar.extend({ 150 * // function should have name 151 * foo: function foo(digit) { 152 * return foo.base(this, parseInt(digit)) 153 * } 154 * }); 155 * Deep version. 156 * @private 157 */ 158 var DeepSupplement = function() { 159 var target = arguments[0]; 160 for (var i = 1, l = arguments.length; i < l; i++) { 161 var extension = arguments[i]; 162 for (var key in extension) { 163 var copy = extension[key]; 164 if (copy === target[key]) continue; 165 if (isObject(copy)) DeepSupplement((target[key] || (target[key] = {})), copy); 166 // copying super with the name base if it does'nt has one already 167 if (typeof copy == 'function' && typeof target[key] == 'function' && !target[key].base) { 168 target[key].base = copy; 169 } 170 // target doesn't has propery that is owned by extension copying it 171 if (!target.hasOwnProperty(key) && extension.hasOwnProperty(key)){ 172 target[key] = copy; 173 } 174 } 175 } 176 return target; 177 }; 178 179 180 /** 181 * @name Joint 182 * @constructor 183 * @param {RaphaelObject|Shape|object} from Object/position where the connection starts. 184 * @param {RaphaelObject|Shape|object} to Object/position where the connection ends. 185 * @param {object} [opts] opt Options 186 * @param {object} [opts.interactive] Is the joint interactive? [default = true] 187 * @param {object} [opts.attrs] Connection options (see Raphael possible parameters) 188 * @param {string} [opts.cursor] Connection CSS cursor property 189 * @param {boolean} [opts.beSmooth] Connection enable/disable smoothing 190 * @param {string|array} [opts.label] Connection label(s) 191 * @param {object|array} [opts.labelAttrs] Label(s) options (see Raphael possible parameters) + position attribute (<0, [0, 1], >1) 192 * @param {object|array} [opts.labelBoxAttrs] SVG Attributes of the label(s) bounding rectangle + padding attribute 193 * @param {object} [opts.startArrow] Start arrow options 194 * @param {string} [opts.startArrow.type] "none"|"basic" 195 * @param {number} [opts.startArrow.size] Start arrow size 196 * @param {object} [opts.startArrow.attrs] Start Arrow options (see Raphael possible parameters) 197 * @param {object} [opts.endArrow] End arrow options 198 * @param {string} [opts.endArrow.type] "none"|"basic" 199 * @param {number} [opts.endArrow.size] End arrow size 200 * @param {object} [opts.endArrow.attrs] End Arrow options (see Raphael possible parameters) 201 * @param {object} [opts.dummy] Dummy node options (shows when dragging arrows) 202 * @param {object} [opts.dummy.start] Start dummy node options 203 * @param {number} [opts.dummy.start.radius] Start dummy radius 204 * @param {object} [opts.dummy.start.attrs] Start dummy options (see Raphael possible parameters) 205 * @param {object} [opts.dummy.end] End dummy node options 206 * @param {number} [opts.dummy.end.radius] End dummy radius 207 * @param {object} [opts.dummy.end.attrs] End dummy options (see Raphael possible parameters) 208 * @param {object} [opts.handle] Handle options 209 * @param {number} [opts.handle.timeout] Number of milliseconds handle stays shown 210 * @param {object} [opts.handle.start] Start handle options 211 * @param {boolean} [opts.handle.start.enabled] Start handle enabled/disabled 212 * @param {number} [opts.handle.start.radius] Start handle radius 213 * @param {object} [opts.handle.start.attrs] Start handle attributes (see Raphael possible parameters) 214 * @param {object} [opts.handle.end] End handle options 215 * @param {boolean} [opts.handle.end.enabled] End handle enabled/disabled 216 * @param {number} [opts.handle.end.radius] End handle radius 217 * @param {object} [opts.handle.end.attrs] End handle attributes (see Raphael possible parameters) 218 * @param {object} [opts.bboxCorrection] Correction of a bounding box (useful when, e.g., the connection should start in the center of an object) 219 * @param {object} [opts.bboxCorrection.start] BBox correction of the start object. 220 * @param {string} [opts.bboxCorrection.start.type] "ellipse"|"rect" 221 * @param {number} [opts.bboxCorrection.start.x] Translation in the x-axis 222 * @param {number} [opts.bboxCorrection.start.y] Translation in the y-axis 223 * @param {number} [opts.bboxCorrection.start.width] BBox width 224 * @param {number} [opts.bboxCorrection.start.height] BBox height 225 * @param {object} [opts.bboxCorrection.end] BBox correction of the end object. 226 * @param {string} [opts.bboxCorrection.end.type] "ellipse"|"rect" 227 * @param {number} [opts.bboxCorrection.end.x] Translation in the x-axis 228 * @param {number} [opts.bboxCorrection.end.y] Translation in the y-axis 229 * @param {number} [opts.bboxCorrection.end.width] BBox width 230 * @param {number} [opts.bboxCorrection.end.height] BBox height 231 * @example 232 * Joint({x: 10, y: 10}, {x: 300, y: 100}, { 233 * label: "my label", 234 * beSmooth: true, 235 * startArrow: { 236 * type: "basic", 237 * size: 7, 238 * attrs: { 239 * fill: "red", 240 * stroke: "blue" 241 * } 242 * }, 243 * handle: { 244 * timeout: 4000, 245 * start: { 246 * radius: 6, 247 * attrs: { 248 * fill: "green", 249 * stroke: "black" 250 * } 251 * }, 252 * end: { 253 * radius: 4, 254 * attrs: { 255 * fill: "red", 256 * stroke: "black" 257 * } 258 * } 259 * } 260 * }); 261 */ 262 function Joint(from, to, opt){ 263 if (!(this instanceof Joint)){ 264 return new Joint(from, to, opt); 265 } 266 /** 267 * @private 268 * @type RaphaelPaper 269 */ 270 var paper = this.paper = Joint.paper(); 271 272 // these objects are the ones I can connect to 273 this._registeredObjects = []; 274 275 this._conVerticesCurrentIndex = 0; 276 this._nearbyVertexSqrDist = 500; // sqrt(this._nearbyVertexSqrDist) is tolerable distance of vertex moving 277 278 this.dom = {}; // holds all dom elements 279 280 // connection from start to end 281 this._start = { // start object 282 shape: null, // Raphael object 283 dummy: false // is it a dummy object? 284 }; 285 this._end = { // end object 286 shape: null, // Raphael object 287 dummy: false // is it a dummy object? 288 }; 289 290 // connection options 291 this._opt = { 292 vertices: [], // joint path vertices 293 attrs: { 294 "stroke": "#000", 295 // "fill": "#fff", // can not be used if connection wiring is enabled 296 "fill-opacity": 0.0, 297 "stroke-width": 1, 298 "stroke-dasharray": "-", 299 "stroke-linecap": "round", // butt/square/round/mitter 300 "stroke-linejoin": "round", // butt/square/round/mitter 301 "stroke-miterlimit": 1, 302 "stroke-opacity": 1.0 303 }, 304 cursor: "move", // CSS cursor property 305 beSmooth: false,// be a smooth line? (bezier curve aproximation) 306 interactive: true, // is the connection interactive? 307 label: undefined, 308 labelAttrsDefault: { 309 position: 1/2, 310 offset: 0, // y-offset 311 "font-size": 12, 312 "fill": "#000" 313 }, 314 labelAttrs: [], 315 labelBoxAttrsDefault: { stroke: "white", fill: "white" }, 316 labelBoxAttrs: [], 317 // bounding box correction 318 // (useful when the connection should start in the center of an object, etc...) 319 bboxCorrection: { 320 start: { type: null, x: 0, y: 0, width: 0, height: 0 }, 321 end: { type: null, x: 0, y: 0, width: 0, height: 0 } 322 }, 323 // dummy nodes radius and SVG attributes 324 dummy: { 325 start: { 326 radius: 1, 327 attrs: {"opacity": 0.0, "fill": "red"} 328 }, 329 end: { 330 radius: 1, 331 attrs: {"opacity": 0.0, "fill": "yellow"} 332 } 333 }, 334 // handles (usefull when picking "none" type of arrow) 335 handle: { 336 timeout: 2000, 337 start: { 338 enabled: false, 339 radius: 4, 340 attrs: { opacity: 1.0, fill: "red", stroke: "black" } 341 }, 342 end: { 343 enabled: false, 344 radius: 4, 345 attrs: { opacity: 1.0, fill: "red", stroke: "black" } 346 } 347 } 348 }; 349 // used arrows (default values) 350 this._opt.arrow = { 351 start: Joint.getArrow("none", 2, this._opt.attrs), 352 end: Joint.getArrow("basic", 5) 353 }; 354 // options 355 if (opt) this.processOptions(opt); 356 357 JointDOMBuilder.init(this.paper, this._opt, this._start, this._end); 358 359 var startObject = this._start, 360 endObject = this._end; 361 362 if (from.x && from.y) // from is point? 363 JointDOMBuilder.dummy(startObject, from, this._opt.dummy.start); 364 else 365 startObject.shape = from.yourself(); 366 367 if (to.x && to.y) // to is point? 368 JointDOMBuilder.dummy(endObject, to, this._opt.dummy.end); 369 else 370 endObject.shape = to.yourself(); 371 372 // to be able to dispatch events in Raphael element attr method 373 // TODO: possible source of memory leaks!!! 374 this.addJoint(startObject.shape); 375 this.addJoint(endObject.shape); 376 // draw 377 this.update(); 378 } 379 global.Joint = Joint; // the only global variable 380 381 Joint.euid = 1; // elements/joints unique id 382 /** 383 * Assign unique id to this. 384 * @private 385 * @example Joint.generateEuid.call(obj); 386 */ 387 Joint.generateEuid = function(){ 388 if (this._euid === undefined) this._euid = Joint.euid++; 389 return this._euid; 390 }; 391 392 /** 393 * @private 394 */ 395 Joint.prototype = { 396 // temporaries for moving objects 397 _dx: undefined, 398 _dy: undefined, 399 /* 400 * States. 401 */ 402 IDLE: 0, 403 STARTCAPDRAGGING: 1, 404 ENDCAPDRAGGING: 2, 405 CONNECTIONWIRING: 3, 406 state: 0, // IDLE 407 /* 408 * Callbacks. 409 * @name Callbacks 410 */ 411 _callbacks: { 412 // called when a joint has just connected to an object 413 // the object is accessed using this, 414 // the only argument is what side has been connected ("start" | "end") 415 justConnected: function(side){}, 416 disconnected: function(side){}, 417 justBroken: function(mousePos){}, 418 wiring: function(mousePos){}, 419 objectMoving: function(obj){} 420 }, 421 /** 422 * @return {String} Joint unique identifier. 423 */ 424 euid: function(){ 425 return Joint.generateEuid.call(this); 426 }, 427 /* 428 * Getters. 429 */ 430 connection: function(){ return this.dom.connection[0]; }, 431 endObject: function(){ return this._end.shape; }, 432 startObject: function(){ return this._start.shape; }, 433 endCap: function(){ return this.dom.endCap; }, 434 endCapConnected: function(){ return !this._end.dummy; }, 435 startCap: function(){ return this.dom.startCap; }, 436 startCapConnected: function(){ return !this._start.dummy; }, 437 isStartDummy: function(){ return this._start.dummy; }, 438 isEndDummy: function(){ return this._end.dummy; }, 439 /** 440 * Replaces dummy object with a new object. 441 * @private 442 * @param {object} startOrEnd start or end object (old dummy) 443 * @param {RaphaelObject} o 444 */ 445 replaceDummy: function(startOrEnd, o){ 446 // assert(startOrEnd.dummy == true) 447 startOrEnd.shape.remove(); 448 startOrEnd.dummy = false; 449 startOrEnd.shape = o; 450 }, 451 /** 452 * Calls a callback. 453 * @private 454 * @param fnc Callback 455 * @param {object} scope Scope of the callback 456 * @param {array} args Array of arguments 457 */ 458 callback: function(fnc, scope, args){ 459 this._callbacks[fnc].apply(scope, args); 460 return this; 461 }, 462 /** 463 * Search the registered objects and get the one (if any) 464 * who's bounding box contains the point p. 465 * @todo check document.elementFromPoint(x, y) 466 * @private 467 * @param {Point} p 468 */ 469 objectContainingPoint: function(p){ 470 var register = this._registeredObjects, 471 idx = (register ? register.length : 0), o; 472 while (idx--){ 473 o = register[idx].yourself(); 474 if (rect(o.getBBox()).containsPoint(p)) 475 return o; 476 } 477 return null; 478 }, 479 /** 480 * Remove reference to Joint from obj. 481 * @private 482 * @param {StartObject|EndObject} obj 483 */ 484 freeJoint: function(obj){ 485 var jar = obj.yourself().joints(), 486 i = jar.indexOf(this); 487 jar.splice(i, 1); 488 return this; 489 }, 490 /** 491 * Add reference to Joint to obj. 492 * @private 493 * @param {RaphaelObject} obj 494 */ 495 addJoint: function(obj){ 496 var joints = obj.joints();; 497 // push the Joint object into obj.joints array 498 // but only if obj.joints already doesn't have that Joint object 499 if (joints.indexOf(this) === -1) joints.push(this); 500 }, 501 /** 502 * MouseDown event callback when on cap. 503 * @private 504 * @param {Event} e 505 * @param {RaphaelObject} cap 506 */ 507 capMouseDown: function(e, cap){ 508 Joint.currentJoint = this; // keep global reference to me 509 this._dx = e.clientX; 510 this._dy = e.clientY; 511 512 if (cap === this.dom.startCap){ 513 this.disconnect("start"); 514 this.state = this.STARTCAPDRAGGING; 515 } else if (cap === this.dom.endCap){ 516 this.disconnect("end"); 517 this.state = this.ENDCAPDRAGGING; 518 } 519 }, 520 /** 521 * MouseDown event callback when on connection. 522 * @private 523 * @param {Event} e 524 */ 525 connectionMouseDown: function(e){ 526 Joint.currentJoint = this; // keep global reference to me 527 var mousePos = Joint.getMousePosition(e, this.paper.canvas); 528 529 // if the mouse position is nearby a connection vertex 530 // do not create a new one but move the selected one instead 531 for (var i = 0, len = this._opt.vertices.length; i < len; i++){ 532 var v = this._opt.vertices[i]; 533 if (line(v, mousePos).squaredLength() < this._nearbyVertexSqrDist){ 534 this._conVerticesCurrentIndex = i; 535 this.state = this.CONNECTIONWIRING; 536 return; 537 } 538 } 539 540 // new vertices can be added CORRECTLY only at the end 541 // or at the start of the connection 542 // -> @todo 543 var sbbCenter = rect(this.startObject().getBBox()).center(), 544 ebbCenter = rect(this.endObject().getBBox()).center(), 545 // squared lengths of the lines from the center of 546 // start/end object bbox to the mouse position 547 smLineSqrLen = line(sbbCenter, mousePos).squaredLength(), 548 emLineSqrLen = line(ebbCenter, mousePos).squaredLength(); 549 if (smLineSqrLen < emLineSqrLen){ 550 // new vertex is added to the beginning of the vertex array 551 this._conVerticesCurrentIndex = 0; 552 this._opt.vertices.unshift(mousePos); 553 } else { 554 // new vertex is added to the end of the vertex array 555 this._conVerticesCurrentIndex = this._opt.vertices.push(mousePos) - 1; 556 } 557 this.state = this.CONNECTIONWIRING; 558 this.callback("justBroken", this, [mousePos]); 559 }, 560 capDragging: function(e){ 561 // move dummy object 562 if (this.state === this.STARTCAPDRAGGING){ 563 this.startObject().translate(e.clientX - this._dx, e.clientY - this._dy); 564 } else if (this.state === this.ENDCAPDRAGGING) { 565 this.endObject().translate(e.clientX - this._dx, e.clientY - this._dy); 566 } else { 567 return; // should not happen 568 } 569 this._dx = e.clientX; 570 this._dy = e.clientY; 571 572 this.update(); 573 }, 574 capEndDragging: function(){ 575 var dummyBB, 576 STARTCAPDRAGGING = (this.state === this.STARTCAPDRAGGING), 577 ENDCAPDRAGGING = (this.state === this.ENDCAPDRAGGING), 578 capType = (STARTCAPDRAGGING) ? "start" : "end"; 579 580 581 if (STARTCAPDRAGGING){ 582 dummyBB = this.startObject().getBBox(); 583 } else if (ENDCAPDRAGGING){ 584 dummyBB = this.endObject().getBBox(); 585 } 586 587 var o = this.objectContainingPoint(point(dummyBB.x, dummyBB.y)); 588 if (o){ 589 this.callback("justConnected", o, [capType]); 590 this.replaceDummy(this["_" + capType], o); 591 this.addJoint(o); 592 } 593 594 this.update(); 595 }, 596 connectionWiring: function(e){ 597 var mousePos = Joint.getMousePosition(e, this.paper.canvas); 598 this._opt.vertices[this._conVerticesCurrentIndex] = mousePos; 599 this.update(); 600 this.callback("wiring", this, [mousePos]); 601 }, 602 update: function(){ 603 this.redraw().listenAll(); 604 // setTimeout makes drawing much faster! 605 // var self = this; 606 // enqueue(function(){self.redraw().listenAll();}); 607 }, 608 redraw: function(){ 609 this.clean(["connection", "startCap", "endCap", "handleStart", "handleEnd", "label"]); 610 this.draw(["connection", "startCap", "endCap", "handleStart", "handleEnd", "label"]); 611 return this; 612 }, 613 listenAll: function(){ 614 if (!this._opt.interactive){ 615 return this; 616 } 617 var self = this; 618 this.dom.startCap.mousedown(function(e){ 619 Joint.fixEvent(e); 620 self.capMouseDown(e, self.dom.startCap); 621 e.stopPropagation(); 622 e.preventDefault(); 623 }); 624 this.dom.endCap.mousedown(function(e){ 625 Joint.fixEvent(e); 626 self.capMouseDown(e, self.dom.endCap); 627 e.stopPropagation(); 628 e.preventDefault(); 629 }); 630 var i; 631 i = this.dom.connection.length; 632 while (i--) { 633 this.dom.connection[i].mousedown(function(e){ 634 Joint.fixEvent(e); 635 self.connectionMouseDown(e); 636 e.stopPropagation(); 637 e.preventDefault(); 638 }); 639 } 640 if (this._opt.handle.start.enabled){ 641 this.dom.handleStart.mousedown(function(e){ 642 Joint.fixEvent(e); 643 self.capMouseDown(e, self.dom.startCap); 644 e.stopPropagation(); 645 e.preventDefault(); 646 }); 647 } 648 if (this._opt.handle.end.enabled){ 649 this.dom.handleEnd.mousedown(function(e){ 650 Joint.fixEvent(e); 651 self.capMouseDown(e, self.dom.endCap); 652 e.stopPropagation(); 653 e.preventDefault(); 654 }); 655 } 656 if (this._opt.handle.timeout !== Infinity){ 657 i = this.dom.connection.length; 658 while (i--) { 659 this.dom.connection[i].mouseover(function(e){ 660 Joint.fixEvent(e); 661 self.showHandle(); 662 setTimeout(function(){ 663 self.hideHandle(); 664 }, self._opt.handle.timeout); 665 e.stopPropagation(); 666 e.preventDefault(); 667 }); 668 } 669 } 670 return this; 671 }, 672 /** 673 * @private 674 */ 675 boundPoint: function(bbox, type, rotation, p){ 676 if (type === "circle" || type === "ellipse") 677 return ellipse(bbox.center(), bbox.width/2, bbox.height/2).intersectionWithLineFromCenterToPoint(p); 678 else if (type === 'rect' && bbox.width == bbox.height && rotation != 0) { 679 // compute new bounding box of a rotated square 680 // @todo Compute intersection properly 681 var w = bbox.width, 682 dia = Math.sqrt(w*w + w*w), 683 origin = bbox.center().offset(-dia/2, -dia/2); 684 bbox = rect(origin.x, origin.y, dia, dia); 685 return bbox.boundPoint(p) || bbox.center(); 686 } 687 return bbox.boundPoint(p) || bbox.center(); 688 }, 689 /** 690 * @private 691 * @param {object} start 692 * @param {rect} start.bbox Start object bounding box. 693 * @param {string} start.type Start object geometrical type. 694 * @param {point} start.shift Start arrow offsets. 695 * @param {object} end 696 * @param {rect} end.bbox End object bounding box. 697 * @param {string} end.type End object geometrical type. 698 * @param {point} end.shift End arrow offsets. 699 * @param {array} vertices Connection vertices. 700 * @return {object} Object containing location of start/end of the joint. 701 */ 702 jointLocation: function(start, end, vertices){ 703 var verticesLength = vertices.length, theta, 704 firstVertex = (vertices.length ? vertices[0] : undefined), 705 lastVertex = (vertices.length ? vertices[verticesLength - 1] : undefined), 706 p1, p1bp, c1t, c1r, p2, p2bp, c2t, c2r; 707 708 // start object boundary point 709 p1bp = this.boundPoint(start.bbox, start.type, start.rotation, firstVertex || end.bbox.center()); 710 // shift 711 theta = start.bbox.center().theta(firstVertex || end.bbox.center()); 712 // first point of the connection 713 p1 = point(p1bp.x + (2 * start.shift.dx * cos(theta.radians)), 714 p1bp.y + (-2 * start.shift.dy * sin(theta.radians))); 715 // start arrow translation 716 c1t = point(p1bp.x + start.shift.dx * cos(theta.radians), 717 p1bp.y - start.shift.dy * sin(theta.radians)); 718 // start arrow rotation 719 c1r = 360 - theta.degrees + 180; 720 721 // end object boundary point 722 p2bp = this.boundPoint(end.bbox, end.type, end.rotation, lastVertex || start.bbox.center()); 723 // shift 724 theta = (lastVertex || start.bbox.center()).theta(end.bbox.center()); 725 // last point of the connection 726 p2 = point(p2bp.x + (-2 * end.shift.dx * cos(theta.radians)), 727 p2bp.y + (2 * end.shift.dy * sin(theta.radians))); 728 // end arrow translation 729 c2t = point(p2bp.x - end.shift.dx * cos(theta.radians), 730 p2bp.y + end.shift.dy * sin(theta.radians)); 731 // end arrow rotation 732 c2r = 360 - theta.degrees; 733 734 return { 735 start: { 736 bound: p1bp, 737 connection: p1, 738 translate: c1t, 739 rotate: c1r 740 }, 741 end: { 742 bound: p2bp, 743 connection: p2, 744 translate: c2t, 745 rotate: c2r 746 } 747 }; 748 }, 749 /** 750 * @private 751 * @param {point} start Joint start location. 752 * @param {point} end Joint end location. 753 * @param {array} vertices Connection vertices. 754 * @param {boolean} smooth Connection smooth flag. 755 * @return {array} SVG path commands. 756 */ 757 connectionPathCommands: function(start, end, vertices, smooth){ 758 if (smooth) 759 return Bezier.curveThroughPoints([start].concat(vertices, [end])); 760 var commands = ["M", start.x, start.y], i = 0, l = vertices.length; 761 for (; i < l; i++) 762 commands.push("L", vertices[i].x, vertices[i].y); 763 commands.push("L", end.x, end.y); 764 return commands; 765 }, 766 767 /** 768 * @private 769 * @param {point} start Joint start location. 770 * @param {point} end Joint end location. 771 * @param {array} vertices Connection vertices. 772 * @return {array} Locations of the label (array of points). 773 */ 774 labelLocation: function(connectionPathCommands){ 775 var path = this.paper.path(connectionPathCommands.join(' ')), 776 length = path.getTotalLength(), 777 locations = [], attrs = this._opt.labelAttrs, len = attrs.length, i = 0, 778 position; 779 for (; i < len; i++) { 780 position = attrs[i].position; 781 position = (position > length) ? length : position; // sanity check 782 position = (position < 0) ? length + position : position; 783 position = position > 1 ? position : length * position; 784 locations.push(path.getPointAtLength(position)); 785 } 786 path.remove(); 787 return locations; 788 }, 789 790 /** 791 * @private 792 */ 793 draw: function(components){ 794 var self = this, 795 paper = this.paper, 796 jointLocation = this.jointLocation( 797 { 798 bbox: rect(this.startObject().getBBox()).moveAndExpand(this._opt.bboxCorrection.start), 799 type: this.startObject().type, 800 rotation: this.startObject().attrs.rotation, 801 shift: this._opt.arrow.start 802 }, 803 { 804 bbox: rect(this.endObject().getBBox()).moveAndExpand(this._opt.bboxCorrection.end), 805 type: this.endObject().type, 806 rotation: this.endObject().attrs.rotation, 807 shift: this._opt.arrow.end 808 }, 809 this._opt.vertices 810 ), 811 connectionPathCommands = this.connectionPathCommands( 812 jointLocation.start.connection, 813 jointLocation.end.connection, 814 this._opt.vertices, 815 this._opt.beSmooth 816 ), 817 labelLocation = this.labelLocation(connectionPathCommands), 818 dom = JointDOMBuilder.init(this.paper, this._opt, this._start, this._end, jointLocation, connectionPathCommands, labelLocation), 819 l = components.length, 820 component; 821 822 for (var i = 0; i < l; i++){ 823 component = components[i]; 824 this.dom[component] = dom[component](); 825 } 826 }, 827 /** 828 * Clean operations. 829 * Remove the DOM elements of connection/startCap/endCap/label if they exist. 830 * Clean operations support chaining. 831 * @private 832 */ 833 clean: function(components){ 834 var component, name, subComponents, idx = components.length; 835 while (idx--){ 836 name = components[idx]; 837 component = this.dom[name]; 838 if (component){ 839 if (component.node){ 840 component.remove(); 841 this.dom[name] = null; 842 } else { // component is a composite object 843 subComponents = component; 844 for (var key in subComponents){ 845 if (subComponents.hasOwnProperty(key)) 846 subComponents[key].remove(); 847 } 848 } 849 this.dom[name] = null; 850 } 851 } 852 }, 853 854 /** 855 * Process options. 856 * @todo Please fix me! I look like spagethi. 857 * @private 858 * @param {object} opt 859 */ 860 processOptions: function(opt){ 861 var key, myopt = this._opt, 862 topOptions = ['interactive', 'cursor', 'beSmooth'], i = topOptions.length; 863 864 // top options 865 while (i--) { 866 if (opt[topOptions[i]] !== undefined) 867 myopt[topOptions[i]] = opt[topOptions[i]]; 868 } 869 870 myopt.subConnectionAttrs = opt.subConnectionAttrs || undefined; 871 872 Mixin(myopt.attrs, opt.attrs); 873 Mixin(myopt.bboxCorrection.start, opt.bboxCorrection && opt.bboxCorrection.start); 874 Mixin(myopt.bboxCorrection.end, opt.bboxCorrection && opt.bboxCorrection.end); 875 if (opt.vertices) this._setVertices(opt.vertices); 876 877 // label(s) related options 878 if (opt.label) { 879 myopt.label = isArray(opt.label) ? opt.label : [opt.label]; 880 if (!isArray(opt.labelAttrs)) opt.labelAttrs = [opt.labelAttrs]; 881 for (i = 0; i < myopt.label.length; i++) { 882 Supplement(opt.labelAttrs[i] || (opt.labelAttrs[i] = {}), myopt.labelAttrsDefault); 883 } 884 myopt.labelAttrs = opt.labelAttrs; // make a copy? (parse(stringify(opt))) 885 886 var spread = undefined; 887 if (!isArray(opt.labelBoxAttrs)) { 888 if (typeof opt.labelBoxAttrs === 'object') 889 spread = opt.labelBoxAttrs; 890 opt.labelBoxAttrs = [opt.labelBoxAttrs]; 891 } 892 for (i = 0; i < myopt.label.length; i++) { 893 if (spread) opt.labelBoxAttrs[i] = spread; 894 Supplement(opt.labelBoxAttrs[i] || (opt.labelBoxAttrs[i] = {}), this._opt.labelBoxAttrsDefault); 895 } 896 myopt.labelBoxAttrs = opt.labelBoxAttrs; // make a copy? (parse(stringify(opt))) 897 } 898 899 // arrowheads 900 var sa = opt.startArrow, ea = opt.endArrow; 901 if (sa && sa.type) myopt.arrow.start = Joint.getArrow(sa.type, sa.size, sa.attrs); 902 if (ea && ea.type) myopt.arrow.end = Joint.getArrow(ea.type, ea.size, ea.attrs); 903 // direct arrow specification rewrites types 904 if (opt.arrow) { 905 myopt.arrow.start = opt.arrow.start || myopt.arrow.start; 906 myopt.arrow.end = opt.arrow.end || myopt.arrow.end; 907 } 908 909 // dummies 910 if (opt.dummy && opt.dummy.start) { 911 if (opt.dummy.start.radius) myopt.dummy.start.radius = opt.dummy.start.radius; 912 Mixin(myopt.dummy.start.attrs, opt.dummy.start.attrs); 913 } 914 if (opt.dummy && opt.dummy.end) { 915 if (opt.dummy.end.radius) myopt.dummy.end.radius = opt.dummy.end.radius; 916 Mixin(myopt.dummy.end.attrs, opt.dummy.end.attrs); 917 } 918 // handles 919 if (opt.handle){ 920 if (opt.handle.timeout) myopt.handle.timeout = opt.handle.timeout; 921 if (opt.handle.start){ 922 if (opt.handle.start.enabled) myopt.handle.start.enabled = opt.handle.start.enabled; 923 if (opt.handle.start.radius) myopt.handle.start.radius = opt.handle.start.radius; 924 Mixin(myopt.handle.start.attrs, opt.handle.start.attrs); 925 } 926 if (opt.handle.end){ 927 if (opt.handle.end.enabled) myopt.handle.end.enabled = opt.handle.end.enabled; 928 if (opt.handle.end.radius) myopt.handle.end.radius = opt.handle.end.radius; 929 Mixin(myopt.handle.end.attrs, opt.handle.end.attrs); 930 } 931 } 932 }, 933 // Public API 934 935 /** 936 * Disconnects joint from objects. 937 * @param {string} cap "start|end|both" which side to disconnect 938 * @return {Joint} return this to allow chaining 939 */ 940 disconnect: function(cap){ 941 var disconnectedFrom, camelCap = (cap === "start") 942 ? "Start" 943 : (cap === "end" 944 ? "End" 945 : "Both"); 946 947 if (cap === "both" || cap === undefined){ 948 this.freeJoint(this.startObject()) 949 .freeJoint(this.endObject()); 950 951 if (!this.isStartDummy()){ 952 disconnectedFrom = this.startObject(); 953 this.draw(["dummyStart"]); 954 this.callback("disconnected", disconnectedFrom, [cap]); 955 } 956 if (!this.isEndDummy()){ 957 disconnectedFrom = this.endObject(); 958 this.draw(["dummyEnd"]); 959 this.callback("disconnected", disconnectedFrom, [cap]); 960 } 961 962 } else if (!this["is" + camelCap + "Dummy" ]()){ 963 // do not do anything with already disconnected side 964 disconnectedFrom = this[cap + "Object"](); 965 if (this.startObject() !== this.endObject()){ 966 this.freeJoint(disconnectedFrom); 967 } 968 this.draw(["dummy" + camelCap]); 969 this.callback("disconnected", disconnectedFrom, [cap]); 970 } 971 972 return this; 973 }, 974 975 /** 976 * Register object(s) so that it can be pointed by my cap. 977 * @param {RaphaelObject|Shape|array} obj 978 * @param {string} cap "start|end|both" cap to register default: "both" 979 * @return {Joint} 980 * @example j.register(circle, "end") 981 */ 982 register: function(obj, cap){ 983 if (!cap){ 984 cap = "both"; 985 } 986 // prepare array of objects that are to be registered 987 var toRegister = (obj.constructor == Array) ? obj : [obj]; 988 // register all objects in toRegister array 989 for (var i = 0, len = toRegister.length; i < len; i++){ 990 toRegister[i].yourself()._capToStick = cap; 991 this._registeredObjects.push(toRegister[i]); 992 } 993 return this; 994 }, 995 /** 996 * The difference between register and registerForever is that registerForever 997 * saves reference to an array passed as argument. It means that all objects pushed 998 * into the array before and/or after the call of this method will be registered (for both caps). 999 * This method is useful for applications that do not know to which objects the connection 1000 * can be sticked when the joint is created. 1001 * @param {array} arr An array holding objects which the joint is going to be registered to. 1002 * @return {Joint} 1003 * @example 1004 * var all = []; 1005 * j.registerForever(all); 1006 * // ... create objects and push them into all array 1007 */ 1008 registerForever: function(arr){ 1009 if (Object.prototype.toString.call(arr) !== "[object Array]") 1010 arr = Array.prototype.slice.call(arguments); 1011 this._registeredObjects = arr; 1012 return this; 1013 }, 1014 /** 1015 * Cancel registration of an object. 1016 * @param {RaphaelObject|Shape} obj 1017 * @param {string} cap "start|end|both" cap to unregister default: "both" 1018 * @return {Joint} 1019 * @example j.unregister(circle, "end"); 1020 */ 1021 unregister: function(obj, cap){ 1022 cap = cap || "both"; 1023 1024 var index = -1; 1025 for (var i = 0, len = this._registeredObjects.length; i < len; i++){ 1026 var capToStick = this._registeredObjects[i].yourself()._capToStick || "both"; 1027 if (this._registeredObjects[i] === obj && capToStick === cap){ 1028 index = i; 1029 break; 1030 } 1031 } 1032 if (index !== -1){ 1033 this._registeredObjects.splice(index, 1); 1034 } 1035 return this; 1036 }, 1037 /** 1038 * @return {array} Registered Objects. 1039 */ 1040 registeredObjects: function(){ 1041 return this._registeredObjects; 1042 }, 1043 /** 1044 * Set the vertices of the connection 1045 * @param {array} vertices Array of points (vertices) - either of the form: {x: 5, y; 10} or "5 10" or "5@10" 1046 * @return {Joint} 1047 */ 1048 setVertices: function(vertices){ 1049 this._setVertices(vertices); 1050 this.update(); 1051 return this; 1052 }, 1053 /** 1054 * Set connection vertices. 1055 * @private 1056 */ 1057 _setVertices: function(vertices) { 1058 var conVertices = this._opt.vertices = [], p; 1059 // cast vertices to points 1060 for (var i = 0, l = vertices.length; i < l; i++){ 1061 p = (vertices[i].y === undefined) ? 1062 point(vertices[i]) : point(vertices[i].x, vertices[i].y); 1063 conVertices.push(p); 1064 } 1065 return this; 1066 }, 1067 /** 1068 * Get connection vertices. 1069 * @return {array} array of connection vertices 1070 */ 1071 getVertices: function(){ 1072 return this._opt.vertices; 1073 }, 1074 /** 1075 * Toggle the connection smoothing (bezier/straight). 1076 * @return {Joint} 1077 */ 1078 toggleSmoothing: function(){ 1079 this._opt.beSmooth = !this._opt.beSmooth; 1080 this.update(); 1081 return this; 1082 }, 1083 /** 1084 * Find out whether the connection is smooth or not. 1085 * @return {boolean} true if connection is smooth 1086 */ 1087 isSmooth: function(){ 1088 return this._opt.beSmooth; 1089 }, 1090 /** 1091 * Set a label of the connection. 1092 * @param {string|array} str label(s) 1093 * @return {Joint} 1094 */ 1095 label: function(str){ 1096 this._opt.label = isArray(str) ? str : [str]; 1097 for (var i = 0; i < str.length; i++) { 1098 this._opt.labelAttrs[i] = this._opt.labelAttrsDefault; 1099 this._opt.labelBoxAttrs[i] = this._opt.labelBoxAttrsDefault; 1100 } 1101 this.update(); 1102 return this; 1103 }, 1104 /** 1105 * Register callback function on various events. 1106 * @link Callbacks 1107 * @param {string} evt "justConnected"|"disconnected"|"justBroken"|"wiring"|"objectMoving" 1108 * @param fnc Callback 1109 * @return {Joint} 1110 * @example 1111 * j.registerCallback("justConnected", function(side){ ... this points to the object the joint was just connected to ... }); 1112 * j.registerCallback("disconnected", function(side){ ... this points to the object the joint was just disconnected from ... }); 1113 * j.registerCallback("justBroken", function(mousePos){ ... this points to the joint object ... }); 1114 * j.registerCallback("wiring", function(mousePos){ ... this points to the joint object ... }); 1115 * j.registerCallback("objectMoving", function(obj){ ... this points to the joint object ... }); 1116 * 1117 * j.registerCallback("justConnected", function(side){ 1118 * if (side === "start"){ 1119 * console.log("Start cap connected."); 1120 * } else { // side === "end" 1121 * console.log("End cap connected"); 1122 * } 1123 * }); 1124 */ 1125 registerCallback: function(evt, fnc){ 1126 this._callbacks[evt] = fnc; 1127 return this; 1128 }, 1129 /** 1130 * Straighten the bent connection path. 1131 * @return {Joint} 1132 */ 1133 straighten: function(){ 1134 this._opt.vertices = []; 1135 this.update(); 1136 return this; 1137 }, 1138 /** 1139 * Show/hide handle(s). 1140 * If a connection arrow is, e.g., of type none, it is difficult to grab the end of the connection. 1141 * For these cases, you can use handles, which are just simple circles showing at the end of a connection. 1142 * @param {string} cap &optional [start|end] Specifies on what side handle should be shown. 1143 * @return {Joint} 1144 */ 1145 toggleHandle: function(cap){ 1146 var handle = this._opt.handle; 1147 if (!cap){ 1148 handle.start.enabled = !handle.start.enabled; 1149 handle.end.enabled = !handle.end.enabled; 1150 } else { 1151 handle[cap].enabled = !handle[cap].enabled; 1152 } 1153 this.update(); 1154 return this; 1155 }, 1156 /** 1157 * Show handle. 1158 * @return {Joint} 1159 */ 1160 showHandle: function(cap){ 1161 var handle = this._opt.handle; 1162 if (!cap){ 1163 handle.start.enabled = true; 1164 handle.end.enabled = true; 1165 } else { 1166 handle[cap].enabled = true; 1167 } 1168 this.update(); 1169 return this; 1170 }, 1171 /** 1172 * Hide handle. 1173 * @return {Joint} 1174 */ 1175 hideHandle: function(cap){ 1176 var handle = this._opt.handle; 1177 if (!cap){ 1178 handle.start.enabled = false; 1179 handle.end.enabled = false; 1180 } else { 1181 handle[cap].enabled = false; 1182 } 1183 this.update(); 1184 return this; 1185 }, 1186 /** 1187 * Set bounding box correction. 1188 * This advanced feature of Joint library allows you to shift a point to which a connection sticks. 1189 * You can for example modify a connection to point to the center of an object or you can set a distance 1190 * between an object and a connection arrow. 1191 * @param {object} [corr] correction Correction 1192 * @param {string} [corr.type] fake type of an object to which a cap points 1193 * @param {number} [corr.x] x-axis shift of an object bounding box 1194 * @param {number} [corr.y] y-axis shift of an object bounding box 1195 * @param {number} [corr.width] change in an object bounding box width (can be negative) 1196 * @param {number} [corr.height] change in an object bounding box height (can be negative) 1197 * @param {string} cap "start|end"|undefined cap (undefined === both caps) 1198 * @return {Joint} 1199 * @example 1200 * // 1.) both sides of the connection will point to the center of 1201 * // a circular object with radius == 30 1202 * j.setBBoxCorrection({ 1203 * type: "ellipse", 1204 * x: 30, 1205 * y: 30, 1206 * width: -60, 1207 * height: -60 1208 * }); 1209 * 1210 * // 2.) keep 20px distance between connection's arrow 1211 * // and a circular object 1212 * j.setBBoxCorrection({ 1213 * type: "ellipse", 1214 * x: -20, 1215 * y: -20, 1216 * width: 40, 1217 * height: 40 1218 * }); 1219 */ 1220 setBBoxCorrection: function(corr, cap){ 1221 if (!cap){ 1222 this._opt.bboxCorrection.start = this._opt.bboxCorrection.end = corr; 1223 } else { 1224 this._opt.bboxCorrection[cap] = corr; 1225 } 1226 this.update(); 1227 return this; 1228 }, 1229 1230 /** 1231 * Highlight connection. 1232 * Note that highlight diseappears after the first update. 1233 * @return {Joint} Return this. 1234 */ 1235 highlight: function(color){ 1236 color = color || "red"; 1237 this.connection().attr("stroke", color); 1238 return this; 1239 }, 1240 1241 /** 1242 * Unhighlight connection. 1243 * @return {Joint} Return this. 1244 */ 1245 unhighlight: function(){ 1246 this.connection().attr("stroke", this._opt.attrs.stroke || "#000"); 1247 return this; 1248 } 1249 }; 1250 1251 /** 1252 * Reference to current joint when an object is dragging 1253 * can be global across all raphael 'worlds' because only one object can be dragged at a time. 1254 * @private 1255 * @type Joint 1256 */ 1257 Joint.currentJoint = null; 1258 1259 /** 1260 * Set a paper for graphics rendering. 1261 * @param {Raphael|number,number,number,number|string,number,number|HTMLElement} p 1262 * @return {Raphael} Paper. 1263 * @example 1264 * // create paper from existing HTMLElement with id "world" specifying width and height 1265 * Joint.paper("world", 640, 480); 1266 * // create paper specifying x, y position and width and height 1267 * Joint.paper(50, 50, 640, 480); 1268 * // paper is created from the HTMLElement with id "world" 1269 * Joint.paper(document.getElementById("world")); 1270 * // create paper using Raphael 1271 * Joint.paper(Raphael("world", 640, 480)); 1272 */ 1273 Joint.paper = function paper(){ 1274 var p = arguments[0]; 1275 if (p === undefined){ 1276 return this._paper; 1277 } 1278 this._paperArguments = arguments; // save for later reset 1279 if (!(p instanceof global.Raphael)){ 1280 return (this._paper = global.Raphael.apply(global, arguments)); 1281 } 1282 return (this._paper = p); 1283 }; 1284 1285 /** 1286 * Clear paper, reset again. 1287 * @example 1288 * // create paper from existing HTMLElement with id "world" specifying width and height 1289 * Joint.paper("world", 640, 480); 1290 * // ... draw objects, diagrams, etc. ... 1291 * Joint.resetPaper(); 1292 * // paper is clear and ready for next usage 1293 */ 1294 Joint.resetPaper = function resetPaper(){ 1295 if (!this._paper){ 1296 return; 1297 } 1298 var canvas = this._paper.canvas; 1299 canvas.parentNode.removeChild(canvas); 1300 Joint.paper.apply(Joint, this._paperArguments); 1301 }; 1302 1303 // get an arrow object 1304 Joint.getArrow = function(type, size, attrs){ 1305 if (!size){ 1306 size = 2; // default 1307 } 1308 var arrow = Joint.arrows[type](size); 1309 if (!arrow.attrs) arrow.attrs = {}; 1310 1311 if (attrs){ 1312 for (var key in attrs){ 1313 arrow.attrs[key] = attrs[key]; 1314 } 1315 } 1316 return arrow; 1317 }; 1318 1319 /** 1320 * This object contains predefined arrow types. Currently, there are only two types: none and basic. 1321 * These are considered general types and are suitable for most diagrams. Nevertheless, new arrows 1322 * can be easily added. See arrows.js plugin, which provides some fancier arrows. 1323 * The names can be used as startArrow|endArrow types. 1324 * @example circle.joint(rect, { startArrow: { type: basic, size: 5, attrs: ... } }); 1325 */ 1326 Joint.arrows = { 1327 none: function(size){ 1328 if (!size){ size = 2; } 1329 return { 1330 path: ["M",size.toString(),"0","L",(-size).toString(),"0"], 1331 dx: size, 1332 dy: size, 1333 attrs: { opacity: 0 } 1334 }; 1335 }, 1336 basic: function(size){ 1337 if (!size){ size = 5; } 1338 return { 1339 path: ["M",size.toString(),"0", 1340 "L",(-size).toString(),(-size).toString(), 1341 "L",(-size).toString(),size.toString(),"z"], 1342 dx: size, 1343 dy: size, 1344 attrs: { 1345 stroke: "black", 1346 fill: "black" 1347 } 1348 }; 1349 } 1350 }; 1351 1352 /** 1353 * Get an absolute position of an element. 1354 * @private 1355 * @return {Point} 1356 */ 1357 Joint.findPos = function(el){ 1358 var p = point(0, 0); 1359 if (el.offsetParent){ 1360 while (el){ 1361 p.offset(el.offsetLeft, el.offsetTop); 1362 el = el.offsetParent; 1363 } 1364 } else { 1365 // firefox (supposing el is Raphael canvas element) 1366 p.offset(el.parentNode.offsetLeft, el.parentNode.offsetTop); 1367 } 1368 return p; 1369 }; 1370 /** 1371 * Get the mouse position relative to the raphael paper. 1372 * @private 1373 * @param {Event} e Javascript event object 1374 * @param {Element} el DOM element 1375 * @return {Point} 1376 */ 1377 Joint.getMousePosition = function(e, el){ 1378 var pos; 1379 if (e.pageX || e.pageY) { 1380 pos = point(e.pageX, e.pageY); 1381 } else { 1382 var 1383 docEl = document.documentElement, 1384 docBody = document.body; 1385 pos = point(e.clientX + (docEl.scrollLeft || docBody.scrollLeft) - docEl.clientLeft, 1386 e.clientY + (docEl.scrollTop || docBody.scrollTop) - docEl.clientTop); 1387 } 1388 var rp = Joint.findPos(el); 1389 return point(pos.x - rp.x, pos.y - rp.y); 1390 }; 1391 /** 1392 * MouseMove event callback. 1393 * @private 1394 * @param {Event} e 1395 */ 1396 Joint.mouseMove = function(e){ 1397 if (Joint.currentJoint !== null){ 1398 var joint = Joint.currentJoint; 1399 if (joint.state === joint.STARTCAPDRAGGING || 1400 joint.state === joint.ENDCAPDRAGGING){ 1401 joint.capDragging(e); 1402 } else if (joint.state === joint.CONNECTIONWIRING){ 1403 joint.connectionWiring(e); 1404 } 1405 } 1406 }; 1407 /** 1408 * MouseUp event callback. 1409 * @private 1410 * @param {Event} e 1411 */ 1412 Joint.mouseUp = function(e){ 1413 if (Joint.currentJoint !== null){ 1414 var joint = Joint.currentJoint; 1415 if (joint.state === joint.STARTCAPDRAGGING || 1416 joint.state === joint.ENDCAPDRAGGING){ 1417 joint.capEndDragging(); 1418 } 1419 } 1420 Joint.currentJoint = null; 1421 }; 1422 1423 Joint.fixEvent = function(event) { 1424 // add W3C standard event methods 1425 event.preventDefault = Joint.fixEvent.preventDefault; 1426 event.stopPropagation = Joint.fixEvent.stopPropagation; 1427 return event; 1428 }; 1429 Joint.fixEvent.preventDefault = function() { 1430 this.returnValue = false; 1431 }; 1432 Joint.fixEvent.stopPropagation = function() { 1433 this.cancelBubble = true; 1434 }; 1435 Joint.handleEvent = function(event){ 1436 var returnValue = true; 1437 // grab the event object (IE uses a global event object) 1438 event = event || Joint.fixEvent(((global.ownerDocument || global.document || global).parentWindow || global).event); 1439 // get a reference to the hash table of event handlers 1440 var handlers = this.events[event.type]; 1441 // execute each event handler 1442 for (var i in handlers) { 1443 this.$$handleEvent = handlers[i]; 1444 if (this.$$handleEvent(event) === false) { 1445 returnValue = false; 1446 } 1447 } 1448 return returnValue; 1449 }; 1450 Joint.addEvent = function(element, type, handler){ 1451 if (element.addEventListener) { 1452 element.addEventListener(type, handler, false); 1453 } else { 1454 // assign each event handler a unique ID 1455 if (!handler.$$guid){ handler.$$guid = Joint.addEvent.guid++; } 1456 // create a hash table of event types for the element 1457 if (!element.events){ element.events = {}; } 1458 // create a hash table of event handlers for each element/event pair 1459 var handlers = element.events[type]; 1460 if (!handlers) { 1461 handlers = element.events[type] = {}; 1462 // store the existing event handler (if there is one) 1463 if (element["on" + type]) { 1464 handlers[0] = element["on" + type]; 1465 } 1466 } 1467 // store the event handler in the hash table 1468 handlers[handler.$$guid] = handler; 1469 // assign a global event handler to do all the work 1470 element["on" + type] = Joint.handleEvent; 1471 } 1472 }; 1473 // a counter used to create unique IDs 1474 Joint.addEvent.guid = 1; 1475 1476 Joint.removeEvent = function(element, type, handler){ 1477 if (element.removeEventListener) { 1478 element.removeEventListener(type, handler, false); 1479 } else { 1480 // delete the event handler from the hash table 1481 if (element.events && element.events[type]){ 1482 delete element.events[type][handler.$$guid]; 1483 } 1484 } 1485 }; 1486 1487 /* 1488 * @todo register handlers only if draggable caps 1489 * are allowed in options. Applications may not need it. 1490 */ 1491 Joint.addEvent(document, "mousemove", Joint.mouseMove); 1492 Joint.addEvent(document, "mouseup", Joint.mouseUp); 1493 1494 var JointDOMBuilder = { 1495 init: function(paper, opt, start, end, jointLocation, connectionPathCommands, labelLocation){ 1496 this.paper = paper; 1497 this.opt = opt; 1498 this.start = start; 1499 this.end = end; 1500 this.jointLocation = jointLocation; 1501 this.connectionPathCommands = connectionPathCommands; 1502 this.labelLocation = labelLocation; 1503 return this; 1504 }, 1505 dummy: function(startOrEnd, pos, opt){ 1506 startOrEnd.dummy = true; 1507 startOrEnd.shape = this.paper.circle(pos.x, pos.y, opt.radius).attr(opt.attrs); 1508 startOrEnd.shape.show(); 1509 return startOrEnd.shape; 1510 }, 1511 dummyStart: function(){ 1512 return this.dummy(this.start, this.jointLocation.start.bound, this.opt.dummy.start); 1513 }, 1514 dummyEnd: function(){ 1515 return this.dummy(this.end, this.jointLocation.end.bound, this.opt.dummy.end); 1516 }, 1517 handleStart: function(){ 1518 var opt = this.opt.handle.start; 1519 if (!opt.enabled) return undefined; 1520 var pos = this.jointLocation.start.bound; 1521 return this.paper.circle(pos.x, pos.y, opt.radius).attr(opt.attrs); 1522 }, 1523 handleEnd: function(){ 1524 var opt = this.opt.handle.end; 1525 if (!opt.enabled) return undefined; 1526 var pos = this.jointLocation.end.bound; 1527 return this.paper.circle(pos.x, pos.y, opt.radius).attr(opt.attrs); 1528 }, 1529 connection: function(){ 1530 var opt = this.opt, paths = [], 1531 con = this.paper.path(this.connectionPathCommands.join(" ")).attr(opt.attrs); 1532 if (opt.subConnectionAttrs) { 1533 var i = 0, l = opt.subConnectionAttrs.length, 1534 length = con.getTotalLength(); 1535 for (; i < l; i++) { 1536 var attrs = opt.subConnectionAttrs[i]; 1537 var from = attrs.from || 2, to = attrs.to || 1; 1538 from = from > length ? length : from; 1539 from = from < 0 ? length + from : from; 1540 from = from > 1 ? from : length * from; 1541 to = to > length ? length : to; 1542 to = to < 0 ? length + to : to; 1543 to = to > 1 ? to : length * to; 1544 var subPath = this.paper.path(con.getSubpath(from, to)).attr(attrs); 1545 subPath.node.style.cursor = opt.cursor; 1546 paths.push(subPath); 1547 } 1548 } 1549 con.node.style.cursor = opt.cursor; 1550 con.show(); 1551 return [con].concat(paths); 1552 }, 1553 label: function(){ 1554 if (this.opt.label === undefined) return undefined; 1555 var labels = isArray(this.opt.label) ? this.opt.label : [this.opt.label], 1556 attrs = this.opt.labelAttrs, 1557 len = labels.length, i = 0, components = []; 1558 1559 for (; i < len; i++) { 1560 var pos = this.labelLocation[i], 1561 labelText = this.paper.text(pos.x, pos.y + (attrs[i].offset || 0), labels[i]).attr(attrs[i]), 1562 bb = labelText.getBBox(), 1563 padding = attrs[i].padding || 0, 1564 labelBox = this.paper.rect(bb.x - padding, bb.y - padding + (attrs[i].offset || 0), bb.width + 2*padding, bb.height + 2*padding).attr(this.opt.labelBoxAttrs[i]); 1565 labelText.insertAfter(labelBox); 1566 components.push(labelText, labelBox) 1567 } 1568 return components; 1569 }, 1570 startCap: function(){ 1571 var opt = this.opt.arrow.start, 1572 startCap = this.paper.path(opt.path.join(" ")).attr(opt.attrs); 1573 startCap.translate(this.jointLocation.start.translate.x, 1574 this.jointLocation.start.translate.y); 1575 startCap.rotate(this.jointLocation.start.rotate); 1576 startCap.show(); 1577 return startCap; 1578 }, 1579 endCap: function(){ 1580 var opt = this.opt.arrow.end, 1581 endCap = this.paper.path(opt.path.join(" ")).attr(opt.attrs); 1582 endCap.translate(this.jointLocation.end.translate.x, 1583 this.jointLocation.end.translate.y); 1584 endCap.rotate(this.jointLocation.end.rotate); 1585 endCap.show(); 1586 return endCap; 1587 } 1588 }; 1589 1590 /** 1591 * Geometry-Primitives. 1592 */ 1593 1594 /** 1595 * Point object. 1596 * @constructor 1597 */ 1598 function Point(x, y){ 1599 var xy; 1600 if (y === undefined){ 1601 // from string 1602 xy = x.split(x.indexOf("@") === -1 ? " " : "@"); 1603 this.x = parseInt(xy[0], 10); 1604 this.y = parseInt(xy[1], 10); 1605 } else { 1606 this.x = x; 1607 this.y = y; 1608 } 1609 } 1610 function point(x, y){ return new Point(x, y); } 1611 1612 Point.prototype = { 1613 constructor: Point, 1614 _isPoint: true, 1615 1616 toString: function(){ return this.x + "@" + this.y; }, 1617 1618 deepCopy: function(){ return point(this.x, this.y); }, 1619 /** 1620 * If I lie outside rectangle r, return the nearest point on the boundary of rect r, 1621 * otherwise return me. 1622 * (see Squeak Smalltalk, Point>>adhereTo:) 1623 * @param {Rect} r 1624 * @return {Point} 1625 */ 1626 adhereToRect: function(r){ 1627 if (r.containsPoint(this)){ 1628 return this; 1629 } 1630 this.x = mmin(mmax(this.x, r.x), r.x + r.width); 1631 this.y = mmin(mmax(this.y, r.y), r.y + r.height); 1632 return this; 1633 }, 1634 1635 /** 1636 * Compute the angle between me and p and the x axis. 1637 * (cartesian-to-polar coordinates conversion) 1638 * @param {Point} p 1639 * @return {object} theta in degrees and radians 1640 */ 1641 theta: function(p){ 1642 var y = -(p.y - this.y), // invert the y-axis 1643 x = p.x - this.x, 1644 rad = atan2(y, x); 1645 if (rad < 0){ // correction for III. and IV. quadrant 1646 rad = 2*PI + rad; 1647 } 1648 return { 1649 degrees: 180*rad / PI, 1650 radians: rad 1651 }; 1652 }, 1653 1654 /** 1655 * @return {number} distance between me and point p 1656 */ 1657 distance: function(p){ 1658 return line(this, p).length(); 1659 }, 1660 1661 /** 1662 * Offset me by the specified amount. 1663 */ 1664 offset: function(dx, dy){ 1665 this.x += dx; 1666 this.y += dy; 1667 return this; 1668 }, 1669 1670 /** 1671 * Scale the line segment between (0,0) and me to have a length of len 1672 */ 1673 normalize: function(len){ 1674 var s = len / sqrt((this.x*this.x) + (this.y*this.y)); 1675 this.x = s * this.x; 1676 this.y = s * this.y; 1677 return this; 1678 } 1679 }; 1680 1681 /** 1682 * Alternative constructor, from polar coordinates. 1683 */ 1684 Point.fromPolar = function(r, angle){ 1685 return point(r * cos(angle), r * sin(angle)); 1686 }; 1687 1688 1689 /** 1690 * Line object. 1691 */ 1692 function Line(p1, p2){ 1693 this.start = p1; 1694 this.end = p2; 1695 } 1696 1697 function line(p1, p2) { return new Line(p1, p2); } 1698 1699 Line.prototype = { 1700 constructor: Line, 1701 1702 toString: function(){ 1703 return "start: " + this.start.toString() + " end:" + this.end.toString(); 1704 }, 1705 1706 /** 1707 * @return {double} length of the line 1708 */ 1709 length: function(){ return sqrt(this.squaredLength()); }, 1710 1711 /** 1712 * @return {integer} length without sqrt 1713 * @note for applications where the exact length is not necessary (e.g. compare only) 1714 */ 1715 squaredLength: function(){ 1716 var 1717 x0 = this.start.x, y0 = this.start.y, 1718 x1 = this.end.x, y1 = this.end.y; 1719 return (x0 -= x1)*x0 + (y0 -= y1)*y0; 1720 }, 1721 1722 /** 1723 * @return {point} my midpoint 1724 */ 1725 midpoint: function(){ 1726 return point((this.start.x + this.end.x) / 2, 1727 (this.start.y + this.end.y) / 2); 1728 }, 1729 1730 1731 /** 1732 * @return {point} where I intersect l. 1733 * @see Squeak Smalltalk, LineSegment>>intersectionWith: 1734 */ 1735 intersection: function(l){ 1736 var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y), 1737 pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y), 1738 det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x), 1739 deltaPt = point(l.start.x - this.start.x, l.start.y - this.start.y), 1740 alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x), 1741 beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x); 1742 1743 if (det === 0 || 1744 alpha * det < 0 || 1745 beta * det < 0){ 1746 return null; // no intersection 1747 } 1748 1749 if (det > 0){ 1750 if (alpha > det || beta > det){ 1751 return null; 1752 } 1753 } else { 1754 if (alpha < det || beta < det){ 1755 return null; 1756 } 1757 } 1758 return point(this.start.x + (alpha * pt1Dir.x / det), 1759 this.start.y + (alpha * pt1Dir.y / det)); 1760 } 1761 }; 1762 1763 /** 1764 * Rectangle object. 1765 */ 1766 function Rect(o){ 1767 this.x = o.x; 1768 this.y = o.y; 1769 this.width = o.width; 1770 this.height = o.height; 1771 } 1772 1773 function rect(o){ 1774 if (typeof o.width === "undefined"){ 1775 return new Rect({x: arguments[0], 1776 y: arguments[1], 1777 width: arguments[2], 1778 height: arguments[3]}); 1779 } 1780 return new Rect(o); 1781 } 1782 1783 Rect.prototype = { 1784 constructor: Rect, 1785 1786 toString: function(){ 1787 return "origin: " + this.origin().toString() + " corner: " + this.corner().toString(); 1788 }, 1789 1790 origin: function(){ return point(this.x, this.y); }, 1791 corner: function(){ return point(this.x + this.width, this.y + this.height); }, 1792 topRight: function(){ return point(this.x + this.width, this.y); }, 1793 bottomLeft: function(){ return point(this.x, this.y + this.height); }, 1794 center: function(){ return point(this.x + this.width/2, this.y + this.height/2); }, 1795 1796 /** 1797 * @return {boolean} true if rectangles intersect 1798 */ 1799 intersect: function(r){ 1800 var myOrigin = this.origin(), 1801 myCorner = this.corner(), 1802 rOrigin = r.origin(), 1803 rCorner = r.corner(); 1804 if (rCorner.x <= myOrigin.x){ return false; } 1805 if (rCorner.y <= myOrigin.y){ return false; } 1806 if (rOrigin.x >= myCorner.x){ return false; } 1807 if (rOrigin.y >= myCorner.y){ return false; } 1808 return true; 1809 }, 1810 1811 /** 1812 * @return {string} (left|right|top|bottom) side which is nearest to point 1813 * @see Squeak Smalltalk, Rectangle>>sideNearestTo: 1814 */ 1815 sideNearestToPoint: function(p){ 1816 var distToLeft = p.x - this.x, 1817 distToRight = (this.x + this.width) - p.x, 1818 distToTop = p.y - this.y, 1819 distToBottom = (this.y + this.height) - p.y, 1820 closest = distToLeft, 1821 side = "left"; 1822 if (distToRight < closest){ 1823 closest = distToRight; 1824 side = "right"; 1825 } 1826 if (distToTop < closest){ 1827 closest = distToTop; 1828 side = "top"; 1829 } 1830 if (distToBottom < closest){ 1831 closest = distToBottom; 1832 side = "bottom"; 1833 } 1834 return side; 1835 }, 1836 1837 /** 1838 * @return {bool} true if point p is insight me 1839 */ 1840 containsPoint: function(p){ 1841 if (p.x > this.x && p.x < this.x + this.width && 1842 p.y > this.y && p.y < this.y + this.height){ 1843 return true; 1844 } 1845 return false; 1846 }, 1847 1848 /** 1849 * @return {point} a point on my border nearest to parameter point 1850 * @see Squeak Smalltalk, Rectangle>>pointNearestTo: 1851 */ 1852 pointNearestToPoint: function(p){ 1853 if (this.containsPoint(p)){ 1854 var side = this.sideNearestToPoint(p); 1855 switch (side){ 1856 case "right": return point(this.x + this.width, p.y); 1857 case "left": return point(this.x, p.y); 1858 case "bottom": return point(p.x, this.y + this.height); 1859 case "top": return point(p.x, this.y); 1860 } 1861 } else { 1862 return p.adhereToRect(this); 1863 } 1864 }, 1865 1866 /** 1867 * Find point on me where line starting 1868 * from my center ending in point p intersects my boundary. 1869 */ 1870 boundPoint: function(p){ 1871 var center = point(this.x + this.width/2, this.y + this.height/2); 1872 // (clockwise, starting from the top side) 1873 var sides = [ 1874 line(this.origin(), this.topRight()), 1875 line(this.topRight(), this.corner()), 1876 line(this.corner(), this.bottomLeft()), 1877 line(this.bottomLeft(), this.origin()) 1878 ], 1879 connector = line(center, p); 1880 for (var i = sides.length - 1; i >= 0; --i){ 1881 var intersection = sides[i].intersection(connector); 1882 if (intersection !== null){ 1883 return intersection; 1884 } 1885 } 1886 // assert(false) 1887 }, 1888 1889 /** 1890 * Move and expand me. 1891 * @param r {rectangle} representing deltas 1892 */ 1893 moveAndExpand: function(r){ 1894 this.x += r.x; 1895 this.y += r.y; 1896 this.width += r.width; 1897 this.height += r.height; 1898 return this; 1899 } 1900 }; 1901 1902 /** 1903 * Ellipse object. 1904 */ 1905 function Ellipse(c, a, b){ 1906 this.x = c.x; 1907 this.y = c.y; 1908 this.a = a; 1909 this.b = b; 1910 } 1911 1912 function ellipse(c, a, b){ return new Ellipse(c, a, b); } 1913 1914 Ellipse.prototype = { 1915 constructor: Ellipse, 1916 1917 bbox: function(){ 1918 return rect({x: this.x - this.a, y: this.y - this.b, width: 2*this.a, height: 2*this.b}); 1919 }, 1920 1921 /** 1922 * Find point on me where line from my center to 1923 * point p intersects my boundary. 1924 * @see Squeak Smalltalk, EllipseMorph>>intersectionWithLineSegmentFromCenterTo: 1925 */ 1926 intersectionWithLineFromCenterToPoint: function(p){ 1927 var dx = p.x - this.x, 1928 dy = p.y - this.y; 1929 if (dx === 0){ 1930 return this.bbox().pointNearestToPoint(p); 1931 } 1932 1933 var m = dy / dx, 1934 mSquared = m * m, 1935 aSquared = this.a * this.a, 1936 bSquared = this.b * this.b, 1937 x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared))); 1938 if (dx < 0){ 1939 x = -x; 1940 } 1941 var y = m * x; 1942 return point(this.x + x, this.y + y); 1943 } 1944 1945 }; 1946 1947 /** 1948 * Bezier segment object. 1949 */ 1950 function BezierSegment(p0, p1, p2, p3){ 1951 this.p0 = p0; 1952 this.p1 = p1; 1953 this.p2 = p2; 1954 this.p3 = p3; 1955 } 1956 1957 function bezierSegment(p0, p1, p2, p3){ 1958 return new BezierSegment(p0, p1, p2, p3); 1959 } 1960 1961 BezierSegment.prototype = { 1962 constructor: BezierSegment, 1963 1964 /** 1965 * Get a point on me at the specified time t. 1966 */ 1967 getPoint: function(t){ 1968 var 1969 a = 1 - t, // (1 - t) 1970 b = a*a, // (1 - t)^2 1971 c = b*a, // (1 - t)^3 1972 tt = t*t, // t^2 1973 ttt = tt*t; // t^3 1974 1975 return point(c*this.p0.x + 3*b*t*this.p1.x + 3*a*tt*this.p2.x + ttt*this.p3.x, 1976 c*this.p0.y + 3*b*t*this.p1.y + 3*a*tt*this.p2.y + ttt*this.p3.y); 1977 } 1978 1979 }; 1980 1981 /** 1982 * Various methods for Bezier curves manipulation. 1983 */ 1984 function Bezier(){} 1985 1986 /** 1987 * Cubic Bezier curve path through points. 1988 * Ported from ActionScript implementation by Andy Woodruff (http://cartogrammar.com/blog) 1989 */ 1990 Bezier.curveThroughPoints = function(points, z, angleFactor){ 1991 // default values 1992 if (typeof z === "undefined"){ 1993 z = 0.5; 1994 } 1995 if (typeof angleFactor === "undefined"){ 1996 angleFactor = 0.75; 1997 } 1998 1999 var path = []; // the result SVG path as an array of path commands 2000 if (points.length < 2){ 2001 throw new Error("Points array must have minimum of two points."); 2002 } 2003 2004 var p = [points[0]]; 2005 // remove duplicate neighbours 2006 for (var i = 1, len = points.length; i < len; i++){ 2007 if (points[i].x != points[i-1].x || points[i].y != points[i-1].y){ 2008 p.push(points[i]); 2009 } 2010 } 2011 2012 // z is_in (0,1] 2013 if (z <= 0){ 2014 z = 0.5; 2015 } else if (z > 1){ 2016 z = 1; 2017 } 2018 2019 // angleFactor is_in [0,1] 2020 if (angleFactor < 0){ 2021 angleFactor = 0; 2022 } else if (angleFactor > 1){ 2023 angleFactor = 1; 2024 } 2025 2026 /** 2027 * Calculate all the curve control points. 2028 */ 2029 2030 // None of this junk will do any good if there are only two points 2031 if (p.length > 2){ 2032 // Ordinarily, curve calculations will start with the second point 2033 // and go through the second-to-last point 2034 var firstPt = 1; 2035 var lastPt = p.length-1; 2036 // Check if this is a closed line 2037 if (p[0].x == p[lastPt].x && p[0].y == p[lastPt].y){ 2038 // Include first and last points in curve calculations 2039 firstPt = 0; 2040 lastPt = p.length; 2041 } 2042 2043 // An array to store the two control points for each point 2044 var controlPts = []; 2045 // Loop through all the points (except the first and last 2046 // if not a closed line) to get curve control points for each. 2047 for (var i = firstPt; i < lastPt; i++) { 2048 // The previous, current, and next points 2049 2050 // If the first point (of a closed line), use the 2051 // second-to-last point as the previous point 2052 var p0 = (i-1 < 0) ? p[p.length-2] : p[i-1]; 2053 var p1 = p[i]; 2054 // If the last point (of a closed line), use the 2055 // second point as the next point 2056 var p2 = (i+1 == p.length) ? p[1] : p[i+1]; 2057 2058 // Distance from previous point to current point 2059 var a = p0.distance(p1); 2060 // Correct for near-zero distances, a cheap way to prevent 2061 // division by zero 2062 if (a < 0.001){ a = 0.001; } 2063 // Distance from current point to next point 2064 var b = p1.distance(p2); 2065 if (b < 0.001){ b = 0.001; } 2066 // Distance from previous point to next point 2067 var c = p0.distance(p2); 2068 if (c < 0.001){ c = 0.001; } 2069 var cos = (b*b+a*a-c*c)/(2*b*a); 2070 // Make sure above value is between -1 and 1 so that acos will work 2071 if (cos < -1){ cos = -1; } 2072 else if (cos > 1){ cos = 1; } 2073 // Angle formed by the two sides of the triangle 2074 // (described by the three points above) adjacent to the current point 2075 var C = acos(cos); 2076 // Duplicate set of points. Start by giving previous and next points 2077 // values RELATIVE to the current point. 2078 var aPt = point(p0.x-p1.x,p0.y-p1.y); 2079 var bPt = point(p1.x,p1.y); 2080 var cPt = point(p2.x-p1.x,p2.y-p1.y); 2081 2082 /* We'll be adding adding the vectors from the previous and next points 2083 to the current point, but we don't want differing magnitudes (i.e. 2084 line segment lengths) to affect the direction of the new vector. 2085 Therefore we make sure the segments we use, based on the duplicate points 2086 created above, are of equal length. The angle of the new vector will 2087 thus bisect angle C (defined above) and the perpendicular to this is 2088 nice for the line tangent to the curve. The curve control points will 2089 be along that tangent line. 2090 */ 2091 if (a > b){ 2092 // Scale the segment to aPt (bPt to aPt) to the size of b 2093 // (bPt to cPt) if b is shorter. 2094 aPt.normalize(b); 2095 } else if (b > a){ 2096 // Scale the segment to cPt (bPt to cPt) to the size of a (aPt to bPt) 2097 // if a is shorter. 2098 cPt.normalize(a); 2099 } 2100 // Offset aPt and cPt by the current point to get them back to 2101 // their absolute position. 2102 aPt.offset(p1.x,p1.y); 2103 cPt.offset(p1.x,p1.y); 2104 2105 // Get the sum of the two vectors, which is perpendicular to the line 2106 // along which our curve control points will lie. 2107 2108 // x component of the segment from previous to current point 2109 var ax = bPt.x-aPt.x; 2110 var ay = bPt.y-aPt.y; 2111 // x component of the segment from next to current point 2112 var bx = bPt.x-cPt.x; 2113 var by = bPt.y-cPt.y; 2114 // sum of x components 2115 var rx = ax + bx; 2116 var ry = ay + by; 2117 // Correct for three points in a line by finding the angle between just two of them 2118 if (rx === 0 && ry === 0){ 2119 // Really not sure why this seems to have to be negative 2120 rx = -bx; 2121 ry = by; 2122 } 2123 // Switch rx and ry when y or x difference is 0. This seems to prevent 2124 // the angle from being perpendicular to what it should be. 2125 if (ay === 0 && by === 0){ 2126 rx = 0; 2127 ry = 1; 2128 } else if (ax === 0 && bx === 0){ 2129 rx = 1; 2130 ry = 0; 2131 } 2132 // length of the summed vector - not being used, but there it is anyway 2133 // var r = sqrt(rx*rx+ry*ry); 2134 // angle of the new vector 2135 var theta = atan2(ry,rx); 2136 // Distance of curve control points from current point: a fraction 2137 // the length of the shorter adjacent triangle side 2138 var controlDist = mmin(a,b)*z; 2139 // Scale the distance based on the acuteness of the angle. Prevents 2140 // big loops around long, sharp-angled triangles. 2141 var controlScaleFactor = C/PI; 2142 // Mess with this for some fine-tuning 2143 controlDist *= ((1-angleFactor) + angleFactor*controlScaleFactor); 2144 // The angle from the current point to control points: 2145 // the new vector angle plus 90 degrees (tangent to the curve). 2146 var controlAngle = theta+PI/2; 2147 // Control point 2, curving to the next point. 2148 var controlPoint2 = Point.fromPolar(controlDist,controlAngle); 2149 // Control point 1, curving from the previous point 2150 // (180 degrees away from control point 2). 2151 var controlPoint1 = Point.fromPolar(controlDist,controlAngle+PI); 2152 2153 // Offset control points to put them in the correct absolute position 2154 controlPoint1.offset(p1.x,p1.y); 2155 controlPoint2.offset(p1.x,p1.y); 2156 2157 /* Haven't quite worked out how this happens, but some control 2158 points will be reversed. In this case controlPoint2 will be 2159 farther from the next point than controlPoint1 is. 2160 Check for that and switch them if it's true. 2161 */ 2162 if (controlPoint2.distance(p2) > controlPoint1.distance(p2)){ 2163 // Add the two control points to the array in reverse order 2164 controlPts[i] = [controlPoint2,controlPoint1]; 2165 } else { 2166 // Otherwise add the two control points to the array in normal order 2167 controlPts[i] = [controlPoint1,controlPoint2]; 2168 } 2169 }//endfor (var i = firstPt; i < lastPt; i++) { 2170 2171 // DRAW THE CURVE 2172 2173 path.push("M", p[0].x, p[0].y); 2174 // console.log(controlPts); 2175 2176 // If this isn't a closed line 2177 if (firstPt == 1){ 2178 // Draw a regular quadratic Bézier curve from the first to second points, 2179 // using the first control point of the second point 2180 path.push("S", controlPts[1][0].x,controlPts[1][0].y,p[1].x,p[1].y); 2181 } 2182 2183 // Change to true if you want to use lineTo for straight lines of 3 or 2184 // more points rather than curves. You'll get straight lines but possible sharp corners! 2185 var straightLines = true; 2186 // Loop through points to draw cubic Bézier curves through the penultimate 2187 // point, or through the last point if the line is closed. 2188 for (var i = firstPt; i < lastPt - 1; i++){ 2189 // Determine if multiple points in a row are in a straight line 2190 var isStraight = false; 2191 if ( ( i > 0 && atan2(p[i].y-p[i-1].y,p[i].x-p[i-1].x) == atan2(p[i+1].y-p[i].y,p[i+1].x-p[i].x) )|| ( i < p.length - 2 && atan2(p[i+2].y-p[i+1].y,p[i+2].x-p[i+1].x) == atan2(p[i+1].y-p[i].y,p[i+1].x-p[i].x) ) ){ 2192 isStraight = true; 2193 } 2194 2195 if (straightLines && isStraight){ 2196 path.push("L", p[i+1].x,p[i+1].y); 2197 } else { 2198 // BezierSegment instance using the current point, its second control 2199 // point, the next point's first control point, and the next point 2200 var bezier = bezierSegment(p[i],controlPts[i][1],controlPts[i+1][0],p[i+1]); 2201 // Construct the curve out of 100 segments (adjust number for less/more detail) 2202 for (var t = 0.01; t < 1.01; t += 0.01){ 2203 // x,y on the curve for a given t 2204 var val = bezier.getPoint(t); 2205 path.push("L", val.x, val.y); 2206 } 2207 } 2208 } 2209 // If this isn't a closed line 2210 if (lastPt == p.length-1){ 2211 // Curve to the last point using the second control point of the penultimate point. 2212 path.push("S", controlPts[i][1].x,controlPts[i][1].y,p[i+1].x,p[i+1].y); 2213 } 2214 2215 // just draw a line if only two points 2216 } else if (p.length == 2){ 2217 path.push("M", p[0].x,p[0].y); 2218 path.push("L", p[1].x,p[1].y); 2219 } 2220 return path; 2221 }; 2222 2223 Joint.Point = Point; 2224 Joint.point = point; 2225 Joint.Rect = Rect; 2226 Joint.rect = rect; 2227 Joint.Line = Line; 2228 Joint.line = line; 2229 Joint.Ellipse = Ellipse; 2230 Joint.ellipse = ellipse; 2231 Joint.BezierSegment = BezierSegment; 2232 Joint.bezierSegment = bezierSegment; 2233 Joint.Bezier = Bezier; 2234 Joint.Mixin = Mixin; 2235 Joint.Supplement = Supplement; 2236 Joint.DeepMixin = DeepMixin; 2237 Joint.DeepSupplement = DeepSupplement; 2238 2239 /** 2240 * TODO: rotation support. there is a problem because 2241 * rotation does not set any attribute in this.attrs but 2242 * instead it sets transformation directly to let the browser 2243 * SVG engine compute the position. 2244 */ 2245 var _attr = global.Raphael.el.attr; 2246 global.Raphael.el.attr = function(){ 2247 // is it a getter or el is not a joint object? 2248 if ((arguments.length == 1 && 2249 (typeof arguments[0] === "string" || typeof arguments[0] === "array")) || 2250 (typeof this.joints === "undefined")){ 2251 return _attr.apply(this, arguments); // yes 2252 } 2253 2254 // old attributes 2255 var o = {}; 2256 for (var key in this.attrs){ 2257 o[key] = this.attrs[key]; 2258 } 2259 2260 _attr.apply(this, arguments); 2261 2262 var 2263 n = this.attrs, // new attributes 2264 positionChanged = false, 2265 strokeChanged = false; 2266 2267 if (o.x != n.x || o.y != n.y || // rect/image/text 2268 o.cx != n.cx || o.cy != n.cy || // circle/ellipse 2269 o.path != n.path || // path 2270 o.r != n.r){ // radius 2271 positionChanged = true; 2272 } 2273 if (o.stroke != n.stroke){ 2274 strokeChanged = true; 2275 } 2276 2277 for (var i = this.joints().length - 1; i >= 0; --i){ 2278 var joint = this.joints()[i]; 2279 2280 if (positionChanged){ 2281 joint.update(); 2282 joint.callback("objectMoving", joint, [this]); 2283 } 2284 //if (strokeChanged){} 2285 } 2286 return this; 2287 }; 2288 2289 2290 /** 2291 * Create a joint between a Raphael object and to object. 2292 * @param {RaphaelObject} to 2293 * @param {object} [opts] opt {@link Joint} 2294 * @return {Joint} 2295 */ 2296 global.Raphael.el.joint = function(to, opt){ 2297 Joint.paper(this.paper); 2298 return new Joint(this, to, opt); 2299 }; 2300 2301 /** 2302 * Return element unique id. 2303 */ 2304 global.Raphael.el.euid = function(){ 2305 return Joint.generateEuid.call(this); 2306 }; 2307 2308 global.Raphael.el.yourself = function(){ 2309 return this; 2310 }; 2311 2312 global.Raphael.el.joints = function(){ 2313 return (this._joints || (this._joints = [])); 2314 }; 2315 2316 global.Raphael.fn.euid = function(){ 2317 return Joint.generateEuid.call(this); 2318 }; 2319 2320 })(this); // END CLOSURE 2321