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