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