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