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