1 /* 2 Copyright 2008-2015 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Alfred Wassermann 7 8 This file is part of JSXGraph. 9 10 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 11 12 You can redistribute it and/or modify it under the terms of the 13 14 * GNU Lesser General Public License as published by 15 the Free Software Foundation, either version 3 of the License, or 16 (at your option) any later version 17 OR 18 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 19 20 JSXGraph is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU Lesser General Public License for more details. 24 25 You should have received a copy of the GNU Lesser General Public License and 26 the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/> 27 and <http://opensource.org/licenses/MIT/>. 28 */ 29 30 31 /*global JXG: true, define: true, console: true, window: true*/ 32 /*jslint nomen: true, plusplus: true*/ 33 34 /* depends: 35 jxg 36 options 37 math/math 38 math/geometry 39 math/numerics 40 base/coords 41 base/constants 42 base/element 43 parser/geonext 44 utils/type 45 elements: 46 transform 47 */ 48 49 /** 50 * @fileoverview The geometry object CoordsElement is defined in this file. 51 * This object provides the coordinate handling of points, images and texts. 52 */ 53 54 define([ 55 'jxg', 'options', 'math/math', 'math/geometry', 'math/numerics', 'math/statistics', 'base/coords', 'base/constants', 'base/element', 56 'parser/geonext', 'utils/type', 'base/transformation' 57 ], function (JXG, Options, Mat, Geometry, Numerics, Statistics, Coords, Const, GeometryElement, GeonextParser, Type, Transform) { 58 59 "use strict"; 60 61 /** 62 * An element containing coords is the basic geometric element. Based on points lines and circles can be constructed which can be intersected 63 * which in turn are points again which can be used to construct new lines, circles, polygons, etc. This class holds methods for 64 * all kind of coordinate elements like points, texts and images. 65 * @class Creates a new coords element object. Do not use this constructor to create an element. 66 * 67 * @private 68 * @augments JXG.GeometryElement 69 * @param {Array} coordinates An array with the affine user coordinates of the point. 70 * {@link JXG.Options#elements}, and - optionally - a name and an id. 71 */ 72 JXG.CoordsElement = function (coordinates, isLabel) { 73 var i; 74 75 if (!Type.exists(coordinates)) { 76 coordinates = [1, 0, 0]; 77 } 78 79 for (i = 0; i < coordinates.length; ++i) { 80 coordinates[i] = parseFloat(coordinates[i]); 81 } 82 83 /** 84 * Coordinates of the element. 85 * @type JXG.Coords 86 * @private 87 */ 88 this.coords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 89 this.initialCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 90 91 /** 92 * Relative position on a slide element (line, circle, curve) if element is a glider on this element. 93 * @type Number 94 * @private 95 */ 96 this.position = null; 97 98 /** 99 * Determines whether the element slides on a polygon if point is a glider. 100 * @type boolean 101 * @default false 102 * @private 103 */ 104 this.onPolygon = false; 105 106 /** 107 * When used as a glider this member stores the object, where to glide on. 108 * To set the object to glide on use the method 109 * {@link JXG.Point#makeGlider} and DO NOT set this property directly 110 * as it will break the dependency tree. 111 * @type JXG.GeometryElement 112 * @name Glider#slideObject 113 */ 114 this.slideObject = null; 115 116 /** 117 * List of elements the element is bound to, i.e. the element glides on. 118 * Only the last entry is active. 119 * Use {@link JXG.Point#popSlideObject} to remove the currently active slideObject. 120 */ 121 this.slideObjects = []; 122 123 /** 124 * A {@link JXG.CoordsElement#updateGlider} call is usually followed 125 * by a general {@link JXG.Board#update} which calls 126 * {@link JXG.CoordsElement#updateGliderFromParent}. 127 * To prevent double updates, {@link JXG.CoordsElement#needsUpdateFromParent} 128 * is set to false in updateGlider() and reset to true in the following call to 129 * {@link JXG.CoordsElement#updateGliderFromParent} 130 * @type {Boolean} 131 */ 132 this.needsUpdateFromParent = true; 133 134 /** 135 * Dummy function for unconstrained points or gliders. 136 * @private 137 */ 138 this.updateConstraint = function () { 139 return this; 140 }; 141 142 /* 143 * Do we need this? 144 */ 145 this.Xjc = null; 146 this.Yjc = null; 147 148 // documented in GeometryElement 149 this.methodMap = Type.deepCopy(this.methodMap, { 150 move: 'moveTo', 151 moveTo: 'moveTo', 152 moveAlong: 'moveAlong', 153 visit: 'visit', 154 glide: 'makeGlider', 155 makeGlider: 'makeGlider', 156 intersect: 'makeIntersection', 157 makeIntersection: 'makeIntersection', 158 X: 'X', 159 Y: 'Y', 160 free: 'free', 161 setPosition: 'setGliderPosition', 162 setGliderPosition: 'setGliderPosition', 163 addConstraint: 'addConstraint', 164 dist: 'Dist', 165 onPolygon: 'onPolygon' 166 }); 167 168 /** 169 * Stores the groups of this element in an array of Group. 170 * @type array 171 * @see JXG.Group 172 * @private 173 */ 174 this.group = []; 175 176 /* 177 * this.element may have been set by the object constructor. 178 */ 179 if (Type.exists(this.element)) { 180 this.addAnchor(coordinates, isLabel); 181 } 182 this.isDraggable = true; 183 184 }; 185 186 JXG.extend(JXG.CoordsElement.prototype, /** @lends JXG.CoordsElement.prototype */ { 187 /** 188 * Updates the coordinates of the element. 189 * @private 190 */ 191 updateCoords: function (fromParent) { 192 if (!this.needsUpdate) { 193 return this; 194 } 195 196 if (!Type.exists(fromParent)) { 197 fromParent = false; 198 } 199 200 /* 201 * We need to calculate the new coordinates no matter of the elements visibility because 202 * a child could be visible and depend on the coordinates of the element/point (e.g. perpendicular). 203 * 204 * Check if the element is a glider and calculate new coords in dependency of this.slideObject. 205 * This function is called with fromParent==true in case it is a glider element for example if 206 * the defining elements of the line or circle have been changed. 207 */ 208 if (this.type === Const.OBJECT_TYPE_GLIDER) { 209 if (fromParent) { 210 this.updateGliderFromParent(); 211 } else { 212 this.updateGlider(); 213 } 214 } 215 216 if (!this.visProp.frozen) { 217 this.updateConstraint(); 218 } 219 this.updateTransform(); 220 221 return this; 222 }, 223 224 /** 225 * Update of glider in case of dragging the glider or setting the postion of the glider. 226 * The relative position of the glider has to be updated. 227 * 228 * In case of a glider on a line: 229 * If the second point is an ideal point, then -1 < this.position < 1, 230 * this.position==+/-1 equals point2, this.position==0 equals point1 231 * 232 * If the first point is an ideal point, then 0 < this.position < 2 233 * this.position==0 or 2 equals point1, this.position==1 equals point2 234 * 235 * @private 236 */ 237 updateGlider: function () { 238 var i, p1c, p2c, d, v, poly, cc, pos, sgn, 239 alpha, beta, 240 delta = 2.0 * Math.PI, 241 angle, 242 cp, c, invMat, newCoords, newPos, 243 doRound = false, 244 slide = this.slideObject; 245 246 this.needsUpdateFromParent = false; 247 248 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 249 if (this.visProp.isgeonext) { 250 delta = 1.0; 251 } 252 //this.coords.setCoordinates(Const.COORDS_BY_USER, 253 // Geometry.projectPointToCircle(this, slide, this.board).usrCoords, false); 254 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 255 newPos = Geometry.rad([slide.center.X() + 1.0, slide.center.Y()], slide.center, this) / delta; 256 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 257 /* 258 * onPolygon==true: the point is a slider on a segment and this segment is one of the 259 * "borders" of a polygon. 260 * This is a GEONExT feature. 261 */ 262 if (this.onPolygon) { 263 p1c = slide.point1.coords.usrCoords; 264 p2c = slide.point2.coords.usrCoords; 265 i = 1; 266 d = p2c[i] - p1c[i]; 267 268 if (Math.abs(d) < Mat.eps) { 269 i = 2; 270 d = p2c[i] - p1c[i]; 271 } 272 273 cc = Geometry.projectPointToLine(this, slide, this.board); 274 pos = (cc.usrCoords[i] - p1c[i]) / d; 275 poly = slide.parentPolygon; 276 277 if (pos < 0) { 278 for (i = 0; i < poly.borders.length; i++) { 279 if (slide === poly.borders[i]) { 280 slide = poly.borders[(i - 1 + poly.borders.length) % poly.borders.length]; 281 break; 282 } 283 } 284 } else if (pos > 1.0) { 285 for (i = 0; i < poly.borders.length; i++) { 286 if (slide === poly.borders[i]) { 287 slide = poly.borders[(i + 1 + poly.borders.length) % poly.borders.length]; 288 break; 289 } 290 } 291 } 292 293 // If the slide object has changed, save the change to the glider. 294 if (slide.id !== this.slideObject.id) { 295 this.slideObject = slide; 296 } 297 } 298 299 p1c = slide.point1.coords; 300 p2c = slide.point2.coords; 301 302 // Distance between the two defining points 303 d = p1c.distance(Const.COORDS_BY_USER, p2c); 304 305 // The defining points are identical 306 if (d < Mat.eps) { 307 //this.coords.setCoordinates(Const.COORDS_BY_USER, p1c); 308 newCoords = p1c; 309 doRound = true; 310 newPos = 0.0; 311 } else { 312 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToLine(this, slide, this.board).usrCoords, false); 313 newCoords = Geometry.projectPointToLine(this, slide, this.board); 314 p1c = p1c.usrCoords.slice(0); 315 p2c = p2c.usrCoords.slice(0); 316 317 // The second point is an ideal point 318 if (Math.abs(p2c[0]) < Mat.eps) { 319 i = 1; 320 d = p2c[i]; 321 322 if (Math.abs(d) < Mat.eps) { 323 i = 2; 324 d = p2c[i]; 325 } 326 327 d = (newCoords.usrCoords[i] - p1c[i]) / d; 328 sgn = (d >= 0) ? 1 : -1; 329 d = Math.abs(d); 330 newPos = sgn * d / (d + 1); 331 332 // The first point is an ideal point 333 } else if (Math.abs(p1c[0]) < Mat.eps) { 334 i = 1; 335 d = p1c[i]; 336 337 if (Math.abs(d) < Mat.eps) { 338 i = 2; 339 d = p1c[i]; 340 } 341 342 d = (newCoords.usrCoords[i] - p2c[i]) / d; 343 344 // 1.0 - d/(1-d); 345 if (d < 0.0) { 346 newPos = (1 - 2.0 * d) / (1.0 - d); 347 } else { 348 newPos = 1 / (d + 1); 349 } 350 } else { 351 i = 1; 352 d = p2c[i] - p1c[i]; 353 354 if (Math.abs(d) < Mat.eps) { 355 i = 2; 356 d = p2c[i] - p1c[i]; 357 } 358 newPos = (newCoords.usrCoords[i] - p1c[i]) / d; 359 } 360 } 361 362 // Snap the glider point of the slider into its appropiate position 363 // First, recalculate the new value of this.position 364 // Second, call update(fromParent==true) to make the positioning snappier. 365 if (this.visProp.snapwidth > 0.0 && Math.abs(this._smax - this._smin) >= Mat.eps) { 366 newPos = Math.max(Math.min(newPos, 1), 0); 367 368 v = newPos * (this._smax - this._smin) + this._smin; 369 v = Math.round(v / this.visProp.snapwidth) * this.visProp.snapwidth; 370 newPos = (v - this._smin) / (this._smax - this._smin); 371 this.update(true); 372 } 373 374 p1c = slide.point1.coords; 375 if (!slide.visProp.straightfirst && Math.abs(p1c.usrCoords[0]) > Mat.eps && newPos < 0) { 376 //this.coords.setCoordinates(Const.COORDS_BY_USER, p1c); 377 newCoords = p1c; 378 doRound = true; 379 newPos = 0; 380 } 381 382 p2c = slide.point2.coords; 383 if (!slide.visProp.straightlast && Math.abs(p2c.usrCoords[0]) > Mat.eps && newPos > 1) { 384 //this.coords.setCoordinates(Const.COORDS_BY_USER, p2c); 385 newCoords = p2c; 386 doRound = true; 387 newPos = 1; 388 } 389 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 390 // In case, the point is a constrained glider. 391 // side-effect: this.position is overwritten 392 this.updateConstraint(); 393 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToTurtle(this, slide, this.board).usrCoords, false); 394 newCoords = Geometry.projectPointToTurtle(this, slide, this.board); 395 newPos = this.position; // save position for the overwriting below 396 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 397 if ((slide.type === Const.OBJECT_TYPE_ARC || 398 slide.type === Const.OBJECT_TYPE_SECTOR)) { 399 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 400 401 angle = Geometry.rad(slide.radiuspoint, slide.center, this); 402 alpha = 0.0; 403 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 404 newPos = angle; 405 406 if ((slide.visProp.selection === 'minor' && beta > Math.PI) || 407 (slide.visProp.selection === 'major' && beta < Math.PI)) { 408 alpha = beta; 409 beta = 2 * Math.PI; 410 } 411 412 // Correct the position if we are outside of the sector/arc 413 if (angle < alpha || angle > beta) { 414 newPos = beta; 415 416 if ((angle < alpha && angle > alpha * 0.5) || (angle > beta && angle > beta * 0.5 + Math.PI)) { 417 newPos = alpha; 418 } 419 420 this.needsUpdateFromParent = true; 421 this.updateGliderFromParent(); 422 } 423 424 delta = beta - alpha; 425 if (this.visProp.isgeonext) { 426 delta = 1.0; 427 } 428 if (Math.abs(delta) > Mat.eps) { 429 newPos /= delta; 430 } 431 } else { 432 // In case, the point is a constrained glider. 433 this.updateConstraint(); 434 435 if (slide.transformations.length > 0) { 436 slide.updateTransformMatrix(); 437 invMat = Mat.inverse(slide.transformMat); 438 c = Mat.matVecMult(invMat, this.coords.usrCoords); 439 440 cp = (new Coords(Const.COORDS_BY_USER, c, this.board)).usrCoords; 441 c = Geometry.projectCoordsToCurve(cp[1], cp[2], this.position || 0, slide, this.board); 442 443 newCoords = c[0]; 444 newPos = c[1]; 445 } else { 446 // side-effect: this.position is overwritten 447 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToCurve(this, slide, this.board).usrCoords, false); 448 newCoords = Geometry.projectPointToCurve(this, slide, this.board); 449 newPos = this.position; // save position for the overwriting below 450 } 451 } 452 } else if (Type.isPoint(slide)) { 453 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToPoint(this, slide, this.board).usrCoords, false); 454 newCoords = Geometry.projectPointToPoint(this, slide, this.board); 455 newPos = this.position; // save position for the overwriting below 456 } 457 458 this.coords.setCoordinates(Const.COORDS_BY_USER, newCoords.usrCoords, doRound); 459 this.position = newPos; 460 }, 461 462 /** 463 * Update of a glider in case a parent element has been updated. That means the 464 * relative position of the glider stays the same. 465 * @private 466 */ 467 updateGliderFromParent: function () { 468 var p1c, p2c, r, lbda, c, 469 slide = this.slideObject, 470 baseangle, alpha, angle, beta, 471 delta = 2.0 * Math.PI, 472 newPos; 473 474 if (!this.needsUpdateFromParent) { 475 this.needsUpdateFromParent = true; 476 return; 477 } 478 479 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 480 r = slide.Radius(); 481 if (this.visProp.isgeonext) { 482 delta = 1.0; 483 } 484 c = [ 485 slide.center.X() + r * Math.cos(this.position * delta), 486 slide.center.Y() + r * Math.sin(this.position * delta) 487 ]; 488 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 489 p1c = slide.point1.coords.usrCoords; 490 p2c = slide.point2.coords.usrCoords; 491 492 // The second point is an ideal point 493 if (Math.abs(p2c[0]) < Mat.eps) { 494 lbda = Math.min(Math.abs(this.position), 1 - Mat.eps); 495 lbda /= (1.0 - lbda); 496 497 if (this.position < 0) { 498 lbda = -lbda; 499 } 500 501 c = [ 502 p1c[0] + lbda * p2c[0], 503 p1c[1] + lbda * p2c[1], 504 p1c[2] + lbda * p2c[2] 505 ]; 506 // The first point is an ideal point 507 } else if (Math.abs(p1c[0]) < Mat.eps) { 508 lbda = Math.max(this.position, Mat.eps); 509 lbda = Math.min(lbda, 2 - Mat.eps); 510 511 if (lbda > 1) { 512 lbda = (lbda - 1) / (lbda - 2); 513 } else { 514 lbda = (1 - lbda) / lbda; 515 } 516 517 c = [ 518 p2c[0] + lbda * p1c[0], 519 p2c[1] + lbda * p1c[1], 520 p2c[2] + lbda * p1c[2] 521 ]; 522 } else { 523 lbda = this.position; 524 c = [ 525 p1c[0] + lbda * (p2c[0] - p1c[0]), 526 p1c[1] + lbda * (p2c[1] - p1c[1]), 527 p1c[2] + lbda * (p2c[2] - p1c[2]) 528 ]; 529 } 530 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 531 this.coords.setCoordinates(Const.COORDS_BY_USER, [slide.Z(this.position), slide.X(this.position), slide.Y(this.position)]); 532 // In case, the point is a constrained glider. 533 // side-effect: this.position is overwritten: 534 this.updateConstraint(); 535 c = Geometry.projectPointToTurtle(this, slide, this.board).usrCoords; 536 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 537 this.coords.setCoordinates(Const.COORDS_BY_USER, [slide.Z(this.position), slide.X(this.position), slide.Y(this.position)]); 538 539 if (slide.type === Const.OBJECT_TYPE_ARC || slide.type === Const.OBJECT_TYPE_SECTOR) { 540 baseangle = Geometry.rad([slide.center.X() + 1, slide.center.Y()], slide.center, slide.radiuspoint); 541 542 alpha = 0.0; 543 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 544 545 if ((slide.visProp.selection === 'minor' && beta > Math.PI) || 546 (slide.visProp.selection === 'major' && beta < Math.PI)) { 547 alpha = beta; 548 beta = 2 * Math.PI; 549 } 550 551 delta = beta - alpha; 552 if (this.visProp.isgeonext) { 553 delta = 1.0; 554 } 555 angle = this.position * delta; 556 557 // Correct the position if we are outside of the sector/arc 558 if (angle < alpha || angle > beta) { 559 angle = beta; 560 561 if ((angle < alpha && angle > alpha * 0.5) || 562 (angle > beta && angle > beta * 0.5 + Math.PI)) { 563 angle = alpha; 564 } 565 566 this.position = angle; 567 if (Math.abs(delta) > Mat.eps) { 568 this.position /= delta; 569 } 570 } 571 572 r = slide.Radius(); 573 c = [ 574 slide.center.X() + r * Math.cos(this.position * delta + baseangle), 575 slide.center.Y() + r * Math.sin(this.position * delta + baseangle) 576 ]; 577 } else { 578 // In case, the point is a constrained glider. 579 // side-effect: this.position is overwritten 580 this.updateConstraint(); 581 c = Geometry.projectPointToCurve(this, slide, this.board).usrCoords; 582 } 583 584 } else if (Type.isPoint(slide)) { 585 c = Geometry.projectPointToPoint(this, slide, this.board).usrCoords; 586 } 587 588 this.coords.setCoordinates(Const.COORDS_BY_USER, c, false); 589 }, 590 591 updateRendererGeneric: function (rendererMethod) { 592 var wasReal; 593 594 if (!this.needsUpdate) { 595 return this; 596 } 597 598 /* Call the renderer only if point is visible. */ 599 if (this.visProp.visible) { 600 wasReal = this.isReal; 601 this.isReal = (!isNaN(this.coords.usrCoords[1] + this.coords.usrCoords[2])); 602 //Homogeneous coords: ideal point 603 this.isReal = (Math.abs(this.coords.usrCoords[0]) > Mat.eps) ? this.isReal : false; 604 605 if (this.isReal) { 606 if (wasReal !== this.isReal) { 607 this.board.renderer.show(this); 608 609 if (this.hasLabel && this.label.visProp.visible) { 610 this.board.renderer.show(this.label); 611 } 612 } 613 this.board.renderer[rendererMethod](this); 614 } else { 615 if (wasReal !== this.isReal) { 616 this.board.renderer.hide(this); 617 618 if (this.hasLabel && this.label.visProp.visible) { 619 this.board.renderer.hide(this.label); 620 } 621 } 622 } 623 } 624 625 /* Update the label if visible. */ 626 if (this.hasLabel && this.visProp.visible && this.label && this.label.visProp.visible && this.isReal) { 627 this.label.update(); 628 this.board.renderer.updateText(this.label); 629 } 630 631 this.needsUpdate = false; 632 633 return this; 634 }, 635 636 /** 637 * Getter method for x, this is used by for CAS-points to access point coordinates. 638 * @returns {Number} User coordinate of point in x direction. 639 */ 640 X: function () { 641 return this.coords.usrCoords[1]; 642 }, 643 644 /** 645 * Getter method for y, this is used by CAS-points to access point coordinates. 646 * @returns {Number} User coordinate of point in y direction. 647 */ 648 Y: function () { 649 return this.coords.usrCoords[2]; 650 }, 651 652 /** 653 * Getter method for z, this is used by CAS-points to access point coordinates. 654 * @returns {Number} User coordinate of point in z direction. 655 */ 656 Z: function () { 657 return this.coords.usrCoords[0]; 658 }, 659 660 /** 661 * New evaluation of the function term. 662 * This is required for CAS-points: Their XTerm() method is overwritten in {@link #addConstraint} 663 * @returns {Number} User coordinate of point in x direction. 664 * @private 665 */ 666 XEval: function () { 667 return this.coords.usrCoords[1]; 668 }, 669 670 /** 671 * New evaluation of the function term. 672 * This is required for CAS-points: Their YTerm() method is overwritten in {@link #addConstraint} 673 * @returns {Number} User coordinate of point in y direction. 674 * @private 675 */ 676 YEval: function () { 677 return this.coords.usrCoords[2]; 678 }, 679 680 /** 681 * New evaluation of the function term. 682 * This is required for CAS-points: Their ZTerm() method is overwritten in {@link #addConstraint} 683 * @returns {Number} User coordinate of point in z direction. 684 * @private 685 */ 686 ZEval: function () { 687 return this.coords.usrCoords[0]; 688 }, 689 690 /** 691 * Getter method for the distance to a second point, this is required for CAS-elements. 692 * Here, function inlining seems to be worthwile (for plotting). 693 * @param {JXG.Point} point2 The point to which the distance shall be calculated. 694 * @returns {Number} Distance in user coordinate to the given point 695 */ 696 Dist: function (point2) { 697 if (this.isReal && point2.isReal) { 698 return this.coords.distance(Const.COORDS_BY_USER, point2.coords); 699 } 700 return NaN; 701 }, 702 703 /** 704 * Alias for {@link JXG.Element#handleSnapToGrid} 705 * @param {Boolean} force force snapping independent from what the snaptogrid attribute says 706 * @returns {JXG.Point} Reference to this element 707 */ 708 snapToGrid: function (force) { 709 return this.handleSnapToGrid(force); 710 }, 711 712 /** 713 * Let a point snap to the nearest point in distance of 714 * {@link JXG.Point#attractorDistance}. 715 * The function uses the coords object of the point as 716 * its actual position. 717 * @param {Boolean} force force snapping independent from what the snaptogrid attribute says 718 * @returns {JXG.Point} Reference to this element 719 */ 720 handleSnapToPoints: function (force) { 721 var i, pEl, pCoords, 722 d = 0, 723 dMax = Infinity, 724 c = null; 725 726 if (this.visProp.snaptopoints || force) { 727 for (i = 0; i < this.board.objectsList.length; i++) { 728 pEl = this.board.objectsList[i]; 729 730 if (Type.isPoint(pEl) && pEl !== this && pEl.visProp.visible) { 731 pCoords = Geometry.projectPointToPoint(this, pEl, this.board); 732 if (this.visProp.attractorunit === 'screen') { 733 d = pCoords.distance(Const.COORDS_BY_SCREEN, this.coords); 734 } else { 735 d = pCoords.distance(Const.COORDS_BY_USER, this.coords); 736 } 737 738 if (d < this.visProp.attractordistance && d < dMax) { 739 dMax = d; 740 c = pCoords; 741 } 742 } 743 } 744 745 if (c !== null) { 746 this.coords.setCoordinates(Const.COORDS_BY_USER, c.usrCoords); 747 } 748 } 749 750 return this; 751 }, 752 753 /** 754 * Alias for {@link #handleSnapToPoints}. 755 * @param {Boolean} force force snapping independent from what the snaptogrid attribute says 756 * @returns {JXG.Point} Reference to this element 757 */ 758 snapToPoints: function (force) { 759 return this.handleSnapToPoints(force); 760 }, 761 762 /** 763 * A point can change its type from free point to glider 764 * and vice versa. If it is given an array of attractor elements 765 * (attribute attractors) and the attribute attractorDistance 766 * then the pint will be made a glider if it less than attractorDistance 767 * apart from one of its attractor elements. 768 * If attractorDistance is equal to zero, the point stays in its 769 * current form. 770 * @returns {JXG.Point} Reference to this element 771 */ 772 handleAttractors: function () { 773 var i, el, projCoords, 774 d = 0.0, 775 len = this.visProp.attractors.length; 776 777 if (this.visProp.attractordistance === 0.0) { 778 return; 779 } 780 781 for (i = 0; i < len; i++) { 782 el = this.board.select(this.visProp.attractors[i]); 783 784 if (Type.exists(el) && el !== this) { 785 if (Type.isPoint(el)) { 786 projCoords = Geometry.projectPointToPoint(this, el, this.board); 787 } else if (el.elementClass === Const.OBJECT_CLASS_LINE) { 788 projCoords = Geometry.projectPointToLine(this, el, this.board); 789 } else if (el.elementClass === Const.OBJECT_CLASS_CIRCLE) { 790 projCoords = Geometry.projectPointToCircle(this, el, this.board); 791 } else if (el.elementClass === Const.OBJECT_CLASS_CURVE) { 792 projCoords = Geometry.projectPointToCurve(this, el, this.board); 793 } else if (el.type === Const.OBJECT_TYPE_TURTLE) { 794 projCoords = Geometry.projectPointToTurtle(this, el, this.board); 795 } 796 797 if (this.visProp.attractorunit === 'screen') { 798 d = projCoords.distance(Const.COORDS_BY_SCREEN, this.coords); 799 } else { 800 d = projCoords.distance(Const.COORDS_BY_USER, this.coords); 801 } 802 803 if (d < this.visProp.attractordistance) { 804 if (!(this.type === Const.OBJECT_TYPE_GLIDER && this.slideObject === el)) { 805 this.makeGlider(el); 806 } 807 808 break; // bind the point to the first attractor in its list. 809 } else { 810 if (el === this.slideObject && d >= this.visProp.snatchdistance) { 811 this.popSlideObject(); 812 } 813 } 814 } 815 } 816 817 return this; 818 }, 819 820 /** 821 * Sets coordinates and calls the point's update() method. 822 * @param {Number} method The type of coordinates used here. 823 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 824 * @param {Array} coords coordinates <tt>([z], x, y)</tt> in screen/user units 825 * @param {Array} lastPosition (optional) coordinates <tt>(x, y)</tt> in screen units of the last position. 826 * Usually this is the position where the last drag event had occurred. This is needed to prevent jumps 827 * to the lower left corner when dragging an image. 828 * @returns {JXG.Point} this element 829 */ 830 setPositionDirectly: function (method, coords, lastPosition) { 831 var i, offset, c, dc, 832 oldCoords = this.coords, 833 newCoords; 834 835 // Correct offset for large objects like images and texts to prevent that the 836 // corner of the object jumps to the mouse pointer. 837 if (Type.exists(lastPosition) && 838 !this.visProp.snaptogrid && 839 !this.visProp.snaptopoints && 840 this.visProp.attractors.length === 0) { 841 offset = Statistics.subtract(this.coords.scrCoords.slice(1), lastPosition); 842 coords = Statistics.add(coords, offset); 843 } 844 845 if (this.relativeCoords) { 846 c = new Coords(method, coords, this.board); 847 if (this.visProp.islabel) { 848 dc = Statistics.subtract(c.scrCoords, oldCoords.scrCoords); 849 this.relativeCoords.scrCoords[1] += dc[1]; 850 this.relativeCoords.scrCoords[2] += dc[2]; 851 } else { 852 dc = Statistics.subtract(c.usrCoords, oldCoords.usrCoords); 853 this.relativeCoords.usrCoords[1] += dc[1]; 854 this.relativeCoords.usrCoords[2] += dc[2]; 855 } 856 857 return this; 858 } 859 860 this.coords.setCoordinates(method, coords); 861 this.handleSnapToGrid(); 862 this.handleSnapToPoints(); 863 this.handleAttractors(); 864 865 if (this.group.length === 0) { 866 // Here used to be the update of the groups. I'm not sure why we don't need to execute 867 // the else branch if there are groups defined on this point, hence I'll let the if live. 868 869 // Update the initial coordinates. This is needed for free points 870 // that have a transformation bound to it. 871 for (i = this.transformations.length - 1; i >= 0; i--) { 872 if (method === Const.COORDS_BY_SCREEN) { 873 newCoords = (new Coords(method, coords, this.board)).usrCoords; 874 } else { 875 if (coords.length === 2) { 876 coords = [1].concat(coords); 877 } 878 newCoords = coords; 879 } 880 this.initialCoords.setCoordinates(Const.COORDS_BY_USER, Mat.matVecMult(Mat.inverse(this.transformations[i].matrix), newCoords)); 881 } 882 this.prepareUpdate().update(); 883 } 884 885 // If the user suspends the board updates we need to recalculate the relative position of 886 // the point on the slide object. this is done in updateGlider() which is NOT called during the 887 // update process triggered by unsuspendUpdate. 888 if (this.board.isSuspendedUpdate && this.type === Const.OBJECT_TYPE_GLIDER) { 889 this.updateGlider(); 890 } 891 892 return this; 893 }, 894 895 /** 896 * Translates the point by <tt>tv = (x, y)</tt>. 897 * @param {Number} method The type of coordinates used here. 898 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 899 * @param {Number} tv (x, y) 900 * @returns {JXG.Point} 901 */ 902 setPositionByTransform: function (method, tv) { 903 var t; 904 905 tv = new Coords(method, tv, this.board); 906 t = this.board.create('transform', tv.usrCoords.slice(1), {type: 'translate'}); 907 908 if (this.transformations.length > 0 && 909 this.transformations[this.transformations.length - 1].isNumericMatrix) { 910 this.transformations[this.transformations.length - 1].melt(t); 911 } else { 912 this.addTransform(this, t); 913 } 914 915 this.prepareUpdate().update(); 916 917 return this; 918 }, 919 920 /** 921 * Sets coordinates and calls the point's update() method. 922 * @param {Number} method The type of coordinates used here. 923 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 924 * @param {Array} coords coordinates in screen/user units 925 * @param {Array} lastPosition (optional) coordinates <tt>(x, y)</tt> in screen units of the last position. 926 * Usually this is the position where the last drag event had occurred. This is needed to prevent jumps 927 * to the lower left corner when dragging an image. 928 * @returns {JXG.Point} 929 */ 930 setPosition: function (method, coords, lastPosition) { 931 return this.setPositionDirectly(method, coords, lastPosition); 932 }, 933 934 /** 935 * Sets the position of a glider relative to the defining elements 936 * of the {@link JXG.Point#slideObject}. 937 * @param {Number} x 938 * @returns {JXG.Point} Reference to the point element. 939 */ 940 setGliderPosition: function (x) { 941 if (this.type === Const.OBJECT_TYPE_GLIDER) { 942 this.position = x; 943 this.board.update(); 944 } 945 946 return this; 947 }, 948 949 /** 950 * Convert the point to glider and update the construction. 951 * To move the point visual onto the glider, a call of board update is necessary. 952 * @param {String|Object} slide The object the point will be bound to. 953 */ 954 makeGlider: function (slide) { 955 var slideobj = this.board.select(slide); 956 957 /* Gliders on Ticks are forbidden */ 958 if (!Type.exists(slideobj)) { 959 throw new Error("JSXGraph: slide object undefined."); 960 } else if (slideobj.type === Const.OBJECT_TYPE_TICKS) { 961 throw new Error("JSXGraph: gliders on ticks are not possible."); 962 } 963 964 this.slideObject = this.board.select(slide); 965 this.slideObjects.push(this.slideObject); 966 this.addParents(slide); 967 968 this.type = Const.OBJECT_TYPE_GLIDER; 969 this.elType = 'glider'; 970 this.visProp.snapwidth = -1; // By default, deactivate snapWidth 971 this.slideObject.addChild(this); 972 this.isDraggable = true; 973 974 this.generatePolynomial = function () { 975 return this.slideObject.generatePolynomial(this); 976 }; 977 978 // Determine the initial value of this.position 979 this.updateGlider(); 980 this.needsUpdateFromParent = true; 981 this.updateGliderFromParent(); 982 983 return this; 984 }, 985 986 /** 987 * Remove the last slideObject. If there are more than one elements the point is bound to, 988 * the second last element is the new active slideObject. 989 */ 990 popSlideObject: function () { 991 if (this.slideObjects.length > 0) { 992 this.slideObjects.pop(); 993 994 // It may not be sufficient to remove the point from 995 // the list of childElement. For complex dependencies 996 // one may have to go to the list of ancestor and descendants. A.W. 997 // yes indeed, see #51 on github bugtracker 998 //delete this.slideObject.childElements[this.id]; 999 this.slideObject.removeChild(this); 1000 1001 if (this.slideObjects.length === 0) { 1002 this.type = this._org_type; 1003 if (this.type === Const.OBJECT_TYPE_POINT) { 1004 this.elType = 'point'; 1005 } else if (this.elementClass === Const.OBJECT_CLASS_TEXT) { 1006 this.elType = 'text'; 1007 } else if (this.type === Const.OBJECT_TYPE_IMAGE) { 1008 this.elType = 'image'; 1009 } 1010 1011 this.slideObject = null; 1012 } else { 1013 this.slideObject = this.slideObjects[this.slideObjects.length - 1]; 1014 } 1015 } 1016 }, 1017 1018 /** 1019 * Converts a calculated element into a free element, 1020 * i.e. it will delete all ancestors and transformations and, 1021 * if the element is currently a glider, will remove the slideObject reference. 1022 */ 1023 free: function () { 1024 var ancestorId, ancestor, child; 1025 1026 if (this.type !== Const.OBJECT_TYPE_GLIDER) { 1027 // remove all transformations 1028 this.transformations.length = 0; 1029 1030 if (!this.isDraggable) { 1031 this.isDraggable = true; 1032 1033 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1034 this.type = Const.OBJECT_TYPE_POINT; 1035 this.elType = 'point'; 1036 } 1037 1038 this.XEval = function () { 1039 return this.coords.usrCoords[1]; 1040 }; 1041 1042 this.YEval = function () { 1043 return this.coords.usrCoords[2]; 1044 }; 1045 1046 this.ZEval = function () { 1047 return this.coords.usrCoords[0]; 1048 }; 1049 1050 this.Xjc = null; 1051 this.Yjc = null; 1052 } else { 1053 return; 1054 } 1055 } 1056 1057 // a free point does not depend on anything. And instead of running through tons of descendants and ancestor 1058 // structures, where we eventually are going to visit a lot of objects twice or thrice with hard to read and 1059 // comprehend code, just run once through all objects and delete all references to this point and its label. 1060 for (ancestorId in this.board.objects) { 1061 if (this.board.objects.hasOwnProperty(ancestorId)) { 1062 ancestor = this.board.objects[ancestorId]; 1063 1064 if (ancestor.descendants) { 1065 delete ancestor.descendants[this.id]; 1066 delete ancestor.childElements[this.id]; 1067 1068 if (this.hasLabel) { 1069 delete ancestor.descendants[this.label.id]; 1070 delete ancestor.childElements[this.label.id]; 1071 } 1072 } 1073 } 1074 } 1075 1076 // A free point does not depend on anything. Remove all ancestors. 1077 this.ancestors = {}; // only remove the reference 1078 1079 // Completely remove all slideObjects of the element 1080 this.slideObject = null; 1081 this.slideObjects = []; 1082 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1083 this.type = Const.OBJECT_TYPE_POINT; 1084 this.elType = 'point'; 1085 } else if (this.elementClass === Const.OBJECT_CLASS_TEXT) { 1086 this.type = this._org_type; 1087 this.elType = 'text'; 1088 } else if (this.elementClass === Const.OBJECT_CLASS_OTHER) { 1089 this.type = this._org_type; 1090 this.elType = 'image'; 1091 } 1092 }, 1093 1094 /** 1095 * Convert the point to CAS point and call update(). 1096 * @param {Array} terms [[zterm], xterm, yterm] defining terms for the z, x and y coordinate. 1097 * The z-coordinate is optional and it is used for homogeneous coordinates. 1098 * The coordinates may be either <ul> 1099 * <li>a JavaScript function,</li> 1100 * <li>a string containing GEONExT syntax. This string will be converted into a JavaScript 1101 * function here,</li> 1102 * <li>a Number</li> 1103 * <li>a pointer to a slider object. This will be converted into a call of the Value()-method 1104 * of this slider.</li> 1105 * </ul> 1106 * @see JXG.GeonextParser#geonext2JS 1107 */ 1108 addConstraint: function (terms) { 1109 var fs, i, v, t, 1110 newfuncs = [], 1111 what = ['X', 'Y'], 1112 1113 makeConstFunction = function (z) { 1114 return function () { 1115 return z; 1116 }; 1117 }, 1118 1119 makeSliderFunction = function (a) { 1120 return function () { 1121 return a.Value(); 1122 }; 1123 }; 1124 1125 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1126 this.type = Const.OBJECT_TYPE_CAS; 1127 } 1128 1129 this.isDraggable = false; 1130 1131 for (i = 0; i < terms.length; i++) { 1132 v = terms[i]; 1133 1134 if (typeof v === 'string') { 1135 // Convert GEONExT syntax into JavaScript syntax 1136 //t = JXG.GeonextParser.geonext2JS(v, this.board); 1137 //newfuncs[i] = new Function('','return ' + t + ';'); 1138 //v = GeonextParser.replaceNameById(v, this.board); 1139 newfuncs[i] = this.board.jc.snippet(v, true, null, true); 1140 1141 if (terms.length === 2) { 1142 this[what[i] + 'jc'] = terms[i]; 1143 } 1144 } else if (typeof v === 'function') { 1145 newfuncs[i] = v; 1146 } else if (typeof v === 'number') { 1147 newfuncs[i] = makeConstFunction(v); 1148 // Slider 1149 } else if (typeof v === 'object' && typeof v.Value === 'function') { 1150 newfuncs[i] = makeSliderFunction(v); 1151 } 1152 1153 newfuncs[i].origin = v; 1154 } 1155 1156 // Intersection function 1157 if (terms.length === 1) { 1158 this.updateConstraint = function () { 1159 var c = newfuncs[0](); 1160 1161 // Array 1162 if (Type.isArray(c)) { 1163 this.coords.setCoordinates(Const.COORDS_BY_USER, c); 1164 // Coords object 1165 } else { 1166 this.coords = c; 1167 } 1168 }; 1169 // Euclidean coordinates 1170 } else if (terms.length === 2) { 1171 this.XEval = newfuncs[0]; 1172 this.YEval = newfuncs[1]; 1173 1174 this.parents = [newfuncs[0].origin, newfuncs[1].origin]; 1175 1176 this.updateConstraint = function () { 1177 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.XEval(), this.YEval()]); 1178 }; 1179 // Homogeneous coordinates 1180 } else { 1181 this.ZEval = newfuncs[0]; 1182 this.XEval = newfuncs[1]; 1183 this.YEval = newfuncs[2]; 1184 1185 this.parents = [newfuncs[0].origin, newfuncs[1].origin, newfuncs[2].origin]; 1186 1187 this.updateConstraint = function () { 1188 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.ZEval(), this.XEval(), this.YEval()]); 1189 }; 1190 } 1191 1192 /** 1193 * We have to do an update. Otherwise, elements relying on this point will receive NaN. 1194 */ 1195 this.prepareUpdate().update(); 1196 1197 if (!this.board.isSuspendedUpdate) { 1198 this.updateRenderer(); 1199 } 1200 1201 return this; 1202 }, 1203 1204 /** 1205 * In case there is an attribute "anchor", the element is bound to 1206 * this anchor element. 1207 * This is handled with this.relativeCoords. If the element is a label 1208 * relativeCoords are given in scrCoords, otherwise in usrCoords. 1209 * @param{Array} coordinates Offset from th anchor element. These are the values for this.relativeCoords. 1210 * In case of a label, coordinates are screen coordinates. Otherwise, coordinates are user coordinates. 1211 * @param{Boolean} isLabel Yes/no 1212 * @private 1213 */ 1214 addAnchor: function (coordinates, isLabel) { 1215 if (isLabel) { 1216 this.relativeCoords = new Coords(Const.COORDS_BY_SCREEN, coordinates.slice(0, 2), this.board); 1217 } else { 1218 this.relativeCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 1219 } 1220 this.element.addChild(this); 1221 this.addParents(this.element); 1222 1223 this.XEval = function () { 1224 var sx, coords, anchor; 1225 1226 if (this.visProp.islabel) { 1227 sx = parseFloat(this.visProp.offset[0]); 1228 anchor = this.element.getLabelAnchor(); 1229 coords = new Coords(Const.COORDS_BY_SCREEN, 1230 [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], this.board); 1231 1232 return coords.usrCoords[1]; 1233 } 1234 1235 anchor = this.element.getTextAnchor(); 1236 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1]; 1237 }; 1238 1239 this.YEval = function () { 1240 var sy, coords, anchor; 1241 1242 if (this.visProp.islabel) { 1243 sy = -parseFloat(this.visProp.offset[1]); 1244 anchor = this.element.getLabelAnchor(); 1245 coords = new Coords(Const.COORDS_BY_SCREEN, 1246 [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board); 1247 1248 return coords.usrCoords[2]; 1249 } 1250 1251 anchor = this.element.getTextAnchor(); 1252 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2]; 1253 }; 1254 1255 this.ZEval = Type.createFunction(1, this.board, ''); 1256 1257 this.updateConstraint = function () { 1258 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.ZEval(), this.XEval(), this.YEval()]); 1259 }; 1260 1261 this.coords = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this.board); 1262 }, 1263 1264 /** 1265 * Applies the transformations of the element. 1266 * This method applies to text and images. Point transformations are handled differently. 1267 * @returns {JXG.CoordsElement} Reference to this object. 1268 */ 1269 updateTransform: function () { 1270 var i; 1271 1272 if (this.transformations.length === 0) { 1273 return this; 1274 } 1275 1276 for (i = 0; i < this.transformations.length; i++) { 1277 this.transformations[i].update(); 1278 } 1279 1280 return this; 1281 }, 1282 1283 /** 1284 * Add transformations to this point. 1285 * @param {JXG.GeometryElement} el 1286 * @param {JXG.Transformation|Array} transform Either one {@link JXG.Transformation} 1287 * or an array of {@link JXG.Transformation}s. 1288 * @returns {JXG.Point} Reference to this point object. 1289 */ 1290 addTransform: function (el, transform) { 1291 var i, 1292 list = Type.isArray(transform) ? transform : [transform], 1293 len = list.length; 1294 1295 // There is only one baseElement possible 1296 if (this.transformations.length === 0) { 1297 this.baseElement = el; 1298 } 1299 1300 for (i = 0; i < len; i++) { 1301 this.transformations.push(list[i]); 1302 } 1303 1304 return this; 1305 }, 1306 1307 /** 1308 * Animate the point. 1309 * @param {Number} direction The direction the glider is animated. Can be +1 or -1. 1310 * @param {Number} stepCount The number of steps. 1311 * @name Glider#startAnimation 1312 * @see Glider#stopAnimation 1313 * @function 1314 */ 1315 startAnimation: function (direction, stepCount) { 1316 var that = this; 1317 1318 if ((this.type === Const.OBJECT_TYPE_GLIDER) && !Type.exists(this.intervalCode)) { 1319 this.intervalCode = window.setInterval(function () { 1320 that._anim(direction, stepCount); 1321 }, 250); 1322 1323 if (!Type.exists(this.intervalCount)) { 1324 this.intervalCount = 0; 1325 } 1326 } 1327 return this; 1328 }, 1329 1330 /** 1331 * Stop animation. 1332 * @name Glider#stopAnimation 1333 * @see Glider#startAnimation 1334 * @function 1335 */ 1336 stopAnimation: function () { 1337 if (Type.exists(this.intervalCode)) { 1338 window.clearInterval(this.intervalCode); 1339 delete this.intervalCode; 1340 } 1341 1342 return this; 1343 }, 1344 1345 /** 1346 * Starts an animation which moves the point along a given path in given time. 1347 * @param {Array|function} path The path the point is moved on. 1348 * This can be either an array of arrays containing x and y values of the points of 1349 * the path, or function taking the amount of elapsed time since the animation 1350 * has started and returns an array containing a x and a y value or NaN. 1351 * In case of NaN the animation stops. 1352 * @param {Number} time The time in milliseconds in which to finish the animation 1353 * @param {Object} [options] Optional settings for the animation. 1354 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1355 * @param {Boolean} [options.interpolate=true] If <tt>path</tt> is an array moveAlong() 1356 * will interpolate the path 1357 * using {@link JXG.Math.Numerics#Neville}. Set this flag to false if you don't want to use interpolation. 1358 * @returns {JXG.Point} Reference to the point. 1359 */ 1360 moveAlong: function (path, time, options) { 1361 options = options || {}; 1362 1363 var i, neville, 1364 interpath = [], 1365 p = [], 1366 delay = this.board.attr.animationdelay, 1367 steps = time / delay, 1368 1369 makeFakeFunction = function (i, j) { 1370 return function () { 1371 return path[i][j]; 1372 }; 1373 }; 1374 1375 if (Type.isArray(path)) { 1376 for (i = 0; i < path.length; i++) { 1377 if (Type.isPoint(path[i])) { 1378 p[i] = path[i]; 1379 } else { 1380 p[i] = { 1381 elementClass: Const.OBJECT_CLASS_POINT, 1382 X: makeFakeFunction(i, 0), 1383 Y: makeFakeFunction(i, 1) 1384 }; 1385 } 1386 } 1387 1388 time = time || 0; 1389 if (time === 0) { 1390 this.setPosition(Const.COORDS_BY_USER, [p[p.length - 1].X(), p[p.length - 1].Y()]); 1391 return this.board.update(this); 1392 } 1393 1394 if (!Type.exists(options.interpolate) || options.interpolate) { 1395 neville = Numerics.Neville(p); 1396 for (i = 0; i < steps; i++) { 1397 interpath[i] = []; 1398 interpath[i][0] = neville[0]((steps - i) / steps * neville[3]()); 1399 interpath[i][1] = neville[1]((steps - i) / steps * neville[3]()); 1400 } 1401 } else { 1402 for (i = 0; i < steps; i++) { 1403 interpath[i] = []; 1404 interpath[i][0] = path[Math.floor((steps - i) / steps * (path.length - 1))][0]; 1405 interpath[i][1] = path[Math.floor((steps - i) / steps * (path.length - 1))][1]; 1406 } 1407 } 1408 1409 this.animationPath = interpath; 1410 } else if (Type.isFunction(path)) { 1411 this.animationPath = path; 1412 this.animationStart = new Date().getTime(); 1413 } 1414 1415 this.animationCallback = options.callback; 1416 this.board.addAnimation(this); 1417 1418 return this; 1419 }, 1420 1421 /** 1422 * Starts an animated point movement towards the given coordinates <tt>where</tt>. 1423 * The animation is done after <tt>time</tt> milliseconds. 1424 * If the second parameter is not given or is equal to 0, setPosition() is called, see #setPosition. 1425 * @param {Array} where Array containing the x and y coordinate of the target location. 1426 * @param {Number} [time] Number of milliseconds the animation should last. 1427 * @param {Object} [options] Optional settings for the animation 1428 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1429 * @param {String} [options.effect='<>'] animation effects like speed fade in and out. possible values are 1430 * '<>' for speed increase on start and slow down at the end (default) and '--' for constant speed during 1431 * the whole animation. 1432 * @returns {JXG.Point} Reference to itself. 1433 * @see #animate 1434 */ 1435 moveTo: function (where, time, options) { 1436 options = options || {}; 1437 where = new Coords(Const.COORDS_BY_USER, where, this.board); 1438 1439 var i, 1440 delay = this.board.attr.animationdelay, 1441 steps = Math.ceil(time / delay), 1442 coords = [], 1443 X = this.coords.usrCoords[1], 1444 Y = this.coords.usrCoords[2], 1445 dX = (where.usrCoords[1] - X), 1446 dY = (where.usrCoords[2] - Y), 1447 1448 /** @ignore */ 1449 stepFun = function (i) { 1450 if (options.effect && options.effect === '<>') { 1451 return Math.pow(Math.sin((i / steps) * Math.PI / 2), 2); 1452 } 1453 return i / steps; 1454 }; 1455 1456 if (!Type.exists(time) || time === 0 || (Math.abs(where.usrCoords[0] - this.coords.usrCoords[0]) > Mat.eps)) { 1457 this.setPosition(Const.COORDS_BY_USER, where.usrCoords); 1458 return this.board.update(this); 1459 } 1460 1461 if (Math.abs(dX) < Mat.eps && Math.abs(dY) < Mat.eps) { 1462 return this; 1463 } 1464 1465 for (i = steps; i >= 0; i--) { 1466 coords[steps - i] = [where.usrCoords[0], X + dX * stepFun(i), Y + dY * stepFun(i)]; 1467 } 1468 1469 this.animationPath = coords; 1470 this.animationCallback = options.callback; 1471 this.board.addAnimation(this); 1472 1473 return this; 1474 }, 1475 1476 /** 1477 * Starts an animated point movement towards the given coordinates <tt>where</tt>. After arriving at 1478 * <tt>where</tt> the point moves back to where it started. The animation is done after <tt>time</tt> 1479 * milliseconds. 1480 * @param {Array} where Array containing the x and y coordinate of the target location. 1481 * @param {Number} time Number of milliseconds the animation should last. 1482 * @param {Object} [options] Optional settings for the animation 1483 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1484 * @param {String} [options.effect='<>'] animation effects like speed fade in and out. possible values are 1485 * '<>' for speed increase on start and slow down at the end (default) and '--' for constant speed during 1486 * the whole animation. 1487 * @param {Number} [options.repeat=1] How often this animation should be repeated. 1488 * @returns {JXG.Point} Reference to itself. 1489 * @see #animate 1490 */ 1491 visit: function (where, time, options) { 1492 where = new Coords(Const.COORDS_BY_USER, where, this.board); 1493 1494 var i, j, steps, 1495 delay = this.board.attr.animationdelay, 1496 coords = [], 1497 X = this.coords.usrCoords[1], 1498 Y = this.coords.usrCoords[2], 1499 dX = (where.usrCoords[1] - X), 1500 dY = (where.usrCoords[2] - Y), 1501 1502 /** @ignore */ 1503 stepFun = function (i) { 1504 var x = (i < steps / 2 ? 2 * i / steps : 2 * (steps - i) / steps); 1505 1506 if (options.effect && options.effect === '<>') { 1507 return Math.pow(Math.sin(x * Math.PI / 2), 2); 1508 } 1509 1510 return x; 1511 }; 1512 1513 // support legacy interface where the third parameter was the number of repeats 1514 if (typeof options === 'number') { 1515 options = {repeat: options}; 1516 } else { 1517 options = options || {}; 1518 if (!Type.exists(options.repeat)) { 1519 options.repeat = 1; 1520 } 1521 } 1522 1523 steps = Math.ceil(time / (delay * options.repeat)); 1524 1525 for (j = 0; j < options.repeat; j++) { 1526 for (i = steps; i >= 0; i--) { 1527 coords[j * (steps + 1) + steps - i] = [where.usrCoords[0], X + dX * stepFun(i), Y + dY * stepFun(i)]; 1528 } 1529 } 1530 this.animationPath = coords; 1531 this.animationCallback = options.callback; 1532 this.board.addAnimation(this); 1533 1534 return this; 1535 }, 1536 1537 /** 1538 * Animates a glider. Is called by the browser after startAnimation is called. 1539 * @param {Number} direction The direction the glider is animated. 1540 * @param {Number} stepCount The number of steps. 1541 * @see #startAnimation 1542 * @see #stopAnimation 1543 * @private 1544 */ 1545 _anim: function (direction, stepCount) { 1546 var distance, slope, dX, dY, alpha, startPoint, newX, radius, 1547 factor = 1; 1548 1549 this.intervalCount += 1; 1550 if (this.intervalCount > stepCount) { 1551 this.intervalCount = 0; 1552 } 1553 1554 if (this.slideObject.elementClass === Const.OBJECT_CLASS_LINE) { 1555 distance = this.slideObject.point1.coords.distance(Const.COORDS_BY_SCREEN, this.slideObject.point2.coords); 1556 slope = this.slideObject.getSlope(); 1557 if (slope !== Infinity) { 1558 alpha = Math.atan(slope); 1559 dX = Math.round((this.intervalCount / stepCount) * distance * Math.cos(alpha)); 1560 dY = Math.round((this.intervalCount / stepCount) * distance * Math.sin(alpha)); 1561 } else { 1562 dX = 0; 1563 dY = Math.round((this.intervalCount / stepCount) * distance); 1564 } 1565 1566 if (direction < 0) { 1567 startPoint = this.slideObject.point2; 1568 1569 if (this.slideObject.point2.coords.scrCoords[1] - this.slideObject.point1.coords.scrCoords[1] > 0) { 1570 factor = -1; 1571 } else if (this.slideObject.point2.coords.scrCoords[1] - this.slideObject.point1.coords.scrCoords[1] === 0) { 1572 if (this.slideObject.point2.coords.scrCoords[2] - this.slideObject.point1.coords.scrCoords[2] > 0) { 1573 factor = -1; 1574 } 1575 } 1576 } else { 1577 startPoint = this.slideObject.point1; 1578 1579 if (this.slideObject.point1.coords.scrCoords[1] - this.slideObject.point2.coords.scrCoords[1] > 0) { 1580 factor = -1; 1581 } else if (this.slideObject.point1.coords.scrCoords[1] - this.slideObject.point2.coords.scrCoords[1] === 0) { 1582 if (this.slideObject.point1.coords.scrCoords[2] - this.slideObject.point2.coords.scrCoords[2] > 0) { 1583 factor = -1; 1584 } 1585 } 1586 } 1587 1588 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [ 1589 startPoint.coords.scrCoords[1] + factor * dX, 1590 startPoint.coords.scrCoords[2] + factor * dY 1591 ]); 1592 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CURVE) { 1593 if (direction > 0) { 1594 newX = Math.round(this.intervalCount / stepCount * this.board.canvasWidth); 1595 } else { 1596 newX = Math.round((stepCount - this.intervalCount) / stepCount * this.board.canvasWidth); 1597 } 1598 1599 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [newX, 0]); 1600 this.coords = Geometry.projectPointToCurve(this, this.slideObject, this.board); 1601 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1602 if (direction < 0) { 1603 alpha = this.intervalCount / stepCount * 2 * Math.PI; 1604 } else { 1605 alpha = (stepCount - this.intervalCount) / stepCount * 2 * Math.PI; 1606 } 1607 1608 radius = this.slideObject.Radius(); 1609 1610 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 1611 this.slideObject.center.coords.usrCoords[1] + radius * Math.cos(alpha), 1612 this.slideObject.center.coords.usrCoords[2] + radius * Math.sin(alpha) 1613 ]); 1614 } 1615 1616 this.board.update(this); 1617 return this; 1618 }, 1619 1620 // documented in GeometryElement 1621 getTextAnchor: function () { 1622 return this.coords; 1623 }, 1624 1625 // documented in GeometryElement 1626 getLabelAnchor: function () { 1627 return this.coords; 1628 }, 1629 1630 getParents: function () { 1631 var p = [this.Z(), this.X(), this.Y()]; 1632 1633 if (this.parents) { 1634 p = this.parents; 1635 } 1636 1637 if (this.type === Const.OBJECT_TYPE_GLIDER) { 1638 p = [this.X(), this.Y(), this.slideObject.id]; 1639 1640 } 1641 1642 return p; 1643 } 1644 1645 }); 1646 1647 /** 1648 * Generic method to create point, text or image. 1649 * Determines the type of the construction, i.e. free, or constrained by function, 1650 * transformation or of glider type. 1651 * @param{Object} Callback Object type, e.g. JXG.Point, JXG.Text or JXG.Image 1652 * @param{Object} board Link to the board object 1653 * @param{Array} coords Array with coordinates. This may be: array of numbers, function 1654 * returning an array of numbers, array of functions returning a number, object and transformation. 1655 * If the attribute "slideObject" exists, a glider element is constructed. 1656 * @param{Object} attr Attributes object 1657 * @param{Object} arg1 Optional argument 1: in case of text this is the text content, 1658 * in case of an image this is the url. 1659 * @param{Array} arg2 Optional argument 2: in case of image this is an array containing the size of 1660 * the image. 1661 * @returns{Object} returns the created object or false. 1662 */ 1663 JXG.CoordsElement.create = function (Callback, board, coords, attr, arg1, arg2) { 1664 var el, isConstrained = false, i; 1665 1666 for (i = 0; i < coords.length; i++) { 1667 if (typeof coords[i] === 'function' || typeof coords[i] === 'string') { 1668 isConstrained = true; 1669 } 1670 } 1671 1672 if (!isConstrained) { 1673 if ((Type.isNumber(coords[0])) && (Type.isNumber(coords[1]))) { 1674 el = new Callback(board, coords, attr, arg1, arg2); 1675 1676 if (Type.exists(attr.slideobject)) { 1677 el.makeGlider(attr.slideobject); 1678 } else { 1679 // Free element 1680 el.baseElement = el; 1681 } 1682 el.isDraggable = true; 1683 } else if ((typeof coords[0] === 'object') && (typeof coords[1] === 'object')) { 1684 // Transformation 1685 el = new Callback(board, [0, 0], attr, arg1, arg2); 1686 el.addTransform(coords[0], coords[1]); 1687 el.isDraggable = false; 1688 1689 //el.parents = [coords[0].id]; 1690 } else { 1691 return false; 1692 } 1693 } else { 1694 el = new Callback(board, [0, 0], attr, arg1, arg2); 1695 el.addConstraint(coords); 1696 } 1697 1698 el.handleSnapToGrid(); 1699 el.handleSnapToPoints(); 1700 el.handleAttractors(); 1701 1702 el.addParents(coords); 1703 return el; 1704 }; 1705 1706 return JXG.CoordsElement; 1707 1708 }); 1709