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