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