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 a 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 * @param {Number} [maxRounds=-1] The number of rounds the glider will be animated. The glider will run infinitely if 1739 * maxRounds is negative or equal to Infinity. 1740 * @returns {JXG.CoordsElement} Reference to itself. 1741 * 1742 * @name Glider#startAnimation 1743 * @see Glider#stopAnimation 1744 * @function 1745 * @example 1746 * // Divide the circle line into 6 steps and 1747 * // visit every step 330 msec counterclockwise. 1748 * var ci = board.create('circle', [[-1,2], [2,1]]); 1749 * var gl = board.create('glider', [0,2, ci]); 1750 * gl.startAnimation(-1, 6, 330); 1751 * 1752 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 1753 * <script type="text/javascript"> 1754 * (function() { 1755 * var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3', 1756 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1757 * // Divide the circle line into 6 steps and 1758 * // visit every step 330 msec counterclockwise. 1759 * var ci = board.create('circle', [[-1,2], [2,1]]); 1760 * var gl = board.create('glider', [0,2, ci]); 1761 * gl.startAnimation(-1, 6, 330); 1762 * 1763 * })(); 1764 * 1765 * </script><pre> 1766 * @example 1767 * //animate example closed curve 1768 * var c1 = board.create('curve',[(u)=>4*Math.cos(u),(u)=>2*Math.sin(u)+2,0,2*Math.PI]); 1769 * var p2 = board.create('glider', [c1]); 1770 * var button1 = board.create('button', [1, 7, 'start animation',function(){p2.startAnimation(1,8)}]); 1771 * var button2 = board.create('button', [1, 5, 'stop animation',function(){p2.stopAnimation()}]); 1772 * </pre><div class="jxgbox" id="JXG10e885ea-b05d-4e7d-a473-bac2554bce68" style="width: 200px; height: 200px;"></div> 1773 * <script type="text/javascript"> 1774 * var gpex4_board = JXG.JSXGraph.initBoard('JXG10e885ea-b05d-4e7d-a473-bac2554bce68', {boundingbox: [-1, 10, 10, -1], axis: true, showcopyright: false, shownavigation: false}); 1775 * var gpex4_c1 = gpex4_board.create('curve',[(u)=>4*Math.cos(u)+4,(u)=>2*Math.sin(u)+2,0,2*Math.PI]); 1776 * var gpex4_p2 = gpex4_board.create('glider', [gpex4_c1]); 1777 * gpex4_board.create('button', [1, 7, 'start animation',function(){gpex4_p2.startAnimation(1,8)}]); 1778 * gpex4_board.create('button', [1, 5, 'stop animation',function(){gpex4_p2.stopAnimation()}]); 1779 * </script><pre> 1780 * 1781 * @example 1782 * // Divide the slider area into 20 steps and 1783 * // visit every step 30 msec. Stop after 2 rounds. 1784 * var n = board.create('slider',[[-2,4],[2,4],[1,5,100]],{name:'n'}); 1785 * n.startAnimation(1, 20, 30, 2); 1786 * 1787 * </pre><div id="JXG40ce04b8-e99c-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 1788 * <script type="text/javascript"> 1789 * (function() { 1790 * var board = JXG.JSXGraph.initBoard('JXG40ce04b8-e99c-11e8-a1ca-04d3b0c2aad3', 1791 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1792 * // Divide the slider area into 20 steps and 1793 * // visit every step 30 msec. 1794 * var n = board.create('slider',[[-2,4],[2,4],[1,5,100]],{name:'n'}); 1795 * n.startAnimation(1, 20, 30, 2); 1796 * 1797 * })(); 1798 * </script><pre> 1799 * 1800 */ 1801 startAnimation: function (direction, stepCount, delay, maxRounds) { 1802 var dir = Type.evaluate(direction), 1803 sc = Type.evaluate(stepCount), 1804 that = this; 1805 1806 delay = Type.evaluate(delay) || 250; 1807 maxRounds = Type.evaluate(maxRounds); 1808 maxRounds = (maxRounds !== 'undefined') ? maxRounds : -1; 1809 1810 if (this.type === Const.OBJECT_TYPE_GLIDER && !Type.exists(this.intervalCode) && maxRounds !== 0) { 1811 this.roundsCount = 0; 1812 this.intervalCode = window.setInterval(function () { 1813 that._anim(dir, sc, maxRounds); 1814 }, delay); 1815 1816 if (!Type.exists(this.intervalCount)) { 1817 this.intervalCount = 0; 1818 1819 } 1820 } 1821 return this; 1822 }, 1823 1824 /** 1825 * Stop animation. 1826 * @name Glider#stopAnimation 1827 * @see Glider#startAnimation 1828 * @function 1829 * @returns {JXG.CoordsElement} Reference to itself. 1830 */ 1831 stopAnimation: function () { 1832 if (Type.exists(this.intervalCode)) { 1833 window.clearInterval(this.intervalCode); 1834 delete this.intervalCode; 1835 } 1836 1837 return this; 1838 }, 1839 1840 /** 1841 * Starts an animation which moves the point along a given path in given time. 1842 * @param {Array|function} path The path the point is moved on. 1843 * This can be either an array of arrays or containing x and y values of the points of 1844 * the path, or an array of points, or a function taking the amount of elapsed time since the animation 1845 * has started and returns an array containing a x and a y value or NaN. 1846 * In case of NaN the animation stops. 1847 * @param {Number} time The time in milliseconds in which to finish the animation 1848 * @param {Object} [options] Optional settings for the animation. 1849 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1850 * @param {Boolean} [options.interpolate=true] If <tt>path</tt> is an array moveAlong() 1851 * will interpolate the path 1852 * using {@link JXG.Math.Numerics.Neville}. Set this flag to false if you don't want to use interpolation. 1853 * @returns {JXG.CoordsElement} Reference to itself. 1854 * @see JXG.CoordsElement#moveTo 1855 * @see JXG.CoordsElement#visit 1856 * @see JXG.CoordsElement#moveAlongES6 1857 * @see JXG.GeometryElement#animate 1858 */ 1859 moveAlong: function (path, time, options) { 1860 options = options || {}; 1861 1862 var i, 1863 neville, 1864 interpath = [], 1865 p = [], 1866 delay = this.board.attr.animationdelay, 1867 steps = time / delay, 1868 len, 1869 pos, 1870 part, 1871 makeFakeFunction = function (i, j) { 1872 return function () { 1873 return path[i][j]; 1874 }; 1875 }; 1876 1877 if (Type.isArray(path)) { 1878 len = path.length; 1879 for (i = 0; i < len; i++) { 1880 if (Type.isPoint(path[i])) { 1881 p[i] = path[i]; 1882 } else { 1883 p[i] = { 1884 elementClass: Const.OBJECT_CLASS_POINT, 1885 X: makeFakeFunction(i, 0), 1886 Y: makeFakeFunction(i, 1) 1887 }; 1888 } 1889 } 1890 1891 time = time || 0; 1892 if (time === 0) { 1893 this.setPosition(Const.COORDS_BY_USER, [ 1894 p[p.length - 1].X(), 1895 p[p.length - 1].Y() 1896 ]); 1897 return this.board.update(this); 1898 } 1899 1900 if (!Type.exists(options.interpolate) || options.interpolate) { 1901 neville = Numerics.Neville(p); 1902 for (i = 0; i < steps; i++) { 1903 interpath[i] = []; 1904 interpath[i][0] = neville[0](((steps - i) / steps) * neville[3]()); 1905 interpath[i][1] = neville[1](((steps - i) / steps) * neville[3]()); 1906 } 1907 } else { 1908 len = path.length - 1; 1909 for (i = 0; i < steps; ++i) { 1910 pos = Math.floor((i / steps) * len); 1911 part = (i / steps) * len - pos; 1912 1913 interpath[i] = []; 1914 interpath[i][0] = (1.0 - part) * p[pos].X() + part * p[pos + 1].X(); 1915 interpath[i][1] = (1.0 - part) * p[pos].Y() + part * p[pos + 1].Y(); 1916 } 1917 interpath.push([p[len].X(), p[len].Y()]); 1918 interpath.reverse(); 1919 /* 1920 for (i = 0; i < steps; i++) { 1921 interpath[i] = []; 1922 interpath[i][0] = path[Math.floor((steps - i) / steps * (path.length - 1))][0]; 1923 interpath[i][1] = path[Math.floor((steps - i) / steps * (path.length - 1))][1]; 1924 } 1925 */ 1926 } 1927 1928 this.animationPath = interpath; 1929 } else if (Type.isFunction(path)) { 1930 this.animationPath = path; 1931 this.animationStart = new Date().getTime(); 1932 } 1933 1934 this.animationCallback = options.callback; 1935 this.board.addAnimation(this); 1936 1937 return this; 1938 }, 1939 1940 /** 1941 * Starts an animated point movement towards the given coordinates <tt>where</tt>. 1942 * The animation is done after <tt>time</tt> milliseconds. 1943 * If the second parameter is not given or is equal to 0, setPosition() is called, see 1944 * {@link JXG.CoordsElement#setPosition}, 1945 * i.e. the coordinates are changed without animation. 1946 * @param {Array} where Array containing the x and y coordinate of the target location. 1947 * @param {Number} [time] Number of milliseconds the animation should last. 1948 * @param {Object} [options] Optional settings for the animation 1949 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1950 * @param {String} [options.effect='<>'|'>'|'<'] animation effects like speed fade in and out. possible values are 1951 * '<>' for speed increase on start and slow down at the end (default), '<' for speed up, '>' for slow down, and '--' for constant speed during 1952 * the whole animation. 1953 * @returns {JXG.CoordsElement} Reference to itself. 1954 * @see JXG.CoordsElement#setPosition 1955 * @see JXG.CoordsElement#moveAlong 1956 * @see JXG.CoordsElement#visit 1957 * @see JXG.CoordsElement#moveToES6 1958 * @see JXG.GeometryElement#animate 1959 * @example 1960 * // moveTo() with different easing options and callback options 1961 * let yInit = 3 1962 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 1963 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 1964 * 1965 * let isLeftRight = true; 1966 * let buttonMove = board.create('button', [-2, 4, 'left', 1967 * () => { 1968 * isLeftRight = !isLeftRight; 1969 * buttonMove.rendNodeButton.innerHTML = isLeftRight ? 'left' : 'right' 1970 * let x = isLeftRight ? 4 : -4 1971 * let sym = isLeftRight ? 'triangleleft' : 'triangleright' 1972 * 1973 * A.moveTo([x, 3], 1000, { callback: () => A.setAttribute({ face: sym, size: 5 }) }) 1974 * B.moveTo([x, 2], 1000, { callback: () => B.setAttribute({ face: sym, size: 5 }), effect: "<>" }) 1975 * C.moveTo([x, 1], 1000, { callback: () => C.setAttribute({ face: sym, size: 5 }), effect: "<" }) 1976 * D.moveTo([x, 0], 1000, { callback: () => D.setAttribute({ face: sym, size: 5 }), effect: ">" }) 1977 * 1978 * }]); 1979 * 1980 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad4" class="jxgbox" style="width: 300px; height: 300px;"></div> 1981 * <script type="text/javascript"> 1982 * { 1983 * let board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad4') 1984 * let yInit = 3 1985 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 1986 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 1987 * 1988 * let isLeftRight = true; 1989 * let buttonMove = board.create('button', [-2, 4, 'left', 1990 * () => { 1991 * isLeftRight = !isLeftRight; 1992 * buttonMove.rendNodeButton.innerHTML = isLeftRight ? 'left' : 'right' 1993 * let x = isLeftRight ? 4 : -4 1994 * let sym = isLeftRight ? 'triangleleft' : 'triangleright' 1995 * 1996 * A.moveTo([x, 3], 1000, { callback: () => A.setAttribute({ face: sym, size: 5 }) }) 1997 * B.moveTo([x, 2], 1000, { callback: () => B.setAttribute({ face: sym, size: 5 }), effect: "<>" }) 1998 * C.moveTo([x, 1], 1000, { callback: () => C.setAttribute({ face: sym, size: 5 }), effect: "<" }) 1999 * D.moveTo([x, 0], 1000, { callback: () => D.setAttribute({ face: sym, size: 5 }), effect: ">" }) 2000 * 2001 * }]); 2002 *} 2003 *</script><pre> 2004 */ 2005 moveTo: function (where, time, options) { 2006 options = options || {}; 2007 where = new Coords(Const.COORDS_BY_USER, where, this.board); 2008 2009 var i, 2010 delay = this.board.attr.animationdelay, 2011 steps = Math.ceil(time / delay), 2012 coords = [], 2013 X = this.coords.usrCoords[1], 2014 Y = this.coords.usrCoords[2], 2015 dX = where.usrCoords[1] - X, 2016 dY = where.usrCoords[2] - Y, 2017 /** @ignore */ 2018 stepFun = function (i) { 2019 var x = i / steps; // absolute progress of the animatin 2020 2021 if (options.effect) { 2022 if (options.effect === "<>") { 2023 return Math.pow(Math.sin((x * Math.PI) / 2), 2); 2024 } 2025 if (options.effect === "<") { // cubic ease in 2026 return x * x * x; 2027 } 2028 if (options.effect === ">") { // cubic ease out 2029 return 1 - Math.pow(1 - x, 3); 2030 } 2031 if (options.effect === "==") { 2032 return i / steps; // linear 2033 } 2034 throw new Error("valid effects are '==', '<>', '>', and '<'."); 2035 } 2036 return i / steps; // default 2037 }; 2038 2039 if ( 2040 !Type.exists(time) || 2041 time === 0 || 2042 Math.abs(where.usrCoords[0] - this.coords.usrCoords[0]) > Mat.eps 2043 ) { 2044 this.setPosition(Const.COORDS_BY_USER, where.usrCoords); 2045 return this.board.update(this); 2046 } 2047 2048 // In case there is no callback and we are already at the endpoint we can stop here 2049 if ( 2050 !Type.exists(options.callback) && 2051 Math.abs(dX) < Mat.eps && 2052 Math.abs(dY) < Mat.eps 2053 ) { 2054 return this; 2055 } 2056 2057 for (i = steps; i >= 0; i--) { 2058 coords[steps - i] = [ 2059 where.usrCoords[0], 2060 X + dX * stepFun(i), 2061 Y + dY * stepFun(i) 2062 ]; 2063 } 2064 2065 this.animationPath = coords; 2066 this.animationCallback = options.callback; 2067 this.board.addAnimation(this); 2068 2069 return this; 2070 }, 2071 2072 /** 2073 * Starts an animated point movement towards the given coordinates <tt>where</tt>. After arriving at 2074 * <tt>where</tt> the point moves back to where it started. The animation is done after <tt>time</tt> 2075 * milliseconds. 2076 * @param {Array} where Array containing the x and y coordinate of the target location. 2077 * @param {Number} time Number of milliseconds the animation should last. 2078 * @param {Object} [options] Optional settings for the animation 2079 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 2080 * @param {String} [options.effect='<>'|'>'|'<'] animation effects like speed fade in and out. possible values are 2081 * '<>' for speed increase on start and slow down at the end (default), '<' for speed up, '>' for slow down, and '--' for constant speed during 2082 * the whole animation. 2083 * @param {Number} [options.repeat=1] How often this animation should be repeated. 2084 * @returns {JXG.CoordsElement} Reference to itself. 2085 * @see JXG.CoordsElement#moveAlong 2086 * @see JXG.CoordsElement#moveTo 2087 * @see JXG.CoordsElement#visitES6 2088 * @see JXG.GeometryElement#animate 2089 * @example 2090 * // visit() with different easing options 2091 * let yInit = 3 2092 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 2093 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 2094 * 2095 *let isLeftRight = true; 2096 *let buttonVisit = board.create('button', [0, 4, 'visit', 2097 * () => { 2098 * let x = isLeftRight ? 4 : -4 2099 * 2100 * A.visit([-x, 3], 4000, { effect: "==", repeat: 2 }) // linear 2101 * B.visit([-x, 2], 4000, { effect: "<>", repeat: 2 }) 2102 * C.visit([-x, 1], 4000, { effect: "<", repeat: 2 }) 2103 * D.visit([-x, 0], 4000, { effect: ">", repeat: 2 }) 2104 * }]) 2105 * 2106 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad5" class="jxgbox" style="width: 300px; height: 300px;"></div> 2107 * <script type="text/javascript"> 2108 * { 2109 * let board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad5') 2110 * let yInit = 3 2111 * let [A, B, C, D] = ['==', '<>', '<', '>'].map((s) => board.create('point', [4, yInit--], { name: s, label: { fontSize: 24 } })) 2112 * let seg = board.create('segment', [A, [() => A.X(), 0]]) // shows linear 2113 * 2114 * let isLeftRight = true; 2115 * let buttonVisit = board.create('button', [0, 4, 'visit', 2116 * () => { 2117 * let x = isLeftRight ? 4 : -4 2118 * 2119 * A.visit([-x, 3], 4000, { effect: "==", repeat: 2 }) // linear 2120 * B.visit([-x, 2], 4000, { effect: "<>", repeat: 2 }) 2121 * C.visit([-x, 1], 4000, { effect: "<", repeat: 2 }) 2122 * D.visit([-x, 0], 4000, { effect: ">", repeat: 2 }) 2123 * }]) 2124 * } 2125 * </script><pre> 2126 * 2127 */ 2128 visit: function (where, time, options) { 2129 where = new Coords(Const.COORDS_BY_USER, where, this.board); 2130 2131 var i, 2132 j, 2133 steps, 2134 delay = this.board.attr.animationdelay, 2135 coords = [], 2136 X = this.coords.usrCoords[1], 2137 Y = this.coords.usrCoords[2], 2138 dX = where.usrCoords[1] - X, 2139 dY = where.usrCoords[2] - Y, 2140 /** @ignore */ 2141 stepFun = function (i) { 2142 var x = i < steps / 2 ? (2 * i) / steps : (2 * (steps - i)) / steps; 2143 2144 if (options.effect) { 2145 if (options.effect === "<>") { // slow at beginning and end 2146 return Math.pow(Math.sin((x * Math.PI) / 2), 2); 2147 } 2148 if (options.effect === "<") { // cubic ease in 2149 return x * x * x; 2150 } 2151 if (options.effect === ">") { // cubic ease out 2152 return 1 - Math.pow(1 - x, 3); 2153 } 2154 if (options.effect === "==") { 2155 return x; // linear 2156 } 2157 throw new Error("valid effects are '==', '<>', '>', and '<'."); 2158 2159 } 2160 return x; 2161 }; 2162 2163 // support legacy interface where the third parameter was the number of repeats 2164 if (Type.isNumber(options)) { 2165 options = { repeat: options }; 2166 } else { 2167 options = options || {}; 2168 if (!Type.exists(options.repeat)) { 2169 options.repeat = 1; 2170 } 2171 } 2172 2173 steps = Math.ceil(time / (delay * options.repeat)); 2174 2175 for (j = 0; j < options.repeat; j++) { 2176 for (i = steps; i >= 0; i--) { 2177 coords[j * (steps + 1) + steps - i] = [ 2178 where.usrCoords[0], 2179 X + dX * stepFun(i), 2180 Y + dY * stepFun(i) 2181 ]; 2182 } 2183 } 2184 this.animationPath = coords; 2185 this.animationCallback = options.callback; 2186 this.board.addAnimation(this); 2187 2188 return this; 2189 }, 2190 2191 /** 2192 * ES6 version of {@link JXG.CoordsElement#moveAlong} using a promise. 2193 * 2194 * @param {Array} where Array containing the x and y coordinate of the target location. 2195 * @param {Number} [time] Number of milliseconds the animation should last. 2196 * @param {Object} [options] Optional settings for the animation 2197 * @returns Promise 2198 * @see JXG.CoordsElement#moveAlong 2199 * @example 2200 * var A = board.create('point', [4, 4]); 2201 * A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 2000) 2202 * .then(() => A.moveToES6([-3, -3], 1000)); 2203 * 2204 * </pre><div id="JXGa45032e5-a517-4f1d-868a-65d698d344cf" class="jxgbox" style="width: 300px; height: 300px;"></div> 2205 * <script type="text/javascript"> 2206 * (function() { 2207 * var board = JXG.JSXGraph.initBoard('JXGa45032e5-a517-4f1d-868a-65d698d344cf', 2208 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2209 * var A = board.create('point', [4, 4]); 2210 * A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 2000) 2211 * .then(() => A.moveToES6([-3, -3], 1000)); 2212 * 2213 * })(); 2214 * 2215 * </script><pre> 2216 * 2217 */ 2218 moveAlongES6: function (path, time, options) { 2219 return new Promise((resolve, reject) => { 2220 if (Type.exists(options) && Type.exists(options.callback)) { 2221 options.callback = resolve; 2222 } else { 2223 options = { 2224 callback: resolve 2225 }; 2226 } 2227 this.moveAlong(path, time, options); 2228 }); 2229 }, 2230 2231 /** 2232 * ES6 version of {@link JXG.CoordsElement#moveTo} using a promise. 2233 * 2234 * @param {Array} where Array containing the x and y coordinate of the target location. 2235 * @param {Number} [time] Number of milliseconds the animation should last. 2236 * @param {Object} [options] Optional settings for the animation 2237 * @returns Promise 2238 * @see JXG.CoordsElement#moveTo 2239 * 2240 * @example 2241 * var A = board.create('point', [4, 4]); 2242 * A.moveToES6([-3, 3], 1000) 2243 * .then(() => A.moveToES6([-3, -3], 1000)) 2244 * .then(() => A.moveToES6([3, -3], 1000)) 2245 * .then(() => A.moveToES6([3, -3], 1000)); 2246 * 2247 * </pre><div id="JXGabdc7771-34f0-4655-bb7b-fc329e773b89" class="jxgbox" style="width: 300px; height: 300px;"></div> 2248 * <script type="text/javascript"> 2249 * (function() { 2250 * var board = JXG.JSXGraph.initBoard('JXGabdc7771-34f0-4655-bb7b-fc329e773b89', 2251 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2252 * var A = board.create('point', [4, 4]); 2253 * A.moveToES6([-3, 3], 1000) 2254 * .then(() => A.moveToES6([-3, -3], 1000)) 2255 * .then(() => A.moveToES6([3, -3], 1000)) 2256 * .then(() => A.moveToES6([3, -3], 1000)); 2257 * 2258 * })(); 2259 * 2260 * </script><pre> 2261 * 2262 * @example 2263 * var A = board.create('point', [4, 4]); 2264 * A.moveToES6([-3, 3], 1000) 2265 * .then(function() { 2266 * return A.moveToES6([-3, -3], 1000); 2267 * }).then(function() { 2268 * return A.moveToES6([ 3, -3], 1000); 2269 * }).then(function() { 2270 * return A.moveToES6([ 3, -3], 1000); 2271 * }).then(function() { 2272 * return A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 5000); 2273 * }).then(function() { 2274 * return A.visitES6([-4, -4], 3000); 2275 * }); 2276 * 2277 * </pre><div id="JXGa9439ce5-516d-4dba-9233-2a4ad9589995" class="jxgbox" style="width: 300px; height: 300px;"></div> 2278 * <script type="text/javascript"> 2279 * (function() { 2280 * var board = JXG.JSXGraph.initBoard('JXGa9439ce5-516d-4dba-9233-2a4ad9589995', 2281 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2282 * var A = board.create('point', [4, 4]); 2283 * A.moveToES6([-3, 3], 1000) 2284 * .then(function() { 2285 * return A.moveToES6([-3, -3], 1000); 2286 * }).then(function() { 2287 * return A.moveToES6([ 3, -3], 1000); 2288 * }).then(function() { 2289 * return A.moveToES6([ 3, -3], 1000); 2290 * }).then(function() { 2291 * return A.moveAlongES6([[3, -2], [4, 0], [3, 1], [4, 4]], 5000); 2292 * }).then(function() { 2293 * return A.visitES6([-4, -4], 3000); 2294 * }); 2295 * 2296 * })(); 2297 * 2298 * </script><pre> 2299 * 2300 */ 2301 moveToES6: function (where, time, options) { 2302 return new Promise((resolve, reject) => { 2303 if (Type.exists(options) && Type.exists(options.callback)) { 2304 options.callback = resolve; 2305 } else { 2306 options = { 2307 callback: resolve 2308 }; 2309 } 2310 this.moveTo(where, time, options); 2311 }); 2312 }, 2313 2314 /** 2315 * ES6 version of {@link JXG.CoordsElement#moveVisit} using a promise. 2316 * 2317 * @param {Array} where Array containing the x and y coordinate of the target location. 2318 * @param {Number} [time] Number of milliseconds the animation should last. 2319 * @param {Object} [options] Optional settings for the animation 2320 * @returns Promise 2321 * @see JXG.CoordsElement#visit 2322 * @example 2323 * var A = board.create('point', [4, 4]); 2324 * A.visitES6([-4, -4], 3000) 2325 * .then(() => A.moveToES6([-3, 3], 1000)); 2326 * 2327 * </pre><div id="JXG640f1fd2-05ec-46cb-b977-36d96648ce41" class="jxgbox" style="width: 300px; height: 300px;"></div> 2328 * <script type="text/javascript"> 2329 * (function() { 2330 * var board = JXG.JSXGraph.initBoard('JXG640f1fd2-05ec-46cb-b977-36d96648ce41', 2331 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2332 * var A = board.create('point', [4, 4]); 2333 * A.visitES6([-4, -4], 3000) 2334 * .then(() => A.moveToES6([-3, 3], 1000)); 2335 * 2336 * })(); 2337 * 2338 * </script><pre> 2339 * 2340 */ 2341 visitES6: function (where, time, options) { 2342 return new Promise((resolve, reject) => { 2343 if (Type.exists(options) && Type.exists(options.callback)) { 2344 options.callback = resolve; 2345 } else { 2346 options = { 2347 callback: resolve 2348 }; 2349 } 2350 this.visit(where, time, options); 2351 }); 2352 }, 2353 2354 /** 2355 * Animates a glider. Is called by the browser after startAnimation is called. 2356 * @param {Number} direction The direction the glider is animated. 2357 * @param {Number} stepCount The number of steps in which the parent element is divided. 2358 * Must be at least 1. 2359 * @param {Number} [maxRounds=-1] The number of rounds the glider will be animated. The glider will run infinitely if 2360 * maxRounds is negative or equal to Infinity. 2361 * @see JXG.CoordsElement#startAnimation 2362 * @see JXG.CoordsElement#stopAnimation 2363 * @private 2364 * @returns {JXG.CoordsElement} Reference to itself. 2365 */ 2366 _anim: function (direction, stepCount, maxRounds) { 2367 var dX, dY, alpha, startPoint, newX, radius, sp1c, sp2c, res; 2368 2369 this.intervalCount += 1; 2370 if (this.intervalCount > stepCount) { 2371 this.intervalCount = 0; 2372 2373 this.roundsCount += 1; 2374 if (maxRounds > 0 && this.roundsCount >= maxRounds) { 2375 this.roundsCount = 0; 2376 return this.stopAnimation(); 2377 } 2378 } 2379 2380 if (this.slideObject.elementClass === Const.OBJECT_CLASS_LINE) { 2381 sp1c = this.slideObject.point1.coords.scrCoords; 2382 sp2c = this.slideObject.point2.coords.scrCoords; 2383 2384 dX = Math.round(((sp2c[1] - sp1c[1]) * this.intervalCount) / stepCount); 2385 dY = Math.round(((sp2c[2] - sp1c[2]) * this.intervalCount) / stepCount); 2386 if (direction > 0) { 2387 startPoint = this.slideObject.point1; 2388 } else { 2389 startPoint = this.slideObject.point2; 2390 dX *= -1; 2391 dY *= -1; 2392 } 2393 2394 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [ 2395 startPoint.coords.scrCoords[1] + dX, 2396 startPoint.coords.scrCoords[2] + dY 2397 ]); 2398 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CURVE) { 2399 if (direction > 0) { 2400 newX = (this.slideObject.maxX() - this.slideObject.minX()) * this.intervalCount / stepCount + this.slideObject.minX(); 2401 } else { 2402 newX = -(this.slideObject.maxX() - this.slideObject.minX()) * this.intervalCount / stepCount + this.slideObject.maxX(); 2403 } 2404 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.slideObject.X(newX), this.slideObject.Y(newX)]); 2405 2406 res = Geometry.projectPointToCurve(this, this.slideObject, this.board); 2407 this.coords = res[0]; 2408 this.position = res[1]; 2409 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CIRCLE) { 2410 alpha = 2 * Math.PI; 2411 if (direction < 0) { 2412 alpha *= this.intervalCount / stepCount; 2413 } else { 2414 alpha *= (stepCount - this.intervalCount) / stepCount; 2415 } 2416 radius = this.slideObject.Radius(); 2417 2418 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 2419 this.slideObject.center.coords.usrCoords[1] + radius * Math.cos(alpha), 2420 this.slideObject.center.coords.usrCoords[2] + radius * Math.sin(alpha) 2421 ]); 2422 } 2423 2424 this.board.update(this); 2425 return this; 2426 }, 2427 2428 // documented in GeometryElement 2429 getTextAnchor: function () { 2430 return this.coords; 2431 }, 2432 2433 // documented in GeometryElement 2434 getLabelAnchor: function () { 2435 return this.coords; 2436 }, 2437 2438 // documented in element.js 2439 getParents: function () { 2440 var p = [this.Z(), this.X(), this.Y()]; 2441 2442 if (this.parents.length !== 0) { 2443 p = this.parents; 2444 } 2445 2446 if (this.type === Const.OBJECT_TYPE_GLIDER) { 2447 p = [this.X(), this.Y(), this.slideObject.id]; 2448 } 2449 2450 return p; 2451 } 2452 } 2453 ); 2454 2455 /** 2456 * Generic method to create point, text or image. 2457 * Determines the type of the construction, i.e. free, or constrained by function, 2458 * transformation or of glider type. 2459 * @param{Object} Callback Object type, e.g. JXG.Point, JXG.Text or JXG.Image 2460 * @param{Object} board Link to the board object 2461 * @param{Array} coords Array with coordinates. This may be: array of numbers, function 2462 * returning an array of numbers, array of functions returning a number, object and transformation. 2463 * If the attribute "slideObject" exists, a glider element is constructed. 2464 * @param{Object} attr Attributes object 2465 * @param{Object} arg1 Optional argument 1: in case of text this is the text content, 2466 * in case of an image this is the url. 2467 * @param{Array} arg2 Optional argument 2: in case of image this is an array containing the size of 2468 * the image. 2469 * @returns{Object} returns the created object or false. 2470 */ 2471 JXG.CoordsElement.create = function (Callback, board, coords, attr, arg1, arg2) { 2472 var el, 2473 isConstrained = false, 2474 i; 2475 2476 for (i = 0; i < coords.length; i++) { 2477 if (Type.isFunction(coords[i]) || Type.isString(coords[i])) { 2478 isConstrained = true; 2479 } 2480 } 2481 2482 if (!isConstrained) { 2483 if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) { 2484 el = new Callback(board, coords, attr, arg1, arg2); 2485 2486 if (Type.exists(attr.slideobject)) { 2487 el.makeGlider(attr.slideobject); 2488 } else { 2489 // Free element 2490 el.baseElement = el; 2491 } 2492 el.isDraggable = true; 2493 } else if (Type.isObject(coords[0]) && Type.isTransformationOrArray(coords[1])) { 2494 // Transformation 2495 // TODO less general specification of isObject 2496 el = new Callback(board, [0, 0], attr, arg1, arg2); 2497 el.addTransform(coords[0], coords[1]); 2498 el.isDraggable = false; 2499 } else { 2500 return false; 2501 } 2502 } else { 2503 el = new Callback(board, [0, 0], attr, arg1, arg2); 2504 el.addConstraint(coords); 2505 } 2506 2507 el.handleSnapToGrid(); 2508 el.handleSnapToPoints(); 2509 el.handleAttractors(); 2510 2511 el.addParents(coords); 2512 return el; 2513 }; 2514 2515 export default JXG.CoordsElement; 2516