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