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