1 /*
  2     Copyright 2008-2024
  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 <https://www.gnu.org/licenses/>
 29     and <https://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 import JXG from "../jxg.js";
 36 import Options from "../options.js";
 37 import AbstractRenderer from "./abstract.js";
 38 import Const from "../base/constants.js";
 39 import Type from "../utils/type.js";
 40 import Color from "../utils/color.js";
 41 import Base64 from "../utils/base64.js";
 42 import Numerics from "../math/numerics.js";
 43 
 44 /**
 45  * Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
 46  * @class JXG.SVGRenderer
 47  * @augments JXG.AbstractRenderer
 48  * @param {Node} container Reference to a DOM node containing the board.
 49  * @param {Object} dim The dimensions of the board
 50  * @param {Number} dim.width
 51  * @param {Number} dim.height
 52  * @see JXG.AbstractRenderer
 53  */
 54 JXG.SVGRenderer = function (container, dim) {
 55     var i;
 56 
 57     // docstring in AbstractRenderer
 58     this.type = "svg";
 59 
 60     this.isIE =
 61         navigator.appVersion.indexOf("MSIE") !== -1 || navigator.userAgent.match(/Trident\//);
 62 
 63     /**
 64      * SVG root node
 65      * @type Node
 66      */
 67     this.svgRoot = null;
 68 
 69     /**
 70      * The SVG Namespace used in JSXGraph.
 71      * @see http://www.w3.org/TR/SVG2/
 72      * @type String
 73      * @default http://www.w3.org/2000/svg
 74      */
 75     this.svgNamespace = "http://www.w3.org/2000/svg";
 76 
 77     /**
 78      * The xlink namespace. This is used for images.
 79      * @see http://www.w3.org/TR/xlink/
 80      * @type String
 81      * @default http://www.w3.org/1999/xlink
 82      */
 83     this.xlinkNamespace = "http://www.w3.org/1999/xlink";
 84 
 85     // container is documented in AbstractRenderer.
 86     // Type node
 87     this.container = container;
 88 
 89     // prepare the div container and the svg root node for use with JSXGraph
 90     this.container.style.MozUserSelect = "none";
 91     this.container.style.userSelect = "none";
 92 
 93     this.container.style.overflow = "hidden";
 94     if (this.container.style.position === "") {
 95         this.container.style.position = "relative";
 96     }
 97 
 98     this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, "svg");
 99     this.svgRoot.style.overflow = "hidden";
100     this.svgRoot.style.display = "block";
101     this.resize(dim.width, dim.height);
102 
103     //this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision');
104 
105     this.container.appendChild(this.svgRoot);
106 
107     /**
108      * The <tt>defs</tt> element is a container element to reference reusable SVG elements.
109      * @type Node
110      * @see https://www.w3.org/TR/SVG2/struct.html#DefsElement
111      */
112     this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, "defs");
113     this.svgRoot.appendChild(this.defs);
114 
115     /**
116      * Filters are used to apply shadows.
117      * @type Node
118      * @see https://www.w3.org/TR/SVG2/struct.html#DefsElement
119      */
120     /**
121      * Create an SVG shadow filter. If the object's RGB color is [r,g,b], it's opacity is op, and
122      * the parameter color is given as [r', g', b'] with opacity op'
123      * the shadow will have RGB color [blend*r + r', blend*g + g', blend*b + b'] and the opacity will be equal to op * op'.
124      * Further, blur and offset can be adjusted.
125      *
126      * The shadow color is [r*ble
127      * @param {String} id Node is of the filter.
128      * @param {Array|String} rgb RGB value for the blend color or the string 'none' for default values. Default 'black'.
129      * @param {Number} opacity Value between 0 and 1, default is 1.
130      * @param {Number} blend  Value between 0 and 1, default is 0.1.
131      * @param {Number} blur  Default: 3
132      * @param {Array} offset [dx, dy]. Default is [5,5].
133      * @returns DOM node to be added to this.defs.
134      * @private
135      */
136     this.createShadowFilter = function (id, rgb, opacity, blend, blur, offset) {
137         var filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter'),
138             feOffset, feColor, feGaussianBlur, feBlend,
139             mat;
140 
141         filter.setAttributeNS(null, 'id', id);
142         filter.setAttributeNS(null, 'width', '300%');
143         filter.setAttributeNS(null, 'height', '300%');
144         filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
145 
146         feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
147         feOffset.setAttributeNS(null, 'in', 'SourceGraphic'); // b/w: SourceAlpha, Color: SourceGraphic
148         feOffset.setAttributeNS(null, 'result', 'offOut');
149         feOffset.setAttributeNS(null, 'dx', offset[0]);
150         feOffset.setAttributeNS(null, 'dy', offset[1]);
151         filter.appendChild(feOffset);
152 
153         feColor = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feColorMatrix');
154         feColor.setAttributeNS(null, 'in', 'offOut');
155         feColor.setAttributeNS(null, 'result', 'colorOut');
156         feColor.setAttributeNS(null, 'type', 'matrix');
157         // See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix
158         if (rgb === 'none' || !Type.isArray(rgb) || rgb.length < 3) {
159             feColor.setAttributeNS(null, 'values', '0.1 0 0 0 0  0 0.1 0 0 0  0 0 0.1 0 0  0 0 0 ' + opacity + ' 0');
160         } else {
161             rgb[0] /= 255;
162             rgb[1] /= 255;
163             rgb[2] /= 255;
164             mat = blend + ' 0 0 0 ' + rgb[0] +
165                 '  0 ' + blend + ' 0 0 ' + rgb[1] +
166                 '  0 0 ' + blend + ' 0 ' + rgb[2] +
167                 '  0 0 0 ' + opacity + ' 0';
168             feColor.setAttributeNS(null, 'values', mat);
169         }
170         filter.appendChild(feColor);
171 
172         feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
173         feGaussianBlur.setAttributeNS(null, 'in', 'colorOut');
174         feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
175         feGaussianBlur.setAttributeNS(null, 'stdDeviation', blur);
176         filter.appendChild(feGaussianBlur);
177 
178         feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
179         feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
180         feBlend.setAttributeNS(null, 'in2', 'blurOut');
181         feBlend.setAttributeNS(null, 'mode', 'normal');
182         filter.appendChild(feBlend);
183 
184         return filter;
185     };
186 
187     /**
188      * Create a "unique" string id from the arguments of the function.
189      * Concatenate all arguments by "_".
190      * "Unique" is achieved by simply prepending the container id.
191      * Do not escape the string.
192      *
193      * If the id is used in an "url()" call it must be eascaped.
194      *
195      * @params {String} one or strings which will be concatenated.
196      * @return {String}
197      * @private
198      */
199     this.uniqName = function () {
200         return this.container.id + '_' +
201             Array.prototype.slice.call(arguments).join('_');
202     };
203 
204     /**
205      * Combine arguments to a string, joined by empty string.
206      * Masks the container id with CSS.escape.
207      *
208      * @params {String} str variable number of strings
209      * @returns String
210      * @see JXG.SVGRenderer#toURL
211      * @private
212      * @example
213      * this.toStr('aaa', '_', 'bbb', 'TriangleEnd')
214      * // Output:
215      * // xxx_bbbTriangleEnd
216      */
217     this.toStr = function() {
218         // ES6 would be [...arguments].join()
219         var str = Array.prototype.slice.call(arguments).join('');
220         // Mask special symbols like '/' and '\' in id
221         if (Type.exists(CSS) && Type.exists(CSS.escape)) {
222             str = CSS.escape(str);
223         }
224         return str;
225     };
226 
227     /**
228      * Combine arguments to an URL string of the form
229      * url(#...)
230      * Masks the container id. Calls {@link JXG.SVGRenderer#toStr}.
231      *
232      * @params {String} str variable number of strings
233      * @returns URL string
234      * @see JXG.SVGRenderer#toStr
235      * @private
236      * @example
237      * this.toURL('aaa', '_', 'bbb', 'TriangleEnd')
238      * // Output:
239      * // url(#xxx_bbbTriangleEnd)
240      */
241     this.toURL = function () {
242         return 'url(#' +
243             this.toStr.apply(this, arguments) + // Pass the arguments to toStr
244             ')';
245     };
246 
247     /* Default shadow filter */
248     this.defs.appendChild(this.createShadowFilter(this.uniqName('f1'), 'none', 1, 0.1, 3, [5, 5]));
249 
250     /**
251      * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
252      * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
253      * there, too. The higher the number, the "more on top" are the elements on this layer.
254      * @type Array
255      */
256     this.layer = [];
257     for (i = 0; i < Options.layer.numlayers; i++) {
258         this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
259         this.svgRoot.appendChild(this.layer[i]);
260     }
261 
262     try {
263         this.foreignObjLayer = this.container.ownerDocument.createElementNS(
264             this.svgNamespace,
265             "foreignObject"
266         );
267         this.foreignObjLayer.setAttribute("display", "none");
268         this.foreignObjLayer.setAttribute("x", 0);
269         this.foreignObjLayer.setAttribute("y", 0);
270         this.foreignObjLayer.setAttribute("width", "100%");
271         this.foreignObjLayer.setAttribute("height", "100%");
272         this.foreignObjLayer.setAttribute("id", this.uniqName('foreignObj'));
273         this.svgRoot.appendChild(this.foreignObjLayer);
274         this.supportsForeignObject = true;
275     } catch (e) {
276         this.supportsForeignObject = false;
277     }
278 };
279 
280 JXG.SVGRenderer.prototype = new AbstractRenderer();
281 
282 JXG.extend(
283     JXG.SVGRenderer.prototype,
284     /** @lends JXG.SVGRenderer.prototype */ {
285         /* ******************************** *
286          *  This renderer does not need to
287          *  override draw/update* methods
288          *  since it provides draw/update*Prim
289          *  methods except for some cases like
290          *  internal texts or images.
291          * ******************************** */
292 
293         /* ********* Arrow head related stuff *********** */
294 
295         /**
296          * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
297          * @private
298          * @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached.
299          * @param {String} [idAppendix=''] A string that is added to the node's id.
300          * @returns {Node} Reference to the node added to the DOM.
301          */
302         _createArrowHead: function (el, idAppendix, type) {
303             var node2,
304                 node3,
305                 id = el.id + "Triangle",
306                 //type = null,
307                 v,
308                 h;
309 
310             if (Type.exists(idAppendix)) {
311                 id += idAppendix;
312             }
313             if (Type.exists(type)) {
314                 id += type;
315             }
316             node2 = this.createPrim("marker", id);
317 
318             node2.setAttributeNS(null, "stroke", el.evalVisProp('strokecolor'));
319             node2.setAttributeNS(
320                 null,
321                 "stroke-opacity",
322                 el.evalVisProp('strokeopacity')
323             );
324             node2.setAttributeNS(null, "fill", el.evalVisProp('strokecolor'));
325             node2.setAttributeNS(null, "fill-opacity", el.evalVisProp('strokeopacity'));
326             node2.setAttributeNS(null, "stroke-width", 0); // this is the stroke-width of the arrow head.
327             // Should be zero to simplify the calculations
328 
329             node2.setAttributeNS(null, "orient", "auto");
330             node2.setAttributeNS(null, "markerUnits", "strokeWidth"); // 'strokeWidth' 'userSpaceOnUse');
331 
332             /*
333                Types 1, 2:
334                The arrow head is an isosceles triangle with base length 10 and height 10.
335 
336                Type 3:
337                A rectangle
338 
339                Types 4, 5, 6:
340                Defined by Bezier curves from mp_arrowheads.html
341 
342                In any case but type 3 the arrow head is 10 units long,
343                type 3 is 10 units high.
344                These 10 units are scaled to strokeWidth * arrowSize pixels, see
345                this._setArrowWidth().
346 
347                See also abstractRenderer.updateLine() where the line path is shortened accordingly.
348 
349                Changes here are also necessary in setArrowWidth().
350 
351                So far, lines with arrow heads are shortenend to avoid overlapping of
352                arrow head and line. This is not the case for curves, yet.
353                Therefore, the offset refX has to be adapted to the path type.
354             */
355             node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, "path");
356             h = 5;
357             if (idAppendix === "Start") {
358                 // First arrow
359                 v = 0;
360                 if (type === 2) {
361                     node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 L 5,5 z");
362                 } else if (type === 3) {
363                     node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z");
364                 } else if (type === 4) {
365                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
366                     h = 3.31;
367                     node3.setAttributeNS(
368                         null,
369                         "d",
370                         "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"
371                     );
372                 } else if (type === 5) {
373                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
374                     h = 3.28;
375                     node3.setAttributeNS(
376                         null,
377                         "d",
378                         "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"
379                     );
380                 } else if (type === 6) {
381                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
382                     h = 2.84;
383                     node3.setAttributeNS(
384                         null,
385                         "d",
386                         "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"
387                     );
388                 } else if (type === 7) {
389                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
390                     h = 5.2;
391                     node3.setAttributeNS(
392                         null,
393                         "d",
394                         "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"
395                     );
396                 } else {
397                     // type == 1 or > 6
398                     node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 z");
399                 }
400                 if (
401                     // !Type.exists(el.rendNode.getTotalLength) &&
402                     el.elementClass === Const.OBJECT_CLASS_LINE
403                 ) {
404                     if (type === 2) {
405                         v = 4.9;
406                     } else if (type === 3) {
407                         v = 3.3;
408                     } else if (type === 4 || type === 5 || type === 6) {
409                         v = 6.66;
410                     } else if (type === 7) {
411                         v = 0.0;
412                     } else {
413                         v = 10.0;
414                     }
415                 }
416             } else {
417                 // Last arrow
418                 v = 10.0;
419                 if (type === 2) {
420                     node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 L 5,5 z");
421                 } else if (type === 3) {
422                     v = 3.3;
423                     node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z");
424                 } else if (type === 4) {
425                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
426                     h = 3.31;
427                     node3.setAttributeNS(
428                         null,
429                         "d",
430                         "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"
431                     );
432                 } else if (type === 5) {
433                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
434                     h = 3.28;
435                     node3.setAttributeNS(
436                         null,
437                         "d",
438                         "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"
439                     );
440                 } else if (type === 6) {
441                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
442                     h = 2.84;
443                     node3.setAttributeNS(
444                         null,
445                         "d",
446                         "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"
447                     );
448                 } else if (type === 7) {
449                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
450                     h = 5.2;
451                     node3.setAttributeNS(
452                         null,
453                         "d",
454                         "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"
455                     );
456                 } else {
457                     // type == 1 or > 6
458                     node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 z");
459                 }
460                 if (
461                     // !Type.exists(el.rendNode.getTotalLength) &&
462                     el.elementClass === Const.OBJECT_CLASS_LINE
463                 ) {
464                     if (type === 2) {
465                         v = 5.1;
466                     } else if (type === 3) {
467                         v = 0.02;
468                     } else if (type === 4 || type === 5 || type === 6) {
469                         v = 3.33;
470                     } else if (type === 7) {
471                         v = 10.0;
472                     } else {
473                         v = 0.05;
474                     }
475                 }
476             }
477             if (type === 7) {
478                 node2.setAttributeNS(null, "fill", "none");
479                 node2.setAttributeNS(null, "stroke-width", 1); // this is the stroke-width of the arrow head.
480             }
481             node2.setAttributeNS(null, "refY", h);
482             node2.setAttributeNS(null, "refX", v);
483 
484             node2.appendChild(node3);
485             return node2;
486         },
487 
488         /**
489          * Updates color of an arrow DOM node.
490          * @param {Node} node The arrow node.
491          * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green.
492          * @param {Number} opacity
493          * @param {JXG.GeometryElement} el The element the arrows are to be attached to
494          */
495         _setArrowColor: function (node, color, opacity, el, type) {
496             if (node) {
497                 if (Type.isString(color)) {
498                     if (type !== 7) {
499                         this._setAttribute(function () {
500                             node.setAttributeNS(null, "stroke", color);
501                             node.setAttributeNS(null, "fill", color);
502                             node.setAttributeNS(null, "stroke-opacity", opacity);
503                             node.setAttributeNS(null, "fill-opacity", opacity);
504                         }, el.visPropOld.fillcolor);
505                     } else {
506                         this._setAttribute(function () {
507                             node.setAttributeNS(null, "fill", "none");
508                             node.setAttributeNS(null, "stroke", color);
509                             node.setAttributeNS(null, "stroke-opacity", opacity);
510                         }, el.visPropOld.fillcolor);
511                     }
512                 }
513 
514                 // if (this.isIE) {
515                     // Necessary, since Safari is the new IE (11.2024)
516                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
517                 // }
518             }
519         },
520 
521         // Already documented in JXG.AbstractRenderer
522         _setArrowWidth: function (node, width, parentNode, size) {
523             var s, d;
524 
525             if (node) {
526                 // if (width === 0) {
527                 //     // display:none does not work well in webkit
528                 //     node.setAttributeNS(null, 'display', 'none');
529                 // } else {
530                 s = width;
531                 d = s * size;
532                 node.setAttributeNS(null, "viewBox", 0 + " " + 0 + " " + s * 10 + " " + s * 10);
533                 node.setAttributeNS(null, "markerHeight", d);
534                 node.setAttributeNS(null, "markerWidth", d);
535                 node.setAttributeNS(null, "display", "inherit");
536                 // }
537 
538                 // if (this.isIE) {
539                     // Necessary, since Safari is the new IE (11.2024)
540                     parentNode.parentNode.insertBefore(parentNode, parentNode);
541                 // }
542             }
543         },
544 
545         /* ********* Line related stuff *********** */
546 
547         // documented in AbstractRenderer
548         updateTicks: function (ticks) {
549             var i,
550                 j,
551                 c,
552                 node,
553                 x,
554                 y,
555                 tickStr = "",
556                 len = ticks.ticks.length,
557                 len2,
558                 str,
559                 isReal = true;
560 
561             for (i = 0; i < len; i++) {
562                 c = ticks.ticks[i];
563                 x = c[0];
564                 y = c[1];
565 
566                 len2 = x.length;
567                 str = " M " + x[0] + " " + y[0];
568                 if (!Type.isNumber(x[0])) {
569                     isReal = false;
570                 }
571                 for (j = 1; isReal && j < len2; ++j) {
572                     if (Type.isNumber(x[j])) {
573                         str += " L " + x[j] + " " + y[j];
574                     } else {
575                         isReal = false;
576                     }
577                 }
578                 if (isReal) {
579                     tickStr += str;
580                 }
581             }
582 
583             node = ticks.rendNode;
584 
585             if (!Type.exists(node)) {
586                 node = this.createPrim("path", ticks.id);
587                 this.appendChildPrim(node, ticks.evalVisProp('layer'));
588                 ticks.rendNode = node;
589             }
590 
591             node.setAttributeNS(null, "stroke", ticks.evalVisProp('strokecolor'));
592             node.setAttributeNS(null, "fill", "none");
593             // node.setAttributeNS(null, 'fill', ticks.evalVisProp('fillcolor'));
594             // node.setAttributeNS(null, 'fill-opacity', ticks.evalVisProp('fillopacity'));
595             node.setAttributeNS(
596                 null,
597                 "stroke-opacity",
598                 ticks.evalVisProp('strokeopacity')
599             );
600             node.setAttributeNS(null, "stroke-width", ticks.evalVisProp('strokewidth'));
601             this.updatePathPrim(node, tickStr, ticks.board);
602         },
603 
604         /* ********* Text related stuff *********** */
605 
606         // Already documented in JXG.AbstractRenderer
607         displayCopyright: function (str, fontsize) {
608             var node = this.createPrim("text", 'licenseText'),
609                 t;
610             node.setAttributeNS(null, 'x', '20px');
611             node.setAttributeNS(null, 'y', 2 + fontsize + 'px');
612             node.setAttributeNS(null, 'style', 'font-family:Arial,Helvetica,sans-serif; font-size:' +
613                 fontsize + 'px; fill:#356AA0;  opacity:0.3;');
614             t = this.container.ownerDocument.createTextNode(str);
615             node.setAttributeNS(null, 'aria-hidden', 'true');  // should NEVER be in screen reader
616             node.appendChild(t);
617             this.appendChildPrim(node, 0);
618         },
619 
620         // Already documented in JXG.AbstractRenderer
621         drawInternalText: function (el) {
622             var node = this.createPrim("text", el.id);
623 
624             //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
625             // Preserve spaces
626             //node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve");
627             node.style.whiteSpace = "nowrap";
628 
629             el.rendNodeText = this.container.ownerDocument.createTextNode("");
630             node.appendChild(el.rendNodeText);
631             this.appendChildPrim(node, el.evalVisProp('layer'));
632 
633             return node;
634         },
635 
636         // Already documented in JXG.AbstractRenderer
637         updateInternalText: function (el) {
638             var content = el.plaintext,
639                 v, css,
640                 ev_ax = el.getAnchorX(),
641                 ev_ay = el.getAnchorY();
642 
643             css = el.evalVisProp('cssclass');
644             if (el.rendNode.getAttributeNS(null, "class") !== css) {
645                 el.rendNode.setAttributeNS(null, "class", css);
646                 el.needsSizeUpdate = true;
647             }
648 
649             if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
650                 // Horizontal
651                 v = el.coords.scrCoords[1];
652                 if (el.visPropOld.left !== ev_ax + v) {
653                     el.rendNode.setAttributeNS(null, "x", v + "px");
654 
655                     if (ev_ax === "left") {
656                         el.rendNode.setAttributeNS(null, "text-anchor", "start");
657                     } else if (ev_ax === "right") {
658                         el.rendNode.setAttributeNS(null, "text-anchor", "end");
659                     } else if (ev_ax === "middle") {
660                         el.rendNode.setAttributeNS(null, "text-anchor", "middle");
661                     }
662                     el.visPropOld.left = ev_ax + v;
663                 }
664 
665                 // Vertical
666                 v = el.coords.scrCoords[2];
667                 if (el.visPropOld.top !== ev_ay + v) {
668                     el.rendNode.setAttributeNS(null, "y", v + this.vOffsetText * 0.5 + "px");
669 
670                     // Not supported by IE, edge
671                     // el.rendNode.setAttributeNS(null, "dy", "0");
672                     // if (ev_ay === "bottom") {
673                     //     el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-after-edge');
674                     // } else if (ev_ay === "top") {
675                     //     el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge');
676                     // } else if (ev_ay === "middle") {
677                     //     el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
678                     // }
679 
680                     if (ev_ay === "bottom") {
681                         el.rendNode.setAttributeNS(null, "dy", "0");
682                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'auto');
683                     } else if (ev_ay === "top") {
684                         el.rendNode.setAttributeNS(null, "dy", "1.6ex");
685                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'auto');
686                     } else if (ev_ay === "middle") {
687                         el.rendNode.setAttributeNS(null, "dy", "0.6ex");
688                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'auto');
689                     }
690                     el.visPropOld.top = ev_ay + v;
691                 }
692             }
693             if (el.htmlStr !== content) {
694                 el.rendNodeText.data = content;
695                 el.htmlStr = content;
696             }
697             this.transformRect(el, el.transformations);
698         },
699 
700         /**
701          * Set color and opacity of internal texts.
702          * @private
703          * @see JXG.AbstractRenderer#updateTextStyle
704          * @see JXG.AbstractRenderer#updateInternalTextStyle
705          */
706         updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) {
707             this.setObjectFillColor(el, strokeColor, strokeOpacity);
708         },
709 
710         /* ********* Image related stuff *********** */
711 
712         // Already documented in JXG.AbstractRenderer
713         drawImage: function (el) {
714             var node = this.createPrim("image", el.id);
715 
716             node.setAttributeNS(null, "preserveAspectRatio", "none");
717             this.appendChildPrim(node, el.evalVisProp('layer'));
718             el.rendNode = node;
719 
720             this.updateImage(el);
721         },
722 
723         // Already documented in JXG.AbstractRenderer
724         transformRect: function (el, t) {
725             var s, m, node,
726                 str = "",
727                 cx, cy,
728                 len = t.length;
729 
730             if (len > 0) {
731                 node = el.rendNode;
732                 m = this.joinTransforms(el, t);
733                 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(",");
734                 if (s.indexOf('NaN') === -1) {
735                     str += " matrix(" + s + ") ";
736                     if (el.elementClass === Const.OBJECT_CLASS_TEXT && el.visProp.display === 'html') {
737                         node.style.transform = str;
738                         cx = -el.coords.scrCoords[1];
739                         cy = -el.coords.scrCoords[2];
740                         switch (el.evalVisProp('anchorx')) {
741                             case 'right': cx += el.size[0]; break;
742                             case 'middle': cx += el.size[0] * 0.5; break;
743                         }
744                         switch (el.evalVisProp('anchory')) {
745                             case 'bottom': cy += el.size[1]; break;
746                             case 'middle': cy += el.size[1] * 0.5; break;
747                         }
748                         node.style['transform-origin'] = (cx) + 'px ' + (cy) + 'px';
749                     } else {
750                         // Images and texts with display:'internal'
751                         node.setAttributeNS(null, "transform", str);
752                     }
753                 }
754             }
755         },
756 
757         // Already documented in JXG.AbstractRenderer
758         updateImageURL: function (el) {
759             var url = el.eval(el.url);
760 
761             if (el._src !== url) {
762                 el.imgIsLoaded = false;
763                 el.rendNode.setAttributeNS(this.xlinkNamespace, "xlink:href", url);
764                 el._src = url;
765 
766                 return true;
767             }
768 
769             return false;
770         },
771 
772         // Already documented in JXG.AbstractRenderer
773         updateImageStyle: function (el, doHighlight) {
774             var css = el.evalVisProp(
775                 doHighlight ? 'highlightcssclass' : 'cssclass'
776             );
777 
778             el.rendNode.setAttributeNS(null, "class", css);
779         },
780 
781         // Already documented in JXG.AbstractRenderer
782         drawForeignObject: function (el) {
783             el.rendNode = this.appendChildPrim(
784                 this.createPrim("foreignObject", el.id),
785                 el.evalVisProp('layer')
786             );
787 
788             this.appendNodesToElement(el, "foreignObject");
789             this.updateForeignObject(el);
790         },
791 
792         // Already documented in JXG.AbstractRenderer
793         updateForeignObject: function (el) {
794             if (el._useUserSize) {
795                 el.rendNode.style.overflow = "hidden";
796             } else {
797                 el.rendNode.style.overflow = "visible";
798             }
799 
800             this.updateRectPrim(
801                 el.rendNode,
802                 el.coords.scrCoords[1],
803                 el.coords.scrCoords[2] - el.size[1],
804                 el.size[0],
805                 el.size[1]
806             );
807 
808             el.rendNode.innerHTML = el.content;
809             this._updateVisual(el, { stroke: true, dash: true }, true);
810         },
811 
812         /* ********* Render primitive objects *********** */
813 
814         // Already documented in JXG.AbstractRenderer
815         appendChildPrim: function (node, level) {
816             if (!Type.exists(level)) {
817                 // trace nodes have level not set
818                 level = 0;
819             } else if (level >= Options.layer.numlayers) {
820                 level = Options.layer.numlayers - 1;
821             }
822             this.layer[level].appendChild(node);
823 
824             return node;
825         },
826 
827         // Already documented in JXG.AbstractRenderer
828         createPrim: function (type, id) {
829             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
830             node.setAttributeNS(null, "id", this.uniqName(id));
831             node.style.position = "absolute";
832             if (type === "path") {
833                 node.setAttributeNS(null, "stroke-linecap", "round");
834                 node.setAttributeNS(null, "stroke-linejoin", "round");
835                 node.setAttributeNS(null, "fill-rule", "evenodd");
836             }
837 
838             return node;
839         },
840 
841         // Already documented in JXG.AbstractRenderer
842         remove: function (shape) {
843             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
844                 shape.parentNode.removeChild(shape);
845             }
846         },
847 
848         // Already documented in JXG.AbstractRenderer
849         setLayer: function (el, level) {
850             if (!Type.exists(level)) {
851                 level = 0;
852             } else if (level >= Options.layer.numlayers) {
853                 level = Options.layer.numlayers - 1;
854             }
855 
856             this.layer[level].appendChild(el.rendNode);
857         },
858 
859         // Already documented in JXG.AbstractRenderer
860         makeArrows: function (el, a) {
861             var node2, str,
862                 ev_fa = a.evFirst,
863                 ev_la = a.evLast;
864 
865             if (this.isIE && el.visPropCalc.visible && (ev_fa || ev_la)) {
866                 // Necessary, since Safari is the new IE (11.2024)
867                 el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
868                 return;
869             }
870 
871             // We can not compare against visPropOld if there is need for a new arrow head,
872             // since here visPropOld and ev_fa / ev_la already have the same value.
873             // This has been set in _updateVisual.
874             //
875             node2 = el.rendNodeTriangleStart;
876             if (ev_fa) {
877                 str = this.toStr(this.container.id, '_', el.id, 'TriangleStart', a.typeFirst);
878 
879                 // If we try to set the same arrow head as is already set, we can bail out now
880                 if (!Type.exists(node2) || node2.id !== str) {
881                     node2 = this.container.ownerDocument.getElementById(str);
882                     // Check if the marker already exists.
883                     // If not, create a new marker
884                     if (node2 === null) {
885                         node2 = this._createArrowHead(el, "Start", a.typeFirst);
886                         this.defs.appendChild(node2);
887                     }
888                     el.rendNodeTriangleStart = node2;
889                     el.rendNode.setAttributeNS(null, "marker-start", this.toURL(str));
890                 }
891             } else {
892                 if (Type.exists(node2)) {
893                     this.remove(node2);
894                     el.rendNodeTriangleStart = null;
895                 }
896                 el.rendNode.setAttributeNS(null, "marker-start", null);
897             }
898 
899             node2 = el.rendNodeTriangleEnd;
900             if (ev_la) {
901                 str = this.toStr(this.container.id, '_', el.id, 'TriangleEnd', a.typeLast);
902 
903                 // If we try to set the same arrow head as is already set, we can bail out now
904                 if (!Type.exists(node2) || node2.id !== str) {
905                     node2 = this.container.ownerDocument.getElementById(str);
906                     // Check if the marker already exists.
907                     // If not, create a new marker
908                     if (node2 === null) {
909                         node2 = this._createArrowHead(el, "End", a.typeLast);
910                         this.defs.appendChild(node2);
911                     }
912                     el.rendNodeTriangleEnd = node2;
913                     el.rendNode.setAttributeNS(null, "marker-end", this.toURL(str));
914                 }
915             } else {
916                 if (Type.exists(node2)) {
917                     this.remove(node2);
918                     el.rendNodeTriangleEnd = null;
919                 }
920                 el.rendNode.setAttributeNS(null, "marker-end", null);
921             }
922         },
923 
924         // Already documented in JXG.AbstractRenderer
925         updateEllipsePrim: function (node, x, y, rx, ry) {
926             var huge = 1000000;
927 
928             huge = 200000; // IE
929             // webkit does not like huge values if the object is dashed
930             // iE doesn't like huge values above 216000
931             x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x);
932             y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y);
933             rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx);
934             ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry);
935 
936             node.setAttributeNS(null, "cx", x);
937             node.setAttributeNS(null, "cy", y);
938             node.setAttributeNS(null, "rx", Math.abs(rx));
939             node.setAttributeNS(null, "ry", Math.abs(ry));
940         },
941 
942         // Already documented in JXG.AbstractRenderer
943         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
944             var huge = 1000000;
945 
946             huge = 200000; //IE
947             if (!isNaN(p1x + p1y + p2x + p2y)) {
948                 // webkit does not like huge values if the object is dashed
949                 // IE doesn't like huge values above 216000
950                 p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x);
951                 p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y);
952                 p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x);
953                 p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y);
954 
955                 node.setAttributeNS(null, "x1", p1x);
956                 node.setAttributeNS(null, "y1", p1y);
957                 node.setAttributeNS(null, "x2", p2x);
958                 node.setAttributeNS(null, "y2", p2y);
959             }
960         },
961 
962         // Already documented in JXG.AbstractRenderer
963         updatePathPrim: function (node, pointString) {
964             if (pointString === "") {
965                 pointString = "M 0 0";
966             }
967             node.setAttributeNS(null, "d", pointString);
968         },
969 
970         // Already documented in JXG.AbstractRenderer
971         updatePathStringPoint: function (el, size, type) {
972             var s = "",
973                 scr = el.coords.scrCoords,
974                 sqrt32 = size * Math.sqrt(3) * 0.5,
975                 s05 = size * 0.5;
976 
977             if (type === "x") {
978                 s =
979                     " M " +
980                     (scr[1] - size) +
981                     " " +
982                     (scr[2] - size) +
983                     " L " +
984                     (scr[1] + size) +
985                     " " +
986                     (scr[2] + size) +
987                     " M " +
988                     (scr[1] + size) +
989                     " " +
990                     (scr[2] - size) +
991                     " L " +
992                     (scr[1] - size) +
993                     " " +
994                     (scr[2] + size);
995             } else if (type === "+") {
996                 s =
997                     " M " +
998                     (scr[1] - size) +
999                     " " +
1000                     scr[2] +
1001                     " L " +
1002                     (scr[1] + size) +
1003                     " " +
1004                     scr[2] +
1005                     " M " +
1006                     scr[1] +
1007                     " " +
1008                     (scr[2] - size) +
1009                     " L " +
1010                     scr[1] +
1011                     " " +
1012                     (scr[2] + size);
1013             } else if (type === "|") {
1014                 s =
1015                     " M " +
1016                     scr[1] +
1017                     " " +
1018                     (scr[2] - size) +
1019                     " L " +
1020                     scr[1] +
1021                     " " +
1022                     (scr[2] + size);
1023             } else if (type === "-") {
1024                 s =
1025                     " M " +
1026                     (scr[1] - size) +
1027                     " " +
1028                     scr[2] +
1029                     " L " +
1030                     (scr[1] + size) +
1031                     " " +
1032                     scr[2];
1033             } else if (type === "<>" || type === "<<>>") {
1034                 if (type === "<<>>") {
1035                     size *= 1.41;
1036                 }
1037                 s =
1038                     " M " +
1039                     (scr[1] - size) +
1040                     " " +
1041                     scr[2] +
1042                     " L " +
1043                     scr[1] +
1044                     " " +
1045                     (scr[2] + size) +
1046                     " L " +
1047                     (scr[1] + size) +
1048                     " " +
1049                     scr[2] +
1050                     " L " +
1051                     scr[1] +
1052                     " " +
1053                     (scr[2] - size) +
1054                     " Z ";
1055                 } else if (type === "^") {
1056                     s =
1057                     " M " +
1058                     scr[1] +
1059                     " " +
1060                     (scr[2] - size) +
1061                     " L " +
1062                     (scr[1] - sqrt32) +
1063                     " " +
1064                     (scr[2] + s05) +
1065                     " L " +
1066                     (scr[1] + sqrt32) +
1067                     " " +
1068                     (scr[2] + s05) +
1069                     " Z "; // close path
1070             } else if (type === "v") {
1071                 s =
1072                     " M " +
1073                     scr[1] +
1074                     " " +
1075                     (scr[2] + size) +
1076                     " L " +
1077                     (scr[1] - sqrt32) +
1078                     " " +
1079                     (scr[2] - s05) +
1080                     " L " +
1081                     (scr[1] + sqrt32) +
1082                     " " +
1083                     (scr[2] - s05) +
1084                     " Z ";
1085             } else if (type === ">") {
1086                 s =
1087                     " M " +
1088                     (scr[1] + size) +
1089                     " " +
1090                     scr[2] +
1091                     " L " +
1092                     (scr[1] - s05) +
1093                     " " +
1094                     (scr[2] - sqrt32) +
1095                     " L " +
1096                     (scr[1] - s05) +
1097                     " " +
1098                     (scr[2] + sqrt32) +
1099                     " Z ";
1100             } else if (type === "<") {
1101                 s =
1102                     " M " +
1103                     (scr[1] - size) +
1104                     " " +
1105                     scr[2] +
1106                     " L " +
1107                     (scr[1] + s05) +
1108                     " " +
1109                     (scr[2] - sqrt32) +
1110                     " L " +
1111                     (scr[1] + s05) +
1112                     " " +
1113                     (scr[2] + sqrt32) +
1114                     " Z ";
1115             }
1116             return s;
1117         },
1118 
1119         // Already documented in JXG.AbstractRenderer
1120         updatePathStringPrim: function (el) {
1121             var i,
1122                 scr,
1123                 len,
1124                 symbm = " M ",
1125                 symbl = " L ",
1126                 symbc = " C ",
1127                 nextSymb = symbm,
1128                 maxSize = 5000.0,
1129                 pStr = "";
1130 
1131             if (el.numberPoints <= 0) {
1132                 return "";
1133             }
1134 
1135             len = Math.min(el.points.length, el.numberPoints);
1136 
1137             if (el.bezierDegree === 1) {
1138                 for (i = 0; i < len; i++) {
1139                     scr = el.points[i].scrCoords;
1140                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1141                         // PenUp
1142                         nextSymb = symbm;
1143                     } else {
1144                         // Chrome has problems with values being too far away.
1145                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
1146                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
1147 
1148                         // Attention: first coordinate may be inaccurate if far way
1149                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1150                         pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox)
1151                         nextSymb = symbl;
1152                     }
1153                 }
1154             } else if (el.bezierDegree === 3) {
1155                 i = 0;
1156                 while (i < len) {
1157                     scr = el.points[i].scrCoords;
1158                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1159                         // PenUp
1160                         nextSymb = symbm;
1161                     } else {
1162                         pStr += nextSymb + scr[1] + " " + scr[2];
1163                         if (nextSymb === symbc) {
1164                             i += 1;
1165                             scr = el.points[i].scrCoords;
1166                             pStr += " " + scr[1] + " " + scr[2];
1167                             i += 1;
1168                             scr = el.points[i].scrCoords;
1169                             pStr += " " + scr[1] + " " + scr[2];
1170                         }
1171                         nextSymb = symbc;
1172                     }
1173                     i += 1;
1174                 }
1175             }
1176             return pStr;
1177         },
1178 
1179         // Already documented in JXG.AbstractRenderer
1180         updatePathStringBezierPrim: function (el) {
1181             var i, j, k,
1182                 scr,
1183                 lx, ly,
1184                 len,
1185                 symbm = " M ",
1186                 symbl = " C ",
1187                 nextSymb = symbm,
1188                 maxSize = 5000.0,
1189                 pStr = "",
1190                 f = el.evalVisProp('strokewidth'),
1191                 isNoPlot = el.evalVisProp('curvetype') !== "plot";
1192 
1193             if (el.numberPoints <= 0) {
1194                 return "";
1195             }
1196 
1197             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
1198                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
1199             }
1200 
1201             len = Math.min(el.points.length, el.numberPoints);
1202             for (j = 1; j < 3; j++) {
1203                 nextSymb = symbm;
1204                 for (i = 0; i < len; i++) {
1205                     scr = el.points[i].scrCoords;
1206 
1207                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1208                         // PenUp
1209                         nextSymb = symbm;
1210                     } else {
1211                         // Chrome has problems with values being too far away.
1212                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
1213                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
1214 
1215                         // Attention: first coordinate may be inaccurate if far way
1216                         if (nextSymb === symbm) {
1217                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1218                             pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox)
1219                         } else {
1220                             k = 2 * j;
1221                             pStr += [
1222                                 nextSymb,
1223                                 lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j),
1224                                 " ",
1225                                 ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j),
1226                                 " ",
1227                                 lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j),
1228                                 " ",
1229                                 ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j),
1230                                 " ",
1231                                 scr[1],
1232                                 " ",
1233                                 scr[2]
1234                             ].join("");
1235                         }
1236 
1237                         nextSymb = symbl;
1238                         lx = scr[1];
1239                         ly = scr[2];
1240                     }
1241                 }
1242             }
1243             return pStr;
1244         },
1245 
1246         // Already documented in JXG.AbstractRenderer
1247         updatePolygonPrim: function (node, el) {
1248             var i,
1249                 pStr = "",
1250                 scrCoords,
1251                 len = el.vertices.length;
1252 
1253             node.setAttributeNS(null, "stroke", "none");
1254             node.setAttributeNS(null, "fill-rule", "evenodd");
1255             if (el.elType === "polygonalchain") {
1256                 len++;
1257             }
1258 
1259             for (i = 0; i < len - 1; i++) {
1260                 if (el.vertices[i].isReal) {
1261                     scrCoords = el.vertices[i].coords.scrCoords;
1262                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
1263                 } else {
1264                     node.setAttributeNS(null, "points", "");
1265                     return;
1266                 }
1267 
1268                 if (i < len - 2) {
1269                     pStr += " ";
1270                 }
1271             }
1272             if (pStr.indexOf("NaN") === -1) {
1273                 node.setAttributeNS(null, "points", pStr);
1274             }
1275         },
1276 
1277         // Already documented in JXG.AbstractRenderer
1278         updateRectPrim: function (node, x, y, w, h) {
1279             node.setAttributeNS(null, "x", x);
1280             node.setAttributeNS(null, "y", y);
1281             node.setAttributeNS(null, "width", w);
1282             node.setAttributeNS(null, "height", h);
1283         },
1284 
1285         /* ********* Set attributes *********** */
1286 
1287         /**
1288          * Call user-defined function to set visual attributes.
1289          * If "testAttribute" is the empty string, the function
1290          * is called immediately, otherwise it is called in a timeOut.
1291          *
1292          * This is necessary to realize smooth transitions but avoid transitions
1293          * when first creating the objects.
1294          *
1295          * Usually, the string in testAttribute is the visPropOld attribute
1296          * of the values which are set.
1297          *
1298          * @param {Function} setFunc       Some function which usually sets some attributes
1299          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
1300          *                               otherwise it is called in a setImeout.
1301          * @see JXG.SVGRenderer#setObjectFillColor
1302          * @see JXG.SVGRenderer#setObjectStrokeColor
1303          * @see JXG.SVGRenderer#_setArrowColor
1304          * @private
1305          */
1306         _setAttribute: function (setFunc, testAttribute) {
1307             if (testAttribute === "") {
1308                 setFunc();
1309             } else {
1310                 window.setTimeout(setFunc, 1);
1311             }
1312         },
1313 
1314         display: function (el, val) {
1315             var node;
1316 
1317             if (el && el.rendNode) {
1318                 el.visPropOld.visible = val;
1319                 node = el.rendNode;
1320                 if (val) {
1321                     node.setAttributeNS(null, "display", "inline");
1322                     node.style.visibility = "inherit";
1323                 } else {
1324                     node.setAttributeNS(null, "display", "none");
1325                     node.style.visibility = "hidden";
1326                 }
1327             }
1328         },
1329 
1330         // documented in JXG.AbstractRenderer
1331         hide: function (el) {
1332             JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()");
1333             this.display(el, false);
1334         },
1335 
1336         // documented in JXG.AbstractRenderer
1337         setARIA: function(el) {
1338             // This method is only called in abstractRenderer._updateVisual() if aria.enabled == true.
1339             var key, k, v;
1340 
1341             // this.setPropertyPrim(el.rendNode, 'aria-label', el.evalVisProp('aria.label'));
1342             // this.setPropertyPrim(el.rendNode, 'aria-live', el.evalVisProp('aria.live'));
1343             for (key in el.visProp.aria) {
1344                 if (el.visProp.aria.hasOwnProperty(key) && key !== 'enabled') {
1345                     k = 'aria.' + key;
1346                     v = el.evalVisProp('aria.' + key);
1347                     if (el.visPropOld[k] !== v) {
1348                         this.setPropertyPrim(el.rendNode, 'aria-' + key, v);
1349                         el.visPropOld[k] = v;
1350                     }
1351                 }
1352             }
1353         },
1354 
1355         // documented in JXG.AbstractRenderer
1356         setBuffering: function (el, type) {
1357             el.rendNode.setAttribute("buffered-rendering", type);
1358         },
1359 
1360         // documented in JXG.AbstractRenderer
1361         setCssClass(el, cssClass) {
1362 
1363             if (el.visPropOld.cssclass !== cssClass) {
1364                 this.setPropertyPrim(el.rendNode, 'class', cssClass);
1365                 el.visPropOld.cssclass = cssClass;
1366             }
1367         },
1368 
1369         // documented in JXG.AbstractRenderer
1370         setDashStyle: function (el) {
1371             var dashStyle = el.evalVisProp('dash'),
1372                 ds = el.evalVisProp('dashscale'),
1373                 sw = ds ? 0.5 * el.evalVisProp('strokewidth') : 1,
1374                 node = el.rendNode;
1375 
1376             if (dashStyle > 0) {
1377                 node.setAttributeNS(null, "stroke-dasharray",
1378                     // sw could distinguish highlighting or not.
1379                     // But it seems to preferable to ignore this.
1380                     this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',')
1381                 );
1382             } else {
1383                 if (node.hasAttributeNS(null, "stroke-dasharray")) {
1384                     node.removeAttributeNS(null, "stroke-dasharray");
1385                 }
1386             }
1387         },
1388 
1389         // documented in JXG.AbstractRenderer
1390         setGradient: function (el) {
1391             var fillNode = el.rendNode,
1392                 node, node2, node3,
1393                 ev_g = el.evalVisProp('gradient');
1394 
1395             if (ev_g === "linear" || ev_g === "radial") {
1396                 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient");
1397                 node2 = this.createPrim("stop", el.id + "_gradient1");
1398                 node3 = this.createPrim("stop", el.id + "_gradient2");
1399                 node.appendChild(node2);
1400                 node.appendChild(node3);
1401                 this.defs.appendChild(node);
1402                 fillNode.setAttributeNS(
1403                     null,
1404                     'style',
1405                     // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)"
1406                     'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient')
1407                 );
1408                 el.gradNode1 = node2;
1409                 el.gradNode2 = node3;
1410                 el.gradNode = node;
1411             } else {
1412                 fillNode.removeAttributeNS(null, "style");
1413             }
1414         },
1415 
1416         // documented in JXG.AbstractRenderer
1417         setLineCap: function (el) {
1418             var capStyle = el.evalVisProp('linecap');
1419 
1420             if (
1421                 capStyle === undefined ||
1422                 capStyle === "" ||
1423                 el.visPropOld.linecap === capStyle ||
1424                 !Type.exists(el.rendNode)
1425             ) {
1426                 return;
1427             }
1428 
1429             this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle);
1430             el.visPropOld.linecap = capStyle;
1431         },
1432 
1433         // documented in JXG.AbstractRenderer
1434         setObjectFillColor: function (el, color, opacity, rendNode) {
1435             var node, c, rgbo, oo,
1436                 rgba = color,
1437                 o = opacity,
1438                 grad = el.evalVisProp('gradient');
1439 
1440             o = o > 0 ? o : 0;
1441 
1442             // TODO  save gradient and gradientangle
1443             if (
1444                 el.visPropOld.fillcolor === rgba &&
1445                 el.visPropOld.fillopacity === o &&
1446                 grad === null
1447             ) {
1448                 return;
1449             }
1450             if (Type.exists(rgba) && rgba !== false) {
1451                 if (rgba.length !== 9) {
1452                     // RGB, not RGBA
1453                     c = rgba;
1454                     oo = o;
1455                 } else {
1456                     // True RGBA, not RGB
1457                     rgbo = Color.rgba2rgbo(rgba);
1458                     c = rgbo[0];
1459                     oo = o * rgbo[1];
1460                 }
1461 
1462                 if (rendNode === undefined) {
1463                     node = el.rendNode;
1464                 } else {
1465                     node = rendNode;
1466                 }
1467 
1468                 if (c !== "none") {
1469                     this._setAttribute(function () {
1470                         node.setAttributeNS(null, "fill", c);
1471                     }, el.visPropOld.fillcolor);
1472                 }
1473 
1474                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
1475                     this._setAttribute(function () {
1476                         node.setAttributeNS(null, "opacity", oo);
1477                     }, el.visPropOld.fillopacity);
1478                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
1479                 } else {
1480                     if (c === "none") {
1481                         // This is done only for non-images
1482                         // because images have no fill color.
1483                         oo = 0;
1484                         // This is necessary if there is a foreignObject below.
1485                         node.setAttributeNS(null, "pointer-events", "visibleStroke");
1486                     } else {
1487                         // This is the default
1488                         node.setAttributeNS(null, "pointer-events", "visiblePainted");
1489                     }
1490                     this._setAttribute(function () {
1491                         node.setAttributeNS(null, "fill-opacity", oo);
1492                     }, el.visPropOld.fillopacity);
1493                 }
1494 
1495                 if (grad === "linear" || grad === "radial") {
1496                     this.updateGradient(el);
1497                 }
1498             }
1499             el.visPropOld.fillcolor = rgba;
1500             el.visPropOld.fillopacity = o;
1501         },
1502 
1503         // documented in JXG.AbstractRenderer
1504         setObjectStrokeColor: function (el, color, opacity) {
1505             var rgba = color,
1506                 c, rgbo,
1507                 o = opacity,
1508                 oo, node;
1509 
1510             o = o > 0 ? o : 0;
1511 
1512             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
1513                 return;
1514             }
1515 
1516             if (Type.exists(rgba) && rgba !== false) {
1517                 if (rgba.length !== 9) {
1518                     // RGB, not RGBA
1519                     c = rgba;
1520                     oo = o;
1521                 } else {
1522                     // True RGBA, not RGB
1523                     rgbo = Color.rgba2rgbo(rgba);
1524                     c = rgbo[0];
1525                     oo = o * rgbo[1];
1526                 }
1527 
1528                 node = el.rendNode;
1529 
1530                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
1531                     if (el.evalVisProp('display') === "html") {
1532                         this._setAttribute(function () {
1533                             node.style.color = c;
1534                             node.style.opacity = oo;
1535                         }, el.visPropOld.strokecolor);
1536                     } else {
1537                         this._setAttribute(function () {
1538                             node.setAttributeNS(null, "style", "fill:" + c);
1539                             node.setAttributeNS(null, "style", "fill-opacity:" + oo);
1540                         }, el.visPropOld.strokecolor);
1541                     }
1542                 } else {
1543                     this._setAttribute(function () {
1544                         node.setAttributeNS(null, "stroke", c);
1545                         node.setAttributeNS(null, "stroke-opacity", oo);
1546                     }, el.visPropOld.strokecolor);
1547                 }
1548 
1549                 if (
1550                     el.elementClass === Const.OBJECT_CLASS_CURVE ||
1551                     el.elementClass === Const.OBJECT_CLASS_LINE
1552                 ) {
1553                     if (el.evalVisProp('firstarrow')) {
1554                         this._setArrowColor(
1555                             el.rendNodeTriangleStart,
1556                             c, oo, el,
1557                             el.visPropCalc.typeFirst
1558                         );
1559                     }
1560 
1561                     if (el.evalVisProp('lastarrow')) {
1562                         this._setArrowColor(
1563                             el.rendNodeTriangleEnd,
1564                             c, oo, el,
1565                             el.visPropCalc.typeLast
1566                         );
1567                     }
1568                 }
1569             }
1570 
1571             el.visPropOld.strokecolor = rgba;
1572             el.visPropOld.strokeopacity = o;
1573         },
1574 
1575         // documented in JXG.AbstractRenderer
1576         setObjectStrokeWidth: function (el, width) {
1577             var node,
1578                 w = width;
1579 
1580             if (isNaN(w) || el.visPropOld.strokewidth === w) {
1581                 return;
1582             }
1583 
1584             node = el.rendNode;
1585             this.setPropertyPrim(node, "stroked", "true");
1586             if (Type.exists(w)) {
1587                 this.setPropertyPrim(node, "stroke-width", w + "px");
1588 
1589                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
1590                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
1591                 //     if (el.evalVisProp('firstarrow')) {
1592                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
1593                 //     }
1594                 //
1595                 //     if (el.evalVisProp('lastarrow')) {
1596                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
1597                 //     }
1598                 // }
1599             }
1600             el.visPropOld.strokewidth = w;
1601         },
1602 
1603         // documented in JXG.AbstractRenderer
1604         setObjectTransition: function (el, duration) {
1605             var node, props,
1606                 transitionArr = [],
1607                 transitionStr,
1608                 i,
1609                 len = 0,
1610                 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"];
1611 
1612             if (duration === undefined) {
1613                 duration = el.evalVisProp('transitionduration');
1614             }
1615 
1616             props = el.evalVisProp('transitionproperties');
1617             if (duration === el.visPropOld.transitionduration &&
1618                 props === el.visPropOld.transitionproperties) {
1619                 return;
1620             }
1621 
1622             // if (
1623             //     el.elementClass === Const.OBJECT_CLASS_TEXT &&
1624             //     el.evalVisProp('display') === "html"
1625             // ) {
1626             //     // transitionStr = " color " + duration + "ms," +
1627             //     //     " opacity " + duration + "ms";
1628             //     transitionStr = " all " + duration + "ms ease";
1629             // } else {
1630             //     transitionStr =
1631             //         " fill " + duration + "ms," +
1632             //         " fill-opacity " + duration + "ms," +
1633             //         " stroke " + duration + "ms," +
1634             //         " stroke-opacity " + duration + "ms," +
1635             //         " stroke-width " + duration + "ms," +
1636             //         " width " + duration + "ms," +
1637             //         " height " + duration + "ms," +
1638             //         " rx " + duration + "ms," +
1639             //         " ry " + duration + "ms";
1640             // }
1641 
1642             if (Type.exists(props)) {
1643                 len = props.length;
1644             }
1645             for (i = 0; i < len; i++) {
1646                 transitionArr.push(props[i] + ' ' + duration + 'ms');
1647             }
1648             transitionStr = transitionArr.join(', ');
1649 
1650             len = nodes.length;
1651             for (i = 0; i < len; ++i) {
1652                 if (el[nodes[i]]) {
1653                     node = el[nodes[i]];
1654                     node.style.transition = transitionStr;
1655                 }
1656             }
1657 
1658             el.visPropOld.transitionduration = duration;
1659             el.visPropOld.transitionproperties = props;
1660         },
1661 
1662         // documented in JXG.AbstractRenderer
1663         setShadow: function (el) {
1664             var ev_s = el.evalVisProp('shadow'),
1665                 ev_s_json, c, b, bl, o, op, id, node,
1666                 use_board_filter = true,
1667                 show = false;
1668 
1669             ev_s_json = JSON.stringify(ev_s);
1670             if (ev_s_json === el.visPropOld.shadow) {
1671                 return;
1672             }
1673 
1674             if (typeof ev_s === 'boolean') {
1675                 use_board_filter = true;
1676                 show = ev_s;
1677                 c = 'none';
1678                 b = 3;
1679                 bl = 0.1;
1680                 o = [5, 5];
1681                 op = 1;
1682             } else {
1683                 if (el.evalVisProp('shadow.enabled')) {
1684                     use_board_filter = false;
1685                     show = true;
1686                     c = JXG.rgbParser(el.evalVisProp('shadow.color'));
1687                     b = el.evalVisProp('shadow.blur');
1688                     bl = el.evalVisProp('shadow.blend');
1689                     o = el.evalVisProp('shadow.offset');
1690                     op = el.evalVisProp('shadow.opacity');
1691                 } else {
1692                     show = false;
1693                 }
1694             }
1695 
1696             if (Type.exists(el.rendNode)) {
1697                 if (show) {
1698                     if (use_board_filter) {
1699                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1'));
1700                         // 'url(#' + this.container.id + '_' + 'f1)');
1701                     } else {
1702                         node = this.container.ownerDocument.getElementById(id);
1703                         if (node) {
1704                             this.defs.removeChild(node);
1705                         }
1706                         id = el.rendNode.id + '_' + 'f1';
1707                         this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o));
1708                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(id));
1709                         // 'url(#' + id + ')');
1710                     }
1711                 } else {
1712                     el.rendNode.removeAttributeNS(null, 'filter');
1713                 }
1714             }
1715 
1716             el.visPropOld.shadow = ev_s_json;
1717         },
1718 
1719         // documented in JXG.AbstractRenderer
1720         setTabindex: function (el) {
1721             var val;
1722             if (el.board.attr.keyboard.enabled && Type.exists(el.rendNode)) {
1723                 val = el.evalVisProp('tabindex');
1724                 if (!el.visPropCalc.visible /* || el.evalVisProp('fixed') */) {
1725                     val = null;
1726                 }
1727                 if (val !== el.visPropOld.tabindex) {
1728                     el.rendNode.setAttribute("tabindex", val);
1729                     el.visPropOld.tabindex = val;
1730                 }
1731             }
1732         },
1733 
1734         // documented in JXG.AbstractRenderer
1735         setPropertyPrim: function (node, key, val) {
1736             if (key === "stroked") {
1737                 return;
1738             }
1739             node.setAttributeNS(null, key, val);
1740         },
1741 
1742         // documented in JXG.AbstractRenderer
1743         show: function (el) {
1744             JXG.deprecated("Board.renderer.show()", "Board.renderer.display()");
1745             this.display(el, true);
1746             // var node;
1747             //
1748             // if (el && el.rendNode) {
1749             //     node = el.rendNode;
1750             //     node.setAttributeNS(null, 'display', 'inline');
1751             //     node.style.visibility = "inherit";
1752             // }
1753         },
1754 
1755         // documented in JXG.AbstractRenderer
1756         updateGradient: function (el) {
1757             var col,
1758                 op,
1759                 node2 = el.gradNode1,
1760                 node3 = el.gradNode2,
1761                 ev_g = el.evalVisProp('gradient');
1762 
1763             if (!Type.exists(node2) || !Type.exists(node3)) {
1764                 return;
1765             }
1766 
1767             op = el.evalVisProp('fillopacity');
1768             op = op > 0 ? op : 0;
1769             col = el.evalVisProp('fillcolor');
1770 
1771             node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op);
1772             node3.setAttributeNS(
1773                 null,
1774                 "style",
1775                 "stop-color:" +
1776                 el.evalVisProp('gradientsecondcolor') +
1777                 ";stop-opacity:" +
1778                 el.evalVisProp('gradientsecondopacity')
1779             );
1780             node2.setAttributeNS(
1781                 null,
1782                 "offset",
1783                 el.evalVisProp('gradientstartoffset') * 100 + "%"
1784             );
1785             node3.setAttributeNS(
1786                 null,
1787                 "offset",
1788                 el.evalVisProp('gradientendoffset') * 100 + "%"
1789             );
1790             if (ev_g === "linear") {
1791                 this.updateGradientAngle(el.gradNode, el.evalVisProp('gradientangle'));
1792             } else if (ev_g === "radial") {
1793                 this.updateGradientCircle(
1794                     el.gradNode,
1795                     el.evalVisProp('gradientcx'),
1796                     el.evalVisProp('gradientcy'),
1797                     el.evalVisProp('gradientr'),
1798                     el.evalVisProp('gradientfx'),
1799                     el.evalVisProp('gradientfy'),
1800                     el.evalVisProp('gradientfr')
1801                 );
1802             }
1803         },
1804 
1805         /**
1806          * Set the gradient angle for linear color gradients.
1807          *
1808          * @private
1809          * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
1810          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
1811          */
1812         updateGradientAngle: function (node, radians) {
1813             // Angles:
1814             // 0: ->
1815             // 90: down
1816             // 180: <-
1817             // 90: up
1818             var f = 1.0,
1819                 co = Math.cos(radians),
1820                 si = Math.sin(radians);
1821 
1822             if (Math.abs(co) > Math.abs(si)) {
1823                 f /= Math.abs(co);
1824             } else {
1825                 f /= Math.abs(si);
1826             }
1827 
1828             if (co >= 0) {
1829                 node.setAttributeNS(null, "x1", 0);
1830                 node.setAttributeNS(null, "x2", co * f);
1831             } else {
1832                 node.setAttributeNS(null, "x1", -co * f);
1833                 node.setAttributeNS(null, "x2", 0);
1834             }
1835             if (si >= 0) {
1836                 node.setAttributeNS(null, "y1", 0);
1837                 node.setAttributeNS(null, "y2", si * f);
1838             } else {
1839                 node.setAttributeNS(null, "y1", -si * f);
1840                 node.setAttributeNS(null, "y2", 0);
1841             }
1842         },
1843 
1844         /**
1845          * Set circles for radial color gradients.
1846          *
1847          * @private
1848          * @param {SVGnode} node SVG gradient node
1849          * @param {Number} cx SVG value cx (value between 0 and 1)
1850          * @param {Number} cy  SVG value cy (value between 0 and 1)
1851          * @param {Number} r  SVG value r (value between 0 and 1)
1852          * @param {Number} fx  SVG value fx (value between 0 and 1)
1853          * @param {Number} fy  SVG value fy (value between 0 and 1)
1854          * @param {Number} fr  SVG value fr (value between 0 and 1)
1855          */
1856         updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) {
1857             node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color
1858             node.setAttributeNS(null, "cy", cy * 100 + "%");
1859             node.setAttributeNS(null, "r", r * 100 + "%");
1860             node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point
1861             node.setAttributeNS(null, "fy", fy * 100 + "%");
1862             node.setAttributeNS(null, "fr", fr * 100 + "%");
1863         },
1864 
1865         /* ********* Renderer control *********** */
1866 
1867         // documented in JXG.AbstractRenderer
1868         suspendRedraw: function () {
1869             // It seems to be important for the Linux version of firefox
1870             this.suspendHandle = this.svgRoot.suspendRedraw(10000);
1871         },
1872 
1873         // documented in JXG.AbstractRenderer
1874         unsuspendRedraw: function () {
1875             this.svgRoot.unsuspendRedraw(this.suspendHandle);
1876             // this.svgRoot.unsuspendRedrawAll();
1877             //this.svgRoot.forceRedraw();
1878         },
1879 
1880         // documented in AbstractRenderer
1881         resize: function (w, h) {
1882             this.svgRoot.setAttribute("width", parseFloat(w));
1883             this.svgRoot.setAttribute("height", parseFloat(h));
1884         },
1885 
1886         // documented in JXG.AbstractRenderer
1887         createTouchpoints: function (n) {
1888             var i, na1, na2, node;
1889             this.touchpoints = [];
1890             for (i = 0; i < n; i++) {
1891                 na1 = "touchpoint1_" + i;
1892                 node = this.createPrim("path", na1);
1893                 this.appendChildPrim(node, 19);
1894                 node.setAttributeNS(null, "d", "M 0 0");
1895                 this.touchpoints.push(node);
1896 
1897                 this.setPropertyPrim(node, "stroked", "true");
1898                 this.setPropertyPrim(node, "stroke-width", "1px");
1899                 node.setAttributeNS(null, "stroke", "#000000");
1900                 node.setAttributeNS(null, "stroke-opacity", 1.0);
1901                 node.setAttributeNS(null, "display", "none");
1902 
1903                 na2 = "touchpoint2_" + i;
1904                 node = this.createPrim("ellipse", na2);
1905                 this.appendChildPrim(node, 19);
1906                 this.updateEllipsePrim(node, 0, 0, 0, 0);
1907                 this.touchpoints.push(node);
1908 
1909                 this.setPropertyPrim(node, "stroked", "true");
1910                 this.setPropertyPrim(node, "stroke-width", "1px");
1911                 node.setAttributeNS(null, "stroke", "#000000");
1912                 node.setAttributeNS(null, "stroke-opacity", 1.0);
1913                 node.setAttributeNS(null, "fill", "#ffffff");
1914                 node.setAttributeNS(null, "fill-opacity", 0.0);
1915 
1916                 node.setAttributeNS(null, "display", "none");
1917             }
1918         },
1919 
1920         // documented in JXG.AbstractRenderer
1921         showTouchpoint: function (i) {
1922             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1923                 this.touchpoints[2 * i].setAttributeNS(null, "display", "inline");
1924                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "inline");
1925             }
1926         },
1927 
1928         // documented in JXG.AbstractRenderer
1929         hideTouchpoint: function (i) {
1930             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1931                 this.touchpoints[2 * i].setAttributeNS(null, "display", "none");
1932                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "none");
1933             }
1934         },
1935 
1936         // documented in JXG.AbstractRenderer
1937         updateTouchpoint: function (i, pos) {
1938             var x,
1939                 y,
1940                 d = 37;
1941 
1942             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1943                 x = pos[0];
1944                 y = pos[1];
1945 
1946                 this.touchpoints[2 * i].setAttributeNS(
1947                     null,
1948                     "d",
1949                     "M " +
1950                     (x - d) +
1951                     " " +
1952                     y +
1953                     " " +
1954                     "L " +
1955                     (x + d) +
1956                     " " +
1957                     y +
1958                     " " +
1959                     "M " +
1960                     x +
1961                     " " +
1962                     (y - d) +
1963                     " " +
1964                     "L " +
1965                     x +
1966                     " " +
1967                     (y + d)
1968                 );
1969                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
1970             }
1971         },
1972 
1973         /* ********* Dump related stuff *********** */
1974 
1975         /**
1976          * Walk recursively through the DOM subtree of a node and collect all
1977          * value attributes together with the id of that node.
1978          * <b>Attention:</b> Only values of nodes having a valid id are taken.
1979          * @param  {Node} node   root node of DOM subtree that will be searched recursively.
1980          * @return {Array}      Array with entries of the form [id, value]
1981          * @private
1982          */
1983         _getValuesOfDOMElements: function (node) {
1984             var values = [];
1985             if (node.nodeType === 1) {
1986                 node = node.firstChild;
1987                 while (node) {
1988                     if (node.id !== undefined && node.value !== undefined) {
1989                         values.push([node.id, node.value]);
1990                     }
1991                     Type.concat(values, this._getValuesOfDOMElements(node));
1992                     node = node.nextSibling;
1993                 }
1994             }
1995             return values;
1996         },
1997 
1998         // _getDataUri: function (url, callback) {
1999         //     var image = new Image();
2000         //     image.onload = function () {
2001         //         var canvas = document.createElement("canvas");
2002         //         canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
2003         //         canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
2004         //         canvas.getContext("2d").drawImage(this, 0, 0);
2005         //         callback(canvas.toDataURL("image/png"));
2006         //         canvas.remove();
2007         //     };
2008         //     image.src = url;
2009         // },
2010 
2011         _getImgDataURL: function (svgRoot) {
2012             var images, len, canvas, ctx, ur, i;
2013 
2014             images = svgRoot.getElementsByTagName("image");
2015             len = images.length;
2016             if (len > 0) {
2017                 canvas = document.createElement("canvas");
2018                 //img = new Image();
2019                 for (i = 0; i < len; i++) {
2020                     images[i].setAttribute("crossorigin", "anonymous");
2021                     //img.src = images[i].href;
2022                     //img.onload = function() {
2023                     // img.crossOrigin = "anonymous";
2024                     ctx = canvas.getContext("2d");
2025                     canvas.width = images[i].getAttribute("width");
2026                     canvas.height = images[i].getAttribute("height");
2027                     try {
2028                         ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
2029 
2030                         // If the image is not png, the format must be specified here
2031                         ur = canvas.toDataURL();
2032                         images[i].setAttribute("xlink:href", ur);
2033                     } catch (err) {
2034                         console.log("CORS problem! Image can not be used", err);
2035                     }
2036                 }
2037                 //canvas.remove();
2038             }
2039             return true;
2040         },
2041 
2042         /**
2043          * Return a data URI of the SVG code representing the construction.
2044          * The SVG code of the construction is base64 encoded. The return string starts
2045          * with "data:image/svg+xml;base64,...".
2046          *
2047          * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
2048          * This is necessary for older versions of Safari. Default: false
2049          * @returns {String}  data URI string
2050          *
2051          * @example
2052          * var A = board.create('point', [2, 2]);
2053          *
2054          * var txt = board.renderer.dumpToDataURI(false);
2055          * // txt consists of a string of the form
2056          * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2057          * // Behind the comma, there is the base64 encoded SVG code
2058          * // which is decoded with atob().
2059          * // The call of decodeURIComponent(escape(...)) is necessary
2060          * // to handle unicode strings correctly.
2061          * var ar = txt.split(',');
2062          * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1])));
2063          *
2064          * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div>
2065          * <textarea id="output2023" rows="5" cols="50"></textarea>
2066          * <script type="text/javascript">
2067          *     (function() {
2068          *         var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d',
2069          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
2070          *     var A = board.create('point', [2, 2]);
2071          *
2072          *     var txt = board.renderer.dumpToDataURI(false);
2073          *     // txt consists of a string of the form
2074          *     // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2075          *     // Behind the comma, there is the base64 encoded SVG code
2076          *     // which is decoded with atob().
2077          *     // The call of decodeURIComponent(escape(...)) is necessary
2078          *     // to handle unicode strings correctly.
2079          *     var ar = txt.split(',');
2080          *     document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1])));
2081          *
2082          *     })();
2083          *
2084          * </script><pre>
2085          *
2086          */
2087         dumpToDataURI: function (ignoreTexts) {
2088             var svgRoot = this.svgRoot,
2089                 btoa = window.btoa || Base64.encode,
2090                 svg, i, len,
2091                 values = [];
2092 
2093             // Move all HTML tags (beside the SVG root) of the container
2094             // to the foreignObject element inside of the svgRoot node
2095             // Problem:
2096             // input values are not copied. This can be verified by looking at an innerHTML output
2097             // of an input element. Therefore, we do it "by hand".
2098             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
2099                 if (!ignoreTexts) {
2100                     this.foreignObjLayer.setAttribute("display", "inline");
2101                 }
2102                 while (svgRoot.nextSibling) {
2103                     // Copy all value attributes
2104                     Type.concat(values, this._getValuesOfDOMElements(svgRoot.nextSibling));
2105                     this.foreignObjLayer.appendChild(svgRoot.nextSibling);
2106                 }
2107             }
2108 
2109             this._getImgDataURL(svgRoot);
2110 
2111             // Convert the SVG graphic into a string containing SVG code
2112             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2113             svg = new XMLSerializer().serializeToString(svgRoot);
2114 
2115             if (ignoreTexts !== true) {
2116                 // Handle SVG texts
2117                 // Insert all value attributes back into the svg string
2118                 len = values.length;
2119                 for (i = 0; i < len; i++) {
2120                     svg = svg.replace(
2121                         'id="' + values[i][0] + '"',
2122                         'id="' + values[i][0] + '" value="' + values[i][1] + '"'
2123                     );
2124                 }
2125             }
2126 
2127             // if (false) {
2128             //     // Debug: use example svg image
2129             //     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>';
2130             // }
2131 
2132             // In IE we have to remove the namespace again.
2133             // Since 2024 we have to check if the namespace attribute appears twice in one tag, because
2134             // there might by a svg inside of the svg, e.g. the screenshot icon.
2135             if (this.isIE &&
2136                 (svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1
2137             ) {
2138                 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g, "");
2139             }
2140 
2141             // Safari fails if the svg string contains a " "
2142             // Obsolete with Safari 12+
2143             svg = svg.replace(/ /g, " ");
2144             // Replacing "s might be necessary for older Safari versions
2145             // svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); // Bug: does not replace matching "s
2146             // svg = svg.replace(/"/g, "");
2147 
2148             // Move all HTML tags back from
2149             // the foreignObject element to the container
2150             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
2151                 // Restore all HTML elements
2152                 while (this.foreignObjLayer.firstChild) {
2153                     this.container.appendChild(this.foreignObjLayer.firstChild);
2154                 }
2155                 this.foreignObjLayer.setAttribute("display", "none");
2156             }
2157 
2158             return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg)));
2159         },
2160 
2161         /**
2162          * Convert the SVG construction into an HTML canvas image.
2163          * This works for all SVG supporting browsers. Implemented as Promise.
2164          * <p>
2165          * Might fail if any text element or foreign object element contains SVG. This
2166          * is the case e.g. for the default fullscreen symbol.
2167          * <p>
2168          * For IE, it is realized as function.
2169          * It works from version 9, with the exception that HTML texts
2170          * are ignored on IE. The drawing is done with a delay of
2171          * 200 ms. Otherwise there would be problems with IE.
2172          *
2173          * @param {String} canvasId Id of an HTML canvas element
2174          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
2175          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
2176          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
2177          * This is necessary for older versions of Safari. Default: false
2178          * @returns {Promise}  Promise object
2179          *
2180          * @example
2181          * 	board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
2182          *
2183          * @example
2184          *  // IE 11 example:
2185          * 	board.renderer.dumpToCanvas('canvas');
2186          * 	setTimeout(function() { console.log('done'); }, 400);
2187          */
2188         dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
2189             var svg, tmpImg,
2190                 cv, ctx,
2191                 doc = this.container.ownerDocument;
2192 
2193             // Prepare the canvas element
2194             cv = doc.getElementById(canvasId);
2195 
2196             // Clear the canvas
2197             /* eslint-disable no-self-assign */
2198             cv.width = cv.width;
2199             /* eslint-enable no-self-assign */
2200 
2201             ctx = cv.getContext("2d");
2202             if (w !== undefined && h !== undefined) {
2203                 cv.style.width = parseFloat(w) + "px";
2204                 cv.style.height = parseFloat(h) + "px";
2205                 // Scale twice the CSS size to make the image crisp
2206                 // cv.setAttribute('width', 2 * parseFloat(wOrg));
2207                 // cv.setAttribute('height', 2 * parseFloat(hOrg));
2208                 // ctx.scale(2 * wOrg / w, 2 * hOrg / h);
2209                 cv.setAttribute("width", parseFloat(w));
2210                 cv.setAttribute("height", parseFloat(h));
2211             }
2212 
2213             // Display the SVG string as data-uri in an HTML img.
2214             /**
2215              * @type {Image}
2216              * @ignore
2217              * {ignore}
2218              */
2219             tmpImg = new Image();
2220             svg = this.dumpToDataURI(ignoreTexts);
2221             tmpImg.src = svg;
2222 
2223             // Finally, draw the HTML img in the canvas.
2224             if (!("Promise" in window)) {
2225                 /**
2226                  * @function
2227                  * @ignore
2228                  */
2229                 tmpImg.onload = function () {
2230                     // IE needs a pause...
2231                     // Seems to be broken
2232                     window.setTimeout(function () {
2233                         try {
2234                             ctx.drawImage(tmpImg, 0, 0, w, h);
2235                         } catch (err) {
2236                             console.log("screenshots not longer supported on IE");
2237                         }
2238                     }, 200);
2239                 };
2240                 return this;
2241             }
2242 
2243             return new Promise(function (resolve, reject) {
2244                 try {
2245                     tmpImg.onload = function () {
2246                         ctx.drawImage(tmpImg, 0, 0, w, h);
2247                         resolve();
2248                     };
2249                 } catch (e) {
2250                     reject(e);
2251                 }
2252             });
2253         },
2254 
2255         /**
2256          * Display SVG image in html img-tag which enables
2257          * easy download for the user.
2258          *
2259          * Support:
2260          * <ul>
2261          * <li> IE: No
2262          * <li> Edge: full
2263          * <li> Firefox: full
2264          * <li> Chrome: full
2265          * <li> Safari: full (No text support in versions prior to 12).
2266          * </ul>
2267          *
2268          * @param {JXG.Board} board Link to the board.
2269          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
2270          * the screenshot is copied to this img object. The width and height will be set to the values of the
2271          * JSXGraph container.
2272          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
2273          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
2274          * @return {Object}       the svg renderer object
2275          */
2276         screenshot: function (board, imgId, ignoreTexts) {
2277             var node,
2278                 doc = this.container.ownerDocument,
2279                 parent = this.container.parentNode,
2280                 // cPos,
2281                 // cssTxt,
2282                 canvas, id, img,
2283                 button, buttonText,
2284                 w, h,
2285                 bas = board.attr.screenshot,
2286                 navbar, navbarDisplay, insert,
2287                 newImg = false,
2288                 _copyCanvasToImg,
2289                 isDebug = false;
2290 
2291             if (this.type === "no") {
2292                 return this;
2293             }
2294 
2295             w = bas.scale * this.container.getBoundingClientRect().width;
2296             h = bas.scale * this.container.getBoundingClientRect().height;
2297 
2298             if (imgId === undefined || imgId === "") {
2299                 newImg = true;
2300                 img = new Image(); //doc.createElement('img');
2301                 img.style.width = w + "px";
2302                 img.style.height = h + "px";
2303             } else {
2304                 newImg = false;
2305                 img = doc.getElementById(imgId);
2306             }
2307             // img.crossOrigin = 'anonymous';
2308 
2309             // Create div which contains canvas element and close button
2310             if (newImg) {
2311                 node = doc.createElement("div");
2312                 node.style.cssText = bas.css;
2313                 node.style.width = w + "px";
2314                 node.style.height = h + "px";
2315                 node.style.zIndex = this.container.style.zIndex + 120;
2316 
2317                 // Try to position the div exactly over the JSXGraph board
2318                 node.style.position = "absolute";
2319                 node.style.top = this.container.offsetTop + "px";
2320                 node.style.left = this.container.offsetLeft + "px";
2321             }
2322 
2323             if (!isDebug) {
2324                 // Create canvas element and add it to the DOM
2325                 // It will be removed after the image has been stored.
2326                 canvas = doc.createElement("canvas");
2327                 id = Math.random().toString(36).slice(2, 7);
2328                 canvas.setAttribute("id", id);
2329                 canvas.setAttribute("width", w);
2330                 canvas.setAttribute("height", h);
2331                 canvas.style.width = w + "px";
2332                 canvas.style.height = w + "px";
2333                 canvas.style.display = "none";
2334                 parent.appendChild(canvas);
2335             } else {
2336                 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
2337                 id = "jxgbox_canvas";
2338                 canvas = doc.getElementById(id);
2339             }
2340 
2341             if (newImg) {
2342                 // Create close button
2343                 button = doc.createElement("span");
2344                 buttonText = doc.createTextNode("\u2716");
2345                 button.style.cssText = bas.cssButton;
2346                 button.appendChild(buttonText);
2347                 button.onclick = function () {
2348                     node.parentNode.removeChild(node);
2349                 };
2350 
2351                 // Add all nodes
2352                 node.appendChild(img);
2353                 node.appendChild(button);
2354                 parent.insertBefore(node, this.container.nextSibling);
2355             }
2356 
2357             // Hide navigation bar in board
2358             navbar = doc.getElementById(this.uniqName('navigationbar'));
2359             if (Type.exists(navbar)) {
2360                 navbarDisplay = navbar.style.display;
2361                 navbar.style.display = "none";
2362                 insert = this.removeToInsertLater(navbar);
2363             }
2364 
2365             _copyCanvasToImg = function () {
2366                 // Show image in img tag
2367                 img.src = canvas.toDataURL("image/png");
2368 
2369                 // Remove canvas node
2370                 if (!isDebug) {
2371                     parent.removeChild(canvas);
2372                 }
2373             };
2374 
2375             // Create screenshot in image element
2376             if ("Promise" in window) {
2377                 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
2378             } else {
2379                 // IE
2380                 this.dumpToCanvas(id, w, h, ignoreTexts);
2381                 window.setTimeout(_copyCanvasToImg, 200);
2382             }
2383 
2384             // Reinsert navigation bar in board
2385             if (Type.exists(navbar)) {
2386                 navbar.style.display = navbarDisplay;
2387                 insert();
2388             }
2389 
2390             return this;
2391         }
2392     }
2393 );
2394 
2395 export default JXG.SVGRenderer;
2396