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