1 /* 2 Copyright 2008-2022 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/> 29 and <http://opensource.org/licenses/MIT/>. 30 */ 31 32 34 35 /*jslint nomen: true, plusplus: true*/ 36 37 /* depends: 38 jxg 39 base/constants 40 base/coords 41 options 42 math/numerics 43 math/math 44 math/geometry 45 math/complex 46 parser/jessiecode 47 parser/geonext 48 utils/color 49 utils/type 50 utils/event 51 utils/env 52 elements: 53 transform 54 point 55 line 56 text 57 grid 58 */ 59 60 /** 61 * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods 62 * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc. 63 */ 64 65 define([ 66 'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex', 67 'math/statistics', 68 'parser/jessiecode', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 69 'base/composition' 70 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, Color, Type, 71 EventEmitter, Env, Composition) { 72 73 'use strict'; 74 75 /** 76 * Constructs a new Board object. 77 * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric 78 * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly. 79 * Please use {@link JXG.JSXGraph.initBoard} to initialize a board. 80 * @constructor 81 * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div. 82 * @param {JXG.AbstractRenderer} renderer The reference of a renderer. 83 * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined. 84 * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates. 85 * @param {Number} zoomX Zoom factor in x-axis direction 86 * @param {Number} zoomY Zoom factor in y-axis direction 87 * @param {Number} unitX Units in x-axis direction 88 * @param {Number} unitY Units in y-axis direction 89 * @param {Number} canvasWidth The width of canvas 90 * @param {Number} canvasHeight The height of canvas 91 * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard} 92 * @borrows JXG.EventEmitter#on as this.on 93 * @borrows JXG.EventEmitter#off as this.off 94 * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers 95 * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers 96 */ 97 JXG.Board = function (container, renderer, id, origin, zoomX, zoomY, unitX, unitY, canvasWidth, canvasHeight, attributes) { 98 /** 99 * Board is in no special mode, objects are highlighted on mouse over and objects may be 100 * clicked to start drag&drop. 101 * @type Number 102 * @constant 103 */ 104 this.BOARD_MODE_NONE = 0x0000; 105 106 /** 107 * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in 108 * {@link JXG.Board#mouse} is updated on mouse movement. 109 * @type Number 110 * @constant 111 * @see JXG.Board#drag_obj 112 */ 113 this.BOARD_MODE_DRAG = 0x0001; 114 115 /** 116 * In this mode a mouse move changes the origin's screen coordinates. 117 * @type Number 118 * @constant 119 */ 120 this.BOARD_MODE_MOVE_ORIGIN = 0x0002; 121 122 /** 123 * Update is made with high quality, e.g. graphs are evaluated at much more points. 124 * @type Number 125 * @constant 126 * @see JXG.Board#updateQuality 127 */ 128 this.BOARD_MODE_ZOOM = 0x0011; 129 130 /** 131 * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points. 132 * @type Number 133 * @constant 134 * @see JXG.Board#updateQuality 135 */ 136 this.BOARD_QUALITY_LOW = 0x1; 137 138 /** 139 * Update is made with high quality, e.g. graphs are evaluated at much more points. 140 * @type Number 141 * @constant 142 * @see JXG.Board#updateQuality 143 */ 144 this.BOARD_QUALITY_HIGH = 0x2; 145 146 /** 147 * Pointer to the document element containing the board. 148 * @type Object 149 */ 150 // Former version: 151 // this.document = attributes.document || document; 152 if (Type.exists(attributes.document) && attributes.document !== false) { 153 this.document = attributes.document; 154 } else if (document !== undefined && Type.isObject(document)) { 155 this.document = document; 156 } 157 158 /** 159 * The html-id of the html element containing the board. 160 * @type String 161 */ 162 this.container = container; 163 164 /** 165 * Pointer to the html element containing the board. 166 * @type Object 167 */ 168 this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null); 169 170 if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) { 171 throw new Error("\nJSXGraph: HTML container element '" + container + "' not found."); 172 } 173 174 /** 175 * A reference to this boards renderer. 176 * @type JXG.AbstractRenderer 177 * @name JXG.Board#renderer 178 * @private 179 * @ignore 180 */ 181 this.renderer = renderer; 182 183 /** 184 * Grids keeps track of all grids attached to this board. 185 * @type Array 186 * @private 187 */ 188 this.grids = []; 189 190 /** 191 * Some standard options 192 * @type JXG.Options 193 */ 194 this.options = Type.deepCopy(Options); 195 this.attr = attributes; 196 197 /** 198 * Dimension of the board. 199 * @default 2 200 * @type Number 201 */ 202 this.dimension = 2; 203 204 this.jc = new JessieCode(); 205 this.jc.use(this); 206 207 /** 208 * Coordinates of the boards origin. This a object with the two properties 209 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 210 * stores the boards origin in homogeneous screen coordinates. 211 * @type Object 212 * @private 213 */ 214 this.origin = {}; 215 this.origin.usrCoords = [1, 0, 0]; 216 this.origin.scrCoords = [1, origin[0], origin[1]]; 217 218 /** 219 * Zoom factor in X direction. It only stores the zoom factor to be able 220 * to get back to 100% in zoom100(). 221 * @name JXG.Board.zoomX 222 * @type Number 223 * @private 224 * @ignore 225 */ 226 this.zoomX = zoomX; 227 228 /** 229 * Zoom factor in Y direction. It only stores the zoom factor to be able 230 * to get back to 100% in zoom100(). 231 * @name JXG.Board.zoomY 232 * @type Number 233 * @private 234 * @ignore 235 */ 236 this.zoomY = zoomY; 237 238 /** 239 * The number of pixels which represent one unit in user-coordinates in x direction. 240 * @type Number 241 * @private 242 */ 243 this.unitX = unitX * this.zoomX; 244 245 /** 246 * The number of pixels which represent one unit in user-coordinates in y direction. 247 * @type Number 248 * @private 249 */ 250 this.unitY = unitY * this.zoomY; 251 252 /** 253 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 254 * width/height ratio of the canvas. 255 * @type Boolean 256 * @private 257 */ 258 this.keepaspectratio = false; 259 260 /** 261 * Canvas width. 262 * @type Number 263 * @private 264 */ 265 this.canvasWidth = canvasWidth; 266 267 /** 268 * Canvas Height 269 * @type Number 270 * @private 271 */ 272 this.canvasHeight = canvasHeight; 273 274 // If the given id is not valid, generate an unique id 275 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 276 this.id = id; 277 } else { 278 this.id = this.generateId(); 279 } 280 281 EventEmitter.eventify(this); 282 283 this.hooks = []; 284 285 /** 286 * An array containing all other boards that are updated after this board has been updated. 287 * @type Array 288 * @see JXG.Board#addChild 289 * @see JXG.Board#removeChild 290 */ 291 this.dependentBoards = []; 292 293 /** 294 * During the update process this is set to false to prevent an endless loop. 295 * @default false 296 * @type Boolean 297 */ 298 this.inUpdate = false; 299 300 /** 301 * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object. 302 * @type Object 303 */ 304 this.objects = {}; 305 306 /** 307 * An array containing all geometric objects on the board in the order of construction. 308 * @type Array 309 */ 310 this.objectsList = []; 311 312 /** 313 * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object. 314 * @type Object 315 */ 316 this.groups = {}; 317 318 /** 319 * Stores all the objects that are currently running an animation. 320 * @type Object 321 */ 322 this.animationObjects = {}; 323 324 /** 325 * An associative array containing all highlighted elements belonging to the board. 326 * @type Object 327 */ 328 this.highlightedObjects = {}; 329 330 /** 331 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 332 * @type Number 333 */ 334 this.numObjects = 0; 335 336 /** 337 * An associative array to store the objects of the board by name. the name of the object is the key and value is a reference to the object. 338 * @type Object 339 */ 340 this.elementsByName = {}; 341 342 /** 343 * The board mode the board is currently in. Possible values are 344 * <ul> 345 * <li>JXG.Board.BOARD_MODE_NONE</li> 346 * <li>JXG.Board.BOARD_MODE_DRAG</li> 347 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 348 * </ul> 349 * @type Number 350 */ 351 this.mode = this.BOARD_MODE_NONE; 352 353 /** 354 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 355 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 356 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 357 * evaluation points when plotting functions. Possible values are 358 * <ul> 359 * <li>BOARD_QUALITY_LOW</li> 360 * <li>BOARD_QUALITY_HIGH</li> 361 * </ul> 362 * @type Number 363 * @see JXG.Board#mode 364 */ 365 this.updateQuality = this.BOARD_QUALITY_HIGH; 366 367 /** 368 * If true updates are skipped. 369 * @type Boolean 370 */ 371 this.isSuspendedRedraw = false; 372 373 this.calculateSnapSizes(); 374 375 /** 376 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 377 * @type Number 378 * @see JXG.Board#drag_dy 379 * @see JXG.Board#drag_obj 380 */ 381 this.drag_dx = 0; 382 383 /** 384 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 385 * @type Number 386 * @see JXG.Board#drag_dx 387 * @see JXG.Board#drag_obj 388 */ 389 this.drag_dy = 0; 390 391 /** 392 * The last position where a drag event has been fired. 393 * @type Array 394 * @see JXG.Board#moveObject 395 */ 396 this.drag_position = [0, 0]; 397 398 /** 399 * References to the object that is dragged with the mouse on the board. 400 * @type JXG.GeometryElement 401 * @see JXG.Board#touches 402 */ 403 this.mouse = {}; 404 405 /** 406 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 407 * @type Array 408 * @see JXG.Board#mouse 409 */ 410 this.touches = []; 411 412 /** 413 * A string containing the XML text of the construction. 414 * This is set in {@link JXG.FileReader.parseString}. 415 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 416 * @type String 417 */ 418 this.xmlString = ''; 419 420 /** 421 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 422 * @type Array 423 */ 424 this.cPos = []; 425 426 /** 427 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 428 * touchStart because Android's Webkit browser fires too much of them. 429 * @type Number 430 */ 431 this.touchMoveLast = 0; 432 433 /** 434 * Contains the pointerId of the last touchMove event which was not thrown away or since 435 * touchStart because Android's Webkit browser fires too much of them. 436 * @type Number 437 */ 438 this.touchMoveLastId = Infinity; 439 440 /** 441 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 442 * @type Number 443 */ 444 this.positionAccessLast = 0; 445 446 /** 447 * Collects all elements that triggered a mouse down event. 448 * @type Array 449 */ 450 this.downObjects = []; 451 452 if (this.attr.showcopyright) { 453 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 454 } 455 456 /** 457 * Full updates are needed after zoom and axis translates. This saves some time during an update. 458 * @default false 459 * @type Boolean 460 */ 461 this.needsFullUpdate = false; 462 463 /** 464 * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following 465 * elements are updated during mouse move. On mouse up the whole construction is 466 * updated. This enables us to be fast even on very slow devices. 467 * @type Boolean 468 * @default false 469 */ 470 this.reducedUpdate = false; 471 472 /** 473 * The current color blindness deficiency is stored in this property. If color blindness is not emulated 474 * at the moment, it's value is 'none'. 475 */ 476 this.currentCBDef = 'none'; 477 478 /** 479 * If GEONExT constructions are displayed, then this property should be set to true. 480 * At the moment there should be no difference. But this may change. 481 * This is set in {@link JXG.GeonextReader.readGeonext}. 482 * @type Boolean 483 * @default false 484 * @see JXG.GeonextReader.readGeonext 485 */ 486 this.geonextCompatibilityMode = false; 487 488 if (this.options.text.useASCIIMathML && translateASCIIMath) { 489 init(); 490 } else { 491 this.options.text.useASCIIMathML = false; 492 } 493 494 /** 495 * A flag which tells if the board registers mouse events. 496 * @type Boolean 497 * @default false 498 */ 499 this.hasMouseHandlers = false; 500 501 /** 502 * A flag which tells if the board registers touch events. 503 * @type Boolean 504 * @default false 505 */ 506 this.hasTouchHandlers = false; 507 508 /** 509 * A flag which stores if the board registered pointer events. 510 * @type Boolean 511 * @default false 512 */ 513 this.hasPointerHandlers = false; 514 515 /** 516 * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered. 517 * @type Boolean 518 * @default false 519 */ 520 this.hasMouseUp = false; 521 522 /** 523 * A flag which tells if the board the JXG.Board#touchEndListener is currently registered. 524 * @type Boolean 525 * @default false 526 */ 527 this.hasTouchEnd = false; 528 529 /** 530 * A flag which tells us if the board has a pointerUp event registered at the moment. 531 * @type Boolean 532 * @default false 533 */ 534 this.hasPointerUp = false; 535 536 /** 537 * Offset for large coords elements like images 538 * @type Array 539 * @private 540 * @default [0, 0] 541 */ 542 this._drag_offset = [0, 0]; 543 544 /** 545 * Stores the input device used in the last down or move event. 546 * @type String 547 * @private 548 * @default 'mouse' 549 */ 550 this._inputDevice = 'mouse'; 551 552 /** 553 * Keeps a list of pointer devices which are currently touching the screen. 554 * @type Array 555 * @private 556 */ 557 this._board_touches = []; 558 559 /** 560 * A flag which tells us if the board is in the selecting mode 561 * @type Boolean 562 * @default false 563 */ 564 this.selectingMode = false; 565 566 /** 567 * A flag which tells us if the user is selecting 568 * @type Boolean 569 * @default false 570 */ 571 this.isSelecting = false; 572 573 /** 574 * A flag which tells us if the user is scrolling the viewport 575 * @type Boolean 576 * @private 577 * @default false 578 * @see JXG.Board#scrollListener 579 */ 580 this._isScrolling = false; 581 582 /** 583 * A flag which tells us if a resize is in process 584 * @type Boolean 585 * @private 586 * @default false 587 * @see JXG.Board#resizeListener 588 */ 589 this._isResizing = false; 590 591 /** 592 * A bounding box for the selection 593 * @type Array 594 * @default [ [0,0], [0,0] ] 595 */ 596 this.selectingBox = [[0, 0], [0, 0]]; 597 598 this.mathLib = Math; // Math or JXG.Math.IntervalArithmetic 599 this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic 600 601 if (this.attr.registerevents) { 602 this.addEventHandlers(); 603 } 604 605 this.methodMap = { 606 update: 'update', 607 fullUpdate: 'fullUpdate', 608 on: 'on', 609 off: 'off', 610 trigger: 'trigger', 611 setView: 'setBoundingBox', 612 setBoundingBox: 'setBoundingBox', 613 migratePoint: 'migratePoint', 614 colorblind: 'emulateColorblindness', 615 suspendUpdate: 'suspendUpdate', 616 unsuspendUpdate: 'unsuspendUpdate', 617 clearTraces: 'clearTraces', 618 left: 'clickLeftArrow', 619 right: 'clickRightArrow', 620 up: 'clickUpArrow', 621 down: 'clickDownArrow', 622 zoomIn: 'zoomIn', 623 zoomOut: 'zoomOut', 624 zoom100: 'zoom100', 625 zoomElements: 'zoomElements', 626 remove: 'removeObject', 627 removeObject: 'removeObject' 628 }; 629 }; 630 631 JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ { 632 633 /** 634 * Generates an unique name for the given object. The result depends on the objects type, if the 635 * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line} 636 * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower 637 * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is 638 * generated using lower case characters. prefixed with k_ is used. In any other case, lower case 639 * chars prefixed with s_ is used. 640 * @param {Object} object Reference of an JXG.GeometryElement that is to be named. 641 * @returns {String} Unique name for the object. 642 */ 643 generateName: function (object) { 644 var possibleNames, i, 645 maxNameLength = this.attr.maxnamelength, 646 pre = '', 647 post = '', 648 indices = [], 649 name = ''; 650 651 if (object.type === Const.OBJECT_TYPE_TICKS) { 652 return ''; 653 } 654 655 if (Type.isPoint(object)) { 656 // points have capital letters 657 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 658 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 659 } else if (object.type === Const.OBJECT_TYPE_ANGLE) { 660 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 661 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 662 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω']; 663 } else { 664 // all other elements get lowercase labels 665 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 666 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; 667 } 668 669 if (!Type.isPoint(object) && 670 object.elementClass !== Const.OBJECT_CLASS_LINE && 671 object.type !== Const.OBJECT_TYPE_ANGLE) { 672 if (object.type === Const.OBJECT_TYPE_POLYGON) { 673 pre = 'P_{'; 674 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) { 675 pre = 'k_{'; 676 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) { 677 pre = 't_{'; 678 } else { 679 pre = 's_{'; 680 } 681 post = '}'; 682 } 683 684 for (i = 0; i < maxNameLength; i++) { 685 indices[i] = 0; 686 } 687 688 while (indices[maxNameLength - 1] < possibleNames.length) { 689 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) { 690 name = pre; 691 692 for (i = maxNameLength; i > 0; i--) { 693 name += possibleNames[indices[i - 1]]; 694 } 695 696 if (!Type.exists(this.elementsByName[name + post])) { 697 return name + post; 698 } 699 700 } 701 indices[0] = possibleNames.length; 702 703 for (i = 1; i < maxNameLength; i++) { 704 if (indices[i - 1] === possibleNames.length) { 705 indices[i - 1] = 1; 706 indices[i] += 1; 707 } 708 } 709 } 710 711 return ''; 712 }, 713 714 /** 715 * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'. 716 * @returns {String} Unique id for a board. 717 */ 718 generateId: function () { 719 var r = 1; 720 721 // as long as we don't have a unique id generate a new one 722 while (Type.exists(JXG.boards['jxgBoard' + r])) { 723 r = Math.round(Math.random() * 65535); 724 } 725 726 return ('jxgBoard' + r); 727 }, 728 729 /** 730 * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the 731 * object type. As a side effect {@link JXG.Board#numObjects} 732 * is updated. 733 * @param {Object} obj Reference of an geometry object that needs an id. 734 * @param {Number} type Type of the object. 735 * @returns {String} Unique id for an element. 736 */ 737 setId: function (obj, type) { 738 var randomNumber, 739 num = this.numObjects, 740 elId = obj.id; 741 742 this.numObjects += 1; 743 744 // If no id is provided or id is empty string, a new one is chosen 745 if (elId === '' || !Type.exists(elId)) { 746 elId = this.id + type + num; 747 while (Type.exists(this.objects[elId])) { 748 randomNumber = Math.round(Math.random() * 65535); 749 elId = this.id + type + num + '-' + randomNumber; 750 } 751 } 752 753 obj.id = elId; 754 this.objects[elId] = obj; 755 obj._pos = this.objectsList.length; 756 this.objectsList[this.objectsList.length] = obj; 757 758 return elId; 759 }, 760 761 /** 762 * After construction of the object the visibility is set 763 * and the label is constructed if necessary. 764 * @param {Object} obj The object to add. 765 */ 766 finalizeAdding: function (obj) { 767 if (Type.evaluate(obj.visProp.visible) === false) { 768 this.renderer.display(obj, false); 769 } 770 }, 771 772 finalizeLabel: function (obj) { 773 if (obj.hasLabel && 774 !Type.evaluate(obj.label.visProp.islabel) && 775 Type.evaluate(obj.label.visProp.visible) === false) { 776 this.renderer.display(obj.label, false); 777 } 778 }, 779 780 /********************************************************** 781 * 782 * Event Handler helpers 783 * 784 **********************************************************/ 785 786 /** 787 * Returns false if the event has been triggered faster than the maximum frame rate. 788 * 789 * @param {Event} evt Event object given by the browser (unused) 790 * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned. 791 * @private 792 * @see JXG.Board#pointerMoveListener 793 * @see JXG.Board#touchMoveListener 794 * @see JXG.Board#mouseMoveListener 795 */ 796 checkFrameRate: function(evt) { 797 var handleEvt = false, 798 time = new Date().getTime(); 799 800 if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) { 801 handleEvt = true; 802 this.touchMoveLastId = evt.pointerId; 803 } 804 if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) { 805 handleEvt = true; 806 } 807 if (handleEvt) { 808 this.touchMoveLast = time; 809 } 810 return handleEvt; 811 }, 812 813 /** 814 * Calculates mouse coordinates relative to the boards container. 815 * @returns {Array} Array of coordinates relative the boards container top left corner. 816 */ 817 getCoordsTopLeftCorner: function () { 818 var cPos, doc, crect, 819 docElement = this.document.documentElement || this.document.body.parentNode, 820 docBody = this.document.body, 821 container = this.containerObj, 822 // viewport, content, 823 zoom, o; 824 825 /** 826 * During drags and origin moves the container element is usually not changed. 827 * Check the position of the upper left corner at most every 1000 msecs 828 */ 829 if (this.cPos.length > 0 && 830 (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN || 831 (new Date()).getTime() - this.positionAccessLast < 1000)) { 832 return this.cPos; 833 } 834 this.positionAccessLast = (new Date()).getTime(); 835 836 // Check if getBoundingClientRect exists. If so, use this as this covers *everything* 837 // even CSS3D transformations etc. 838 // Supported by all browsers but IE 6, 7. 839 840 if (container.getBoundingClientRect) { 841 crect = container.getBoundingClientRect(); 842 843 844 zoom = 1.0; 845 // Recursively search for zoom style entries. 846 // This is necessary for reveal.js on webkit. 847 // It fails if the user does zooming 848 o = container; 849 while (o && Type.exists(o.parentNode)) { 850 if (Type.exists(o.style) && Type.exists(o.style.zoom) && o.style.zoom !== '') { 851 zoom *= parseFloat(o.style.zoom); 852 } 853 o = o.parentNode; 854 } 855 cPos = [crect.left * zoom, crect.top * zoom]; 856 857 // add border width 858 cPos[0] += Env.getProp(container, 'border-left-width'); 859 cPos[1] += Env.getProp(container, 'border-top-width'); 860 861 // vml seems to ignore paddings 862 if (this.renderer.type !== 'vml') { 863 // add padding 864 cPos[0] += Env.getProp(container, 'padding-left'); 865 cPos[1] += Env.getProp(container, 'padding-top'); 866 } 867 868 this.cPos = cPos.slice(); 869 return this.cPos; 870 } 871 872 // 873 // OLD CODE 874 // IE 6-7 only: 875 // 876 cPos = Env.getOffset(container); 877 doc = this.document.documentElement.ownerDocument; 878 879 if (!this.containerObj.currentStyle && doc.defaultView) { // Non IE 880 // this is for hacks like this one used in wordpress for the admin bar: 881 // html { margin-top: 28px } 882 // seems like it doesn't work in IE 883 884 cPos[0] += Env.getProp(docElement, 'margin-left'); 885 cPos[1] += Env.getProp(docElement, 'margin-top'); 886 887 cPos[0] += Env.getProp(docElement, 'border-left-width'); 888 cPos[1] += Env.getProp(docElement, 'border-top-width'); 889 890 cPos[0] += Env.getProp(docElement, 'padding-left'); 891 cPos[1] += Env.getProp(docElement, 'padding-top'); 892 } 893 894 if (docBody) { 895 cPos[0] += Env.getProp(docBody, 'left'); 896 cPos[1] += Env.getProp(docBody, 'top'); 897 } 898 899 // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX 900 // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly 901 // available version so we're doing it the hacky way: Add a fixed offset. 904 cPos[0] += 10; 905 cPos[1] += 25; 906 } 907 908 // add border width 909 cPos[0] += Env.getProp(container, 'border-left-width'); 910 cPos[1] += Env.getProp(container, 'border-top-width'); 911 912 // vml seems to ignore paddings 913 if (this.renderer.type !== 'vml') { 914 // add padding 915 cPos[0] += Env.getProp(container, 'padding-left'); 916 cPos[1] += Env.getProp(container, 'padding-top'); 917 } 918 919 cPos[0] += this.attr.offsetx; 920 cPos[1] += this.attr.offsety; 921 922 this.cPos = cPos.slice(); 923 return this.cPos; 924 }, 925 926 /** 927 * Get the position of the mouse in screen coordinates, relative to the upper left corner 928 * of the host tag. 929 * @param {Event} e Event object given by the browser. 930 * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set 931 * for mouseevents. 932 * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords} 933 */ 934 getMousePosition: function (e, i) { 935 var cPos = this.getCoordsTopLeftCorner(), 936 absPos, 937 v; 938 939 // Position of cursor using clientX/Y 940 absPos = Env.getPosition(e, i, this.document); 941 942 /** 943 * In case there has been no down event before. 944 */ 945 if (!Type.exists(this.cssTransMat)) { 946 this.updateCSSTransforms(); 947 } 948 // Position relative to the top left corner 949 v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]]; 950 v = Mat.matVecMult(this.cssTransMat, v); 951 v[1] /= v[0]; 952 v[2] /= v[0]; 953 return [v[1], v[2]]; 954 955 // Method without CSS transformation 956 /* 957 return [absPos[0] - cPos[0], absPos[1] - cPos[1]]; 958 */ 959 }, 960 961 /** 962 * Initiate moving the origin. This is used in mouseDown and touchStart listeners. 963 * @param {Number} x Current mouse/touch coordinates 964 * @param {Number} y Current mouse/touch coordinates 965 */ 966 initMoveOrigin: function (x, y) { 967 this.drag_dx = x - this.origin.scrCoords[1]; 968 this.drag_dy = y - this.origin.scrCoords[2]; 969 970 this.mode = this.BOARD_MODE_MOVE_ORIGIN; 971 this.updateQuality = this.BOARD_QUALITY_LOW; 972 }, 973 974 /** 975 * Collects all elements below the current mouse pointer and fulfilling the following constraints: 976 * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul> 977 * @param {Number} x Current mouse/touch coordinates 978 * @param {Number} y current mouse/touch coordinates 979 * @param {Object} evt An event object 980 * @param {String} type What type of event? 'touch', 'mouse' or 'pen'. 981 * @returns {Array} A list of geometric elements. 982 */ 983 initMoveObject: function (x, y, evt, type) { 984 var pEl, 985 el, 986 collect = [], 987 offset = [], 988 haspoint, 989 len = this.objectsList.length, 990 dragEl = {visProp: {layer: -10000}}; 991 992 //for (el in this.objects) { 993 for (el = 0; el < len; el++) { 994 pEl = this.objectsList[el]; 995 haspoint = pEl.hasPoint && pEl.hasPoint(x, y); 996 997 if (pEl.visPropCalc.visible && haspoint) { 998 pEl.triggerEventHandlers([type + 'down', 'down'], [evt]); 999 this.downObjects.push(pEl); 1000 } 1001 1002 if (haspoint && 1003 pEl.isDraggable && 1004 pEl.visPropCalc.visible && 1005 ((this.geonextCompatibilityMode && 1006 (Type.isPoint(pEl) || 1007 pEl.elementClass === Const.OBJECT_CLASS_TEXT) 1008 ) || 1009 !this.geonextCompatibilityMode 1010 ) && 1011 !Type.evaluate(pEl.visProp.fixed) 1012 /*(!pEl.visProp.frozen) &&*/ 1013 ) { 1014 1015 // Elements in the highest layer get priority. 1016 if (pEl.visProp.layer > dragEl.visProp.layer || 1017 (pEl.visProp.layer === dragEl.visProp.layer && 1018 pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime() 1019 )) { 1020 // If an element and its label have the focus 1021 // simultaneously, the element is taken. 1022 // This only works if we assume that every browser runs 1023 // through this.objects in the right order, i.e. an element A 1024 // added before element B turns up here before B does. 1025 if (!this.attr.ignorelabels || 1026 (!Type.exists(dragEl.label) || pEl !== dragEl.label)) { 1027 dragEl = pEl; 1028 collect.push(dragEl); 1029 1030 // Save offset for large coords elements. 1031 if (Type.exists(dragEl.coords)) { 1032 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y])); 1033 } else { 1034 offset.push([0, 0]); 1035 } 1036 1037 // we can't drop out of this loop because of the event handling system 1038 //if (this.attr.takefirst) { 1039 // return collect; 1040 //} 1041 } 1042 } 1043 } 1044 } 1045 1046 if (this.attr.drag.enabled && collect.length > 0) { 1047 this.mode = this.BOARD_MODE_DRAG; 1048 } 1049 1050 // A one-element array is returned. 1051 if (this.attr.takefirst) { 1052 collect.length = 1; 1053 this._drag_offset = offset[0]; 1054 } else { 1055 collect = collect.slice(-1); 1056 this._drag_offset = offset[offset.length - 1]; 1057 } 1058 1059 if (!this._drag_offset) { 1060 this._drag_offset = [0, 0]; 1061 } 1062 1063 // Move drag element to the top of the layer 1064 if (this.renderer.type === 'svg' && 1065 Type.exists(collect[0]) && 1066 Type.evaluate(collect[0].visProp.dragtotopoflayer) && 1067 collect.length === 1 && 1068 Type.exists(collect[0].rendNode)) { 1069 1070 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode); 1071 } 1072 1073 // Init rotation angle and scale factor for two finger movements 1074 this.previousRotation = 0.0; 1075 this.previousScale = 1.0; 1076 1077 if (collect.length >= 1) { 1078 collect[0].highlight(true); 1079 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]); 1080 } 1081 1082 return collect; 1083 }, 1084 1085 /** 1086 * Moves an object. 1087 * @param {Number} x Coordinate 1088 * @param {Number} y Coordinate 1089 * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}. 1090 * @param {Object} evt The event object. 1091 * @param {String} type Mouse or touch event? 1092 */ 1093 moveObject: function (x, y, o, evt, type) { 1094 var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this), 1095 drag, 1096 dragScrCoords, newDragScrCoords; 1097 1098 if (!(o && o.obj)) { 1099 return; 1100 } 1101 drag = o.obj; 1102 1103 // Save updates for very small movements of coordsElements, see below 1104 if (drag.coords) { 1105 dragScrCoords = drag.coords.scrCoords.slice(); 1106 } 1107 1108 /* 1109 * Save the position. 1110 */ 1111 this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]]; 1112 this.drag_position = Statistics.add(this.drag_position, this._drag_offset); 1113 // 1114 // We have to distinguish between CoordsElements and other elements like lines. 1115 // The latter need the difference between two move events. 1116 if (Type.exists(drag.coords)) { 1117 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position); 1118 } else { 1119 this.displayInfobox(false); 1120 // Hide infobox in case the user has touched an intersection point 1121 // and drags the underlying line now. 1122 1123 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) { 1124 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, 1125 [newPos.scrCoords[1], newPos.scrCoords[2]], 1126 [o.targets[0].Xprev, o.targets[0].Yprev] 1127 ); 1128 } 1129 // Remember the actual position for the next move event. Then we are able to 1130 // compute the difference vector. 1131 o.targets[0].Xprev = newPos.scrCoords[1]; 1132 o.targets[0].Yprev = newPos.scrCoords[2]; 1133 } 1134 // This may be necessary for some gliders and labels 1135 if (Type.exists(drag.coords)) { 1136 drag.prepareUpdate().update(false).updateRenderer(); 1137 this.updateInfobox(drag); 1138 drag.prepareUpdate().update(true).updateRenderer(); 1139 } 1140 1141 if (drag.coords) { 1142 newDragScrCoords = drag.coords.scrCoords; 1143 } 1144 // No updates for very small movements of coordsElements 1145 if (!drag.coords || 1146 dragScrCoords[1] !== newDragScrCoords[1] || 1147 dragScrCoords[2] !== newDragScrCoords[2]) { 1148 1149 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]); 1150 1151 this.update(); 1152 } 1153 drag.highlight(true); 1154 this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]); 1155 1156 drag.lastDragTime = new Date(); 1157 }, 1158 1159 /** 1160 * Moves elements in multitouch mode. 1161 * @param {Array} p1 x,y coordinates of first touch 1162 * @param {Array} p2 x,y coordinates of second touch 1163 * @param {Object} o The touch object that is dragged: {JXG.Board#touches}. 1164 * @param {Object} evt The event object that lead to this movement. 1165 */ 1166 twoFingerMove: function (o, id, evt) { 1167 var drag; 1168 1169 if (Type.exists(o) && Type.exists(o.obj)) { 1170 drag = o.obj; 1171 } else { 1172 return; 1173 } 1174 1175 if (drag.elementClass === Const.OBJECT_CLASS_LINE || 1176 drag.type === Const.OBJECT_TYPE_POLYGON) { 1177 this.twoFingerTouchObject(o.targets, drag, id); 1178 } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1179 this.twoFingerTouchCircle(o.targets, drag, id); 1180 } 1181 1182 if (evt) { 1183 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]); 1184 } 1185 }, 1186 1187 /** 1188 * Moves, rotates and scales a line or polygon with two fingers. 1189 * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}. 1190 * @param {object} drag The object that is dragged: 1191 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1192 */ 1193 twoFingerTouchObject: function (tar, drag, id) { 1194 var np, op, nd, od, 1195 d, alpha, 1196 S, t1, t3, t4, t5, 1197 ar, i, len, 1198 fixEl, moveEl, fix; 1199 1200 if (Type.exists(tar[0]) && Type.exists(tar[1]) && 1201 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) { 1202 1203 if (id === tar[0].num) { 1204 fixEl = tar[1]; 1205 moveEl = tar[0]; 1206 } else { 1207 fixEl = tar[0]; 1208 moveEl = tar[1]; 1209 } 1210 1211 fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords; 1212 // Previous finger position 1213 op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords; 1214 // New finger position 1215 np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords; 1216 1217 // Old and new directions 1218 od = Mat.crossProduct(fix, op); 1219 nd = Mat.crossProduct(fix, np); 1220 1221 // Intersection between the two directions 1222 S = Mat.crossProduct(od, nd); 1223 1224 // If parallel translate, otherwise rotate 1225 if (Math.abs(S[0]) < Mat.eps) { 1226 return; 1227 } 1228 1229 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1)); 1230 1231 t1 = this.create('transform', [alpha, [fix[1], fix[2]]], {type: 'rotate'}); 1232 t1.update(); 1233 1234 if (Type.evaluate(drag.visProp.scalable)) { 1235 // Scale 1236 d = Geometry.distance(np, fix) / Geometry.distance(op, fix); 1237 1238 t3 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'}); 1239 t4 = this.create('transform', [d, d], {type: 'scale'}); 1240 t5 = this.create('transform', [fix[1], fix[2]], {type: 'translate'}); 1241 t1.melt(t3).melt(t4).melt(t5); 1242 } 1243 1244 if (drag.elementClass === Const.OBJECT_CLASS_LINE) { 1245 ar = []; 1246 if (drag.point1.draggable()) { 1247 ar.push(drag.point1); 1248 } 1249 if (drag.point2.draggable()) { 1250 ar.push(drag.point2); 1251 } 1252 t1.applyOnce(ar); 1253 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) { 1254 ar = []; 1255 len = drag.vertices.length - 1; 1256 for (i = 0; i < len; ++i) { 1257 if (drag.vertices[i].draggable()) { 1258 ar.push(drag.vertices[i]); 1259 } 1260 } 1261 t1.applyOnce(ar); 1262 } 1263 1264 this.update(); 1265 drag.highlight(true); 1266 } 1267 }, 1268 1269 /* 1270 * Moves, rotates and scales a circle with two fingers. 1271 * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}. 1272 * @param {object} drag The object that is dragged: 1273 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1274 */ 1275 twoFingerTouchCircle: function (tar, drag, id) { 1276 var fixEl, moveEl, np, op, fix, 1277 d, alpha, t1, t2, t3, t4; 1278 1279 if (drag.method === 'pointCircle' || drag.method === 'pointLine') { 1280 return; 1281 } 1282 1283 if (Type.exists(tar[0]) && Type.exists(tar[1]) && 1284 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) { 1285 1286 if (id === tar[0].num) { 1287 fixEl = tar[1]; 1288 moveEl = tar[0]; 1289 } else { 1290 fixEl = tar[0]; 1291 moveEl = tar[1]; 1292 } 1293 1294 fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords; 1295 // Previous finger position 1296 op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords; 1297 // New finger position 1298 np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords; 1299 1300 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1)); 1301 1302 // Rotate and scale by the movement of the second finger 1303 t1 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'}); 1304 t2 = this.create('transform', [alpha], {type: 'rotate'}); 1305 t1.melt(t2); 1306 if (Type.evaluate(drag.visProp.scalable)) { 1307 d = Geometry.distance(fix, np) / Geometry.distance(fix, op); 1308 t3 = this.create('transform', [d, d], {type: 'scale'}); 1309 t1.melt(t3); 1310 } 1311 t4 = this.create('transform', [fix[1], fix[2]], {type: 'translate'}); 1312 t1.melt(t4); 1313 1314 if (drag.center.draggable()) { 1315 t1.applyOnce([drag.center]); 1316 } 1317 1318 if (drag.method === 'twoPoints') { 1319 if (drag.point2.draggable()) { 1320 t1.applyOnce([drag.point2]); 1321 } 1322 } else if (drag.method === 'pointRadius') { 1323 if (Type.isNumber(drag.updateRadius.origin)) { 1324 drag.setRadius(drag.radius * d); 1325 } 1326 } 1327 1328 this.update(drag.center); 1329 drag.highlight(true); 1330 } 1331 }, 1332 1333 highlightElements: function (x, y, evt, target) { 1334 var el, pEl, pId, 1335 overObjects = {}, 1336 len = this.objectsList.length; 1337 1338 // Elements below the mouse pointer which are not highlighted yet will be highlighted. 1339 for (el = 0; el < len; el++) { 1340 pEl = this.objectsList[el]; 1341 pId = pEl.id; 1342 if (Type.exists(pEl.hasPoint) && pEl.visPropCalc.visible && pEl.hasPoint(x, y)) { 1343 // this is required in any case because otherwise the box won't be shown until the point is dragged 1344 this.updateInfobox(pEl); 1345 1346 if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted 1347 overObjects[pId] = pEl; 1348 pEl.highlight(); 1349 // triggers board event. 1350 this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]); 1351 } 1352 1353 if (pEl.mouseover) { 1354 pEl.triggerEventHandlers(['mousemove', 'move'], [evt]); 1355 } else { 1356 pEl.triggerEventHandlers(['mouseover', 'over'], [evt]); 1357 pEl.mouseover = true; 1358 } 1359 } 1360 } 1361 1362 for (el = 0; el < len; el++) { 1363 pEl = this.objectsList[el]; 1364 pId = pEl.id; 1365 if (pEl.mouseover) { 1366 if (!overObjects[pId]) { 1367 pEl.triggerEventHandlers(['mouseout', 'out'], [evt]); 1368 pEl.mouseover = false; 1369 } 1370 } 1371 } 1372 }, 1373 1374 /** 1375 * Helper function which returns a reasonable starting point for the object being dragged. 1376 * Formerly known as initXYstart(). 1377 * @private 1378 * @param {JXG.GeometryElement} obj The object to be dragged 1379 * @param {Array} targets Array of targets. It is changed by this function. 1380 */ 1381 saveStartPos: function (obj, targets) { 1382 var xy = [], i, len; 1383 1384 if (obj.type === Const.OBJECT_TYPE_TICKS) { 1385 xy.push([1, NaN, NaN]); 1386 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) { 1387 xy.push(obj.point1.coords.usrCoords); 1388 xy.push(obj.point2.coords.usrCoords); 1389 } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1390 xy.push(obj.center.coords.usrCoords); 1391 if (obj.method === 'twoPoints') { 1392 xy.push(obj.point2.coords.usrCoords); 1393 } 1394 } else if (obj.type === Const.OBJECT_TYPE_POLYGON) { 1395 len = obj.vertices.length - 1; 1396 for (i = 0; i < len; i++) { 1397 xy.push(obj.vertices[i].coords.usrCoords); 1398 } 1399 } else if (obj.type === Const.OBJECT_TYPE_SECTOR) { 1400 xy.push(obj.point1.coords.usrCoords); 1401 xy.push(obj.point2.coords.usrCoords); 1402 xy.push(obj.point3.coords.usrCoords); 1403 } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) { 1404 xy.push(obj.coords.usrCoords); 1405 } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) { 1406 // if (Type.exists(obj.parents)) { 1407 // len = obj.parents.length; 1408 // if (len > 0) { 1409 // for (i = 0; i < len; i++) { 1410 // xy.push(this.select(obj.parents[i]).coords.usrCoords); 1411 // } 1412 // } else 1413 // } 1414 if (obj.points.length > 0) { 1415 xy.push(obj.points[0].usrCoords); 1416 } 1417 } else { 1418 try { 1419 xy.push(obj.coords.usrCoords); 1420 } catch (e) { 1421 JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e); 1422 } 1423 } 1424 1425 len = xy.length; 1426 for (i = 0; i < len; i++) { 1427 targets.Zstart.push(xy[i][0]); 1428 targets.Xstart.push(xy[i][1]); 1429 targets.Ystart.push(xy[i][2]); 1430 } 1431 }, 1432 1433 mouseOriginMoveStart: function (evt) { 1434 var r, pos; 1435 1436 r = this._isRequiredKeyPressed(evt, 'pan'); 1437 if (r) { 1438 pos = this.getMousePosition(evt); 1439 this.initMoveOrigin(pos[0], pos[1]); 1440 } 1441 1442 return r; 1443 }, 1444 1445 mouseOriginMove: function (evt) { 1446 var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN), 1447 pos; 1448 1449 if (r) { 1450 pos = this.getMousePosition(evt); 1451 this.moveOrigin(pos[0], pos[1], true); 1452 } 1453 1454 return r; 1455 }, 1456 1457 /** 1458 * Start moving the origin with one finger. 1459 * @private 1460 * @param {Object} evt Event from touchStartListener 1461 * @return {Boolean} returns if the origin is moved. 1462 */ 1463 touchStartMoveOriginOneFinger: function (evt) { 1464 var touches = evt[JXG.touchProperty], 1465 conditions, pos; 1466 1467 conditions = this.attr.pan.enabled && 1468 !this.attr.pan.needtwofingers && 1469 touches.length === 1; 1470 1471 if (conditions) { 1472 pos = this.getMousePosition(evt, 0); 1473 this.initMoveOrigin(pos[0], pos[1]); 1474 } 1475 1476 return conditions; 1477 }, 1478 1479 /** 1480 * Move the origin with one finger 1481 * @private 1482 * @param {Object} evt Event from touchMoveListener 1483 * @return {Boolean} returns if the origin is moved. 1484 */ 1485 touchOriginMove: function (evt) { 1486 var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN), 1487 pos; 1488 1489 if (r) { 1490 pos = this.getMousePosition(evt, 0); 1491 this.moveOrigin(pos[0], pos[1], true); 1492 } 1493 1494 return r; 1495 }, 1496 1497 /** 1498 * Stop moving the origin with one finger 1499 * @return {null} null 1500 * @private 1501 */ 1502 originMoveEnd: function () { 1503 this.updateQuality = this.BOARD_QUALITY_HIGH; 1504 this.mode = this.BOARD_MODE_NONE; 1505 }, 1506 1507 /********************************************************** 1508 * 1509 * Event Handler 1510 * 1511 **********************************************************/ 1512 1513 /** 1514 * Add all possible event handlers to the board object 1515 */ 1516 addEventHandlers: function () { 1517 if (Env.supportsPointerEvents()) { 1518 this.addPointerEventHandlers(); 1519 } else { 1520 this.addMouseEventHandlers(); 1521 this.addTouchEventHandlers(); 1522 } 1523 1524 // This one produces errors on IE 1525 //Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this); 1526 // This one works on IE, Firefox and Chromium with default configurations. On some Safari 1527 // or Opera versions the user must explicitly allow the deactivation of the context menu. 1528 if (this.containerObj !== null) { 1529 this.containerObj.oncontextmenu = function (e) { 1530 if (Type.exists(e)) { 1531 e.preventDefault(); 1532 } 1533 return false; 1534 }; 1535 } 1536 1537 this.addFullscreenEventHandlers(); 1538 this.addKeyboardEventHandlers(); 1539 1540 if (Env.isBrowser) { 1541 try { 1542 // resizeObserver: triggered if size of the JSXGraph div changes. 1543 this.startResizeObserver(); 1544 } catch (err) { 1545 // resize event: triggered if size of window changes 1546 Env.addEvent(window, 'resize', this.resizeListener, this); 1547 // intersectionObserver: triggered if JSXGraph becomes visible. 1548 this.startIntersectionObserver(); 1549 } 1550 // Scroll event: needs to be captured since on mobile devices 1551 // sometimes a header bar is displayed / hidden, which triggers a 1552 // resize event. 1553 Env.addEvent(window, 'scroll', this.scrollListener, this); 1554 } 1555 }, 1556 1557 /** 1558 * Remove all event handlers from the board object 1559 */ 1560 removeEventHandlers: function () { 1561 this.removeMouseEventHandlers(); 1562 this.removeTouchEventHandlers(); 1563 this.removePointerEventHandlers(); 1564 1565 this.removeFullscreenEventHandlers(); 1566 this.removeKeyboardEventHandlers(); 1567 if (Env.isBrowser) { 1568 if (Type.exists(this.resizeObserver)) { 1569 this.stopResizeObserver(); 1570 } else { 1571 Env.removeEvent(window, 'resize', this.resizeListener, this); 1572 this.stopIntersectionObserver(); 1573 } 1574 Env.removeEvent(window, 'scroll', this.scrollListener, this); 1575 } 1576 }, 1577 1578 /** 1579 * Registers the MSPointer* event handlers. 1580 */ 1581 addPointerEventHandlers: function () { 1582 if (!this.hasPointerHandlers && Env.isBrowser) { 1583 var moveTarget = this.attr.movetarget || this.containerObj; 1584 1585 if (window.navigator.msPointerEnabled) { // IE10- 1586 Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1587 Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 1588 } else { 1589 Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1590 Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 1591 } 1592 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1593 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1594 1595 if (this.containerObj !== null) { 1596 // This is needed for capturing touch events. 1597 // It is also in jsxgraph.css, but one never knows... 1598 this.containerObj.style.touchAction = 'none'; 1599 } 1600 1601 this.hasPointerHandlers = true; 1602 } 1603 }, 1604 1605 /** 1606 * Registers mouse move, down and wheel event handlers. 1607 */ 1608 addMouseEventHandlers: function () { 1609 if (!this.hasMouseHandlers && Env.isBrowser) { 1610 var moveTarget = this.attr.movetarget || this.containerObj; 1611 1612 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1613 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 1614 1615 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1616 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1617 1618 this.hasMouseHandlers = true; 1619 } 1620 }, 1621 1622 /** 1623 * Register touch start and move and gesture start and change event handlers. 1624 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 1625 * will not be registered. 1626 * 1627 * Since iOS 13, touch events were abandoned in favour of pointer events 1628 */ 1629 addTouchEventHandlers: function (appleGestures) { 1630 if (!this.hasTouchHandlers && Env.isBrowser) { 1631 var moveTarget = this.attr.movetarget || this.containerObj; 1632 1633 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1634 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 1635 1636 /* 1637 if (!Type.exists(appleGestures) || appleGestures) { 1638 // Gesture listener are called in touchStart and touchMove. 1639 //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 1640 //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 1641 } 1642 */ 1643 1644 this.hasTouchHandlers = true; 1645 } 1646 }, 1647 1648 /** 1649 * Add fullscreen events which update the CSS transformation matrix to correct 1650 * the mouse/touch/pointer positions in case of CSS transformations. 1651 */ 1652 addFullscreenEventHandlers: function() { 1653 var i, 1654 // standard/Edge, firefox, chrome/safari, IE11 1655 events = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'], 1656 le = events.length; 1657 1658 if (!this.hasFullsceenEventHandlers && Env.isBrowser) { 1659 for (i = 0; i < le; i++) { 1660 Env.addEvent(this.document, events[i], this.fullscreenListener, this); 1661 } 1662 this.hasFullsceenEventHandlers = true; 1663 } 1664 }, 1665 1666 addKeyboardEventHandlers: function() { 1667 if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) { 1668 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this); 1669 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 1670 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 1671 this.hasKeyboardHandlers = true; 1672 } 1673 }, 1674 1675 /** 1676 * Remove all registered touch event handlers. 1677 */ 1678 removeKeyboardEventHandlers: function () { 1679 if (this.hasKeyboardHandlers && Env.isBrowser) { 1680 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this); 1681 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 1682 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 1683 this.hasKeyboardHandlers = false; 1684 } 1685 }, 1686 1687 /** 1688 * Remove all registered event handlers regarding fullscreen mode. 1689 */ 1690 removeFullscreenEventHandlers: function() { 1691 var i, 1692 // standard/Edge, firefox, chrome/safari, IE11 1693 events = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'], 1694 le = events.length; 1695 1696 if (this.hasFullsceenEventHandlers && Env.isBrowser) { 1697 for (i = 0; i < le; i++) { 1698 Env.removeEvent(this.document, events[i], this.fullscreenListener, this); 1699 } 1700 this.hasFullsceenEventHandlers = false; 1701 } 1702 }, 1703 1704 /** 1705 * Remove MSPointer* Event handlers. 1706 */ 1707 removePointerEventHandlers: function () { 1708 if (this.hasPointerHandlers && Env.isBrowser) { 1709 var moveTarget = this.attr.movetarget || this.containerObj; 1710 1711 if (window.navigator.msPointerEnabled) { // IE10- 1712 Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1713 Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 1714 } else { 1715 Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1716 Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 1717 } 1718 1719 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1720 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1721 1722 if (this.hasPointerUp) { 1723 if (window.navigator.msPointerEnabled) { // IE10- 1724 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 1725 } else { 1726 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 1727 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 1728 } 1729 this.hasPointerUp = false; 1730 } 1731 1732 this.hasPointerHandlers = false; 1733 } 1734 }, 1735 1736 /** 1737 * De-register mouse event handlers. 1738 */ 1739 removeMouseEventHandlers: function () { 1740 if (this.hasMouseHandlers && Env.isBrowser) { 1741 var moveTarget = this.attr.movetarget || this.containerObj; 1742 1743 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1744 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 1745 1746 if (this.hasMouseUp) { 1747 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 1748 this.hasMouseUp = false; 1749 } 1750 1751 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1752 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1753 1754 this.hasMouseHandlers = false; 1755 } 1756 }, 1757 1758 /** 1759 * Remove all registered touch event handlers. 1760 */ 1761 removeTouchEventHandlers: function () { 1762 if (this.hasTouchHandlers && Env.isBrowser) { 1763 var moveTarget = this.attr.movetarget || this.containerObj; 1764 1765 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1766 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 1767 1768 if (this.hasTouchEnd) { 1769 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 1770 this.hasTouchEnd = false; 1771 } 1772 1773 this.hasTouchHandlers = false; 1774 } 1775 }, 1776 1777 /** 1778 * Handler for click on left arrow in the navigation bar 1779 * @returns {JXG.Board} Reference to the board 1780 */ 1781 clickLeftArrow: function () { 1782 this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]); 1783 return this; 1784 }, 1785 1786 /** 1787 * Handler for click on right arrow in the navigation bar 1788 * @returns {JXG.Board} Reference to the board 1789 */ 1790 clickRightArrow: function () { 1791 this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]); 1792 return this; 1793 }, 1794 1795 /** 1796 * Handler for click on up arrow in the navigation bar 1797 * @returns {JXG.Board} Reference to the board 1798 */ 1799 clickUpArrow: function () { 1800 this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1); 1801 return this; 1802 }, 1803 1804 /** 1805 * Handler for click on down arrow in the navigation bar 1806 * @returns {JXG.Board} Reference to the board 1807 */ 1808 clickDownArrow: function () { 1809 this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1); 1810 return this; 1811 }, 1812 1813 /** 1814 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 1815 * Works on iOS/Safari and Android. 1816 * @param {Event} evt Browser event object 1817 * @returns {Boolean} 1818 */ 1819 gestureChangeListener: function (evt) { 1820 var c, 1821 dir1 = [], 1822 dir2 = [], 1823 angle, 1824 mi = 10, 1825 isPinch = false, 1826 // Save zoomFactors 1827 zx = this.attr.zoom.factorx, 1828 zy = this.attr.zoom.factory, 1829 factor, 1830 dist, 1831 dx, dy, theta, cx, cy, bound; 1832 1833 if (this.mode !== this.BOARD_MODE_ZOOM) { 1834 return true; 1835 } 1836 evt.preventDefault(); 1837 1838 dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY], 1839 [evt.touches[1].clientX, evt.touches[1].clientY], 2); 1840 1841 // Android pinch to zoom 1842 // evt.scale was available in iOS touch events (pre iOS 13) 1843 // evt.scale is undefined in Android 1844 if (evt.scale === undefined) { 1845 evt.scale = dist / this.prevDist; 1846 } 1847 1848 if (!Type.exists(this.prevCoords)) { 1849 return false; 1850 } 1851 // Compute the angle of the two finger directions 1852 dir1 = [evt.touches[0].clientX - this.prevCoords[0][0], 1853 evt.touches[0].clientY - this.prevCoords[0][1]]; 1854 dir2 = [evt.touches[1].clientX - this.prevCoords[1][0], 1855 evt.touches[1].clientY - this.prevCoords[1][1]]; 1856 1857 if ((dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi) && 1858 (dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi)) { 1859 return false; 1860 } 1861 1862 angle = Geometry.rad(dir1, [0,0], dir2); 1863 if (this.isPreviousGesture !== 'pan' && 1864 Math.abs(angle) > Math.PI * 0.2 && 1865 Math.abs(angle) < Math.PI * 1.8) { 1866 isPinch = true; 1867 } 1868 1869 if (this.isPreviousGesture !== 'pan' && !isPinch) { 1870 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) { 1871 isPinch = true; 1872 } 1873 } 1874 1875 factor = evt.scale / this.prevScale; 1876 this.prevScale = evt.scale; 1877 this.prevCoords = [[evt.touches[0].clientX, evt.touches[0].clientY], 1878 [evt.touches[1].clientX, evt.touches[1].clientY]]; 1879 1880 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 1881 1882 if (this.attr.pan.enabled && 1883 this.attr.pan.needtwofingers && 1884 !isPinch) { 1885 // Pan detected 1886 1887 this.isPreviousGesture = 'pan'; 1888 1889 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true); 1890 } else if (this.attr.zoom.enabled && 1891 Math.abs(factor - 1.0) < 0.5) { 1892 // Pinch detected 1893 1894 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) { 1895 dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX); 1896 dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY); 1897 theta = Math.abs(Math.atan2(dy, dx)); 1898 bound = Math.PI * this.attr.zoom.pinchsensitivity / 90.0; 1899 } 1900 1901 if (this.attr.zoom.pinchhorizontal && theta < bound) { 1902 this.attr.zoom.factorx = factor; 1903 this.attr.zoom.factory = 1.0; 1904 cx = 0; 1905 cy = 0; 1906 } else if (this.attr.zoom.pinchvertical && Math.abs(theta - Math.PI * 0.5) < bound) { 1907 this.attr.zoom.factorx = 1.0; 1908 this.attr.zoom.factory = factor; 1909 cx = 0; 1910 cy = 0; 1911 } else { 1912 this.attr.zoom.factorx = factor; 1913 this.attr.zoom.factory = factor; 1914 cx = c.usrCoords[1]; 1915 cy = c.usrCoords[2]; 1916 } 1917 1918 this.zoomIn(cx, cy); 1919 1920 // Restore zoomFactors 1921 this.attr.zoom.factorx = zx; 1922 this.attr.zoom.factory = zy; 1923 } 1924 1925 return false; 1926 }, 1927 1928 /** 1929 * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari, 1930 * on Android we emulate it. 1931 * @param {Event} evt 1932 * @returns {Boolean} 1933 */ 1934 gestureStartListener: function (evt) { 1935 var pos; 1936 1937 evt.preventDefault(); 1938 this.prevScale = 1.0; 1939 // Android pinch to zoom 1940 this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY], 1941 [evt.touches[1].clientX, evt.touches[1].clientY], 2); 1942 this.prevCoords = [[evt.touches[0].clientX, evt.touches[0].clientY], 1943 [evt.touches[1].clientX, evt.touches[1].clientY]]; 1944 this.isPreviousGesture = 'none'; 1945 1946 // If pinch-to-zoom is interpreted as panning 1947 // we have to prepare move origin 1948 pos = this.getMousePosition(evt, 0); 1949 this.initMoveOrigin(pos[0], pos[1]); 1950 1951 this.mode = this.BOARD_MODE_ZOOM; 1952 return false; 1953 }, 1954 1955 /** 1956 * Test if the required key combination is pressed for wheel zoom, move origin and 1957 * selection 1958 * @private 1959 * @param {Object} evt Mouse or pen event 1960 * @param {String} action String containing the action: 'zoom', 'pan', 'selection'. 1961 * Corresponds to the attribute subobject. 1962 * @return {Boolean} true or false. 1963 */ 1964 _isRequiredKeyPressed: function (evt, action) { 1965 var obj = this.attr[action]; 1966 if (!obj.enabled) { 1967 return false; 1968 } 1969 1970 if (((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) && 1971 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey)) 1972 ) { 1973 return true; 1974 } 1975 1976 return false; 1977 }, 1978 1979 /* 1980 * Pointer events 1981 */ 1982 1983 /** 1984 * 1985 * Check if pointer event is already registered in {@link JXG.Board#_board_touches}. 1986 * 1987 * @param {Object} evt Event object 1988 * @return {Boolean} true if down event has already been sent. 1989 * @private 1990 */ 1991 _isPointerRegistered: function(evt) { 1992 var i, len = this._board_touches.length; 1993 1994 for (i = 0; i < len; i++) { 1995 if (this._board_touches[i].pointerId === evt.pointerId) { 1996 return true; 1997 } 1998 } 1999 return false; 2000 }, 2001 2002 /** 2003 * 2004 * Store the position of a pointer event. 2005 * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}. 2006 * Allows to follow the path of that finger on the screen. 2007 * Only two simultaneous touches are supported. 2008 * 2009 * @param {Object} evt Event object 2010 * @returns {JXG.Board} Reference to the board 2011 * @private 2012 */ 2013 _pointerStorePosition: function (evt) { 2014 var i, found; 2015 2016 for (i = 0, found = false; i < this._board_touches.length; i++) { 2017 if (this._board_touches[i].pointerId === evt.pointerId) { 2018 this._board_touches[i].clientX = evt.clientX; 2019 this._board_touches[i].clientY = evt.clientY; 2020 found = true; 2021 break; 2022 } 2023 } 2024 2025 // Restrict the number of simultaneous touches to 2 2026 if (!found && this._board_touches.length < 2) { 2027 this._board_touches.push({ 2028 pointerId: evt.pointerId, 2029 clientX: evt.clientX, 2030 clientY: evt.clientY 2031 }); 2032 } 2033 2034 return this; 2035 }, 2036 2037 /** 2038 * Deregisters a pointer event in {@link JXG.Board#_board_touches}. 2039 * It happens if a finger has been lifted from the screen. 2040 * 2041 * @param {Object} evt Event object 2042 * @returns {JXG.Board} Reference to the board 2043 * @private 2044 */ 2045 _pointerRemoveTouches: function (evt) { 2046 var i; 2047 for (i = 0; i < this._board_touches.length; i++) { 2048 if (this._board_touches[i].pointerId === evt.pointerId) { 2049 this._board_touches.splice(i, 1); 2050 break; 2051 } 2052 } 2053 2054 return this; 2055 }, 2056 2057 /** 2058 * Remove all registered fingers from {@link JXG.Board#_board_touches}. 2059 * This might be necessary if too many fingers have been registered. 2060 * @returns {JXG.Board} Reference to the board 2061 * @private 2062 */ 2063 _pointerClearTouches: function() { 2064 if (this._board_touches.length > 0) { 2065 this.dehighlightAll(); 2066 } 2067 this.updateQuality = this.BOARD_QUALITY_HIGH; 2068 this.mode = this.BOARD_MODE_NONE; 2069 this._board_touches = []; 2070 this.touches = []; 2071 }, 2072 2073 /** 2074 * Determine which input device is used for this action. 2075 * Possible devices are 'touch', 'pen' and 'mouse'. 2076 * This affects the precision and certain events. 2077 * In case of no browser, 'mouse' is used. 2078 * 2079 * @see JXG.Board#pointerDownListener 2080 * @see JXG.Board#pointerMoveListener 2081 * @see JXG.Board#initMoveObject 2082 * @see JXG.Board#moveObject 2083 * 2084 * @param {Event} evt The browsers event object. 2085 * @returns {String} 'mouse', 'pen', or 'touch' 2086 * @private 2087 */ 2088 _getPointerInputDevice: function(evt) { 2089 if (Env.isBrowser) { 2090 if (evt.pointerType === 'touch' || // New 2091 (window.navigator.msMaxTouchPoints && // Old 2092 window.navigator.msMaxTouchPoints > 1)) { 2093 return 'touch'; 2094 } 2095 if (evt.pointerType === 'mouse') { 2096 return 'mouse'; 2097 } 2098 if (evt.pointerType === 'pen') { 2099 return 'pen'; 2100 } 2101 } 2102 return 'mouse'; 2103 }, 2104 2105 /** 2106 * This method is called by the browser when a pointing device is pressed on the screen. 2107 * @param {Event} evt The browsers event object. 2108 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 2109 * @returns {Boolean} ... 2110 */ 2111 pointerDownListener: function (evt, object) { 2112 var i, j, k, pos, elements, sel, 2113 target_obj, 2114 type = 'mouse', // Used in case of no browser 2115 found, target; 2116 2117 // Fix for Firefox browser: When using a second finger, the 2118 // touch event for the first finger is sent again. 2119 if (!object && this._isPointerRegistered(evt)) { 2120 return false; 2121 } 2122 2123 if (!object && evt.isPrimary) { 2124 // First finger down. To be on the safe side this._board_touches is cleared. 2125 this._pointerClearTouches(); 2126 } 2127 2128 if (!this.hasPointerUp) { 2129 if (window.navigator.msPointerEnabled) { // IE10- 2130 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2131 } else { 2132 // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android 2133 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 2134 Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2135 } 2136 this.hasPointerUp = true; 2137 } 2138 2139 if (this.hasMouseHandlers) { 2140 this.removeMouseEventHandlers(); 2141 } 2142 2143 if (this.hasTouchHandlers) { 2144 this.removeTouchEventHandlers(); 2145 } 2146 2147 // Prevent accidental selection of text 2148 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2149 this.document.selection.empty(); 2150 } else if (window.getSelection) { 2151 sel = window.getSelection(); 2152 if (sel.removeAllRanges) { 2153 try { 2154 sel.removeAllRanges(); 2155 } catch (e) {} 2156 } 2157 } 2158 2159 // Mouse, touch or pen device 2160 this._inputDevice = this._getPointerInputDevice(evt); 2161 type = this._inputDevice; 2162 this.options.precision.hasPoint = this.options.precision[type]; 2163 2164 // Handling of multi touch with pointer events should be easier than the touch events. 2165 // Every pointer device has its own pointerId, e.g. the mouse 2166 // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will 2167 // keep this id until a pointerUp event is fired. What we have to do here is: 2168 // 1. collect all elements under the current pointer 2169 // 2. run through the touches control structure 2170 // a. look for the object collected in step 1. 2171 // b. if an object is found, check the number of pointers. If appropriate, add the pointer. 2172 pos = this.getMousePosition(evt); 2173 2174 // selection 2175 this._testForSelection(evt); 2176 if (this.selectingMode) { 2177 this._startSelecting(pos); 2178 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]); 2179 return; // don't continue as a normal click 2180 } 2181 2182 if (this.attr.drag.enabled && object) { 2183 elements = [ object ]; 2184 this.mode = this.BOARD_MODE_DRAG; 2185 } else { 2186 elements = this.initMoveObject(pos[0], pos[1], evt, type); 2187 } 2188 2189 target_obj = { 2190 num: evt.pointerId, 2191 X: pos[0], 2192 Y: pos[1], 2193 Xprev: NaN, 2194 Yprev: NaN, 2195 Xstart: [], 2196 Ystart: [], 2197 Zstart: [] 2198 }; 2199 2200 // If no draggable object can be found, get out here immediately 2201 if (elements.length > 0) { 2202 // check touches structure 2203 target = elements[elements.length - 1]; 2204 found = false; 2205 2206 // Reminder: this.touches is the list of elements which 2207 // currently "possess" a pointer (mouse, pen, finger) 2208 for (i = 0; i < this.touches.length; i++) { 2209 // An element receives a further touch, i.e. 2210 // the target is already in our touches array, add the pointer to the existing touch 2211 if (this.touches[i].obj === target) { 2212 j = i; 2213 k = this.touches[i].targets.push(target_obj) - 1; 2214 found = true; 2215 break; 2216 } 2217 } 2218 if (!found) { 2219 // An new element hae been touched. 2220 k = 0; 2221 j = this.touches.push({ 2222 obj: target, 2223 targets: [target_obj] 2224 }) - 1; 2225 } 2226 2227 this.dehighlightAll(); 2228 target.highlight(true); 2229 2230 this.saveStartPos(target, this.touches[j].targets[k]); 2231 2232 // Prevent accidental text selection 2233 // this could get us new trouble: input fields, links and drop down boxes placed as text 2234 // on the board don't work anymore. 2235 if (evt && evt.preventDefault) { 2236 evt.preventDefault(); 2237 } else if (window.event) { 2238 window.event.returnValue = false; 2239 } 2240 } 2241 2242 if (this.touches.length > 0) { 2243 evt.preventDefault(); 2244 evt.stopPropagation(); 2245 } 2246 2247 if (!Env.isBrowser) { 2248 return false; 2249 } 2250 if (this._getPointerInputDevice(evt) !== 'touch') { 2251 if (this.mode === this.BOARD_MODE_NONE) { 2252 this.mouseOriginMoveStart(evt); 2253 } 2254 } else { 2255 this._pointerStorePosition(evt); 2256 evt.touches = this._board_touches; 2257 2258 // Touch events on empty areas of the board are handled here, see also touchStartListener 2259 // 1. case: one finger. If allowed, this triggers pan with one finger 2260 if (evt.touches.length === 1 && 2261 this.mode === this.BOARD_MODE_NONE && 2262 this.touchStartMoveOriginOneFinger(evt)) { 2263 // Empty by purpose 2264 } else if (evt.touches.length === 2 && 2265 (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2266 ) { 2267 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2268 // This happens when the second finger hits the device. First, the 2269 // "one finger pan mode" has to be cancelled. 2270 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2271 this.originMoveEnd(); 2272 } 2273 2274 this.gestureStartListener(evt); 2275 } 2276 } 2277 2278 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 2279 return false; 2280 }, 2281 2282 // /** 2283 // * Called if pointer leaves an HTML tag. It is called by the inner-most tag. 2284 // * That means, if a JSXGraph text, i.e. an HTML div, is placed close 2285 // * to the border of the board, this pointerout event will be ignored. 2286 // * @param {Event} evt 2287 // * @return {Boolean} 2288 // */ 2289 // pointerOutListener: function (evt) { 2290 // if (evt.target === this.containerObj || 2291 // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) { 2292 // this.pointerUpListener(evt); 2293 // } 2294 // return this.mode === this.BOARD_MODE_NONE; 2295 // }, 2296 2297 /** 2298 * Called periodically by the browser while the user moves a pointing device across the screen. 2299 * @param {Event} evt 2300 * @returns {Boolean} 2301 */ 2302 pointerMoveListener: function (evt) { 2303 var i, j, pos, touchTargets, 2304 type = 'mouse'; // in case of no browser 2305 2306 if (this._getPointerInputDevice(evt) === 'touch' && !this._isPointerRegistered(evt)) { 2307 // Test, if there was a previous down event of this _getPointerId 2308 // (in case it is a touch event). 2309 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry. 2310 return this.BOARD_MODE_NONE; 2311 } 2312 2313 if (!this.checkFrameRate(evt)) { 2314 return false; 2315 } 2316 2317 if (this.mode !== this.BOARD_MODE_DRAG) { 2318 this.dehighlightAll(); 2319 this.displayInfobox(false); 2320 } 2321 2322 if (this.mode !== this.BOARD_MODE_NONE) { 2323 evt.preventDefault(); 2324 evt.stopPropagation(); 2325 } 2326 2327 this.updateQuality = this.BOARD_QUALITY_LOW; 2328 // Mouse, touch or pen device 2329 this._inputDevice = this._getPointerInputDevice(evt); 2330 type = this._inputDevice; 2331 this.options.precision.hasPoint = this.options.precision[type]; 2332 2333 // selection 2334 if (this.selectingMode) { 2335 pos = this.getMousePosition(evt); 2336 this._moveSelecting(pos); 2337 this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]); 2338 } else if (!this.mouseOriginMove(evt)) { 2339 if (this.mode === this.BOARD_MODE_DRAG) { 2340 // Run through all jsxgraph elements which are touched by at least one finger. 2341 for (i = 0; i < this.touches.length; i++) { 2342 touchTargets = this.touches[i].targets; 2343 // Run through all touch events which have been started on this jsxgraph element. 2344 for (j = 0; j < touchTargets.length; j++) { 2345 if (touchTargets[j].num === evt.pointerId) { 2346 2347 pos = this.getMousePosition(evt); 2348 touchTargets[j].X = pos[0]; 2349 touchTargets[j].Y = pos[1]; 2350 2351 if (touchTargets.length === 1) { 2352 // Touch by one finger: this is possible for all elements that can be dragged 2353 this.moveObject(pos[0], pos[1], this.touches[i], evt, type); 2354 } else if (touchTargets.length === 2) { 2355 // Touch by two fingers: e.g. moving lines 2356 this.twoFingerMove(this.touches[i], evt.pointerId, evt); 2357 2358 touchTargets[j].Xprev = pos[0]; 2359 touchTargets[j].Yprev = pos[1]; 2360 } 2361 2362 // There is only one pointer in the evt object, so there's no point in looking further 2363 break; 2364 } 2365 } 2366 } 2367 } else { 2368 if (this._getPointerInputDevice(evt) === 'touch') { 2369 this._pointerStorePosition(evt); 2370 2371 if (this._board_touches.length === 2) { 2372 evt.touches = this._board_touches; 2373 this.gestureChangeListener(evt); 2374 } 2375 } 2376 2377 // Move event without dragging an element 2378 pos = this.getMousePosition(evt); 2379 this.highlightElements(pos[0], pos[1], evt, -1); 2380 } 2381 } 2382 2383 // Hiding the infobox is commented out, since it prevents showing the infobox 2384 // on IE 11+ on 'over' 2385 //if (this.mode !== this.BOARD_MODE_DRAG) { 2386 //this.displayInfobox(false); 2387 //} 2388 this.triggerEventHandlers(['touchmove', 'move', 'pointermove', 'MSPointerMove'], [evt, this.mode]); 2389 this.updateQuality = this.BOARD_QUALITY_HIGH; 2390 2391 return this.mode === this.BOARD_MODE_NONE; 2392 }, 2393 2394 /** 2395 * Triggered as soon as the user stops touching the device with at least one finger. 2396 * @param {Event} evt 2397 * @returns {Boolean} 2398 */ 2399 pointerUpListener: function (evt) { 2400 var i, j, found, touchTargets; 2401 2402 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 2403 this.displayInfobox(false); 2404 2405 if (evt) { 2406 for (i = 0; i < this.touches.length; i++) { 2407 touchTargets = this.touches[i].targets; 2408 for (j = 0; j < touchTargets.length; j++) { 2409 if (touchTargets[j].num === evt.pointerId) { 2410 touchTargets.splice(j, 1); 2411 if (touchTargets.length === 0) { 2412 this.touches.splice(i, 1); 2413 } 2414 break; 2415 } 2416 } 2417 } 2418 } 2419 2420 this.originMoveEnd(); 2421 this.update(); 2422 2423 // selection 2424 if (this.selectingMode) { 2425 this._stopSelecting(evt); 2426 this.triggerEventHandlers(['touchstopselecting', 'pointerstopselecting', 'stopselecting'], [evt]); 2427 this.stopSelectionMode(); 2428 } else { 2429 for (i = this.downObjects.length - 1; i > -1; i--) { 2430 found = false; 2431 for (j = 0; j < this.touches.length; j++) { 2432 if (this.touches[j].obj.id === this.downObjects[i].id) { 2433 found = true; 2434 } 2435 } 2436 if (!found) { 2437 this.downObjects[i].triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 2438 // this.downObjects[i].snapToGrid(); 2439 // this.downObjects[i].snapToPoints(); 2440 this.downObjects.splice(i, 1); 2441 } 2442 } 2443 } 2444 2445 if (this.hasPointerUp) { 2446 if (window.navigator.msPointerEnabled) { // IE10- 2447 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2448 } else { 2449 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 2450 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2451 } 2452 this.hasPointerUp = false; 2453 } 2454 2455 // this.dehighlightAll(); 2456 // this.updateQuality = this.BOARD_QUALITY_HIGH; 2457 // this.mode = this.BOARD_MODE_NONE; 2458 2459 // this.originMoveEnd(); 2460 // this.update(); 2461 2462 // After one finger leaves the screen the gesture is stopped. 2463 this._pointerClearTouches(); 2464 return true; 2465 }, 2466 2467 /** 2468 * Touch-Events 2469 */ 2470 2471 /** 2472 * This method is called by the browser when a finger touches the surface of the touch-device. 2473 * @param {Event} evt The browsers event object. 2474 * @returns {Boolean} ... 2475 */ 2476 touchStartListener: function (evt) { 2477 var i, pos, elements, j, k, 2478 eps = this.options.precision.touch, 2479 obj, found, targets, 2480 evtTouches = evt[JXG.touchProperty], 2481 target, touchTargets; 2482 2483 if (!this.hasTouchEnd) { 2484 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 2485 this.hasTouchEnd = true; 2486 } 2487 2488 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 2489 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 2490 2491 // prevent accidental selection of text 2492 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2493 this.document.selection.empty(); 2494 } else if (window.getSelection) { 2495 window.getSelection().removeAllRanges(); 2496 } 2497 2498 // multitouch 2499 this._inputDevice = 'touch'; 2500 this.options.precision.hasPoint = this.options.precision.touch; 2501 2502 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 2503 // previous touches. once this is done we run through the existing touches again and watch out for free touches that can be attached to our existing 2504 // touches, e.g. we translate (parallel translation) a line with one finger, now a second finger is over this line. this should change the operation to 2505 // a rotational translation. or one finger moves a circle, a second finger can be attached to the circle: this now changes the operation from translation to 2506 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 2507 // * points have higher priority over other elements. 2508 // * if we find a targettouch over an element that could be transformed with more than one finger, we search the rest of the targettouches, if they are over 2509 // this element and add them. 2510 // ADDENDUM 11/10/11: 2511 // (1) run through the touches control object, 2512 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 2513 // for every target in our touches objects 2514 // (3) if one of the targettouches was bound to a touches targets array, mark it 2515 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 2516 // (a) if no element could be found: mark the target touches and continue 2517 // --- in the following cases, "init" means: 2518 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 2519 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 2520 // (b) if the element is a point, init 2521 // (c) if the element is a line, init and try to find a second targettouch on that line. if a second one is found, add and mark it 2522 // (d) if the element is a circle, init and try to find TWO other targettouches on that circle. if only one is found, mark it and continue. otherwise 2523 // add both to the touches array and mark them. 2524 for (i = 0; i < evtTouches.length; i++) { 2525 evtTouches[i].jxg_isused = false; 2526 } 2527 2528 for (i = 0; i < this.touches.length; i++) { 2529 touchTargets = this.touches[i].targets; 2530 for (j = 0; j < touchTargets.length; j++) { 2531 touchTargets[j].num = -1; 2532 eps = this.options.precision.touch; 2533 2534 do { 2535 for (k = 0; k < evtTouches.length; k++) { 2536 // find the new targettouches 2537 if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 2538 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) { 2539 touchTargets[j].num = k; 2540 touchTargets[j].X = evtTouches[k].screenX; 2541 touchTargets[j].Y = evtTouches[k].screenY; 2542 evtTouches[k].jxg_isused = true; 2543 break; 2544 } 2545 } 2546 2547 eps *= 2; 2548 2549 } while (touchTargets[j].num === -1 && 2550 eps < this.options.precision.touchMax); 2551 2552 if (touchTargets[j].num === -1) { 2553 JXG.debug('i couldn\'t find a targettouches for target no ' + j + ' on ' + this.touches[i].obj.name + ' (' + this.touches[i].obj.id + '). Removed the target.'); 2554 JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax); 2555 touchTargets.splice(i, 1); 2556 } 2557 2558 } 2559 } 2560 2561 // we just re-mapped the targettouches to our existing touches list. 2562 // now we have to initialize some touches from additional targettouches 2563 for (i = 0; i < evtTouches.length; i++) { 2564 if (!evtTouches[i].jxg_isused) { 2565 2566 pos = this.getMousePosition(evt, i); 2567 // selection 2568 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 2569 if (this.selectingMode) { 2570 this._startSelecting(pos); 2571 this.triggerEventHandlers(['touchstartselecting', 'startselecting'], [evt]); 2572 evt.preventDefault(); 2573 evt.stopPropagation(); 2574 this.options.precision.hasPoint = this.options.precision.mouse; 2575 return this.touches.length > 0; // don't continue as a normal click 2576 } 2577 2578 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 2579 if (elements.length !== 0) { 2580 obj = elements[elements.length - 1]; 2581 target = {num: i, 2582 X: evtTouches[i].screenX, 2583 Y: evtTouches[i].screenY, 2584 Xprev: NaN, 2585 Yprev: NaN, 2586 Xstart: [], 2587 Ystart: [], 2588 Zstart: [] 2589 }; 2590 2591 if (Type.isPoint(obj) || 2592 obj.elementClass === Const.OBJECT_CLASS_TEXT || 2593 obj.type === Const.OBJECT_TYPE_TICKS || 2594 obj.type === Const.OBJECT_TYPE_IMAGE) { 2595 // It's a point, so it's single touch, so we just push it to our touches 2596 targets = [target]; 2597 2598 // For the UNDO/REDO of object moves 2599 this.saveStartPos(obj, targets[0]); 2600 2601 this.touches.push({ obj: obj, targets: targets }); 2602 obj.highlight(true); 2603 2604 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE || 2605 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 2606 obj.elementClass === Const.OBJECT_CLASS_CURVE || 2607 obj.type === Const.OBJECT_TYPE_POLYGON) { 2608 found = false; 2609 2610 // first check if this geometric object is already captured in this.touches 2611 for (j = 0; j < this.touches.length; j++) { 2612 if (obj.id === this.touches[j].obj.id) { 2613 found = true; 2614 // only add it, if we don't have two targets in there already 2615 if (this.touches[j].targets.length === 1) { 2616 // For the UNDO/REDO of object moves 2617 this.saveStartPos(obj, target); 2618 this.touches[j].targets.push(target); 2619 } 2620 2621 evtTouches[i].jxg_isused = true; 2622 } 2623 } 2624 2625 // we couldn't find it in touches, so we just init a new touches 2626 // IF there is a second touch targetting this line, we will find it later on, and then add it to 2627 // the touches control object. 2628 if (!found) { 2629 targets = [target]; 2630 2631 // For the UNDO/REDO of object moves 2632 this.saveStartPos(obj, targets[0]); 2633 this.touches.push({ obj: obj, targets: targets }); 2634 obj.highlight(true); 2635 } 2636 } 2637 } 2638 2639 evtTouches[i].jxg_isused = true; 2640 } 2641 } 2642 2643 if (this.touches.length > 0) { 2644 evt.preventDefault(); 2645 evt.stopPropagation(); 2646 } 2647 2648 // Touch events on empty areas of the board are handled here: 2649 // 1. case: one finger. If allowed, this triggers pan with one finger 2650 if (evtTouches.length === 1 && this.mode === this.BOARD_MODE_NONE && this.touchStartMoveOriginOneFinger(evt)) { 2651 } else if (evtTouches.length === 2 && 2652 (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2653 ) { 2654 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2655 // This happens when the second finger hits the device. First, the 2656 // "one finger pan mode" has to be cancelled. 2657 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2658 this.originMoveEnd(); 2659 } 2660 this.gestureStartListener(evt); 2661 } 2662 2663 this.options.precision.hasPoint = this.options.precision.mouse; 2664 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 2665 2666 return false; 2667 //return this.touches.length > 0; 2668 }, 2669 2670 /** 2671 * Called periodically by the browser while the user moves his fingers across the device. 2672 * @param {Event} evt 2673 * @returns {Boolean} 2674 */ 2675 touchMoveListener: function (evt) { 2676 var i, pos1, pos2, 2677 touchTargets, 2678 evtTouches = evt[JXG.touchProperty]; 2679 2680 if (!this.checkFrameRate(evt)) { 2681 return false; 2682 } 2683 2684 if (this.mode !== this.BOARD_MODE_NONE) { 2685 evt.preventDefault(); 2686 evt.stopPropagation(); 2687 } 2688 2689 if (this.mode !== this.BOARD_MODE_DRAG) { 2690 this.dehighlightAll(); 2691 this.displayInfobox(false); 2692 } 2693 2694 this._inputDevice = 'touch'; 2695 this.options.precision.hasPoint = this.options.precision.touch; 2696 this.updateQuality = this.BOARD_QUALITY_LOW; 2697 2698 // selection 2699 if (this.selectingMode) { 2700 for (i = 0; i < evtTouches.length; i++) { 2701 if (!evtTouches[i].jxg_isused) { 2702 pos1 = this.getMousePosition(evt, i); 2703 this._moveSelecting(pos1); 2704 this.triggerEventHandlers(['touchmoves', 'moveselecting'], [evt, this.mode]); 2705 break; 2706 } 2707 } 2708 } else { 2709 if (!this.touchOriginMove(evt)) { 2710 if (this.mode === this.BOARD_MODE_DRAG) { 2711 // Runs over through all elements which are touched 2712 // by at least one finger. 2713 for (i = 0; i < this.touches.length; i++) { 2714 touchTargets = this.touches[i].targets; 2715 if (touchTargets.length === 1) { 2716 2717 2718 // Touch by one finger: this is possible for all elements that can be dragged 2719 if (evtTouches[touchTargets[0].num]) { 2720 pos1 = this.getMousePosition(evt, touchTargets[0].num); 2721 if (pos1[0] < 0 || pos1[0] > this.canvasWidth || 2722 pos1[1] < 0 || pos1[1] > this.canvasHeight) { 2723 return; 2724 } 2725 touchTargets[0].X = pos1[0]; 2726 touchTargets[0].Y = pos1[1]; 2727 this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch'); 2728 } 2729 2730 } else if (touchTargets.length === 2 && 2731 touchTargets[0].num > -1 && 2732 touchTargets[1].num > -1) { 2733 2734 // Touch by two fingers: moving lines, ... 2735 if (evtTouches[touchTargets[0].num] && 2736 evtTouches[touchTargets[1].num]) { 2737 2738 // Get coordinates of the two touches 2739 pos1 = this.getMousePosition(evt, touchTargets[0].num); 2740 pos2 = this.getMousePosition(evt, touchTargets[1].num); 2741 if (pos1[0] < 0 || pos1[0] > this.canvasWidth || 2742 pos1[1] < 0 || pos1[1] > this.canvasHeight || 2743 pos2[0] < 0 || pos2[0] > this.canvasWidth || 2744 pos2[1] < 0 || pos2[1] > this.canvasHeight) { 2745 return; 2746 } 2747 2748 touchTargets[0].X = pos1[0]; 2749 touchTargets[0].Y = pos1[1]; 2750 touchTargets[1].X = pos2[0]; 2751 touchTargets[1].Y = pos2[1]; 2752 2753 this.twoFingerMove(this.touches[i], touchTargets[0].num, evt); 2754 this.twoFingerMove(this.touches[i], touchTargets[1].num); 2755 2756 touchTargets[0].Xprev = pos1[0]; 2757 touchTargets[0].Yprev = pos1[1]; 2758 touchTargets[1].Xprev = pos2[0]; 2759 touchTargets[1].Yprev = pos2[1]; 2760 } 2761 } 2762 } 2763 } else { 2764 if (evtTouches.length === 2) { 2765 this.gestureChangeListener(evt); 2766 } 2767 // Move event without dragging an element 2768 pos1 = this.getMousePosition(evt, 0); 2769 this.highlightElements(pos1[0], pos1[1], evt, -1); 2770 } 2771 } 2772 } 2773 2774 if (this.mode !== this.BOARD_MODE_DRAG) { 2775 this.displayInfobox(false); 2776 } 2777 2778 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 2779 this.options.precision.hasPoint = this.options.precision.mouse; 2780 this.updateQuality = this.BOARD_QUALITY_HIGH; 2781 2782 return this.mode === this.BOARD_MODE_NONE; 2783 }, 2784 2785 /** 2786 * Triggered as soon as the user stops touching the device with at least one finger. 2787 * @param {Event} evt 2788 * @returns {Boolean} 2789 */ 2790 touchEndListener: function (evt) { 2791 var i, j, k, 2792 eps = this.options.precision.touch, 2793 tmpTouches = [], found, foundNumber, 2794 evtTouches = evt && evt[JXG.touchProperty], 2795 touchTargets; 2796 2797 this.triggerEventHandlers(['touchend', 'up'], [evt]); 2798 this.displayInfobox(false); 2799 2800 // selection 2801 if (this.selectingMode) { 2802 this._stopSelecting(evt); 2803 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 2804 this.stopSelectionMode(); 2805 } else if (evtTouches && evtTouches.length > 0) { 2806 for (i = 0; i < this.touches.length; i++) { 2807 tmpTouches[i] = this.touches[i]; 2808 } 2809 this.touches.length = 0; 2810 2811 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 2812 // convert the operation to a simple one-finger-translation. 2813 // ADDENDUM 11/10/11: 2814 // see addendum to touchStartListener from 11/10/11 2815 // (1) run through the tmptouches 2816 // (2) check the touches.obj, if it is a 2817 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 2818 // (b) line with 2819 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 2820 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 2821 // (c) circle with [proceed like in line] 2822 2823 // init the targettouches marker 2824 for (i = 0; i < evtTouches.length; i++) { 2825 evtTouches[i].jxg_isused = false; 2826 } 2827 2828 for (i = 0; i < tmpTouches.length; i++) { 2829 // could all targets of the current this.touches.obj be assigned to targettouches? 2830 found = false; 2831 foundNumber = 0; 2832 touchTargets = tmpTouches[i].targets; 2833 2834 for (j = 0; j < touchTargets.length; j++) { 2835 touchTargets[j].found = false; 2836 for (k = 0; k < evtTouches.length; k++) { 2837 if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) { 2838 touchTargets[j].found = true; 2839 touchTargets[j].num = k; 2840 touchTargets[j].X = evtTouches[k].screenX; 2841 touchTargets[j].Y = evtTouches[k].screenY; 2842 foundNumber += 1; 2843 break; 2844 } 2845 } 2846 } 2847 2848 if (Type.isPoint(tmpTouches[i].obj)) { 2849 found = (touchTargets[0] && touchTargets[0].found); 2850 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 2851 found = (touchTargets[0] && touchTargets[0].found) || (touchTargets[1] && touchTargets[1].found); 2852 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 2853 found = foundNumber === 1 || foundNumber === 3; 2854 } 2855 2856 // if we found this object to be still dragged by the user, add it back to this.touches 2857 if (found) { 2858 this.touches.push({ 2859 obj: tmpTouches[i].obj, 2860 targets: [] 2861 }); 2862 2863 for (j = 0; j < touchTargets.length; j++) { 2864 if (touchTargets[j].found) { 2865 this.touches[this.touches.length - 1].targets.push({ 2866 num: touchTargets[j].num, 2867 X: touchTargets[j].screenX, 2868 Y: touchTargets[j].screenY, 2869 Xprev: NaN, 2870 Yprev: NaN, 2871 Xstart: touchTargets[j].Xstart, 2872 Ystart: touchTargets[j].Ystart, 2873 Zstart: touchTargets[j].Zstart 2874 }); 2875 } 2876 } 2877 2878 } else { 2879 tmpTouches[i].obj.noHighlight(); 2880 } 2881 } 2882 2883 } else { 2884 this.touches.length = 0; 2885 } 2886 2887 for (i = this.downObjects.length - 1; i > -1; i--) { 2888 found = false; 2889 for (j = 0; j < this.touches.length; j++) { 2890 if (this.touches[j].obj.id === this.downObjects[i].id) { 2891 found = true; 2892 } 2893 } 2894 if (!found) { 2895 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 2896 // this.downObjects[i].snapToGrid(); 2897 // this.downObjects[i].snapToPoints(); 2898 this.downObjects.splice(i, 1); 2899 } 2900 } 2901 2902 if (!evtTouches || evtTouches.length === 0) { 2903 2904 if (this.hasTouchEnd) { 2905 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 2906 this.hasTouchEnd = false; 2907 } 2908 2909 this.dehighlightAll(); 2910 this.updateQuality = this.BOARD_QUALITY_HIGH; 2911 2912 this.originMoveEnd(); 2913 this.update(); 2914 } 2915 2916 return true; 2917 }, 2918 2919 /** 2920 * This method is called by the browser when the mouse button is clicked. 2921 * @param {Event} evt The browsers event object. 2922 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 2923 */ 2924 mouseDownListener: function (evt) { 2925 var pos, elements, result; 2926 2927 // prevent accidental selection of text 2928 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2929 this.document.selection.empty(); 2930 } else if (window.getSelection) { 2931 window.getSelection().removeAllRanges(); 2932 } 2933 2934 if (!this.hasMouseUp) { 2935 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 2936 this.hasMouseUp = true; 2937 } else { 2938 // In case this.hasMouseUp==true, it may be that there was a 2939 // mousedown event before which was not followed by an mouseup event. 2940 // This seems to happen with interactive whiteboard pens sometimes. 2941 return; 2942 } 2943 2944 this._inputDevice = 'mouse'; 2945 this.options.precision.hasPoint = this.options.precision.mouse; 2946 pos = this.getMousePosition(evt); 2947 2948 // selection 2949 this._testForSelection(evt); 2950 if (this.selectingMode) { 2951 this._startSelecting(pos); 2952 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 2953 return; // don't continue as a normal click 2954 } 2955 2956 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 2957 2958 // if no draggable object can be found, get out here immediately 2959 if (elements.length === 0) { 2960 this.mode = this.BOARD_MODE_NONE; 2961 result = true; 2962 } else { 2963 /** @ignore */ 2964 this.mouse = { 2965 obj: null, 2966 targets: [{ 2967 X: pos[0], 2968 Y: pos[1], 2969 Xprev: NaN, 2970 Yprev: NaN 2971 }] 2972 }; 2973 this.mouse.obj = elements[elements.length - 1]; 2974 2975 this.dehighlightAll(); 2976 this.mouse.obj.highlight(true); 2977 2978 this.mouse.targets[0].Xstart = []; 2979 this.mouse.targets[0].Ystart = []; 2980 this.mouse.targets[0].Zstart = []; 2981 2982 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 2983 2984 // prevent accidental text selection 2985 // this could get us new trouble: input fields, links and drop down boxes placed as text 2986 // on the board don't work anymore. 2987 if (evt && evt.preventDefault) { 2988 evt.preventDefault(); 2989 } else if (window.event) { 2990 window.event.returnValue = false; 2991 } 2992 } 2993 2994 if (this.mode === this.BOARD_MODE_NONE) { 2995 result = this.mouseOriginMoveStart(evt); 2996 } 2997 2998 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 2999 3000 return result; 3001 }, 3002 3003 /** 3004 * This method is called by the browser when the mouse is moved. 3005 * @param {Event} evt The browsers event object. 3006 */ 3007 mouseMoveListener: function (evt) { 3008 var pos; 3009 3010 if (!this.checkFrameRate(evt)) { 3011 return false; 3012 } 3013 3014 pos = this.getMousePosition(evt); 3015 3016 this.updateQuality = this.BOARD_QUALITY_LOW; 3017 3018 if (this.mode !== this.BOARD_MODE_DRAG) { 3019 this.dehighlightAll(); 3020 this.displayInfobox(false); 3021 } 3022 3023 // we have to check for four cases: 3024 // * user moves origin 3025 // * user drags an object 3026 // * user just moves the mouse, here highlight all elements at 3027 // the current mouse position 3028 // * the user is selecting 3029 3030 // selection 3031 if (this.selectingMode) { 3032 this._moveSelecting(pos); 3033 this.triggerEventHandlers(['mousemoveselecting', 'moveselecting'], [evt, this.mode]); 3034 } else if (!this.mouseOriginMove(evt)) { 3035 if (this.mode === this.BOARD_MODE_DRAG) { 3036 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 3037 } else { // BOARD_MODE_NONE 3038 // Move event without dragging an element 3039 this.highlightElements(pos[0], pos[1], evt, -1); 3040 } 3041 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 3042 } 3043 this.updateQuality = this.BOARD_QUALITY_HIGH; 3044 }, 3045 3046 /** 3047 * This method is called by the browser when the mouse button is released. 3048 * @param {Event} evt 3049 */ 3050 mouseUpListener: function (evt) { 3051 var i; 3052 3053 if (this.selectingMode === false) { 3054 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 3055 } 3056 3057 // redraw with high precision 3058 this.updateQuality = this.BOARD_QUALITY_HIGH; 3059 3060 // if (this.mouse && this.mouse.obj) { 3061 // // The parameter is needed for lines with snapToGrid enabled 3062 // this.mouse.obj.snapToGrid(this.mouse.targets[0]); 3063 // this.mouse.obj.snapToPoints(); 3064 // } 3065 3066 this.originMoveEnd(); 3067 this.dehighlightAll(); 3068 this.update(); 3069 3070 // selection 3071 if (this.selectingMode) { 3072 this._stopSelecting(evt); 3073 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 3074 this.stopSelectionMode(); 3075 } else { 3076 for (i = 0; i < this.downObjects.length; i++) { 3077 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 3078 } 3079 } 3080 3081 this.downObjects.length = 0; 3082 3083 if (this.hasMouseUp) { 3084 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 3085 this.hasMouseUp = false; 3086 } 3087 3088 // release dragged mouse object 3089 /** @ignore */ 3090 this.mouse = null; 3091 }, 3092 3093 /** 3094 * Handler for mouse wheel events. Used to zoom in and out of the board. 3095 * @param {Event} evt 3096 * @returns {Boolean} 3097 */ 3098 mouseWheelListener: function (evt) { 3099 if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) { 3100 return true; 3101 } 3102 3103 evt = evt || window.event; 3104 var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40, 3105 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this); 3106 3107 if (wd > 0) { 3108 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]); 3109 } else { 3110 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]); 3111 } 3112 3113 this.triggerEventHandlers(['mousewheel'], [evt]); 3114 3115 evt.preventDefault(); 3116 return false; 3117 }, 3118 3119 /** 3120 * Allow moving of JSXGraph elements with arrow keys 3121 * and zooming of the construction with + / -. 3122 * Panning of the construction is done with arrow keys 3123 * if the pan key (shift or ctrl) is pressed. 3124 * The selection of the element is done with the tab key. 3125 * 3126 * @param {Event} evt The browser's event object 3127 * 3128 * @see JXG.Board#keyboard 3129 * @see JXG.Board#keyFocusInListener 3130 * @see JXG.Board#keyFocusOutListener 3131 * 3132 */ 3133 keyDownListener: function (evt) { 3134 var id_node = evt.target.id, 3135 id, el, res, 3136 sX = 0, 3137 sY = 0, 3138 // dx, dy are provided in screen units and 3139 // are converted to user coordinates 3140 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX, 3141 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY, 3142 doZoom = false, 3143 done = true, 3144 dir, actPos; 3145 3146 if (!this.attr.keyboard.enabled || id_node === '') { 3147 return false; 3148 } 3149 3150 // Get the JSXGraph id from the id of the SVG node. 3151 id = id_node.replace(this.containerObj.id + '_', ''); 3152 el = this.select(id); 3153 3154 if (Type.exists(el.coords)) { 3155 actPos = el.coords.usrCoords.slice(1); 3156 } 3157 3158 if (Type.evaluate(this.attr.keyboard.panshift) || Type.evaluate(this.attr.keyboard.panctrl)) { 3159 doZoom = true; 3160 } 3161 3162 if ((Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || 3163 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey)) { 3164 if (evt.keyCode === 38) { // up 3165 this.clickUpArrow(); 3166 } else if (evt.keyCode === 40) { // down 3167 this.clickDownArrow(); 3168 } else if (evt.keyCode === 37) { // left 3169 this.clickLeftArrow(); 3170 } else if (evt.keyCode === 39) { // right 3171 this.clickRightArrow(); 3172 } else { 3173 done = false; 3174 } 3175 } else { 3176 // Adapt dx, dy to snapToGrid and attractToGrid 3177 // snapToGrid has priority. 3178 if (Type.exists(el.visProp)) { 3179 if (Type.exists(el.visProp.snaptogrid) && 3180 el.visProp.snaptogrid && 3181 Type.evaluate(el.visProp.snapsizex) && 3182 Type.evaluate(el.visProp.snapsizey)) { 3183 3184 // Adapt dx, dy such that snapToGrid is possible 3185 res = el.getSnapSizes(); 3186 sX = res[0]; 3187 sY = res[1]; 3188 dx = Math.max(sX, dx); 3189 dy = Math.max(sY, dy); 3190 3191 } else if (Type.exists(el.visProp.attracttogrid) && 3192 el.visProp.attracttogrid && 3193 Type.evaluate(el.visProp.attractordistance) && 3194 Type.evaluate(el.visProp.attractorunit)) { 3195 3196 // Adapt dx, dy such that attractToGrid is possible 3197 sX = 1.1 * Type.evaluate(el.visProp.attractordistance); 3198 sY = sX; 3199 3200 if (Type.evaluate(el.visProp.attractorunit) === 'screen') { 3201 sX /= this.unitX; 3202 sY /= this.unitX; 3203 } 3204 dx = Math.max(sX, dx); 3205 dy = Math.max(sY, dy); 3206 } 3207 3208 } 3209 3210 if (evt.keyCode === 38) { // up 3211 dir = [0, dy]; 3212 } else if (evt.keyCode === 40) { // down 3213 dir = [0, -dy]; 3214 } else if (evt.keyCode === 37) { // left 3215 dir = [-dx, 0]; 3216 } else if (evt.keyCode === 39) { // right 3217 dir = [dx, 0]; 3218 // } else if (evt.keyCode === 9) { // tab 3219 3220 } else if (doZoom && evt.key === '+') { // + 3221 this.zoomIn(); 3222 } else if (doZoom && evt.key === '-') { // - 3223 this.zoomOut(); 3224 } else if (doZoom && evt.key === 'o') { // o 3225 this.zoom100(); 3226 } else { 3227 done = false; 3228 } 3229 3230 if (dir && el.isDraggable && 3231 el.visPropCalc.visible && 3232 ((this.geonextCompatibilityMode && 3233 (Type.isPoint(el) || 3234 el.elementClass === Const.OBJECT_CLASS_TEXT) 3235 ) || !this.geonextCompatibilityMode) && 3236 !Type.evaluate(el.visProp.fixed) 3237 ) { 3238 3239 if (Type.exists(el.coords)) { 3240 dir[0] += actPos[0]; 3241 dir[1] += actPos[1]; 3242 } 3243 // For coordsElement setPosition has to call setPositionDirectly. 3244 // Otherwise the position is set by a translation. 3245 el.setPosition(JXG.COORDS_BY_USER, dir); 3246 if (Type.exists(el.coords)) { 3247 this.updateInfobox(el); 3248 } 3249 this.triggerEventHandlers(['hit'], [evt, el]); 3250 } 3251 } 3252 3253 this.update(); 3254 3255 if (done && Type.exists(evt.preventDefault)) { 3256 evt.preventDefault(); 3257 } 3258 return true; 3259 }, 3260 3261 /** 3262 * Event listener for SVG elements getting focus. 3263 * This is needed for highlighting when using keyboard control. 3264 * 3265 * @see JXG.Board#keyFocusOutListener 3266 * @see JXG.Board#keyDownListener 3267 * @see JXG.Board#keyboard 3268 * 3269 * @param {Event} evt The browser's event object 3270 */ 3271 keyFocusInListener: function (evt) { 3272 var id_node = evt.target.id, 3273 id, el; 3274 3275 if (!this.attr.keyboard.enabled || id_node === '') { 3276 return false; 3277 } 3278 3279 id = id_node.replace(this.containerObj.id + '_', ''); 3280 el = this.select(id); 3281 if (Type.exists(el.highlight)) { 3282 el.highlight(true); 3283 } 3284 if (Type.exists(el.coords)) { 3285 this.updateInfobox(el); 3286 } 3287 this.triggerEventHandlers(['hit'], [evt, el]); 3288 }, 3289 3290 /** 3291 * Event listener for SVG elements losing focus. 3292 * This is needed for dehighlighting when using keyboard control. 3293 * 3294 * @see JXG.Board#keyFocusInListener 3295 * @see JXG.Board#keyDownListener 3296 * @see JXG.Board#keyboard 3297 * 3298 * @param {Event} evt The browser's event object 3299 */ 3300 keyFocusOutListener: function (evt) { 3301 if (!this.attr.keyboard.enabled) { 3302 return false; 3303 } 3304 // var id_node = evt.target.id, 3305 // id, el; 3306 3307 // id = id_node.replace(this.containerObj.id + '_', ''); 3308 // el = this.select(id); 3309 this.dehighlightAll(); 3310 this.displayInfobox(false); 3311 }, 3312 3313 /** 3314 * Update the width and height of the JSXGraph container div element. 3315 * Read actual values with getBoundingClientRect(), 3316 * and call board.resizeContainer() with this values. 3317 * <p> 3318 * If necessary, also call setBoundingBox(). 3319 * 3320 * @see JXG.Board#startResizeObserver 3321 * @see JXG.Board#resizeListener 3322 * @see JXG.Board#resizeContainer 3323 * @see JXG.Board#setBoundingBox 3324 * 3325 */ 3326 updateContainerDims: function() { 3327 var w, h, 3328 bb, css; 3329 3330 // Get size of the board's container div 3331 bb = this.containerObj.getBoundingClientRect(); 3332 w = bb.width; 3333 h = bb.height; 3334 3335 // Subtract the border size 3336 if (window && window.getComputedStyle) { 3337 css = window.getComputedStyle(this.containerObj, null); 3338 w -= parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width')); 3339 h -= parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width')); 3340 } 3341 3342 // If div is invisible - do nothing 3343 if (w <= 0 || h <= 0) { 3344 return; 3345 } 3346 3347 // If bounding box is not yet initialized, do it now. 3348 if (isNaN(this.getBoundingBox()[0])) { 3349 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep'); 3350 } 3351 3352 // Do nothing if the dimension did not change since being visible 3353 // the last time. Note that if the div had display:none in the mean time, 3354 // we did not store this._prevDim. 3355 if (Type.exists(this._prevDim) && 3356 this._prevDim.w === w && this._prevDim.h === h) { 3357 return; 3358 } 3359 3360 // Set the size of the SVG or canvas element 3361 this.resizeContainer(w, h, true); 3362 this._prevDim = { 3363 w: w, 3364 h: h 3365 }; 3366 }, 3367 3368 /** 3369 * Start observer which reacts to size changes of the JSXGraph 3370 * container div element. Calls updateContainerDims(). 3371 * If not available, an event listener for the window-resize event is started. 3372 * On mobile devices also scrolling might trigger resizes. 3373 * However, resize events triggered by scrolling events should be ignored. 3374 * Therefore, also a scrollListener is started. 3375 * Resize can be controlled with the board attribute resize. 3376 * 3377 * @see JXG.Board#updateContainerDims 3378 * @see JXG.Board#resizeListener 3379 * @see JXG.Board#scrollListener 3380 * @see JXG.Board#resize 3381 * 3382 */ 3383 startResizeObserver: function() { 3384 var that = this; 3385 3386 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 3387 return; 3388 } 3389 3390 this.resizeObserver = new ResizeObserver(function(entries) { 3391 if (!that._isResizing) { 3392 that._isResizing = true; 3393 window.setTimeout(function() { 3394 try { 3395 that.updateContainerDims(); 3396 } catch (err) { 3397 that.stopResizeObserver(); 3398 } finally { 3399 that._isResizing = false; 3400 } 3401 }, that.attr.resize.throttle); 3402 } 3403 }); 3404 this.resizeObserver.observe(this.containerObj); 3405 }, 3406 3407 /** 3408 * Stops the resize observer. 3409 * @see JXG.Board#startResizeObserver 3410 * 3411 */ 3412 stopResizeObserver: function() { 3413 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 3414 return; 3415 } 3416 3417 if (Type.exists(this.resizeObserver)) { 3418 this.resizeObserver.unobserve(this.containerObj); 3419 } 3420 }, 3421 3422 /** 3423 * Fallback solutions if there is no resizeObserver available in the browser. 3424 * Reacts to resize events of the window (only). Otherwise similar to 3425 * startResizeObserver(). To handle changes of the visibility 3426 * of the JSXGraph container element, additionally an intersection observer is used. 3427 * which watches changes in the visibility of the JSXGraph container element. 3428 * This is necessary e.g. for register tabs or dia shows. 3429 * 3430 * @see JXG.Board#startResizeObserver 3431 * @see JXG.Board#startIntersectionObserver 3432 */ 3433 resizeListener: function() { 3434 var that = this; 3435 3436 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 3437 return; 3438 } 3439 if (!this._isScrolling && !this._isResizing) { 3440 this._isResizing = true; 3441 window.setTimeout(function() { 3442 that.updateContainerDims(); 3443 that._isResizing = false; 3444 }, this.attr.resize.throttle); 3445 } 3446 }, 3447 3448 /** 3449 * Listener to watch for scroll events. Sets board._isScrolling = true 3450 * @param {Event} evt The browser's event object 3451 * 3452 * @see JXG.Board#startResizeObserver 3453 * @see JXG.Board#resizeListener 3454 * 3455 */ 3456 scrollListener: function(evt) { 3457 var that = this; 3458 3459 if (!Env.isBrowser) { 3460 return; 3461 } 3462 if (!this._isScrolling) { 3463 this._isScrolling = true; 3464 window.setTimeout(function() { 3465 that._isScrolling = false; 3466 }, 66); 3467 } 3468 }, 3469 3470 /** 3471 * Watch for changes of the visibility of the JSXGraph container element. 3472 * 3473 * @see JXG.Board#startResizeObserver 3474 * @see JXG.Board#resizeListener 3475 * 3476 */ 3477 startIntersectionObserver: function() { 3478 var that = this, 3479 options = { 3480 root: null, 3481 rootMargin: '0px', 3482 threshold: 0.8 3483 }; 3484 3485 try { 3486 this.intersectionObserver = new IntersectionObserver(function(entries) { 3487 // If bounding box is not yet initialized, do it now. 3488 if (isNaN(that.getBoundingBox()[0])) { 3489 that.updateContainerDims(); 3490 } 3491 }, options); 3492 this.intersectionObserver.observe(that.containerObj); 3493 } catch (err) { 3494 console.log('JSXGraph: IntersectionObserver not available in this browser.'); 3495 } 3496 }, 3497 3498 /** 3499 * Stop the intersection observer 3500 * 3501 * @see JXG.Board#startIntersectionObserver 3502 * 3503 */ 3504 stopIntersectionObserver: function() { 3505 if (Type.exists(this.intersectionObserver)) { 3506 this.intersectionObserver.unobserve(this.containerObj); 3507 } 3508 }, 3509 3510 /********************************************************** 3511 * 3512 * End of Event Handlers 3513 * 3514 **********************************************************/ 3515 3516 /** 3517 * Initialize the info box object which is used to display 3518 * the coordinates of points near the mouse pointer, 3519 * @returns {JXG.Board} Reference to the board 3520 */ 3521 initInfobox: function () { 3522 var attr = Type.copyAttributes({}, this.options, 'infobox'); 3523 3524 attr.id = this.id + '_infobox'; 3525 /** 3526 * Infobox close to points in which the points' coordinates are displayed. 3527 * This is simply a JXG.Text element. Access through board.infobox. 3528 * Uses CSS class .JXGinfobox. 3529 * @type JXG.Text 3530 * 3531 */ 3532 this.infobox = this.create('text', [0, 0, '0,0'], attr); 3533 3534 this.infobox.distanceX = -20; 3535 this.infobox.distanceY = 25; 3536 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 3537 3538 this.infobox.dump = false; 3539 3540 this.displayInfobox(false); 3541 return this; 3542 }, 3543 3544 /** 3545 * Updates and displays a little info box to show coordinates of current selected points. 3546 * @param {JXG.GeometryElement} el A GeometryElement 3547 * @returns {JXG.Board} Reference to the board 3548 * @see JXG.Board#displayInfobox 3549 * @see JXG.Board#showInfobox 3550 * @see Point#showInfobox 3551 * 3552 */ 3553 updateInfobox: function (el) { 3554 var x, y, xc, yc, 3555 vpinfoboxdigits, 3556 vpsi = Type.evaluate(el.visProp.showinfobox); 3557 3558 if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || 3559 !vpsi) { 3560 return this; 3561 } 3562 3563 if (Type.isPoint(el)) { 3564 xc = el.coords.usrCoords[1]; 3565 yc = el.coords.usrCoords[2]; 3566 3567 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits); 3568 this.infobox.setCoords(xc + this.infobox.distanceX / this.unitX, 3569 yc + this.infobox.distanceY / this.unitY); 3570 3571 if (typeof el.infoboxText !== 'string') { 3572 if (vpinfoboxdigits === 'auto') { 3573 x = Type.autoDigits(xc); 3574 y = Type.autoDigits(yc); 3575 } else if (Type.isNumber(vpinfoboxdigits)) { 3576 x = Type.toFixed(xc, vpinfoboxdigits); 3577 y = Type.toFixed(yc, vpinfoboxdigits); 3578 } else { 3579 x = xc; 3580 y = yc; 3581 } 3582 3583 this.highlightInfobox(x, y, el); 3584 } else { 3585 this.highlightCustomInfobox(el.infoboxText, el); 3586 } 3587 3588 this.displayInfobox(true); 3589 } 3590 return this; 3591 }, 3592 3593 /** 3594 * Set infobox visible / invisible. 3595 * 3596 * It uses its property hiddenByParent to memorize its status. 3597 * In this way, many DOM access can be avoided. 3598 * 3599 * @param {Boolean} val true for visible, false for invisible 3600 * @returns {JXG.Board} Reference to the board. 3601 * @see JXG.Board#updateInfobox 3602 * 3603 */ 3604 displayInfobox: function(val) { 3605 if (this.infobox.hiddenByParent === val) { 3606 this.infobox.hiddenByParent = !val; 3607 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 3608 } 3609 return this; 3610 }, 3611 3612 // Alias for displayInfobox to be backwards compatible. 3613 // The method showInfobox clashes with the board attribute showInfobox 3614 showInfobox: function(val) { 3615 return this.displayInfobox(val); 3616 }, 3617 3618 /** 3619 * Changes the text of the info box to show the given coordinates. 3620 * @param {Number} x 3621 * @param {Number} y 3622 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 3623 * @returns {JXG.Board} Reference to the board. 3624 */ 3625 highlightInfobox: function (x, y, el) { 3626 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 3627 return this; 3628 }, 3629 3630 /** 3631 * Changes the text of the info box to what is provided via text. 3632 * @param {String} text 3633 * @param {JXG.GeometryElement} [el] 3634 * @returns {JXG.Board} Reference to the board. 3635 */ 3636 highlightCustomInfobox: function (text, el) { 3637 this.infobox.setText(text); 3638 return this; 3639 }, 3640 3641 /** 3642 * Remove highlighting of all elements. 3643 * @returns {JXG.Board} Reference to the board. 3644 */ 3645 dehighlightAll: function () { 3646 var el, pEl, needsDehighlight = false; 3647 3648 for (el in this.highlightedObjects) { 3649 if (this.highlightedObjects.hasOwnProperty(el)) { 3650 pEl = this.highlightedObjects[el]; 3651 3652 if (this.hasMouseHandlers || this.hasPointerHandlers) { 3653 pEl.noHighlight(); 3654 } 3655 3656 needsDehighlight = true; 3657 3658 // In highlightedObjects should only be objects which fulfill all these conditions 3659 // And in case of complex elements, like a turtle based fractal, it should be faster to 3660 // just de-highlight the element instead of checking hasPoint... 3661 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 3662 } 3663 } 3664 3665 this.highlightedObjects = {}; 3666 3667 // We do not need to redraw during dehighlighting in CanvasRenderer 3668 // because we are redrawing anyhow 3669 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 3670 // another object is highlighted. 3671 if (this.renderer.type === 'canvas' && needsDehighlight) { 3672 this.prepareUpdate(); 3673 this.renderer.suspendRedraw(this); 3674 this.updateRenderer(); 3675 this.renderer.unsuspendRedraw(); 3676 } 3677 3678 return this; 3679 }, 3680 3681 /** 3682 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 3683 * once. 3684 * @private 3685 * @param {Number} x X coordinate in screen coordinates 3686 * @param {Number} y Y coordinate in screen coordinates 3687 * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates. 3688 * @see JXG.Board#getUsrCoordsOfMouse 3689 */ 3690 getScrCoordsOfMouse: function (x, y) { 3691 return [x, y]; 3692 }, 3693 3694 /** 3695 * This method calculates the user coords of the current mouse coordinates. 3696 * @param {Event} evt Event object containing the mouse coordinates. 3697 * @returns {Array} Coordinates [x, y] of the mouse in user coordinates. 3698 * @example 3699 * board.on('up', function (evt) { 3700 * var a = board.getUsrCoordsOfMouse(evt), 3701 * x = a[0], 3702 * y = a[1], 3703 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 3704 * // Shorter version: 3705 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 3706 * }); 3707 * 3708 * </pre><div id="JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746" class="jxgbox" style="width: 300px; height: 300px;"></div> 3709 * <script type="text/javascript"> 3710 * (function() { 3711 * var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746', 3712 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 3713 * board.on('up', function (evt) { 3714 * var a = board.getUsrCoordsOfMouse(evt), 3715 * x = a[0], 3716 * y = a[1], 3717 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 3718 * // Shorter version: 3719 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 3720 * }); 3721 * 3722 * })(); 3723 * 3724 * </script><pre> 3725 * 3726 * @see JXG.Board#getScrCoordsOfMouse 3727 * @see JXG.Board#getAllUnderMouse 3728 */ 3729 getUsrCoordsOfMouse: function (evt) { 3730 var cPos = this.getCoordsTopLeftCorner(), 3731 absPos = Env.getPosition(evt, null, this.document), 3732 x = absPos[0] - cPos[0], 3733 y = absPos[1] - cPos[1], 3734 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 3735 3736 return newCoords.usrCoords.slice(1); 3737 }, 3738 3739 /** 3740 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 3741 * @param {Event} evt Event object containing the mouse coordinates. 3742 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 3743 * @see JXG.Board#getUsrCoordsOfMouse 3744 * @see JXG.Board#getAllObjectsUnderMouse 3745 */ 3746 getAllUnderMouse: function (evt) { 3747 var elList = this.getAllObjectsUnderMouse(evt); 3748 elList.push(this.getUsrCoordsOfMouse(evt)); 3749 3750 return elList; 3751 }, 3752 3753 /** 3754 * Collects all elements under current mouse position. 3755 * @param {Event} evt Event object containing the mouse coordinates. 3756 * @returns {Array} Array of elements at the current mouse position. 3757 * @see JXG.Board#getAllUnderMouse 3758 */ 3759 getAllObjectsUnderMouse: function (evt) { 3760 var cPos = this.getCoordsTopLeftCorner(), 3761 absPos = Env.getPosition(evt, null, this.document), 3762 dx = absPos[0] - cPos[0], 3763 dy = absPos[1] - cPos[1], 3764 elList = [], 3765 el, 3766 pEl, 3767 len = this.objectsList.length; 3768 3769 for (el = 0; el < len; el++) { 3770 pEl = this.objectsList[el]; 3771 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 3772 elList[elList.length] = pEl; 3773 } 3774 } 3775 3776 return elList; 3777 }, 3778 3779 /** 3780 * Update the coords object of all elements which possess this 3781 * property. This is necessary after changing the viewport. 3782 * @returns {JXG.Board} Reference to this board. 3783 **/ 3784 updateCoords: function () { 3785 var el, ob, len = this.objectsList.length; 3786 3787 for (ob = 0; ob < len; ob++) { 3788 el = this.objectsList[ob]; 3789 3790 if (Type.exists(el.coords)) { 3791 if (Type.evaluate(el.visProp.frozen)) { 3792 el.coords.screen2usr(); 3793 } else { 3794 el.coords.usr2screen(); 3795 } 3796 } 3797 } 3798 return this; 3799 }, 3800 3801 /** 3802 * Moves the origin and initializes an update of all elements. 3803 * @param {Number} x 3804 * @param {Number} y 3805 * @param {Boolean} [diff=false] 3806 * @returns {JXG.Board} Reference to this board. 3807 */ 3808 moveOrigin: function (x, y, diff) { 3809 var ox, oy, ul, lr; 3810 if (Type.exists(x) && Type.exists(y)) { 3811 ox = this.origin.scrCoords[1]; 3812 oy = this.origin.scrCoords[2]; 3813 3814 this.origin.scrCoords[1] = x; 3815 this.origin.scrCoords[2] = y; 3816 3817 if (diff) { 3818 this.origin.scrCoords[1] -= this.drag_dx; 3819 this.origin.scrCoords[2] -= this.drag_dy; 3820 } 3821 3822 ul = (new Coords(Const.COORDS_BY_SCREEN, [0, 0], this)).usrCoords; 3823 lr = (new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this)).usrCoords; 3824 if (ul[1] < this.maxboundingbox[0] || 3825 ul[2] > this.maxboundingbox[1] || 3826 lr[1] > this.maxboundingbox[2] || 3827 lr[2] < this.maxboundingbox[3]) { 3828 3829 this.origin.scrCoords[1] = ox; 3830 this.origin.scrCoords[2] = oy; 3831 } 3832 } 3833 3834 this.updateCoords().clearTraces().fullUpdate(); 3835 this.triggerEventHandlers(['boundingbox']); 3836 3837 return this; 3838 }, 3839 3840 /** 3841 * Add conditional updates to the elements. 3842 * @param {String} str String containing coniditional update in geonext syntax 3843 */ 3844 addConditions: function (str) { 3845 var term, m, left, right, name, el, property, 3846 functions = [], 3847 // plaintext = 'var el, x, y, c, rgbo;\n', 3848 i = str.indexOf('<data>'), 3849 j = str.indexOf('<' + '/data>'), 3850 3851 xyFun = function (board, el, f, what) { 3852 return function () { 3853 var e, t; 3854 3855 e = board.select(el.id); 3856 t = e.coords.usrCoords[what]; 3857 3858 if (what === 2) { 3859 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 3860 } else { 3861 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 3862 } 3863 e.prepareUpdate().update(); 3864 }; 3865 }, 3866 3867 visFun = function (board, el, f) { 3868 return function () { 3869 var e, v; 3870 3871 e = board.select(el.id); 3872 v = f(); 3873 3874 e.setAttribute({visible: v}); 3875 }; 3876 }, 3877 3878 colFun = function (board, el, f, what) { 3879 return function () { 3880 var e, v; 3881 3882 e = board.select(el.id); 3883 v = f(); 3884 3885 if (what === 'strokewidth') { 3886 e.visProp.strokewidth = v; 3887 } else { 3888 v = Color.rgba2rgbo(v); 3889 e.visProp[what + 'color'] = v[0]; 3890 e.visProp[what + 'opacity'] = v[1]; 3891 } 3892 }; 3893 }, 3894 3895 posFun = function (board, el, f) { 3896 return function () { 3897 var e = board.select(el.id); 3898 3899 e.position = f(); 3900 }; 3901 }, 3902 3903 styleFun = function (board, el, f) { 3904 return function () { 3905 var e = board.select(el.id); 3906 3907 e.setStyle(f()); 3908 }; 3909 }; 3910 3911 if (i < 0) { 3912 return; 3913 } 3914 3915 while (i >= 0) { 3916 term = str.slice(i + 6, j); // throw away <data> 3917 m = term.indexOf('='); 3918 left = term.slice(0, m); 3919 right = term.slice(m + 1); 3920 m = left.indexOf('.'); // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt." 3921 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 3922 el = this.elementsByName[Type.unescapeHTML(name)]; 3923 3924 property = left.slice(m + 1).replace(/\s+/g, '').toLowerCase(); // remove whitespace in property 3925 right = Type.createFunction (right, this, '', true); 3926 3927 // Debug 3928 if (!Type.exists(this.elementsByName[name])) { 3929 JXG.debug("debug conditions: |" + name + "| undefined"); 3930 } else { 3931 // plaintext += "el = this.objects[\"" + el.id + "\"];\n"; 3932 3933 switch (property) { 3934 case 'x': 3935 functions.push(xyFun(this, el, right, 2)); 3936 break; 3937 case 'y': 3938 functions.push(xyFun(this, el, right, 1)); 3939 break; 3940 case 'visible': 3941 functions.push(visFun(this, el, right)); 3942 break; 3943 case 'position': 3944 functions.push(posFun(this, el, right)); 3945 break; 3946 case 'stroke': 3947 functions.push(colFun(this, el, right, 'stroke')); 3948 break; 3949 case 'style': 3950 functions.push(styleFun(this, el, right)); 3951 break; 3952 case 'strokewidth': 3953 functions.push(colFun(this, el, right, 'strokewidth')); 3954 break; 3955 case 'fill': 3956 functions.push(colFun(this, el, right, 'fill')); 3957 break; 3958 case 'label': 3959 break; 3960 default: 3961 JXG.debug("property '" + property + "' in conditions not yet implemented:" + right); 3962 break; 3963 } 3964 } 3965 str = str.slice(j + 7); // cut off "</data>" 3966 i = str.indexOf('<data>'); 3967 j = str.indexOf('<' + '/data>'); 3968 } 3969 3970 this.updateConditions = function () { 3971 var i; 3972 3973 for (i = 0; i < functions.length; i++) { 3974 functions[i](); 3975 } 3976 3977 this.prepareUpdate().updateElements(); 3978 return true; 3979 }; 3980 this.updateConditions(); 3981 }, 3982 3983 /** 3984 * Computes the commands in the conditions-section of the gxt file. 3985 * It is evaluated after an update, before the unsuspendRedraw. 3986 * The function is generated in 3987 * @see JXG.Board#addConditions 3988 * @private 3989 */ 3990 updateConditions: function () { 3991 return false; 3992 }, 3993 3994 /** 3995 * Calculates adequate snap sizes. 3996 * @returns {JXG.Board} Reference to the board. 3997 */ 3998 calculateSnapSizes: function () { 3999 var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this), 4000 p2 = new Coords(Const.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this), 4001 x = p1.scrCoords[1] - p2.scrCoords[1], 4002 y = p1.scrCoords[2] - p2.scrCoords[2]; 4003 4004 this.options.grid.snapSizeX = this.options.grid.gridX; 4005 while (Math.abs(x) > 25) { 4006 this.options.grid.snapSizeX *= 2; 4007 x /= 2; 4008 } 4009 4010 this.options.grid.snapSizeY = this.options.grid.gridY; 4011 while (Math.abs(y) > 25) { 4012 this.options.grid.snapSizeY *= 2; 4013 y /= 2; 4014 } 4015 4016 return this; 4017 }, 4018 4019 /** 4020 * Apply update on all objects with the new zoom-factors. Clears all traces. 4021 * @returns {JXG.Board} Reference to the board. 4022 */ 4023 applyZoom: function () { 4024 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 4025 4026 return this; 4027 }, 4028 4029 /** 4030 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 4031 * The zoom operation is centered at x, y. 4032 * @param {Number} [x] 4033 * @param {Number} [y] 4034 * @returns {JXG.Board} Reference to the board 4035 */ 4036 zoomIn: function (x, y) { 4037 var bb = this.getBoundingBox(), 4038 zX = this.attr.zoom.factorx, 4039 zY = this.attr.zoom.factory, 4040 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 4041 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 4042 lr = 0.5, 4043 tr = 0.5, 4044 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 4045 4046 if ((this.zoomX > this.attr.zoom.max && zX > 1.0) || 4047 (this.zoomY > this.attr.zoom.max && zY > 1.0) || 4048 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 4049 (this.zoomY < mi && zY < 1.0)) { 4050 return this; 4051 } 4052 4053 if (Type.isNumber(x) && Type.isNumber(y)) { 4054 lr = (x - bb[0]) / (bb[2] - bb[0]); 4055 tr = (bb[1] - y) / (bb[1] - bb[3]); 4056 } 4057 4058 this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update'); 4059 return this.applyZoom(); 4060 }, 4061 4062 /** 4063 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 4064 * The zoom operation is centered at x, y. 4065 * 4066 * @param {Number} [x] 4067 * @param {Number} [y] 4068 * @returns {JXG.Board} Reference to the board 4069 */ 4070 zoomOut: function (x, y) { 4071 var bb = this.getBoundingBox(), 4072 zX = this.attr.zoom.factorx, 4073 zY = this.attr.zoom.factory, 4074 dX = (bb[2] - bb[0]) * (1.0 - zX), 4075 dY = (bb[1] - bb[3]) * (1.0 - zY), 4076 lr = 0.5, 4077 tr = 0.5, 4078 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 4079 4080 if (this.zoomX < mi || this.zoomY < mi) { 4081 return this; 4082 } 4083 4084 if (Type.isNumber(x) && Type.isNumber(y)) { 4085 lr = (x - bb[0]) / (bb[2] - bb[0]); 4086 tr = (bb[1] - y) / (bb[1] - bb[3]); 4087 } 4088 4089 this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update'); 4090 4091 return this.applyZoom(); 4092 }, 4093 4094 /** 4095 * Reset the zoom level to the original zoom level from initBoard(); 4096 * Additionally, if the board as been initialized with a boundingBox (which is the default), 4097 * restore the viewport to the original viewport during initialization. Otherwise, 4098 * (i.e. if the board as been initialized with unitX/Y and originX/Y), 4099 * just set the zoom level to 100%. 4100 * 4101 * @returns {JXG.Board} Reference to the board 4102 */ 4103 zoom100: function () { 4104 var bb, dX, dY; 4105 4106 if (Type.exists(this.attr.boundingbox)) { 4107 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset'); 4108 } else { 4109 // Board has been set up with unitX/Y and originX/Y 4110 bb = this.getBoundingBox(); 4111 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5; 4112 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 4113 this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], this.keepaspectratio, 'reset'); 4114 } 4115 return this.applyZoom(); 4116 }, 4117 4118 /** 4119 * Zooms the board so every visible point is shown. Keeps aspect ratio. 4120 * @returns {JXG.Board} Reference to the board 4121 */ 4122 zoomAllPoints: function () { 4123 var el, border, borderX, borderY, pEl, 4124 minX = 0, 4125 maxX = 0, 4126 minY = 0, 4127 maxY = 0, 4128 len = this.objectsList.length; 4129 4130 for (el = 0; el < len; el++) { 4131 pEl = this.objectsList[el]; 4132 4133 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 4134 if (pEl.coords.usrCoords[1] < minX) { 4135 minX = pEl.coords.usrCoords[1]; 4136 } else if (pEl.coords.usrCoords[1] > maxX) { 4137 maxX = pEl.coords.usrCoords[1]; 4138 } 4139 if (pEl.coords.usrCoords[2] > maxY) { 4140 maxY = pEl.coords.usrCoords[2]; 4141 } else if (pEl.coords.usrCoords[2] < minY) { 4142 minY = pEl.coords.usrCoords[2]; 4143 } 4144 } 4145 } 4146 4147 border = 50; 4148 borderX = border / this.unitX; 4149 borderY = border / this.unitY; 4150 4151 this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], this.keepaspectratio, 'update'); 4152 4153 return this.applyZoom(); 4154 }, 4155 4156 /** 4157 * Reset the bounding box and the zoom level to 100% such that a given set of elements is 4158 * within the board's viewport. 4159 * @param {Array} elements A set of elements given by id, reference, or name. 4160 * @returns {JXG.Board} Reference to the board. 4161 */ 4162 zoomElements: function (elements) { 4163 var i, e, box, 4164 newBBox = [Infinity, -Infinity, -Infinity, Infinity], 4165 cx, cy, dx, dy, d; 4166 4167 if (!Type.isArray(elements) || elements.length === 0) { 4168 return this; 4169 } 4170 4171 for (i = 0; i < elements.length; i++) { 4172 e = this.select(elements[i]); 4173 4174 box = e.bounds(); 4175 if (Type.isArray(box)) { 4176 if (box[0] < newBBox[0]) { newBBox[0] = box[0]; } 4177 if (box[1] > newBBox[1]) { newBBox[1] = box[1]; } 4178 if (box[2] > newBBox[2]) { newBBox[2] = box[2]; } 4179 if (box[3] < newBBox[3]) { newBBox[3] = box[3]; } 4180 } 4181 } 4182 4183 if (Type.isArray(newBBox)) { 4184 cx = 0.5 * (newBBox[0] + newBBox[2]); 4185 cy = 0.5 * (newBBox[1] + newBBox[3]); 4186 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5; 4187 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5; 4188 d = Math.max(dx, dy); 4189 this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], this.keepaspectratio, 'update'); 4190 } 4191 4192 return this; 4193 }, 4194 4195 /** 4196 * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>. 4197 * @param {Number} fX 4198 * @param {Number} fY 4199 * @returns {JXG.Board} Reference to the board. 4200 */ 4201 setZoom: function (fX, fY) { 4202 var oX = this.attr.zoom.factorx, 4203 oY = this.attr.zoom.factory; 4204 4205 this.attr.zoom.factorx = fX / this.zoomX; 4206 this.attr.zoom.factory = fY / this.zoomY; 4207 4208 this.zoomIn(); 4209 4210 this.attr.zoom.factorx = oX; 4211 this.attr.zoom.factory = oY; 4212 4213 return this; 4214 }, 4215 4216 /** 4217 * Removes object from board and renderer. 4218 * <p> 4219 * <b>Performance hints:</b> It is recommended to use the object's id. 4220 * If many elements are removed, it is best to call <tt>board.suspendUpdate()</tt> 4221 * before looping through the elements to be removed and call 4222 * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop 4223 * in reverse order, i.e. remove the object in reverse order of their creation time. 4224 * 4225 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 4226 * The element(s) is/are given by name, id or a reference. 4227 * @param {Boolean} saveMethod If true, the algorithm runs through all elements 4228 * and tests if the element to be deleted is a child element. If yes, it will be 4229 * removed from the list of child elements. If false (default), the element 4230 * is removed from the lists of child elements of all its ancestors. 4231 * This should be much faster. 4232 * @returns {JXG.Board} Reference to the board 4233 */ 4234 removeObject: function (object, saveMethod) { 4235 var el, i; 4236 4237 if (Type.isArray(object)) { 4238 for (i = 0; i < object.length; i++) { 4239 this.removeObject(object[i]); 4240 } 4241 4242 return this; 4243 } 4244 4245 object = this.select(object); 4246 4247 // If the object which is about to be removed unknown or a string, do nothing. 4248 // it is a string if a string was given and could not be resolved to an element. 4249 if (!Type.exists(object) || Type.isString(object)) { 4250 return this; 4251 } 4252 4253 try { 4254 // remove all children. 4255 for (el in object.childElements) { 4256 if (object.childElements.hasOwnProperty(el)) { 4257 object.childElements[el].board.removeObject(object.childElements[el]); 4258 } 4259 } 4260 4261 // Remove all children in elements like turtle 4262 for (el in object.objects) { 4263 if (object.objects.hasOwnProperty(el)) { 4264 object.objects[el].board.removeObject(object.objects[el]); 4265 } 4266 } 4267 4268 // Remove the element from the childElement list and the descendant list of all elements. 4269 if (saveMethod) { 4270 // Running through all objects has quadratic complexity if many objects are deleted. 4271 for (el in this.objects) { 4272 if (this.objects.hasOwnProperty(el)) { 4273 if (Type.exists(this.objects[el].childElements) && 4274 Type.exists(this.objects[el].childElements.hasOwnProperty(object.id)) 4275 ) { 4276 delete this.objects[el].childElements[object.id]; 4277 delete this.objects[el].descendants[object.id]; 4278 } 4279 } 4280 } 4281 } else if (Type.exists(object.ancestors)) { 4282 // Running through the ancestors should be much more efficient. 4283 for (el in object.ancestors) { 4284 if (object.ancestors.hasOwnProperty(el)) { 4285 if (Type.exists(object.ancestors[el].childElements) && 4286 Type.exists(object.ancestors[el].childElements.hasOwnProperty(object.id)) 4287 ) { 4288 delete object.ancestors[el].childElements[object.id]; 4289 delete object.ancestors[el].descendants[object.id]; 4290 } 4291 } 4292 } 4293 } 4294 4295 // remove the object itself from our control structures 4296 if (object._pos > -1) { 4297 this.objectsList.splice(object._pos, 1); 4298 for (el = object._pos; el < this.objectsList.length; el++) { 4299 this.objectsList[el]._pos--; 4300 } 4301 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) { 4302 JXG.debug('Board.removeObject: object ' + object.id + ' not found in list.'); 4303 } 4304 4305 delete this.objects[object.id]; 4306 delete this.elementsByName[object.name]; 4307 4308 if (object.visProp && Type.evaluate(object.visProp.trace)) { 4309 object.clearTrace(); 4310 } 4311 4312 // the object deletion itself is handled by the object. 4313 if (Type.exists(object.remove)) { 4314 object.remove(); 4315 } 4316 } catch (e) { 4317 JXG.debug(object.id + ': Could not be removed: ' + e); 4318 } 4319 4320 this.update(); 4321 4322 return this; 4323 }, 4324 4325 /** 4326 * Removes the ancestors of an object an the object itself from board and renderer. 4327 * @param {JXG.GeometryElement} object The object to remove. 4328 * @returns {JXG.Board} Reference to the board 4329 */ 4330 removeAncestors: function (object) { 4331 var anc; 4332 4333 for (anc in object.ancestors) { 4334 if (object.ancestors.hasOwnProperty(anc)) { 4335 this.removeAncestors(object.ancestors[anc]); 4336 } 4337 } 4338 4339 this.removeObject(object); 4340 4341 return this; 4342 }, 4343 4344 /** 4345 * Initialize some objects which are contained in every GEONExT construction by default, 4346 * but are not contained in the gxt files. 4347 * @returns {JXG.Board} Reference to the board 4348 */ 4349 initGeonextBoard: function () { 4350 var p1, p2, p3; 4351 4352 p1 = this.create('point', [0, 0], { 4353 id: this.id + 'g00e0', 4354 name: 'Ursprung', 4355 withLabel: false, 4356 visible: false, 4357 fixed: true 4358 }); 4359 4360 p2 = this.create('point', [1, 0], { 4361 id: this.id + 'gX0e0', 4362 name: 'Punkt_1_0', 4363 withLabel: false, 4364 visible: false, 4365 fixed: true 4366 }); 4367 4368 p3 = this.create('point', [0, 1], { 4369 id: this.id + 'gY0e0', 4370 name: 'Punkt_0_1', 4371 withLabel: false, 4372 visible: false, 4373 fixed: true 4374 }); 4375 4376 this.create('line', [p1, p2], { 4377 id: this.id + 'gXLe0', 4378 name: 'X-Achse', 4379 withLabel: false, 4380 visible: false 4381 }); 4382 4383 this.create('line', [p1, p3], { 4384 id: this.id + 'gYLe0', 4385 name: 'Y-Achse', 4386 withLabel: false, 4387 visible: false 4388 }); 4389 4390 return this; 4391 }, 4392 4393 /** 4394 * Change the height and width of the board's container. 4395 * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using 4396 * the actual size of the bounding box and the actual value of keepaspectratio. 4397 * If setBoundingbox() should not be called automatically, 4398 * call resizeContainer with dontSetBoundingBox == true. 4399 * @param {Number} canvasWidth New width of the container. 4400 * @param {Number} canvasHeight New height of the container. 4401 * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element. 4402 * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(). 4403 * @returns {JXG.Board} Reference to the board 4404 */ 4405 resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) { 4406 var box; 4407 // w, h, cx, cy; 4408 // box_act, 4409 // shift_x = 0, 4410 // shift_y = 0; 4411 4412 if (!dontSetBoundingBox) { 4413 // box_act = this.getBoundingBox(); // This is the actual bounding box. 4414 box = this.getBoundingBox(); // This is the actual bounding box. 4415 } 4416 4417 this.canvasWidth = parseFloat(canvasWidth); 4418 this.canvasHeight = parseFloat(canvasHeight); 4419 4420 // if (!dontSetBoundingBox) { 4421 // box = this.attr.boundingbox; // This is the intended bounding box. 4422 4423 // // The shift values compensate the follow-up correction 4424 // // in setBoundingBox in case of "this.keepaspectratio==true" 4425 // // Otherwise, shift_x and shift_y will be zero. 4426 // // Obsolet since setBoundingBox centers in case of "this.keepaspectratio==true". 4427 // // shift_x = box_act[0] - box[0] / this.zoomX; 4428 // // shift_y = box_act[1] - box[1] / this.zoomY; 4429 4430 // cx = (box[2] + box[0]) * 0.5; // + shift_x; 4431 // cy = (box[3] + box[1]) * 0.5; // + shift_y; 4432 4433 // w = (box[2] - box[0]) * 0.5 / this.zoomX; 4434 // h = (box[1] - box[3]) * 0.5 / this.zoomY; 4435 4436 // box = [cx - w, cy + h, cx + w, cy - h]; 4437 // } 4438 4439 if (!dontset) { 4440 this.containerObj.style.width = (this.canvasWidth) + 'px'; 4441 this.containerObj.style.height = (this.canvasHeight) + 'px'; 4442 } 4443 this.renderer.resize(this.canvasWidth, this.canvasHeight); 4444 4445 if (!dontSetBoundingBox) { 4446 this.setBoundingBox(box, this.keepaspectratio, 'keep'); 4447 } 4448 4449 return this; 4450 }, 4451 4452 /** 4453 * Lists the dependencies graph in a new HTML-window. 4454 * @returns {JXG.Board} Reference to the board 4455 */ 4456 showDependencies: function () { 4457 var el, t, c, f, i; 4458 4459 t = '<p>\n'; 4460 for (el in this.objects) { 4461 if (this.objects.hasOwnProperty(el)) { 4462 i = 0; 4463 for (c in this.objects[el].childElements) { 4464 if (this.objects[el].childElements.hasOwnProperty(c)) { 4465 i += 1; 4466 } 4467 } 4468 if (i >= 0) { 4469 t += '<strong>' + this.objects[el].id + ':<' + '/strong> '; 4470 } 4471 4472 for (c in this.objects[el].childElements) { 4473 if (this.objects[el].childElements.hasOwnProperty(c)) { 4474 t += this.objects[el].childElements[c].id + '(' + this.objects[el].childElements[c].name + ')' + ', '; 4475 } 4476 } 4477 t += '<p>\n'; 4478 } 4479 } 4480 t += '<' + '/p>\n'; 4481 f = window.open(); 4482 f.document.open(); 4483 f.document.write(t); 4484 f.document.close(); 4485 return this; 4486 }, 4487 4488 /** 4489 * Lists the XML code of the construction in a new HTML-window. 4490 * @returns {JXG.Board} Reference to the board 4491 */ 4492 showXML: function () { 4493 var f = window.open(''); 4494 f.document.open(); 4495 f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>'); 4496 f.document.close(); 4497 return this; 4498 }, 4499 4500 /** 4501 * Sets for all objects the needsUpdate flag to "true". 4502 * @returns {JXG.Board} Reference to the board 4503 */ 4504 prepareUpdate: function () { 4505 var el, pEl, len = this.objectsList.length; 4506 4507 /* 4508 if (this.attr.updatetype === 'hierarchical') { 4509 return this; 4510 } 4511 */ 4512 4513 for (el = 0; el < len; el++) { 4514 pEl = this.objectsList[el]; 4515 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 4516 } 4517 4518 for (el in this.groups) { 4519 if (this.groups.hasOwnProperty(el)) { 4520 pEl = this.groups[el]; 4521 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 4522 } 4523 } 4524 4525 return this; 4526 }, 4527 4528 /** 4529 * Runs through all elements and calls their update() method. 4530 * @param {JXG.GeometryElement} drag Element that caused the update. 4531 * @returns {JXG.Board} Reference to the board 4532 */ 4533 updateElements: function (drag) { 4534 var el, pEl; 4535 //var childId, i = 0; 4536 4537 drag = this.select(drag); 4538 4539 /* 4540 if (Type.exists(drag)) { 4541 for (el = 0; el < this.objectsList.length; el++) { 4542 pEl = this.objectsList[el]; 4543 if (pEl.id === drag.id) { 4544 i = el; 4545 break; 4546 } 4547 } 4548 } 4549 */ 4550 4551 for (el = 0; el < this.objectsList.length; el++) { 4552 pEl = this.objectsList[el]; 4553 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) { 4554 pEl.updateSize(); 4555 } 4556 4557 // For updates of an element we distinguish if the dragged element is updated or 4558 // other elements are updated. 4559 // The difference lies in the treatment of gliders and points based on transformations. 4560 pEl.update(!Type.exists(drag) || pEl.id !== drag.id) 4561 .updateVisibility(); 4562 } 4563 4564 // update groups last 4565 for (el in this.groups) { 4566 if (this.groups.hasOwnProperty(el)) { 4567 this.groups[el].update(drag); 4568 } 4569 } 4570 4571 return this; 4572 }, 4573 4574 /** 4575 * Runs through all elements and calls their update() method. 4576 * @returns {JXG.Board} Reference to the board 4577 */ 4578 updateRenderer: function () { 4579 var el, 4580 len = this.objectsList.length; 4581 4582 /* 4583 objs = this.objectsList.slice(0); 4584 objs.sort(function (a, b) { 4585 if (a.visProp.layer < b.visProp.layer) { 4586 return -1; 4587 } else if (a.visProp.layer === b.visProp.layer) { 4588 return b.lastDragTime.getTime() - a.lastDragTime.getTime(); 4589 } else { 4590 return 1; 4591 } 4592 }); 4593 */ 4594 4595 if (this.renderer.type === 'canvas') { 4596 this.updateRendererCanvas(); 4597 } else { 4598 for (el = 0; el < len; el++) { 4599 this.objectsList[el].updateRenderer(); 4600 } 4601 } 4602 return this; 4603 }, 4604 4605 /** 4606 * Runs through all elements and calls their update() method. 4607 * This is a special version for the CanvasRenderer. 4608 * Here, we have to do our own layer handling. 4609 * @returns {JXG.Board} Reference to the board 4610 */ 4611 updateRendererCanvas: function () { 4612 var el, pEl, i, mini, la, 4613 olen = this.objectsList.length, 4614 layers = this.options.layer, 4615 len = this.options.layer.numlayers, 4616 last = Number.NEGATIVE_INFINITY; 4617 4618 for (i = 0; i < len; i++) { 4619 mini = Number.POSITIVE_INFINITY; 4620 4621 for (la in layers) { 4622 if (layers.hasOwnProperty(la)) { 4623 if (layers[la] > last && layers[la] < mini) { 4624 mini = layers[la]; 4625 } 4626 } 4627 } 4628 4629 last = mini; 4630 4631 for (el = 0; el < olen; el++) { 4632 pEl = this.objectsList[el]; 4633 4634 if (pEl.visProp.layer === mini) { 4635 pEl.prepareUpdate().updateRenderer(); 4636 } 4637 } 4638 } 4639 return this; 4640 }, 4641 4642 /** 4643 * Please use {@link JXG.Board.on} instead. 4644 * @param {Function} hook A function to be called by the board after an update occurred. 4645 * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>. 4646 * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the 4647 * board object the hook is attached to. 4648 * @returns {Number} Id of the hook, required to remove the hook from the board. 4649 * @deprecated 4650 */ 4651 addHook: function (hook, m, context) { 4652 JXG.deprecated('Board.addHook()', 'Board.on()'); 4653 m = Type.def(m, 'update'); 4654 4655 context = Type.def(context, this); 4656 4657 this.hooks.push([m, hook]); 4658 this.on(m, hook, context); 4659 4660 return this.hooks.length - 1; 4661 }, 4662 4663 /** 4664 * Alias of {@link JXG.Board.on}. 4665 */ 4666 addEvent: JXG.shortcut(JXG.Board.prototype, 'on'), 4667 4668 /** 4669 * Please use {@link JXG.Board.off} instead. 4670 * @param {Number|function} id The number you got when you added the hook or a reference to the event handler. 4671 * @returns {JXG.Board} Reference to the board 4672 * @deprecated 4673 */ 4674 removeHook: function (id) { 4675 JXG.deprecated('Board.removeHook()', 'Board.off()'); 4676 if (this.hooks[id]) { 4677 this.off(this.hooks[id][0], this.hooks[id][1]); 4678 this.hooks[id] = null; 4679 } 4680 4681 return this; 4682 }, 4683 4684 /** 4685 * Alias of {@link JXG.Board.off}. 4686 */ 4687 removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'), 4688 4689 /** 4690 * Runs through all hooked functions and calls them. 4691 * @returns {JXG.Board} Reference to the board 4692 * @deprecated 4693 */ 4694 updateHooks: function (m) { 4695 var arg = Array.prototype.slice.call(arguments, 0); 4696 4697 JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()'); 4698 4699 arg[0] = Type.def(arg[0], 'update'); 4700 this.triggerEventHandlers([arg[0]], arguments); 4701 4702 return this; 4703 }, 4704 4705 /** 4706 * Adds a dependent board to this board. 4707 * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred. 4708 * @returns {JXG.Board} Reference to the board 4709 */ 4710 addChild: function (board) { 4711 if (Type.exists(board) && Type.exists(board.containerObj)) { 4712 this.dependentBoards.push(board); 4713 this.update(); 4714 } 4715 return this; 4716 }, 4717 4718 /** 4719 * Deletes a board from the list of dependent boards. 4720 * @param {JXG.Board} board Reference to the board which will be removed. 4721 * @returns {JXG.Board} Reference to the board 4722 */ 4723 removeChild: function (board) { 4724 var i; 4725 4726 for (i = this.dependentBoards.length - 1; i >= 0; i--) { 4727 if (this.dependentBoards[i] === board) { 4728 this.dependentBoards.splice(i, 1); 4729 } 4730 } 4731 return this; 4732 }, 4733 4734 /** 4735 * Runs through most elements and calls their update() method and update the conditions. 4736 * @param {JXG.GeometryElement} [drag] Element that caused the update. 4737 * @returns {JXG.Board} Reference to the board 4738 */ 4739 update: function (drag) { 4740 var i, len, b, insert, 4741 storeActiveEl; 4742 4743 if (this.inUpdate || this.isSuspendedUpdate) { 4744 return this; 4745 } 4746 this.inUpdate = true; 4747 4748 if (this.attr.minimizereflow === 'all' && this.containerObj && this.renderer.type !== 'vml') { 4749 storeActiveEl = document.activeElement; // Store focus element 4750 insert = this.renderer.removeToInsertLater(this.containerObj); 4751 } 4752 4753 if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') { 4754 storeActiveEl = document.activeElement; 4755 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot); 4756 } 4757 4758 this.prepareUpdate().updateElements(drag).updateConditions(); 4759 this.renderer.suspendRedraw(this); 4760 this.updateRenderer(); 4761 this.renderer.unsuspendRedraw(); 4762 this.triggerEventHandlers(['update'], []); 4763 4764 if (insert) { 4765 insert(); 4766 storeActiveEl.focus(); // Restore focus element 4767 } 4768 4769 // To resolve dependencies between boards 4770 // for (var board in JXG.boards) { 4771 len = this.dependentBoards.length; 4772 for (i = 0; i < len; i++) { 4773 b = this.dependentBoards[i]; 4774 if (Type.exists(b) && b !== this) { 4775 b.updateQuality = this.updateQuality; 4776 b.prepareUpdate().updateElements().updateConditions(); 4777 b.renderer.suspendRedraw(); 4778 b.updateRenderer(); 4779 b.renderer.unsuspendRedraw(); 4780 b.triggerEventHandlers(['update'], []); 4781 } 4782 4783 } 4784 4785 this.inUpdate = false; 4786 return this; 4787 }, 4788 4789 /** 4790 * Runs through all elements and calls their update() method and update the conditions. 4791 * This is necessary after zooming and changing the bounding box. 4792 * @returns {JXG.Board} Reference to the board 4793 */ 4794 fullUpdate: function () { 4795 this.needsFullUpdate = true; 4796 this.update(); 4797 this.needsFullUpdate = false; 4798 return this; 4799 }, 4800 4801 /** 4802 * Adds a grid to the board according to the settings given in board.options. 4803 * @returns {JXG.Board} Reference to the board. 4804 */ 4805 addGrid: function () { 4806 this.create('grid', []); 4807 4808 return this; 4809 }, 4810 4811 /** 4812 * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or 4813 * more of the grids. 4814 * @returns {JXG.Board} Reference to the board object. 4815 */ 4816 removeGrids: function () { 4817 var i; 4818 4819 for (i = 0; i < this.grids.length; i++) { 4820 this.removeObject(this.grids[i]); 4821 } 4822 4823 this.grids.length = 0; 4824 this.update(); // required for canvas renderer 4825 4826 return this; 4827 }, 4828 4829 /** 4830 * Creates a new geometric element of type elementType. 4831 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'. 4832 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two 4833 * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 4834 * methods for a list of possible parameters. 4835 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 4836 * Common attributes are name, visible, strokeColor. 4837 * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing 4838 * two or more elements. 4839 */ 4840 create: function (elementType, parents, attributes) { 4841 var el, i; 4842 4843 elementType = elementType.toLowerCase(); 4844 4845 if (!Type.exists(parents)) { 4846 parents = []; 4847 } 4848 4849 if (!Type.exists(attributes)) { 4850 attributes = {}; 4851 } 4852 4853 for (i = 0; i < parents.length; i++) { 4854 if (Type.isString(parents[i]) && 4855 !(elementType === 'text' && i === 2) && 4856 !(elementType === 'solidofrevolution3d' && i === 2) && 4857 !((elementType === 'input' || elementType === 'checkbox' || elementType === 'button') && 4858 (i === 2 || i === 3)) && 4859 !(elementType === 'curve' && i > 0) // Allow curve plots with jessiecode 4860 ) { 4861 parents[i] = this.select(parents[i]); 4862 } 4863 } 4864 4865 if (Type.isFunction(JXG.elements[elementType])) { 4866 el = JXG.elements[elementType](this, parents, attributes); 4867 } else { 4868 throw new Error("JSXGraph: create: Unknown element type given: " + elementType); 4869 } 4870 4871 if (!Type.exists(el)) { 4872 JXG.debug("JSXGraph: create: failure creating " + elementType); 4873 return el; 4874 } 4875 4876 if (el.prepareUpdate && el.update && el.updateRenderer) { 4877 el.fullUpdate(); 4878 } 4879 return el; 4880 }, 4881 4882 /** 4883 * Deprecated name for {@link JXG.Board.create}. 4884 * @deprecated 4885 */ 4886 createElement: function () { 4887 JXG.deprecated('Board.createElement()', 'Board.create()'); 4888 return this.create.apply(this, arguments); 4889 }, 4890 4891 /** 4892 * Delete the elements drawn as part of a trace of an element. 4893 * @returns {JXG.Board} Reference to the board 4894 */ 4895 clearTraces: function () { 4896 var el; 4897 4898 for (el = 0; el < this.objectsList.length; el++) { 4899 this.objectsList[el].clearTrace(); 4900 } 4901 4902 this.numTraces = 0; 4903 return this; 4904 }, 4905 4906 /** 4907 * Stop updates of the board. 4908 * @returns {JXG.Board} Reference to the board 4909 */ 4910 suspendUpdate: function () { 4911 if (!this.inUpdate) { 4912 this.isSuspendedUpdate = true; 4913 } 4914 return this; 4915 }, 4916 4917 /** 4918 * Enable updates of the board. 4919 * @returns {JXG.Board} Reference to the board 4920 */ 4921 unsuspendUpdate: function () { 4922 if (this.isSuspendedUpdate) { 4923 this.isSuspendedUpdate = false; 4924 this.fullUpdate(); 4925 } 4926 return this; 4927 }, 4928 4929 /** 4930 * Set the bounding box of the board. 4931 * @param {Array} bbox New bounding box [x1,y1,x2,y2] 4932 * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but 4933 * the resulting viewport may be larger. 4934 * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset' 4935 * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0). 4936 * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing. 4937 * @returns {JXG.Board} Reference to the board 4938 */ 4939 setBoundingBox: function (bbox, keepaspectratio, setZoom) { 4940 var h, w, ux, uy, 4941 offX = 0, 4942 offY = 0, 4943 dim = Env.getDimensions(this.container, this.document); 4944 4945 if (!Type.isArray(bbox)) { 4946 return this; 4947 } 4948 4949 if (bbox[0] < this.maxboundingbox[0] || 4950 bbox[1] > this.maxboundingbox[1] || 4951 bbox[2] > this.maxboundingbox[2] || 4952 bbox[3] < this.maxboundingbox[3]) { 4953 return this; 4954 } 4955 4956 if (!Type.exists(setZoom)) { 4957 setZoom = 'reset'; 4958 } 4959 4960 ux = this.unitX; 4961 uy = this.unitY; 4962 4963 this.canvasWidth = parseInt(dim.width, 10); 4964 this.canvasHeight = parseInt(dim.height, 10); 4965 w = this.canvasWidth; 4966 h = this.canvasHeight; 4967 if (keepaspectratio) { 4968 this.unitX = w / (bbox[2] - bbox[0]); 4969 this.unitY = h / (bbox[1] - bbox[3]); 4970 if (Math.abs(this.unitX) < Math.abs(this.unitY)) { 4971 this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY); 4972 // Add the additional units in equal portions above and below 4973 offY = (h / this.unitY - (bbox[1] - bbox[3])) * 0.5; 4974 } else { 4975 this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX); 4976 // Add the additional units in equal portions left and right 4977 offX = (w / this.unitX - (bbox[2] - bbox[0])) * 0.5; 4978 } 4979 this.keepaspectratio = true; 4980 } else { 4981 this.unitX = w / (bbox[2] - bbox[0]); 4982 this.unitY = h / (bbox[1] - bbox[3]); 4983 this.keepaspectratio = false; 4984 } 4985 4986 this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY)); 4987 4988 if (setZoom === 'update') { 4989 this.zoomX *= this.unitX / ux; 4990 this.zoomY *= this.unitY / uy; 4991 } else if (setZoom === 'reset') { 4992 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0; 4993 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0; 4994 } 4995 4996 return this; 4997 }, 4998 4999 /** 5000 * Get the bounding box of the board. 5001 * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner 5002 */ 5003 getBoundingBox: function () { 5004 var ul = (new Coords(Const.COORDS_BY_SCREEN, [0, 0], this)).usrCoords, 5005 lr = (new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this)).usrCoords; 5006 5007 return [ul[1], ul[2], lr[1], lr[2]]; 5008 }, 5009 5010 /** 5011 * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the 5012 * animated elements. This function tells the board about new elements to animate. 5013 * @param {JXG.GeometryElement} element The element which is to be animated. 5014 * @returns {JXG.Board} Reference to the board 5015 */ 5016 addAnimation: function (element) { 5017 var that = this; 5018 5019 this.animationObjects[element.id] = element; 5020 5021 if (!this.animationIntervalCode) { 5022 this.animationIntervalCode = window.setInterval(function () { 5023 that.animate(); 5024 }, element.board.attr.animationdelay); 5025 } 5026 5027 return this; 5028 }, 5029 5030 /** 5031 * Cancels all running animations. 5032 * @returns {JXG.Board} Reference to the board 5033 */ 5034 stopAllAnimation: function () { 5035 var el; 5036 5037 for (el in this.animationObjects) { 5038 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) { 5039 this.animationObjects[el] = null; 5040 delete this.animationObjects[el]; 5041 } 5042 } 5043 5044 window.clearInterval(this.animationIntervalCode); 5045 delete this.animationIntervalCode; 5046 5047 return this; 5048 }, 5049 5050 /** 5051 * General purpose animation function. This currently only supports moving points from one place to another. This 5052 * is faster than managing the animation per point, especially if there is more than one animated point at the same time. 5053 * @returns {JXG.Board} Reference to the board 5054 */ 5055 animate: function () { 5056 var props, el, o, newCoords, r, p, c, cbtmp, 5057 count = 0, 5058 obj = null; 5059 5060 for (el in this.animationObjects) { 5061 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) { 5062 count += 1; 5063 o = this.animationObjects[el]; 5064 5065 if (o.animationPath) { 5066 if (Type.isFunction(o.animationPath)) { 5067 newCoords = o.animationPath(new Date().getTime() - o.animationStart); 5068 } else { 5069 newCoords = o.animationPath.pop(); 5070 } 5071 5072 if ((!Type.exists(newCoords)) || (!Type.isArray(newCoords) && isNaN(newCoords))) { 5073 delete o.animationPath; 5074 } else { 5075 o.setPositionDirectly(Const.COORDS_BY_USER, newCoords); 5076 o.fullUpdate(); 5077 obj = o; 5078 } 5079 } 5080 if (o.animationData) { 5081 c = 0; 5082 5083 for (r in o.animationData) { 5084 if (o.animationData.hasOwnProperty(r)) { 5085 p = o.animationData[r].pop(); 5086 5087 if (!Type.exists(p)) { 5088 delete o.animationData[p]; 5089 } else { 5090 c += 1; 5091 props = {}; 5092 props[r] = p; 5093 o.setAttribute(props); 5094 } 5095 } 5096 } 5097 5098 if (c === 0) { 5099 delete o.animationData; 5100 } 5101 } 5102 5103 if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) { 5104 this.animationObjects[el] = null; 5105 delete this.animationObjects[el]; 5106 5107 if (Type.exists(o.animationCallback)) { 5108 cbtmp = o.animationCallback; 5109 o.animationCallback = null; 5110 cbtmp(); 5111 } 5112 } 5113 } 5114 } 5115 5116 if (count === 0) { 5117 window.clearInterval(this.animationIntervalCode); 5118 delete this.animationIntervalCode; 5119 } else { 5120 this.update(obj); 5121 } 5122 5123 return this; 5124 }, 5125 5126 /** 5127 * Migrate the dependency properties of the point src 5128 * to the point dest and delete the point src. 5129 * For example, a circle around the point src 5130 * receives the new center dest. The old center src 5131 * will be deleted. 5132 * @param {JXG.Point} src Original point which will be deleted 5133 * @param {JXG.Point} dest New point with the dependencies of src. 5134 * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the 5135 * dest element. 5136 * @returns {JXG.Board} Reference to the board 5137 */ 5138 migratePoint: function (src, dest, copyName) { 5139 var child, childId, prop, found, i, srcLabelId, srcHasLabel = false; 5140 5141 src = this.select(src); 5142 dest = this.select(dest); 5143 5144 if (Type.exists(src.label)) { 5145 srcLabelId = src.label.id; 5146 srcHasLabel = true; 5147 this.removeObject(src.label); 5148 } 5149 5150 for (childId in src.childElements) { 5151 if (src.childElements.hasOwnProperty(childId)) { 5152 child = src.childElements[childId]; 5153 found = false; 5154 5155 for (prop in child) { 5156 if (child.hasOwnProperty(prop)) { 5157 if (child[prop] === src) { 5158 child[prop] = dest; 5159 found = true; 5160 } 5161 } 5162 } 5163 5164 if (found) { 5165 delete src.childElements[childId]; 5166 } 5167 5168 for (i = 0; i < child.parents.length; i++) { 5169 if (child.parents[i] === src.id) { 5170 child.parents[i] = dest.id; 5171 } 5172 } 5173 5174 dest.addChild(child); 5175 } 5176 } 5177 5178 // The destination object should receive the name 5179 // and the label of the originating (src) object 5180 if (copyName) { 5181 if (srcHasLabel) { 5182 delete dest.childElements[srcLabelId]; 5183 delete dest.descendants[srcLabelId]; 5184 } 5185 5186 if (dest.label) { 5187 this.removeObject(dest.label); 5188 } 5189 5190 delete this.elementsByName[dest.name]; 5191 dest.name = src.name; 5192 if (srcHasLabel) { 5193 dest.createLabel(); 5194 } 5195 } 5196 5197 this.removeObject(src); 5198 5199 if (Type.exists(dest.name) && dest.name !== '') { 5200 this.elementsByName[dest.name] = dest; 5201 } 5202 5203 this.fullUpdate(); 5204 5205 return this; 5206 }, 5207 5208 /** 5209 * Initializes color blindness simulation. 5210 * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'. 5211 * @returns {JXG.Board} Reference to the board 5212 */ 5213 emulateColorblindness: function (deficiency) { 5214 var e, o; 5215 5216 if (!Type.exists(deficiency)) { 5217 deficiency = 'none'; 5218 } 5219 5220 if (this.currentCBDef === deficiency) { 5221 return this; 5222 } 5223 5224 for (e in this.objects) { 5225 if (this.objects.hasOwnProperty(e)) { 5226 o = this.objects[e]; 5227 5228 if (deficiency !== 'none') { 5229 if (this.currentCBDef === 'none') { 5230 // this could be accomplished by JXG.extend, too. But do not use 5231 // JXG.deepCopy as this could result in an infinite loop because in 5232 // visProp there could be geometry elements which contain the board which 5233 // contains all objects which contain board etc. 5234 o.visPropOriginal = { 5235 strokecolor: o.visProp.strokecolor, 5236 fillcolor: o.visProp.fillcolor, 5237 highlightstrokecolor: o.visProp.highlightstrokecolor, 5238 highlightfillcolor: o.visProp.highlightfillcolor 5239 }; 5240 } 5241 o.setAttribute({ 5242 strokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.strokecolor), deficiency), 5243 fillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.fillcolor), deficiency), 5244 highlightstrokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightstrokecolor), deficiency), 5245 highlightfillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightfillcolor), deficiency) 5246 }); 5247 } else if (Type.exists(o.visPropOriginal)) { 5248 JXG.extend(o.visProp, o.visPropOriginal); 5249 } 5250 } 5251 } 5252 this.currentCBDef = deficiency; 5253 this.update(); 5254 5255 return this; 5256 }, 5257 5258 /** 5259 * Select a single or multiple elements at once. 5260 * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will 5261 * be used as a filter to return multiple elements at once filtered by the properties of the object. 5262 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 5263 * The advanced filters consisting of objects or functions are ignored. 5264 * @returns {JXG.GeometryElement|JXG.Composition} 5265 * @example 5266 * // select the element with name A 5267 * board.select('A'); 5268 * 5269 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 5270 * board.select({ 5271 * strokeColor: 'red' 5272 * }); 5273 * 5274 * // select all points on or below the x axis and make them black. 5275 * board.select({ 5276 * elementClass: JXG.OBJECT_CLASS_POINT, 5277 * Y: function (v) { 5278 * return v <= 0; 5279 * } 5280 * }).setAttribute({color: 'black'}); 5281 * 5282 * // select all elements 5283 * board.select(function (el) { 5284 * return true; 5285 * }); 5286 */ 5287 select: function (str, onlyByIdOrName) { 5288 var flist, olist, i, l, 5289 s = str; 5290 5291 if (s === null) { 5292 return s; 5293 } 5294 5295 // it's a string, most likely an id or a name. 5296 if (Type.isString(s) && s !== '') { 5297 // Search by ID 5298 if (Type.exists(this.objects[s])) { 5299 s = this.objects[s]; 5300 // Search by name 5301 } else if (Type.exists(this.elementsByName[s])) { 5302 s = this.elementsByName[s]; 5303 // Search by group ID 5304 } else if (Type.exists(this.groups[s])) { 5305 s = this.groups[s]; 5306 } 5307 // it's a function or an object, but not an element 5308 } else if (!onlyByIdOrName && 5309 (Type.isFunction(s) || 5310 (Type.isObject(s) && !Type.isFunction(s.setAttribute)) 5311 )) { 5312 flist = Type.filterElements(this.objectsList, s); 5313 5314 olist = {}; 5315 l = flist.length; 5316 for (i = 0; i < l; i++) { 5317 olist[flist[i].id] = flist[i]; 5318 } 5319 s = new Composition(olist); 5320 // it's an element which has been deleted (and still hangs around, e.g. in an attractor list 5321 } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) { 5322 s = null; 5323 } 5324 5325 return s; 5326 }, 5327 5328 /** 5329 * Checks if the given point is inside the boundingbox. 5330 * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object. 5331 * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object. 5332 * @returns {Boolean} 5333 */ 5334 hasPoint: function (x, y) { 5335 var px = x, 5336 py = y, 5337 bbox = this.getBoundingBox(); 5338 5339 if (Type.exists(x) && Type.isArray(x.usrCoords)) { 5340 px = x.usrCoords[1]; 5341 py = x.usrCoords[2]; 5342 } 5343 5344 return !!(Type.isNumber(px) && Type.isNumber(py) && 5345 bbox[0] < px && px < bbox[2] && bbox[1] > py && py > bbox[3]); 5346 }, 5347 5348 /** 5349 * Update CSS transformations of type scaling. It is used to correct the mouse position 5350 * in {@link JXG.Board.getMousePosition}. 5351 * The inverse transformation matrix is updated on each mouseDown and touchStart event. 5352 * 5353 * It is up to the user to call this method after an update of the CSS transformation 5354 * in the DOM. 5355 */ 5356 updateCSSTransforms: function () { 5357 var obj = this.containerObj, 5358 o = obj, 5359 o2 = obj; 5360 5361 this.cssTransMat = Env.getCSSTransformMatrix(o); 5362 5363 /* 5364 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 5365 * if not to the body. In IE and if we are in an position:absolute environment 5366 * offsetParent walks up the DOM hierarchy. 5367 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 5368 * we need the parentNode steps. 5369 */ 5370 o = o.offsetParent; 5371 while (o) { 5372 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 5373 5374 o2 = o2.parentNode; 5375 while (o2 !== o) { 5376 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 5377 o2 = o2.parentNode; 5378 } 5379 5380 o = o.offsetParent; 5381 } 5382 this.cssTransMat = Mat.inverse(this.cssTransMat); 5383 5384 return this; 5385 }, 5386 5387 /** 5388 * Start selection mode. This function can either be triggered from outside or by 5389 * a down event together with correct key pressing. The default keys are 5390 * shift+ctrl. But this can be changed in the options. 5391 * 5392 * Starting from out side can be realized for example with a button like this: 5393 * <pre> 5394 * <button onclick="board.startSelectionMode()">Start</button> 5395 * </pre> 5396 * @example 5397 * // 5398 * // Set a new bounding box from the selection rectangle 5399 * // 5400 * var board = JXG.JSXGraph.initBoard('jxgbox', { 5401 * boundingBox:[-3,2,3,-2], 5402 * keepAspectRatio: false, 5403 * axis:true, 5404 * selection: { 5405 * enabled: true, 5406 * needShift: false, 5407 * needCtrl: true, 5408 * withLines: false, 5409 * vertices: { 5410 * visible: false 5411 * }, 5412 * fillColor: '#ffff00', 5413 * } 5414 * }); 5415 * 5416 * var f = function f(x) { return Math.cos(x); }, 5417 * curve = board.create('functiongraph', [f]); 5418 * 5419 * board.on('stopselecting', function(){ 5420 * var box = board.stopSelectionMode(), 5421 * 5422 * // bbox has the coordinates of the selection rectangle. 5423 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 5424 * // are homogeneous coordinates. 5425 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 5426 * 5427 * // Set a new bounding box 5428 * board.setBoundingBox(bbox, false); 5429 * }); 5430 * 5431 * 5432 * </pre><div class="jxgbox" id="JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723" style="width: 300px; height: 300px;"></div> 5433 * <script type="text/javascript"> 5434 * (function() { 5435 * // 5436 * // Set a new bounding box from the selection rectangle 5437 * // 5438 * var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', { 5439 * boundingBox:[-3,2,3,-2], 5440 * keepAspectRatio: false, 5441 * axis:true, 5442 * selection: { 5443 * enabled: true, 5444 * needShift: false, 5445 * needCtrl: true, 5446 * withLines: false, 5447 * vertices: { 5448 * visible: false 5449 * }, 5450 * fillColor: '#ffff00', 5451 * } 5452 * }); 5453 * 5454 * var f = function f(x) { return Math.cos(x); }, 5455 * curve = board.create('functiongraph', [f]); 5456 * 5457 * board.on('stopselecting', function(){ 5458 * var box = board.stopSelectionMode(), 5459 * 5460 * // bbox has the coordinates of the selection rectangle. 5461 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 5462 * // are homogeneous coordinates. 5463 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 5464 * 5465 * // Set a new bounding box 5466 * board.setBoundingBox(bbox, false); 5467 * }); 5468 * })(); 5469 * 5470 * </script><pre> 5471 * 5472 */ 5473 startSelectionMode: function () { 5474 this.selectingMode = true; 5475 this.selectionPolygon.setAttribute({visible: true}); 5476 this.selectingBox = [[0, 0], [0, 0]]; 5477 this._setSelectionPolygonFromBox(); 5478 this.selectionPolygon.fullUpdate(); 5479 }, 5480 5481 /** 5482 * Finalize the selection: disable selection mode and return the coordinates 5483 * of the selection rectangle. 5484 * @returns {Array} Coordinates of the selection rectangle. The array 5485 * contains two {@link JXG.Coords} objects. One the upper left corner and 5486 * the second for the lower right corner. 5487 */ 5488 stopSelectionMode: function () { 5489 this.selectingMode = false; 5490 this.selectionPolygon.setAttribute({visible: false}); 5491 return [this.selectionPolygon.vertices[0].coords, this.selectionPolygon.vertices[2].coords]; 5492 }, 5493 5494 /** 5495 * Start the selection of a region. 5496 * @private 5497 * @param {Array} pos Screen coordiates of the upper left corner of the 5498 * selection rectangle. 5499 */ 5500 _startSelecting: function (pos) { 5501 this.isSelecting = true; 5502 this.selectingBox = [ [pos[0], pos[1]], [pos[0], pos[1]] ]; 5503 this._setSelectionPolygonFromBox(); 5504 }, 5505 5506 /** 5507 * Update the selection rectangle during a move event. 5508 * @private 5509 * @param {Array} pos Screen coordiates of the move event 5510 */ 5511 _moveSelecting: function (pos) { 5512 if (this.isSelecting) { 5513 this.selectingBox[1] = [pos[0], pos[1]]; 5514 this._setSelectionPolygonFromBox(); 5515 this.selectionPolygon.fullUpdate(); 5516 } 5517 }, 5518 5519 /** 5520 * Update the selection rectangle during an up event. Stop selection. 5521 * @private 5522 * @param {Object} evt Event object 5523 */ 5524 _stopSelecting: function (evt) { 5525 var pos = this.getMousePosition(evt); 5526 5527 this.isSelecting = false; 5528 this.selectingBox[1] = [pos[0], pos[1]]; 5529 this._setSelectionPolygonFromBox(); 5530 }, 5531 5532 /** 5533 * Update the Selection rectangle. 5534 * @private 5535 */ 5536 _setSelectionPolygonFromBox: function () { 5537 var A = this.selectingBox[0], 5538 B = this.selectingBox[1]; 5539 5540 this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], A[1]]); 5541 this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], B[1]]); 5542 this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], B[1]]); 5543 this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], A[1]]); 5544 }, 5545 5546 /** 5547 * Test if a down event should start a selection. Test if the 5548 * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called. 5549 * @param {Object} evt Event object 5550 */ 5551 _testForSelection: function (evt) { 5552 if (this._isRequiredKeyPressed(evt, 'selection')) { 5553 if (!Type.exists(this.selectionPolygon)) { 5554 this._createSelectionPolygon(this.attr); 5555 } 5556 this.startSelectionMode(); 5557 } 5558 }, 5559 5560 /** 5561 * Create the internal selection polygon, which will be available as board.selectionPolygon. 5562 * @private 5563 * @param {Object} attr board attributes, e.g. the subobject board.attr. 5564 * @returns {Object} pointer to the board to enable chaining. 5565 */ 5566 _createSelectionPolygon: function(attr) { 5567 var selectionattr; 5568 5569 if (!Type.exists(this.selectionPolygon)) { 5570 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection'); 5571 if (selectionattr.enabled === true) { 5572 this.selectionPolygon = this.create('polygon', [[0, 0], [0, 0], [0, 0], [0, 0]], selectionattr); 5573 } 5574 } 5575 5576 return this; 5577 }, 5578 5579 /* ************************** 5580 * EVENT DEFINITION 5581 * for documentation purposes 5582 * ************************** */ 5583 5584 //region Event handler documentation 5585 5586 /** 5587 * @event 5588 * @description Whenever the user starts to touch or click the board. 5589 * @name JXG.Board#down 5590 * @param {Event} e The browser's event object. 5591 */ 5592 __evt__down: function (e) { }, 5593 5594 /** 5595 * @event 5596 * @description Whenever the user starts to click on the board. 5597 * @name JXG.Board#mousedown 5598 * @param {Event} e The browser's event object. 5599 */ 5600 __evt__mousedown: function (e) { }, 5601 5602 /** 5603 * @event 5604 * @description Whenever the user taps the pen on the board. 5605 * @name JXG.Board#pendown 5606 * @param {Event} e The browser's event object. 5607 */ 5608 __evt__pendown: function (e) { }, 5609 5610 /** 5611 * @event 5612 * @description Whenever the user starts to click on the board with a 5613 * device sending pointer events. 5614 * @name JXG.Board#pointerdown 5615 * @param {Event} e The browser's event object. 5616 */ 5617 __evt__pointerdown: function (e) { }, 5618 5619 /** 5620 * @event 5621 * @description Whenever the user starts to touch the board. 5622 * @name JXG.Board#touchstart 5623 * @param {Event} e The browser's event object. 5624 */ 5625 __evt__touchstart: function (e) { }, 5626 5627 /** 5628 * @event 5629 * @description Whenever the user stops to touch or click the board. 5630 * @name JXG.Board#up 5631 * @param {Event} e The browser's event object. 5632 */ 5633 __evt__up: function (e) { }, 5634 5635 /** 5636 * @event 5637 * @description Whenever the user releases the mousebutton over the board. 5638 * @name JXG.Board#mouseup 5639 * @param {Event} e The browser's event object. 5640 */ 5641 __evt__mouseup: function (e) { }, 5642 5643 /** 5644 * @event 5645 * @description Whenever the user releases the mousebutton over the board with a 5646 * device sending pointer events. 5647 * @name JXG.Board#pointerup 5648 * @param {Event} e The browser's event object. 5649 */ 5650 __evt__pointerup: function (e) { }, 5651 5652 /** 5653 * @event 5654 * @description Whenever the user stops touching the board. 5655 * @name JXG.Board#touchend 5656 * @param {Event} e The browser's event object. 5657 */ 5658 __evt__touchend: function (e) { }, 5659 5660 /** 5661 * @event 5662 * @description This event is fired whenever the user is moving the finger or mouse pointer over the board. 5663 * @name JXG.Board#move 5664 * @param {Event} e The browser's event object. 5665 * @param {Number} mode The mode the board currently is in 5666 * @see JXG.Board#mode 5667 */ 5668 __evt__move: function (e, mode) { }, 5669 5670 /** 5671 * @event 5672 * @description This event is fired whenever the user is moving the mouse over the board. 5673 * @name JXG.Board#mousemove 5674 * @param {Event} e The browser's event object. 5675 * @param {Number} mode The mode the board currently is in 5676 * @see JXG.Board#mode 5677 */ 5678 __evt__mousemove: function (e, mode) { }, 5679 5680 /** 5681 * @event 5682 * @description This event is fired whenever the user is moving the pen over the board. 5683 * @name JXG.Board#penmove 5684 * @param {Event} e The browser's event object. 5685 * @param {Number} mode The mode the board currently is in 5686 * @see JXG.Board#mode 5687 */ 5688 __evt__penmove: function (e, mode) { }, 5689 5690 /** 5691 * @event 5692 * @description This event is fired whenever the user is moving the mouse over the board with a 5693 * device sending pointer events. 5694 * @name JXG.Board#pointermove 5695 * @param {Event} e The browser's event object. 5696 * @param {Number} mode The mode the board currently is in 5697 * @see JXG.Board#mode 5698 */ 5699 __evt__pointermove: function (e, mode) { }, 5700 5701 /** 5702 * @event 5703 * @description This event is fired whenever the user is moving the finger over the board. 5704 * @name JXG.Board#touchmove 5705 * @param {Event} e The browser's event object. 5706 * @param {Number} mode The mode the board currently is in 5707 * @see JXG.Board#mode 5708 */ 5709 __evt__touchmove: function (e, mode) { }, 5710 5711 /** 5712 * @event 5713 * @description Whenever an element is highlighted this event is fired. 5714 * @name JXG.Board#hit 5715 * @param {Event} e The browser's event object. 5716 * @param {JXG.GeometryElement} el The hit element. 5717 * @param target 5718 * 5719 * @example 5720 * var c = board.create('circle', [[1, 1], 2]); 5721 * board.on('hit', function(evt, el) { 5722 * console.log("Hit element", el); 5723 * }); 5724 * 5725 * </pre><div id="JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div> 5726 * <script type="text/javascript"> 5727 * (function() { 5728 * var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723', 5729 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 5730 * var c = board.create('circle', [[1, 1], 2]); 5731 * board.on('hit', function(evt, el) { 5732 * console.log("Hit element", el); 5733 * }); 5734 * 5735 * })(); 5736 * 5737 * </script><pre> 5738 */ 5739 __evt__hit: function (e, el, target) { }, 5740 5741 /** 5742 * @event 5743 * @description Whenever an element is highlighted this event is fired. 5744 * @name JXG.Board#mousehit 5745 * @see JXG.Board#hit 5746 * @param {Event} e The browser's event object. 5747 * @param {JXG.GeometryElement} el The hit element. 5748 * @param target 5749 */ 5750 __evt__mousehit: function (e, el, target) { }, 5751 5752 /** 5753 * @event 5754 * @description This board is updated. 5755 * @name JXG.Board#update 5756 */ 5757 __evt__update: function () { }, 5758 5759 /** 5760 * @event 5761 * @description The bounding box of the board has changed. 5762 * @name JXG.Board#boundingbox 5763 */ 5764 __evt__boundingbox: function () { }, 5765 5766 /** 5767 * @event 5768 * @description Select a region is started during a down event or by calling 5769 * {@link JXG.Board.startSelectionMode} 5770 * @name JXG.Board#startselecting 5771 */ 5772 __evt__startselecting: function () { }, 5773 5774 /** 5775 * @event 5776 * @description Select a region is started during a down event 5777 * from a device sending mouse events or by calling 5778 * {@link JXG.Board.startSelectionMode}. 5779 * @name JXG.Board#mousestartselecting 5780 */ 5781 __evt__mousestartselecting: function () { }, 5782 5783 /** 5784 * @event 5785 * @description Select a region is started during a down event 5786 * from a device sending pointer events or by calling 5787 * {@link JXG.Board.startSelectionMode}. 5788 * @name JXG.Board#pointerstartselecting 5789 */ 5790 __evt__pointerstartselecting: function () { }, 5791 5792 /** 5793 * @event 5794 * @description Select a region is started during a down event 5795 * from a device sending touch events or by calling 5796 * {@link JXG.Board.startSelectionMode}. 5797 * @name JXG.Board#touchstartselecting 5798 */ 5799 __evt__touchstartselecting: function () { }, 5800 5801 /** 5802 * @event 5803 * @description Selection of a region is stopped during an up event. 5804 * @name JXG.Board#stopselecting 5805 */ 5806 __evt__stopselecting: function () { }, 5807 5808 /** 5809 * @event 5810 * @description Selection of a region is stopped during an up event 5811 * from a device sending mouse events. 5812 * @name JXG.Board#mousestopselecting 5813 */ 5814 __evt__mousestopselecting: function () { }, 5815 5816 /** 5817 * @event 5818 * @description Selection of a region is stopped during an up event 5819 * from a device sending pointer events. 5820 * @name JXG.Board#pointerstopselecting 5821 */ 5822 __evt__pointerstopselecting: function () { }, 5823 5824 /** 5825 * @event 5826 * @description Selection of a region is stopped during an up event 5827 * from a device sending touch events. 5828 * @name JXG.Board#touchstopselecting 5829 */ 5830 __evt__touchstopselecting: function () { }, 5831 5832 /** 5833 * @event 5834 * @description A move event while selecting of a region is active. 5835 * @name JXG.Board#moveselecting 5836 */ 5837 __evt__moveselecting: function () { }, 5838 5839 /** 5840 * @event 5841 * @description A move event while selecting of a region is active 5842 * from a device sending mouse events. 5843 * @name JXG.Board#mousemoveselecting 5844 */ 5845 __evt__mousemoveselecting: function () { }, 5846 5847 /** 5848 * @event 5849 * @description Select a region is started during a down event 5850 * from a device sending mouse events. 5851 * @name JXG.Board#pointermoveselecting 5852 */ 5853 __evt__pointermoveselecting: function () { }, 5854 5855 /** 5856 * @event 5857 * @description Select a region is started during a down event 5858 * from a device sending touch events. 5859 * @name JXG.Board#touchmoveselecting 5860 */ 5861 __evt__touchmoveselecting: function () { }, 5862 5863 /** 5864 * @ignore 5865 */ 5866 __evt: function () {}, 5867 5868 //endregion 5869 5870 /** 5871 * Expand the JSXGraph construction to fullscreen. 5872 * In order to preserve the proportions of the JSXGraph element, 5873 * a wrapper div is created which is set to fullscreen. 5874 * <p> 5875 * The wrapping div has the CSS class 'jxgbox_wrap_private' which is 5876 * defined in the file 'jsxgraph.css' 5877 * <p> 5878 * This feature is not available on iPhones (as of December 2021). 5879 * 5880 * @param {String} id (Optional) id of the div element which is brought to fullscreen. 5881 * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick 5882 * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied. 5883 * 5884 * @return {JXG.Board} Reference to the board 5885 * 5886 * @example 5887 * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div> 5888 * <button onClick="board.toFullscreen()">Fullscreen</button> 5889 * 5890 * <script language="Javascript" type='text/javascript'> 5891 * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]}); 5892 * var p = board.create('point', [0, 1]); 5893 * </script> 5894 * 5895 * </pre><div id="JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div> 5896 * <script type="text/javascript"> 5897 * var board_d5bab8b6; 5898 * (function() { 5899 * var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723', 5900 * {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false}); 5901 * var p = board.create('point', [0, 1]); 5902 * board_d5bab8b6 = board; 5903 * })(); 5904 * </script> 5905 * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button> 5906 * <pre> 5907 * 5908 * @example 5909 * <div id='outer' style='max-width: 500px; margin: 0 auto;'> 5910 * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div> 5911 * </div> 5912 * <button onClick="board.toFullscreen('outer')">Fullscreen</button> 5913 * 5914 * <script language="Javascript" type='text/javascript'> 5915 * var board = JXG.JSXGraph.initBoard('jxgbox', { 5916 * axis:true, 5917 * boundingbox:[-5,5,5,-5], 5918 * fullscreen: { id: 'outer' }, 5919 * showFullscreen: true 5920 * }); 5921 * var p = board.create('point', [-2, 3], {}); 5922 * </script> 5923 * 5924 * </pre><div id="JXG7103f6b_outer" style='max-width: 500px; margin: 0 auto;'> 5925 * <div id="JXG7103f6be-6993-4ff8-8133-c78e50a8afac" class="jxgbox" style="height: 0; padding-bottom: 100%;"></div> 5926 * </div> 5927 * <button onClick="board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')">Fullscreen</button> 5928 * <script type="text/javascript"> 5929 * var board_JXG7103f6be; 5930 * (function() { 5931 * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac', 5932 * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true, 5933 * showcopyright: false, shownavigation: false}); 5934 * var p = board.create('point', [-2, 3], {}); 5935 * board_JXG7103f6be = board; 5936 * })(); 5937 * 5938 * </script><pre> 5939 * 5940 * 5941 */ 5942 toFullscreen: function (id) { 5943 var wrap_id, wrap_node, inner_node; 5944 5945 id = id || this.container; 5946 this._fullscreen_inner_id = id; 5947 inner_node = document.getElementById(id); 5948 wrap_id = 'fullscreenwrap_' + id; 5949 5950 // Wrap a div around the JSXGraph div. 5951 if (this.document.getElementById(wrap_id)) { 5952 wrap_node = this.document.getElementById(wrap_id); 5953 } else { 5954 wrap_node = document.createElement('div'); 5955 wrap_node.classList.add('JXG_wrap_private'); 5956 wrap_node.setAttribute('id', wrap_id); 5957 inner_node.parentNode.insertBefore(wrap_node, inner_node); 5958 wrap_node.appendChild(inner_node); 5959 } 5960 5961 // Get the real width and height of the JSXGraph div 5962 // and determine the scaling and vertical shift amount 5963 this._fullscreen_res = Env._getScaleFactors(inner_node); 5964 5965 // Trigger fullscreen mode 5966 wrap_node.requestFullscreen = wrap_node.requestFullscreen || 5967 wrap_node.webkitRequestFullscreen || 5968 wrap_node.mozRequestFullScreen || 5969 wrap_node.msRequestFullscreen; 5970 5971 if (wrap_node.requestFullscreen) { 5972 wrap_node.requestFullscreen(); 5973 } 5974 5975 return this; 5976 }, 5977 5978 /** 5979 * If fullscreen mode is toggled, the possible CSS transformations 5980 * which are applied to the JSXGraph canvas have to be reread. 5981 * Otherwise the position of upper left corner is wrongly interpreted. 5982 * 5983 * @param {Object} evt fullscreen event object (unused) 5984 */ 5985 fullscreenListener: function (evt) { 5986 var res, inner_id, inner_node; 5987 5988 inner_id = this._fullscreen_inner_id; 5989 if (!Type.exists(inner_id)) { 5990 return; 5991 } 5992 5993 document.fullscreenElement = document.fullscreenElement || 5994 document.webkitFullscreenElement || 5995 document.mozFullscreenElement || 5996 document.msFullscreenElement; 5997 5998 inner_node = document.getElementById(inner_id); 5999 // If full screen mode is started we have to remove CSS margin around the JSXGraph div. 6000 // Otherwise, the positioning of the fullscreen div will be false. 6001 // When leaving the fullscreen mode, the margin is put back in. 6002 if (document.fullscreenElement) { 6003 // Just entered fullscreen mode 6004 6005 // Get the data computed in board.toFullscreen() 6006 res = this._fullscreen_res; 6007 6008 // Store the scaling data. 6009 // It is used in AbstractRenderer.updateText to restore the scaling matrix 6010 // which is removed by MathJax. 6011 // Further, the CSS margin has to be removed when in fullscreen mode, 6012 // and must be restored later. 6013 inner_node._cssFullscreenStore = { 6014 id: document.fullscreenElement.id, 6015 isFullscreen: true, 6016 margin: inner_node.style.margin, 6017 width: inner_node.style.width, 6018 scale: res.scale, 6019 vshift: res.vshift 6020 }; 6021 6022 inner_node.style.margin = ''; 6023 inner_node.style.width = res.width + 'px'; 6024 6025 // Do the shifting and scaling via CSS pseudo rules 6026 // We do this after fullscreen mode has been established to get the correct size 6027 // of the JSXGraph div. 6028 Env.scaleJSXGraphDiv(document.fullscreenElement.id, inner_id, res.scale, res.vshift); 6029 6030 // Clear document.fullscreenElement, because Safari doesn't to it and 6031 // when leaving full screen mode it is still set. 6032 document.fullscreenElement = null; 6033 6034 } else if (Type.exists(inner_node._cssFullscreenStore)) { 6035 // Just left the fullscreen mode 6036 6037 // Remove the CSS rules added in Env.scaleJSXGraphDiv 6038 try { 6039 document.styleSheets[document.styleSheets.length - 1].deleteRule(0); 6040 } catch (err) { 6041 console.log('JSXGraph: Could not remove CSS rules for full screen mode'); 6042 } 6043 6044 inner_node._cssFullscreenStore.isFullscreen = false; 6045 inner_node.style.margin = inner_node._cssFullscreenStore.margin; 6046 inner_node.style.width = inner_node._cssFullscreenStore.width; 6047 6048 } 6049 6050 this.updateCSSTransforms(); 6051 }, 6052 6053 /** 6054 * Function to animate a curve rolling on another curve. 6055 * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls 6056 * @param {Curve} c2 JSXGraph curve which rolls on c1. 6057 * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the 6058 * rolling process 6059 * @param {Number} stepsize Increase in t in each step for the curve c1 6060 * @param {Number} direction 6061 * @param {Number} time Delay time for setInterval() 6062 * @param {Array} pointlist Array of points which are rolled in each step. This list should contain 6063 * all points which define c2 and gliders on c2. 6064 * 6065 * @example 6066 * 6067 * // Line which will be the floor to roll upon. 6068 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 6069 * // Center of the rolling circle 6070 * var C = brd.create('point',[0,2],{name:'C'}); 6071 * // Starting point of the rolling circle 6072 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 6073 * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P 6074 * var circle = brd.create('curve',[ 6075 * function (t){var d = P.Dist(C), 6076 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 6077 * t += beta; 6078 * return C.X()+d*Math.cos(t); 6079 * }, 6080 * function (t){var d = P.Dist(C), 6081 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 6082 * t += beta; 6083 * return C.Y()+d*Math.sin(t); 6084 * }, 6085 * 0,2*Math.PI], 6086 * {strokeWidth:6, strokeColor:'green'}); 6087 * 6088 * // Point on circle 6089 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 6090 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 6091 * roll.start() // Start the rolling, to be stopped by roll.stop() 6092 * 6093 * </pre><div class="jxgbox" id="JXGe5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div> 6094 * <script type="text/javascript"> 6095 * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false}); 6096 * // Line which will be the floor to roll upon. 6097 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 6098 * // Center of the rolling circle 6099 * var C = brd.create('point',[0,2],{name:'C'}); 6100 * // Starting point of the rolling circle 6101 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 6102 * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P 6103 * var circle = brd.create('curve',[ 6104 * function (t){var d = P.Dist(C), 6105 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 6106 * t += beta; 6107 * return C.X()+d*Math.cos(t); 6108 * }, 6109 * function (t){var d = P.Dist(C), 6110 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 6111 * t += beta; 6112 * return C.Y()+d*Math.sin(t); 6113 * }, 6114 * 0,2*Math.PI], 6115 * {strokeWidth:6, strokeColor:'green'}); 6116 * 6117 * // Point on circle 6118 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 6119 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 6120 * roll.start() // Start the rolling, to be stopped by roll.stop() 6121 * </script><pre> 6122 */ 6123 createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) { 6124 var brd = this, 6125 Roulette = function () { 6126 var alpha = 0, Tx = 0, Ty = 0, 6127 t1 = start_c1, 6128 t2 = Numerics.root( 6129 function (t) { 6130 var c1x = c1.X(t1), 6131 c1y = c1.Y(t1), 6132 c2x = c2.X(t), 6133 c2y = c2.Y(t); 6134 6135 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y); 6136 }, 6137 [0, Math.PI * 2] 6138 ), 6139 t1_new = 0.0, t2_new = 0.0, 6140 c1dist, 6141 6142 rotation = brd.create('transform', [ 6143 function () { 6144 return alpha; 6145 } 6146 ], {type: 'rotate'}), 6147 6148 rotationLocal = brd.create('transform', [ 6149 function () { 6150 return alpha; 6151 }, 6152 function () { 6153 return c1.X(t1); 6154 }, 6155 function () { 6156 return c1.Y(t1); 6157 } 6158 ], {type: 'rotate'}), 6159 6160 translate = brd.create('transform', [ 6161 function () { 6162 return Tx; 6163 }, 6164 function () { 6165 return Ty; 6166 } 6167 ], {type: 'translate'}), 6168 6169 // arc length via Simpson's rule. 6170 arclen = function (c, a, b) { 6171 var cpxa = Numerics.D(c.X)(a), 6172 cpya = Numerics.D(c.Y)(a), 6173 cpxb = Numerics.D(c.X)(b), 6174 cpyb = Numerics.D(c.Y)(b), 6175 cpxab = Numerics.D(c.X)((a + b) * 0.5), 6176 cpyab = Numerics.D(c.Y)((a + b) * 0.5), 6177 6178 fa = Math.sqrt(cpxa * cpxa + cpya * cpya), 6179 fb = Math.sqrt(cpxb * cpxb + cpyb * cpyb), 6180 fab = Math.sqrt(cpxab * cpxab + cpyab * cpyab); 6181 6182 return (fa + 4 * fab + fb) * (b - a) / 6; 6183 }, 6184 6185 exactDist = function (t) { 6186 return c1dist - arclen(c2, t2, t); 6187 }, 6188 6189 beta = Math.PI / 18, 6190 beta9 = beta * 9, 6191 interval = null; 6192 6193 this.rolling = function () { 6194 var h, g, hp, gp, z; 6195 6196 t1_new = t1 + direction * stepsize; 6197 6198 // arc length between c1(t1) and c1(t1_new) 6199 c1dist = arclen(c1, t1, t1_new); 6200 6201 // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist. 6202 t2_new = Numerics.root(exactDist, t2); 6203 6204 // c1(t) as complex number 6205 h = new Complex(c1.X(t1_new), c1.Y(t1_new)); 6206 6207 // c2(t) as complex number 6208 g = new Complex(c2.X(t2_new), c2.Y(t2_new)); 6209 6210 hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new)); 6211 gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new)); 6212 6213 // z is angle between the tangents of c1 at t1_new, and c2 at t2_new 6214 z = Complex.C.div(hp, gp); 6215 6216 alpha = Math.atan2(z.imaginary, z.real); 6217 // Normalizing the quotient 6218 z.div(Complex.C.abs(z)); 6219 z.mult(g); 6220 Tx = h.real - z.real; 6221 6222 // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new); 6223 Ty = h.imaginary - z.imaginary; 6224 6225 // -(10-90) degrees: make corners roll smoothly 6226 if (alpha < -beta && alpha > -beta9) { 6227 alpha = -beta; 6228 rotationLocal.applyOnce(pointlist); 6229 } else if (alpha > beta && alpha < beta9) { 6230 alpha = beta; 6231 rotationLocal.applyOnce(pointlist); 6232 } else { 6233 rotation.applyOnce(pointlist); 6234 translate.applyOnce(pointlist); 6235 t1 = t1_new; 6236 t2 = t2_new; 6237 } 6238 brd.update(); 6239 }; 6240 6241 this.start = function () { 6242 if (time > 0) { 6243 interval = window.setInterval(this.rolling, time); 6244 } 6245 return this; 6246 }; 6247 6248 this.stop = function () { 6249 window.clearInterval(interval); 6250 return this; 6251 }; 6252 return this; 6253 }; 6254 return new Roulette(); 6255 } 6256 }); 6257 6258 return JXG.Board; 6259 }); 6260