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                 // d, z1, scr1, lbda, mu,
1146                 // xt, xb, yt, yb,
1147                 // xl, xr, yl, yr,
1148                 pStr = '';
1149 
1150             if (el.numberPoints <= 0) {
1151                 return '';
1152             }
1153 
1154             len = Math.min(el.points.length, el.numberPoints);
1155 
1156             if (el.bezierDegree === 1) {
1157                 for (i = 0; i < len; i++) {
1158                     scr = el.points[i].scrCoords;
1159                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1160                         // PenUp
1161                         nextSymb = symbm;
1162                     } else {
1163                         // Chrome has problems with values being too far away.
1164                         // In early implementations it was recommended to restrict numbers to abs value 5000,
1165                         // see https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html#:~:text=If%20you%20are%20creating%20a,no%20bigger%20than%20%C2%B15%2C000.
1166                         // Attention: there may be conflicts with RDP smoothing.
1167                         //
1168                         // March 2026: This restriction seems to be osbsolete.
1169                         // Meanwhile all major browsers support 32 floats, see
1170                         // https://www.w3.org/TR/SVG/types.html, section "4.2.1. Real number precision"
1171                         //
1172                         // Change in-place:
1173                         // scr[1] = Math.max(Math.min(scr[1], M), -M);
1174                         // scr[2] = Math.max(Math.min(scr[2], M), -M);
1175                         // Change not in-place (preferred 2026):
1176                         // sc1 = Math.max(Math.min(scr[1], M), -M);
1177                         // sc2 = Math.max(Math.min(scr[2], M), -M);
1178                         //
1179                         scx = scr[1];
1180                         scy = scr[2];
1181 
1182                         // Some first steps to project coordinates to the virtual
1183                         // clip box [-5000, 5000, 5000, -5000].
1184                         // But - hopefully - we do not need to develop this anymore.
1185                         // Intersections with the clip box.
1186                         // Todo: choose the right one.
1187                         // if (i > 0) {
1188                         //     scr1 = el.points[i - 1].scrCoords;
1189                         //     d = sc2 - scr1[2];
1190                         //     if (d !== 0) {
1191                         //         lbda = (M - scr1[2]) / d;
1192                         //         xt = scr1[1] + lbda * (sc1 - scr1[1]); yt = M;
1193 
1194                         //         lbda = (-M - scr1[2]) / d;
1195                         //         xb = scr1[1] + lbda * (sc1 - scr1[1]); yb = -M;
1196                         //     }
1197                         //     d = sc1 - scr1[1];
1198                         //     if (d !== 0) {
1199                         //         lbda = (M - scr1[2]) / d;
1200                         //         yr = scr1[2] + lbda * (sc2 - scr1[2]); xr = M;
1201                         //         lbda = (-M - scr1[2]) / d;
1202                         //         yl = scr1[2] + lbda * (sc2 - scr1[2]); xl = -M;
1203                         //     }
1204                         // }
1205                         //
1206                         // Attention: first coordinate may be inaccurate if far way
1207                         // pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1208                         // pStr += nextSymb + scr[1] + ' ' + scr[2]; // '+' seems to be faster than 'join' now (webkit and firefox)
1209                         pStr += nextSymb + scx + ' ' + scy; // '+' seems to be faster than 'join' now (webkit and firefox)
1210                         nextSymb = symbl;
1211                     }
1212                 }
1213             } else if (el.bezierDegree === 3) {
1214                 i = 0;
1215                 while (i < len) {
1216                     scr = el.points[i].scrCoords;
1217                     scx = scr[1];
1218                     scy = scr[2];
1219                     if (isNaN(scx) || isNaN(scy)) {
1220                         // PenUp
1221                         nextSymb = symbm;
1222                     } else {
1223                         pStr += nextSymb + scx + ' ' + scy;
1224                         if (nextSymb === symbc) {
1225                             i += 1;
1226                             scr = el.points[i].scrCoords;
1227                             pStr += ' ' + scr[1] + ' ' + scr[2];
1228                             i += 1;
1229                             scr = el.points[i].scrCoords;
1230                             pStr += ' ' + scr[1] + ' ' + scr[2];
1231                         }
1232                         nextSymb = symbc;
1233                     }
1234                     i += 1;
1235                 }
1236             }
1237             return pStr;
1238         },
1239 
1240         // Already documented in JXG.AbstractRenderer
1241         updatePathStringBezierPrim: function (el) {
1242             var i, j, k,
1243                 scr, sc1, sc2,
1244                 lx, ly,
1245                 len,
1246                 symbm = ' M ',
1247                 symbl = ' C ',
1248                 nextSymb = symbm,
1249                 // M = Env.maxScreenCoord,
1250                 pStr = '',
1251                 f = el.evalVisProp('strokewidth'),
1252                 isNoPlot = el.evalVisProp('curvetype') !== 'plot';
1253 
1254             if (el.numberPoints <= 0) {
1255                 return '';
1256             }
1257 
1258             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
1259                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
1260             }
1261 
1262             len = Math.min(el.points.length, el.numberPoints);
1263             for (j = 1; j < 3; j++) {
1264                 nextSymb = symbm;
1265                 for (i = 0; i < len; i++) {
1266                     scr = el.points[i].scrCoords;
1267 
1268                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1269                         // PenUp
1270                         nextSymb = symbm;
1271                     } else {
1272                         // Chrome has problems with values being too far away.
1273                         // scr[1] = Math.max(Math.min(scr[1], M), -M);
1274                         // scr[2] = Math.max(Math.min(scr[2], M), -M);
1275                         // sc1 = Math.max(Math.min(scr[1], M), -M);
1276                         // sc2 = Math.max(Math.min(scr[2], M), -M);
1277                         sc1 = scr[1];
1278                         sc2 = scr[2];
1279 
1280                         // Attention: first coordinate may be inaccurate if far way
1281                         if (nextSymb === symbm) {
1282                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1283                             pStr += nextSymb + sc1 + ' ' + sc2;   // Seems to be faster now (webkit and firefox)
1284                         } else {
1285                             k = 2 * j;
1286                             pStr += [
1287                                 nextSymb,
1288                                 lx + (sc1 - lx) * 0.333 + f * (k * Math.random() - j), ' ',
1289                                 ly + (sc2 - ly) * 0.333 + f * (k * Math.random() - j), ' ',
1290                                 lx + (sc1 - lx) * 0.666 + f * (k * Math.random() - j), ' ',
1291                                 ly + (sc2 - ly) * 0.666 + f * (k * Math.random() - j), ' ',
1292                                 sc1, ' ', sc2
1293                             ].join('');
1294                         }
1295 
1296                         nextSymb = symbl;
1297                         lx = sc1;
1298                         ly = sc2;
1299                     }
1300                 }
1301             }
1302             return pStr;
1303         },
1304 
1305         // Already documented in JXG.AbstractRenderer
1306         updatePolygonPrim: function (node, el) {
1307             var i,
1308                 pStr = "",
1309                 scrCoords,
1310                 len = el.vertices.length;
1311 
1312             node.setAttributeNS(null, "stroke", 'none');
1313             node.setAttributeNS(null, "fill-rule", 'evenodd');
1314             if (el.elType === 'polygonalchain') {
1315                 len++;
1316             }
1317 
1318             for (i = 0; i < len - 1; i++) {
1319                 if (el.vertices[i].isReal) {
1320                     scrCoords = el.vertices[i].coords.scrCoords;
1321                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
1322                 } else {
1323                     node.setAttributeNS(null, "points", "");
1324                     return;
1325                 }
1326 
1327                 if (i < len - 2) {
1328                     pStr += " ";
1329                 }
1330             }
1331             if (pStr.indexOf('NaN') === -1) {
1332                 node.setAttributeNS(null, "points", pStr);
1333             }
1334         },
1335 
1336         // Already documented in JXG.AbstractRenderer
1337         updateRectPrim: function (node, x, y, w, h) {
1338             node.setAttributeNS(null, "x", x);
1339             node.setAttributeNS(null, "y", y);
1340             node.setAttributeNS(null, "width", w);
1341             node.setAttributeNS(null, "height", h);
1342         },
1343 
1344         /* ********* Set attributes *********** */
1345 
1346         /**
1347          * Call user-defined function to set visual attributes.
1348          * If "testAttribute" is the empty string, the function
1349          * is called immediately, otherwise it is called in a timeOut.
1350          *
1351          * This is necessary to realize smooth transitions but avoid transitions
1352          * when first creating the objects.
1353          *
1354          * Usually, the string in testAttribute is the visPropOld attribute
1355          * of the values which are set.
1356          *
1357          * @param {Function} setFunc       Some function which usually sets some attributes
1358          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
1359          *                               otherwise it is called in a setImeout.
1360          * @see JXG.SVGRenderer#setObjectFillColor
1361          * @see JXG.SVGRenderer#setObjectStrokeColor
1362          * @see JXG.SVGRenderer#_setArrowColor
1363          * @private
1364          */
1365         _setAttribute: function (setFunc, testAttribute) {
1366             if (testAttribute === "") {
1367                 setFunc();
1368             } else {
1369                 window.setTimeout(setFunc, 1);
1370             }
1371         },
1372 
1373         display: function (el, val) {
1374             var node;
1375 
1376             if (el && el.rendNode) {
1377                 el.visPropOld.visible = val;
1378                 node = el.rendNode;
1379                 if (val) {
1380                     node.setAttributeNS(null, "display", 'inline');
1381                     node.style.visibility = 'inherit';
1382                 } else {
1383                     node.setAttributeNS(null, "display", 'none');
1384                     node.style.visibility = 'hidden';
1385                 }
1386             }
1387         },
1388 
1389         // documented in JXG.AbstractRenderer
1390         hide: function (el) {
1391             JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()");
1392             this.display(el, false);
1393         },
1394 
1395         // documented in JXG.AbstractRenderer
1396         setARIA: function(el) {
1397             // This method is only called in abstractRenderer._updateVisual() if aria.enabled == true.
1398             var key, k, v;
1399 
1400             // this.setPropertyPrim(el.rendNode, 'aria-label', el.evalVisProp('aria.label'));
1401             // this.setPropertyPrim(el.rendNode, 'aria-live', el.evalVisProp('aria.live'));
1402             for (key in el.visProp.aria) {
1403                 if (el.visProp.aria.hasOwnProperty(key) && key !== 'enabled') {
1404                     k = 'aria.' + key;
1405                     v = el.evalVisProp('aria.' + key);
1406                     if (el.visPropOld[k] !== v) {
1407                         this.setPropertyPrim(el.rendNode, 'aria-' + key, v);
1408                         el.visPropOld[k] = v;
1409                     }
1410                 }
1411             }
1412         },
1413 
1414         // documented in JXG.AbstractRenderer
1415         setBuffering: function (el, type) {
1416             el.rendNode.setAttribute("buffered-rendering", type);
1417         },
1418 
1419         // documented in JXG.AbstractRenderer
1420         setCssClass(el, cssClass) {
1421 
1422             if (el.visPropOld.cssclass !== cssClass) {
1423                 this.setPropertyPrim(el.rendNode, 'class', cssClass);
1424                 el.visPropOld.cssclass = cssClass;
1425             }
1426         },
1427 
1428         // documented in JXG.AbstractRenderer
1429         setDashStyle: function (el) {
1430             var dashStyle = el.evalVisProp('dash'),
1431                 ds = el.evalVisProp('dashscale'),
1432                 sw = ds ? 0.5 * el.evalVisProp('strokewidth') : 1,
1433                 node = el.rendNode;
1434 
1435             if (dashStyle > 0) {
1436                 node.setAttributeNS(null, "stroke-dasharray",
1437                     // sw could distinguish highlighting or not.
1438                     // But it seems to preferable to ignore this.
1439                     this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',')
1440                 );
1441             } else {
1442                 if (node.hasAttributeNS(null, "stroke-dasharray")) {
1443                     node.removeAttributeNS(null, "stroke-dasharray");
1444                 }
1445             }
1446         },
1447 
1448         // documented in JXG.AbstractRenderer
1449         setGradient: function (el) {
1450             var fillNode = el.rendNode,
1451                 node, node2, node3,
1452                 ev_g = el.evalVisProp('gradient');
1453 
1454             if (ev_g === "linear" || ev_g === 'radial') {
1455                 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient");
1456                 node2 = this.createPrim("stop", el.id + "_gradient1");
1457                 node3 = this.createPrim("stop", el.id + "_gradient2");
1458                 node.appendChild(node2);
1459                 node.appendChild(node3);
1460                 this.defs.appendChild(node);
1461                 fillNode.setAttributeNS(
1462                     null,
1463                     'style',
1464                     // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)"
1465                     'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient')
1466                 );
1467                 el.gradNode1 = node2;
1468                 el.gradNode2 = node3;
1469                 el.gradNode = node;
1470             } else {
1471                 fillNode.removeAttributeNS(null, 'style');
1472             }
1473         },
1474 
1475         // documented in JXG.AbstractRenderer
1476         setLineCap: function (el) {
1477             var capStyle = el.evalVisProp('linecap');
1478 
1479             if (
1480                 capStyle === undefined ||
1481                 capStyle === "" ||
1482                 el.visPropOld.linecap === capStyle ||
1483                 !Type.exists(el.rendNode)
1484             ) {
1485                 return;
1486             }
1487 
1488             this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle);
1489             el.visPropOld.linecap = capStyle;
1490         },
1491 
1492         // documented in JXG.AbstractRenderer
1493         setObjectFillColor: function (el, color, opacity, rendNode) {
1494             var node, c, rgbo, oo,
1495                 rgba = color,
1496                 o = opacity,
1497                 grad = el.evalVisProp('gradient');
1498 
1499             o = o > 0 ? o : 0;
1500 
1501             // TODO  save gradient and gradientangle
1502             if (
1503                 el.visPropOld.fillcolor === rgba &&
1504                 el.visPropOld.fillopacity === o &&
1505                 grad === null
1506             ) {
1507                 return;
1508             }
1509             if (Type.exists(rgba) && rgba !== false) {
1510                 if (rgba.length !== 9) {
1511                     // RGB, not RGBA
1512                     c = rgba;
1513                     oo = o;
1514                 } else {
1515                     // True RGBA, not RGB
1516                     rgbo = Color.rgba2rgbo(rgba);
1517                     c = rgbo[0];
1518                     oo = o * rgbo[1];
1519                 }
1520 
1521                 if (rendNode === undefined) {
1522                     node = el.rendNode;
1523                 } else {
1524                     node = rendNode;
1525                 }
1526 
1527                 if (c !== "none" && c !== "" && c !== false) {
1528                     this._setAttribute(function () {
1529                         node.setAttributeNS(null, "fill", c);
1530                     }, el.visPropOld.fillcolor);
1531                 }
1532 
1533                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
1534                     this._setAttribute(function () {
1535                         node.setAttributeNS(null, "opacity", oo);
1536                     }, el.visPropOld.fillopacity);
1537                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
1538                 } else {
1539                     if (c === 'none') {
1540                         // This is done only for non-images
1541                         // because images have no fill color.
1542                         oo = 0;
1543                         // This is necessary if there is a foreignObject below.
1544                         node.setAttributeNS(null, "pointer-events", 'visibleStroke');
1545                     } else {
1546                         // This is the default
1547                         node.setAttributeNS(null, "pointer-events", 'visiblePainted');
1548                     }
1549                     this._setAttribute(function () {
1550                         node.setAttributeNS(null, 'fill-opacity', oo);
1551                     }, el.visPropOld.fillopacity);
1552                 }
1553 
1554                 if (grad === "linear" || grad === 'radial') {
1555                     this.updateGradient(el);
1556                 }
1557             }
1558             el.visPropOld.fillcolor = rgba;
1559             el.visPropOld.fillopacity = o;
1560         },
1561 
1562         // documented in JXG.AbstractRenderer
1563         setObjectStrokeColor: function (el, color, opacity) {
1564             var rgba = color,
1565                 c, rgbo,
1566                 o = opacity,
1567                 oo, node;
1568 
1569             o = o > 0 ? o : 0;
1570 
1571             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
1572                 return;
1573             }
1574 
1575             if (Type.exists(rgba) && rgba !== false) {
1576                 if (rgba.length !== 9) {
1577                     // RGB, not RGBA
1578                     c = rgba;
1579                     oo = o;
1580                 } else {
1581                     // True RGBA, not RGB
1582                     rgbo = Color.rgba2rgbo(rgba);
1583                     c = rgbo[0];
1584                     oo = o * rgbo[1];
1585                 }
1586 
1587                 node = el.rendNode;
1588 
1589                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
1590                     if (el.evalVisProp('display') === 'html') {
1591                         this._setAttribute(function () {
1592                             node.style.color = c;
1593                             node.style.opacity = oo;
1594                         }, el.visPropOld.strokecolor);
1595                     } else {
1596                         this._setAttribute(function () {
1597                             node.setAttributeNS(null, 'fill', c);
1598                             node.setAttributeNS(null, 'fill-opacity', oo);
1599                         }, el.visPropOld.strokecolor);
1600                     }
1601                 } else {
1602                     this._setAttribute(function () {
1603                         node.setAttributeNS(null, "stroke", c);
1604                         node.setAttributeNS(null, 'stroke-opacity', oo);
1605                     }, el.visPropOld.strokecolor);
1606                 }
1607 
1608                 if (
1609                     el.elementClass === Const.OBJECT_CLASS_CURVE ||
1610                     el.elementClass === Const.OBJECT_CLASS_LINE
1611                 ) {
1612                     if (el.evalVisProp('firstarrow')) {
1613                         this._setArrowColor(
1614                             el.rendNodeTriangleStart,
1615                             c, oo, el,
1616                             el.visPropCalc.typeFirst
1617                         );
1618                     }
1619 
1620                     if (el.evalVisProp('lastarrow')) {
1621                         this._setArrowColor(
1622                             el.rendNodeTriangleEnd,
1623                             c, oo, el,
1624                             el.visPropCalc.typeLast
1625                         );
1626                     }
1627                 }
1628             }
1629 
1630             el.visPropOld.strokecolor = rgba;
1631             el.visPropOld.strokeopacity = o;
1632         },
1633 
1634         // documented in JXG.AbstractRenderer
1635         setObjectStrokeWidth: function (el, width) {
1636             var node,
1637                 w = width;
1638 
1639             if (isNaN(w) || el.visPropOld.strokewidth === w) {
1640                 return;
1641             }
1642 
1643             node = el.rendNode;
1644             this.setPropertyPrim(node, "stroked", 'true');
1645             if (Type.exists(w)) {
1646                 this.setPropertyPrim(node, "stroke-width", w + 'px');
1647 
1648                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
1649                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
1650                 //     if (el.evalVisProp('firstarrow')) {
1651                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
1652                 //     }
1653                 //
1654                 //     if (el.evalVisProp('lastarrow')) {
1655                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
1656                 //     }
1657                 // }
1658             }
1659             el.visPropOld.strokewidth = w;
1660         },
1661 
1662         // documented in JXG.AbstractRenderer
1663         setObjectTransition: function (el, duration) {
1664             var node, props,
1665                 transitionArr = [],
1666                 transitionStr,
1667                 i,
1668                 len = 0,
1669                 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"];
1670 
1671             if (duration === undefined) {
1672                 duration = el.evalVisProp('transitionduration');
1673             }
1674 
1675             props = el.evalVisProp('transitionproperties');
1676             if (duration === el.visPropOld.transitionduration &&
1677                 props === el.visPropOld.transitionproperties) {
1678                 return;
1679             }
1680 
1681             // if (
1682             //     el.elementClass === Const.OBJECT_CLASS_TEXT &&
1683             //     el.evalVisProp('display') === "html"
1684             // ) {
1685             //     // transitionStr = " color " + duration + "ms," +
1686             //     //     " opacity " + duration + 'ms'
1687             //     transitionStr = " all " + duration + "ms ease";
1688             // } else {
1689             //     transitionStr =
1690             //         " fill " + duration + "ms," +
1691             //         " fill-opacity " + duration + "ms," +
1692             //         " stroke " + duration + "ms," +
1693             //         " stroke-opacity " + duration + "ms," +
1694             //         " stroke-width " + duration + "ms," +
1695             //         " width " + duration + "ms," +
1696             //         " height " + duration + "ms," +
1697             //         " rx " + duration + "ms," +
1698             //         " ry " + duration + 'ms'
1699             // }
1700 
1701             if (Type.exists(props)) {
1702                 len = props.length;
1703             }
1704             for (i = 0; i < len; i++) {
1705                 transitionArr.push(props[i] + ' ' + duration + 'ms');
1706             }
1707             transitionStr = transitionArr.join(', ');
1708 
1709             len = nodes.length;
1710             for (i = 0; i < len; ++i) {
1711                 if (el[nodes[i]]) {
1712                     node = el[nodes[i]];
1713                     node.style.transition = transitionStr;
1714                 }
1715             }
1716 
1717             el.visPropOld.transitionduration = duration;
1718             el.visPropOld.transitionproperties = props;
1719         },
1720 
1721         // documented in JXG.AbstractRenderer
1722         setShadow: function (el) {
1723             var ev_s = el.evalVisProp('shadow'),
1724                 ev_s_json, c, b, bl, o, op, id, node,
1725                 use_board_filter = true,
1726                 show = false;
1727 
1728             ev_s_json = JSON.stringify(ev_s);
1729             if (ev_s_json === el.visPropOld.shadow) {
1730                 return;
1731             }
1732 
1733             if (typeof ev_s === 'boolean') {
1734                 use_board_filter = true;
1735                 show = ev_s;
1736                 c = 'none';
1737                 b = 3;
1738                 bl = 0.1;
1739                 o = [5, 5];
1740                 op = 1;
1741             } else {
1742                 if (el.evalVisProp('shadow.enabled')) {
1743                     use_board_filter = false;
1744                     show = true;
1745                     c = JXG.rgbParser(el.evalVisProp('shadow.color'));
1746                     b = el.evalVisProp('shadow.blur');
1747                     bl = el.evalVisProp('shadow.blend');
1748                     o = el.evalVisProp('shadow.offset');
1749                     op = el.evalVisProp('shadow.opacity');
1750                 } else {
1751                     show = false;
1752                 }
1753             }
1754 
1755             if (Type.exists(el.rendNode)) {
1756                 if (show) {
1757                     if (use_board_filter) {
1758                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1'));
1759                         // 'url(#' + this.container.id + '_' + 'f1)');
1760                     } else {
1761                         node = this.container.ownerDocument.getElementById(id);
1762                         if (node) {
1763                             this.defs.removeChild(node);
1764                         }
1765                         id = el.rendNode.id + '_' + 'f1';
1766                         this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o));
1767                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(id));
1768                         // 'url(#' + id + ')');
1769                     }
1770                 } else {
1771                     el.rendNode.removeAttributeNS(null, 'filter');
1772                 }
1773             }
1774 
1775             el.visPropOld.shadow = ev_s_json;
1776         },
1777 
1778         // documented in JXG.AbstractRenderer
1779         setTabindex: function (el) {
1780             var val;
1781             if (el.board.attr.keyboard.enabled && Type.exists(el.rendNode)) {
1782                 val = el.evalVisProp('tabindex');
1783                 if (!el.visPropCalc.visible /* || el.evalVisProp('fixed') */) {
1784                     val = null;
1785                 }
1786                 if (val !== el.visPropOld.tabindex) {
1787                     el.rendNode.setAttribute("tabindex", val);
1788                     el.visPropOld.tabindex = val;
1789                 }
1790             }
1791         },
1792 
1793         // documented in JXG.AbstractRenderer
1794         setPropertyPrim: function (node, key, val) {
1795             if (key === 'stroked') {
1796                 return;
1797             }
1798             node.setAttributeNS(null, key, val);
1799         },
1800 
1801         // documented in JXG.AbstractRenderer
1802         show: function (el) {
1803             JXG.deprecated("Board.renderer.show()", "Board.renderer.display()");
1804             this.display(el, true);
1805             // var node;
1806             //
1807             // if (el && el.rendNode) {
1808             //     node = el.rendNode;
1809             //     node.setAttributeNS(null, 'display', 'inline');
1810             //     node.style.visibility = 'inherit'
1811             // }
1812         },
1813 
1814         // documented in JXG.AbstractRenderer
1815         updateGradient: function (el) {
1816             var col,
1817                 op,
1818                 node2 = el.gradNode1,
1819                 node3 = el.gradNode2,
1820                 ev_g = el.evalVisProp('gradient');
1821 
1822             if (!Type.exists(node2) || !Type.exists(node3)) {
1823                 return;
1824             }
1825 
1826             op = el.evalVisProp('fillopacity');
1827             op = op > 0 ? op : 0;
1828             col = el.evalVisProp('fillcolor');
1829 
1830             node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op);
1831             node3.setAttributeNS(
1832                 null,
1833                 "style",
1834                 "stop-color:" +
1835                 el.evalVisProp('gradientsecondcolor') +
1836                 ";stop-opacity:" +
1837                 el.evalVisProp('gradientsecondopacity')
1838             );
1839             node2.setAttributeNS(
1840                 null,
1841                 "offset",
1842                 el.evalVisProp('gradientstartoffset') * 100 + "%"
1843             );
1844             node3.setAttributeNS(
1845                 null,
1846                 "offset",
1847                 el.evalVisProp('gradientendoffset') * 100 + "%"
1848             );
1849             if (ev_g === 'linear') {
1850                 this.updateGradientAngle(el.gradNode, el.evalVisProp('gradientangle'));
1851             } else if (ev_g === 'radial') {
1852                 this.updateGradientCircle(
1853                     el.gradNode,
1854                     el.evalVisProp('gradientcx'),
1855                     el.evalVisProp('gradientcy'),
1856                     el.evalVisProp('gradientr'),
1857                     el.evalVisProp('gradientfx'),
1858                     el.evalVisProp('gradientfy'),
1859                     el.evalVisProp('gradientfr')
1860                 );
1861             }
1862         },
1863 
1864         /**
1865          * Set the gradient angle for linear color gradients.
1866          *
1867          * @private
1868          * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
1869          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
1870          */
1871         updateGradientAngle: function (node, radians) {
1872             // Angles:
1873             // 0: ->
1874             // 90: down
1875             // 180: <-
1876             // 90: up
1877             var f = 1.0,
1878                 co = Math.cos(radians),
1879                 si = Math.sin(radians);
1880 
1881             if (Math.abs(co) > Math.abs(si)) {
1882                 f /= Math.abs(co);
1883             } else {
1884                 f /= Math.abs(si);
1885             }
1886 
1887             if (co >= 0) {
1888                 node.setAttributeNS(null, "x1", 0);
1889                 node.setAttributeNS(null, "x2", co * f);
1890             } else {
1891                 node.setAttributeNS(null, "x1", -co * f);
1892                 node.setAttributeNS(null, "x2", 0);
1893             }
1894             if (si >= 0) {
1895                 node.setAttributeNS(null, "y1", 0);
1896                 node.setAttributeNS(null, "y2", si * f);
1897             } else {
1898                 node.setAttributeNS(null, "y1", -si * f);
1899                 node.setAttributeNS(null, "y2", 0);
1900             }
1901         },
1902 
1903         /**
1904          * Set circles for radial color gradients.
1905          *
1906          * @private
1907          * @param {SVGnode} node SVG gradient node
1908          * @param {Number} cx SVG value cx (value between 0 and 1)
1909          * @param {Number} cy  SVG value cy (value between 0 and 1)
1910          * @param {Number} r  SVG value r (value between 0 and 1)
1911          * @param {Number} fx  SVG value fx (value between 0 and 1)
1912          * @param {Number} fy  SVG value fy (value between 0 and 1)
1913          * @param {Number} fr  SVG value fr (value between 0 and 1)
1914          */
1915         updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) {
1916             node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color
1917             node.setAttributeNS(null, "cy", cy * 100 + "%");
1918             node.setAttributeNS(null, "r", r * 100 + "%");
1919             node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point
1920             node.setAttributeNS(null, "fy", fy * 100 + "%");
1921             node.setAttributeNS(null, "fr", fr * 100 + "%");
1922         },
1923 
1924         /* ********* Renderer control *********** */
1925 
1926         // documented in JXG.AbstractRenderer
1927         suspendRedraw: function () {
1928             // It seems to be important for the Linux version of firefox
1929             this.suspendHandle = this.svgRoot.suspendRedraw(10000);
1930         },
1931 
1932         // documented in JXG.AbstractRenderer
1933         unsuspendRedraw: function () {
1934             this.svgRoot.unsuspendRedraw(this.suspendHandle);
1935             // this.svgRoot.unsuspendRedrawAll();
1936             //this.svgRoot.forceRedraw();
1937         },
1938 
1939         // documented in AbstractRenderer
1940         resize: function (w, h) {
1941             this.svgRoot.setAttribute("width", parseFloat(w));
1942             this.svgRoot.setAttribute("height", parseFloat(h));
1943             if (Type.exists(this.updateClipPathRect)) {
1944                 // Update clip-path element of the SVG box
1945                 this.updateClipPathRect(w, h);
1946             }
1947         },
1948 
1949         // documented in JXG.AbstractRenderer
1950         createTouchpoints: function (n) {
1951             var i, na1, na2, node;
1952             this.touchpoints = [];
1953             for (i = 0; i < n; i++) {
1954                 na1 = "touchpoint1_" + i;
1955                 node = this.createPrim("path", na1);
1956                 this.appendChildPrim(node, 19);
1957                 node.setAttributeNS(null, "d", "M 0 0");
1958                 this.touchpoints.push(node);
1959 
1960                 this.setPropertyPrim(node, "stroked", 'true');
1961                 this.setPropertyPrim(node, "stroke-width", '1px');
1962                 node.setAttributeNS(null, "stroke", "#000000");
1963                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
1964                 node.setAttributeNS(null, "display", 'none');
1965 
1966                 na2 = "touchpoint2_" + i;
1967                 node = this.createPrim("ellipse", na2);
1968                 this.appendChildPrim(node, 19);
1969                 this.updateEllipsePrim(node, 0, 0, 0, 0);
1970                 this.touchpoints.push(node);
1971 
1972                 this.setPropertyPrim(node, "stroked", 'true');
1973                 this.setPropertyPrim(node, "stroke-width", '1px');
1974                 node.setAttributeNS(null, "stroke", "#000000");
1975                 node.setAttributeNS(null, "fill", "#ffffff");
1976                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
1977                 node.setAttributeNS(null, 'fill-opacity', 0.0);
1978                 node.setAttributeNS(null, "display", 'none');
1979             }
1980         },
1981 
1982         // documented in JXG.AbstractRenderer
1983         showTouchpoint: function (i) {
1984             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1985                 this.touchpoints[2 * i].setAttributeNS(null, "display", 'inline');
1986                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", 'inline');
1987             }
1988         },
1989 
1990         // documented in JXG.AbstractRenderer
1991         hideTouchpoint: function (i) {
1992             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1993                 this.touchpoints[2 * i].setAttributeNS(null, "display", 'none');
1994                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", 'none');
1995             }
1996         },
1997 
1998         // documented in JXG.AbstractRenderer
1999         updateTouchpoint: function (i, pos) {
2000             var x,
2001                 y,
2002                 d = 37;
2003 
2004             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
2005                 x = pos[0];
2006                 y = pos[1];
2007 
2008                 this.touchpoints[2 * i].setAttributeNS(
2009                     null,
2010                     "d",
2011                     "M " +
2012                     (x - d) +
2013                     " " +
2014                     y +
2015                     " " +
2016                     "L " +
2017                     (x + d) +
2018                     " " +
2019                     y +
2020                     " " +
2021                     "M " +
2022                     x +
2023                     " " +
2024                     (y - d) +
2025                     " " +
2026                     "L " +
2027                     x +
2028                     " " +
2029                     (y + d)
2030                 );
2031                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
2032             }
2033         },
2034 
2035         /* ********* Dump related stuff *********** */
2036 
2037         /**
2038          * Walk recursively through the DOM subtree of a node and collect all
2039          * value attributes together with the id of that node.
2040          * <b>Attention:</b> Only values of nodes having a valid id are taken.
2041          * @param  {Node} node   root node of DOM subtree that will be searched recursively.
2042          * @return {Array}      Array with entries of the form [id, value]
2043          * @private
2044          */
2045         _getValuesOfDOMElements: function (node) {
2046             var values = [];
2047             if (node.nodeType === 1) {
2048                 node = node.firstChild;
2049                 while (node) {
2050                     if (node.id !== undefined && node.value !== undefined) {
2051                         values.push([node.id, node.value]);
2052                     }
2053                     Type.concat(values, this._getValuesOfDOMElements(node));
2054                     node = node.nextSibling;
2055                 }
2056             }
2057             return values;
2058         },
2059 
2060         // _getDataUri: function (url, callback) {
2061         //     var image = new Image();
2062         //     image.onload = function () {
2063         //         var canvas = document.createElement('canvas');
2064         //         canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
2065         //         canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
2066         //         canvas.getContext('2d').drawImage(this, 0, 0);
2067         //         callback(canvas.toDataURL("image/png"));
2068         //         canvas.remove();
2069         //     };
2070         //     image.src = url;
2071         // },
2072 
2073         _getImgDataURL: function (svgRoot) {
2074             var images, len, canvas, ctx, ur, i,
2075                 str;
2076 
2077             images = svgRoot.getElementsByTagName('image');
2078             len = images.length;
2079             if (len > 0) {
2080                 canvas = document.createElement('canvas');
2081 
2082                 for (i = 0; i < len; i++) {
2083                     if (images[i].attributes.getNamedItem('href') !== null) {
2084                         str = images[i].attributes.getNamedItem('href').value;
2085                     } else {
2086                         // Deprecated approach
2087                         str = images[i].attributes.getNamedItemNS(this.xlinkNamespace, 'xlink:href').value;
2088                     }
2089 
2090                     // If the image is already a data-URI we are done
2091                     if (str.indexOf('data:image') === 0) {
2092                         continue;
2093                     }
2094 
2095                     images[i].setAttribute("crossorigin", 'anonymous');
2096                     ctx = canvas.getContext('2d');
2097                     canvas.width = images[i].getAttribute('width');
2098                     canvas.height = images[i].getAttribute('height');
2099                     try {
2100                         ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
2101 
2102                         // If the image is not png, the format must be specified here
2103                         ur = canvas.toDataURL();
2104                         images[i].setAttribute('xlink:href', ur); // Deprecated
2105                         images[i].setAttribute('href', ur);
2106                     } catch (err) {
2107                         console.log("CORS problem! Image can not be used", err);
2108                     }
2109                 }
2110                 //canvas.remove();
2111             }
2112             return true;
2113         },
2114 
2115         /**
2116          * Return a data URI of the SVG code representing the construction.
2117          * The SVG code of the construction is base64 encoded. The return string starts
2118          * with "data:image/svg+xml;base64,...".
2119          *
2120          * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
2121          * This is necessary for older versions of Safari. Default: false
2122          * @returns {String}  data URI string
2123          *
2124          * @example
2125          * var A = board.create('point', [2, 2]);
2126          *
2127          * var txt = board.renderer.dumpToDataURI(false);
2128          * // txt consists of a string of the form
2129          * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2130          * // Behind the comma, there is the base64 encoded SVG code
2131          * // which is decoded with atob().
2132          * // The call of decodeURIComponent(escape(...)) is necessary
2133          * // to handle unicode strings correctly.
2134          * var ar = txt.split(',');
2135          * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1])));
2136          *
2137          * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div>
2138          * <textarea id="output2023" rows="5" cols="50"></textarea>
2139          * <script type="text/javascript">
2140          *     (function() {
2141          *         var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d',
2142          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
2143          *     var A = board.create('point', [2, 2]);
2144          *
2145          *     var txt = board.renderer.dumpToDataURI(false);
2146          *     // txt consists of a string of the form
2147          *     // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2148          *     // Behind the comma, there is the base64 encoded SVG code
2149          *     // which is decoded with atob().
2150          *     // The call of decodeURIComponent(escape(...)) is necessary
2151          *     // to handle unicode strings correctly.
2152          *     var ar = txt.split(',');
2153          *     document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1])));
2154          *
2155          *     })();
2156          *
2157          * </script><pre>
2158          *
2159          */
2160         dumpToDataURI: function (ignoreTexts) {
2161             var svgRoot = this.svgRoot,
2162                 btoa = window.btoa || Base64.encode,
2163                 svg, i, len, str,
2164                 values = [];
2165 
2166             // Move all HTML tags (beside the SVG root) of the container
2167             // to the foreignObject element inside of the svgRoot node
2168             // Problem:
2169             // input values are not copied. This can be verified by looking at an innerHTML output
2170             // of an input element. Therefore, we do it "by hand".
2171             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
2172                 if (!ignoreTexts) {
2173                     this.foreignObjLayer.setAttribute("display", 'inline');
2174                 }
2175                 while (svgRoot.nextSibling) {
2176                     // Copy all value attributes
2177                     Type.concat(values, this._getValuesOfDOMElements(svgRoot.nextSibling));
2178                     this.foreignObjLayer.appendChild(svgRoot.nextSibling);
2179                 }
2180             }
2181 
2182             // Dump all image tags
2183             this._getImgDataURL(svgRoot);
2184 
2185             // Convert the SVG graphic into a string containing SVG code
2186             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2187             svg = new XMLSerializer().serializeToString(svgRoot);
2188 
2189             if (ignoreTexts !== true) {
2190                 // Handle SVG texts
2191                 // Insert all value attributes back into the svg string
2192                 len = values.length;
2193                 for (i = 0; i < len; i++) {
2194                     svg = svg.replace(
2195                         'id="' + values[i][0] + '"',
2196                         'id="' + values[i][0] + '" value="' + values[i][1] + '"'
2197                     );
2198                 }
2199             }
2200 
2201             // if (false) {
2202             //     // Debug: use example svg image
2203             //     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>';
2204             // }
2205 
2206             // In IE we have to remove the namespace again.
2207             // Since 2024 we have to check if the namespace attribute appears twice in one tag, because
2208             // there might by a svg inside of the svg, e.g. the screenshot icon.
2209             if (this.isIE &&
2210                 (svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1
2211             ) {
2212                 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g, "");
2213             }
2214 
2215             // Safari fails if the svg string contains a " "
2216             // Obsolete with Safari 12+
2217             svg = svg.replace(/ /g, " ");
2218             // Replacing "s might be necessary for older Safari versions
2219             // svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); // Bug: does not replace matching "s
2220             // svg = svg.replace(/"/g, "");
2221 
2222             // Move all HTML tags back from
2223             // the foreignObject element to the container
2224             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
2225                 // Restore all HTML elements
2226                 while (this.foreignObjLayer.firstChild) {
2227                     this.container.appendChild(this.foreignObjLayer.firstChild);
2228                 }
2229                 this.foreignObjLayer.setAttribute("display", 'none');
2230             }
2231 
2232             // Parameter for btoa(): Replace utf-16 chars by their numerical entity
2233             // In particular, this is necessary for the coyright sign
2234             // From https://stackoverflow.com/questions/23223718/failed-to-execute-btoa-on-window-the-string-to-be-encoded-contains-characte/26603875#26603875
2235 
2236             // str = btoa(svg.replace(/[\u00A0-\u2666]/g, function(c) { return '&#' + c.charCodeAt(0) + ';'; })); // Fails for MathJax-SVG
2237             str = btoa(unescape(encodeURIComponent(svg))); // unescape is deprecated and can handle utf-16 chars only partially
2238             return "data:image/svg+xml;base64," + str;
2239         },
2240 
2241         /**
2242          * Convert the SVG construction into an HTML canvas image.
2243          * This works for all SVG supporting browsers. Implemented as Promise.
2244          * <p>
2245          * Might fail if any text element or foreign object element contains SVG. This
2246          * is the case e.g. for the default fullscreen symbol.
2247          * <p>
2248          * For IE, it is realized as function.
2249          * It works from version 9, with the exception that HTML texts
2250          * are ignored on IE. The drawing is done with a delay of
2251          * 200 ms. Otherwise there would be problems with IE.
2252          *
2253          * @param {String} canvasId Id of an HTML canvas element
2254          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
2255          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
2256          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
2257          * This is necessary for older versions of Safari. Default: false
2258          * @returns {Promise}  Promise object
2259          *
2260          * @example
2261          * 	board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
2262          *
2263          * @example
2264          *  // IE 11 example:
2265          * 	board.renderer.dumpToCanvas('canvas');
2266          * 	setTimeout(function() { console.log('done'); }, 400);
2267          */
2268         dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
2269             var svg, tmpImg,
2270                 cv, ctx,
2271                 doc = this.container.ownerDocument;
2272 
2273             // Prepare the canvas element
2274             cv = doc.getElementById(canvasId);
2275 
2276             // Clear the canvas
2277             /* eslint-disable no-self-assign */
2278             cv.width = cv.width;
2279             /* eslint-enable no-self-assign */
2280 
2281             ctx = cv.getContext('2d');
2282             if (w !== undefined && h !== undefined) {
2283                 cv.style.width = parseFloat(w) + 'px';
2284                 cv.style.height = parseFloat(h) + 'px';
2285                 // Scale twice the CSS size to make the image crisp
2286                 // cv.setAttribute('width', 2 * parseFloat(wOrg));
2287                 // cv.setAttribute('height', 2 * parseFloat(hOrg));
2288                 // ctx.scale(2 * wOrg / w, 2 * hOrg / h);
2289                 cv.setAttribute("width", parseFloat(w));
2290                 cv.setAttribute("height", parseFloat(h));
2291             }
2292 
2293             // Display the SVG string as data-uri in an HTML img.
2294             /**
2295              * @type {Image}
2296              * @ignore
2297              * {ignore}
2298              */
2299             tmpImg = new Image();
2300             svg = this.dumpToDataURI(ignoreTexts);
2301             tmpImg.src = svg;
2302 
2303             // Finally, draw the HTML img in the canvas.
2304             if (!("Promise" in window)) {
2305                 /**
2306                  * @function
2307                  * @ignore
2308                  */
2309                 tmpImg.onload = function () {
2310                     // IE needs a pause...
2311                     // Seems to be broken
2312                     window.setTimeout(function () {
2313                         try {
2314                             ctx.drawImage(tmpImg, 0, 0, w, h);
2315                         } catch (err) {
2316                             console.log("screenshots not longer supported on IE");
2317                         }
2318                     }, 200);
2319                 };
2320                 return this;
2321             }
2322 
2323             return new Promise(function (resolve, reject) {
2324                 try {
2325                     tmpImg.onload = function () {
2326                         ctx.drawImage(tmpImg, 0, 0, w, h);
2327                         resolve();
2328                     };
2329                 } catch (e) {
2330                     reject(e);
2331                 }
2332             });
2333         },
2334 
2335         /**
2336          * Display SVG image in html img-tag which enables
2337          * easy download for the user.
2338          *
2339          * Support:
2340          * <ul>
2341          * <li> IE: No
2342          * <li> Edge: full
2343          * <li> Firefox: full
2344          * <li> Chrome: full
2345          * <li> Safari: full (No text support in versions prior to 12).
2346          * </ul>
2347          *
2348          * @param {JXG.Board} board Link to the board.
2349          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
2350          * the screenshot is copied to this img object. The width and height will be set to the values of the
2351          * JSXGraph container.
2352          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
2353          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
2354          * @return {Object}       the svg renderer object
2355          */
2356         screenshot: function (board, imgId, ignoreTexts) {
2357             var node,
2358                 doc = this.container.ownerDocument,
2359                 parent = this.container.parentNode,
2360                 // cPos,
2361                 // cssTxt,
2362                 canvas, id, img,
2363                 button, buttonText,
2364                 w, h,
2365                 bas = board.attr.screenshot,
2366                 navbar, navbarDisplay, insert,
2367                 newImg = false,
2368                 _copyCanvasToImg,
2369                 isDebug = false;
2370 
2371             if (this.type === 'no') {
2372                 return this;
2373             }
2374 
2375             w = bas.scale * this.container.getBoundingClientRect().width;
2376             h = bas.scale * this.container.getBoundingClientRect().height;
2377 
2378             if (imgId === undefined || imgId === "") {
2379                 newImg = true;
2380                 img = new Image(); //doc.createElement('img');
2381                 img.style.width = w + 'px';
2382                 img.style.height = h + 'px';
2383             } else {
2384                 newImg = false;
2385                 img = doc.getElementById(imgId);
2386             }
2387             // img.crossOrigin = 'anonymous';
2388 
2389             // Create div which contains canvas element and close button
2390             if (newImg) {
2391                 node = doc.createElement('div');
2392                 node.style.cssText = bas.css;
2393                 node.style.width = w + 'px';
2394                 node.style.height = h + 'px';
2395                 node.style.zIndex = this.container.style.zIndex + 120;
2396 
2397                 // Try to position the div exactly over the JSXGraph board
2398                 node.style.position = 'absolute';
2399                 node.style.top = this.container.offsetTop + 'px';
2400                 node.style.left = this.container.offsetLeft + 'px';
2401             }
2402 
2403             if (!isDebug) {
2404                 // Create canvas element and add it to the DOM
2405                 // It will be removed after the image has been stored.
2406                 canvas = doc.createElement('canvas');
2407                 id = Math.random().toString(36).slice(2, 7);
2408                 canvas.setAttribute("id", id);
2409                 canvas.setAttribute("width", w);
2410                 canvas.setAttribute("height", h);
2411                 canvas.style.width = w + 'px';
2412                 canvas.style.height = w + 'px';
2413                 canvas.style.display = 'none';
2414                 parent.appendChild(canvas);
2415             } else {
2416                 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
2417                 id = "jxgbox_canvas";
2418                 canvas = doc.getElementById(id);
2419             }
2420 
2421             if (newImg) {
2422                 // Create close button
2423                 button = doc.createElement('span');
2424                 buttonText = doc.createTextNode("\u2716");
2425                 button.style.cssText = bas.cssButton;
2426                 button.appendChild(buttonText);
2427                 button.onclick = function () {
2428                     node.parentNode.removeChild(node);
2429                 };
2430 
2431                 // Add all nodes
2432                 node.appendChild(img);
2433                 node.appendChild(button);
2434                 parent.insertBefore(node, this.container.nextSibling);
2435             }
2436 
2437             // Hide navigation bar in board
2438             navbar = doc.getElementById(this.uniqName('navigationbar'));
2439             if (Type.exists(navbar)) {
2440                 navbarDisplay = navbar.style.display;
2441                 navbar.style.display = 'none';
2442                 insert = this.removeToInsertLater(navbar);
2443             }
2444 
2445             _copyCanvasToImg = function () {
2446                 // Show image in img tag
2447                 img.src = canvas.toDataURL("image/png");
2448 
2449                 // Remove canvas node
2450                 if (!isDebug) {
2451                     parent.removeChild(canvas);
2452                 }
2453             };
2454 
2455             // Create screenshot in image element
2456             if ("Promise" in window) {
2457                 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
2458             } else {
2459                 // IE
2460                 this.dumpToCanvas(id, w, h, ignoreTexts);
2461                 window.setTimeout(_copyCanvasToImg, 200);
2462             }
2463 
2464             // Reinsert navigation bar in board
2465             if (Type.exists(navbar)) {
2466                 navbar.style.display = navbarDisplay;
2467                 insert();
2468             }
2469 
2470             return this;
2471         }
2472     }
2473 );
2474 
2475 export default JXG.SVGRenderer;
2476