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