jsxgraph 1.3.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +2 -2
  2. package/distrib/README.md +2 -2
  3. package/distrib/geogebra.min.js +3 -3
  4. package/distrib/geonext.min.js +3 -3
  5. package/distrib/index.d.ts +4964 -4627
  6. package/distrib/intergeo.min.js +3 -3
  7. package/distrib/jsxgraph.css +10 -15
  8. package/distrib/jsxgraphcore.js +9 -25
  9. package/distrib/jsxgraphsrc.js +28437 -25331
  10. package/distrib/sketch.min.js +3 -3
  11. package/package.json +11 -22
  12. package/src/base/board.js +555 -427
  13. package/src/base/chart.js +1 -1
  14. package/src/base/circle.js +23 -1
  15. package/src/base/composition.js +1 -1
  16. package/src/base/constants.js +33 -11
  17. package/src/base/coords.js +4 -4
  18. package/src/base/coordselement.js +47 -20
  19. package/src/base/curve.js +308 -117
  20. package/src/base/element.js +80 -51
  21. package/src/base/foreignobject.js +463 -0
  22. package/src/base/group.js +2 -2
  23. package/src/base/image.js +12 -15
  24. package/src/base/line.js +1 -1
  25. package/src/base/point.js +20 -41
  26. package/src/base/polygon.js +75 -18
  27. package/src/base/text.js +116 -63
  28. package/src/base/ticks.js +35 -12
  29. package/src/base/transformation.js +1 -1
  30. package/src/base/turtle.js +1 -1
  31. package/src/element/arc.js +1 -1
  32. package/src/element/button.js +29 -2
  33. package/src/element/checkbox.js +31 -1
  34. package/src/element/comb.js +5 -2
  35. package/src/element/composition.js +27 -6
  36. package/src/element/conic.js +46 -5
  37. package/src/element/input.js +32 -1
  38. package/src/element/locus.js +2 -2
  39. package/src/element/measure.js +9 -4
  40. package/src/element/sector.js +46 -12
  41. package/src/element/slider.js +17 -12
  42. package/src/element/slopetriangle.js +1 -1
  43. package/src/jsxgraph.js +28 -9
  44. package/src/jxg.js +4 -3
  45. package/src/loadjsxgraph.js +3 -2
  46. package/src/math/bst.js +1 -1
  47. package/src/math/clip.js +550 -345
  48. package/src/math/complex.js +1 -1
  49. package/src/math/extrapolate.js +1 -1
  50. package/src/math/geometry.js +109 -128
  51. package/src/math/ia.js +290 -21
  52. package/src/math/math.js +30 -1
  53. package/src/math/metapost.js +1 -1
  54. package/src/math/nlp.js +25 -5
  55. package/src/math/numerics.js +183 -4
  56. package/src/math/plot.js +4 -4
  57. package/src/math/poly.js +1 -1
  58. package/src/math/probfuncs.js +2 -1
  59. package/src/math/qdt.js +68 -62
  60. package/src/math/statistics.js +58 -7
  61. package/src/math/symbolic.js +1 -1
  62. package/src/options.js +393 -115
  63. package/src/parser/ca.js +1297 -1291
  64. package/src/parser/datasource.js +1 -1
  65. package/src/parser/geonext.js +6 -6
  66. package/src/parser/jessiecode.js +378 -79
  67. package/src/parser/jessiescript.js +1 -1
  68. package/src/reader/cinderella.js +1 -1
  69. package/src/reader/file.js +1 -1
  70. package/src/reader/geogebra.js +1 -1
  71. package/src/reader/geonext.js +1 -1
  72. package/src/reader/graph.js +1 -1
  73. package/src/reader/intergeo.js +1 -1
  74. package/src/reader/sketch.js +25 -17
  75. package/src/renderer/abstract.js +25 -7
  76. package/src/renderer/canvas.js +20 -15
  77. package/src/renderer/no.js +11 -4
  78. package/src/renderer/svg.js +68 -49
  79. package/src/renderer/vml.js +36 -32
  80. package/src/server/server.js +1 -1
  81. package/src/utils/base64.js +2 -2
  82. package/src/utils/color.js +147 -3
  83. package/src/utils/dump.js +20 -14
  84. package/src/utils/env.js +26 -62
  85. package/src/utils/event.js +1 -1
  86. package/src/utils/expect.js +1 -1
  87. package/src/utils/type.js +30 -19
  88. package/src/utils/xml.js +1 -1
  89. package/src/utils/zip.js +1 -1
  90. package/CHANGELOG.md +0 -1305
package/src/base/board.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2008-2021
2
+ Copyright 2008-2022
3
3
  Matthias Ehmann,
4
4
  Michael Gerhaeuser,
5
5
  Carsten Miller,
@@ -65,10 +65,10 @@
65
65
  define([
66
66
  'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex',
67
67
  'math/statistics',
68
- 'parser/jessiecode', 'parser/geonext', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 'base/transformation',
69
- 'base/point', 'base/line', 'base/text', 'element/composition', 'base/composition'
70
- ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, GeonextParser, Color, Type,
71
- EventEmitter, Env, Transform, Point, Line, Text, Composition, EComposition) {
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
72
 
73
73
  'use strict';
74
74
 
@@ -120,20 +120,20 @@ define([
120
120
  this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
121
121
 
122
122
  /**
123
- * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
123
+ * Update is made with high quality, e.g. graphs are evaluated at much more points.
124
124
  * @type Number
125
125
  * @constant
126
126
  * @see JXG.Board#updateQuality
127
127
  */
128
- this.BOARD_QUALITY_LOW = 0x1;
128
+ this.BOARD_MODE_ZOOM = 0x0011;
129
129
 
130
130
  /**
131
- * Update is made with high quality, e.g. graphs are evaluated at much more points.
131
+ * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
132
132
  * @type Number
133
133
  * @constant
134
134
  * @see JXG.Board#updateQuality
135
135
  */
136
- this.BOARD_QUALITY_HIGH = 0x2;
136
+ this.BOARD_QUALITY_LOW = 0x1;
137
137
 
138
138
  /**
139
139
  * Update is made with high quality, e.g. graphs are evaluated at much more points.
@@ -141,7 +141,7 @@ define([
141
141
  * @constant
142
142
  * @see JXG.Board#updateQuality
143
143
  */
144
- this.BOARD_MODE_ZOOM = 0x0011;
144
+ this.BOARD_QUALITY_HIGH = 0x2;
145
145
 
146
146
  /**
147
147
  * Pointer to the document element containing the board.
@@ -174,11 +174,16 @@ define([
174
174
  /**
175
175
  * A reference to this boards renderer.
176
176
  * @type JXG.AbstractRenderer
177
+ * @name JXG.Board#renderer
178
+ * @private
179
+ * @ignore
177
180
  */
178
181
  this.renderer = renderer;
179
182
 
180
183
  /**
181
184
  * Grids keeps track of all grids attached to this board.
185
+ * @type Array
186
+ * @private
182
187
  */
183
188
  this.grids = [];
184
189
 
@@ -204,6 +209,7 @@ define([
204
209
  * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
205
210
  * stores the boards origin in homogeneous screen coordinates.
206
211
  * @type Object
212
+ * @private
207
213
  */
208
214
  this.origin = {};
209
215
  this.origin.usrCoords = [1, 0, 0];
@@ -212,44 +218,56 @@ define([
212
218
  /**
213
219
  * Zoom factor in X direction. It only stores the zoom factor to be able
214
220
  * to get back to 100% in zoom100().
221
+ * @name JXG.Board.zoomX
215
222
  * @type Number
223
+ * @private
224
+ * @ignore
216
225
  */
217
226
  this.zoomX = zoomX;
218
227
 
219
228
  /**
220
229
  * Zoom factor in Y direction. It only stores the zoom factor to be able
221
230
  * to get back to 100% in zoom100().
231
+ * @name JXG.Board.zoomY
222
232
  * @type Number
233
+ * @private
234
+ * @ignore
223
235
  */
224
236
  this.zoomY = zoomY;
225
237
 
226
238
  /**
227
239
  * The number of pixels which represent one unit in user-coordinates in x direction.
228
240
  * @type Number
241
+ * @private
229
242
  */
230
243
  this.unitX = unitX * this.zoomX;
231
244
 
232
245
  /**
233
246
  * The number of pixels which represent one unit in user-coordinates in y direction.
234
247
  * @type Number
248
+ * @private
235
249
  */
236
250
  this.unitY = unitY * this.zoomY;
237
251
 
238
252
  /**
239
253
  * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
240
254
  * width/height ratio of the canvas.
255
+ * @type Boolean
256
+ * @private
241
257
  */
242
258
  this.keepaspectratio = false;
243
259
 
244
260
  /**
245
261
  * Canvas width.
246
262
  * @type Number
263
+ * @private
247
264
  */
248
265
  this.canvasWidth = canvasWidth;
249
266
 
250
267
  /**
251
268
  * Canvas Height
252
269
  * @type Number
270
+ * @private
253
271
  */
254
272
  this.canvasHeight = canvasHeight;
255
273
 
@@ -379,15 +397,15 @@ define([
379
397
 
380
398
  /**
381
399
  * References to the object that is dragged with the mouse on the board.
382
- * @type {@link JXG.GeometryElement}.
383
- * @see {JXG.Board#touches}
400
+ * @type JXG.GeometryElement
401
+ * @see JXG.Board#touches
384
402
  */
385
403
  this.mouse = {};
386
404
 
387
405
  /**
388
406
  * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
389
407
  * @type Array
390
- * @see {JXG.Board#mouse}
408
+ * @see JXG.Board#mouse
391
409
  */
392
410
  this.touches = [];
393
411
 
@@ -412,6 +430,13 @@ define([
412
430
  */
413
431
  this.touchMoveLast = 0;
414
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
+
415
440
  /**
416
441
  * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
417
442
  * @type Number
@@ -482,7 +507,7 @@ define([
482
507
 
483
508
  /**
484
509
  * A flag which stores if the board registered pointer events.
485
- * @type {Boolean}
510
+ * @type Boolean
486
511
  * @default false
487
512
  */
488
513
  this.hasPointerHandlers = false;
@@ -503,14 +528,14 @@ define([
503
528
 
504
529
  /**
505
530
  * A flag which tells us if the board has a pointerUp event registered at the moment.
506
- * @type {Boolean}
531
+ * @type Boolean
507
532
  * @default false
508
533
  */
509
534
  this.hasPointerUp = false;
510
535
 
511
536
  /**
512
537
  * Offset for large coords elements like images
513
- * @type {Array}
538
+ * @type Array
514
539
  * @private
515
540
  * @default [0, 0]
516
541
  */
@@ -518,31 +543,36 @@ define([
518
543
 
519
544
  /**
520
545
  * Stores the input device used in the last down or move event.
521
- * @type {String}
546
+ * @type String
522
547
  * @private
523
548
  * @default 'mouse'
524
549
  */
525
550
  this._inputDevice = 'mouse';
526
551
 
552
+ /**
553
+ * Keeps a list of pointer devices which are currently touching the screen.
554
+ * @type Array
555
+ * @private
556
+ */
527
557
  this._board_touches = [];
528
558
 
529
559
  /**
530
560
  * A flag which tells us if the board is in the selecting mode
531
- * @type {Boolean}
561
+ * @type Boolean
532
562
  * @default false
533
563
  */
534
564
  this.selectingMode = false;
535
565
 
536
566
  /**
537
567
  * A flag which tells us if the user is selecting
538
- * @type {Boolean}
568
+ * @type Boolean
539
569
  * @default false
540
570
  */
541
571
  this.isSelecting = false;
542
572
 
543
573
  /**
544
574
  * A flag which tells us if the user is scrolling the viewport
545
- * @type {Boolean}
575
+ * @type Boolean
546
576
  * @private
547
577
  * @default false
548
578
  * @see JXG.Board#scrollListener
@@ -551,7 +581,7 @@ define([
551
581
 
552
582
  /**
553
583
  * A flag which tells us if a resize is in process
554
- * @type {Boolean}
584
+ * @type Boolean
555
585
  * @private
556
586
  * @default false
557
587
  * @see JXG.Board#resizeListener
@@ -560,7 +590,7 @@ define([
560
590
 
561
591
  /**
562
592
  * A bounding box for the selection
563
- * @type {Array}
593
+ * @type Array
564
594
  * @default [ [0,0], [0,0] ]
565
595
  */
566
596
  this.selectingBox = [[0, 0], [0, 0]];
@@ -764,17 +794,20 @@ define([
764
794
  * @see JXG.Board#mouseMoveListener
765
795
  */
766
796
  checkFrameRate: function(evt) {
767
- var time = new Date().getTime();
768
-
769
- if ((time - this.touchMoveLast) * this.attr.maxframerate < 1000) {
770
- // this.updateQuality = this.BOARD_QUALITY_HIGH;
771
- // this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
797
+ var handleEvt = false,
798
+ time = new Date().getTime();
772
799
 
773
- return false;
800
+ if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
801
+ handleEvt = true;
802
+ this.touchMoveLastId = evt.pointerId;
774
803
  }
775
-
776
- this.touchMoveLast = time;
777
- return true;
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;
778
811
  },
779
812
 
780
813
  /**
@@ -903,7 +936,7 @@ define([
903
936
  absPos,
904
937
  v;
905
938
 
906
- // position of mouse cursor relative to containers position of container
939
+ // Position of cursor using clientX/Y
907
940
  absPos = Env.getPosition(e, i, this.document);
908
941
 
909
942
  /**
@@ -912,6 +945,7 @@ define([
912
945
  if (!Type.exists(this.cssTransMat)) {
913
946
  this.updateCSSTransforms();
914
947
  }
948
+ // Position relative to the top left corner
915
949
  v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
916
950
  v = Mat.matVecMult(this.cssTransMat, v);
917
951
  v[1] /= v[0];
@@ -1129,8 +1163,8 @@ define([
1129
1163
  * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
1130
1164
  * @param {Object} evt The event object that lead to this movement.
1131
1165
  */
1132
- twoFingerMove: function (p1, p2, o, evt) {
1133
- var np1c, np2c, drag;
1166
+ twoFingerMove: function (o, id, evt) {
1167
+ var drag;
1134
1168
 
1135
1169
  if (Type.exists(o) && Type.exists(o.obj)) {
1136
1170
  drag = o.obj;
@@ -1138,60 +1172,51 @@ define([
1138
1172
  return;
1139
1173
  }
1140
1174
 
1141
- // New finger position
1142
- np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this);
1143
- np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this);
1144
-
1145
1175
  if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
1146
- drag.type === Const.OBJECT_TYPE_POLYGON) {
1147
- this.twoFingerTouchObject(np1c, np2c, o, drag, evt);
1176
+ drag.type === Const.OBJECT_TYPE_POLYGON) {
1177
+ this.twoFingerTouchObject(o.targets, drag, id);
1148
1178
  } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1149
- this.twoFingerTouchCircle(np1c, np2c, o, drag);
1179
+ this.twoFingerTouchCircle(o.targets, drag, id);
1150
1180
  }
1151
1181
 
1152
- drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1153
-
1154
- o.targets[0].Xprev = np1c.scrCoords[1];
1155
- o.targets[0].Yprev = np1c.scrCoords[2];
1156
- o.targets[1].Xprev = np2c.scrCoords[1];
1157
- o.targets[1].Yprev = np2c.scrCoords[2];
1182
+ if (evt) {
1183
+ drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1184
+ }
1158
1185
  },
1159
1186
 
1160
1187
  /**
1161
1188
  * Moves, rotates and scales a line or polygon with two fingers.
1162
- * @param {JXG.Coords} np1c x,y coordinates of first touch
1163
- * @param {JXG.Coords} np2c x,y coordinates of second touch
1164
- * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1189
+ * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1165
1190
  * @param {object} drag The object that is dragged:
1166
- * @param {Object} evt The event object that lead to this movement.
1191
+ * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1167
1192
  */
1168
- twoFingerTouchObject: function (np1c, np2c, o, drag, evt) {
1169
- var np1, np2, op1, op2,
1170
- nmid, omid, nd, od,
1193
+ twoFingerTouchObject: function (tar, drag, id) {
1194
+ var np, op, nd, od,
1171
1195
  d, alpha,
1172
- S, t1, t2, t3, t4, t5,
1196
+ S, t1, t3, t4, t5,
1173
1197
  ar, i, len,
1174
- pi180 = 0.017453292519943295; // Math.PI / 180.0
1175
-
1176
- if (Type.exists(o.targets[0]) &&
1177
- Type.exists(o.targets[1]) &&
1178
- !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1198
+ fixEl, moveEl, fix;
1179
1199
 
1180
- // Actual fingers' position
1181
- np1 = np1c.usrCoords;
1182
- np2 = np2c.usrCoords;
1200
+ if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
1201
+ !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
1183
1202
 
1184
- // Previous fingers' position
1185
- op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1186
- op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1203
+ if (id === tar[0].num) {
1204
+ fixEl = tar[1];
1205
+ moveEl = tar[0];
1206
+ } else {
1207
+ fixEl = tar[0];
1208
+ moveEl = tar[1];
1209
+ }
1187
1210
 
1188
- // Affine mid points of the old and new positions
1189
- omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5];
1190
- nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5];
1211
+ fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
1212
+ // Previous finger position
1213
+ op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
1214
+ // New finger position
1215
+ np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
1191
1216
 
1192
1217
  // Old and new directions
1193
- od = Mat.crossProduct(op1, op2);
1194
- nd = Mat.crossProduct(np1, np2);
1218
+ od = Mat.crossProduct(fix, op);
1219
+ nd = Mat.crossProduct(fix, np);
1195
1220
 
1196
1221
  // Intersection between the two directions
1197
1222
  S = Mat.crossProduct(od, nd);
@@ -1201,45 +1226,18 @@ define([
1201
1226
  return;
1202
1227
  }
1203
1228
 
1204
- // Normalize the coordinates
1205
- S[1] /= S[0];
1206
- S[2] /= S[0];
1229
+ alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1207
1230
 
1208
- if (Type.exists(evt.rotation) && evt.type !== 'pointermove') {
1209
- // iOS touch events contain the angle for free
1210
- alpha = evt.rotation - this.previousRotation;
1211
- this.previousRotation = evt.rotation;
1212
- alpha *= -pi180;
1213
- } else {
1214
- // For pointer events, the rotation angle has to be calculated.
1215
- alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1));
1216
- }
1217
-
1218
- t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'});
1219
-
1220
- // Old midpoint of fingers after first transformation:
1231
+ t1 = this.create('transform', [alpha, [fix[1], fix[2]]], {type: 'rotate'});
1221
1232
  t1.update();
1222
- omid = Mat.matVecMult(t1.matrix, omid);
1223
- omid[1] /= omid[0];
1224
- omid[2] /= omid[0];
1225
1233
 
1226
- // Shift to the new mid point
1227
- t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'});
1228
- t2.update();
1229
-
1230
- t1.melt(t2);
1231
1234
  if (Type.evaluate(drag.visProp.scalable)) {
1232
1235
  // Scale
1233
- if (Type.exists(evt.scale)) {
1234
- d = evt.scale / this.previousScale;
1235
- this.previousScale = evt.scale;
1236
- } else {
1237
- d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1238
- }
1236
+ d = Geometry.distance(np, fix) / Geometry.distance(op, fix);
1239
1237
 
1240
- t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'});
1238
+ t3 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
1241
1239
  t4 = this.create('transform', [d, d], {type: 'scale'});
1242
- t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'});
1240
+ t5 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
1243
1241
  t1.melt(t3).melt(t4).melt(t5);
1244
1242
  }
1245
1243
 
@@ -1270,46 +1268,48 @@ define([
1270
1268
 
1271
1269
  /*
1272
1270
  * Moves, rotates and scales a circle with two fingers.
1273
- * @param {JXG.Coords} np1c x,y coordinates of first touch
1274
- * @param {JXG.Coords} np2c x,y coordinates of second touch
1275
- * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1271
+ * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1276
1272
  * @param {object} drag The object that is dragged:
1273
+ * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1277
1274
  */
1278
- twoFingerTouchCircle: function (np1c, np2c, o, drag) {
1279
- var np1, np2, op1, op2,
1280
- d, alpha, t1, t2, t3, t4, t5;
1275
+ twoFingerTouchCircle: function (tar, drag, id) {
1276
+ var fixEl, moveEl, np, op, fix,
1277
+ d, alpha, t1, t2, t3, t4;
1281
1278
 
1282
- if (drag.method === 'pointCircle' ||
1283
- drag.method === 'pointLine') {
1279
+ if (drag.method === 'pointCircle' || drag.method === 'pointLine') {
1284
1280
  return;
1285
1281
  }
1286
1282
 
1287
- if (Type.exists(o.targets[0]) &&
1288
- Type.exists(o.targets[1]) &&
1289
- !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1283
+ if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
1284
+ !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
1290
1285
 
1291
- np1 = np1c.usrCoords;
1292
- np2 = np2c.usrCoords;
1286
+ if (id === tar[0].num) {
1287
+ fixEl = tar[1];
1288
+ moveEl = tar[0];
1289
+ } else {
1290
+ fixEl = tar[0];
1291
+ moveEl = tar[1];
1292
+ }
1293
+
1294
+ fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
1293
1295
  // Previous finger position
1294
- op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1295
- op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1296
+ op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
1297
+ // New finger position
1298
+ np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
1296
1299
 
1297
- // Shift by the movement of the first finger
1298
- t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'});
1299
- alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1));
1300
+ alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1300
1301
 
1301
1302
  // Rotate and scale by the movement of the second finger
1302
- t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'});
1303
- t3 = this.create('transform', [alpha], {type: 'rotate'});
1304
- t1.melt(t2).melt(t3);
1305
-
1303
+ t1 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
1304
+ t2 = this.create('transform', [alpha], {type: 'rotate'});
1305
+ t1.melt(t2);
1306
1306
  if (Type.evaluate(drag.visProp.scalable)) {
1307
- d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1308
- t4 = this.create('transform', [d, d], {type: 'scale'});
1309
- t1.melt(t4);
1307
+ d = Geometry.distance(fix, np) / Geometry.distance(fix, op);
1308
+ t3 = this.create('transform', [d, d], {type: 'scale'});
1309
+ t1.melt(t3);
1310
1310
  }
1311
- t5 = this.create('transform', [np1[1], np1[2]], {type: 'translate'});
1312
- t1.melt(t5);
1311
+ t4 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
1312
+ t1.melt(t4);
1313
1313
 
1314
1314
  if (drag.center.draggable()) {
1315
1315
  t1.applyOnce([drag.center]);
@@ -1664,7 +1664,7 @@ define([
1664
1664
  },
1665
1665
 
1666
1666
  addKeyboardEventHandlers: function() {
1667
- if (!this.hasKeyboardHandlers && Env.isBrowser) {
1667
+ if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) {
1668
1668
  Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this);
1669
1669
  Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
1670
1670
  Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
@@ -1982,7 +1982,7 @@ define([
1982
1982
 
1983
1983
  /**
1984
1984
  *
1985
- * Check if pointer event is already registered in this._board_touches.
1985
+ * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
1986
1986
  *
1987
1987
  * @param {Object} evt Event object
1988
1988
  * @return {Boolean} true if down event has already been sent.
@@ -2001,14 +2001,16 @@ define([
2001
2001
 
2002
2002
  /**
2003
2003
  *
2004
- * Registers a pointer event in this._board_touches.
2004
+ * Store the position of a pointer event.
2005
+ * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
2005
2006
  * Allows to follow the path of that finger on the screen.
2007
+ * Only two simultaneous touches are supported.
2006
2008
  *
2007
2009
  * @param {Object} evt Event object
2008
2010
  * @returns {JXG.Board} Reference to the board
2009
2011
  * @private
2010
2012
  */
2011
- _pointerAddBoardTouches: function (evt) {
2013
+ _pointerStorePosition: function (evt) {
2012
2014
  var i, found;
2013
2015
 
2014
2016
  for (i = 0, found = false; i < this._board_touches.length; i++) {
@@ -2020,7 +2022,8 @@ define([
2020
2022
  }
2021
2023
  }
2022
2024
 
2023
- if (!found) {
2025
+ // Restrict the number of simultaneous touches to 2
2026
+ if (!found && this._board_touches.length < 2) {
2024
2027
  this._board_touches.push({
2025
2028
  pointerId: evt.pointerId,
2026
2029
  clientX: evt.clientX,
@@ -2032,14 +2035,14 @@ define([
2032
2035
  },
2033
2036
 
2034
2037
  /**
2035
- * Deregisters a pointer event in this._board_touches.
2036
- * The finger has been lifted from the screen.
2038
+ * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
2039
+ * It happens if a finger has been lifted from the screen.
2037
2040
  *
2038
- * @param {Object}} evt Event object
2041
+ * @param {Object} evt Event object
2039
2042
  * @returns {JXG.Board} Reference to the board
2040
2043
  * @private
2041
2044
  */
2042
- _pointerRemoveBoardTouches: function (evt) {
2045
+ _pointerRemoveTouches: function (evt) {
2043
2046
  var i;
2044
2047
  for (i = 0; i < this._board_touches.length; i++) {
2045
2048
  if (this._board_touches[i].pointerId === evt.pointerId) {
@@ -2052,7 +2055,7 @@ define([
2052
2055
  },
2053
2056
 
2054
2057
  /**
2055
- * Remove all registered fingers from this._board_touches.
2058
+ * Remove all registered fingers from {@link JXG.Board#_board_touches}.
2056
2059
  * This might be necessary if too many fingers have been registered.
2057
2060
  * @returns {JXG.Board} Reference to the board
2058
2061
  * @private
@@ -2060,10 +2063,11 @@ define([
2060
2063
  _pointerClearTouches: function() {
2061
2064
  if (this._board_touches.length > 0) {
2062
2065
  this.dehighlightAll();
2063
- this.updateQuality = this.BOARD_QUALITY_HIGH;
2064
- this.mode = this.BOARD_MODE_NONE;
2065
2066
  }
2067
+ this.updateQuality = this.BOARD_QUALITY_HIGH;
2068
+ this.mode = this.BOARD_MODE_NONE;
2066
2069
  this._board_touches = [];
2070
+ this.touches = [];
2067
2071
  },
2068
2072
 
2069
2073
  /**
@@ -2106,6 +2110,7 @@ define([
2106
2110
  */
2107
2111
  pointerDownListener: function (evt, object) {
2108
2112
  var i, j, k, pos, elements, sel,
2113
+ target_obj,
2109
2114
  type = 'mouse', // Used in case of no browser
2110
2115
  found, target;
2111
2116
 
@@ -2118,7 +2123,7 @@ define([
2118
2123
  if (!object && evt.isPrimary) {
2119
2124
  // First finger down. To be on the safe side this._board_touches is cleared.
2120
2125
  this._pointerClearTouches();
2121
- }
2126
+ }
2122
2127
 
2123
2128
  if (!this.hasPointerUp) {
2124
2129
  if (window.navigator.msPointerEnabled) { // IE10-
@@ -2156,14 +2161,14 @@ define([
2156
2161
  type = this._inputDevice;
2157
2162
  this.options.precision.hasPoint = this.options.precision[type];
2158
2163
 
2159
- // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse
2160
- // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will
2164
+ // Handling of multi touch with pointer events should be easier than the touch events.
2165
+ // Every pointer device has its own pointerId, e.g. the mouse
2166
+ // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
2161
2167
  // keep this id until a pointerUp event is fired. What we have to do here is:
2162
2168
  // 1. collect all elements under the current pointer
2163
2169
  // 2. run through the touches control structure
2164
2170
  // a. look for the object collected in step 1.
2165
2171
  // b. if an object is found, check the number of pointers. If appropriate, add the pointer.
2166
-
2167
2172
  pos = this.getMousePosition(evt);
2168
2173
 
2169
2174
  // selection
@@ -2181,45 +2186,41 @@ define([
2181
2186
  elements = this.initMoveObject(pos[0], pos[1], evt, type);
2182
2187
  }
2183
2188
 
2189
+ target_obj = {
2190
+ num: evt.pointerId,
2191
+ X: pos[0],
2192
+ Y: pos[1],
2193
+ Xprev: NaN,
2194
+ Yprev: NaN,
2195
+ Xstart: [],
2196
+ Ystart: [],
2197
+ Zstart: []
2198
+ };
2199
+
2184
2200
  // If no draggable object can be found, get out here immediately
2185
2201
  if (elements.length > 0) {
2186
2202
  // check touches structure
2187
2203
  target = elements[elements.length - 1];
2188
2204
  found = false;
2205
+
2206
+ // Reminder: this.touches is the list of elements which
2207
+ // currently "possess" a pointer (mouse, pen, finger)
2189
2208
  for (i = 0; i < this.touches.length; i++) {
2190
- // the target is already in our touches array, try to add the pointer to the existing touch
2209
+ // An element receives a further touch, i.e.
2210
+ // the target is already in our touches array, add the pointer to the existing touch
2191
2211
  if (this.touches[i].obj === target) {
2192
2212
  j = i;
2193
- k = this.touches[i].targets.push({
2194
- num: evt.pointerId,
2195
- X: pos[0],
2196
- Y: pos[1],
2197
- Xprev: NaN,
2198
- Yprev: NaN,
2199
- Xstart: [],
2200
- Ystart: [],
2201
- Zstart: []
2202
- }) - 1;
2203
-
2213
+ k = this.touches[i].targets.push(target_obj) - 1;
2204
2214
  found = true;
2205
2215
  break;
2206
2216
  }
2207
2217
  }
2208
-
2209
2218
  if (!found) {
2219
+ // An new element hae been touched.
2210
2220
  k = 0;
2211
2221
  j = this.touches.push({
2212
2222
  obj: target,
2213
- targets: [{
2214
- num: evt.pointerId,
2215
- X: pos[0],
2216
- Y: pos[1],
2217
- Xprev: NaN,
2218
- Yprev: NaN,
2219
- Xstart: [],
2220
- Ystart: [],
2221
- Zstart: []
2222
- }]
2223
+ targets: [target_obj]
2223
2224
  }) - 1;
2224
2225
  }
2225
2226
 
@@ -2243,18 +2244,24 @@ define([
2243
2244
  evt.stopPropagation();
2244
2245
  }
2245
2246
 
2246
- if (Env.isBrowser && this._getPointerInputDevice(evt) !== 'touch') {
2247
+ if (!Env.isBrowser) {
2248
+ return false;
2249
+ }
2250
+ if (this._getPointerInputDevice(evt) !== 'touch') {
2247
2251
  if (this.mode === this.BOARD_MODE_NONE) {
2248
2252
  this.mouseOriginMoveStart(evt);
2249
2253
  }
2250
2254
  } else {
2251
- this._pointerAddBoardTouches(evt);
2255
+ this._pointerStorePosition(evt);
2252
2256
  evt.touches = this._board_touches;
2253
2257
 
2254
2258
  // Touch events on empty areas of the board are handled here, see also touchStartListener
2255
2259
  // 1. case: one finger. If allowed, this triggers pan with one finger
2256
- if (evt.touches.length == 1 && this.mode === this.BOARD_MODE_NONE && this.touchStartMoveOriginOneFinger(evt)) {
2257
- } else if (evt.touches.length == 2 &&
2260
+ if (evt.touches.length === 1 &&
2261
+ this.mode === this.BOARD_MODE_NONE &&
2262
+ this.touchStartMoveOriginOneFinger(evt)) {
2263
+ // Empty by purpose
2264
+ } else if (evt.touches.length === 2 &&
2258
2265
  (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2259
2266
  ) {
2260
2267
  // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
@@ -2293,7 +2300,7 @@ define([
2293
2300
  * @returns {Boolean}
2294
2301
  */
2295
2302
  pointerMoveListener: function (evt) {
2296
- var i, j, pos,
2303
+ var i, j, pos, touchTargets,
2297
2304
  type = 'mouse'; // in case of no browser
2298
2305
 
2299
2306
  if (this._getPointerInputDevice(evt) === 'touch' && !this._isPointerRegistered(evt)) {
@@ -2330,45 +2337,37 @@ define([
2330
2337
  this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]);
2331
2338
  } else if (!this.mouseOriginMove(evt)) {
2332
2339
  if (this.mode === this.BOARD_MODE_DRAG) {
2333
- // Runs through all jsxgraph elements which are touched by at least one finger.
2340
+ // Run through all jsxgraph elements which are touched by at least one finger.
2334
2341
  for (i = 0; i < this.touches.length; i++) {
2342
+ touchTargets = this.touches[i].targets;
2335
2343
  // Run through all touch events which have been started on this jsxgraph element.
2336
- for (j = 0; j < this.touches[i].targets.length; j++) {
2337
- if (this.touches[i].targets[j].num === evt.pointerId) {
2338
- if (this.touches[i].targets.length === 1) {
2339
-
2340
- // Touch by one finger: this is possible for all elements that can be dragged
2341
- this.touches[i].targets[j].X = evt.pageX;
2342
- this.touches[i].targets[j].Y = evt.pageY;
2343
- pos = this.getMousePosition(evt);
2344
- this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
2344
+ for (j = 0; j < touchTargets.length; j++) {
2345
+ if (touchTargets[j].num === evt.pointerId) {
2345
2346
 
2346
- } else if (this.touches[i].targets.length === 2) {
2347
+ pos = this.getMousePosition(evt);
2348
+ touchTargets[j].X = pos[0];
2349
+ touchTargets[j].Y = pos[1];
2347
2350
 
2351
+ if (touchTargets.length === 1) {
2352
+ // Touch by one finger: this is possible for all elements that can be dragged
2353
+ this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
2354
+ } else if (touchTargets.length === 2) {
2348
2355
  // Touch by two fingers: e.g. moving lines
2349
- this.touches[i].targets[j].X = evt.pageX;
2350
- this.touches[i].targets[j].Y = evt.pageY;
2351
-
2352
- this.twoFingerMove(
2353
- this.getMousePosition({
2354
- clientX: this.touches[i].targets[0].X,
2355
- clientY: this.touches[i].targets[0].Y
2356
- }),
2357
- this.getMousePosition({
2358
- clientX: this.touches[i].targets[1].X,
2359
- clientY: this.touches[i].targets[1].Y
2360
- }),
2361
- this.touches[i], evt);
2356
+ this.twoFingerMove(this.touches[i], evt.pointerId, evt);
2357
+
2358
+ touchTargets[j].Xprev = pos[0];
2359
+ touchTargets[j].Yprev = pos[1];
2362
2360
  }
2363
2361
 
2364
- // there is only one pointer in the evt object, there's no point in looking further
2362
+ // There is only one pointer in the evt object, so there's no point in looking further
2365
2363
  break;
2366
2364
  }
2367
2365
  }
2368
2366
  }
2369
2367
  } else {
2370
2368
  if (this._getPointerInputDevice(evt) === 'touch') {
2371
- this._pointerAddBoardTouches(evt);
2369
+ this._pointerStorePosition(evt);
2370
+
2372
2371
  if (this._board_touches.length === 2) {
2373
2372
  evt.touches = this._board_touches;
2374
2373
  this.gestureChangeListener(evt);
@@ -2398,21 +2397,20 @@ define([
2398
2397
  * @returns {Boolean}
2399
2398
  */
2400
2399
  pointerUpListener: function (evt) {
2401
- var i, j, found;
2400
+ var i, j, found, touchTargets;
2402
2401
 
2403
2402
  this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
2404
2403
  this.displayInfobox(false);
2405
2404
 
2406
2405
  if (evt) {
2407
2406
  for (i = 0; i < this.touches.length; i++) {
2408
- for (j = 0; j < this.touches[i].targets.length; j++) {
2409
- if (this.touches[i].targets[j].num === evt.pointerId) {
2410
- this.touches[i].targets.splice(j, 1);
2411
-
2412
- if (this.touches[i].targets.length === 0) {
2407
+ touchTargets = this.touches[i].targets;
2408
+ for (j = 0; j < touchTargets.length; j++) {
2409
+ if (touchTargets[j].num === evt.pointerId) {
2410
+ touchTargets.splice(j, 1);
2411
+ if (touchTargets.length === 0) {
2413
2412
  this.touches.splice(i, 1);
2414
2413
  }
2415
-
2416
2414
  break;
2417
2415
  }
2418
2416
  }
@@ -2440,9 +2438,8 @@ define([
2440
2438
  }
2441
2439
  }
2442
2440
 
2443
- this._pointerRemoveBoardTouches(evt);
2444
-
2445
- if (this._board_touches.length === 0) {
2441
+ // this._pointerRemoveTouches(evt);
2442
+ // if (this._board_touches.length === 0) {
2446
2443
  if (this.hasPointerUp) {
2447
2444
  if (window.navigator.msPointerEnabled) { // IE10-
2448
2445
  Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
@@ -2453,14 +2450,15 @@ define([
2453
2450
  this.hasPointerUp = false;
2454
2451
  }
2455
2452
 
2456
- this.dehighlightAll();
2457
- this.updateQuality = this.BOARD_QUALITY_HIGH;
2458
- this.mode = this.BOARD_MODE_NONE;
2453
+ // this.dehighlightAll();
2454
+ // this.updateQuality = this.BOARD_QUALITY_HIGH;
2455
+ // this.mode = this.BOARD_MODE_NONE;
2459
2456
 
2460
2457
  this.originMoveEnd();
2461
2458
  this.update();
2462
- }
2463
-
2459
+ // }
2460
+ // After one finger leaves the screen the gesture is stopped.
2461
+ this._pointerClearTouches();
2464
2462
  return true;
2465
2463
  },
2466
2464
 
@@ -2478,7 +2476,7 @@ define([
2478
2476
  eps = this.options.precision.touch,
2479
2477
  obj, found, targets,
2480
2478
  evtTouches = evt[JXG.touchProperty],
2481
- target;
2479
+ target, touchTargets;
2482
2480
 
2483
2481
  if (!this.hasTouchEnd) {
2484
2482
  Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
@@ -2526,19 +2524,19 @@ define([
2526
2524
  }
2527
2525
 
2528
2526
  for (i = 0; i < this.touches.length; i++) {
2529
- for (j = 0; j < this.touches[i].targets.length; j++) {
2530
- this.touches[i].targets[j].num = -1;
2527
+ touchTargets = this.touches[i].targets;
2528
+ for (j = 0; j < touchTargets.length; j++) {
2529
+ touchTargets[j].num = -1;
2531
2530
  eps = this.options.precision.touch;
2532
2531
 
2533
2532
  do {
2534
2533
  for (k = 0; k < evtTouches.length; k++) {
2535
2534
  // find the new targettouches
2536
- if (Math.abs(Math.pow(evtTouches[k].screenX - this.touches[i].targets[j].X, 2) +
2537
- Math.pow(evtTouches[k].screenY - this.touches[i].targets[j].Y, 2)) < eps * eps) {
2538
- this.touches[i].targets[j].num = k;
2539
-
2540
- this.touches[i].targets[j].X = evtTouches[k].screenX;
2541
- this.touches[i].targets[j].Y = evtTouches[k].screenY;
2535
+ if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
2536
+ Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
2537
+ touchTargets[j].num = k;
2538
+ touchTargets[j].X = evtTouches[k].screenX;
2539
+ touchTargets[j].Y = evtTouches[k].screenY;
2542
2540
  evtTouches[k].jxg_isused = true;
2543
2541
  break;
2544
2542
  }
@@ -2546,13 +2544,13 @@ define([
2546
2544
 
2547
2545
  eps *= 2;
2548
2546
 
2549
- } while (this.touches[i].targets[j].num === -1 &&
2547
+ } while (touchTargets[j].num === -1 &&
2550
2548
  eps < this.options.precision.touchMax);
2551
2549
 
2552
- if (this.touches[i].targets[j].num === -1) {
2550
+ if (touchTargets[j].num === -1) {
2553
2551
  JXG.debug('i couldn\'t find a targettouches for target no ' + j + ' on ' + this.touches[i].obj.name + ' (' + this.touches[i].obj.id + '). Removed the target.');
2554
2552
  JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax);
2555
- this.touches[i].targets.splice(i, 1);
2553
+ touchTargets.splice(i, 1);
2556
2554
  }
2557
2555
 
2558
2556
  }
@@ -2578,20 +2576,22 @@ define([
2578
2576
  elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
2579
2577
  if (elements.length !== 0) {
2580
2578
  obj = elements[elements.length - 1];
2579
+ target = {num: i,
2580
+ X: evtTouches[i].screenX,
2581
+ Y: evtTouches[i].screenY,
2582
+ Xprev: NaN,
2583
+ Yprev: NaN,
2584
+ Xstart: [],
2585
+ Ystart: [],
2586
+ Zstart: []
2587
+ };
2581
2588
 
2582
2589
  if (Type.isPoint(obj) ||
2583
2590
  obj.elementClass === Const.OBJECT_CLASS_TEXT ||
2584
2591
  obj.type === Const.OBJECT_TYPE_TICKS ||
2585
2592
  obj.type === Const.OBJECT_TYPE_IMAGE) {
2586
- // it's a point, so it's single touch, so we just push it to our touches
2587
- targets = [{num: i,
2588
- X: evtTouches[i].screenX,
2589
- Y: evtTouches[i].screenY,
2590
- Xprev: NaN,
2591
- Yprev: NaN,
2592
- Xstart: [],
2593
- Ystart: [],
2594
- Zstart: [] }];
2593
+ // It's a point, so it's single touch, so we just push it to our touches
2594
+ targets = [target];
2595
2595
 
2596
2596
  // For the UNDO/REDO of object moves
2597
2597
  this.saveStartPos(obj, targets[0]);
@@ -2611,15 +2611,6 @@ define([
2611
2611
  found = true;
2612
2612
  // only add it, if we don't have two targets in there already
2613
2613
  if (this.touches[j].targets.length === 1) {
2614
- target = {num: i,
2615
- X: evtTouches[i].screenX,
2616
- Y: evtTouches[i].screenY,
2617
- Xprev: NaN,
2618
- Yprev: NaN,
2619
- Xstart: [],
2620
- Ystart: [],
2621
- Zstart: [] };
2622
-
2623
2614
  // For the UNDO/REDO of object moves
2624
2615
  this.saveStartPos(obj, target);
2625
2616
  this.touches[j].targets.push(target);
@@ -2633,14 +2624,7 @@ define([
2633
2624
  // IF there is a second touch targetting this line, we will find it later on, and then add it to
2634
2625
  // the touches control object.
2635
2626
  if (!found) {
2636
- targets = [{num: i,
2637
- X: evtTouches[i].screenX,
2638
- Y: evtTouches[i].screenY,
2639
- Xprev: NaN,
2640
- Yprev: NaN,
2641
- Xstart: [],
2642
- Ystart: [],
2643
- Zstart: [] }];
2627
+ targets = [target];
2644
2628
 
2645
2629
  // For the UNDO/REDO of object moves
2646
2630
  this.saveStartPos(obj, targets[0]);
@@ -2687,7 +2671,8 @@ define([
2687
2671
  * @returns {Boolean}
2688
2672
  */
2689
2673
  touchMoveListener: function (evt) {
2690
- var i, pos1, pos2, time,
2674
+ var i, pos1, pos2,
2675
+ touchTargets,
2691
2676
  evtTouches = evt[JXG.touchProperty];
2692
2677
 
2693
2678
  if (!this.checkFrameRate(evt)) {
@@ -2724,40 +2709,52 @@ define([
2724
2709
  // Runs over through all elements which are touched
2725
2710
  // by at least one finger.
2726
2711
  for (i = 0; i < this.touches.length; i++) {
2727
- if (this.touches[i].targets.length === 1) {
2712
+ touchTargets = this.touches[i].targets;
2713
+ if (touchTargets.length === 1) {
2714
+
2715
+
2728
2716
  // Touch by one finger: this is possible for all elements that can be dragged
2729
- if (evtTouches[this.touches[i].targets[0].num]) {
2730
- pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num);
2717
+ if (evtTouches[touchTargets[0].num]) {
2718
+ pos1 = this.getMousePosition(evt, touchTargets[0].num);
2731
2719
  if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
2732
2720
  pos1[1] < 0 || pos1[1] > this.canvasHeight) {
2733
2721
  return;
2734
2722
  }
2735
- this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
2736
- this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
2723
+ touchTargets[0].X = pos1[0];
2724
+ touchTargets[0].Y = pos1[1];
2737
2725
  this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch');
2738
2726
  }
2739
2727
 
2740
- } else if (this.touches[i].targets.length === 2 &&
2741
- this.touches[i].targets[0].num > -1 &&
2742
- this.touches[i].targets[1].num > -1) {
2728
+ } else if (touchTargets.length === 2 &&
2729
+ touchTargets[0].num > -1 &&
2730
+ touchTargets[1].num > -1) {
2731
+
2743
2732
  // Touch by two fingers: moving lines, ...
2744
- if (evtTouches[this.touches[i].targets[0].num] &&
2745
- evtTouches[this.touches[i].targets[1].num]) {
2733
+ if (evtTouches[touchTargets[0].num] &&
2734
+ evtTouches[touchTargets[1].num]) {
2746
2735
 
2747
2736
  // Get coordinates of the two touches
2748
- pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num);
2749
- pos2 = this.getMousePosition(evt, this.touches[i].targets[1].num);
2737
+ pos1 = this.getMousePosition(evt, touchTargets[0].num);
2738
+ pos2 = this.getMousePosition(evt, touchTargets[1].num);
2750
2739
  if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
2751
2740
  pos1[1] < 0 || pos1[1] > this.canvasHeight ||
2752
2741
  pos2[0] < 0 || pos2[0] > this.canvasWidth ||
2753
2742
  pos2[1] < 0 || pos2[1] > this.canvasHeight) {
2754
2743
  return;
2755
2744
  }
2756
- this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
2757
- this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
2758
- this.touches[i].targets[1].X = evtTouches[this.touches[i].targets[1].num].screenX;
2759
- this.touches[i].targets[1].Y = evtTouches[this.touches[i].targets[1].num].screenY;
2760
- this.twoFingerMove(pos1, pos2, this.touches[i], evt);
2745
+
2746
+ touchTargets[0].X = pos1[0];
2747
+ touchTargets[0].Y = pos1[1];
2748
+ touchTargets[1].X = pos2[0];
2749
+ touchTargets[1].Y = pos2[1];
2750
+
2751
+ this.twoFingerMove(this.touches[i], touchTargets[0].num, evt);
2752
+ this.twoFingerMove(this.touches[i], touchTargets[1].num);
2753
+
2754
+ touchTargets[0].Xprev = pos1[0];
2755
+ touchTargets[0].Yprev = pos1[1];
2756
+ touchTargets[1].Xprev = pos2[0];
2757
+ touchTargets[1].Yprev = pos2[1];
2761
2758
  }
2762
2759
  }
2763
2760
  }
@@ -2792,7 +2789,8 @@ define([
2792
2789
  var i, j, k,
2793
2790
  eps = this.options.precision.touch,
2794
2791
  tmpTouches = [], found, foundNumber,
2795
- evtTouches = evt && evt[JXG.touchProperty];
2792
+ evtTouches = evt && evt[JXG.touchProperty],
2793
+ touchTargets;
2796
2794
 
2797
2795
  this.triggerEventHandlers(['touchend', 'up'], [evt]);
2798
2796
  this.displayInfobox(false);
@@ -2828,15 +2826,16 @@ define([
2828
2826
  // could all targets of the current this.touches.obj be assigned to targettouches?
2829
2827
  found = false;
2830
2828
  foundNumber = 0;
2829
+ touchTargets = tmpTouches[i].targets;
2831
2830
 
2832
- for (j = 0; j < tmpTouches[i].targets.length; j++) {
2833
- tmpTouches[i].targets[j].found = false;
2831
+ for (j = 0; j < touchTargets.length; j++) {
2832
+ touchTargets[j].found = false;
2834
2833
  for (k = 0; k < evtTouches.length; k++) {
2835
- if (Math.abs(Math.pow(evtTouches[k].screenX - tmpTouches[i].targets[j].X, 2) + Math.pow(evtTouches[k].screenY - tmpTouches[i].targets[j].Y, 2)) < eps * eps) {
2836
- tmpTouches[i].targets[j].found = true;
2837
- tmpTouches[i].targets[j].num = k;
2838
- tmpTouches[i].targets[j].X = evtTouches[k].screenX;
2839
- tmpTouches[i].targets[j].Y = evtTouches[k].screenY;
2834
+ if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
2835
+ touchTargets[j].found = true;
2836
+ touchTargets[j].num = k;
2837
+ touchTargets[j].X = evtTouches[k].screenX;
2838
+ touchTargets[j].Y = evtTouches[k].screenY;
2840
2839
  foundNumber += 1;
2841
2840
  break;
2842
2841
  }
@@ -2844,9 +2843,9 @@ define([
2844
2843
  }
2845
2844
 
2846
2845
  if (Type.isPoint(tmpTouches[i].obj)) {
2847
- found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found);
2846
+ found = (touchTargets[0] && touchTargets[0].found);
2848
2847
  } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
2849
- found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found) || (tmpTouches[i].targets[1] && tmpTouches[i].targets[1].found);
2848
+ found = (touchTargets[0] && touchTargets[0].found) || (touchTargets[1] && touchTargets[1].found);
2850
2849
  } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
2851
2850
  found = foundNumber === 1 || foundNumber === 3;
2852
2851
  }
@@ -2858,17 +2857,17 @@ define([
2858
2857
  targets: []
2859
2858
  });
2860
2859
 
2861
- for (j = 0; j < tmpTouches[i].targets.length; j++) {
2862
- if (tmpTouches[i].targets[j].found) {
2860
+ for (j = 0; j < touchTargets.length; j++) {
2861
+ if (touchTargets[j].found) {
2863
2862
  this.touches[this.touches.length - 1].targets.push({
2864
- num: tmpTouches[i].targets[j].num,
2865
- X: tmpTouches[i].targets[j].screenX,
2866
- Y: tmpTouches[i].targets[j].screenY,
2863
+ num: touchTargets[j].num,
2864
+ X: touchTargets[j].screenX,
2865
+ Y: touchTargets[j].screenY,
2867
2866
  Xprev: NaN,
2868
2867
  Yprev: NaN,
2869
- Xstart: tmpTouches[i].targets[j].Xstart,
2870
- Ystart: tmpTouches[i].targets[j].Ystart,
2871
- Zstart: tmpTouches[i].targets[j].Zstart
2868
+ Xstart: touchTargets[j].Xstart,
2869
+ Ystart: touchTargets[j].Ystart,
2870
+ Zstart: touchTargets[j].Zstart
2872
2871
  });
2873
2872
  }
2874
2873
  }
@@ -2958,6 +2957,7 @@ define([
2958
2957
  this.mode = this.BOARD_MODE_NONE;
2959
2958
  result = true;
2960
2959
  } else {
2960
+ /** @ignore */
2961
2961
  this.mouse = {
2962
2962
  obj: null,
2963
2963
  targets: [{
@@ -3082,6 +3082,7 @@ define([
3082
3082
  }
3083
3083
 
3084
3084
  // release dragged mouse object
3085
+ /** @ignore */
3085
3086
  this.mouse = null;
3086
3087
  },
3087
3088
 
@@ -3110,6 +3111,7 @@ define([
3110
3111
  evt.preventDefault();
3111
3112
  return false;
3112
3113
  },
3114
+
3113
3115
  /**
3114
3116
  * Allow moving of JSXGraph elements with arrow keys
3115
3117
  * and zooming of the construction with + / -.
@@ -3126,10 +3128,15 @@ define([
3126
3128
  */
3127
3129
  keyDownListener: function (evt) {
3128
3130
  var id_node = evt.target.id,
3129
- id, el,
3131
+ id, el, res,
3132
+ sX = 0,
3133
+ sY = 0,
3134
+ // dx, dy are provided in screen units and
3135
+ // are converted to user coordinates
3130
3136
  dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
3131
3137
  dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
3132
3138
  doZoom = false,
3139
+ done = true,
3133
3140
  dir, actPos;
3134
3141
 
3135
3142
  if (!this.attr.keyboard.enabled || id_node === '') {
@@ -3158,8 +3165,44 @@ define([
3158
3165
  this.clickLeftArrow();
3159
3166
  } else if (evt.keyCode === 39) { // right
3160
3167
  this.clickRightArrow();
3168
+ } else {
3169
+ done = false;
3161
3170
  }
3162
3171
  } else {
3172
+ // Adapt dx, dy to snapToGrid and attractToGrid
3173
+ // snapToGrid has priority.
3174
+ if (Type.exists(el.visProp)) {
3175
+ if (Type.exists(el.visProp.snaptogrid) &&
3176
+ el.visProp.snaptogrid &&
3177
+ Type.evaluate(el.visProp.snapsizex) &&
3178
+ Type.evaluate(el.visProp.snapsizey)) {
3179
+
3180
+ // Adapt dx, dy such that snapToGrid is possible
3181
+ res = el.getSnapSizes();
3182
+ sX = res[0];
3183
+ sY = res[1];
3184
+ dx = Math.max(sX, dx);
3185
+ dy = Math.max(sY, dy);
3186
+
3187
+ } else if (Type.exists(el.visProp.attracttogrid) &&
3188
+ el.visProp.attracttogrid &&
3189
+ Type.evaluate(el.visProp.attractordistance) &&
3190
+ Type.evaluate(el.visProp.attractorunit)) {
3191
+
3192
+ // Adapt dx, dy such that attractToGrid is possible
3193
+ sX = 1.1 * Type.evaluate(el.visProp.attractordistance);
3194
+ sY = sX;
3195
+
3196
+ if (Type.evaluate(el.visProp.attractorunit) === 'screen') {
3197
+ sX /= this.unitX;
3198
+ sY /= this.unitX;
3199
+ }
3200
+ dx = Math.max(sX, dx);
3201
+ dy = Math.max(sY, dy);
3202
+ }
3203
+
3204
+ }
3205
+
3163
3206
  if (evt.keyCode === 38) { // up
3164
3207
  dir = [0, dy];
3165
3208
  } else if (evt.keyCode === 40) { // down
@@ -3174,9 +3217,12 @@ define([
3174
3217
  this.zoomIn();
3175
3218
  } else if (doZoom && evt.key === '-') { // -
3176
3219
  this.zoomOut();
3177
- } else if (doZoom && evt.key === 'o') { // o
3220
+ } else if (doZoom && evt.key === 'o') { // o
3178
3221
  this.zoom100();
3222
+ } else {
3223
+ done = false;
3179
3224
  }
3225
+
3180
3226
  if (dir && el.isDraggable &&
3181
3227
  el.visPropCalc.visible &&
3182
3228
  ((this.geonextCompatibilityMode &&
@@ -3193,11 +3239,18 @@ define([
3193
3239
  // For coordsElement setPosition has to call setPositionDirectly.
3194
3240
  // Otherwise the position is set by a translation.
3195
3241
  el.setPosition(JXG.COORDS_BY_USER, dir);
3242
+ if (Type.exists(el.coords)) {
3243
+ this.updateInfobox(el);
3244
+ }
3245
+ this.triggerEventHandlers(['hit'], [evt, el]);
3196
3246
  }
3197
3247
  }
3198
3248
 
3199
3249
  this.update();
3200
3250
 
3251
+ if (done) {
3252
+ evt.preventDefault();
3253
+ }
3201
3254
  return true;
3202
3255
  },
3203
3256
 
@@ -3224,6 +3277,10 @@ define([
3224
3277
  if (Type.exists(el.highlight)) {
3225
3278
  el.highlight(true);
3226
3279
  }
3280
+ if (Type.exists(el.coords)) {
3281
+ this.updateInfobox(el);
3282
+ }
3283
+ this.triggerEventHandlers(['hit'], [evt, el]);
3227
3284
  },
3228
3285
 
3229
3286
  /**
@@ -3236,7 +3293,7 @@ define([
3236
3293
  *
3237
3294
  * @param {Event} evt The browser's event object
3238
3295
  */
3239
- keyFocusOutListener: function (evt) {
3296
+ keyFocusOutListener: function (evt) {
3240
3297
  if (!this.attr.keyboard.enabled) {
3241
3298
  return false;
3242
3299
  }
@@ -3246,6 +3303,7 @@ define([
3246
3303
  // id = id_node.replace(this.containerObj.id + '_', '');
3247
3304
  // el = this.select(id);
3248
3305
  this.dehighlightAll();
3306
+ this.displayInfobox(false);
3249
3307
  },
3250
3308
 
3251
3309
  /**
@@ -3284,7 +3342,7 @@ define([
3284
3342
 
3285
3343
  // If bounding box is not yet initialized, do it now.
3286
3344
  if (isNaN(this.getBoundingBox()[0])) {
3287
- this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio);
3345
+ this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
3288
3346
  }
3289
3347
 
3290
3348
  // Do nothing if the dimension did not change since being visible
@@ -3300,7 +3358,7 @@ define([
3300
3358
  this._prevDim = {
3301
3359
  w: w,
3302
3360
  h: h
3303
- }
3361
+ };
3304
3362
  },
3305
3363
 
3306
3364
  /**
@@ -3424,13 +3482,12 @@ define([
3424
3482
  this.intersectionObserver = new IntersectionObserver(function(entries) {
3425
3483
  // If bounding box is not yet initialized, do it now.
3426
3484
  if (isNaN(that.getBoundingBox()[0])) {
3427
- that.setBoundingBox(that.attr.boundingbox, that.keepaspectratio);
3428
3485
  that.updateContainerDims();
3429
3486
  }
3430
3487
  }, options);
3431
3488
  this.intersectionObserver.observe(that.containerObj);
3432
3489
  } catch (err) {
3433
- console.log('Info: IntersectionObserver not available in this browser');
3490
+ console.log('JSXGraph: IntersectionObserver not available in this browser.');
3434
3491
  }
3435
3492
  },
3436
3493
 
@@ -3465,7 +3522,7 @@ define([
3465
3522
  * Infobox close to points in which the points' coordinates are displayed.
3466
3523
  * This is simply a JXG.Text element. Access through board.infobox.
3467
3524
  * Uses CSS class .JXGinfobox.
3468
- * @type {JXG.Text}
3525
+ * @type JXG.Text
3469
3526
  *
3470
3527
  */
3471
3528
  this.infobox = this.create('text', [0, 0, '0,0'], attr);
@@ -3657,11 +3714,11 @@ define([
3657
3714
  * // Shorter version:
3658
3715
  * //somePoint = board.create('point', a, {name:'SomePoint',size:4});
3659
3716
  * });
3660
- *
3717
+ *
3661
3718
  * })();
3662
3719
  *
3663
3720
  * </script><pre>
3664
- *
3721
+ *
3665
3722
  * @see JXG.Board#getScrCoordsOfMouse
3666
3723
  * @see JXG.Board#getAllUnderMouse
3667
3724
  */
@@ -3783,7 +3840,7 @@ define([
3783
3840
  addConditions: function (str) {
3784
3841
  var term, m, left, right, name, el, property,
3785
3842
  functions = [],
3786
- plaintext = 'var el, x, y, c, rgbo;\n',
3843
+ // plaintext = 'var el, x, y, c, rgbo;\n',
3787
3844
  i = str.indexOf('<data>'),
3788
3845
  j = str.indexOf('<' + '/data>'),
3789
3846
 
@@ -3867,7 +3924,7 @@ define([
3867
3924
  if (!Type.exists(this.elementsByName[name])) {
3868
3925
  JXG.debug("debug conditions: |" + name + "| undefined");
3869
3926
  } else {
3870
- plaintext += "el = this.objects[\"" + el.id + "\"];\n";
3927
+ // plaintext += "el = this.objects[\"" + el.id + "\"];\n";
3871
3928
 
3872
3929
  switch (property) {
3873
3930
  case 'x':
@@ -3994,9 +4051,7 @@ define([
3994
4051
  tr = (bb[1] - y) / (bb[1] - bb[3]);
3995
4052
  }
3996
4053
 
3997
- this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio);
3998
- this.zoomX *= zX;
3999
- this.zoomY *= zY;
4054
+ this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
4000
4055
  return this.applyZoom();
4001
4056
  },
4002
4057
 
@@ -4027,25 +4082,32 @@ define([
4027
4082
  tr = (bb[1] - y) / (bb[1] - bb[3]);
4028
4083
  }
4029
4084
 
4030
- this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio);
4031
- this.zoomX /= zX;
4032
- this.zoomY /= zY;
4085
+ this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
4033
4086
 
4034
4087
  return this.applyZoom();
4035
4088
  },
4036
4089
 
4037
4090
  /**
4038
- * Resets zoom factor to 100%.
4091
+ * Reset the zoom level to the original zoom level from initBoard();
4092
+ * Additionally, if the board as been initialized with a boundingBox (which is the default),
4093
+ * restore the viewport to the original viewport during initialization. Otherwise,
4094
+ * (i.e. if the board as been initialized with unitX/Y and originX/Y),
4095
+ * just set the zoom level to 100%.
4096
+ *
4039
4097
  * @returns {JXG.Board} Reference to the board
4040
4098
  */
4041
4099
  zoom100: function () {
4042
- var bb = this.getBoundingBox(),
4043
- dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5,
4044
- dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
4100
+ var bb, dX, dY;
4045
4101
 
4046
- this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], this.keepaspectratio);
4047
- this.zoomX = 1.0;
4048
- this.zoomY = 1.0;
4102
+ if (Type.exists(this.attr.boundingbox)) {
4103
+ this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
4104
+ } else {
4105
+ // Board has been set up with unitX/Y and originX/Y
4106
+ bb = this.getBoundingBox();
4107
+ dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
4108
+ dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
4109
+ this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], this.keepaspectratio, 'reset');
4110
+ }
4049
4111
  return this.applyZoom();
4050
4112
  },
4051
4113
 
@@ -4082,10 +4144,7 @@ define([
4082
4144
  borderX = border / this.unitX;
4083
4145
  borderY = border / this.unitY;
4084
4146
 
4085
- this.zoomX = 1.0;
4086
- this.zoomY = 1.0;
4087
-
4088
- this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], this.keepaspectratio);
4147
+ this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], this.keepaspectratio, 'update');
4089
4148
 
4090
4149
  return this.applyZoom();
4091
4150
  },
@@ -4118,53 +4177,12 @@ define([
4118
4177
  }
4119
4178
 
4120
4179
  if (Type.isArray(newBBox)) {
4121
- this.zoomX = 1.0;
4122
- this.zoomY = 1.0;
4123
4180
  cx = 0.5 * (newBBox[0] + newBBox[2]);
4124
4181
  cy = 0.5 * (newBBox[1] + newBBox[3]);
4125
4182
  dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
4126
4183
  dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
4127
4184
  d = Math.max(dx, dy);
4128
- this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], this.keepaspectratio);
4129
- }
4130
-
4131
- return this;
4132
- },
4133
-
4134
- zoomElementsOld: function (elements) {
4135
- var i, j, e, box,
4136
- newBBox = [0, 0, 0, 0],
4137
- dir = [1, -1, -1, 1];
4138
-
4139
- if (!Type.isArray(elements) || elements.length === 0) {
4140
- return this;
4141
- }
4142
-
4143
- for (i = 0; i < elements.length; i++) {
4144
- e = this.select(elements[i]);
4145
-
4146
- box = e.bounds();
4147
- if (Type.isArray(box)) {
4148
- if (Type.isArray(newBBox)) {
4149
- for (j = 0; j < 4; j++) {
4150
- if (dir[j] * box[j] < dir[j] * newBBox[j]) {
4151
- newBBox[j] = box[j];
4152
- }
4153
- }
4154
- } else {
4155
- newBBox = box;
4156
- }
4157
- }
4158
- }
4159
-
4160
- if (Type.isArray(newBBox)) {
4161
- for (j = 0; j < 4; j++) {
4162
- newBBox[j] -= dir[j];
4163
- }
4164
-
4165
- this.zoomX = 1.0;
4166
- this.zoomY = 1.0;
4167
- this.setBoundingBox(newBBox, this.keepaspectratio);
4185
+ this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], this.keepaspectratio, 'update');
4168
4186
  }
4169
4187
 
4170
4188
  return this;
@@ -4381,33 +4399,38 @@ define([
4381
4399
  * @returns {JXG.Board} Reference to the board
4382
4400
  */
4383
4401
  resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
4384
- var box_act, box, w, h, cx, cy,
4385
- shift_x = 0,
4386
- shift_y = 0;
4402
+ var box;
4403
+ // w, h, cx, cy;
4404
+ // box_act,
4405
+ // shift_x = 0,
4406
+ // shift_y = 0;
4407
+
4408
+ if (!dontSetBoundingBox) {
4409
+ // box_act = this.getBoundingBox(); // This is the actual bounding box.
4410
+ box = this.getBoundingBox(); // This is the actual bounding box.
4411
+ }
4387
4412
 
4388
4413
  this.canvasWidth = parseFloat(canvasWidth);
4389
4414
  this.canvasHeight = parseFloat(canvasHeight);
4390
4415
 
4391
- if (!dontSetBoundingBox) {
4392
- box_act = this.getBoundingBox(); // This is the actual bounding box.
4393
- box = this.attr.boundingbox; // This is the intended bounding box.
4394
- // The shift values compensate the follow-up
4395
- // correction in setBoundingBox in case this.keepaspectratio==true
4396
- // Otherwise, shift_x and shift_y will be zero.
4397
- shift_x = box_act[0] - box[0];
4398
- shift_y = box_act[1] - box[1];
4416
+ // if (!dontSetBoundingBox) {
4417
+ // box = this.attr.boundingbox; // This is the intended bounding box.
4399
4418
 
4400
- cx = (box[2] + box[0]) * 0.5 + shift_x;
4401
- cy = (box[3] + box[1]) * 0.5 + shift_y;
4402
- cx /= this.zoomX;
4403
- cy /= this.zoomY;
4419
+ // // The shift values compensate the follow-up correction
4420
+ // // in setBoundingBox in case of "this.keepaspectratio==true"
4421
+ // // Otherwise, shift_x and shift_y will be zero.
4422
+ // // Obsolet since setBoundingBox centers in case of "this.keepaspectratio==true".
4423
+ // // shift_x = box_act[0] - box[0] / this.zoomX;
4424
+ // // shift_y = box_act[1] - box[1] / this.zoomY;
4404
4425
 
4405
- w = (box[2] - box[0]) * 0.5 / this.zoomX;
4406
- h = (box[1] - box[3]) * 0.5 / this.zoomY;
4426
+ // cx = (box[2] + box[0]) * 0.5; // + shift_x;
4427
+ // cy = (box[3] + box[1]) * 0.5; // + shift_y;
4407
4428
 
4408
- box = [cx - w, cy + h, cx + w, cy - h];
4409
- }
4429
+ // w = (box[2] - box[0]) * 0.5 / this.zoomX;
4430
+ // h = (box[1] - box[3]) * 0.5 / this.zoomY;
4410
4431
 
4432
+ // box = [cx - w, cy + h, cx + w, cy - h];
4433
+ // }
4411
4434
 
4412
4435
  if (!dontset) {
4413
4436
  this.containerObj.style.width = (this.canvasWidth) + 'px';
@@ -4416,7 +4439,7 @@ define([
4416
4439
  this.renderer.resize(this.canvasWidth, this.canvasHeight);
4417
4440
 
4418
4441
  if (!dontSetBoundingBox) {
4419
- this.setBoundingBox(box, this.keepaspectratio);
4442
+ this.setBoundingBox(box, this.keepaspectratio, 'keep');
4420
4443
  }
4421
4444
 
4422
4445
  return this;
@@ -4523,13 +4546,13 @@ define([
4523
4546
 
4524
4547
  for (el = 0; el < this.objectsList.length; el++) {
4525
4548
  pEl = this.objectsList[el];
4526
- if (this.needsFullUpdate && pEl.elementClass == Const.OBJECT_CLASS_TEXT) {
4549
+ if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
4527
4550
  pEl.updateSize();
4528
4551
  }
4529
4552
 
4530
4553
  // For updates of an element we distinguish if the dragged element is updated or
4531
4554
  // other elements are updated.
4532
- // The difference lies in the treatment of gliders.
4555
+ // The difference lies in the treatment of gliders and points based on transformations.
4533
4556
  pEl.update(!Type.exists(drag) || pEl.id !== drag.id)
4534
4557
  .updateVisibility();
4535
4558
  }
@@ -4826,6 +4849,7 @@ define([
4826
4849
  for (i = 0; i < parents.length; i++) {
4827
4850
  if (Type.isString(parents[i]) &&
4828
4851
  !(elementType === 'text' && i === 2) &&
4852
+ !(elementType === 'solidofrevolution3d' && i === 2) &&
4829
4853
  !((elementType === 'input' || elementType === 'checkbox' || elementType === 'button') &&
4830
4854
  (i === 2 || i === 3)) &&
4831
4855
  !(elementType === 'curve' && i > 0) // Allow curve plots with jessiecode
@@ -4903,15 +4927,21 @@ define([
4903
4927
  * @param {Array} bbox New bounding box [x1,y1,x2,y2]
4904
4928
  * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
4905
4929
  * the resulting viewport may be larger.
4930
+ * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
4931
+ * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
4932
+ * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
4906
4933
  * @returns {JXG.Board} Reference to the board
4907
4934
  */
4908
- setBoundingBox: function (bbox, keepaspectratio) {
4909
- var h, w,
4935
+ setBoundingBox: function (bbox, keepaspectratio, setZoom) {
4936
+ var h, w, ux, uy,
4937
+ offX = 0,
4938
+ offY = 0,
4910
4939
  dim = Env.getDimensions(this.container, this.document);
4911
4940
 
4912
4941
  if (!Type.isArray(bbox)) {
4913
4942
  return this;
4914
4943
  }
4944
+
4915
4945
  if (bbox[0] < this.maxboundingbox[0] ||
4916
4946
  bbox[1] > this.maxboundingbox[1] ||
4917
4947
  bbox[2] > this.maxboundingbox[2] ||
@@ -4919,20 +4949,28 @@ define([
4919
4949
  return this;
4920
4950
  }
4921
4951
 
4922
- // this.plainBB = bbox;
4952
+ if (!Type.exists(setZoom)) {
4953
+ setZoom = 'reset';
4954
+ }
4955
+
4956
+ ux = this.unitX;
4957
+ uy = this.unitY;
4923
4958
 
4924
4959
  this.canvasWidth = parseInt(dim.width, 10);
4925
4960
  this.canvasHeight = parseInt(dim.height, 10);
4926
4961
  w = this.canvasWidth;
4927
4962
  h = this.canvasHeight;
4928
-
4929
4963
  if (keepaspectratio) {
4930
4964
  this.unitX = w / (bbox[2] - bbox[0]);
4931
4965
  this.unitY = h / (bbox[1] - bbox[3]);
4932
4966
  if (Math.abs(this.unitX) < Math.abs(this.unitY)) {
4933
4967
  this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY);
4968
+ // Add the additional units in equal portions above and below
4969
+ offY = (h / this.unitY - (bbox[1] - bbox[3])) * 0.5;
4934
4970
  } else {
4935
4971
  this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX);
4972
+ // Add the additional units in equal portions left and right
4973
+ offX = (w / this.unitX - (bbox[2] - bbox[0])) * 0.5;
4936
4974
  }
4937
4975
  this.keepaspectratio = true;
4938
4976
  } else {
@@ -4941,7 +4979,15 @@ define([
4941
4979
  this.keepaspectratio = false;
4942
4980
  }
4943
4981
 
4944
- this.moveOrigin(-this.unitX * bbox[0], this.unitY * bbox[1]);
4982
+ this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
4983
+
4984
+ if (setZoom === 'update') {
4985
+ this.zoomX *= this.unitX / ux;
4986
+ this.zoomY *= this.unitY / uy;
4987
+ } else if (setZoom === 'reset') {
4988
+ this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
4989
+ this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
4990
+ }
4945
4991
 
4946
4992
  return this;
4947
4993
  },
@@ -5266,7 +5312,7 @@ define([
5266
5312
  for (i = 0; i < l; i++) {
5267
5313
  olist[flist[i].id] = flist[i];
5268
5314
  }
5269
- s = new EComposition(olist);
5315
+ s = new Composition(olist);
5270
5316
  // it's an element which has been deleted (and still hangs around, e.g. in an attractor list
5271
5317
  } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) {
5272
5318
  s = null;
@@ -5613,7 +5659,7 @@ define([
5613
5659
  * @name JXG.Board#move
5614
5660
  * @param {Event} e The browser's event object.
5615
5661
  * @param {Number} mode The mode the board currently is in
5616
- * @see {JXG.Board#mode}
5662
+ * @see JXG.Board#mode
5617
5663
  */
5618
5664
  __evt__move: function (e, mode) { },
5619
5665
 
@@ -5623,7 +5669,7 @@ define([
5623
5669
  * @name JXG.Board#mousemove
5624
5670
  * @param {Event} e The browser's event object.
5625
5671
  * @param {Number} mode The mode the board currently is in
5626
- * @see {JXG.Board#mode}
5672
+ * @see JXG.Board#mode
5627
5673
  */
5628
5674
  __evt__mousemove: function (e, mode) { },
5629
5675
 
@@ -5633,7 +5679,7 @@ define([
5633
5679
  * @name JXG.Board#penmove
5634
5680
  * @param {Event} e The browser's event object.
5635
5681
  * @param {Number} mode The mode the board currently is in
5636
- * @see {JXG.Board#mode}
5682
+ * @see JXG.Board#mode
5637
5683
  */
5638
5684
  __evt__penmove: function (e, mode) { },
5639
5685
 
@@ -5644,7 +5690,7 @@ define([
5644
5690
  * @name JXG.Board#pointermove
5645
5691
  * @param {Event} e The browser's event object.
5646
5692
  * @param {Number} mode The mode the board currently is in
5647
- * @see {JXG.Board#mode}
5693
+ * @see JXG.Board#mode
5648
5694
  */
5649
5695
  __evt__pointermove: function (e, mode) { },
5650
5696
 
@@ -5654,7 +5700,7 @@ define([
5654
5700
  * @name JXG.Board#touchmove
5655
5701
  * @param {Event} e The browser's event object.
5656
5702
  * @param {Number} mode The mode the board currently is in
5657
- * @see {JXG.Board#mode}
5703
+ * @see JXG.Board#mode
5658
5704
  */
5659
5705
  __evt__touchmove: function (e, mode) { },
5660
5706
 
@@ -5824,6 +5870,12 @@ define([
5824
5870
  * <p>
5825
5871
  * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
5826
5872
  * defined in the file 'jsxgraph.css'
5873
+ * <p>
5874
+ * This feature is not available on iPhones (as of December 2021).
5875
+ *
5876
+ * @param {String} id (Optional) id of the div element which is brought to fullscreen.
5877
+ * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
5878
+ * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
5827
5879
  *
5828
5880
  * @return {JXG.Board} Reference to the board
5829
5881
  *
@@ -5845,29 +5897,76 @@ define([
5845
5897
  * var p = board.create('point', [0, 1]);
5846
5898
  * board_d5bab8b6 = board;
5847
5899
  * })();
5848
- * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button>
5849
- *
5850
5900
  * </script>
5901
+ * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button>
5851
5902
  * <pre>
5852
5903
  *
5904
+ * @example
5905
+ * &lt;div id='outer' style='max-width: 500px; margin: 0 auto;'&gt;
5906
+ * &lt;div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'&gt;&lt;/div&gt;
5907
+ * &lt;/div&gt;
5908
+ * &lt;button onClick="board.toFullscreen('outer')"&gt;Fullscreen&lt;/button&gt;
5909
+ *
5910
+ * &lt;script language="Javascript" type='text/javascript'&gt;
5911
+ * var board = JXG.JSXGraph.initBoard('jxgbox', {
5912
+ * axis:true,
5913
+ * boundingbox:[-5,5,5,-5],
5914
+ * fullscreen: { id: 'outer' },
5915
+ * showFullscreen: true
5916
+ * });
5917
+ * var p = board.create('point', [-2, 3], {});
5918
+ * &lt;/script&gt;
5919
+ *
5920
+ * </pre><div id="JXG7103f6b_outer" style='max-width: 500px; margin: 0 auto;'>
5921
+ * <div id="JXG7103f6be-6993-4ff8-8133-c78e50a8afac" class="jxgbox" style="height: 0; padding-bottom: 100%;"></div>
5922
+ * </div>
5923
+ * <button onClick="board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')">Fullscreen</button>
5924
+ * <script type="text/javascript">
5925
+ * var board_JXG7103f6be;
5926
+ * (function() {
5927
+ * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
5928
+ * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
5929
+ * showcopyright: false, shownavigation: false});
5930
+ * var p = board.create('point', [-2, 3], {});
5931
+ * board_JXG7103f6be = board;
5932
+ * })();
5933
+ *
5934
+ * </script><pre>
5935
+ *
5936
+ *
5853
5937
  */
5854
- toFullscreen: function() {
5855
- var id = this.container,
5856
- wrap_id = 'fullscreenwrap_' + id,
5857
- wrapper = document.createElement('div'),
5858
- el;
5938
+ toFullscreen: function (id) {
5939
+ var wrap_id, wrap_node, inner_node;
5940
+
5941
+ id = id || this.container;
5942
+ this._fullscreen_inner_id = id;
5943
+ inner_node = document.getElementById(id);
5944
+ wrap_id = 'fullscreenwrap_' + id;
5859
5945
 
5860
- // If necessary, wrap a div around the JSXGraph div.
5861
- if (!this.document.getElementById(wrap_id)) {
5862
- wrapper.classList.add('JXG_wrap_private');
5863
- wrapper.setAttribute('id', wrap_id);
5864
- el = this.containerObj;
5865
- el.parentNode.insertBefore(wrapper, el);
5866
- wrapper.appendChild(el);
5946
+ // Wrap a div around the JSXGraph div.
5947
+ if (this.document.getElementById(wrap_id)) {
5948
+ wrap_node = this.document.getElementById(wrap_id);
5949
+ } else {
5950
+ wrap_node = document.createElement('div');
5951
+ wrap_node.classList.add('JXG_wrap_private');
5952
+ wrap_node.setAttribute('id', wrap_id);
5953
+ inner_node.parentNode.insertBefore(wrap_node, inner_node);
5954
+ wrap_node.appendChild(inner_node);
5867
5955
  }
5868
5956
 
5869
- // Start fullscreen mode
5870
- Env.toFullscreen(wrap_id);
5957
+ // Get the real width and height of the JSXGraph div
5958
+ // and determine the scaling and vertical shift amount
5959
+ this._fullscreen_res = Env._getScaleFactors(inner_node);
5960
+
5961
+ // Trigger fullscreen mode
5962
+ wrap_node.requestFullscreen = wrap_node.requestFullscreen ||
5963
+ wrap_node.webkitRequestFullscreen ||
5964
+ wrap_node.mozRequestFullScreen ||
5965
+ wrap_node.msRequestFullscreen;
5966
+
5967
+ if (wrap_node.requestFullscreen) {
5968
+ wrap_node.requestFullscreen();
5969
+ }
5871
5970
 
5872
5971
  return this;
5873
5972
  },
@@ -5879,40 +5978,69 @@ define([
5879
5978
  *
5880
5979
  * @param {Object} evt fullscreen event object (unused)
5881
5980
  */
5882
- fullscreenListener: function(evt) {
5883
- var el = this.containerObj, res;
5981
+ fullscreenListener: function (evt) {
5982
+ var res, inner_id, inner_node;
5884
5983
 
5984
+ inner_id = this._fullscreen_inner_id;
5985
+ if (!Type.exists(inner_id)) {
5986
+ return;
5987
+ }
5988
+
5989
+ document.fullscreenElement = document.fullscreenElement ||
5990
+ document.webkitFullscreenElement ||
5991
+ document.mozFullscreenElement ||
5992
+ document.msFullscreenElement;
5993
+
5994
+ inner_node = document.getElementById(inner_id);
5885
5995
  // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
5886
5996
  // Otherwise, the positioning of the fullscreen div will be false.
5887
5997
  // When leaving the fullscreen mode, the margin is put back in.
5888
-
5889
5998
  if (document.fullscreenElement) {
5890
- // Entered fullscreen mode
5999
+ // Just entered fullscreen mode
5891
6000
 
5892
- el.style.margin = '';
5893
-
5894
- // Do the shifting and scaling via CSS pseudo rules
5895
- // We do this after fullscreen mode has been established to get the correct size
5896
- // of the JSXGraph div
5897
- res = Env._getScaleFactors(el);
5898
- Env.scaleJSXGraphDiv('#' + document.fullscreenElement.id, '#' + this.container, res.scale, res.vshift);
6001
+ // Get the data computed in board.toFullscreen()
6002
+ res = this._fullscreen_res;
5899
6003
 
5900
6004
  // Store the scaling data.
5901
6005
  // It is used in AbstractRenderer.updateText to restore the scaling matrix
5902
6006
  // which is removed by MathJax.
5903
6007
  // Further, the CSS margin has to be removed when in fullscreen mode,
5904
6008
  // and must be restored later.
5905
- el._cssFullscreenStore = {
6009
+ inner_node._cssFullscreenStore = {
6010
+ id: document.fullscreenElement.id,
5906
6011
  isFullscreen: true,
5907
- margin: el.style.margin,
5908
- scale: res.scale,
6012
+ margin: inner_node.style.margin,
6013
+ width: inner_node.style.width,
6014
+ scale: res.scale,
5909
6015
  vshift: res.vshift
5910
6016
  };
5911
- } else if (Type.exists(el._cssFullscreenStore)) {
5912
- // Left fullscreen mode
5913
6017
 
5914
- el._cssFullscreenStore.isFullscreen = false;
5915
- el.style.margin = el._cssFullscreenStore.margin;
6018
+ inner_node.style.margin = '';
6019
+ inner_node.style.width = res.width + 'px';
6020
+
6021
+ // Do the shifting and scaling via CSS pseudo rules
6022
+ // We do this after fullscreen mode has been established to get the correct size
6023
+ // of the JSXGraph div.
6024
+ Env.scaleJSXGraphDiv(document.fullscreenElement.id, inner_id, res.scale, res.vshift);
6025
+
6026
+ // Clear document.fullscreenElement, because Safari doesn't to it and
6027
+ // when leaving full screen mode it is still set.
6028
+ document.fullscreenElement = null;
6029
+
6030
+ } else if (Type.exists(inner_node._cssFullscreenStore)) {
6031
+ // Just left the fullscreen mode
6032
+
6033
+ // Remove the CSS rules added in Env.scaleJSXGraphDiv
6034
+ try {
6035
+ document.styleSheets[document.styleSheets.length - 1].deleteRule(0);
6036
+ } catch (err) {
6037
+ console.log('JSXGraph: Could not remove CSS rules for full screen mode');
6038
+ }
6039
+
6040
+ inner_node._cssFullscreenStore.isFullscreen = false;
6041
+ inner_node.style.margin = inner_node._cssFullscreenStore.margin;
6042
+ inner_node.style.width = inner_node._cssFullscreenStore.width;
6043
+
5916
6044
  }
5917
6045
 
5918
6046
  this.updateCSSTransforms();