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