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