1 /* 2 Copyright 2008-2025 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 <https://www.gnu.org/licenses/> 27 and <https://opensource.org/licenses/MIT/>. 28 */ 29 30 /*global JXG: true, define: true, console: true, window: true*/ 31 /*jslint nomen: true, plusplus: true*/ 32 33 /** 34 * @fileoverview The geometry object CoordsElement is defined in this file. 35 * This object provides the coordinate handling of points, images and texts. 36 */ 37 38 import JXG from "../jxg.js"; 39 import Mat from "../math/math.js"; 40 import Geometry from "../math/geometry.js"; 41 import Numerics from "../math/numerics.js"; 42 import Statistics from "../math/statistics.js"; 43 import Coords from "./coords.js"; 44 import Const from "./constants.js"; 45 import Type from "../utils/type.js"; 46 47 /** 48 * An element containing coords is a basic geometric element. 49 * This is a parent class for points, images and texts. 50 * It holds common methods for 51 * all kind of coordinate elements like points, texts and images. 52 * It can not be used directly. 53 * @class Creates a new coords element object. It is a parent class for points, images and texts. 54 * Do not use this constructor to create an element. 55 * 56 * @private 57 * @augments JXG.GeometryElement 58 * @param {Array} coordinates An array with the affine user coordinates of the point. 59 * {@link JXG.Options#elements}, and - optionally - a name and an id. 60 */ 61 JXG.CoordsElement = function (coordinates, isLabel) { 62 var i; 63 64 if (!Type.exists(coordinates)) { 65 coordinates = [1, 0, 0]; 66 } 67 68 for (i = 0; i < coordinates.length; ++i) { 69 coordinates[i] = parseFloat(coordinates[i]); 70 } 71 72 /** 73 * Coordinates of the element. 74 * @type JXG.Coords 75 * @private 76 */ 77 this.coords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 78 79 // initialCoords and actualCoords are needed to handle transformations 80 // and dragging of objects simultaneously. 81 // actualCoords are needed for non-points since the visible objects 82 // is transformed in the renderer. 83 // For labels and other relative texts, actualCoords is ignored, see 84 // board.initMoveObject 85 this.initialCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 86 this.actualCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 87 88 /** 89 * Relative position on a slide element (line, circle, curve) if element is a glider on this element. 90 * @type Number 91 * @private 92 */ 93 this.position = null; 94 95 /** 96 * True if there the method this.updateConstraint() has been set. It is 97 * probably different from the prototype function() {return this;}. 98 * Used in updateCoords fo glider elements. 99 * 100 * @see JXG.CoordsElement#updateCoords 101 * @type Boolean 102 * @private 103 */ 104 this.isConstrained = false; 105 106 /** 107 * Determines whether the element slides on a polygon if point is a glider. 108 * @type Boolean 109 * @default false 110 * @private 111 */ 112 this.onPolygon = false; 113 114 /** 115 * When used as a glider this member stores the object, where to glide on. 116 * To set the object to glide on use the method 117 * {@link JXG.Point#makeGlider} and DO NOT set this property directly 118 * as it will break the dependency tree. 119 * @type JXG.GeometryElement 120 */ 121 this.slideObject = null; 122 123 /** 124 * List of elements the element is bound to, i.e. the element glides on. 125 * Only the last entry is active. 126 * Use {@link JXG.Point#popSlideObject} to remove the currently active slideObject. 127 */ 128 this.slideObjects = []; 129 130 /** 131 * A {@link JXG.CoordsElement#updateGlider} call is usually followed 132 * by a general {@link JXG.Board#update} which calls 133 * {@link JXG.CoordsElement#updateGliderFromParent}. 134 * To prevent double updates, {@link JXG.CoordsElement#needsUpdateFromParent} 135 * is set to false in updateGlider() and reset to true in the following call to 136 * {@link JXG.CoordsElement#updateGliderFromParent} 137 * @type Boolean 138 */ 139 this.needsUpdateFromParent = true; 140 141 /** 142 * Stores the groups of this element in an array of Group. 143 * @type Array 144 * @see JXG.Group 145 * @private 146 */ 147 this.groups = []; 148 149 /* 150 * Do we need this? 151 */ 152 this.Xjc = null; 153 this.Yjc = null; 154 155 // documented in GeometryElement 156 this.methodMap = Type.deepCopy(this.methodMap, { 157 move: "moveTo", 158 moveTo: "moveTo", 159 moveAlong: "moveAlong", 160 visit: "visit", 161 glide: "makeGlider", 162 makeGlider: "makeGlider", 163 intersect: "makeIntersection", 164 makeIntersection: "makeIntersection", 165 X: "X", 166 Y: "Y", 167 Coords: "Coords", 168 free: "free", 169 setPosition: "setGliderPosition", 170 setGliderPosition: "setGliderPosition", 171 addConstraint: "addConstraint", 172 dist: "Dist", 173 Dist: "Dist", 174 onPolygon: "onPolygon", 175 startAnimation: "startAnimation", 176 stopAnimation: "stopAnimation" 177 }); 178 179 /* 180 * this.element may have been set by the object constructor. 181 */ 182 if (Type.exists(this.element)) { 183 this.addAnchor(coordinates, isLabel); 184 } 185 this.isDraggable = true; 186 }; 187 188 JXG.extend( 189 JXG.CoordsElement.prototype, 190 /** @lends JXG.CoordsElement.prototype */ { 191 /** 192 * Dummy function for unconstrained points or gliders. 193 * @private 194 */ 195 updateConstraint: function () { 196 return this; 197 }, 198 199 /** 200 * Updates the coordinates of the element. 201 * @private 202 */ 203 updateCoords: function (fromParent) { 204 if (!this.needsUpdate) { 205 return this; 206 } 207 208 if (!Type.exists(fromParent)) { 209 fromParent = false; 210 } 211 212 if (!this.evalVisProp('frozen')) { 213 this.updateConstraint(); 214 } 215 216 /* 217 * We need to calculate the new coordinates no matter of the elements visibility because 218 * a child could be visible and depend on the coordinates of the element/point (e.g. perpendicular). 219 * 220 * Check if the element is a glider and calculate new coords in dependency of this.slideObject. 221 * This function is called with fromParent==true in case it is a glider element for example if 222 * the defining elements of the line or circle have been changed. 223 */ 224 if (this.type === Const.OBJECT_TYPE_GLIDER) { 225 if (this.isConstrained) { 226 fromParent = false; 227 } 228 229 if (fromParent) { 230 this.updateGliderFromParent(); 231 } else { 232 this.updateGlider(); 233 } 234 } 235 this.updateTransform(fromParent); 236 237 return this; 238 }, 239 240 /** 241 * Update of glider in case of dragging the glider or setting the postion of the glider. 242 * The relative position of the glider has to be updated. 243 * 244 * In case of a glider on a line: 245 * If the second point is an ideal point, then -1 < this.position < 1, 246 * this.position==+/-1 equals point2, this.position==0 equals point1 247 * 248 * If the first point is an ideal point, then 0 < this.position < 2 249 * this.position==0 or 2 equals point1, this.position==1 equals point2 250 * 251 * @private 252 */ 253 updateGlider: function () { 254 var i, d, v, 255 p1c, p2c, poly, cc, pos, 256 angle, sgn, alpha, beta, 257 delta = 2.0 * Math.PI, 258 cp, c, invMat, 259 newCoords, newPos, 260 doRound = false, 261 ev_sw, 262 snappedTo, snapValues, 263 slide = this.slideObject, 264 res, cu, 265 slides = [], 266 isTransformed; 267 268 this.needsUpdateFromParent = false; 269 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 270 if (this.evalVisProp('isgeonext')) { 271 delta = 1.0; 272 } 273 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 274 newPos = 275 Geometry.rad( 276 [slide.center.X() + 1.0, slide.center.Y()], 277 slide.center, 278 this 279 ) / delta; 280 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 281 /* 282 * onPolygon==true: the point is a slider on a segment and this segment is one of the 283 * "borders" of a polygon. 284 * This is a GEONExT feature. 285 */ 286 if (this.onPolygon) { 287 p1c = slide.point1.coords.usrCoords; 288 p2c = slide.point2.coords.usrCoords; 289 i = 1; 290 d = p2c[i] - p1c[i]; 291 292 if (Math.abs(d) < Mat.eps) { 293 i = 2; 294 d = p2c[i] - p1c[i]; 295 } 296 297 cc = Geometry.projectPointToLine(this, slide, this.board); 298 pos = (cc.usrCoords[i] - p1c[i]) / d; 299 poly = slide.parentPolygon; 300 301 if (pos < 0) { 302 for (i = 0; i < poly.borders.length; i++) { 303 if (slide === poly.borders[i]) { 304 slide = 305 poly.borders[ 306 (i - 1 + poly.borders.length) % poly.borders.length 307 ]; 308 break; 309 } 310 } 311 } else if (pos > 1.0) { 312 for (i = 0; i < poly.borders.length; i++) { 313 if (slide === poly.borders[i]) { 314 slide = 315 poly.borders[ 316 (i + 1 + poly.borders.length) % poly.borders.length 317 ]; 318 break; 319 } 320 } 321 } 322 323 // If the slide object has changed, save the change to the glider. 324 if (slide.id !== this.slideObject.id) { 325 this.slideObject = slide; 326 } 327 } 328 329 p1c = slide.point1.coords; 330 p2c = slide.point2.coords; 331 332 // Distance between the two defining points 333 d = p1c.distance(Const.COORDS_BY_USER, p2c); 334 335 // The defining points are identical 336 if (d < Mat.eps) { 337 //this.coords.setCoordinates(Const.COORDS_BY_USER, p1c); 338 newCoords = p1c; 339 doRound = true; 340 newPos = 0.0; 341 } else { 342 newCoords = Geometry.projectPointToLine(this, slide, this.board); 343 p1c = p1c.usrCoords.slice(0); 344 p2c = p2c.usrCoords.slice(0); 345 346 // The second point is an ideal point 347 if (Math.abs(p2c[0]) < Mat.eps) { 348 i = 1; 349 d = p2c[i]; 350 351 if (Math.abs(d) < Mat.eps) { 352 i = 2; 353 d = p2c[i]; 354 } 355 356 d = (newCoords.usrCoords[i] - p1c[i]) / d; 357 sgn = d >= 0 ? 1 : -1; 358 d = Math.abs(d); 359 newPos = (sgn * d) / (d + 1); 360 361 // The first point is an ideal point 362 } else if (Math.abs(p1c[0]) < Mat.eps) { 363 i = 1; 364 d = p1c[i]; 365 366 if (Math.abs(d) < Mat.eps) { 367 i = 2; 368 d = p1c[i]; 369 } 370 371 d = (newCoords.usrCoords[i] - p2c[i]) / d; 372 373 // 1.0 - d/(1-d); 374 if (d < 0.0) { 375 newPos = (1 - 2.0 * d) / (1.0 - d); 376 } else { 377 newPos = 1 / (d + 1); 378 } 379 } else { 380 i = 1; 381 d = p2c[i] - p1c[i]; 382 383 if (Math.abs(d) < Mat.eps) { 384 i = 2; 385 d = p2c[i] - p1c[i]; 386 } 387 newPos = (newCoords.usrCoords[i] - p1c[i]) / d; 388 } 389 } 390 391 // Snap the glider to snap values. 392 snappedTo = this.findClosestSnapValue(newPos); 393 if (snappedTo !== null) { 394 snapValues = this.evalVisProp('snapvalues'); 395 newPos = (snapValues[snappedTo] - this._smin) / (this._smax - this._smin); 396 this.update(true); 397 } else { 398 // Snap the glider point of the slider into its appropriate position 399 // First, recalculate the new value of this.position 400 // Second, call update(fromParent==true) to make the positioning snappier. 401 ev_sw = this.evalVisProp('snapwidth'); 402 if ( 403 ev_sw > 0.0 && Math.abs(this._smax - this._smin) >= Mat.eps 404 ) { 405 newPos = Math.max(Math.min(newPos, 1), 0); 406 // v = newPos * (this._smax - this._smin) + this._smin; 407 // v = Math.round(v / ev_sw) * ev_sw; 408 v = newPos * (this._smax - this._smin); 409 v = Math.round(v / ev_sw) * ev_sw + this._smin; 410 newPos = (v - this._smin) / (this._smax - this._smin); 411 this.update(true); 412 } 413 } 414 415 p1c = slide.point1.coords; 416 if ( 417 !slide.evalVisProp('straightfirst') && 418 Math.abs(p1c.usrCoords[0]) > Mat.eps && 419 newPos < 0 420 ) { 421 newCoords = p1c; 422 doRound = true; 423 newPos = 0; 424 } 425 426 p2c = slide.point2.coords; 427 if ( 428 !slide.evalVisProp('straightlast') && 429 Math.abs(p2c.usrCoords[0]) > Mat.eps && 430 newPos > 1 431 ) { 432 newCoords = p2c; 433 doRound = true; 434 newPos = 1; 435 } 436 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 437 // In case, the point is a constrained glider. 438 this.updateConstraint(); 439 res = Geometry.projectPointToTurtle(this, slide, this.board); 440 newCoords = res[0]; 441 newPos = res[1]; // save position for the overwriting below 442 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 443 if ( 444 slide.type === Const.OBJECT_TYPE_ARC || 445 slide.type === Const.OBJECT_TYPE_SECTOR 446 ) { 447 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 448 449 angle = Geometry.rad(slide.radiuspoint, slide.center, this); 450 alpha = 0.0; 451 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 452 newPos = angle; 453 454 ev_sw = slide.evalVisProp('selection'); 455 if ( 456 (ev_sw === "minor" && beta > Math.PI) || 457 (ev_sw === "major" && beta < Math.PI) 458 ) { 459 alpha = beta; 460 beta = 2 * Math.PI; 461 } 462 463 // Correct the position if we are outside of the sector/arc 464 if (angle < alpha || angle > beta) { 465 newPos = beta; 466 467 if ( 468 (angle < alpha && angle > alpha * 0.5) || 469 (angle > beta && angle > beta * 0.5 + Math.PI) 470 ) { 471 newPos = alpha; 472 } 473 474 this.needsUpdateFromParent = true; 475 this.updateGliderFromParent(); 476 } 477 478 delta = beta - alpha; 479 if (this.visProp.isgeonext) { 480 delta = 1.0; 481 } 482 if (Math.abs(delta) > Mat.eps) { 483 newPos /= delta; 484 } 485 } else { 486 // In case, the point is a constrained glider. 487 this.updateConstraint(); 488 489 // Handle the case if the curve comes from a transformation of a continuous curve. 490 if (slide.transformations.length > 0) { 491 isTransformed = false; 492 // TODO this might buggy, see the recursion 493 // in line.js getCurveTangentDir 494 res = slide.getTransformationSource(); 495 if (res[0]) { 496 isTransformed = res[0]; 497 slides.push(slide); 498 slides.push(res[1]); 499 } 500 // Recurse 501 while (res[0] && Type.exists(res[1]._transformationSource)) { 502 res = res[1].getTransformationSource(); 503 slides.push(res[1]); 504 } 505 506 cu = this.coords.usrCoords; 507 if (isTransformed) { 508 for (i = 0; i < slides.length; i++) { 509 slides[i].updateTransformMatrix(); 510 invMat = Mat.inverse(slides[i].transformMat); 511 cu = Mat.matVecMult(invMat, cu); 512 } 513 cp = new Coords(Const.COORDS_BY_USER, cu, this.board).usrCoords; 514 c = Geometry.projectCoordsToCurve( 515 cp[1], 516 cp[2], 517 this.position || 0, 518 slides[slides.length - 1], 519 this.board 520 ); 521 // projectPointCurve() already would apply the transformation. 522 // Since we are projecting on the original curve, we have to do 523 // the transformations "by hand". 524 cu = c[0].usrCoords; 525 for (i = slides.length - 2; i >= 0; i--) { 526 cu = Mat.matVecMult(slides[i].transformMat, cu); 527 } 528 c[0] = new Coords(Const.COORDS_BY_USER, cu, this.board); 529 } else { 530 slide.updateTransformMatrix(); 531 invMat = Mat.inverse(slide.transformMat); 532 cu = Mat.matVecMult(invMat, cu); 533 cp = new Coords(Const.COORDS_BY_USER, cu, this.board).usrCoords; 534 c = Geometry.projectCoordsToCurve( 535 cp[1], 536 cp[2], 537 this.position || 0, 538 slide, 539 this.board 540 ); 541 } 542 543 newCoords = c[0]; 544 newPos = c[1]; 545 } else { 546 res = Geometry.projectPointToCurve(this, slide, this.board); 547 newCoords = res[0]; 548 newPos = res[1]; // save position for the overwriting below 549 } 550 } 551 } else if (Type.isPoint(slide)) { 552 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToPoint(this, slide, this.board).usrCoords, false); 553 newCoords = Geometry.projectPointToPoint(this, slide, this.board); 554 newPos = this.position; // save position for the overwriting below 555 } 556 557 this.coords.setCoordinates(Const.COORDS_BY_USER, newCoords.usrCoords, doRound); 558 this.position = newPos; 559 }, 560 561 /** 562 * Find the closest entry in snapValues that is within snapValueDistance of pos. 563 * 564 * @param {Number} pos Value for which snapping is calculated. 565 * @returns {Number} Index of the value to snap to, or null. 566 * @private 567 */ 568 findClosestSnapValue: function (pos) { 569 var i, d, 570 snapValues, snapValueDistance, 571 snappedTo = null; 572 573 // Snap the glider to snap values. 574 snapValues = this.evalVisProp('snapvalues'); 575 snapValueDistance = this.evalVisProp('snapvaluedistance'); 576 577 if (Type.isArray(snapValues) && 578 Math.abs(this._smax - this._smin) >= Mat.eps && 579 snapValueDistance > 0.0) { 580 for (i = 0; i < snapValues.length; i++) { 581 d = Math.abs(pos * (this._smax - this._smin) + this._smin - snapValues[i]); 582 if (d < snapValueDistance) { 583 snapValueDistance = d; 584 snappedTo = i; 585 } 586 } 587 } 588 589 return snappedTo; 590 }, 591 592 /** 593 * Update of a glider in case a parent element has been updated. That means the 594 * relative position of the glider stays the same. 595 * @private 596 */ 597 updateGliderFromParent: function () { 598 var p1c, p2c, r, lbda, c, 599 slide = this.slideObject, 600 slides = [], 601 res, i, isTransformed, 602 baseangle, alpha, angle, beta, 603 delta = 2.0 * Math.PI; 604 605 if (!this.needsUpdateFromParent) { 606 this.needsUpdateFromParent = true; 607 return; 608 } 609 610 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 611 r = slide.Radius(); 612 if (this.evalVisProp('isgeonext')) { 613 delta = 1.0; 614 } 615 c = [ 616 slide.center.X() + r * Math.cos(this.position * delta), 617 slide.center.Y() + r * Math.sin(this.position * delta) 618 ]; 619 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 620 p1c = slide.point1.coords.usrCoords; 621 p2c = slide.point2.coords.usrCoords; 622 623 // If one of the defining points of the line does not exist, 624 // the glider should disappear 625 if ( 626 (p1c[0] === 0 && p1c[1] === 0 && p1c[2] === 0) || 627 (p2c[0] === 0 && p2c[1] === 0 && p2c[2] === 0) 628 ) { 629 c = [0, 0, 0]; 630 // The second point is an ideal point 631 } else if (Math.abs(p2c[0]) < Mat.eps) { 632 lbda = Math.min(Math.abs(this.position), 1 - Mat.eps); 633 lbda /= 1.0 - lbda; 634 635 if (this.position < 0) { 636 lbda = -lbda; 637 } 638 639 c = [ 640 p1c[0] + lbda * p2c[0], 641 p1c[1] + lbda * p2c[1], 642 p1c[2] + lbda * p2c[2] 643 ]; 644 // The first point is an ideal point 645 } else if (Math.abs(p1c[0]) < Mat.eps) { 646 lbda = Math.max(this.position, Mat.eps); 647 lbda = Math.min(lbda, 2 - Mat.eps); 648 649 if (lbda > 1) { 650 lbda = (lbda - 1) / (lbda - 2); 651 } else { 652 lbda = (1 - lbda) / lbda; 653 } 654 655 c = [ 656 p2c[0] + lbda * p1c[0], 657 p2c[1] + lbda * p1c[1], 658 p2c[2] + lbda * p1c[2] 659 ]; 660 } else { 661 lbda = this.position; 662 c = [ 663 p1c[0] + lbda * (p2c[0] - p1c[0]), 664 p1c[1] + lbda * (p2c[1] - p1c[1]), 665 p1c[2] + lbda * (p2c[2] - p1c[2]) 666 ]; 667 } 668 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 669 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 670 slide.Z(this.position), 671 slide.X(this.position), 672 slide.Y(this.position) 673 ]); 674 // In case, the point is a constrained glider. 675 this.updateConstraint(); 676 c = Geometry.projectPointToTurtle(this, slide, this.board)[0].usrCoords; 677 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 678 // Handle the case if the curve comes from a transformation of a continuous curve. 679 isTransformed = false; 680 res = slide.getTransformationSource(); 681 if (res[0]) { 682 isTransformed = res[0]; 683 slides.push(slide); 684 slides.push(res[1]); 685 } 686 // Recurse 687 while (res[0] && Type.exists(res[1]._transformationSource)) { 688 res = res[1].getTransformationSource(); 689 slides.push(res[1]); 690 } 691 if (isTransformed) { 692 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 693 slides[slides.length - 1].Z(this.position), 694 slides[slides.length - 1].X(this.position), 695 slides[slides.length - 1].Y(this.position) 696 ]); 697 } else { 698 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 699 slide.Z(this.position), 700 slide.X(this.position), 701 slide.Y(this.position) 702 ]); 703 } 704 705 if ( 706 slide.type === Const.OBJECT_TYPE_ARC || 707 slide.type === Const.OBJECT_TYPE_SECTOR 708 ) { 709 baseangle = Geometry.rad( 710 [slide.center.X() + 1, slide.center.Y()], 711 slide.center, 712 slide.radiuspoint 713 ); 714 715 alpha = 0.0; 716 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 717 718 if ( 719 (slide.visProp.selection === "minor" && beta > Math.PI) || 720 (slide.visProp.selection === "major" && beta < Math.PI) 721 ) { 722 alpha = beta; 723 beta = 2 * Math.PI; 724 } 725 726 delta = beta - alpha; 727 if (this.evalVisProp('isgeonext')) { 728 delta = 1.0; 729 } 730 angle = this.position * delta; 731 732 // Correct the position if we are outside of the sector/arc 733 if (angle < alpha || angle > beta) { 734 angle = beta; 735 736 if ( 737 (angle < alpha && angle > alpha * 0.5) || 738 (angle > beta && angle > beta * 0.5 + Math.PI) 739 ) { 740 angle = alpha; 741 } 742 743 this.position = angle; 744 if (Math.abs(delta) > Mat.eps) { 745 this.position /= delta; 746 } 747 } 748 749 r = slide.Radius(); 750 c = [ 751 slide.center.X() + r * Math.cos(this.position * delta + baseangle), 752 slide.center.Y() + r * Math.sin(this.position * delta + baseangle) 753 ]; 754 } else { 755 // In case, the point is a constrained glider. 756 this.updateConstraint(); 757 758 if (isTransformed) { 759 c = Geometry.projectPointToCurve( 760 this, 761 slides[slides.length - 1], 762 this.board 763 )[0].usrCoords; 764 // projectPointCurve() already would do the transformation. 765 // But since we are projecting on the original curve, we have to do 766 // the transformation "by hand". 767 for (i = slides.length - 2; i >= 0; i--) { 768 c = new Coords( 769 Const.COORDS_BY_USER, 770 Mat.matVecMult(slides[i].transformMat, c), 771 this.board 772 ).usrCoords; 773 } 774 } else { 775 c = Geometry.projectPointToCurve(this, slide, this.board)[0].usrCoords; 776 } 777 } 778 } else if (Type.isPoint(slide)) { 779 c = Geometry.projectPointToPoint(this, slide, this.board).usrCoords; 780 } 781 782 this.coords.setCoordinates(Const.COORDS_BY_USER, c, false); 783 }, 784 785 updateRendererGeneric: function (rendererMethod) { 786 //var wasReal; 787 788 if (!this.needsUpdate || !this.board.renderer) { 789 return this; 790 } 791 792 if (this.visPropCalc.visible) { 793 //wasReal = this.isReal; 794 this.isReal = !isNaN(this.coords.usrCoords[1] + this.coords.usrCoords[2]); 795 //Homogeneous coords: ideal point 796 this.isReal = 797 Math.abs(this.coords.usrCoords[0]) > Mat.eps ? this.isReal : false; 798 799 if ( 800 // wasReal && 801 !this.isReal 802 ) { 803 this.updateVisibility(false); 804 } 805 } 806 807 // Call the renderer only if element is visible. 808 // Update the position 809 if (this.visPropCalc.visible) { 810 this.board.renderer[rendererMethod](this); 811 } 812 813 // Update the label if visible. 814 if ( 815 this.hasLabel && 816 this.visPropCalc.visible && 817 this.label && 818 this.label.visPropCalc.visible && 819 this.isReal 820 ) { 821 this.label.update(); 822 this.board.renderer.updateText(this.label); 823 } 824 825 // Update rendNode display 826 this.setDisplayRendNode(); 827 // if (this.visPropCalc.visible !== this.visPropOld.visible) { 828 // this.board.renderer.display(this, this.visPropCalc.visible); 829 // this.visPropOld.visible = this.visPropCalc.visible; 830 // 831 // if (this.hasLabel) { 832 // this.board.renderer.display(this.label, this.label.visPropCalc.visible); 833 // } 834 // } 835 836 this.needsUpdate = false; 837 return this; 838 }, 839 840 /** 841 * Getter method for x, this is used by for CAS-points to access point coordinates. 842 * @returns {Number} User coordinate of point in x direction. 843 */ 844 X: function () { 845 return this.coords.usrCoords[1]; 846 }, 847 848 /** 849 * Getter method for y, this is used by CAS-points to access point coordinates. 850 * @returns {Number} User coordinate of point in y direction. 851 */ 852 Y: function () { 853 return this.coords.usrCoords[2]; 854 }, 855 856 /** 857 * Getter method for z, this is used by CAS-points to access point coordinates. 858 * @returns {Number} User coordinate of point in z direction. 859 */ 860 Z: function () { 861 return this.coords.usrCoords[0]; 862 }, 863 864 /** 865 * Getter method for coordinates x, y and (optional) z. 866 * @param {Number|String} [digits='auto'] Truncating rule for the digits in the infobox. 867 * <ul> 868 * <li>'auto': done automatically by JXG.autoDigits() 869 * <li>'none': no truncation 870 * <li>number: truncate after "number digits" with JXG.toFixed() 871 * </ul> 872 * @param {Boolean} [withZ=false] If set to true the return value will be <tt>(x | y | z)</tt> instead of <tt>(x, y)</tt>. 873 * @returns {String} User coordinates of point. 874 */ 875 Coords: function (withZ) { 876 if (withZ) { 877 return this.coords.usrCoords.slice(); 878 } 879 return this.coords.usrCoords.slice(1); 880 }, 881 // Coords: function (digits, withZ) { 882 // var arr, sep; 883 884 // digits = digits || 'auto'; 885 886 // if (withZ) { 887 // sep = ' | '; 888 // } else { 889 // sep = ', '; 890 // } 891 892 // if (digits === 'none') { 893 // arr = [this.X(), sep, this.Y()]; 894 // if (withZ) { 895 // arr.push(sep, this.Z()); 896 // } 897 898 // } else if (digits === 'auto') { 899 // if (this.useLocale()) { 900 // arr = [this.formatNumberLocale(this.X()), sep, this.formatNumberLocale(this.Y())]; 901 // if (withZ) { 902 // arr.push(sep, this.formatNumberLocale(this.Z())); 903 // } 904 // } else { 905 // arr = [Type.autoDigits(this.X()), sep, Type.autoDigits(this.Y())]; 906 // if (withZ) { 907 // arr.push(sep, Type.autoDigits(this.Z())); 908 // } 909 // } 910 911 // } else { 912 // if (this.useLocale()) { 913 // arr = [this.formatNumberLocale(this.X(), digits), sep, this.formatNumberLocale(this.Y(), digits)]; 914 // if (withZ) { 915 // arr.push(sep, this.formatNumberLocale(this.Z(), digits)); 916 // } 917 // } else { 918 // arr = [Type.toFixed(this.X(), digits), sep, Type.toFixed(this.Y(), digits)]; 919 // if (withZ) { 920 // arr.push(sep, Type.toFixed(this.Z(), digits)); 921 // } 922 // } 923 // } 924 925 // return '(' + arr.join('') + ')'; 926 // }, 927 928 /** 929 * New evaluation of the function term. 930 * This is required for CAS-points: Their XTerm() method is 931 * overwritten in {@link JXG.CoordsElement#addConstraint}. 932 * 933 * @returns {Number} User coordinate of point in x direction. 934 * @private 935 */ 936 XEval: function () { 937 return this.coords.usrCoords[1]; 938 }, 939 940 /** 941 * New evaluation of the function term. 942 * This is required for CAS-points: Their YTerm() method is overwritten 943 * in {@link JXG.CoordsElement#addConstraint}. 944 * 945 * @returns {Number} User coordinate of point in y direction. 946 * @private 947 */ 948 YEval: function () { 949 return this.coords.usrCoords[2]; 950 }, 951 952 /** 953 * New evaluation of the function term. 954 * This is required for CAS-points: Their ZTerm() method is overwritten in 955 * {@link JXG.CoordsElement#addConstraint}. 956 * 957 * @returns {Number} User coordinate of point in z direction. 958 * @private 959 */ 960 ZEval: function () { 961 return this.coords.usrCoords[0]; 962 }, 963 964 /** 965 * Getter method for the distance to a second point, this is required for CAS-elements. 966 * Here, function inlining seems to be worthwile (for plotting). 967 * @param {JXG.Point} point2 The point to which the distance shall be calculated. 968 * @returns {Number} Distance in user coordinate to the given point 969 */ 970 Dist: function (point2) { 971 if (this.isReal && point2.isReal) { 972 return this.coords.distance(Const.COORDS_BY_USER, point2.coords); 973 } 974 return NaN; 975 }, 976 977 /** 978 * Alias for {@link JXG.Element#handleSnapToGrid} 979 * @param {Boolean} force force snapping independent of what the snaptogrid attribute says 980 * @returns {JXG.CoordsElement} Reference to this element 981 */ 982 snapToGrid: function (force) { 983 return this.handleSnapToGrid(force); 984 }, 985 986 /** 987 * Let a point snap to the nearest point in distance of 988 * {@link JXG.Point#attractorDistance}. 989 * The function uses the coords object of the point as 990 * its actual position. 991 * @param {Boolean} force force snapping independent of what the snaptogrid attribute says 992 * @returns {JXG.CoordsElement} Reference to this element 993 */ 994 handleSnapToPoints: function (force) { 995 var i, 996 pEl, 997 pCoords, 998 d = 0, 999 len, 1000 dMax = Infinity, 1001 c = null, 1002 ev_au, 1003 ev_ad, 1004 ev_is2p = this.evalVisProp('ignoredsnaptopoints'), 1005 len2, 1006 j, 1007 ignore = false; 1008 1009 len = this.board.objectsList.length; 1010 1011 if (ev_is2p) { 1012 len2 = ev_is2p.length; 1013 } 1014 1015 if (this.evalVisProp('snaptopoints') || force) { 1016 ev_au = this.evalVisProp('attractorunit'); 1017 ev_ad = this.evalVisProp('attractordistance'); 1018 1019 for (i = 0; i < len; i++) { 1020 pEl = this.board.objectsList[i]; 1021 1022 if (ev_is2p) { 1023 ignore = false; 1024 for (j = 0; j < len2; j++) { 1025 if (pEl === this.board.select(ev_is2p[j])) { 1026 ignore = true; 1027 break; 1028 } 1029 } 1030 if (ignore) { 1031 continue; 1032 } 1033 } 1034 1035 if (Type.isPoint(pEl) && pEl !== this && pEl.visPropCalc.visible) { 1036 pCoords = Geometry.projectPointToPoint(this, pEl, this.board); 1037 if (ev_au === "screen") { 1038 d = pCoords.distance(Const.COORDS_BY_SCREEN, this.coords); 1039 } else { 1040 d = pCoords.distance(Const.COORDS_BY_USER, this.coords); 1041 } 1042 1043 if (d < ev_ad && d < dMax) { 1044 dMax = d; 1045 c = pCoords; 1046 } 1047 } 1048 } 1049 1050 if (c !== null) { 1051 this.coords.setCoordinates(Const.COORDS_BY_USER, c.usrCoords); 1052 } 1053 } 1054 1055 return this; 1056 }, 1057 1058 /** 1059 * Alias for {@link JXG.CoordsElement#handleSnapToPoints}. 1060 * 1061 * @param {Boolean} force force snapping independent of what the snaptogrid attribute says 1062 * @returns {JXG.CoordsElement} Reference to this element 1063 */ 1064 snapToPoints: function (force) { 1065 return this.handleSnapToPoints(force); 1066 }, 1067 1068 /** 1069 * A point can change its type from free point to glider 1070 * and vice versa. If it is given an array of attractor elements 1071 * (attribute attractors) and the attribute attractorDistance 1072 * then the point will be made a glider if it less than attractorDistance 1073 * apart from one of its attractor elements. 1074 * If attractorDistance is equal to zero, the point stays in its 1075 * current form. 1076 * @returns {JXG.CoordsElement} Reference to this element 1077 */ 1078 handleAttractors: function () { 1079 var i, 1080 el, 1081 projCoords, 1082 d = 0.0, 1083 projection, 1084 ev_au = this.evalVisProp('attractorunit'), 1085 ev_ad = this.evalVisProp('attractordistance'), 1086 ev_sd = this.evalVisProp('snatchdistance'), 1087 ev_a = this.evalVisProp('attractors'), 1088 len = ev_a.length; 1089 1090 if (ev_ad === 0.0) { 1091 return; 1092 } 1093 1094 for (i = 0; i < len; i++) { 1095 el = this.board.select(ev_a[i]); 1096 1097 if (Type.exists(el) && el !== this) { 1098 if (Type.isPoint(el)) { 1099 projCoords = Geometry.projectPointToPoint(this, el, this.board); 1100 } else if (el.elementClass === Const.OBJECT_CLASS_LINE) { 1101 projection = Geometry.projectCoordsToSegment( 1102 this.coords.usrCoords, 1103 el.point1.coords.usrCoords, 1104 el.point2.coords.usrCoords 1105 ); 1106 if (!el.evalVisProp('straightfirst') && projection[1] < 0.0) { 1107 projCoords = el.point1.coords; 1108 } else if ( 1109 !el.evalVisProp('straightlast') && 1110 projection[1] > 1.0 1111 ) { 1112 projCoords = el.point2.coords; 1113 } else { 1114 projCoords = new Coords( 1115 Const.COORDS_BY_USER, 1116 projection[0], 1117 this.board 1118 ); 1119 } 1120 } else if (el.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1121 projCoords = Geometry.projectPointToCircle(this, el, this.board); 1122 } else if (el.elementClass === Const.OBJECT_CLASS_CURVE) { 1123 projCoords = Geometry.projectPointToCurve(this, el, this.board)[0]; 1124 } else if (el.type === Const.OBJECT_TYPE_TURTLE) { 1125 projCoords = Geometry.projectPointToTurtle(this, el, this.board)[0]; 1126 } else if (el.type === Const.OBJECT_TYPE_POLYGON) { 1127 projCoords = new Coords( 1128 Const.COORDS_BY_USER, 1129 Geometry.projectCoordsToPolygon(this.coords.usrCoords, el), 1130 this.board 1131 ); 1132 } 1133 1134 if (ev_au === "screen") { 1135 d = projCoords.distance(Const.COORDS_BY_SCREEN, this.coords); 1136 } else { 1137 d = projCoords.distance(Const.COORDS_BY_USER, this.coords); 1138 } 1139 1140 if (d < ev_ad) { 1141 if ( 1142 !( 1143 this.type === Const.OBJECT_TYPE_GLIDER && 1144 (el === this.slideObject || 1145 (this.slideObject && 1146 this.onPolygon && 1147 this.slideObject.parentPolygon === el)) 1148 ) 1149 ) { 1150 this.makeGlider(el); 1151 } 1152 break; // bind the point to the first attractor in its list. 1153 } 1154 if ( 1155 d >= ev_sd && 1156 (el === this.slideObject || 1157 (this.slideObject && 1158 this.onPolygon && 1159 this.slideObject.parentPolygon === el)) 1160 ) { 1161 this.popSlideObject(); 1162 } 1163 } 1164 } 1165 1166 return this; 1167 }, 1168 1169 /** 1170 * Sets coordinates and calls the elements's update() method. 1171 * @param {Number} method The type of coordinates used here. 1172 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 1173 * @param {Array} coords coordinates <tt>([z], x, y)</tt> in screen/user units 1174 * @returns {JXG.CoordsElement} this element 1175 */ 1176 setPositionDirectly: function (method, coords) { 1177 var i, 1178 c, dc, m, 1179 oldCoords = this.coords, 1180 newCoords; 1181 1182 if (this.relativeCoords) { 1183 c = new Coords(method, coords, this.board); 1184 if (this.evalVisProp('islabel')) { 1185 dc = Statistics.subtract(c.scrCoords, oldCoords.scrCoords); 1186 this.relativeCoords.scrCoords[1] += dc[1]; 1187 this.relativeCoords.scrCoords[2] += dc[2]; 1188 } else { 1189 dc = Statistics.subtract(c.usrCoords, oldCoords.usrCoords); 1190 this.relativeCoords.usrCoords[1] += dc[1]; 1191 this.relativeCoords.usrCoords[2] += dc[2]; 1192 } 1193 1194 return this; 1195 } 1196 1197 this.coords.setCoordinates(method, coords); 1198 this.handleSnapToGrid(); 1199 this.handleSnapToPoints(); 1200 this.handleAttractors(); 1201 1202 // Here, we set the object's "actualCoords", because 1203 // coords and initialCoords coincide since transformations 1204 // for these elements are handled in the renderers. 1205 this.actualCoords.setCoordinates(Const.COORDS_BY_USER, this.coords.usrCoords); 1206 1207 // The element's coords have been set above to the new position `coords`. 1208 // Now, determine the preimage of `coords`, prior to all transformations. 1209 // This is needed for free elements that have a transformation bound to it. 1210 if (this.transformations.length > 0) { 1211 if (method === Const.COORDS_BY_SCREEN) { 1212 newCoords = new Coords(method, coords, this.board).usrCoords; 1213 } else { 1214 if (coords.length === 2) { 1215 coords = [1].concat(coords); 1216 } 1217 newCoords = coords; 1218 } 1219 m = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; 1220 for (i = 0; i < this.transformations.length; i++) { 1221 m = Mat.matMatMult(this.transformations[i].matrix, m); 1222 } 1223 newCoords = Mat.matVecMult(Mat.inverse(m), newCoords); 1224 1225 this.initialCoords.setCoordinates(Const.COORDS_BY_USER, newCoords); 1226 if (this.elementClass !== Const.OBJECT_CLASS_POINT) { 1227 // This is necessary for images and texts. 1228 this.coords.setCoordinates(Const.COORDS_BY_USER, newCoords); 1229 } 1230 } 1231 this.prepareUpdate().update(); 1232 1233 // If the user suspends the board updates we need to recalculate the relative position of 1234 // the point on the slide object. This is done in updateGlider() which is NOT called during the 1235 // update process triggered by unsuspendUpdate. 1236 if (this.board.isSuspendedUpdate && this.type === Const.OBJECT_TYPE_GLIDER) { 1237 this.updateGlider(); 1238 } 1239 1240 return this; 1241 }, 1242 1243 /** 1244 * Translates the point by <tt>tv = (x, y)</tt>. 1245 * @param {Number} method The type of coordinates used here. 1246 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 1247 * @param {Array} tv (x, y) 1248 * @returns {JXG.CoordsElement} 1249 */ 1250 setPositionByTransform: function (method, tv) { 1251 var t; 1252 1253 tv = new Coords(method, tv, this.board); 1254 t = this.board.create("transform", tv.usrCoords.slice(1), { 1255 type: "translate" 1256 }); 1257 1258 if ( 1259 this.transformations.length > 0 && 1260 this.transformations[this.transformations.length - 1].isNumericMatrix 1261 ) { 1262 this.transformations[this.transformations.length - 1].melt(t); 1263 } else { 1264 this.addTransform(this, t); 1265 } 1266 1267 this.prepareUpdate().update(); 1268 1269 return this; 1270 }, 1271 1272 /** 1273 * Sets coordinates and calls the element's update() method. 1274 * @param {Number} method The type of coordinates used here. 1275 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 1276 * @param {Array} coords coordinates in screen/user units 1277 * @returns {JXG.CoordsElement} 1278 */ 1279 setPosition: function (method, coords) { 1280 return this.setPositionDirectly(method, coords); 1281 }, 1282 1283 /** 1284 * Sets the position of a glider relative to the defining elements 1285 * of the {@link JXG.Point#slideObject}. 1286 * @param {Number} x 1287 * @returns {JXG.Point} Reference to the point element. 1288 */ 1289 setGliderPosition: function (x) { 1290 if (this.type === Const.OBJECT_TYPE_GLIDER) { 1291 this.position = x; 1292 this.board.update(); 1293 } 1294 1295 return this; 1296 }, 1297 1298 /** 1299 * Convert the point to glider and update the construction. 1300 * To move the point visual onto the glider, a call of board update is necessary. 1301 * @param {String|Object} slide The object the point will be bound to. 1302 */ 1303 makeGlider: function (slide) { 1304 var slideobj = this.board.select(slide), 1305 onPolygon = false, 1306 min, i, dist; 1307 1308 if (slideobj.type === Const.OBJECT_TYPE_POLYGON) { 1309 // Search for the closest edge of the polygon. 1310 min = Number.MAX_VALUE; 1311 for (i = 0; i < slideobj.borders.length; i++) { 1312 dist = JXG.Math.Geometry.distPointLine( 1313 this.coords.usrCoords, 1314 slideobj.borders[i].stdform 1315 ); 1316 if (dist < min) { 1317 min = dist; 1318 slide = slideobj.borders[i]; 1319 } 1320 } 1321 slideobj = this.board.select(slide); 1322 onPolygon = true; 1323 } 1324 1325 /* Gliders on Ticks are forbidden */ 1326 if (!Type.exists(slideobj)) { 1327 throw new Error("JSXGraph: slide object undefined."); 1328 } else if (slideobj.type === Const.OBJECT_TYPE_TICKS) { 1329 throw new Error("JSXGraph: gliders on ticks are not possible."); 1330 } 1331 1332 this.slideObject = this.board.select(slide); 1333 this.slideObjects.push(this.slideObject); 1334 this.addParents(slide); 1335 1336 this.type = Const.OBJECT_TYPE_GLIDER; 1337 this.elType = 'glider'; 1338 this.visProp.snapwidth = -1; // By default, deactivate snapWidth 1339 this.slideObject.addChild(this); 1340 this.isDraggable = true; 1341 this.onPolygon = onPolygon; 1342 1343 this.generatePolynomial = function () { 1344 return this.slideObject.generatePolynomial(this); 1345 }; 1346 1347 // Determine the initial value of this.position 1348 this.updateGlider(); 1349 this.needsUpdateFromParent = true; 1350 this.updateGliderFromParent(); 1351 1352 return this; 1353 }, 1354 1355 /** 1356 * Remove the last slideObject. If there are more than one elements the point is bound to, 1357 * the second last element is the new active slideObject. 1358 */ 1359 popSlideObject: function () { 1360 if (this.slideObjects.length > 0) { 1361 this.slideObjects.pop(); 1362 1363 // It may not be sufficient to remove the point from 1364 // the list of childElement. For complex dependencies 1365 // one may have to go to the list of ancestor and descendants. A.W. 1366 // Yes indeed, see #51 on github bug tracker 1367 // delete this.slideObject.childElements[this.id]; 1368 this.slideObject.removeChild(this); 1369 1370 if (this.slideObjects.length === 0) { 1371 this.type = this._org_type; 1372 if (this.type === Const.OBJECT_TYPE_POINT) { 1373 this.elType = "point"; 1374 } else if (this.elementClass === Const.OBJECT_CLASS_TEXT) { 1375 this.elType = "text"; 1376 } else if (this.type === Const.OBJECT_TYPE_IMAGE) { 1377 this.elType = "image"; 1378 } else if (this.type === Const.OBJECT_TYPE_FOREIGNOBJECT) { 1379 this.elType = "foreignobject"; 1380 } 1381 1382 this.slideObject = null; 1383 } else { 1384 this.slideObject = this.slideObjects[this.slideObjects.length - 1]; 1385 } 1386 } 1387 }, 1388 1389 /** 1390 * Converts a calculated element into a free element, 1391 * i.e. it will delete all ancestors and transformations and, 1392 * if the element is currently a glider, will remove the slideObject reference. 1393 */ 1394 free: function () { 1395 var ancestorId, ancestor; 1396 // child; 1397 1398 if (this.type !== Const.OBJECT_TYPE_GLIDER) { 1399 // remove all transformations 1400 this.transformations.length = 0; 1401 1402 delete this.updateConstraint; 1403 this.isConstrained = false; 1404 // this.updateConstraint = function () { 1405 // return this; 1406 // }; 1407 1408 if (!this.isDraggable) { 1409 this.isDraggable = true; 1410 1411 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1412 this.type = Const.OBJECT_TYPE_POINT; 1413 this.elType = "point"; 1414 } 1415 1416 this.XEval = function () { 1417 return this.coords.usrCoords[1]; 1418 }; 1419 1420 this.YEval = function () { 1421 return this.coords.usrCoords[2]; 1422 }; 1423 1424 this.ZEval = function () { 1425 return this.coords.usrCoords[0]; 1426 }; 1427 1428 this.Xjc = null; 1429 this.Yjc = null; 1430 } else { 1431 return; 1432 } 1433 } 1434 1435 // a free point does not depend on anything. And instead of running through tons of descendants and ancestor 1436 // structures, where we eventually are going to visit a lot of objects twice or thrice with hard to read and 1437 // comprehend code, just run once through all objects and delete all references to this point and its label. 1438 for (ancestorId in this.board.objects) { 1439 if (this.board.objects.hasOwnProperty(ancestorId)) { 1440 ancestor = this.board.objects[ancestorId]; 1441 1442 if (ancestor.descendants) { 1443 delete ancestor.descendants[this.id]; 1444 delete ancestor.childElements[this.id]; 1445 1446 if (this.hasLabel) { 1447 delete ancestor.descendants[this.label.id]; 1448 delete ancestor.childElements[this.label.id]; 1449 } 1450 } 1451 } 1452 } 1453 1454 // A free point does not depend on anything. Remove all ancestors. 1455 this.ancestors = {}; // only remove the reference 1456 this.parents = []; 1457 1458 // Completely remove all slideObjects of the element 1459 this.slideObject = null; 1460 this.slideObjects = []; 1461 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1462 this.type = Const.OBJECT_TYPE_POINT; 1463 this.elType = "point"; 1464 } else if (this.elementClass === Const.OBJECT_CLASS_TEXT) { 1465 this.type = this._org_type; 1466 this.elType = "text"; 1467 } else if (this.elementClass === Const.OBJECT_CLASS_OTHER) { 1468 this.type = this._org_type; 1469 this.elType = "image"; 1470 } 1471 }, 1472 1473 /** 1474 * Convert the point to CAS point and call update(). 1475 * @param {Array} terms [[zterm], xterm, yterm] defining terms for the z, x and y coordinate. 1476 * The z-coordinate is optional and it is used for homogeneous coordinates. 1477 * The coordinates may be either <ul> 1478 * <li>a JavaScript function,</li> 1479 * <li>a string containing GEONExT syntax. This string will be converted into a JavaScript 1480 * function here,</li> 1481 * <li>a Number</li> 1482 * <li>a pointer to a slider object. This will be converted into a call of the Value()-method 1483 * of this slider.</li> 1484 * </ul> 1485 * @see JXG.GeonextParser#geonext2JS 1486 */ 1487 addConstraint: function (terms) { 1488 var i, v, 1489 newfuncs = [], 1490 what = ["X", "Y"], 1491 makeConstFunction = function (z) { 1492 return function () { 1493 return z; 1494 }; 1495 }, 1496 makeSliderFunction = function (a) { 1497 return function () { 1498 return a.Value(); 1499 }; 1500 }; 1501 1502 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1503 this.type = Const.OBJECT_TYPE_CAS; 1504 } 1505 1506 this.isDraggable = false; 1507 1508 for (i = 0; i < terms.length; i++) { 1509 v = terms[i]; 1510 1511 if (Type.isString(v)) { 1512 // Convert GEONExT syntax into JavaScript syntax 1513 //t = JXG.GeonextParser.geonext2JS(v, this.board); 1514 //newfuncs[i] = new Function('','return ' + t + ';'); 1515 //v = GeonextParser.replaceNameById(v, this.board); 1516 newfuncs[i] = this.board.jc.snippet(v, true, null, true); 1517 this.addParentsFromJCFunctions([newfuncs[i]]); 1518 1519 // Store original term as 'Xjc' or 'Yjc' 1520 if (terms.length === 2) { 1521 this[what[i] + "jc"] = terms[i]; 1522 } 1523 } else if (Type.isFunction(v)) { 1524 newfuncs[i] = v; 1525 } else if (Type.isNumber(v)) { 1526 newfuncs[i] = makeConstFunction(v); 1527 } else if (Type.isObject(v) && Type.isFunction(v.Value)) { 1528 // Slider 1529 newfuncs[i] = makeSliderFunction(v); 1530 } 1531 1532 newfuncs[i].origin = v; 1533 } 1534 1535 // Intersection function 1536 if (terms.length === 1) { 1537 this.updateConstraint = function () { 1538 var c = newfuncs[0](); 1539 1540 // Array 1541 if (Type.isArray(c)) { 1542 this.coords.setCoordinates(Const.COORDS_BY_USER, c); 1543 // Coords object 1544 } else { 1545 this.coords = c; 1546 } 1547 return this; 1548 }; 1549 // Euclidean coordinates 1550 } else if (terms.length === 2) { 1551 this.XEval = newfuncs[0]; 1552 this.YEval = newfuncs[1]; 1553 this.addParents([newfuncs[0].origin, newfuncs[1].origin]); 1554 1555 this.updateConstraint = function () { 1556 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 1557 this.XEval(), 1558 this.YEval() 1559 ]); 1560 return this; 1561 }; 1562 // Homogeneous coordinates 1563 } else { 1564 this.ZEval = newfuncs[0]; 1565 this.XEval = newfuncs[1]; 1566 this.YEval = newfuncs[2]; 1567 1568 this.addParents([newfuncs[0].origin, newfuncs[1].origin, newfuncs[2].origin]); 1569 1570 this.updateConstraint = function () { 1571 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 1572 this.ZEval(), 1573 this.XEval(), 1574 this.YEval() 1575 ]); 1576 return this; 1577 }; 1578 } 1579 this.isConstrained = true; 1580 1581 /** 1582 * We have to do an update. Otherwise, elements relying on this point will receive NaN. 1583 */ 1584 this.prepareUpdate().update(); 1585 if (!this.board.isSuspendedUpdate) { 1586 this.updateVisibility().updateRenderer(); 1587 if (this.hasLabel) { 1588 this.label.fullUpdate(); 1589 } 1590 } 1591 1592 return this; 1593 }, 1594 1595 /** 1596 * In case there is an attribute "anchor", the element is bound to 1597 * this anchor element. 1598 * This is handled with this.relativeCoords. If the element is a label 1599 * relativeCoords are given in scrCoords, otherwise in usrCoords. 1600 * @param{Array} coordinates Offset from the anchor element. These are the values for this.relativeCoords. 1601 * In case of a label, coordinates are screen coordinates. Otherwise, coordinates are user coordinates. 1602 * @param{Boolean} isLabel Yes/no 1603 * @private 1604 */ 1605 addAnchor: function (coordinates, isLabel) { 1606 if (isLabel) { 1607 this.relativeCoords = new Coords( 1608 Const.COORDS_BY_SCREEN, 1609 coordinates.slice(0, 2), 1610 this.board 1611 ); 1612 } else { 1613 this.relativeCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 1614 } 1615 this.element.addChild(this); 1616 if (isLabel) { 1617 this.addParents(this.element); 1618 } 1619 1620 this.XEval = function () { 1621 var sx, coords, anchor, ev_o; 1622 1623 if (this.evalVisProp('islabel')) { 1624 ev_o = this.evalVisProp('offset'); 1625 sx = parseFloat(ev_o[0]); 1626 anchor = this.element.getLabelAnchor(); 1627 coords = new Coords( 1628 Const.COORDS_BY_SCREEN, 1629 [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], 1630 this.board 1631 ); 1632 1633 return coords.usrCoords[1]; 1634 } 1635 1636 anchor = this.element.getTextAnchor(); 1637 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1]; 1638 }; 1639 1640 this.YEval = function () { 1641 var sy, coords, anchor, ev_o; 1642 1643 if (this.evalVisProp('islabel')) { 1644 ev_o = this.evalVisProp('offset'); 1645 sy = -parseFloat(ev_o[1]); 1646 anchor = this.element.getLabelAnchor(); 1647 coords = new Coords( 1648 Const.COORDS_BY_SCREEN, 1649 [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], 1650 this.board 1651 ); 1652 1653 return coords.usrCoords[2]; 1654 } 1655 1656 anchor = this.element.getTextAnchor(); 1657 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2]; 1658 }; 1659 1660 this.ZEval = Type.createFunction(1, this.board, ""); 1661 1662 this.updateConstraint = function () { 1663 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 1664 this.ZEval(), 1665 this.XEval(), 1666 this.YEval() 1667 ]); 1668 }; 1669 this.isConstrained = true; 1670 1671 this.updateConstraint(); 1672 }, 1673 1674 /** 1675 * Applies the transformations of the element. 1676 * This method applies to text and images. Point transformations are handled differently. 1677 * @param {Boolean} fromParent True if the drag comes from a child element. Unused. 1678 * @returns {JXG.CoordsElement} Reference to itself. 1679 */ 1680 updateTransform: function (fromParent) { 1681 var c, i; 1682 1683 if (this.transformations.length === 0 || this.baseElement === null) { 1684 return this; 1685 } 1686 1687 // This method is called for non-points only. 1688 // Here, we set the object's "actualCoords", because 1689 // coords and initialCoords coincide since transformations 1690 // for these elements are handled in the renderers. 1691 1692 this.transformations[0].update(); 1693 if (this === this.baseElement) { 1694 // Case of bindTo 1695 c = this.transformations[0].apply(this, "self"); 1696 } else { 1697 c = this.transformations[0].apply(this.baseElement); 1698 } 1699 for (i = 1; i < this.transformations.length; i++) { 1700 this.transformations[i].update(); 1701 c = Mat.matVecMult(this.transformations[i].matrix, c); 1702 } 1703 this.actualCoords.setCoordinates(Const.COORDS_BY_USER, c); 1704 1705 return this; 1706 }, 1707 1708 /** 1709 * Add transformations to this element. 1710 * @param {JXG.GeometryElement} el 1711 * @param {JXG.Transformation|Array} transform Either one {@link JXG.Transformation} 1712 * or an array of {@link JXG.Transformation}s. 1713 * @returns {JXG.CoordsElement} Reference to itself. 1714 */ 1715 addTransform: function (el, transform) { 1716 var i, 1717 list = Type.isArray(transform) ? transform : [transform], 1718 len = list.length; 1719 1720 // There is only one baseElement possible 1721 if (this.transformations.length === 0) { 1722 this.baseElement = el; 1723 } 1724 1725 for (i = 0; i < len; i++) { 1726 this.transformations.push(list[i]); 1727 } 1728 1729 return this; 1730 }, 1731 1732 /** 1733 * Animate the point. 1734 * @param {Number|Function} direction The direction the glider is animated. Can be +1 or -1. 1735 * @param {Number|Function} stepCount The number of steps in which the parent element is divided. 1736 * Must be at least 1. 1737 * @param {Number|Function} delay Time in msec between two animation steps. Default is 250. 1738 * @returns {JXG.CoordsElement} Reference to iself. 1739 * 1740 * @name Glider#startAnimation 1741 * @see Glider#stopAnimation 1742 * @function 1743 * @example 1744 * // Divide the circle line into 6 steps and 1745 * // visit every step 330 msec counterclockwise. 1746 * var ci = board.create('circle', [[-1,2], [2,1]]); 1747 * var gl = board.create('glider', [0,2, ci]); 1748 * gl.startAnimation(-1, 6, 330); 1749 * 1750 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 1751 * <script type="text/javascript"> 1752 * (function() { 1753 * var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3', 1754 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1755 * // Divide the circle line into 6 steps and 1756 * // visit every step 330 msec counterclockwise. 1757 * var ci = board.create('circle', [[-1,2], [2,1]]); 1758 * var gl = board.create('glider', [0,2, ci]); 1759 * gl.startAnimation(-1, 6, 330); 1760 * 1761 * })(); 1762 * 1763 * </script><pre> 1764 * @example 1765 * //animate example closed curve 1766 * var c1 = board.create('curve',[(u)=>4*Math.cos(u),(u)=>2*Math.sin(u)+2,0,2*Math.PI]); 1767 * var p2 = board.create('glider', [c1]); 1768 * var button1 = board.create('button', [1, 7, 'start animation',function(){p2.startAnimation(1,8)}]); 1769 * var button2 = board.create('button', [1, 5, 'stop animation',function(){p2.stopAnimation()}]); 1770 * </pre><div class="jxgbox" id="JXG10e885ea-b05d-4e7d-a473-bac2554bce68" style="width: 200px; height: 200px;"></div> 1771 * <script type="text/javascript"> 1772 * var gpex4_board = JXG.JSXGraph.initBoard('JXG10e885ea-b05d-4e7d-a473-bac2554bce68', {boundingbox: [-1, 10, 10, -1], axis: true, showcopyright: false, shownavigation: false}); 1773 * var gpex4_c1 = gpex4_board.create('curve',[(u)=>4*Math.cos(u)+4,(u)=>2*Math.sin(u)+2,0,2*Math.PI]); 1774 * var gpex4_p2 = gpex4_board.create('glider', [gpex4_c1]); 1775 * gpex4_board.create('button', [1, 7, 'start animation',function(){gpex4_p2.startAnimation(1,8)}]); 1776 * gpex4_board.create('button', [1, 5, 'stop animation',function(){gpex4_p2.stopAnimation()}]); 1777 * </script><pre> 1778 * 1779 * @example 1780 * // Divide the slider area into 20 steps and 1781 * // visit every step 30 msec. 1782 * var n = board.create('slider',[[-2,4],[2,4],[1,5,100]],{name:'n'}); 1783 * n.startAnimation(1, 20, 30); 1784 * 1785 * </pre><div id="JXG40ce04b8-e99c-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 1786 * <script type="text/javascript"> 1787 * (function() { 1788 * var board = JXG.JSXGraph.initBoard('JXG40ce04b8-e99c-11e8-a1ca-04d3b0c2aad3', 1789 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1790 * // Divide the slider area into 20 steps and 1791 * // visit every step 30 msec. 1792 * var n = board.create('slider',[[-2,4],[2,4],[1,5,100]],{name:'n'}); 1793 * n.startAnimation(1, 20, 30); 1794 * 1795 * })(); 1796 * </script><pre> 1797 * 1798 */ 1799 startAnimation: function (direction, stepCount, delay) { 1800 var dir = Type.evaluate(direction), 1801 sc = Type.evaluate(stepCount), 1802 that = this; 1803 1804 delay = Type.evaluate(delay) || 250; 1805 1806 if (this.type === Const.OBJECT_TYPE_GLIDER && !Type.exists(this.intervalCode)) { 1807 this.intervalCode = window.setInterval(function () { 1808 that._anim(dir, sc); 1809 }, delay); 1810 1811 if (!Type.exists(this.intervalCount)) { 1812 this.intervalCount = 0; 1813 } 1814 } 1815 return this; 1816 }, 1817 1818 /** 1819 * Stop animation. 1820 * @name Glider#stopAnimation 1821 * @see Glider#startAnimation 1822 * @function 1823 * @returns {JXG.CoordsElement} Reference to itself. 1824 */ 1825 stopAnimation: function () { 1826 if (Type.exists(this.intervalCode)) { 1827 window.clearInterval(this.intervalCode); 1828 delete this.intervalCode; 1829 } 1830 1831 return this; 1832 }, 1833 1834 /** 1835 * Starts an animation which moves the point along a given path in given time. 1836 * @param {Array|function} path The path the point is moved on. 1837 * This can be either an array of arrays or containing x and y values of the points of 1838 * the path, or an array of points, or a function taking the amount of elapsed time since the animation 1839 * has started and returns an array containing a x and a y value or NaN. 1840 * In case of NaN the animation stops. 1841 * @param {Number} time The time in milliseconds in which to finish the animation 1842 * @param {Object} [options] Optional settings for the animation. 1843 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1844 * @param {Boolean} [options.interpolate=true] If <tt>path</tt> is an array moveAlong() 1845 * will interpolate the path 1846 * using {@link JXG.Math.Numerics.Neville}. Set this flag to false if you don't want to use interpolation. 1847 * @returns {JXG.CoordsElement} Reference to itself. 1848 * @see JXG.CoordsElement#moveTo 1849 * @see JXG.CoordsElement#visit 1850 * @see JXG.CoordsElement#moveAlongES6 1851 * @see JXG.GeometryElement#animate 1852 */ 1853 moveAlong: function (path, time, options) { 1854 options = options || {}; 1855 1856 var i, 1857 neville, 1858 interpath = [], 1859 p = [], 1860 delay = this.board.attr.animationdelay, 1861 steps = time / delay, 1862 len, 1863 pos, 1864 part, 1865 makeFakeFunction = function (i, j) { 1866 return function () { 1867 return path[i][j]; 1868 }; 1869 }; 1870 1871 if (Type.isArray(path)) { 1872 len = path.length; 1873 for (i = 0; i < len; i++) { 1874 if (Type.isPoint(path[i])) { 1875 p[i] = path[i]; 1876 } else { 1877 p[i] = { 1878 elementClass: Const.OBJECT_CLASS_POINT, 1879 X: makeFakeFunction(i, 0), 1880 Y: makeFakeFunction(i, 1) 1881 }; 1882 } 1883 } 1884 1885 time = time || 0; 1886 if (time === 0) { 1887 this.setPosition(Const.COORDS_BY_USER, [ 1888 p[p.length - 1].X(), 1889 p[p.length - 1].Y() 1890 ]); 1891 return this.board.update(this); 1892 } 1893 1894 if (!Type.exists(options.interpolate) || options.interpolate) { 1895 neville = Numerics.Neville(p); 1896 for (i = 0; i < steps; i++) { 1897 interpath[i] = []; 1898 interpath[i][0] = neville[0](((steps - i) / steps) * neville[3]()); 1899 interpath[i][1] = neville[1](((steps - i) / steps) * neville[3]()); 1900 } 1901 } else { 1902 len = path.length - 1; 1903 for (i = 0; i < steps; ++i) { 1904 pos = Math.floor((i / steps) * len); 1905 part = (i / steps) * len - pos; 1906 1907 interpath[i] = []; 1908 interpath[i][0] = (1.0 - part) * p[pos].X() + part * p[pos + 1].X(); 1909 interpath[i][1] = (1.0 - part) * p[pos].Y() + part * p[pos + 1].Y(); 1910 } 1911 interpath.push([p[len].X(), p[len].Y()]); 1912 interpath.reverse(); 1913 /* 1914 for (i = 0; i < steps; i++) { 1915 interpath[i] = []; 1916 interpath[i][0] = path[Math.floor((steps - i) / steps * (path.length - 1))][0]; 1917 interpath[i][1] = path[Math.floor((steps - i) / steps * (path.length - 1))][1]; 1918 } 1919 */ 1920 } 1921 1922 this.animationPath = interpath; 1923 } else if (Type.isFunction(path)) { 1924 this.animationPath = path; 1925 this.animationStart = new Date().getTime(); 1926 } 1927 1928 this.animationCallback = options.callback; 1929 this.board.addAnimation(this); 1930 1931 return this; 1932 }, 1933 1934 /** 1935 * Starts an animated point movement towards the given coordinates <tt>where</tt>. 1936 * The animation is done after <tt>time</tt> milliseconds. 1937 * If the second parameter is not given or is equal to 0, setPosition() is called, see 1938 * {@link JXG.CoordsElement#setPosition}, 1939 * i.e. the coordinates are changed without animation. 1940 * @param {Array} where Array containing the x and y coordinate of the target location. 1941 * @param {Number} [time] Number of milliseconds the animation should last. 1942 * @param {Object} [options] Optional settings for the animation 1943 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1944 * @param {String} [options.effect='<>'|'>'|'<'] animation effects like speed fade in and out. possible values are 1945 * '<>' for speed increase on start and slow down at the end (default), '<' for speed up, '>' for slow down, and '--' for constant speed during 1946 * the whole animation. 1947 * @returns {JXG.CoordsElement} Reference to itself. 1948 * @see JXG.CoordsElement#setPosition 1949 * @see JXG.CoordsElement#moveAlong 1950 * @see JXG.CoordsElement#visit 1951 * @see JXG.CoordsElement#moveToES6 1952 * @see JXG.GeometryElement#animate 1953 * @example 1954 * // moveTo() with different easing options and callback options 1955 * let yInit = 3 1956 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 1957 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 1958 * 1959 * let isLeftRight = true; 1960 * let buttonMove = board.create('button', [-2, 4, 'left', 1961 * () => { 1962 * isLeftRight = !isLeftRight; 1963 * buttonMove.rendNodeButton.innerHTML = isLeftRight ? 'left' : 'right' 1964 * let x = isLeftRight ? 4 : -4 1965 * let sym = isLeftRight ? 'triangleleft' : 'triangleright' 1966 * 1967 * A.moveTo([x, 3], 1000, { callback: () => A.setAttribute({ face: sym, size: 5 }) }) 1968 * B.moveTo([x, 2], 1000, { callback: () => B.setAttribute({ face: sym, size: 5 }), effect: "<>" }) 1969 * C.moveTo([x, 1], 1000, { callback: () => C.setAttribute({ face: sym, size: 5 }), effect: "<" }) 1970 * D.moveTo([x, 0], 1000, { callback: () => D.setAttribute({ face: sym, size: 5 }), effect: ">" }) 1971 * 1972 * }]); 1973 * 1974 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad4" class="jxgbox" style="width: 300px; height: 300px;"></div> 1975 * <script type="text/javascript"> 1976 * { 1977 * let board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad4') 1978 * let yInit = 3 1979 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 1980 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 1981 * 1982 * let isLeftRight = true; 1983 * let buttonMove = board.create('button', [-2, 4, 'left', 1984 * () => { 1985 * isLeftRight = !isLeftRight; 1986 * buttonMove.rendNodeButton.innerHTML = isLeftRight ? 'left' : 'right' 1987 * let x = isLeftRight ? 4 : -4 1988 * let sym = isLeftRight ? 'triangleleft' : 'triangleright' 1989 * 1990 * A.moveTo([x, 3], 1000, { callback: () => A.setAttribute({ face: sym, size: 5 }) }) 1991 * B.moveTo([x, 2], 1000, { callback: () => B.setAttribute({ face: sym, size: 5 }), effect: "<>" }) 1992 * C.moveTo([x, 1], 1000, { callback: () => C.setAttribute({ face: sym, size: 5 }), effect: "<" }) 1993 * D.moveTo([x, 0], 1000, { callback: () => D.setAttribute({ face: sym, size: 5 }), effect: ">" }) 1994 * 1995 * }]); 1996 *} 1997 *</script><pre> 1998 */ 1999 moveTo: function (where, time, options) { 2000 options = options || {}; 2001 where = new Coords(Const.COORDS_BY_USER, where, this.board); 2002 2003 var i, 2004 delay = this.board.attr.animationdelay, 2005 steps = Math.ceil(time / delay), 2006 coords = [], 2007 X = this.coords.usrCoords[1], 2008 Y = this.coords.usrCoords[2], 2009 dX = where.usrCoords[1] - X, 2010 dY = where.usrCoords[2] - Y, 2011 /** @ignore */ 2012 stepFun = function (i) { 2013 var x = i / steps; // absolute progress of the animatin 2014 2015 if (options.effect) { 2016 if (options.effect === "<>") { 2017 return Math.pow(Math.sin((x * Math.PI) / 2), 2); 2018 } 2019 if (options.effect === "<") { // cubic ease in 2020 return x * x * x; 2021 } 2022 if (options.effect === ">") { // cubic ease out 2023 return 1 - Math.pow(1 - x, 3); 2024 } 2025 if (options.effect === "==") { 2026 return i / steps; // linear 2027 } 2028 throw new Error("valid effects are '==', '<>', '>', and '<'."); 2029 } 2030 return i / steps; // default 2031 }; 2032 2033 if ( 2034 !Type.exists(time) || 2035 time === 0 || 2036 Math.abs(where.usrCoords[0] - this.coords.usrCoords[0]) > Mat.eps 2037 ) { 2038 this.setPosition(Const.COORDS_BY_USER, where.usrCoords); 2039 return this.board.update(this); 2040 } 2041 2042 // In case there is no callback and we are already at the endpoint we can stop here 2043 if ( 2044 !Type.exists(options.callback) && 2045 Math.abs(dX) < Mat.eps && 2046 Math.abs(dY) < Mat.eps 2047 ) { 2048 return this; 2049 } 2050 2051 for (i = steps; i >= 0; i--) { 2052 coords[steps - i] = [ 2053 where.usrCoords[0], 2054 X + dX * stepFun(i), 2055 Y + dY * stepFun(i) 2056 ]; 2057 } 2058 2059 this.animationPath = coords; 2060 this.animationCallback = options.callback; 2061 this.board.addAnimation(this); 2062 2063 return this; 2064 }, 2065 2066 /** 2067 * Starts an animated point movement towards the given coordinates <tt>where</tt>. After arriving at 2068 * <tt>where</tt> the point moves back to where it started. The animation is done after <tt>time</tt> 2069 * milliseconds. 2070 * @param {Array} where Array containing the x and y coordinate of the target location. 2071 * @param {Number} time Number of milliseconds the animation should last. 2072 * @param {Object} [options] Optional settings for the animation 2073 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 2074 * @param {String} [options.effect='<>'|'>'|'<'] animation effects like speed fade in and out. possible values are 2075 * '<>' for speed increase on start and slow down at the end (default), '<' for speed up, '>' for slow down, and '--' for constant speed during 2076 * the whole animation. 2077 * @param {Number} [options.repeat=1] How often this animation should be repeated. 2078 * @returns {JXG.CoordsElement} Reference to itself. 2079 * @see JXG.CoordsElement#moveAlong 2080 * @see JXG.CoordsElement#moveTo 2081 * @see JXG.CoordsElement#visitES6 2082 * @see JXG.GeometryElement#animate 2083 * @example 2084 * // visit() with different easing options 2085 * let yInit = 3 2086 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 2087 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 2088 * 2089 *let isLeftRight = true; 2090 *let buttonVisit = board.create('button', [0, 4, 'visit', 2091 * () => { 2092 * let x = isLeftRight ? 4 : -4 2093 * 2094 * A.visit([-x, 3], 4000, { effect: "==", repeat: 2 }) // linear 2095 * B.visit([-x, 2], 4000, { effect: "<>", repeat: 2 }) 2096 * C.visit([-x, 1], 4000, { effect: "<", repeat: 2 }) 2097 * D.visit([-x, 0], 4000, { effect: ">", repeat: 2 }) 2098 * }]) 2099 * 2100 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad5" class="jxgbox" style="width: 300px; height: 300px;"></div> 2101 * <script type="text/javascript"> 2102 * { 2103 * let board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad5') 2104 * let yInit = 3 2105 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 2106 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 2107 * 2108 * let isLeftRight = true; 2109 * let buttonVisit = board.create('button', [0, 4, 'visit', 2110 * () => { 2111 * let x = isLeftRight ? 4 : -4 2112 * 2113 * A.visit([-x, 3], 4000, { effect: "==", repeat: 2 }) // linear 2114 * B.visit([-x, 2], 4000, { effect: "<>", repeat: 2 }) 2115 * C.visit([-x, 1], 4000, { effect: "<", repeat: 2 }) 2116 * D.visit([-x, 0], 4000, { effect: ">", repeat: 2 }) 2117 * }]) 2118 * } 2119 * </script><pre> 2120 * 2121 */ 2122 visit: function (where, time, options) { 2123 where = new Coords(Const.COORDS_BY_USER, where, this.board); 2124 2125 var i, 2126 j, 2127 steps, 2128 delay = this.board.attr.animationdelay, 2129 coords = [], 2130 X = this.coords.usrCoords[1], 2131 Y = this.coords.usrCoords[2], 2132 dX = where.usrCoords[1] - X, 2133 dY = where.usrCoords[2] - Y, 2134 /** @ignore */ 2135 stepFun = function (i) { 2136 var x = i < steps / 2 ? (2 * i) / steps : (2 * (steps - i)) / steps; 2137 2138 if (options.effect) { 2139 if (options.effect === "<>") { // slow at beginning and end 2140 return Math.pow(Math.sin((x * Math.PI) / 2), 2); 2141 } 2142 if (options.effect === "<") { // cubic ease in 2143 return x * x * x; 2144 } 2145 if (options.effect === ">") { // cubic ease out 2146 return 1 - Math.pow(1 - x, 3); 2147 } 2148 if (options.effect === "==") { 2149 return x; // linear 2150 } 2151 throw new Error("valid effects are '==', '<>', '>', and '<'."); 2152 2153 } 2154 return x; 2155 }; 2156 2157 // support legacy interface where the third parameter was the number of repeats 2158 if (Type.isNumber(options)) { 2159 options = { repeat: options }; 2160 } else { 2161 options = options || {}; 2162 if (!Type.exists(options.repeat)) { 2163 options.repeat = 1; 2164 } 2165 } 2166 2167 steps = Math.ceil(time / (delay * options.repeat)); 2168 2169 for (j = 0; j < options.repeat; j++) { 2170 for (i = steps; i >= 0; i--) { 2171 coords[j * (steps + 1) + steps - i] = [ 2172 where.usrCoords[0], 2173 X + dX * stepFun(i), 2174 Y + dY * stepFun(i) 2175 ]; 2176 } 2177 } 2178 this.animationPath = coords; 2179 this.animationCallback = options.callback; 2180 this.board.addAnimation(this); 2181 2182 return this; 2183 }, 2184 2185 /** 2186 * ES6 version of {@link JXG.CoordsElement#moveAlong} using a promise. 2187 * 2188 * @param {Array} where Array containing the x and y coordinate of the target location. 2189 * @param {Number} [time] Number of milliseconds the animation should last. 2190 * @param {Object} [options] Optional settings for the animation 2191 * @returns Promise 2192 * @see JXG.CoordsElement#moveAlong 2193 * @example 2194 * var A = board.create('point', [4, 4]); 2195 * A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 2000) 2196 * .then(() => A.moveToES6([-3, -3], 1000)); 2197 * 2198 * </pre><div id="JXGa45032e5-a517-4f1d-868a-65d698d344cf" class="jxgbox" style="width: 300px; height: 300px;"></div> 2199 * <script type="text/javascript"> 2200 * (function() { 2201 * var board = JXG.JSXGraph.initBoard('JXGa45032e5-a517-4f1d-868a-65d698d344cf', 2202 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2203 * var A = board.create('point', [4, 4]); 2204 * A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 2000) 2205 * .then(() => A.moveToES6([-3, -3], 1000)); 2206 * 2207 * })(); 2208 * 2209 * </script><pre> 2210 * 2211 */ 2212 moveAlongES6: function (path, time, options) { 2213 return new Promise((resolve, reject) => { 2214 if (Type.exists(options) && Type.exists(options.callback)) { 2215 options.callback = resolve; 2216 } else { 2217 options = { 2218 callback: resolve 2219 }; 2220 } 2221 this.moveAlong(path, time, options); 2222 }); 2223 }, 2224 2225 /** 2226 * ES6 version of {@link JXG.CoordsElement#moveTo} using a promise. 2227 * 2228 * @param {Array} where Array containing the x and y coordinate of the target location. 2229 * @param {Number} [time] Number of milliseconds the animation should last. 2230 * @param {Object} [options] Optional settings for the animation 2231 * @returns Promise 2232 * @see JXG.CoordsElement#moveTo 2233 * 2234 * @example 2235 * var A = board.create('point', [4, 4]); 2236 * A.moveToES6([-3, 3], 1000) 2237 * .then(() => A.moveToES6([-3, -3], 1000)) 2238 * .then(() => A.moveToES6([3, -3], 1000)) 2239 * .then(() => A.moveToES6([3, -3], 1000)); 2240 * 2241 * </pre><div id="JXGabdc7771-34f0-4655-bb7b-fc329e773b89" class="jxgbox" style="width: 300px; height: 300px;"></div> 2242 * <script type="text/javascript"> 2243 * (function() { 2244 * var board = JXG.JSXGraph.initBoard('JXGabdc7771-34f0-4655-bb7b-fc329e773b89', 2245 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2246 * var A = board.create('point', [4, 4]); 2247 * A.moveToES6([-3, 3], 1000) 2248 * .then(() => A.moveToES6([-3, -3], 1000)) 2249 * .then(() => A.moveToES6([3, -3], 1000)) 2250 * .then(() => A.moveToES6([3, -3], 1000)); 2251 * 2252 * })(); 2253 * 2254 * </script><pre> 2255 * 2256 * @example 2257 * var A = board.create('point', [4, 4]); 2258 * A.moveToES6([-3, 3], 1000) 2259 * .then(function() { 2260 * return A.moveToES6([-3, -3], 1000); 2261 * }).then(function() { 2262 * return A.moveToES6([ 3, -3], 1000); 2263 * }).then(function() { 2264 * return A.moveToES6([ 3, -3], 1000); 2265 * }).then(function() { 2266 * return A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 5000); 2267 * }).then(function() { 2268 * return A.visitES6([-4, -4], 3000); 2269 * }); 2270 * 2271 * </pre><div id="JXGa9439ce5-516d-4dba-9233-2a4ad9589995" class="jxgbox" style="width: 300px; height: 300px;"></div> 2272 * <script type="text/javascript"> 2273 * (function() { 2274 * var board = JXG.JSXGraph.initBoard('JXGa9439ce5-516d-4dba-9233-2a4ad9589995', 2275 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2276 * var A = board.create('point', [4, 4]); 2277 * A.moveToES6([-3, 3], 1000) 2278 * .then(function() { 2279 * return A.moveToES6([-3, -3], 1000); 2280 * }).then(function() { 2281 * return A.moveToES6([ 3, -3], 1000); 2282 * }).then(function() { 2283 * return A.moveToES6([ 3, -3], 1000); 2284 * }).then(function() { 2285 * return A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 5000); 2286 * }).then(function() { 2287 * return A.visitES6([-4, -4], 3000); 2288 * }); 2289 * 2290 * })(); 2291 * 2292 * </script><pre> 2293 * 2294 */ 2295 moveToES6: function (where, time, options) { 2296 return new Promise((resolve, reject) => { 2297 if (Type.exists(options) && Type.exists(options.callback)) { 2298 options.callback = resolve; 2299 } else { 2300 options = { 2301 callback: resolve 2302 }; 2303 } 2304 this.moveTo(where, time, options); 2305 }); 2306 }, 2307 2308 /** 2309 * ES6 version of {@link JXG.CoordsElement#moveVisit} using a promise. 2310 * 2311 * @param {Array} where Array containing the x and y coordinate of the target location. 2312 * @param {Number} [time] Number of milliseconds the animation should last. 2313 * @param {Object} [options] Optional settings for the animation 2314 * @returns Promise 2315 * @see JXG.CoordsElement#visit 2316 * @example 2317 * var A = board.create('point', [4, 4]); 2318 * A.visitES6([-4, -4], 3000) 2319 * .then(() => A.moveToES6([-3, 3], 1000)); 2320 * 2321 * </pre><div id="JXG640f1fd2-05ec-46cb-b977-36d96648ce41" class="jxgbox" style="width: 300px; height: 300px;"></div> 2322 * <script type="text/javascript"> 2323 * (function() { 2324 * var board = JXG.JSXGraph.initBoard('JXG640f1fd2-05ec-46cb-b977-36d96648ce41', 2325 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2326 * var A = board.create('point', [4, 4]); 2327 * A.visitES6([-4, -4], 3000) 2328 * .then(() => A.moveToES6([-3, 3], 1000)); 2329 * 2330 * })(); 2331 * 2332 * </script><pre> 2333 * 2334 */ 2335 visitES6: function (where, time, options) { 2336 return new Promise((resolve, reject) => { 2337 if (Type.exists(options) && Type.exists(options.callback)) { 2338 options.callback = resolve; 2339 } else { 2340 options = { 2341 callback: resolve 2342 }; 2343 } 2344 this.visit(where, time, options); 2345 }); 2346 }, 2347 2348 /** 2349 * Animates a glider. Is called by the browser after startAnimation is called. 2350 * @param {Number} direction The direction the glider is animated. 2351 * @param {Number} stepCount The number of steps in which the parent element is divided. 2352 * Must be at least 1. 2353 * @see JXG.CoordsElement#startAnimation 2354 * @see JXG.CoordsElement#stopAnimation 2355 * @private 2356 * @returns {JXG.CoordsElement} Reference to itself. 2357 */ 2358 _anim: function (direction, stepCount) { 2359 var dX, dY, alpha, startPoint, newX, radius, sp1c, sp2c, res; 2360 2361 this.intervalCount += 1; 2362 if (this.intervalCount > stepCount) { 2363 this.intervalCount = 0; 2364 } 2365 2366 if (this.slideObject.elementClass === Const.OBJECT_CLASS_LINE) { 2367 sp1c = this.slideObject.point1.coords.scrCoords; 2368 sp2c = this.slideObject.point2.coords.scrCoords; 2369 2370 dX = Math.round(((sp2c[1] - sp1c[1]) * this.intervalCount) / stepCount); 2371 dY = Math.round(((sp2c[2] - sp1c[2]) * this.intervalCount) / stepCount); 2372 if (direction > 0) { 2373 startPoint = this.slideObject.point1; 2374 } else { 2375 startPoint = this.slideObject.point2; 2376 dX *= -1; 2377 dY *= -1; 2378 } 2379 2380 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [ 2381 startPoint.coords.scrCoords[1] + dX, 2382 startPoint.coords.scrCoords[2] + dY 2383 ]); 2384 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CURVE) { 2385 if (direction > 0) { 2386 newX = (this.slideObject.maxX() - this.slideObject.minX()) * this.intervalCount / stepCount + this.slideObject.minX(); 2387 } else { 2388 newX = -(this.slideObject.maxX() - this.slideObject.minX()) * this.intervalCount / stepCount + this.slideObject.maxX(); 2389 } 2390 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.slideObject.X(newX), this.slideObject.Y(newX)]); 2391 2392 res = Geometry.projectPointToCurve(this, this.slideObject, this.board); 2393 this.coords = res[0]; 2394 this.position = res[1]; 2395 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CIRCLE) { 2396 alpha = 2 * Math.PI; 2397 if (direction < 0) { 2398 alpha *= this.intervalCount / stepCount; 2399 } else { 2400 alpha *= (stepCount - this.intervalCount) / stepCount; 2401 } 2402 radius = this.slideObject.Radius(); 2403 2404 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 2405 this.slideObject.center.coords.usrCoords[1] + radius * Math.cos(alpha), 2406 this.slideObject.center.coords.usrCoords[2] + radius * Math.sin(alpha) 2407 ]); 2408 } 2409 2410 this.board.update(this); 2411 return this; 2412 }, 2413 2414 // documented in GeometryElement 2415 getTextAnchor: function () { 2416 return this.coords; 2417 }, 2418 2419 // documented in GeometryElement 2420 getLabelAnchor: function () { 2421 return this.coords; 2422 }, 2423 2424 // documented in element.js 2425 getParents: function () { 2426 var p = [this.Z(), this.X(), this.Y()]; 2427 2428 if (this.parents.length !== 0) { 2429 p = this.parents; 2430 } 2431 2432 if (this.type === Const.OBJECT_TYPE_GLIDER) { 2433 p = [this.X(), this.Y(), this.slideObject.id]; 2434 } 2435 2436 return p; 2437 } 2438 } 2439 ); 2440 2441 /** 2442 * Generic method to create point, text or image. 2443 * Determines the type of the construction, i.e. free, or constrained by function, 2444 * transformation or of glider type. 2445 * @param{Object} Callback Object type, e.g. JXG.Point, JXG.Text or JXG.Image 2446 * @param{Object} board Link to the board object 2447 * @param{Array} coords Array with coordinates. This may be: array of numbers, function 2448 * returning an array of numbers, array of functions returning a number, object and transformation. 2449 * If the attribute "slideObject" exists, a glider element is constructed. 2450 * @param{Object} attr Attributes object 2451 * @param{Object} arg1 Optional argument 1: in case of text this is the text content, 2452 * in case of an image this is the url. 2453 * @param{Array} arg2 Optional argument 2: in case of image this is an array containing the size of 2454 * the image. 2455 * @returns{Object} returns the created object or false. 2456 */ 2457 JXG.CoordsElement.create = function (Callback, board, coords, attr, arg1, arg2) { 2458 var el, 2459 isConstrained = false, 2460 i; 2461 2462 for (i = 0; i < coords.length; i++) { 2463 if (Type.isFunction(coords[i]) || Type.isString(coords[i])) { 2464 isConstrained = true; 2465 } 2466 } 2467 2468 if (!isConstrained) { 2469 if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) { 2470 el = new Callback(board, coords, attr, arg1, arg2); 2471 2472 if (Type.exists(attr.slideobject)) { 2473 el.makeGlider(attr.slideobject); 2474 } else { 2475 // Free element 2476 el.baseElement = el; 2477 } 2478 el.isDraggable = true; 2479 } else if (Type.isObject(coords[0]) && Type.isTransformationOrArray(coords[1])) { 2480 // Transformation 2481 // TODO less general specification of isObject 2482 el = new Callback(board, [0, 0], attr, arg1, arg2); 2483 el.addTransform(coords[0], coords[1]); 2484 el.isDraggable = false; 2485 } else { 2486 return false; 2487 } 2488 } else { 2489 el = new Callback(board, [0, 0], attr, arg1, arg2); 2490 el.addConstraint(coords); 2491 } 2492 2493 el.handleSnapToGrid(); 2494 el.handleSnapToPoints(); 2495 el.handleAttractors(); 2496 2497 el.addParents(coords); 2498 return el; 2499 }; 2500 2501 export default JXG.CoordsElement; 2502