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 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, document: true */
 33 /*jslint nomen: true, plusplus: true, newcap:true*/
 34 
 35 /* depends:
 36  jxg
 37  options
 38  renderer/abstract
 39  base/constants
 40  utils/type
 41  utils/env
 42  utils/color
 43  math/numerics
 44 */
 45 
 46 define([
 47     'jxg', 'options', 'renderer/abstract', 'base/constants', 'utils/type', 'utils/color', 'utils/base64', 'math/numerics'
 48 ], function (JXG, Options, AbstractRenderer, Const, Type, Color, Base64, Numerics) {
 49 
 50     "use strict";
 51 
 52     /**
 53      * Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
 54      * @class JXG.SVGRenderer
 55      * @augments JXG.AbstractRenderer
 56      * @param {Node} container Reference to a DOM node containing the board.
 57      * @param {Object} dim The dimensions of the board
 58      * @param {Number} dim.width
 59      * @param {Number} dim.height
 60      * @see JXG.AbstractRenderer
 61      */
 62     JXG.SVGRenderer = function (container, dim) {
 63         var i;
 64 
 65         // docstring in AbstractRenderer
 66         this.type = 'svg';
 67 
 68         this.isIE = navigator.appVersion.indexOf("MSIE") !== -1 || navigator.userAgent.match(/Trident\//);
 69 
 70         /**
 71          * SVG root node
 72          * @type Node
 73          */
 74         this.svgRoot = null;
 75 
 76         /**
 77          * The SVG Namespace used in JSXGraph.
 78          * @see http://www.w3.org/TR/SVG/
 79          * @type String
 80          * @default http://www.w3.org/2000/svg
 81          */
 82         this.svgNamespace = 'http://www.w3.org/2000/svg';
 83 
 84         /**
 85          * The xlink namespace. This is used for images.
 86          * @see http://www.w3.org/TR/xlink/
 87          * @type String
 88          * @default http://www.w3.org/1999/xlink
 89          */
 90         this.xlinkNamespace = 'http://www.w3.org/1999/xlink';
 91 
 92         // container is documented in AbstractRenderer
 93         this.container = container;
 94 
 95         // prepare the div container and the svg root node for use with JSXGraph
 96         this.container.style.MozUserSelect = 'none';
 97         this.container.style.userSelect = 'none';
 98 
 99         this.container.style.overflow = 'hidden';
100         if (this.container.style.position === '') {
101             this.container.style.position = 'relative';
102         }
103 
104         this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, "svg");
105         this.svgRoot.style.overflow = 'hidden';
106         this.svgRoot.style.display = 'block';
107 
108         this.resize(dim.width, dim.height);
109 
110         //this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision');
111 
112         this.container.appendChild(this.svgRoot);
113 
114         /**
115          * The <tt>defs</tt> element is a container element to reference reusable SVG elements.
116          * @type Node
117          * @see http://www.w3.org/TR/SVG/struct.html#DefsElement
118          */
119         this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, 'defs');
120         this.svgRoot.appendChild(this.defs);
121 
122         /**
123          * Filters are used to apply shadows.
124          * @type Node
125          * @see http://www.w3.org/TR/SVG/filters.html#FilterElement
126          */
127         this.filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter');
128         this.filter.setAttributeNS(null, 'id', this.container.id + '_' + 'f1');
129         /*
130         this.filter.setAttributeNS(null, 'x', '-100%');
131         this.filter.setAttributeNS(null, 'y', '-100%');
132         this.filter.setAttributeNS(null, 'width', '400%');
133         this.filter.setAttributeNS(null, 'height', '400%');
134         //this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
135         */
136         this.filter.setAttributeNS(null, 'width', '300%');
137         this.filter.setAttributeNS(null, 'height', '300%');
138         this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
139 
140         this.feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
141         this.feOffset.setAttributeNS(null, 'in', 'SourceAlpha'); // b/w: SourceAlpha, Color: SourceGraphic
142         this.feOffset.setAttributeNS(null, 'result', 'offOut');
143         this.feOffset.setAttributeNS(null, 'dx', '5');
144         this.feOffset.setAttributeNS(null, 'dy', '5');
145         this.filter.appendChild(this.feOffset);
146 
147         // this.feColor = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feColorMatrix');
148         // this.feColor.setAttributeNS(null, 'in', 'offOut');
149         // this.feColor.setAttributeNS(null, 'result', 'colorOut');
150         // this.feColor.setAttributeNS(null, 'type', 'matrix');
151         // this.feColor.setAttributeNS(null, 'values', '0.1 0 0 0 0  0 0.1 0 0 0  0 0 0.1 0 50  0 0 0 1 0');
152         // this.filter.appendChild(this.feColor);
153 
154         this.feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
155         this.feGaussianBlur.setAttributeNS(null, 'in', 'offOut');
156         this.feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
157         this.feGaussianBlur.setAttributeNS(null, 'stdDeviation', '3');
158         this.filter.appendChild(this.feGaussianBlur);
159 
160         this.feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
161         this.feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
162         this.feBlend.setAttributeNS(null, 'in2', 'blurOut');
163         this.feBlend.setAttributeNS(null, 'mode', 'normal');
164         this.filter.appendChild(this.feBlend);
165 
166         this.defs.appendChild(this.filter);
167 
168         /**
169          * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
170          * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
171          * there, too. The higher the number, the "more on top" are the elements on this layer.
172          * @type Array
173          */
174         this.layer = [];
175         for (i = 0; i < Options.layer.numlayers; i++) {
176             this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
177             this.svgRoot.appendChild(this.layer[i]);
178         }
179 
180         // Already documented in JXG.AbstractRenderer
181         this.supportsForeignObject = document.implementation.hasFeature("http://w3.org/TR/SVG11/feature#Extensibility", "1.1");
182 
183         if (this.supportsForeignObject) {
184             this.foreignObjLayer = this.container.ownerDocument.createElementNS(this.svgNamespace, 'foreignObject');
185             this.foreignObjLayer.setAttribute("display", "none");
186             this.foreignObjLayer.setAttribute("x", 0);
187             this.foreignObjLayer.setAttribute("y", 0);
188             this.foreignObjLayer.setAttribute("width", "100%");
189             this.foreignObjLayer.setAttribute("height", "100%");
190             this.foreignObjLayer.setAttribute('id', this.container.id + '_foreignObj');
191             this.svgRoot.appendChild(this.foreignObjLayer);
192         }
193 
194         /**
195          * Defines dash patterns. Defined styles are: <ol>
196          * <li value="-1"> 2px dash, 2px space</li>
197          * <li> 5px dash, 5px space</li>
198          * <li> 10px dash, 10px space</li>
199          * <li> 20px dash, 20px space</li>
200          * <li> 20px dash, 10px space, 10px dash, 10px dash</li>
201          * <li> 20px dash, 5px space, 10px dash, 5px space</li></ol>
202          * @type Array
203          * @default ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5']
204          * @see http://www.w3.org/TR/SVG/painting.html#StrokeProperties
205          */
206         this.dashArray = ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5'];
207     };
208 
209     JXG.SVGRenderer.prototype = new AbstractRenderer();
210 
211     JXG.extend(JXG.SVGRenderer.prototype, /** @lends JXG.SVGRenderer.prototype */ {
212 
213         /**
214          * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
215          * @private
216          * @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached.
217          * @param {String} [idAppendix=''] A string that is added to the node's id.
218          * @returns {Node} Reference to the node added to the DOM.
219          */
220         _createArrowHead: function (el, idAppendix, type) {
221             var node2, node3,
222                 id = el.id + 'Triangle',
223                 //type = null,
224                 v, h;
225 
226             if (Type.exists(idAppendix)) {
227                 id += idAppendix;
228             }
229             node2 = this.createPrim('marker', id);
230 
231             node2.setAttributeNS(null, 'stroke', Type.evaluate(el.visProp.strokecolor));
232             node2.setAttributeNS(null, 'stroke-opacity', Type.evaluate(el.visProp.strokeopacity));
233             node2.setAttributeNS(null, 'fill', Type.evaluate(el.visProp.strokecolor));
234             node2.setAttributeNS(null, 'fill-opacity', Type.evaluate(el.visProp.strokeopacity));
235             node2.setAttributeNS(null, 'stroke-width', 0);  // this is the stroke-width of the arrow head.
236             // Should be zero to simplify the calculations
237 
238             node2.setAttributeNS(null, 'orient', 'auto');
239             node2.setAttributeNS(null, 'markerUnits', 'strokeWidth'); // 'strokeWidth' 'userSpaceOnUse');
240 
241             /*
242                Types 1, 2:
243                The arrow head is an isosceles triangle with base length 10 and height 10.
244 
245                Type 3:
246                A rectangle
247 
248                Types 4, 5, 6:
249                Defined by Bezier curves from mp_arrowheads.html
250 
251                In any case but type 3 the arrow head is 10 units long,
252                type 3 is 10 unitsb high.
253                These 10 units are scaled to strokeWidth * arrowSize pixels, see
254                this._setArrowWidth().
255 
256                See also abstractRenderer.updateLine() where the line path is shortened accordingly.
257 
258                Changes here are also necessary in setArrowWidth().
259 
260                So far, lines with arrow heads are shortenend to avoid overlapping of
261                arrow head and line. This is not the case for curves, yet.
262                Therefore, the offset refX has to be adapted to the path type.
263             */
264             node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'path');
265             h = 5;
266             if (idAppendix === 'End') {
267                 // First arrow
268                 //type = a.typeFirst;
269                 // if (JXG.exists(ev_fa.type)) {
270                 //     type = Type.evaluate(ev_fa.type);
271                 // }
272 
273                 v = 0;
274                 if (type === 2) {
275                     node3.setAttributeNS(null, 'd', 'M 10,0 L 0,5 L 10,10 L 5,5 z');
276                 } else if (type === 3) {
277                     node3.setAttributeNS(null, 'd', 'M 0,0 L 3.33,0 L 3.33,10 L 0,10 z');
278                 } else if (type === 4) {
279                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
280                     h = 3.31;
281                     node3.setAttributeNS(null, 'd', 'M 0.00,3.31 C 3.53,3.84 7.13,4.50 10.00,6.63 C 9.33,5.52 8.67,4.42 8.00,3.31 C 8.67,2.21 9.33,1.10 10.00,0.00 C 7.13,2.13 3.53,2.79 0.00,3.31');
282                 } else if (type === 5) {
283                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
284                     h = 3.28;
285                     node3.setAttributeNS(null, 'd', 'M 0.00,3.28 C 3.39,4.19 6.81,5.07 10.00,6.55 C 9.38,5.56 9.00,4.44 9.00,3.28 C 9.00,2.11 9.38,0.99 10.00,0.00 C 6.81,1.49 3.39,2.37 0.00,3.28');
286                 } else if (type === 6) {
287                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
288                     h = 2.84;
289                     node3.setAttributeNS(null, 'd', 'M 0.00,2.84 C 3.39,3.59 6.79,4.35 10.00,5.68 C 9.67,4.73 9.33,3.78 9.00,2.84 C 9.33,1.89 9.67,0.95 10.00,0.00 C 6.79,1.33 3.39,2.09 0.00,2.84');
290                 } else if (type === 7) {
291                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
292                     h = 5.20;
293                     node3.setAttributeNS(null, 'd', 'M 0.00,5.20 C 4.04,5.20 7.99,6.92 10.00,10.39 M 10.00,0.00 C 7.99,3.47 4.04,5.20 0.00,5.20');
294                 } else {
295                     // type == 1 or > 6
296                     node3.setAttributeNS(null, 'd', 'M 10,0 L 0,5 L 10,10 z');
297                 }
298                 if (/*!Type.exists(el.rendNode.getTotalLength) && */el.elementClass === Const.OBJECT_CLASS_LINE) {
299                     if (type === 2) {
300                         v = 4.9;
301                     } else if (type === 3) {
302                         v = 3.3;
303                     } else if (type === 4 || type === 5 || type === 6) {
304                         v = 6.66;
305                     } else if (type === 7) {
306                         v = 0.0;
307                     } else {
308                         v = 10.0;
309                     }
310                 }
311             } else {
312                 // Last arrow
313                 // if (JXG.exists(ev_la.type)) {
314                 //     type = Type.evaluate(ev_la.type);
315                 // }
316                 //type = a.typeLast;
317 
318                 v = 10.0;
319                 if (type === 2) {
320                     node3.setAttributeNS(null, 'd', 'M 0,0 L 10,5 L 0,10 L 5,5 z');
321                 } else if (type === 3) {
322                     v = 3.3;
323                     node3.setAttributeNS(null, 'd', 'M 0,0 L 3.33,0 L 3.33,10 L 0,10 z');
324                 } else if (type === 4) {
325                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
326                     h = 3.31;
327                     node3.setAttributeNS(null, 'd', 'M 10.00,3.31 C 6.47,3.84 2.87,4.50 0.00,6.63 C 0.67,5.52 1.33,4.42 2.00,3.31 C 1.33,2.21 0.67,1.10 0.00,0.00 C 2.87,2.13 6.47,2.79 10.00,3.31');
328                 } else if (type === 5) {
329                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
330                     h = 3.28;
331                     node3.setAttributeNS(null, 'd', 'M 10.00,3.28 C 6.61,4.19 3.19,5.07 0.00,6.55 C 0.62,5.56 1.00,4.44 1.00,3.28 C 1.00,2.11 0.62,0.99 0.00,0.00 C 3.19,1.49 6.61,2.37 10.00,3.28');
332                 } else if (type === 6) {
333                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
334                     h = 2.84;
335                     node3.setAttributeNS(null, 'd', 'M 10.00,2.84 C 6.61,3.59 3.21,4.35 0.00,5.68 C 0.33,4.73 0.67,3.78 1.00,2.84 C 0.67,1.89 0.33,0.95 0.00,0.00 C 3.21,1.33 6.61,2.09 10.00,2.84');
336                 } else if (type === 7) {
337                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
338                     h = 5.20;
339                     node3.setAttributeNS(null, 'd', 'M 10.00,5.20 C 5.96,5.20 2.01,6.92 0.00,10.39 M 0.00,0.00 C 2.01,3.47 5.96,5.20 10.00,5.20');
340                 } else {
341                     // type == 1 or > 6
342                     node3.setAttributeNS(null, 'd', 'M 0,0 L 10,5 L 0,10 z');
343                 }
344                 if (/*!Type.exists(el.rendNode.getTotalLength) &&*/ el.elementClass === Const.OBJECT_CLASS_LINE) {
345                     if (type === 2) {
346                         v = 5.1;
347                     } else if (type === 3) {
348                         v = 0.02;
349                     } else if (type === 4 || type === 5 || type === 6) {
350                         v = 3.33;
351                     } else if (type === 7) {
352                         v = 10.0;
353                     } else {
354                         v = 0.05;
355                     }
356                 }
357             }
358             if (type === 7) {
359                 node2.setAttributeNS(null, 'fill', 'none');
360                 node2.setAttributeNS(null, 'stroke-width', 1);  // this is the stroke-width of the arrow head.
361             }
362             node2.setAttributeNS(null, 'refY', h);
363             node2.setAttributeNS(null, 'refX', v);
364 
365             node2.appendChild(node3);
366             return node2;
367         },
368 
369         /**
370          * Updates color of an arrow DOM node.
371          * @param {Node} node The arrow node.
372          * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green.
373          * @param {Number} opacity
374          * @param {JXG.GeometryElement} el The element the arrows are to be attached to
375          */
376         _setArrowColor: function (node, color, opacity, el, type) {
377             if (node) {
378                 if (Type.isString(color)) {
379                     if (type !== 7) {
380                         this._setAttribute(function () {
381                             node.setAttributeNS(null, 'stroke', color);
382                             node.setAttributeNS(null, 'fill', color);
383                             node.setAttributeNS(null, 'stroke-opacity', opacity);
384                             node.setAttributeNS(null, 'fill-opacity', opacity);
385                         }, el.visPropOld.fillcolor);
386                     } else {
387                         this._setAttribute(function () {
388                             node.setAttributeNS(null, 'fill', 'none');
389                             node.setAttributeNS(null, 'stroke', color);
390                             node.setAttributeNS(null, 'stroke-opacity', opacity);
391                         }, el.visPropOld.fillcolor);
392                     }
393                 }
394 
395                 if (this.isIE) {
396                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
397                 }
398             }
399 
400         },
401 
402         // Already documented in JXG.AbstractRenderer
403         _setArrowWidth: function (node, width, parentNode, size) {
404             var s, d;
405 
406             if (node) {
407                 // if (width === 0) {
408                 //     // display:none does not work well in webkit
409                 //     node.setAttributeNS(null, 'display', 'none');
410                 // } else {
411                     s = width;
412                     d = s * size;
413                     node.setAttributeNS(null, 'viewBox', (0) + ' ' + (0) + ' ' + (s * 10) + ' ' + (s * 10));
414                     node.setAttributeNS(null, 'markerHeight', d);
415                     node.setAttributeNS(null, 'markerWidth', d);
416                     node.setAttributeNS(null, 'display', 'inherit');
417                 // }
418 
419                 if (this.isIE) {
420                     parentNode.parentNode.insertBefore(parentNode, parentNode);
421                 }
422             }
423         },
424 
425         /* ******************************** *
426          *  This renderer does not need to
427          *  override draw/update* methods
428          *  since it provides draw/update*Prim
429          *  methods except for some cases like
430          *  internal texts or images.
431          * ******************************** */
432 
433         /* **************************
434          *    Lines
435          * **************************/
436 
437         // documented in AbstractRenderer
438         updateTicks: function (ticks) {
439             var i, j, c, node, x, y,
440                 tickStr = '',
441                 len = ticks.ticks.length,
442                 len2, str,
443                 isReal = true;
444 
445             for (i = 0; i < len; i++) {
446                 c = ticks.ticks[i];
447                 x = c[0];
448                 y = c[1];
449 
450                 len2 = x.length;
451                 str = ' M ' + x[0] + ' ' + y[0];
452                 if (!Type.isNumber(x[0])) {
453                     isReal = false;
454                 }
455                 for (j = 1; isReal && j < len2; ++j) {
456                     if (Type.isNumber(x[j])) {
457                         str += ' L ' + x[j] + ' ' + y[j];
458                     } else {
459                         isReal = false;
460                     }
461 
462                 }
463                 if (isReal) {
464                     tickStr += str;
465                 }
466             }
467 
468             node = ticks.rendNode;
469 
470             if (!Type.exists(node)) {
471                 node = this.createPrim('path', ticks.id);
472                 this.appendChildPrim(node, Type.evaluate(ticks.visProp.layer));
473                 ticks.rendNode = node;
474             }
475 
476             node.setAttributeNS(null, 'stroke', Type.evaluate(ticks.visProp.strokecolor));
477             node.setAttributeNS(null, 'fill', 'none');
478             // node.setAttributeNS(null, 'fill', Type.evaluate(ticks.visProp.fillcolor));
479             // node.setAttributeNS(null, 'fill-opacity', Type.evaluate(ticks.visProp.fillopacity));
480             node.setAttributeNS(null, 'stroke-opacity', Type.evaluate(ticks.visProp.strokeopacity));
481             node.setAttributeNS(null, 'stroke-width', Type.evaluate(ticks.visProp.strokewidth));
482             this.updatePathPrim(node, tickStr, ticks.board);
483         },
484 
485         /* **************************
486          *    Text related stuff
487          * **************************/
488 
489         // Already documented in JXG.AbstractRenderer
490         displayCopyright: function (str, fontsize) {
491             var node = this.createPrim('text', 'licenseText'),
492                 t;
493             node.setAttributeNS(null, 'x', '20px');
494             node.setAttributeNS(null, 'y', (2 + fontsize) + 'px');
495             node.setAttributeNS(null, "style", "font-family:Arial,Helvetica,sans-serif; font-size:" + fontsize + "px; fill:#356AA0;  opacity:0.3;");
496             t = this.container.ownerDocument.createTextNode(str);
497             node.appendChild(t);
498             this.appendChildPrim(node, 0);
499         },
500 
501         // Already documented in JXG.AbstractRenderer
502         drawInternalText: function (el) {
503             var node = this.createPrim('text', el.id);
504 
505             //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
506             // Preserve spaces
507             //node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve");
508             node.style.whiteSpace = 'nowrap';
509 
510             el.rendNodeText = this.container.ownerDocument.createTextNode('');
511             node.appendChild(el.rendNodeText);
512             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
513 
514             return node;
515         },
516 
517         // Already documented in JXG.AbstractRenderer
518         updateInternalText: function (el) {
519             var content = el.plaintext, v,
520                 ev_ax = el.getAnchorX(),
521                 ev_ay = el.getAnchorY();
522 
523             if (el.rendNode.getAttributeNS(null, "class") !== el.visProp.cssclass) {
524                 el.rendNode.setAttributeNS(null, "class", Type.evaluate(el.visProp.cssclass));
525                 el.needsSizeUpdate = true;
526             }
527 
528             if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
529                 // Horizontal
530                 v = el.coords.scrCoords[1];
531                 if (el.visPropOld.left !== (ev_ax + v)) {
532                     el.rendNode.setAttributeNS(null, 'x', v + 'px');
533 
534                     if (ev_ax === 'left') {
535                         el.rendNode.setAttributeNS(null, 'text-anchor', 'start');
536                     } else if (ev_ax === 'right') {
537                         el.rendNode.setAttributeNS(null, 'text-anchor', 'end');
538                     } else if (ev_ax === 'middle') {
539                         el.rendNode.setAttributeNS(null, 'text-anchor', 'middle');
540                     }
541                     el.visPropOld.left = ev_ax + v;
542                 }
543 
544                 // Vertical
545                 v = el.coords.scrCoords[2];
546                 if (el.visPropOld.top !== (ev_ay + v)) {
547                     el.rendNode.setAttributeNS(null, 'y', (v + this.vOffsetText * 0.5) + 'px');
548 
549                     if (ev_ay === 'bottom') {
550                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-after-edge');
551                     } else if (ev_ay === 'top') {
552                         el.rendNode.setAttributeNS(null, 'dy', '1.6ex');
553                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge'); // Not supported by IE, edge
554                     } else if (ev_ay === 'middle') {
555                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
556                         el.rendNode.setAttributeNS(null, 'dy', '0.6ex');
557                     }
558                     el.visPropOld.top = ev_ay + v;
559                 }
560             }
561             if (el.htmlStr !== content) {
562                 el.rendNodeText.data = content;
563                 el.htmlStr = content;
564             }
565             this.transformImage(el, el.transformations);
566         },
567 
568         /**
569          * Set color and opacity of internal texts.
570          * SVG needs its own version.
571          * @private
572          * @see JXG.AbstractRenderer#updateTextStyle
573          * @see JXG.AbstractRenderer#updateInternalTextStyle
574          */
575         updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) {
576             this.setObjectFillColor(el, strokeColor, strokeOpacity);
577         },
578 
579         /* **************************
580          *    Image related stuff
581          * **************************/
582 
583         // Already documented in JXG.AbstractRenderer
584         drawImage: function (el) {
585             var node = this.createPrim('image', el.id);
586 
587             node.setAttributeNS(null, 'preserveAspectRatio', 'none');
588             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
589             el.rendNode = node;
590 
591             this.updateImage(el);
592         },
593 
594         // Already documented in JXG.AbstractRenderer
595         transformImage: function (el, t) {
596             var s, m,
597                 node = el.rendNode,
598                 str = "",
599                 len = t.length;
600 
601             if (len > 0) {
602                 m = this.joinTransforms(el, t);
603                 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(',');
604                 str += ' matrix(' + s + ') ';
605                 node.setAttributeNS(null, 'transform', str);
606             }
607         },
608 
609         // Already documented in JXG.AbstractRenderer
610         updateImageURL: function (el) {
611             var url = Type.evaluate(el.url);
612 
613             if (el._src !== url) {
614                 el.imgIsLoaded = false;
615                 el.rendNode.setAttributeNS(this.xlinkNamespace, 'xlink:href', url);
616                 el._src = url;
617 
618                 return true;
619             }
620 
621             return false;
622         },
623 
624         // Already documented in JXG.AbstractRenderer
625         updateImageStyle: function (el, doHighlight) {
626             var css = Type.evaluate(doHighlight ? el.visProp.highlightcssclass : el.visProp.cssclass);
627 
628             el.rendNode.setAttributeNS(null, 'class', css);
629         },
630 
631         // Already documented in JXG.AbstractRenderer
632         drawForeignObject: function (el) {
633             el.rendNode = this.appendChildPrim(this.createPrim('foreignObject', el.id),
634                                     Type.evaluate(el.visProp.layer));
635 
636             this.appendNodesToElement(el, 'foreignObject');
637             this.updateForeignObject(el);
638         },
639 
640         // Already documented in JXG.AbstractRenderer
641         updateForeignObject: function(el) {
642             if (el._useUserSize) {
643                 el.rendNode.style.overflow = 'hidden';
644             } else {
645                 el.rendNode.style.overflow = 'visible';
646             }
647 
648             this.updateRectPrim(el.rendNode, el.coords.scrCoords[1],
649                 el.coords.scrCoords[2] - el.size[1], el.size[0], el.size[1]);
650 
651             el.rendNode.innerHTML = el.content;
652             this._updateVisual(el, {stroke: true, dash: true}, true);
653         },
654 
655         /* **************************
656          * Render primitive objects
657          * **************************/
658 
659         // Already documented in JXG.AbstractRenderer
660         appendChildPrim: function (node, level) {
661             if (!Type.exists(level)) { // trace nodes have level not set
662                 level = 0;
663             } else if (level >= Options.layer.numlayers) {
664                 level = Options.layer.numlayers - 1;
665             }
666 
667             this.layer[level].appendChild(node);
668 
669             return node;
670         },
671 
672         // Already documented in JXG.AbstractRenderer
673         createPrim: function (type, id) {
674             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
675             node.setAttributeNS(null, 'id', this.container.id + '_' + id);
676             node.style.position = 'absolute';
677             if (type === 'path') {
678                 node.setAttributeNS(null, 'stroke-linecap', 'round');
679                 node.setAttributeNS(null, 'stroke-linejoin', 'round');
680                 node.setAttributeNS(null, 'fill-rule', 'evenodd');
681             }
682             return node;
683         },
684 
685         // Already documented in JXG.AbstractRenderer
686         remove: function (shape) {
687             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
688                 shape.parentNode.removeChild(shape);
689             }
690         },
691 
692         // Already documented in JXG.AbstractRenderer
693         setLayer: function (el, level) {
694             if (!Type.exists(level)) {
695                 level = 0;
696             } else if (level >= Options.layer.numlayers) {
697                 level = Options.layer.numlayers - 1;
698             }
699 
700             this.layer[level].appendChild(el.rendNode);
701         },
702 
703         // Already documented in JXG.AbstractRenderer
704         makeArrows: function (el, a) {
705             var node2,
706                 ev_fa = a.evFirst,
707                 ev_la = a.evLast;
708 
709             // Test if the arrow heads already exist
710             if (el.visPropOld.firstarrow === ev_fa &&
711                 el.visPropOld.lastarrow === ev_la) {
712                 if (this.isIE && el.visPropCalc.visible &&
713                     (ev_fa || ev_la)) {
714                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
715                 }
716                 return;
717             }
718 
719             if (ev_fa) {
720                 node2 = el.rendNodeTriangleStart;
721                 if (!Type.exists(node2)) {
722                     node2 = this._createArrowHead(el, 'End', a.typeFirst);
723                     this.defs.appendChild(node2);
724                     el.rendNodeTriangleStart = node2;
725                     el.rendNode.setAttributeNS(null, 'marker-start', 'url(#' + this.container.id + '_' + el.id + 'TriangleEnd)');
726                 } else {
727                     this.defs.appendChild(node2);
728                 }
729             } else {
730                 node2 = el.rendNodeTriangleStart;
731                 if (Type.exists(node2)) {
732                     this.remove(node2);
733                 }
734             }
735             if (ev_la) {
736                 node2 = el.rendNodeTriangleEnd;
737                 if (!Type.exists(node2)) {
738                     node2 = this._createArrowHead(el, 'Start', a.typeLast);
739                     this.defs.appendChild(node2);
740                     el.rendNodeTriangleEnd = node2;
741                     el.rendNode.setAttributeNS(null, 'marker-end', 'url(#' + this.container.id + '_' + el.id + 'TriangleStart)');
742                 } else {
743                     this.defs.appendChild(node2);
744                 }
745             } else {
746                 node2 = el.rendNodeTriangleEnd;
747                 if (Type.exists(node2)) {
748                     this.remove(node2);
749                 }
750             }
751             el.visPropOld.firstarrow = ev_fa;
752             el.visPropOld.lastarrow = ev_la;
753         },
754 
755         // Already documented in JXG.AbstractRenderer
756         updateEllipsePrim: function (node, x, y, rx, ry) {
757             var huge = 1000000;
758 
759             huge = 200000; // IE
760             // webkit does not like huge values if the object is dashed
761             // iE doesn't like huge values above 216000
762             x = Math.abs(x) < huge ? x : huge * x / Math.abs(x);
763             y = Math.abs(y) < huge ? y : huge * y / Math.abs(y);
764             rx = Math.abs(rx) < huge ? rx : huge * rx / Math.abs(rx);
765             ry = Math.abs(ry) < huge ? ry : huge * ry / Math.abs(ry);
766 
767             node.setAttributeNS(null, 'cx', x);
768             node.setAttributeNS(null, 'cy', y);
769             node.setAttributeNS(null, 'rx', Math.abs(rx));
770             node.setAttributeNS(null, 'ry', Math.abs(ry));
771         },
772 
773         // Already documented in JXG.AbstractRenderer
774         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
775             var huge = 1000000;
776 
777             huge = 200000; //IE
778             if (!isNaN(p1x + p1y + p2x + p2y)) {
779                 // webkit does not like huge values if the object is dashed
780                 // IE doesn't like huge values above 216000
781                 p1x = Math.abs(p1x) < huge ? p1x : huge * p1x / Math.abs(p1x);
782                 p1y = Math.abs(p1y) < huge ? p1y : huge * p1y / Math.abs(p1y);
783                 p2x = Math.abs(p2x) < huge ? p2x : huge * p2x / Math.abs(p2x);
784                 p2y = Math.abs(p2y) < huge ? p2y : huge * p2y / Math.abs(p2y);
785 
786                 node.setAttributeNS(null, 'x1', p1x);
787                 node.setAttributeNS(null, 'y1', p1y);
788                 node.setAttributeNS(null, 'x2', p2x);
789                 node.setAttributeNS(null, 'y2', p2y);
790             }
791         },
792 
793         // Already documented in JXG.AbstractRenderer
794         updatePathPrim: function (node, pointString) {
795             if (pointString === '') {
796                 pointString = 'M 0 0';
797             }
798             node.setAttributeNS(null, 'd', pointString);
799         },
800 
801         // Already documented in JXG.AbstractRenderer
802         updatePathStringPoint: function (el, size, type) {
803             var s = '',
804                 scr = el.coords.scrCoords,
805                 sqrt32 = size * Math.sqrt(3) * 0.5,
806                 s05 = size * 0.5;
807 
808             if (type === 'x') {
809                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2] - size) +
810                     ' L ' + (scr[1] + size) + ' ' + (scr[2] + size) +
811                     ' M ' + (scr[1] + size) + ' ' + (scr[2] - size) +
812                     ' L ' + (scr[1] - size) + ' ' + (scr[2] + size);
813             } else if (type === '+') {
814                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
815                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
816                     ' M ' + (scr[1]) + ' ' + (scr[2] - size) +
817                     ' L ' + (scr[1]) + ' ' + (scr[2] + size);
818             } else if (type === '<>') {
819                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
820                     ' L ' + (scr[1]) + ' ' + (scr[2] + size) +
821                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
822                     ' L ' + (scr[1]) + ' ' + (scr[2] - size) + ' Z ';
823             } else if (type === '^') {
824                 s = ' M ' + (scr[1]) + ' ' + (scr[2] - size) +
825                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] + s05) +
826                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] + s05) +
827                     ' Z ';  // close path
828             } else if (type === 'v') {
829                 s = ' M ' + (scr[1]) + ' ' + (scr[2] + size) +
830                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] - s05) +
831                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] - s05) +
832                     ' Z ';
833             } else if (type === '>') {
834                 s = ' M ' + (scr[1] + size) + ' ' + (scr[2]) +
835                     ' L ' + (scr[1] - s05) + ' ' + (scr[2] - sqrt32) +
836                     ' L ' + (scr[1] - s05) + ' ' + (scr[2] + sqrt32) +
837                     ' Z ';
838             } else if (type === '<') {
839                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
840                     ' L ' + (scr[1] + s05) + ' ' + (scr[2] - sqrt32) +
841                     ' L ' + (scr[1] + s05) + ' ' + (scr[2] + sqrt32) +
842                     ' Z ';
843             }
844             return s;
845         },
846 
847         // Already documented in JXG.AbstractRenderer
848         updatePathStringPrim: function (el) {
849             var i, scr, len,
850                 symbm = ' M ',
851                 symbl = ' L ',
852                 symbc = ' C ',
853                 nextSymb = symbm,
854                 maxSize = 5000.0,
855                 pStr = '';
856 
857             if (el.numberPoints <= 0) {
858                 return '';
859             }
860 
861             len = Math.min(el.points.length, el.numberPoints);
862 
863             if (el.bezierDegree === 1) {
864                 for (i = 0; i < len; i++) {
865                     scr = el.points[i].scrCoords;
866                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
867                         nextSymb = symbm;
868                     } else {
869                         // Chrome has problems with values being too far away.
870                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
871                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
872 
873                         // Attention: first coordinate may be inaccurate if far way
874                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
875                         pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
876                         nextSymb = symbl;
877                     }
878                 }
879             } else if (el.bezierDegree === 3) {
880                 i = 0;
881                 while (i < len) {
882                     scr = el.points[i].scrCoords;
883                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
884                         nextSymb = symbm;
885                     } else {
886                         pStr += nextSymb + scr[1] + ' ' + scr[2];
887                         if (nextSymb === symbc) {
888                             i += 1;
889                             scr = el.points[i].scrCoords;
890                             pStr += ' ' + scr[1] + ' ' + scr[2];
891                             i += 1;
892                             scr = el.points[i].scrCoords;
893                             pStr += ' ' + scr[1] + ' ' + scr[2];
894                         }
895                         nextSymb = symbc;
896                     }
897                     i += 1;
898                 }
899             }
900             return pStr;
901         },
902 
903         // Already documented in JXG.AbstractRenderer
904         updatePathStringBezierPrim: function (el) {
905             var i, j, k, scr, lx, ly, len,
906                 symbm = ' M ',
907                 symbl = ' C ',
908                 nextSymb = symbm,
909                 maxSize = 5000.0,
910                 pStr = '',
911                 f = Type.evaluate(el.visProp.strokewidth),
912                 isNoPlot = (Type.evaluate(el.visProp.curvetype) !== 'plot');
913 
914             if (el.numberPoints <= 0) {
915                 return '';
916             }
917 
918             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
919                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
920             }
921 
922             len = Math.min(el.points.length, el.numberPoints);
923             for (j = 1; j < 3; j++) {
924                 nextSymb = symbm;
925                 for (i = 0; i < len; i++) {
926                     scr = el.points[i].scrCoords;
927 
928                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
929                         nextSymb = symbm;
930                     } else {
931                         // Chrome has problems with values being too far away.
932                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
933                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
934 
935                         // Attention: first coordinate may be inaccurate if far way
936                         if (nextSymb === symbm) {
937                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
938                             pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
939                         } else {
940                             k = 2 * j;
941                             pStr += [nextSymb,
942                                 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)), ' ',
943                                 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)), ' ',
944                                 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)), ' ',
945                                 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)), ' ',
946                                 scr[1], ' ', scr[2]].join('');
947                         }
948 
949                         nextSymb = symbl;
950                         lx = scr[1];
951                         ly = scr[2];
952                     }
953                 }
954             }
955             return pStr;
956         },
957 
958         // Already documented in JXG.AbstractRenderer
959         updatePolygonPrim: function (node, el) {
960             var i,
961                 pStr = '',
962                 scrCoords,
963                 len = el.vertices.length;
964 
965             node.setAttributeNS(null, 'stroke', 'none');
966             node.setAttributeNS(null, 'fill-rule', 'evenodd');
967             if (el.elType === 'polygonalchain') {
968                 len++;
969             }
970 
971             for (i = 0; i < len - 1; i++) {
972                 if (el.vertices[i].isReal) {
973                     scrCoords = el.vertices[i].coords.scrCoords;
974                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
975                 } else {
976                     node.setAttributeNS(null, 'points', '');
977                     return;
978                 }
979 
980                 if (i < len - 2) {
981                     pStr += " ";
982                 }
983             }
984             if (pStr.indexOf('NaN') === -1) {
985                 node.setAttributeNS(null, 'points', pStr);
986             }
987         },
988 
989         // Already documented in JXG.AbstractRenderer
990         updateRectPrim: function (node, x, y, w, h) {
991             node.setAttributeNS(null, 'x', x);
992             node.setAttributeNS(null, 'y', y);
993             node.setAttributeNS(null, 'width', w);
994             node.setAttributeNS(null, 'height', h);
995         },
996 
997         /* **************************
998          *  Set Attributes
999          * **************************/
1000 
1001         // documented in JXG.AbstractRenderer
1002         setPropertyPrim: function (node, key, val) {
1003             if (key === 'stroked') {
1004                 return;
1005             }
1006             node.setAttributeNS(null, key, val);
1007         },
1008 
1009         display: function (el, val) {
1010             var node;
1011 
1012             if (el && el.rendNode) {
1013                 el.visPropOld.visible = val;
1014                 node = el.rendNode;
1015                 if (val) {
1016                     node.setAttributeNS(null, 'display', 'inline');
1017                     node.style.visibility = "inherit";
1018                 } else {
1019                     node.setAttributeNS(null, 'display', 'none');
1020                     node.style.visibility = "hidden";
1021                 }
1022             }
1023         },
1024 
1025         // documented in JXG.AbstractRenderer
1026         show: function (el) {
1027             JXG.deprecated('Board.renderer.show()', 'Board.renderer.display()');
1028             this.display(el, true);
1029             // var node;
1030             //
1031             // if (el && el.rendNode) {
1032             //     node = el.rendNode;
1033             //     node.setAttributeNS(null, 'display', 'inline');
1034             //     node.style.visibility = "inherit";
1035             // }
1036         },
1037 
1038         // documented in JXG.AbstractRenderer
1039         hide: function (el) {
1040             JXG.deprecated('Board.renderer.hide()', 'Board.renderer.display()');
1041             this.display(el, false);
1042             // var node;
1043             //
1044             // if (el && el.rendNode) {
1045             //     node = el.rendNode;
1046             //     node.setAttributeNS(null, 'display', 'none');
1047             //     node.style.visibility = "hidden";
1048             // }
1049         },
1050 
1051         // documented in JXG.AbstractRenderer
1052         setBuffering: function (el, type) {
1053             el.rendNode.setAttribute('buffered-rendering', type);
1054         },
1055 
1056         // documented in JXG.AbstractRenderer
1057         setDashStyle: function (el) {
1058             var dashStyle = Type.evaluate(el.visProp.dash),
1059                 node = el.rendNode;
1060 
1061             if (dashStyle > 0) {
1062                 node.setAttributeNS(null, 'stroke-dasharray', this.dashArray[dashStyle - 1]);
1063             } else {
1064                 if (node.hasAttributeNS(null, 'stroke-dasharray')) {
1065                     node.removeAttributeNS(null, 'stroke-dasharray');
1066                 }
1067             }
1068         },
1069 
1070         // documented in JXG.AbstractRenderer
1071         setGradient: function (el) {
1072             var fillNode = el.rendNode,
1073                 node, node2, node3,
1074                 ev_g = Type.evaluate(el.visProp.gradient);
1075 
1076             if (ev_g === 'linear' || ev_g === 'radial') {
1077                 node = this.createPrim(ev_g + 'Gradient', el.id + '_gradient');
1078                 node2 = this.createPrim('stop', el.id + '_gradient1');
1079                 node3 = this.createPrim('stop', el.id + '_gradient2');
1080                 node.appendChild(node2);
1081                 node.appendChild(node3);
1082                 this.defs.appendChild(node);
1083                 fillNode.setAttributeNS(null, 'style', 'fill:url(#' + this.container.id + '_' + el.id + '_gradient)');
1084                 el.gradNode1 = node2;
1085                 el.gradNode2 = node3;
1086                 el.gradNode = node;
1087             } else {
1088                 fillNode.removeAttributeNS(null, 'style');
1089             }
1090         },
1091 
1092         /**
1093          * Set the gradient angle for linear color gradients.
1094          *
1095          * @private
1096          * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
1097          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
1098          */
1099         updateGradientAngle: function(node, radians) {
1100             // Angles:
1101             // 0: ->
1102             // 90: down
1103             // 180: <-
1104             // 90: up
1105             var f = 1.0,
1106                 co = Math.cos(radians),
1107                 si = Math.sin(radians);
1108 
1109             if (Math.abs(co) > Math.abs(si)) {
1110                 f /= Math.abs(co);
1111             } else {
1112                 f /= Math.abs(si);
1113             }
1114 
1115             if (co >= 0) {
1116                 node.setAttributeNS(null, 'x1', 0);
1117                 node.setAttributeNS(null, 'x2', co * f);
1118             } else {
1119                 node.setAttributeNS(null, 'x1', -co * f);
1120                 node.setAttributeNS(null, 'x2', 0);
1121             }
1122             if (si >= 0) {
1123                 node.setAttributeNS(null, 'y1', 0);
1124                 node.setAttributeNS(null, 'y2', si * f);
1125             } else {
1126                 node.setAttributeNS(null, 'y1', -si * f);
1127                 node.setAttributeNS(null, 'y2', 0);
1128             }
1129         },
1130 
1131         /**
1132          * Set circles for radial color gradients.
1133          *
1134          * @private
1135          * @param {SVGnode} node SVG gradient node
1136          * @param {Number} cx SVG value cx (value between 0 and 1)
1137          * @param {Number} cy  SVG value cy (value between 0 and 1)
1138          * @param {Number} r  SVG value r (value between 0 and 1)
1139          * @param {Number} fx  SVG value fx (value between 0 and 1)
1140          * @param {Number} fy  SVG value fy (value between 0 and 1)
1141          * @param {Number} fr  SVG value fr (value between 0 and 1)
1142          */
1143         updateGradientCircle: function(node, cx, cy, r, fx, fy, fr) {
1144             node.setAttributeNS(null, 'cx', cx * 100 + '%');   // Center first color
1145             node.setAttributeNS(null, 'cy', cy * 100 + '%');
1146             node.setAttributeNS(null, 'r', r * 100 + '%');
1147             node.setAttributeNS(null, 'fx', fx * 100 + '%');   // Center second color / focal point
1148             node.setAttributeNS(null, 'fy', fy * 100 + '%');
1149             node.setAttributeNS(null, 'fr', fr * 100 + '%');
1150         },
1151 
1152         // documented in JXG.AbstractRenderer
1153         updateGradient: function (el) {
1154             var col, op,
1155                 node2 = el.gradNode1,
1156                 node3 = el.gradNode2,
1157                 ev_g = Type.evaluate(el.visProp.gradient);
1158 
1159             if (!Type.exists(node2) || !Type.exists(node3)) {
1160                 return;
1161             }
1162 
1163             op = Type.evaluate(el.visProp.fillopacity);
1164             op = (op > 0) ? op : 0;
1165             col = Type.evaluate(el.visProp.fillcolor);
1166 
1167             node2.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
1168             node3.setAttributeNS(null, 'style',
1169                     'stop-color:'    + Type.evaluate(el.visProp.gradientsecondcolor) +
1170                     ';stop-opacity:' + Type.evaluate(el.visProp.gradientsecondopacity)
1171                 );
1172             node2.setAttributeNS(null, 'offset', Type.evaluate(el.visProp.gradientstartoffset) * 100 + '%');
1173             node3.setAttributeNS(null, 'offset', Type.evaluate(el.visProp.gradientendoffset) * 100 + '%');
1174             if (ev_g === 'linear') {
1175                 this.updateGradientAngle(el.gradNode, Type.evaluate(el.visProp.gradientangle));
1176             } else if (ev_g === 'radial') {
1177                 this.updateGradientCircle(el.gradNode,
1178                     Type.evaluate(el.visProp.gradientcx),
1179                     Type.evaluate(el.visProp.gradientcy),
1180                     Type.evaluate(el.visProp.gradientr),
1181                     Type.evaluate(el.visProp.gradientfx),
1182                     Type.evaluate(el.visProp.gradientfy),
1183                     Type.evaluate(el.visProp.gradientfr)
1184                 );
1185             }
1186         },
1187 
1188         // documented in JXG.AbstractRenderer
1189         setObjectTransition: function (el, duration) {
1190             var node, transitionStr,
1191                 i, len,
1192                 nodes = ['rendNode',
1193                     'rendNodeTriangleStart',
1194                     'rendNodeTriangleEnd'];
1195 
1196             if (duration === undefined) {
1197                 duration = Type.evaluate(el.visProp.transitionduration);
1198             }
1199 
1200             if (duration === el.visPropOld.transitionduration) {
1201                 return;
1202             }
1203 
1204             if (el.elementClass === Const.OBJECT_CLASS_TEXT &&
1205                 Type.evaluate(el.visProp.display) === 'html') {
1206                 transitionStr = ' color ' + duration + 'ms,' +
1207                     ' opacity ' + duration + 'ms';
1208             } else {
1209                 transitionStr = ' fill ' + duration + 'ms,' +
1210                     ' fill-opacity ' + duration + 'ms,' +
1211                     ' stroke ' + duration + 'ms,' +
1212                     ' stroke-opacity ' + duration + 'ms';
1213             }
1214 
1215             len = nodes.length;
1216             for (i = 0; i < len; ++i) {
1217                 if (el[nodes[i]]) {
1218                     node = el[nodes[i]];
1219                     node.style.transition = transitionStr;
1220                 }
1221             }
1222 
1223             el.visPropOld.transitionduration = duration;
1224         },
1225 
1226         /**
1227          * Call user-defined function to set visual attributes.
1228          * If "testAttribute" is the empty string, the function
1229          * is called immediately, otherwise it is called in a timeOut.
1230          *
1231          * This is necessary to realize smooth transitions but avoid transitions
1232          * when first creating the objects.
1233          *
1234          * Usually, the string in testAttribute is the visPropOld attribute
1235          * of the values which are set.
1236          *
1237          * @param {Function} setFunc       Some function which usually sets some attributes
1238          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
1239          *                               otherwise it is called in a setImeout.
1240          * @see JXG.SVGRenderer#setObjectFillColor
1241          * @see JXG.SVGRenderer#setObjectStrokeColor
1242          * @see JXG.SVGRenderer#_setArrowColor
1243          * @private
1244          */
1245         _setAttribute: function (setFunc, testAttribute) {
1246             if (testAttribute === '') {
1247                 setFunc();
1248             } else {
1249                 window.setTimeout(setFunc, 1);
1250             }
1251         },
1252 
1253         // documented in JXG.AbstractRenderer
1254         setObjectFillColor: function (el, color, opacity, rendNode) {
1255             var node, c, rgbo, oo,
1256                 rgba = Type.evaluate(color),
1257                 o = Type.evaluate(opacity),
1258                 grad = Type.evaluate(el.visProp.gradient);
1259 
1260             o = (o > 0) ? o : 0;
1261 
1262             // TODO  save gradient and gradientangle
1263             if (el.visPropOld.fillcolor === rgba && el.visPropOld.fillopacity === o && grad === null) {
1264                 return;
1265             }
1266 
1267             if (Type.exists(rgba) && rgba !== false) {
1268                 if (rgba.length !== 9) {          // RGB, not RGBA
1269                     c = rgba;
1270                     oo = o;
1271                 } else {                       // True RGBA, not RGB
1272                     rgbo = Color.rgba2rgbo(rgba);
1273                     c = rgbo[0];
1274                     oo = o * rgbo[1];
1275                 }
1276 
1277                 if (rendNode === undefined) {
1278                     node = el.rendNode;
1279                 } else {
1280                     node = rendNode;
1281                 }
1282 
1283                 if (c !== 'none') {
1284                     this._setAttribute(function () {
1285                         node.setAttributeNS(null, 'fill', c);
1286                     }, el.visPropOld.fillcolor);
1287                 }
1288 
1289                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
1290                     this._setAttribute(function () {
1291                         node.setAttributeNS(null, 'opacity', oo);
1292                     }, el.visPropOld.fillopacity);
1293                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
1294                 } else {
1295                     if (c === 'none') {  // This is done only for non-images
1296                         // because images have no fill color.
1297                         oo = 0;
1298                         // This is necessary if there is a foreignObject below.
1299                         node.setAttributeNS(null, 'pointer-events', 'visibleStroke');
1300                     } else {
1301                         // This is the default
1302                         node.setAttributeNS(null, 'pointer-events', 'visiblePainted');
1303                     }
1304                     this._setAttribute(function () {
1305                         node.setAttributeNS(null, 'fill-opacity', oo);
1306                     }, el.visPropOld.fillopacity);
1307                 }
1308 
1309                 if (grad === 'linear' || grad === 'radial') {
1310                     this.updateGradient(el);
1311                 }
1312             }
1313             el.visPropOld.fillcolor = rgba;
1314             el.visPropOld.fillopacity = o;
1315         },
1316 
1317         // documented in JXG.AbstractRenderer
1318         setObjectStrokeColor: function (el, color, opacity) {
1319             var rgba = Type.evaluate(color), c, rgbo,
1320                 o = Type.evaluate(opacity), oo,
1321                 node;
1322 
1323             o = (o > 0) ? o : 0;
1324 
1325             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
1326                 return;
1327             }
1328 
1329             if (Type.exists(rgba) && rgba !== false) {
1330                 if (rgba.length !== 9) {          // RGB, not RGBA
1331                     c = rgba;
1332                     oo = o;
1333                 } else {                       // True RGBA, not RGB
1334                     rgbo = Color.rgba2rgbo(rgba);
1335                     c = rgbo[0];
1336                     oo = o * rgbo[1];
1337                 }
1338 
1339                 node = el.rendNode;
1340 
1341                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
1342                     if (Type.evaluate(el.visProp.display) === 'html') {
1343                         this._setAttribute(function () {
1344                             node.style.color = c;
1345                             node.style.opacity = oo;
1346                         }, el.visPropOld.strokecolor);
1347 
1348                     } else {
1349                         this._setAttribute(function () {
1350                             node.setAttributeNS(null, "style", "fill:" + c);
1351                             node.setAttributeNS(null, "style", "fill-opacity:" + oo);
1352                         }, el.visPropOld.strokecolor);
1353                     }
1354                 } else {
1355                     this._setAttribute(function () {
1356                         node.setAttributeNS(null, 'stroke', c);
1357                         node.setAttributeNS(null, 'stroke-opacity', oo);
1358                     }, el.visPropOld.strokecolor);
1359                 }
1360 
1361                 if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
1362                     el.elementClass === Const.OBJECT_CLASS_LINE) {
1363                     if (Type.evaluate(el.visProp.firstarrow)) {
1364                         this._setArrowColor(el.rendNodeTriangleStart, c, oo, el, el.visPropCalc.typeFirst);
1365                     }
1366 
1367                     if (Type.evaluate(el.visProp.lastarrow)) {
1368                         this._setArrowColor(el.rendNodeTriangleEnd, c, oo, el, el.visPropCalc.typeLast);
1369                     }
1370                 }
1371             }
1372 
1373             el.visPropOld.strokecolor = rgba;
1374             el.visPropOld.strokeopacity = o;
1375         },
1376 
1377         // documented in JXG.AbstractRenderer
1378         setObjectStrokeWidth: function (el, width) {
1379             var node,
1380                 w = Type.evaluate(width);
1381 
1382             if (isNaN(w) || el.visPropOld.strokewidth === w) {
1383                 return;
1384             }
1385 
1386             node = el.rendNode;
1387             this.setPropertyPrim(node, 'stroked', 'true');
1388             if (Type.exists(w)) {
1389                 this.setPropertyPrim(node, 'stroke-width', w + 'px');
1390 
1391                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
1392                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
1393                 //     if (Type.evaluate(el.visProp.firstarrow)) {
1394                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
1395                 //     }
1396                 //
1397                 //     if (Type.evaluate(el.visProp.lastarrow)) {
1398                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
1399                 //     }
1400                 // }
1401             }
1402             el.visPropOld.strokewidth = w;
1403         },
1404 
1405         // documented in JXG.AbstractRenderer
1406         setLineCap: function (el) {
1407             var capStyle = Type.evaluate(el.visProp.linecap);
1408 
1409             if (capStyle === undefined || capStyle === '' || el.visPropOld.linecap === capStyle ||
1410                 !Type.exists(el.rendNode)) {
1411                 return;
1412             }
1413 
1414             this.setPropertyPrim(el.rendNode, 'stroke-linecap', capStyle);
1415             el.visPropOld.linecap = capStyle;
1416 
1417         },
1418 
1419         // documented in JXG.AbstractRenderer
1420         setShadow: function (el) {
1421             var ev_s = Type.evaluate(el.visProp.shadow);
1422                 // ev_c = Type.evaluate(el.visProp.shadowcolor),
1423                 // c;
1424 
1425             if (el.visPropOld.shadow === ev_s) {
1426                 return;
1427             }
1428 
1429             // c = JXG.rgbParser(ev_c);
1430 
1431             if (Type.exists(el.rendNode)) {
1432                 if (ev_s) {
1433                     el.rendNode.setAttributeNS(null, 'filter', 'url(#' + this.container.id + '_' + 'f1)');
1434                 } else {
1435                     el.rendNode.removeAttributeNS(null, 'filter');
1436                 }
1437             }
1438             el.visPropOld.shadow = ev_s;
1439         },
1440 
1441         /* **************************
1442          * renderer control
1443          * **************************/
1444 
1445         // documented in JXG.AbstractRenderer
1446         suspendRedraw: function () {
1447             // It seems to be important for the Linux version of firefox
1448             //this.suspendHandle = this.svgRoot.suspendRedraw(10000);
1449         },
1450 
1451         // documented in JXG.AbstractRenderer
1452         unsuspendRedraw: function () {
1453             //this.svgRoot.unsuspendRedraw(this.suspendHandle);
1454             //this.svgRoot.unsuspendRedrawAll();
1455             //this.svgRoot.forceRedraw();
1456         },
1457 
1458         // documented in AbstractRenderer
1459         resize: function (w, h) {
1460             // this.svgRoot.style.width  = parseFloat(w) + 'px';
1461             // this.svgRoot.style.height = parseFloat(h) + 'px';
1462 
1463             this.svgRoot.setAttribute('width',  parseFloat(w));
1464             this.svgRoot.setAttribute('height', parseFloat(h));
1465             // this.svgRoot.setAttribute('width',  '100%');
1466             // this.svgRoot.setAttribute('height', '100%');
1467         },
1468 
1469         // documented in JXG.AbstractRenderer
1470         createTouchpoints: function (n) {
1471             var i, na1, na2, node;
1472             this.touchpoints = [];
1473             for (i = 0; i < n; i++) {
1474                 na1 = 'touchpoint1_' + i;
1475                 node = this.createPrim('path', na1);
1476                 this.appendChildPrim(node, 19);
1477                 node.setAttributeNS(null, 'd', 'M 0 0');
1478                 this.touchpoints.push(node);
1479 
1480                 this.setPropertyPrim(node, 'stroked', 'true');
1481                 this.setPropertyPrim(node, 'stroke-width', '1px');
1482                 node.setAttributeNS(null, 'stroke', '#000000');
1483                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
1484                 node.setAttributeNS(null, 'display', 'none');
1485 
1486                 na2 = 'touchpoint2_' + i;
1487                 node = this.createPrim('ellipse', na2);
1488                 this.appendChildPrim(node, 19);
1489                 this.updateEllipsePrim(node, 0, 0, 0, 0);
1490                 this.touchpoints.push(node);
1491 
1492                 this.setPropertyPrim(node, 'stroked', 'true');
1493                 this.setPropertyPrim(node, 'stroke-width', '1px');
1494                 node.setAttributeNS(null, 'stroke', '#000000');
1495                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
1496                 node.setAttributeNS(null, 'fill', '#ffffff');
1497                 node.setAttributeNS(null, 'fill-opacity', 0.0);
1498 
1499                 node.setAttributeNS(null, 'display', 'none');
1500             }
1501         },
1502 
1503         // documented in JXG.AbstractRenderer
1504         showTouchpoint: function (i) {
1505             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1506                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'inline');
1507                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'inline');
1508             }
1509         },
1510 
1511         // documented in JXG.AbstractRenderer
1512         hideTouchpoint: function (i) {
1513             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1514                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'none');
1515                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'none');
1516             }
1517         },
1518 
1519         // documented in JXG.AbstractRenderer
1520         updateTouchpoint: function (i, pos) {
1521             var x, y,
1522                 d = 37;
1523 
1524             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1525                 x = pos[0];
1526                 y = pos[1];
1527 
1528                 this.touchpoints[2 * i].setAttributeNS(null, 'd', 'M ' + (x - d) + ' ' + y + ' ' +
1529                     'L ' + (x + d) + ' ' + y + ' ' +
1530                     'M ' + x + ' ' + (y - d) + ' ' +
1531                     'L ' + x + ' ' + (y + d));
1532                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
1533             }
1534         },
1535 
1536         /**
1537          * Walk recursively through the DOM subtree of a node and collect all
1538          * value attributes together with the id of that node.
1539          * <b>Attention:</b> Only values of nodes having a valid id are taken.
1540          * @param  {Node} node   root node of DOM subtree that will be searched recursively.
1541          * @return {Array}      Array with entries of the form [id, value]
1542          * @private
1543          */
1544         _getValuesOfDOMElements: function (node) {
1545             var values = [];
1546             if (node.nodeType === 1) {
1547                 node = node.firstChild;
1548                 while (node) {
1549                     if (node.id !== undefined && node.value !== undefined) {
1550                         values.push([node.id, node.value]);
1551                     }
1552                     values = values.concat(this._getValuesOfDOMElements(node));
1553                     node = node.nextSibling;
1554                 }
1555             }
1556             return values;
1557         },
1558 
1559         _getDataUri: function (url, callback) {
1560             var image = new Image();
1561 
1562             image.onload = function () {
1563                 var canvas = document.createElement('canvas');
1564                 canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
1565                 canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
1566 
1567                 canvas.getContext('2d').drawImage(this, 0, 0);
1568 
1569                 callback(canvas.toDataURL('image/png'));
1570                 canvas.remove();
1571             };
1572 
1573             image.src = url;
1574         },
1575 
1576         _getImgDataURL: function(svgRoot) {
1577             var images, len, canvas, ctx,
1578                 ur, i;
1579 
1580             images = svgRoot.getElementsByTagName("image");
1581             len = images.length;
1582             if (len > 0) {
1583                 canvas = document.createElement('canvas');
1584                 //img = new Image();
1585                 for (i = 0; i < len; i++) {
1586                     images[i].setAttribute("crossorigin", "anonymous");
1587                     //img.src = images[i].href;
1588                     //img.onload = function() {
1589                     // img.crossOrigin = "anonymous";
1590                     ctx = canvas.getContext('2d');
1591                     canvas.width = images[i].getAttribute("width");
1592                     canvas.height = images[i].getAttribute("height");
1593                     try {
1594                         ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
1595 
1596                         // If the image is not png, the format must be specified here
1597                         ur = canvas.toDataURL();
1598                         images[i].setAttribute("xlink:href", ur);
1599                     } catch (err) {
1600                         console.log("CORS problem! Image can not be used", err);
1601                     }
1602                 }
1603                 //canvas.remove();
1604             }
1605             return true;
1606         },
1607 
1608         /**
1609          * Return a data URI of the SVG code representeing the construction.
1610          * The SVG code of the construction is base64 encoded. The return string starts
1611          * with "data:image/svg+xml;base64,...".
1612          *
1613          * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
1614          * This is necessary for older versions of Safari. Default: false
1615          * @returns {String}  data URI string
1616          */
1617         dumpToDataURI: function (ignoreTexts) {
1618             var svgRoot = this.svgRoot,
1619                 btoa = window.btoa || Base64.encode,
1620                 svg,
1621                 virtualNode, doc,
1622                 i, len,
1623                 values = [];
1624 
1625             // Move all HTML tags (beside the SVG root) of the container
1626             // to the foreignObject element inside of the svgRoot node
1627             // Problem:
1628             // input values are not copied. This can be verified by looking at an innerHTML output
1629             // of an input element. Therefore, we do it "by hand".
1630             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
1631                 if (!ignoreTexts) {
1632                     this.foreignObjLayer.setAttribute('display', 'inline');
1633                 }
1634                 while (svgRoot.nextSibling) {
1635 
1636                     // Copy all value attributes
1637                     values = values.concat(this._getValuesOfDOMElements(svgRoot.nextSibling));
1638 
1639                     this.foreignObjLayer.appendChild(svgRoot.nextSibling);
1640                 }
1641             }
1642 
1643             this._getImgDataURL(svgRoot);
1644 
1645             // Convert the SVG graphic into a string containing SVG code
1646             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
1647             svg = new XMLSerializer().serializeToString(svgRoot);
1648 
1649             if (ignoreTexts !== true) {
1650                 // Handle SVG texts
1651                 // Insert all value attributes back into the svg string
1652                 len = values.length;
1653                 for (i = 0; i < len; i++) {
1654                     svg = svg.replace('id="' + values[i][0] + '"', 'id="' + values[i][0] + '" value="' + values[i][1] + '"');
1655                 }
1656             }
1657 
1658             // if (false) {
1659             //     // Debug: use example svg image
1660             //     svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="220" height="220"><rect width="66" height="30" x="21" y="32" stroke="#204a87" stroke-width="2" fill="none" /></svg>';
1661             // }
1662 
1663             // In IE we have to remove the namespace again.
1664             if ((svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1) {
1665                 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, '');
1666             }
1667 
1668             // Safari fails if the svg string contains a " "
1669             // Obsolete with Safari 12+
1670             svg = svg.replace(/ /g, ' ');
1671 
1672             // Move all HTML tags back from
1673             // the foreignObject element to the container
1674             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
1675                 // Restore all HTML elements
1676                 while (this.foreignObjLayer.firstChild) {
1677                     this.container.appendChild(this.foreignObjLayer.firstChild);
1678                 }
1679                 this.foreignObjLayer.setAttribute("display", "none");
1680             }
1681 
1682             return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
1683         },
1684 
1685         /**
1686          * Convert the SVG construction into an HTML canvas image.
1687          * This works for all SVG supporting browsers. Implemented as Promise.
1688          * <p>
1689          * For IE, it is realized as function.
1690          * It works from version 9, with the exception that HTML texts
1691          * are ignored on IE. The drawing is done with a delay of
1692          * 200 ms. Otherwise there would be problems with IE.
1693          *
1694          * @param {String} canvasId Id of an HTML canvas element
1695          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
1696          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
1697          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
1698          * This is necessary for older versions of Safari. Default: false
1699          * @returns {Promise}  Promise object
1700          *
1701          * @example
1702          * 	board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
1703          *
1704          * @example
1705          *  // IE 11 example:
1706          * 	board.renderer.dumpToCanvas('canvas');
1707          * 	setTimeout(function() { console.log('done'); }, 400);
1708          */
1709         dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
1710             var svg, tmpImg, cv, ctx,
1711                 doc = this.container.ownerDocument;
1712 
1713             // Prepare the canvas element
1714             cv = doc.getElementById(canvasId);
1715 
1716             // Clear the canvas
1717             /* eslint-disable no-self-assign */
1718             cv.width = cv.width;
1719             /* eslint-enable no-self-assign */
1720 
1721             ctx = cv.getContext("2d");
1722             if (w !== undefined && h !== undefined) {
1723                 cv.style.width = parseFloat(w) + 'px';
1724                 cv.style.height = parseFloat(h) + 'px';
1725                 // Scale twice the CSS size to make the image crisp
1726                 // cv.setAttribute('width', 2 * parseFloat(wOrg));
1727                 // cv.setAttribute('height', 2 * parseFloat(hOrg));
1728                 // ctx.scale(2 * wOrg / w, 2 * hOrg / h);
1729                 cv.setAttribute('width', parseFloat(w));
1730                 cv.setAttribute('height', parseFloat(h));
1731             }
1732 
1733             // Display the SVG string as data-uri in an HTML img.
1734             tmpImg = new Image();
1735             svg = this.dumpToDataURI(ignoreTexts);
1736             tmpImg.src = svg;
1737 
1738             // Finally, draw the HTML img in the canvas.
1739             if (!('Promise' in window)) {
1740                 tmpImg.onload = function () {
1741                     // IE needs a pause...
1742                     // Seems to be broken
1743                     window.setTimeout(function() {
1744                         try {
1745                             ctx.drawImage(tmpImg, 0, 0, w, h);
1746                         } catch (err) {
1747                             console.log("screenshots not longer supported on IE");
1748                         }
1749                     }, 200);
1750                 };
1751                 return this;
1752             }
1753 
1754             return new Promise(function(resolve, reject) {
1755                 try {
1756                     tmpImg.onload = function () {
1757                         ctx.drawImage(tmpImg, 0, 0, w, h);
1758                         resolve();
1759                     };
1760                 } catch (e) {
1761                     reject(e);
1762                 }
1763             });
1764 
1765         },
1766 
1767         /**
1768          * Display SVG image in html img-tag which enables
1769          * easy download for the user.
1770          *
1771          * Support:
1772          * <ul>
1773          * <li> IE: No
1774          * <li> Edge: full
1775          * <li>Firefox: full
1776          * <li> Chrome: full
1777          * <li> Safari: full (No text support in versions prior to 12).
1778          * </ul>
1779          *
1780          * @param {JXG.Board} board Link to the board.
1781          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
1782          * the screenshot is copied to this img object. The width and height will be set to the values of the
1783          * JSXGraph container.
1784          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
1785          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
1786          * @return {Object}       the svg renderer object
1787          */
1788         screenshot: function (board, imgId, ignoreTexts) {
1789             var node,
1790                 doc = this.container.ownerDocument,
1791                 parent = this.container.parentNode,
1792                 cPos,
1793                 canvas, id,
1794                 img,
1795                 button, buttonText,
1796                 w, h,
1797                 bas = board.attr.screenshot,
1798                 zbar, zbarDisplay, cssTxt,
1799                 newImg = false,
1800                 _copyCanvasToImg,
1801                 isDebug = false;
1802 
1803             if (this.type === 'no') {
1804                 return this;
1805             }
1806 
1807             w = bas.scale * this.container.getBoundingClientRect().width;
1808             h = bas.scale * this.container.getBoundingClientRect().height;
1809 
1810             if (imgId === undefined || imgId === '') {
1811                 newImg = true;
1812                 img = new Image(); //doc.createElement('img');
1813                 img.style.width = w + 'px';
1814                 img.style.height = h + 'px';
1815             } else {
1816                 newImg = false;
1817                 img = doc.getElementById(imgId);
1818             }
1819             // img.crossOrigin = 'anonymous';
1820 
1821             // Create div which contains canvas element and close button
1822             if (newImg) {
1823                 node = doc.createElement('div');
1824                 node.style.cssText = bas.css;
1825                 node.style.width = (w) + 'px';
1826                 node.style.height = (h) + 'px';
1827                 node.style.zIndex = this.container.style.zIndex + 120;
1828 
1829                 // Try to position the div exactly over the JSXGraph board
1830                 node.style.position = 'absolute';
1831                 node.style.top = this.container.offsetTop + 'px';
1832                 node.style.left = this.container.offsetLeft + 'px';
1833             }
1834 
1835             if (!isDebug) {
1836                 // Create canvas element and add it to the DOM
1837                 // It will be removed after the image has been stored.
1838                 canvas = doc.createElement('canvas');
1839                 id = Math.random().toString(36).substr(2, 5);
1840                 canvas.setAttribute('id', id);
1841                 canvas.setAttribute('width', w);
1842                 canvas.setAttribute('height', h);
1843                 canvas.style.width = w + 'px';
1844                 canvas.style.height = w + 'px';
1845                 canvas.style.display = 'none';
1846                 parent.appendChild(canvas);
1847             } else {
1848                 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
1849                 id = 'jxgbox_canvas';
1850                 // canvas = document.getElementById(id);
1851                 canvas = doc.getElementById(id);
1852             }
1853 
1854             if (newImg) {
1855                 // Create close button
1856                 button = doc.createElement('span');
1857                 buttonText = doc.createTextNode('\u2716');
1858                 button.style.cssText = bas.cssButton;
1859                 button.appendChild(buttonText);
1860                 button.onclick = function () {
1861                     node.parentNode.removeChild(node);
1862                 };
1863 
1864                 // Add all nodes
1865                 node.appendChild(img);
1866                 node.appendChild(button);
1867                 parent.insertBefore(node, this.container.nextSibling);
1868             }
1869 
1870             // Hide navigation bar in board
1871             // zbar = document.getElementById(this.container.id + '_navigationbar');
1872             zbar = doc.getElementById(this.container.id + '_navigationbar');
1873             if (Type.exists(zbar)) {
1874                 zbarDisplay = zbar.style.display;
1875                 zbar.style.display = 'none';
1876             }
1877 
1878             _copyCanvasToImg = function() {
1879                 // Show image in img tag
1880                 img.src = canvas.toDataURL('image/png');
1881 
1882                 // Remove canvas node
1883                 if (!isDebug) {
1884                     parent.removeChild(canvas);
1885                 }
1886             };
1887 
1888             // Create screenshot in image element
1889             if ('Promise' in window) {
1890                 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
1891             } else {
1892                 // IE
1893                 this.dumpToCanvas(id, w, h, ignoreTexts);
1894                 window.setTimeout(_copyCanvasToImg, 200);
1895             }
1896 
1897             // Show navigation bar in board
1898             if (Type.exists(zbar)) {
1899                 zbar.style.display = zbarDisplay;
1900             }
1901 
1902             return this;
1903         }
1904 
1905     });
1906 
1907     return JXG.SVGRenderer;
1908 });
1909