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