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