1 /*
  2     Copyright 2008-2025
  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             if (el.evalVisProp('evaluateOnlyOnce') !== true || !el.renderedOnce) {
809                 el.rendNode.innerHTML = el.content;
810                 el.renderedOnce = true;
811             }
812             this._updateVisual(el, { stroke: true, dash: true }, true);
813         },
814 
815         /* ********* Render primitive objects *********** */
816 
817         // Already documented in JXG.AbstractRenderer
818         appendChildPrim: function (node, level) {
819             if (!Type.exists(level)) {
820                 // trace nodes have level not set
821                 level = 0;
822             } else if (level >= Options.layer.numlayers) {
823                 level = Options.layer.numlayers - 1;
824             }
825             this.layer[level].appendChild(node);
826 
827             return node;
828         },
829 
830         // Already documented in JXG.AbstractRenderer
831         createPrim: function (type, id) {
832             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
833             node.setAttributeNS(null, "id", this.uniqName(id));
834             node.style.position = "absolute";
835             if (type === "path") {
836                 node.setAttributeNS(null, "stroke-linecap", "round");
837                 node.setAttributeNS(null, "stroke-linejoin", "round");
838                 node.setAttributeNS(null, "fill-rule", "evenodd");
839             }
840 
841             return node;
842         },
843 
844         // Already documented in JXG.AbstractRenderer
845         remove: function (shape) {
846             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
847                 shape.parentNode.removeChild(shape);
848             }
849         },
850 
851         // Already documented in JXG.AbstractRenderer
852         setLayer: function (el, level) {
853             if (!Type.exists(level)) {
854                 level = 0;
855             } else if (level >= Options.layer.numlayers) {
856                 level = Options.layer.numlayers - 1;
857             }
858 
859             this.layer[level].appendChild(el.rendNode);
860         },
861 
862         // Already documented in JXG.AbstractRenderer
863         makeArrows: function (el, a) {
864             var node2, str,
865                 ev_fa = a.evFirst,
866                 ev_la = a.evLast;
867 
868             if (this.isIE && el.visPropCalc.visible && (ev_fa || ev_la)) {
869                 // Necessary, since Safari is the new IE (11.2024)
870                 el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
871                 return;
872             }
873 
874             // We can not compare against visPropOld if there is need for a new arrow head,
875             // since here visPropOld and ev_fa / ev_la already have the same value.
876             // This has been set in _updateVisual.
877             //
878             node2 = el.rendNodeTriangleStart;
879             if (ev_fa) {
880                 str = this.toStr(this.container.id, '_', el.id, 'TriangleStart', a.typeFirst);
881 
882                 // If we try to set the same arrow head as is already set, we can bail out now
883                 if (!Type.exists(node2) || node2.id !== str) {
884                     node2 = this.container.ownerDocument.getElementById(str);
885                     // Check if the marker already exists.
886                     // If not, create a new marker
887                     if (node2 === null) {
888                         node2 = this._createArrowHead(el, "Start", a.typeFirst);
889                         this.defs.appendChild(node2);
890                     }
891                     el.rendNodeTriangleStart = node2;
892                     el.rendNode.setAttributeNS(null, "marker-start", this.toURL(str));
893                 }
894             } else {
895                 if (Type.exists(node2)) {
896                     this.remove(node2);
897                     el.rendNodeTriangleStart = null;
898                 }
899                 el.rendNode.setAttributeNS(null, "marker-start", null);
900             }
901 
902             node2 = el.rendNodeTriangleEnd;
903             if (ev_la) {
904                 str = this.toStr(this.container.id, '_', el.id, 'TriangleEnd', a.typeLast);
905 
906                 // If we try to set the same arrow head as is already set, we can bail out now
907                 if (!Type.exists(node2) || node2.id !== str) {
908                     node2 = this.container.ownerDocument.getElementById(str);
909                     // Check if the marker already exists.
910                     // If not, create a new marker
911                     if (node2 === null) {
912                         node2 = this._createArrowHead(el, "End", a.typeLast);
913                         this.defs.appendChild(node2);
914                     }
915                     el.rendNodeTriangleEnd = node2;
916                     el.rendNode.setAttributeNS(null, "marker-end", this.toURL(str));
917                 }
918             } else {
919                 if (Type.exists(node2)) {
920                     this.remove(node2);
921                     el.rendNodeTriangleEnd = null;
922                 }
923                 el.rendNode.setAttributeNS(null, "marker-end", null);
924             }
925         },
926 
927         // Already documented in JXG.AbstractRenderer
928         updateEllipsePrim: function (node, x, y, rx, ry) {
929             var huge = 1000000;
930 
931             huge = 200000; // IE
932             // webkit does not like huge values if the object is dashed
933             // iE doesn't like huge values above 216000
934             x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x);
935             y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y);
936             rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx);
937             ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry);
938 
939             node.setAttributeNS(null, "cx", x);
940             node.setAttributeNS(null, "cy", y);
941             node.setAttributeNS(null, "rx", Math.abs(rx));
942             node.setAttributeNS(null, "ry", Math.abs(ry));
943         },
944 
945         // Already documented in JXG.AbstractRenderer
946         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
947             var huge = 1000000;
948 
949             huge = 200000; //IE
950             if (!isNaN(p1x + p1y + p2x + p2y)) {
951                 // webkit does not like huge values if the object is dashed
952                 // IE doesn't like huge values above 216000
953                 p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x);
954                 p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y);
955                 p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x);
956                 p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y);
957 
958                 node.setAttributeNS(null, "x1", p1x);
959                 node.setAttributeNS(null, "y1", p1y);
960                 node.setAttributeNS(null, "x2", p2x);
961                 node.setAttributeNS(null, "y2", p2y);
962             }
963         },
964 
965         // Already documented in JXG.AbstractRenderer
966         updatePathPrim: function (node, pointString) {
967             if (pointString === "") {
968                 pointString = "M 0 0";
969             }
970             node.setAttributeNS(null, "d", pointString);
971         },
972 
973         // Already documented in JXG.AbstractRenderer
974         updatePathStringPoint: function (el, size, type) {
975             var s = "",
976                 scr = el.coords.scrCoords,
977                 sqrt32 = size * Math.sqrt(3) * 0.5,
978                 s05 = size * 0.5;
979 
980             if (type === "x") {
981                 s =
982                     " M " +
983                     (scr[1] - size) +
984                     " " +
985                     (scr[2] - size) +
986                     " L " +
987                     (scr[1] + size) +
988                     " " +
989                     (scr[2] + size) +
990                     " M " +
991                     (scr[1] + size) +
992                     " " +
993                     (scr[2] - size) +
994                     " L " +
995                     (scr[1] - size) +
996                     " " +
997                     (scr[2] + size);
998             } else if (type === "+") {
999                 s =
1000                     " M " +
1001                     (scr[1] - size) +
1002                     " " +
1003                     scr[2] +
1004                     " L " +
1005                     (scr[1] + size) +
1006                     " " +
1007                     scr[2] +
1008                     " M " +
1009                     scr[1] +
1010                     " " +
1011                     (scr[2] - size) +
1012                     " L " +
1013                     scr[1] +
1014                     " " +
1015                     (scr[2] + size);
1016             } else if (type === "|") {
1017                 s =
1018                     " M " +
1019                     scr[1] +
1020                     " " +
1021                     (scr[2] - size) +
1022                     " L " +
1023                     scr[1] +
1024                     " " +
1025                     (scr[2] + size);
1026             } else if (type === "-") {
1027                 s =
1028                     " M " +
1029                     (scr[1] - size) +
1030                     " " +
1031                     scr[2] +
1032                     " L " +
1033                     (scr[1] + size) +
1034                     " " +
1035                     scr[2];
1036             } else if (type === "<>" || type === "<<>>") {
1037                 if (type === "<<>>") {
1038                     size *= 1.41;
1039                 }
1040                 s =
1041                     " M " +
1042                     (scr[1] - size) +
1043                     " " +
1044                     scr[2] +
1045                     " L " +
1046                     scr[1] +
1047                     " " +
1048                     (scr[2] + size) +
1049                     " L " +
1050                     (scr[1] + size) +
1051                     " " +
1052                     scr[2] +
1053                     " L " +
1054                     scr[1] +
1055                     " " +
1056                     (scr[2] - size) +
1057                     " Z ";
1058                 } else if (type === "^") {
1059                     s =
1060                     " M " +
1061                     scr[1] +
1062                     " " +
1063                     (scr[2] - size) +
1064                     " L " +
1065                     (scr[1] - sqrt32) +
1066                     " " +
1067                     (scr[2] + s05) +
1068                     " L " +
1069                     (scr[1] + sqrt32) +
1070                     " " +
1071                     (scr[2] + s05) +
1072                     " Z "; // close path
1073             } else if (type === "v") {
1074                 s =
1075                     " M " +
1076                     scr[1] +
1077                     " " +
1078                     (scr[2] + size) +
1079                     " L " +
1080                     (scr[1] - sqrt32) +
1081                     " " +
1082                     (scr[2] - s05) +
1083                     " L " +
1084                     (scr[1] + sqrt32) +
1085                     " " +
1086                     (scr[2] - s05) +
1087                     " Z ";
1088             } else if (type === ">") {
1089                 s =
1090                     " M " +
1091                     (scr[1] + size) +
1092                     " " +
1093                     scr[2] +
1094                     " L " +
1095                     (scr[1] - s05) +
1096                     " " +
1097                     (scr[2] - sqrt32) +
1098                     " L " +
1099                     (scr[1] - s05) +
1100                     " " +
1101                     (scr[2] + sqrt32) +
1102                     " Z ";
1103             } else if (type === "<") {
1104                 s =
1105                     " M " +
1106                     (scr[1] - size) +
1107                     " " +
1108                     scr[2] +
1109                     " L " +
1110                     (scr[1] + s05) +
1111                     " " +
1112                     (scr[2] - sqrt32) +
1113                     " L " +
1114                     (scr[1] + s05) +
1115                     " " +
1116                     (scr[2] + sqrt32) +
1117                     " Z ";
1118             }
1119             return s;
1120         },
1121 
1122         // Already documented in JXG.AbstractRenderer
1123         updatePathStringPrim: function (el) {
1124             var i,
1125                 scr,
1126                 len,
1127                 symbm = " M ",
1128                 symbl = " L ",
1129                 symbc = " C ",
1130                 nextSymb = symbm,
1131                 maxSize = 5000.0,
1132                 pStr = "";
1133 
1134             if (el.numberPoints <= 0) {
1135                 return "";
1136             }
1137 
1138             len = Math.min(el.points.length, el.numberPoints);
1139 
1140             if (el.bezierDegree === 1) {
1141                 for (i = 0; i < len; i++) {
1142                     scr = el.points[i].scrCoords;
1143                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1144                         // PenUp
1145                         nextSymb = symbm;
1146                     } else {
1147                         // Chrome has problems with values being too far away.
1148                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
1149                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
1150 
1151                         // Attention: first coordinate may be inaccurate if far way
1152                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1153                         pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox)
1154                         nextSymb = symbl;
1155                     }
1156                 }
1157             } else if (el.bezierDegree === 3) {
1158                 i = 0;
1159                 while (i < len) {
1160                     scr = el.points[i].scrCoords;
1161                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1162                         // PenUp
1163                         nextSymb = symbm;
1164                     } else {
1165                         pStr += nextSymb + scr[1] + " " + scr[2];
1166                         if (nextSymb === symbc) {
1167                             i += 1;
1168                             scr = el.points[i].scrCoords;
1169                             pStr += " " + scr[1] + " " + scr[2];
1170                             i += 1;
1171                             scr = el.points[i].scrCoords;
1172                             pStr += " " + scr[1] + " " + scr[2];
1173                         }
1174                         nextSymb = symbc;
1175                     }
1176                     i += 1;
1177                 }
1178             }
1179             return pStr;
1180         },
1181 
1182         // Already documented in JXG.AbstractRenderer
1183         updatePathStringBezierPrim: function (el) {
1184             var i, j, k,
1185                 scr,
1186                 lx, ly,
1187                 len,
1188                 symbm = " M ",
1189                 symbl = " C ",
1190                 nextSymb = symbm,
1191                 maxSize = 5000.0,
1192                 pStr = "",
1193                 f = el.evalVisProp('strokewidth'),
1194                 isNoPlot = el.evalVisProp('curvetype') !== "plot";
1195 
1196             if (el.numberPoints <= 0) {
1197                 return "";
1198             }
1199 
1200             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
1201                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
1202             }
1203 
1204             len = Math.min(el.points.length, el.numberPoints);
1205             for (j = 1; j < 3; j++) {
1206                 nextSymb = symbm;
1207                 for (i = 0; i < len; i++) {
1208                     scr = el.points[i].scrCoords;
1209 
1210                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1211                         // PenUp
1212                         nextSymb = symbm;
1213                     } else {
1214                         // Chrome has problems with values being too far away.
1215                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
1216                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
1217 
1218                         // Attention: first coordinate may be inaccurate if far way
1219                         if (nextSymb === symbm) {
1220                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1221                             pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox)
1222                         } else {
1223                             k = 2 * j;
1224                             pStr += [
1225                                 nextSymb,
1226                                 lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j),
1227                                 " ",
1228                                 ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j),
1229                                 " ",
1230                                 lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j),
1231                                 " ",
1232                                 ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j),
1233                                 " ",
1234                                 scr[1],
1235                                 " ",
1236                                 scr[2]
1237                             ].join("");
1238                         }
1239 
1240                         nextSymb = symbl;
1241                         lx = scr[1];
1242                         ly = scr[2];
1243                     }
1244                 }
1245             }
1246             return pStr;
1247         },
1248 
1249         // Already documented in JXG.AbstractRenderer
1250         updatePolygonPrim: function (node, el) {
1251             var i,
1252                 pStr = "",
1253                 scrCoords,
1254                 len = el.vertices.length;
1255 
1256             node.setAttributeNS(null, "stroke", "none");
1257             node.setAttributeNS(null, "fill-rule", "evenodd");
1258             if (el.elType === "polygonalchain") {
1259                 len++;
1260             }
1261 
1262             for (i = 0; i < len - 1; i++) {
1263                 if (el.vertices[i].isReal) {
1264                     scrCoords = el.vertices[i].coords.scrCoords;
1265                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
1266                 } else {
1267                     node.setAttributeNS(null, "points", "");
1268                     return;
1269                 }
1270 
1271                 if (i < len - 2) {
1272                     pStr += " ";
1273                 }
1274             }
1275             if (pStr.indexOf("NaN") === -1) {
1276                 node.setAttributeNS(null, "points", pStr);
1277             }
1278         },
1279 
1280         // Already documented in JXG.AbstractRenderer
1281         updateRectPrim: function (node, x, y, w, h) {
1282             node.setAttributeNS(null, "x", x);
1283             node.setAttributeNS(null, "y", y);
1284             node.setAttributeNS(null, "width", w);
1285             node.setAttributeNS(null, "height", h);
1286         },
1287 
1288         /* ********* Set attributes *********** */
1289 
1290         /**
1291          * Call user-defined function to set visual attributes.
1292          * If "testAttribute" is the empty string, the function
1293          * is called immediately, otherwise it is called in a timeOut.
1294          *
1295          * This is necessary to realize smooth transitions but avoid transitions
1296          * when first creating the objects.
1297          *
1298          * Usually, the string in testAttribute is the visPropOld attribute
1299          * of the values which are set.
1300          *
1301          * @param {Function} setFunc       Some function which usually sets some attributes
1302          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
1303          *                               otherwise it is called in a setImeout.
1304          * @see JXG.SVGRenderer#setObjectFillColor
1305          * @see JXG.SVGRenderer#setObjectStrokeColor
1306          * @see JXG.SVGRenderer#_setArrowColor
1307          * @private
1308          */
1309         _setAttribute: function (setFunc, testAttribute) {
1310             if (testAttribute === "") {
1311                 setFunc();
1312             } else {
1313                 window.setTimeout(setFunc, 1);
1314             }
1315         },
1316 
1317         display: function (el, val) {
1318             var node;
1319 
1320             if (el && el.rendNode) {
1321                 el.visPropOld.visible = val;
1322                 node = el.rendNode;
1323                 if (val) {
1324                     node.setAttributeNS(null, "display", "inline");
1325                     node.style.visibility = "inherit";
1326                 } else {
1327                     node.setAttributeNS(null, "display", "none");
1328                     node.style.visibility = "hidden";
1329                 }
1330             }
1331         },
1332 
1333         // documented in JXG.AbstractRenderer
1334         hide: function (el) {
1335             JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()");
1336             this.display(el, false);
1337         },
1338 
1339         // documented in JXG.AbstractRenderer
1340         setARIA: function(el) {
1341             // This method is only called in abstractRenderer._updateVisual() if aria.enabled == true.
1342             var key, k, v;
1343 
1344             // this.setPropertyPrim(el.rendNode, 'aria-label', el.evalVisProp('aria.label'));
1345             // this.setPropertyPrim(el.rendNode, 'aria-live', el.evalVisProp('aria.live'));
1346             for (key in el.visProp.aria) {
1347                 if (el.visProp.aria.hasOwnProperty(key) && key !== 'enabled') {
1348                     k = 'aria.' + key;
1349                     v = el.evalVisProp('aria.' + key);
1350                     if (el.visPropOld[k] !== v) {
1351                         this.setPropertyPrim(el.rendNode, 'aria-' + key, v);
1352                         el.visPropOld[k] = v;
1353                     }
1354                 }
1355             }
1356         },
1357 
1358         // documented in JXG.AbstractRenderer
1359         setBuffering: function (el, type) {
1360             el.rendNode.setAttribute("buffered-rendering", type);
1361         },
1362 
1363         // documented in JXG.AbstractRenderer
1364         setCssClass(el, cssClass) {
1365 
1366             if (el.visPropOld.cssclass !== cssClass) {
1367                 this.setPropertyPrim(el.rendNode, 'class', cssClass);
1368                 el.visPropOld.cssclass = cssClass;
1369             }
1370         },
1371 
1372         // documented in JXG.AbstractRenderer
1373         setDashStyle: function (el) {
1374             var dashStyle = el.evalVisProp('dash'),
1375                 ds = el.evalVisProp('dashscale'),
1376                 sw = ds ? 0.5 * el.evalVisProp('strokewidth') : 1,
1377                 node = el.rendNode;
1378 
1379             if (dashStyle > 0) {
1380                 node.setAttributeNS(null, "stroke-dasharray",
1381                     // sw could distinguish highlighting or not.
1382                     // But it seems to preferable to ignore this.
1383                     this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',')
1384                 );
1385             } else {
1386                 if (node.hasAttributeNS(null, "stroke-dasharray")) {
1387                     node.removeAttributeNS(null, "stroke-dasharray");
1388                 }
1389             }
1390         },
1391 
1392         // documented in JXG.AbstractRenderer
1393         setGradient: function (el) {
1394             var fillNode = el.rendNode,
1395                 node, node2, node3,
1396                 ev_g = el.evalVisProp('gradient');
1397 
1398             if (ev_g === "linear" || ev_g === "radial") {
1399                 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient");
1400                 node2 = this.createPrim("stop", el.id + "_gradient1");
1401                 node3 = this.createPrim("stop", el.id + "_gradient2");
1402                 node.appendChild(node2);
1403                 node.appendChild(node3);
1404                 this.defs.appendChild(node);
1405                 fillNode.setAttributeNS(
1406                     null,
1407                     'style',
1408                     // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)"
1409                     'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient')
1410                 );
1411                 el.gradNode1 = node2;
1412                 el.gradNode2 = node3;
1413                 el.gradNode = node;
1414             } else {
1415                 fillNode.removeAttributeNS(null, "style");
1416             }
1417         },
1418 
1419         // documented in JXG.AbstractRenderer
1420         setLineCap: function (el) {
1421             var capStyle = el.evalVisProp('linecap');
1422 
1423             if (
1424                 capStyle === undefined ||
1425                 capStyle === "" ||
1426                 el.visPropOld.linecap === capStyle ||
1427                 !Type.exists(el.rendNode)
1428             ) {
1429                 return;
1430             }
1431 
1432             this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle);
1433             el.visPropOld.linecap = capStyle;
1434         },
1435 
1436         // documented in JXG.AbstractRenderer
1437         setObjectFillColor: function (el, color, opacity, rendNode) {
1438             var node, c, rgbo, oo,
1439                 rgba = color,
1440                 o = opacity,
1441                 grad = el.evalVisProp('gradient');
1442 
1443             o = o > 0 ? o : 0;
1444 
1445             // TODO  save gradient and gradientangle
1446             if (
1447                 el.visPropOld.fillcolor === rgba &&
1448                 el.visPropOld.fillopacity === o &&
1449                 grad === null
1450             ) {
1451                 return;
1452             }
1453             if (Type.exists(rgba) && rgba !== false) {
1454                 if (rgba.length !== 9) {
1455                     // RGB, not RGBA
1456                     c = rgba;
1457                     oo = o;
1458                 } else {
1459                     // True RGBA, not RGB
1460                     rgbo = Color.rgba2rgbo(rgba);
1461                     c = rgbo[0];
1462                     oo = o * rgbo[1];
1463                 }
1464 
1465                 if (rendNode === undefined) {
1466                     node = el.rendNode;
1467                 } else {
1468                     node = rendNode;
1469                 }
1470 
1471                 if (c !== "none") {
1472                     this._setAttribute(function () {
1473                         node.setAttributeNS(null, "fill", c);
1474                     }, el.visPropOld.fillcolor);
1475                 }
1476 
1477                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
1478                     this._setAttribute(function () {
1479                         node.setAttributeNS(null, "opacity", oo);
1480                     }, el.visPropOld.fillopacity);
1481                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
1482                 } else {
1483                     if (c === "none") {
1484                         // This is done only for non-images
1485                         // because images have no fill color.
1486                         oo = 0;
1487                         // This is necessary if there is a foreignObject below.
1488                         node.setAttributeNS(null, "pointer-events", "visibleStroke");
1489                     } else {
1490                         // This is the default
1491                         node.setAttributeNS(null, "pointer-events", "visiblePainted");
1492                     }
1493                     this._setAttribute(function () {
1494                         node.setAttributeNS(null, "fill-opacity", oo);
1495                     }, el.visPropOld.fillopacity);
1496                 }
1497 
1498                 if (grad === "linear" || grad === "radial") {
1499                     this.updateGradient(el);
1500                 }
1501             }
1502             el.visPropOld.fillcolor = rgba;
1503             el.visPropOld.fillopacity = o;
1504         },
1505 
1506         // documented in JXG.AbstractRenderer
1507         setObjectStrokeColor: function (el, color, opacity) {
1508             var rgba = color,
1509                 c, rgbo,
1510                 o = opacity,
1511                 oo, node;
1512 
1513             o = o > 0 ? o : 0;
1514 
1515             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
1516                 return;
1517             }
1518 
1519             if (Type.exists(rgba) && rgba !== false) {
1520                 if (rgba.length !== 9) {
1521                     // RGB, not RGBA
1522                     c = rgba;
1523                     oo = o;
1524                 } else {
1525                     // True RGBA, not RGB
1526                     rgbo = Color.rgba2rgbo(rgba);
1527                     c = rgbo[0];
1528                     oo = o * rgbo[1];
1529                 }
1530 
1531                 node = el.rendNode;
1532 
1533                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
1534                     if (el.evalVisProp('display') === "html") {
1535                         this._setAttribute(function () {
1536                             node.style.color = c;
1537                             node.style.opacity = oo;
1538                         }, el.visPropOld.strokecolor);
1539                     } else {
1540                         this._setAttribute(function () {
1541                             node.setAttributeNS(null, "style", "fill:" + c);
1542                             node.setAttributeNS(null, "style", "fill-opacity:" + oo);
1543                         }, el.visPropOld.strokecolor);
1544                     }
1545                 } else {
1546                     this._setAttribute(function () {
1547                         node.setAttributeNS(null, "stroke", c);
1548                         node.setAttributeNS(null, "stroke-opacity", oo);
1549                     }, el.visPropOld.strokecolor);
1550                 }
1551 
1552                 if (
1553                     el.elementClass === Const.OBJECT_CLASS_CURVE ||
1554                     el.elementClass === Const.OBJECT_CLASS_LINE
1555                 ) {
1556                     if (el.evalVisProp('firstarrow')) {
1557                         this._setArrowColor(
1558                             el.rendNodeTriangleStart,
1559                             c, oo, el,
1560                             el.visPropCalc.typeFirst
1561                         );
1562                     }
1563 
1564                     if (el.evalVisProp('lastarrow')) {
1565                         this._setArrowColor(
1566                             el.rendNodeTriangleEnd,
1567                             c, oo, el,
1568                             el.visPropCalc.typeLast
1569                         );
1570                     }
1571                 }
1572             }
1573 
1574             el.visPropOld.strokecolor = rgba;
1575             el.visPropOld.strokeopacity = o;
1576         },
1577 
1578         // documented in JXG.AbstractRenderer
1579         setObjectStrokeWidth: function (el, width) {
1580             var node,
1581                 w = width;
1582 
1583             if (isNaN(w) || el.visPropOld.strokewidth === w) {
1584                 return;
1585             }
1586 
1587             node = el.rendNode;
1588             this.setPropertyPrim(node, "stroked", "true");
1589             if (Type.exists(w)) {
1590                 this.setPropertyPrim(node, "stroke-width", w + "px");
1591 
1592                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
1593                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
1594                 //     if (el.evalVisProp('firstarrow')) {
1595                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
1596                 //     }
1597                 //
1598                 //     if (el.evalVisProp('lastarrow')) {
1599                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
1600                 //     }
1601                 // }
1602             }
1603             el.visPropOld.strokewidth = w;
1604         },
1605 
1606         // documented in JXG.AbstractRenderer
1607         setObjectTransition: function (el, duration) {
1608             var node, props,
1609                 transitionArr = [],
1610                 transitionStr,
1611                 i,
1612                 len = 0,
1613                 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"];
1614 
1615             if (duration === undefined) {
1616                 duration = el.evalVisProp('transitionduration');
1617             }
1618 
1619             props = el.evalVisProp('transitionproperties');
1620             if (duration === el.visPropOld.transitionduration &&
1621                 props === el.visPropOld.transitionproperties) {
1622                 return;
1623             }
1624 
1625             // if (
1626             //     el.elementClass === Const.OBJECT_CLASS_TEXT &&
1627             //     el.evalVisProp('display') === "html"
1628             // ) {
1629             //     // transitionStr = " color " + duration + "ms," +
1630             //     //     " opacity " + duration + "ms";
1631             //     transitionStr = " all " + duration + "ms ease";
1632             // } else {
1633             //     transitionStr =
1634             //         " fill " + duration + "ms," +
1635             //         " fill-opacity " + duration + "ms," +
1636             //         " stroke " + duration + "ms," +
1637             //         " stroke-opacity " + duration + "ms," +
1638             //         " stroke-width " + duration + "ms," +
1639             //         " width " + duration + "ms," +
1640             //         " height " + duration + "ms," +
1641             //         " rx " + duration + "ms," +
1642             //         " ry " + duration + "ms";
1643             // }
1644 
1645             if (Type.exists(props)) {
1646                 len = props.length;
1647             }
1648             for (i = 0; i < len; i++) {
1649                 transitionArr.push(props[i] + ' ' + duration + 'ms');
1650             }
1651             transitionStr = transitionArr.join(', ');
1652 
1653             len = nodes.length;
1654             for (i = 0; i < len; ++i) {
1655                 if (el[nodes[i]]) {
1656                     node = el[nodes[i]];
1657                     node.style.transition = transitionStr;
1658                 }
1659             }
1660 
1661             el.visPropOld.transitionduration = duration;
1662             el.visPropOld.transitionproperties = props;
1663         },
1664 
1665         // documented in JXG.AbstractRenderer
1666         setShadow: function (el) {
1667             var ev_s = el.evalVisProp('shadow'),
1668                 ev_s_json, c, b, bl, o, op, id, node,
1669                 use_board_filter = true,
1670                 show = false;
1671 
1672             ev_s_json = JSON.stringify(ev_s);
1673             if (ev_s_json === el.visPropOld.shadow) {
1674                 return;
1675             }
1676 
1677             if (typeof ev_s === 'boolean') {
1678                 use_board_filter = true;
1679                 show = ev_s;
1680                 c = 'none';
1681                 b = 3;
1682                 bl = 0.1;
1683                 o = [5, 5];
1684                 op = 1;
1685             } else {
1686                 if (el.evalVisProp('shadow.enabled')) {
1687                     use_board_filter = false;
1688                     show = true;
1689                     c = JXG.rgbParser(el.evalVisProp('shadow.color'));
1690                     b = el.evalVisProp('shadow.blur');
1691                     bl = el.evalVisProp('shadow.blend');
1692                     o = el.evalVisProp('shadow.offset');
1693                     op = el.evalVisProp('shadow.opacity');
1694                 } else {
1695                     show = false;
1696                 }
1697             }
1698 
1699             if (Type.exists(el.rendNode)) {
1700                 if (show) {
1701                     if (use_board_filter) {
1702                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1'));
1703                         // 'url(#' + this.container.id + '_' + 'f1)');
1704                     } else {
1705                         node = this.container.ownerDocument.getElementById(id);
1706                         if (node) {
1707                             this.defs.removeChild(node);
1708                         }
1709                         id = el.rendNode.id + '_' + 'f1';
1710                         this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o));
1711                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(id));
1712                         // 'url(#' + id + ')');
1713                     }
1714                 } else {
1715                     el.rendNode.removeAttributeNS(null, 'filter');
1716                 }
1717             }
1718 
1719             el.visPropOld.shadow = ev_s_json;
1720         },
1721 
1722         // documented in JXG.AbstractRenderer
1723         setTabindex: function (el) {
1724             var val;
1725             if (el.board.attr.keyboard.enabled && Type.exists(el.rendNode)) {
1726                 val = el.evalVisProp('tabindex');
1727                 if (!el.visPropCalc.visible /* || el.evalVisProp('fixed') */) {
1728                     val = null;
1729                 }
1730                 if (val !== el.visPropOld.tabindex) {
1731                     el.rendNode.setAttribute("tabindex", val);
1732                     el.visPropOld.tabindex = val;
1733                 }
1734             }
1735         },
1736 
1737         // documented in JXG.AbstractRenderer
1738         setPropertyPrim: function (node, key, val) {
1739             if (key === "stroked") {
1740                 return;
1741             }
1742             node.setAttributeNS(null, key, val);
1743         },
1744 
1745         // documented in JXG.AbstractRenderer
1746         show: function (el) {
1747             JXG.deprecated("Board.renderer.show()", "Board.renderer.display()");
1748             this.display(el, true);
1749             // var node;
1750             //
1751             // if (el && el.rendNode) {
1752             //     node = el.rendNode;
1753             //     node.setAttributeNS(null, 'display', 'inline');
1754             //     node.style.visibility = "inherit";
1755             // }
1756         },
1757 
1758         // documented in JXG.AbstractRenderer
1759         updateGradient: function (el) {
1760             var col,
1761                 op,
1762                 node2 = el.gradNode1,
1763                 node3 = el.gradNode2,
1764                 ev_g = el.evalVisProp('gradient');
1765 
1766             if (!Type.exists(node2) || !Type.exists(node3)) {
1767                 return;
1768             }
1769 
1770             op = el.evalVisProp('fillopacity');
1771             op = op > 0 ? op : 0;
1772             col = el.evalVisProp('fillcolor');
1773 
1774             node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op);
1775             node3.setAttributeNS(
1776                 null,
1777                 "style",
1778                 "stop-color:" +
1779                 el.evalVisProp('gradientsecondcolor') +
1780                 ";stop-opacity:" +
1781                 el.evalVisProp('gradientsecondopacity')
1782             );
1783             node2.setAttributeNS(
1784                 null,
1785                 "offset",
1786                 el.evalVisProp('gradientstartoffset') * 100 + "%"
1787             );
1788             node3.setAttributeNS(
1789                 null,
1790                 "offset",
1791                 el.evalVisProp('gradientendoffset') * 100 + "%"
1792             );
1793             if (ev_g === "linear") {
1794                 this.updateGradientAngle(el.gradNode, el.evalVisProp('gradientangle'));
1795             } else if (ev_g === "radial") {
1796                 this.updateGradientCircle(
1797                     el.gradNode,
1798                     el.evalVisProp('gradientcx'),
1799                     el.evalVisProp('gradientcy'),
1800                     el.evalVisProp('gradientr'),
1801                     el.evalVisProp('gradientfx'),
1802                     el.evalVisProp('gradientfy'),
1803                     el.evalVisProp('gradientfr')
1804                 );
1805             }
1806         },
1807 
1808         /**
1809          * Set the gradient angle for linear color gradients.
1810          *
1811          * @private
1812          * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
1813          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
1814          */
1815         updateGradientAngle: function (node, radians) {
1816             // Angles:
1817             // 0: ->
1818             // 90: down
1819             // 180: <-
1820             // 90: up
1821             var f = 1.0,
1822                 co = Math.cos(radians),
1823                 si = Math.sin(radians);
1824 
1825             if (Math.abs(co) > Math.abs(si)) {
1826                 f /= Math.abs(co);
1827             } else {
1828                 f /= Math.abs(si);
1829             }
1830 
1831             if (co >= 0) {
1832                 node.setAttributeNS(null, "x1", 0);
1833                 node.setAttributeNS(null, "x2", co * f);
1834             } else {
1835                 node.setAttributeNS(null, "x1", -co * f);
1836                 node.setAttributeNS(null, "x2", 0);
1837             }
1838             if (si >= 0) {
1839                 node.setAttributeNS(null, "y1", 0);
1840                 node.setAttributeNS(null, "y2", si * f);
1841             } else {
1842                 node.setAttributeNS(null, "y1", -si * f);
1843                 node.setAttributeNS(null, "y2", 0);
1844             }
1845         },
1846 
1847         /**
1848          * Set circles for radial color gradients.
1849          *
1850          * @private
1851          * @param {SVGnode} node SVG gradient node
1852          * @param {Number} cx SVG value cx (value between 0 and 1)
1853          * @param {Number} cy  SVG value cy (value between 0 and 1)
1854          * @param {Number} r  SVG value r (value between 0 and 1)
1855          * @param {Number} fx  SVG value fx (value between 0 and 1)
1856          * @param {Number} fy  SVG value fy (value between 0 and 1)
1857          * @param {Number} fr  SVG value fr (value between 0 and 1)
1858          */
1859         updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) {
1860             node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color
1861             node.setAttributeNS(null, "cy", cy * 100 + "%");
1862             node.setAttributeNS(null, "r", r * 100 + "%");
1863             node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point
1864             node.setAttributeNS(null, "fy", fy * 100 + "%");
1865             node.setAttributeNS(null, "fr", fr * 100 + "%");
1866         },
1867 
1868         /* ********* Renderer control *********** */
1869 
1870         // documented in JXG.AbstractRenderer
1871         suspendRedraw: function () {
1872             // It seems to be important for the Linux version of firefox
1873             this.suspendHandle = this.svgRoot.suspendRedraw(10000);
1874         },
1875 
1876         // documented in JXG.AbstractRenderer
1877         unsuspendRedraw: function () {
1878             this.svgRoot.unsuspendRedraw(this.suspendHandle);
1879             // this.svgRoot.unsuspendRedrawAll();
1880             //this.svgRoot.forceRedraw();
1881         },
1882 
1883         // documented in AbstractRenderer
1884         resize: function (w, h) {
1885             this.svgRoot.setAttribute("width", parseFloat(w));
1886             this.svgRoot.setAttribute("height", parseFloat(h));
1887         },
1888 
1889         // documented in JXG.AbstractRenderer
1890         createTouchpoints: function (n) {
1891             var i, na1, na2, node;
1892             this.touchpoints = [];
1893             for (i = 0; i < n; i++) {
1894                 na1 = "touchpoint1_" + i;
1895                 node = this.createPrim("path", na1);
1896                 this.appendChildPrim(node, 19);
1897                 node.setAttributeNS(null, "d", "M 0 0");
1898                 this.touchpoints.push(node);
1899 
1900                 this.setPropertyPrim(node, "stroked", "true");
1901                 this.setPropertyPrim(node, "stroke-width", "1px");
1902                 node.setAttributeNS(null, "stroke", "#000000");
1903                 node.setAttributeNS(null, "stroke-opacity", 1.0);
1904                 node.setAttributeNS(null, "display", "none");
1905 
1906                 na2 = "touchpoint2_" + i;
1907                 node = this.createPrim("ellipse", na2);
1908                 this.appendChildPrim(node, 19);
1909                 this.updateEllipsePrim(node, 0, 0, 0, 0);
1910                 this.touchpoints.push(node);
1911 
1912                 this.setPropertyPrim(node, "stroked", "true");
1913                 this.setPropertyPrim(node, "stroke-width", "1px");
1914                 node.setAttributeNS(null, "stroke", "#000000");
1915                 node.setAttributeNS(null, "stroke-opacity", 1.0);
1916                 node.setAttributeNS(null, "fill", "#ffffff");
1917                 node.setAttributeNS(null, "fill-opacity", 0.0);
1918 
1919                 node.setAttributeNS(null, "display", "none");
1920             }
1921         },
1922 
1923         // documented in JXG.AbstractRenderer
1924         showTouchpoint: function (i) {
1925             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1926                 this.touchpoints[2 * i].setAttributeNS(null, "display", "inline");
1927                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "inline");
1928             }
1929         },
1930 
1931         // documented in JXG.AbstractRenderer
1932         hideTouchpoint: function (i) {
1933             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1934                 this.touchpoints[2 * i].setAttributeNS(null, "display", "none");
1935                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "none");
1936             }
1937         },
1938 
1939         // documented in JXG.AbstractRenderer
1940         updateTouchpoint: function (i, pos) {
1941             var x,
1942                 y,
1943                 d = 37;
1944 
1945             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1946                 x = pos[0];
1947                 y = pos[1];
1948 
1949                 this.touchpoints[2 * i].setAttributeNS(
1950                     null,
1951                     "d",
1952                     "M " +
1953                     (x - d) +
1954                     " " +
1955                     y +
1956                     " " +
1957                     "L " +
1958                     (x + d) +
1959                     " " +
1960                     y +
1961                     " " +
1962                     "M " +
1963                     x +
1964                     " " +
1965                     (y - d) +
1966                     " " +
1967                     "L " +
1968                     x +
1969                     " " +
1970                     (y + d)
1971                 );
1972                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
1973             }
1974         },
1975 
1976         /* ********* Dump related stuff *********** */
1977 
1978         /**
1979          * Walk recursively through the DOM subtree of a node and collect all
1980          * value attributes together with the id of that node.
1981          * <b>Attention:</b> Only values of nodes having a valid id are taken.
1982          * @param  {Node} node   root node of DOM subtree that will be searched recursively.
1983          * @return {Array}      Array with entries of the form [id, value]
1984          * @private
1985          */
1986         _getValuesOfDOMElements: function (node) {
1987             var values = [];
1988             if (node.nodeType === 1) {
1989                 node = node.firstChild;
1990                 while (node) {
1991                     if (node.id !== undefined && node.value !== undefined) {
1992                         values.push([node.id, node.value]);
1993                     }
1994                     Type.concat(values, this._getValuesOfDOMElements(node));
1995                     node = node.nextSibling;
1996                 }
1997             }
1998             return values;
1999         },
2000 
2001         // _getDataUri: function (url, callback) {
2002         //     var image = new Image();
2003         //     image.onload = function () {
2004         //         var canvas = document.createElement("canvas");
2005         //         canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
2006         //         canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
2007         //         canvas.getContext("2d").drawImage(this, 0, 0);
2008         //         callback(canvas.toDataURL("image/png"));
2009         //         canvas.remove();
2010         //     };
2011         //     image.src = url;
2012         // },
2013 
2014         _getImgDataURL: function (svgRoot) {
2015             var images, len, canvas, ctx, ur, i;
2016 
2017             images = svgRoot.getElementsByTagName("image");
2018             len = images.length;
2019             if (len > 0) {
2020                 canvas = document.createElement("canvas");
2021                 //img = new Image();
2022                 for (i = 0; i < len; i++) {
2023                     images[i].setAttribute("crossorigin", "anonymous");
2024                     //img.src = images[i].href;
2025                     //img.onload = function() {
2026                     // img.crossOrigin = "anonymous";
2027                     ctx = canvas.getContext("2d");
2028                     canvas.width = images[i].getAttribute("width");
2029                     canvas.height = images[i].getAttribute("height");
2030                     try {
2031                         ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
2032 
2033                         // If the image is not png, the format must be specified here
2034                         ur = canvas.toDataURL();
2035                         images[i].setAttribute("xlink:href", ur);
2036                     } catch (err) {
2037                         console.log("CORS problem! Image can not be used", err);
2038                     }
2039                 }
2040                 //canvas.remove();
2041             }
2042             return true;
2043         },
2044 
2045         /**
2046          * Return a data URI of the SVG code representing the construction.
2047          * The SVG code of the construction is base64 encoded. The return string starts
2048          * with "data:image/svg+xml;base64,...".
2049          *
2050          * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
2051          * This is necessary for older versions of Safari. Default: false
2052          * @returns {String}  data URI string
2053          *
2054          * @example
2055          * var A = board.create('point', [2, 2]);
2056          *
2057          * var txt = board.renderer.dumpToDataURI(false);
2058          * // txt consists of a string of the form
2059          * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2060          * // Behind the comma, there is the base64 encoded SVG code
2061          * // which is decoded with atob().
2062          * // The call of decodeURIComponent(escape(...)) is necessary
2063          * // to handle unicode strings correctly.
2064          * var ar = txt.split(',');
2065          * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1])));
2066          *
2067          * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div>
2068          * <textarea id="output2023" rows="5" cols="50"></textarea>
2069          * <script type="text/javascript">
2070          *     (function() {
2071          *         var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d',
2072          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
2073          *     var A = board.create('point', [2, 2]);
2074          *
2075          *     var txt = board.renderer.dumpToDataURI(false);
2076          *     // txt consists of a string of the form
2077          *     // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2078          *     // Behind the comma, there is the base64 encoded SVG code
2079          *     // which is decoded with atob().
2080          *     // The call of decodeURIComponent(escape(...)) is necessary
2081          *     // to handle unicode strings correctly.
2082          *     var ar = txt.split(',');
2083          *     document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1])));
2084          *
2085          *     })();
2086          *
2087          * </script><pre>
2088          *
2089          */
2090         dumpToDataURI: function (ignoreTexts) {
2091             var svgRoot = this.svgRoot,
2092                 btoa = window.btoa || Base64.encode,
2093                 svg, i, len,
2094                 values = [];
2095 
2096             // Move all HTML tags (beside the SVG root) of the container
2097             // to the foreignObject element inside of the svgRoot node
2098             // Problem:
2099             // input values are not copied. This can be verified by looking at an innerHTML output
2100             // of an input element. Therefore, we do it "by hand".
2101             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
2102                 if (!ignoreTexts) {
2103                     this.foreignObjLayer.setAttribute("display", "inline");
2104                 }
2105                 while (svgRoot.nextSibling) {
2106                     // Copy all value attributes
2107                     Type.concat(values, this._getValuesOfDOMElements(svgRoot.nextSibling));
2108                     this.foreignObjLayer.appendChild(svgRoot.nextSibling);
2109                 }
2110             }
2111 
2112             this._getImgDataURL(svgRoot);
2113 
2114             // Convert the SVG graphic into a string containing SVG code
2115             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2116             svg = new XMLSerializer().serializeToString(svgRoot);
2117 
2118             if (ignoreTexts !== true) {
2119                 // Handle SVG texts
2120                 // Insert all value attributes back into the svg string
2121                 len = values.length;
2122                 for (i = 0; i < len; i++) {
2123                     svg = svg.replace(
2124                         'id="' + values[i][0] + '"',
2125                         'id="' + values[i][0] + '" value="' + values[i][1] + '"'
2126                     );
2127                 }
2128             }
2129 
2130             // if (false) {
2131             //     // Debug: use example svg image
2132             //     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>';
2133             // }
2134 
2135             // In IE we have to remove the namespace again.
2136             // Since 2024 we have to check if the namespace attribute appears twice in one tag, because
2137             // there might by a svg inside of the svg, e.g. the screenshot icon.
2138             if (this.isIE &&
2139                 (svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1
2140             ) {
2141                 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g, "");
2142             }
2143 
2144             // Safari fails if the svg string contains a " "
2145             // Obsolete with Safari 12+
2146             svg = svg.replace(/ /g, " ");
2147             // Replacing "s might be necessary for older Safari versions
2148             // svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); // Bug: does not replace matching "s
2149             // svg = svg.replace(/"/g, "");
2150 
2151             // Move all HTML tags back from
2152             // the foreignObject element to the container
2153             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
2154                 // Restore all HTML elements
2155                 while (this.foreignObjLayer.firstChild) {
2156                     this.container.appendChild(this.foreignObjLayer.firstChild);
2157                 }
2158                 this.foreignObjLayer.setAttribute("display", "none");
2159             }
2160 
2161             return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg)));
2162         },
2163 
2164         /**
2165          * Convert the SVG construction into an HTML canvas image.
2166          * This works for all SVG supporting browsers. Implemented as Promise.
2167          * <p>
2168          * Might fail if any text element or foreign object element contains SVG. This
2169          * is the case e.g. for the default fullscreen symbol.
2170          * <p>
2171          * For IE, it is realized as function.
2172          * It works from version 9, with the exception that HTML texts
2173          * are ignored on IE. The drawing is done with a delay of
2174          * 200 ms. Otherwise there would be problems with IE.
2175          *
2176          * @param {String} canvasId Id of an HTML canvas element
2177          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
2178          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
2179          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
2180          * This is necessary for older versions of Safari. Default: false
2181          * @returns {Promise}  Promise object
2182          *
2183          * @example
2184          * 	board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
2185          *
2186          * @example
2187          *  // IE 11 example:
2188          * 	board.renderer.dumpToCanvas('canvas');
2189          * 	setTimeout(function() { console.log('done'); }, 400);
2190          */
2191         dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
2192             var svg, tmpImg,
2193                 cv, ctx,
2194                 doc = this.container.ownerDocument;
2195 
2196             // Prepare the canvas element
2197             cv = doc.getElementById(canvasId);
2198 
2199             // Clear the canvas
2200             /* eslint-disable no-self-assign */
2201             cv.width = cv.width;
2202             /* eslint-enable no-self-assign */
2203 
2204             ctx = cv.getContext("2d");
2205             if (w !== undefined && h !== undefined) {
2206                 cv.style.width = parseFloat(w) + "px";
2207                 cv.style.height = parseFloat(h) + "px";
2208                 // Scale twice the CSS size to make the image crisp
2209                 // cv.setAttribute('width', 2 * parseFloat(wOrg));
2210                 // cv.setAttribute('height', 2 * parseFloat(hOrg));
2211                 // ctx.scale(2 * wOrg / w, 2 * hOrg / h);
2212                 cv.setAttribute("width", parseFloat(w));
2213                 cv.setAttribute("height", parseFloat(h));
2214             }
2215 
2216             // Display the SVG string as data-uri in an HTML img.
2217             /**
2218              * @type {Image}
2219              * @ignore
2220              * {ignore}
2221              */
2222             tmpImg = new Image();
2223             svg = this.dumpToDataURI(ignoreTexts);
2224             tmpImg.src = svg;
2225 
2226             // Finally, draw the HTML img in the canvas.
2227             if (!("Promise" in window)) {
2228                 /**
2229                  * @function
2230                  * @ignore
2231                  */
2232                 tmpImg.onload = function () {
2233                     // IE needs a pause...
2234                     // Seems to be broken
2235                     window.setTimeout(function () {
2236                         try {
2237                             ctx.drawImage(tmpImg, 0, 0, w, h);
2238                         } catch (err) {
2239                             console.log("screenshots not longer supported on IE");
2240                         }
2241                     }, 200);
2242                 };
2243                 return this;
2244             }
2245 
2246             return new Promise(function (resolve, reject) {
2247                 try {
2248                     tmpImg.onload = function () {
2249                         ctx.drawImage(tmpImg, 0, 0, w, h);
2250                         resolve();
2251                     };
2252                 } catch (e) {
2253                     reject(e);
2254                 }
2255             });
2256         },
2257 
2258         /**
2259          * Display SVG image in html img-tag which enables
2260          * easy download for the user.
2261          *
2262          * Support:
2263          * <ul>
2264          * <li> IE: No
2265          * <li> Edge: full
2266          * <li> Firefox: full
2267          * <li> Chrome: full
2268          * <li> Safari: full (No text support in versions prior to 12).
2269          * </ul>
2270          *
2271          * @param {JXG.Board} board Link to the board.
2272          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
2273          * the screenshot is copied to this img object. The width and height will be set to the values of the
2274          * JSXGraph container.
2275          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
2276          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
2277          * @return {Object}       the svg renderer object
2278          */
2279         screenshot: function (board, imgId, ignoreTexts) {
2280             var node,
2281                 doc = this.container.ownerDocument,
2282                 parent = this.container.parentNode,
2283                 // cPos,
2284                 // cssTxt,
2285                 canvas, id, img,
2286                 button, buttonText,
2287                 w, h,
2288                 bas = board.attr.screenshot,
2289                 navbar, navbarDisplay, insert,
2290                 newImg = false,
2291                 _copyCanvasToImg,
2292                 isDebug = false;
2293 
2294             if (this.type === "no") {
2295                 return this;
2296             }
2297 
2298             w = bas.scale * this.container.getBoundingClientRect().width;
2299             h = bas.scale * this.container.getBoundingClientRect().height;
2300 
2301             if (imgId === undefined || imgId === "") {
2302                 newImg = true;
2303                 img = new Image(); //doc.createElement('img');
2304                 img.style.width = w + "px";
2305                 img.style.height = h + "px";
2306             } else {
2307                 newImg = false;
2308                 img = doc.getElementById(imgId);
2309             }
2310             // img.crossOrigin = 'anonymous';
2311 
2312             // Create div which contains canvas element and close button
2313             if (newImg) {
2314                 node = doc.createElement("div");
2315                 node.style.cssText = bas.css;
2316                 node.style.width = w + "px";
2317                 node.style.height = h + "px";
2318                 node.style.zIndex = this.container.style.zIndex + 120;
2319 
2320                 // Try to position the div exactly over the JSXGraph board
2321                 node.style.position = "absolute";
2322                 node.style.top = this.container.offsetTop + "px";
2323                 node.style.left = this.container.offsetLeft + "px";
2324             }
2325 
2326             if (!isDebug) {
2327                 // Create canvas element and add it to the DOM
2328                 // It will be removed after the image has been stored.
2329                 canvas = doc.createElement("canvas");
2330                 id = Math.random().toString(36).slice(2, 7);
2331                 canvas.setAttribute("id", id);
2332                 canvas.setAttribute("width", w);
2333                 canvas.setAttribute("height", h);
2334                 canvas.style.width = w + "px";
2335                 canvas.style.height = w + "px";
2336                 canvas.style.display = "none";
2337                 parent.appendChild(canvas);
2338             } else {
2339                 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
2340                 id = "jxgbox_canvas";
2341                 canvas = doc.getElementById(id);
2342             }
2343 
2344             if (newImg) {
2345                 // Create close button
2346                 button = doc.createElement("span");
2347                 buttonText = doc.createTextNode("\u2716");
2348                 button.style.cssText = bas.cssButton;
2349                 button.appendChild(buttonText);
2350                 button.onclick = function () {
2351                     node.parentNode.removeChild(node);
2352                 };
2353 
2354                 // Add all nodes
2355                 node.appendChild(img);
2356                 node.appendChild(button);
2357                 parent.insertBefore(node, this.container.nextSibling);
2358             }
2359 
2360             // Hide navigation bar in board
2361             navbar = doc.getElementById(this.uniqName('navigationbar'));
2362             if (Type.exists(navbar)) {
2363                 navbarDisplay = navbar.style.display;
2364                 navbar.style.display = "none";
2365                 insert = this.removeToInsertLater(navbar);
2366             }
2367 
2368             _copyCanvasToImg = function () {
2369                 // Show image in img tag
2370                 img.src = canvas.toDataURL("image/png");
2371 
2372                 // Remove canvas node
2373                 if (!isDebug) {
2374                     parent.removeChild(canvas);
2375                 }
2376             };
2377 
2378             // Create screenshot in image element
2379             if ("Promise" in window) {
2380                 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
2381             } else {
2382                 // IE
2383                 this.dumpToCanvas(id, w, h, ignoreTexts);
2384                 window.setTimeout(_copyCanvasToImg, 200);
2385             }
2386 
2387             // Reinsert navigation bar in board
2388             if (Type.exists(navbar)) {
2389                 navbar.style.display = navbarDisplay;
2390                 insert();
2391             }
2392 
2393             return this;
2394         }
2395     }
2396 );
2397 
2398 export default JXG.SVGRenderer;
2399