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