      1 /*
      2     Copyright 2008-2022
      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 <http://www.gnu.org/licenses/>
     29     and <http://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 /* depends:
     36  jxg
     37  options
     38  renderer/abstract
     39  base/constants
     40  utils/type
     41  utils/env
     42  utils/color
     43  math/numerics
     44 */
     45
     46 define([
     47     'jxg', 'options', 'renderer/abstract', 'base/constants', 'utils/type', 'utils/color', 'utils/base64', 'math/numerics'
     48 ], function (JXG, Options, AbstractRenderer, Const, Type, Color, Base64, Numerics) {
     49
     50     "use strict";
     51
     52     /**
     53      * Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
     54      * @class JXG.SVGRenderer
     55      * @augments JXG.AbstractRenderer
     56      * @param {Node} container Reference to a DOM node containing the board.
     57      * @param {Object} dim The dimensions of the board
     58      * @param {Number} dim.width
     59      * @param {Number} dim.height
     60      * @see JXG.AbstractRenderer
     61      */
     62     JXG.SVGRenderer = function (container, dim) {
     63         var i;
     64
     65         // docstring in AbstractRenderer
     66         this.type = 'svg';
     67
     68         this.isIE = navigator.appVersion.indexOf("MSIE") !== -1 || navigator.userAgent.match(/Trident\//);
     69
     70         /**
     71          * SVG root node
     72          * @type Node
     73          */
     74         this.svgRoot = null;
     75
     76         /**
     77          * The SVG Namespace used in JSXGraph.
     78          * @see http://www.w3.org/TR/SVG/
     79          * @type String
     80          * @default http://www.w3.org/2000/svg
     81          */
     82         this.svgNamespace = 'http://www.w3.org/2000/svg';
     83
     84         /**
     85          * The xlink namespace. This is used for images.
     86          * @see http://www.w3.org/TR/xlink/
     87          * @type String
     88          * @default http://www.w3.org/1999/xlink
     89          */
     90         this.xlinkNamespace = 'http://www.w3.org/1999/xlink';
     91
     92         // container is documented in AbstractRenderer
     93         this.container = container;
     94
     95         // prepare the div container and the svg root node for use with JSXGraph
     96         this.container.style.MozUserSelect = 'none';
     97         this.container.style.userSelect = 'none';
     98
     99         this.container.style.overflow = 'hidden';
    100         if (this.container.style.position === '') {
    101             this.container.style.position = 'relative';
    102         }
    103
    104         this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, "svg");
    105         this.svgRoot.style.overflow = 'hidden';
    106         this.svgRoot.style.display = 'block';
    107
    108         this.resize(dim.width, dim.height);
    109
    110         //this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision');
    111
    112         this.container.appendChild(this.svgRoot);
    113
    114         /**
    115          * The <tt>defs</tt> element is a container element to reference reusable SVG elements.
    116          * @type Node
    117          * @see http://www.w3.org/TR/SVG/struct.html#DefsElement
    118          */
    119         this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, 'defs');
    120         this.svgRoot.appendChild(this.defs);
    121
    122         /**
    123          * Filters are used to apply shadows.
    124          * @type Node
    125          * @see http://www.w3.org/TR/SVG/filters.html#FilterElement
    126          */
    127         this.filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter');
    128         this.filter.setAttributeNS(null, 'id', this.container.id + '_' + 'f1');
    129         /*
    130         this.filter.setAttributeNS(null, 'x', '-100%');
    131         this.filter.setAttributeNS(null, 'y', '-100%');
    132         this.filter.setAttributeNS(null, 'width', '400%');
    133         this.filter.setAttributeNS(null, 'height', '400%');
    134         //this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
    135         */
    136         this.filter.setAttributeNS(null, 'width', '300%');
    137         this.filter.setAttributeNS(null, 'height', '300%');
    138         this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
    139
    140         this.feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
    141         this.feOffset.setAttributeNS(null, 'result', 'offOut');
    142         this.feOffset.setAttributeNS(null, 'in', 'SourceAlpha');
    143         this.feOffset.setAttributeNS(null, 'dx', '5');
    144         this.feOffset.setAttributeNS(null, 'dy', '5');
    145         this.filter.appendChild(this.feOffset);
    146
    147         this.feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
    148         this.feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
    149         this.feGaussianBlur.setAttributeNS(null, 'in', 'offOut');
    150         this.feGaussianBlur.setAttributeNS(null, 'stdDeviation', '3');
    151         this.filter.appendChild(this.feGaussianBlur);
    152
    153         this.feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
    154         this.feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
    155         this.feBlend.setAttributeNS(null, 'in2', 'blurOut');
    156         this.feBlend.setAttributeNS(null, 'mode', 'normal');
    157         this.filter.appendChild(this.feBlend);
    158
    159         this.defs.appendChild(this.filter);
    160
    161         /**
    162          * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
    163          * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
    164          * there, too. The higher the number, the "more on top" are the elements on this layer.
    165          * @type Array
    166          */
    167         this.layer = [];
    168         for (i = 0; i < Options.layer.numlayers; i++) {
    169             this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
    170             this.svgRoot.appendChild(this.layer[i]);
    171         }
    172
    173         // Already documented in JXG.AbstractRenderer
    174         this.supportsForeignObject = document.implementation.hasFeature("http://w3.org/TR/SVG11/feature#Extensibility", "1.1");
    175
    176         if (this.supportsForeignObject) {
    177             this.foreignObjLayer = this.container.ownerDocument.createElementNS(this.svgNamespace, 'foreignObject');
    178             this.foreignObjLayer.setAttribute("display", "none");
    179             this.foreignObjLayer.setAttribute("x", 0);
    180             this.foreignObjLayer.setAttribute("y", 0);
    181             this.foreignObjLayer.setAttribute("width", "100%");
    182             this.foreignObjLayer.setAttribute("height", "100%");
    183             this.foreignObjLayer.setAttribute('id', this.container.id + '_foreignObj');
    184             this.svgRoot.appendChild(this.foreignObjLayer);
    185         }
    186
    187         /**
    188          * Defines dash patterns. Defined styles are: <ol>
    189          * <li value="-1"> 2px dash, 2px space</li>
    190          * <li> 5px dash, 5px space</li>
    191          * <li> 10px dash, 10px space</li>
    192          * <li> 20px dash, 20px space</li>
    193          * <li> 20px dash, 10px space, 10px dash, 10px dash</li>
    194          * <li> 20px dash, 5px space, 10px dash, 5px space</li></ol>
    195          * @type Array
    196          * @default ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5']
    197          * @see http://www.w3.org/TR/SVG/painting.html#StrokeProperties
    198          */
    199         this.dashArray = ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5'];
    200     };
    201
    202     JXG.SVGRenderer.prototype = new AbstractRenderer();
    203
    204     JXG.extend(JXG.SVGRenderer.prototype, /** @lends JXG.SVGRenderer.prototype */ {
    205
    206         /**
    207          * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
    208          * @private
    209          * @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached.
    210          * @param {String} [idAppendix=''] A string that is added to the node's id.
    211          * @returns {Node} Reference to the node added to the DOM.
    212          */
    213         _createArrowHead: function (el, idAppendix, type) {
    214             var node2, node3,
    215                 id = el.id + 'Triangle',
    216                 //type = null,
    217                 v, h;
    218
    219             if (Type.exists(idAppendix)) {
    220                 id += idAppendix;
    221             }
    222             node2 = this.createPrim('marker', id);
    223
    224             node2.setAttributeNS(null, 'stroke', Type.evaluate(el.visProp.strokecolor));
    225             node2.setAttributeNS(null, 'stroke-opacity', Type.evaluate(el.visProp.strokeopacity));
    226             node2.setAttributeNS(null, 'fill', Type.evaluate(el.visProp.strokecolor));
    227             node2.setAttributeNS(null, 'fill-opacity', Type.evaluate(el.visProp.strokeopacity));
    228             node2.setAttributeNS(null, 'stroke-width', 0);  // this is the stroke-width of the arrow head.
    229             // Should be zero to simplify the calculations
    230
    231             node2.setAttributeNS(null, 'orient', 'auto');
    232             node2.setAttributeNS(null, 'markerUnits', 'strokeWidth'); // 'strokeWidth' 'userSpaceOnUse');
    233
    234             /*
    235                Types 1, 2:
    236                The arrow head is an isosceles triangle with base length 10 and height 10.
    237
    238                Type 3:
    239                A rectangle
    240
    241                Types 4, 5, 6:
    242                Defined by Bezier curves from mp_arrowheads.html
    243
    244                In any case but type 3 the arrow head is 10 units long,
    245                type 3 is 10 unitsb high.
    246                These 10 units are scaled to strokeWidth * arrowSize pixels, see
    247                this._setArrowWidth().
    248
    249                See also abstractRenderer.updateLine() where the line path is shortened accordingly.
    250
    251                Changes here are also necessary in setArrowWidth().
    252
    253                So far, lines with arrow heads are shortenend to avoid overlapping of
    254                arrow head and line. This is not the case for curves, yet.
    255                Therefore, the offset refX has to be adapted to the path type.
    256             */
    257             node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'path');
    258             h = 5;
    259             if (idAppendix === 'End') {
    260                 // First arrow
    261                 //type = a.typeFirst;
    262                 // if (JXG.exists(ev_fa.type)) {
    263                 //     type = Type.evaluate(ev_fa.type);
    264                 // }
    265
    266                 v = 0;
    267                 if (type === 2) {
    268                     node3.setAttributeNS(null, 'd', 'M 10,0 L 0,5 L 10,10 L 5,5 z');
    269                 } else if (type === 3) {
    270                     node3.setAttributeNS(null, 'd', 'M 0,0 L 3.33,0 L 3.33,10 L 0,10 z');
    271                 } else if (type === 4) {
    272                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
    273                     h = 3.31;
    274                     node3.setAttributeNS(null, 'd', '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');
    275                 } else if (type === 5) {
    276                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
    277                     h = 3.28;
    278                     node3.setAttributeNS(null, 'd', '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');
    279                 } else if (type === 6) {
    280                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
    281                     h = 2.84;
    282                     node3.setAttributeNS(null, 'd', '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');
    283                 } else if (type === 7) {
    284                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
    285                     h = 5.20;
    286                     node3.setAttributeNS(null, 'd', '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');
    287                 } else {
    288                     // type == 1 or > 6
    289                     node3.setAttributeNS(null, 'd', 'M 10,0 L 0,5 L 10,10 z');
    290                 }
    291                 if (/*!Type.exists(el.rendNode.getTotalLength) && */el.elementClass === Const.OBJECT_CLASS_LINE) {
    292                     if (type === 2) {
    293                         v = 4.9;
    294                     } else if (type === 3) {
    295                         v = 3.3;
    296                     } else if (type === 4 || type === 5 || type === 6) {
    297                         v = 6.66;
    298                     } else if (type === 7) {
    299                         v = 0.0;
    300                     } else {
    301                         v = 10.0;
    302                     }
    303                 }
    304             } else {
    305                 // Last arrow
    306                 // if (JXG.exists(ev_la.type)) {
    307                 //     type = Type.evaluate(ev_la.type);
    308                 // }
    309                 //type = a.typeLast;
    310
    311                 v = 10.0;
    312                 if (type === 2) {
    313                     node3.setAttributeNS(null, 'd', 'M 0,0 L 10,5 L 0,10 L 5,5 z');
    314                 } else if (type === 3) {
    315                     v = 3.3;
    316                     node3.setAttributeNS(null, 'd', 'M 0,0 L 3.33,0 L 3.33,10 L 0,10 z');
    317                 } else if (type === 4) {
    318                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
    319                     h = 3.31;
    320                     node3.setAttributeNS(null, 'd', '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');
    321                 } else if (type === 5) {
    322                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
    323                     h = 3.28;
    324                     node3.setAttributeNS(null, 'd', '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');
    325                 } else if (type === 6) {
    326                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
    327                     h = 2.84;
    328                     node3.setAttributeNS(null, 'd', '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');
    329                 } else if (type === 7) {
    330                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
    331                     h = 5.20;
    332                     node3.setAttributeNS(null, 'd', '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');
    333                 } else {
    334                     // type == 1 or > 6
    335                     node3.setAttributeNS(null, 'd', 'M 0,0 L 10,5 L 0,10 z');
    336                 }
    337                 if (/*!Type.exists(el.rendNode.getTotalLength) &&*/ el.elementClass === Const.OBJECT_CLASS_LINE) {
    338                     if (type === 2) {
    339                         v = 5.1;
    340                     } else if (type === 3) {
    341                         v = 0.02;
    342                     } else if (type === 4 || type === 5 || type === 6) {
    343                         v = 3.33;
    344                     } else if (type === 7) {
    345                         v = 10.0;
    346                     } else {
    347                         v = 0.05;
    348                     }
    349                 }
    350             }
    351             if (type === 7) {
    352                 node2.setAttributeNS(null, 'fill', 'none');
    353                 node2.setAttributeNS(null, 'stroke-width', 1);  // this is the stroke-width of the arrow head.
    354             }
    355             node2.setAttributeNS(null, 'refY', h);
    356             node2.setAttributeNS(null, 'refX', v);
    357
    358             node2.appendChild(node3);
    359             return node2;
    360         },
    361
    362         /**
    363          * Updates color of an arrow DOM node.
    364          * @param {Node} node The arrow node.
    365          * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green.
    366          * @param {Number} opacity
    367          * @param {JXG.GeometryElement} el The element the arrows are to be attached to
    368          */
    369         _setArrowColor: function (node, color, opacity, el, type) {
    370             if (node) {
    371                 if (Type.isString(color)) {
    372                     if (type !== 7) {
    373                         this._setAttribute(function () {
    374                             node.setAttributeNS(null, 'stroke', color);
    375                             node.setAttributeNS(null, 'fill', color);
    376                             node.setAttributeNS(null, 'stroke-opacity', opacity);
    377                             node.setAttributeNS(null, 'fill-opacity', opacity);
    378                         }, el.visPropOld.fillcolor);
    379                     } else {
    380                         this._setAttribute(function () {
    381                             node.setAttributeNS(null, 'fill', 'none');
    382                             node.setAttributeNS(null, 'stroke', color);
    383                             node.setAttributeNS(null, 'stroke-opacity', opacity);
    384                         }, el.visPropOld.fillcolor);
    385                     }
    386                 }
    387
    388                 if (this.isIE) {
    389                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
    390                 }
    391             }
    392
    393         },
    394
    395         // Already documented in JXG.AbstractRenderer
    396         _setArrowWidth: function (node, width, parentNode, size) {
    397             var s, d;
    398
    399             if (node) {
    400                 // if (width === 0) {
    401                 //     // display:none does not work well in webkit
    402                 //     node.setAttributeNS(null, 'display', 'none');
    403                 // } else {
    404                     s = width;
    405                     d = s * size;
    406                     node.setAttributeNS(null, 'viewBox', (0) + ' ' + (0) + ' ' + (s * 10) + ' ' + (s * 10));
    407                     node.setAttributeNS(null, 'markerHeight', d);
    408                     node.setAttributeNS(null, 'markerWidth', d);
    409                     node.setAttributeNS(null, 'display', 'inherit');
    410                 // }
    411
    412                 if (this.isIE) {
    413                     parentNode.parentNode.insertBefore(parentNode, parentNode);
    414                 }
    415             }
    416         },
    417
    418         /* ******************************** *
    419          *  This renderer does not need to
    420          *  override draw/update* methods
    421          *  since it provides draw/update*Prim
    422          *  methods except for some cases like
    423          *  internal texts or images.
    424          * ******************************** */
    425
    426         /* **************************
    427          *    Lines
    428          * **************************/
    429
    430         // documented in AbstractRenderer
    431         updateTicks: function (ticks) {
    432             var i, j, c, node, x, y,
    433                 tickStr = '',
    434                 len = ticks.ticks.length,
    435                 len2, str,
    436                 isReal = true;
    437
    438             for (i = 0; i < len; i++) {
    439                 c = ticks.ticks[i];
    440                 x = c[0];
    441                 y = c[1];
    442
    443                 len2 = x.length;
    444                 str = ' M ' + x[0] + ' ' + y[0];
    445                 if (!Type.isNumber(x[0])) {
    446                     isReal = false;
    447                 }
    448                 for (j = 1; isReal && j < len2; ++j) {
    449                     if (Type.isNumber(x[j])) {
    450                         str += ' L ' + x[j] + ' ' + y[j];
    451                     } else {
    452                         isReal = false;
    453                     }
    454
    455                 }
    456                 if (isReal) {
    457                     tickStr += str;
    458                 }
    459             }
    460
    461             node = ticks.rendNode;
    462
    463             if (!Type.exists(node)) {
    464                 node = this.createPrim('path', ticks.id);
    465                 this.appendChildPrim(node, Type.evaluate(ticks.visProp.layer));
    466                 ticks.rendNode = node;
    467             }
    468
    469             node.setAttributeNS(null, 'stroke', Type.evaluate(ticks.visProp.strokecolor));
    470             node.setAttributeNS(null, 'fill', 'none');
    471             // node.setAttributeNS(null, 'fill', Type.evaluate(ticks.visProp.fillcolor));
    472             // node.setAttributeNS(null, 'fill-opacity', Type.evaluate(ticks.visProp.fillopacity));
    473             node.setAttributeNS(null, 'stroke-opacity', Type.evaluate(ticks.visProp.strokeopacity));
    474             node.setAttributeNS(null, 'stroke-width', Type.evaluate(ticks.visProp.strokewidth));
    475             this.updatePathPrim(node, tickStr, ticks.board);
    476         },
    477
    478         /* **************************
    479          *    Text related stuff
    480          * **************************/
    481
    482         // Already documented in JXG.AbstractRenderer
    483         displayCopyright: function (str, fontsize) {
    484             var node = this.createPrim('text', 'licenseText'),
    485                 t;
    486             node.setAttributeNS(null, 'x', '20px');
    487             node.setAttributeNS(null, 'y', (2 + fontsize) + 'px');
    488             node.setAttributeNS(null, "style", "font-family:Arial,Helvetica,sans-serif; font-size:" + fontsize + "px; fill:#356AA0;  opacity:0.3;");
    489             t = this.container.ownerDocument.createTextNode(str);
    490             node.appendChild(t);
    491             this.appendChildPrim(node, 0);
    492         },
    493
    494         // Already documented in JXG.AbstractRenderer
    495         drawInternalText: function (el) {
    496             var node = this.createPrim('text', el.id);
    497
    498             //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
    499             // Preserve spaces
    500             //node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve");
    501             node.style.whiteSpace = 'nowrap';
    502
    503             el.rendNodeText = this.container.ownerDocument.createTextNode('');
    504             node.appendChild(el.rendNodeText);
    505             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
    506
    507             return node;
    508         },
    509
    510         // Already documented in JXG.AbstractRenderer
    511         updateInternalText: function (el) {
    512             var content = el.plaintext, v,
    513                 ev_ax = el.getAnchorX(),
    514                 ev_ay = el.getAnchorY();
    515
    516             if (el.rendNode.getAttributeNS(null, "class") !== el.visProp.cssclass) {
    517                 el.rendNode.setAttributeNS(null, "class", Type.evaluate(el.visProp.cssclass));
    518                 el.needsSizeUpdate = true;
    519             }
    520
    521             if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
    522                 // Horizontal
    523                 v = el.coords.scrCoords[1];
    524                 if (el.visPropOld.left !== (ev_ax + v)) {
    525                     el.rendNode.setAttributeNS(null, 'x', v + 'px');
    526
    527                     if (ev_ax === 'left') {
    528                         el.rendNode.setAttributeNS(null, 'text-anchor', 'start');
    529                     } else if (ev_ax === 'right') {
    530                         el.rendNode.setAttributeNS(null, 'text-anchor', 'end');
    531                     } else if (ev_ax === 'middle') {
    532                         el.rendNode.setAttributeNS(null, 'text-anchor', 'middle');
    533                     }
    534                     el.visPropOld.left = ev_ax + v;
    535                 }
    536
    537                 // Vertical
    538                 v = el.coords.scrCoords[2];
    539                 if (el.visPropOld.top !== (ev_ay + v)) {
    540                     el.rendNode.setAttributeNS(null, 'y', (v + this.vOffsetText * 0.5) + 'px');
    541
    542                     if (ev_ay === 'bottom') {
    543                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-after-edge');
    544                     } else if (ev_ay === 'top') {
    545                         el.rendNode.setAttributeNS(null, 'dy', '1.6ex');
    546                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge'); // Not supported by IE, edge
    547                     } else if (ev_ay === 'middle') {
    548                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
    549                         el.rendNode.setAttributeNS(null, 'dy', '0.6ex');
    550                     }
    551                     el.visPropOld.top = ev_ay + v;
    552                 }
    553             }
    554             if (el.htmlStr !== content) {
    555                 el.rendNodeText.data = content;
    556                 el.htmlStr = content;
    557             }
    558             this.transformImage(el, el.transformations);
    559         },
    560
    561         /**
    562          * Set color and opacity of internal texts.
    563          * SVG needs its own version.
    564          * @private
    565          * @see JXG.AbstractRenderer#updateTextStyle
    566          * @see JXG.AbstractRenderer#updateInternalTextStyle
    567          */
    568         updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) {
    569             this.setObjectFillColor(el, strokeColor, strokeOpacity);
    570         },
    571
    572         /* **************************
    573          *    Image related stuff
    574          * **************************/
    575
    576         // Already documented in JXG.AbstractRenderer
    577         drawImage: function (el) {
    578             var node = this.createPrim('image', el.id);
    579
    580             node.setAttributeNS(null, 'preserveAspectRatio', 'none');
    581             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
    582             el.rendNode = node;
    583
    584             this.updateImage(el);
    585         },
    586
    587         // Already documented in JXG.AbstractRenderer
    588         transformImage: function (el, t) {
    589             var s, m,
    590                 node = el.rendNode,
    591                 str = "",
    592                 len = t.length;
    593
    594             if (len > 0) {
    595                 m = this.joinTransforms(el, t);
    596                 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(',');
    597                 str += ' matrix(' + s + ') ';
    598                 node.setAttributeNS(null, 'transform', str);
    599             }
    600         },
    601
    602         // Already documented in JXG.AbstractRenderer
    603         updateImageURL: function (el) {
    604             var url = Type.evaluate(el.url);
    605
    606             if (el._src !== url) {
    607                 el.imgIsLoaded = false;
    608                 el.rendNode.setAttributeNS(this.xlinkNamespace, 'xlink:href', url);
    609                 el._src = url;
    610
    611                 return true;
    612             }
    613
    614             return false;
    615         },
    616
    617         // Already documented in JXG.AbstractRenderer
    618         updateImageStyle: function (el, doHighlight) {
    619             var css = Type.evaluate(doHighlight ? el.visProp.highlightcssclass : el.visProp.cssclass);
    620
    621             el.rendNode.setAttributeNS(null, 'class', css);
    622         },
    623
    624         // Already documented in JXG.AbstractRenderer
    625         drawForeignObject: function (el) {
    626             el.rendNode = this.appendChildPrim(this.createPrim('foreignObject', el.id),
    627                                     Type.evaluate(el.visProp.layer));
    628
    629             this.appendNodesToElement(el, 'foreignObject');
    630             this.updateForeignObject(el);
    631         },
    632
    633         // Already documented in JXG.AbstractRenderer
    634         updateForeignObject: function(el) {
    635             if (el._useUserSize) {
    636                 el.rendNode.style.overflow = 'hidden';
    637             } else {
    638                 el.rendNode.style.overflow = 'visible';
    639             }
    640
    641             this.updateRectPrim(el.rendNode, el.coords.scrCoords[1],
    642                 el.coords.scrCoords[2] - el.size[1], el.size[0], el.size[1]);
    643
    644             el.rendNode.innerHTML = el.content;
    645             this._updateVisual(el, {stroke: true, dash: true}, true);
    646         },
    647
    648         /* **************************
    649          * Render primitive objects
    650          * **************************/
    651
    652         // Already documented in JXG.AbstractRenderer
    653         appendChildPrim: function (node, level) {
    654             if (!Type.exists(level)) { // trace nodes have level not set
    655                 level = 0;
    656             } else if (level >= Options.layer.numlayers) {
    657                 level = Options.layer.numlayers - 1;
    658             }
    659
    660             this.layer[level].appendChild(node);
    661
    662             return node;
    663         },
    664
    665         // Already documented in JXG.AbstractRenderer
    666         createPrim: function (type, id) {
    667             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
    668             node.setAttributeNS(null, 'id', this.container.id + '_' + id);
    669             node.style.position = 'absolute';
    670             if (type === 'path') {
    671                 node.setAttributeNS(null, 'stroke-linecap', 'round');
    672                 node.setAttributeNS(null, 'stroke-linejoin', 'round');
    673                 node.setAttributeNS(null, 'fill-rule', 'evenodd');
    674             }
    675             return node;
    676         },
    677
    678         // Already documented in JXG.AbstractRenderer
    679         remove: function (shape) {
    680             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
    681                 shape.parentNode.removeChild(shape);
    682             }
    683         },
    684
    685         // Already documented in JXG.AbstractRenderer
    686         setLayer: function (el, level) {
    687             if (!Type.exists(level)) {
    688                 level = 0;
    689             } else if (level >= Options.layer.numlayers) {
    690                 level = Options.layer.numlayers - 1;
    691             }
    692
    693             this.layer[level].appendChild(el.rendNode);
    694         },
    695
    696         // Already documented in JXG.AbstractRenderer
    697         makeArrows: function (el, a) {
    698             var node2,
    699                 ev_fa = a.evFirst,
    700                 ev_la = a.evLast;
    701
    702             // Test if the arrow heads already exist
    703             if (el.visPropOld.firstarrow === ev_fa &&
    704                 el.visPropOld.lastarrow === ev_la) {
    705                 if (this.isIE && el.visPropCalc.visible &&
    706                     (ev_fa || ev_la)) {
    707                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
    708                 }
    709                 return;
    710             }
    711
    712             if (ev_fa) {
    713                 node2 = el.rendNodeTriangleStart;
    714                 if (!Type.exists(node2)) {
    715                     node2 = this._createArrowHead(el, 'End', a.typeFirst);
    716                     this.defs.appendChild(node2);
    717                     el.rendNodeTriangleStart = node2;
    718                     el.rendNode.setAttributeNS(null, 'marker-start', 'url(#' + this.container.id + '_' + el.id + 'TriangleEnd)');
    719                 } else {
    720                     this.defs.appendChild(node2);
    721                 }
    722             } else {
    723                 node2 = el.rendNodeTriangleStart;
    724                 if (Type.exists(node2)) {
    725                     this.remove(node2);
    726                 }
    727             }
    728             if (ev_la) {
    729                 node2 = el.rendNodeTriangleEnd;
    730                 if (!Type.exists(node2)) {
    731                     node2 = this._createArrowHead(el, 'Start', a.typeLast);
    732                     this.defs.appendChild(node2);
    733                     el.rendNodeTriangleEnd = node2;
    734                     el.rendNode.setAttributeNS(null, 'marker-end', 'url(#' + this.container.id + '_' + el.id + 'TriangleStart)');
    735                 } else {
    736                     this.defs.appendChild(node2);
    737                 }
    738             } else {
    739                 node2 = el.rendNodeTriangleEnd;
    740                 if (Type.exists(node2)) {
    741                     this.remove(node2);
    742                 }
    743             }
    744             el.visPropOld.firstarrow = ev_fa;
    745             el.visPropOld.lastarrow = ev_la;
    746         },
    747
    748         // Already documented in JXG.AbstractRenderer
    749         updateEllipsePrim: function (node, x, y, rx, ry) {
    750             var huge = 1000000;
    751
    752             huge = 200000; // IE
    753             // webkit does not like huge values if the object is dashed
    754             // iE doesn't like huge values above 216000
    755             x = Math.abs(x) < huge ? x : huge * x / Math.abs(x);
    756             y = Math.abs(y) < huge ? y : huge * y / Math.abs(y);
    757             rx = Math.abs(rx) < huge ? rx : huge * rx / Math.abs(rx);
    758             ry = Math.abs(ry) < huge ? ry : huge * ry / Math.abs(ry);
    759
    760             node.setAttributeNS(null, 'cx', x);
    761             node.setAttributeNS(null, 'cy', y);
    762             node.setAttributeNS(null, 'rx', Math.abs(rx));
    763             node.setAttributeNS(null, 'ry', Math.abs(ry));
    764         },
    765
    766         // Already documented in JXG.AbstractRenderer
    767         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
    768             var huge = 1000000;
    769
    770             huge = 200000; //IE
    771             if (!isNaN(p1x + p1y + p2x + p2y)) {
    772                 // webkit does not like huge values if the object is dashed
    773                 // IE doesn't like huge values above 216000
    774                 p1x = Math.abs(p1x) < huge ? p1x : huge * p1x / Math.abs(p1x);
    775                 p1y = Math.abs(p1y) < huge ? p1y : huge * p1y / Math.abs(p1y);
    776                 p2x = Math.abs(p2x) < huge ? p2x : huge * p2x / Math.abs(p2x);
    777                 p2y = Math.abs(p2y) < huge ? p2y : huge * p2y / Math.abs(p2y);
    778
    779                 node.setAttributeNS(null, 'x1', p1x);
    780                 node.setAttributeNS(null, 'y1', p1y);
    781                 node.setAttributeNS(null, 'x2', p2x);
    782                 node.setAttributeNS(null, 'y2', p2y);
    783             }
    784         },
    785
    786         // Already documented in JXG.AbstractRenderer
    787         updatePathPrim: function (node, pointString) {
    788             if (pointString === '') {
    789                 pointString = 'M 0 0';
    790             }
    791             node.setAttributeNS(null, 'd', pointString);
    792         },
    793
    794         // Already documented in JXG.AbstractRenderer
    795         updatePathStringPoint: function (el, size, type) {
    796             var s = '',
    797                 scr = el.coords.scrCoords,
    798                 sqrt32 = size * Math.sqrt(3) * 0.5,
    799                 s05 = size * 0.5;
    800
    801             if (type === 'x') {
    802                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2] - size) +
    803                     ' L ' + (scr[1] + size) + ' ' + (scr[2] + size) +
    804                     ' M ' + (scr[1] + size) + ' ' + (scr[2] - size) +
    805                     ' L ' + (scr[1] - size) + ' ' + (scr[2] + size);
    806             } else if (type === '+') {
    807                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
    808                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
    809                     ' M ' + (scr[1]) + ' ' + (scr[2] - size) +
    810                     ' L ' + (scr[1]) + ' ' + (scr[2] + size);
    811             } else if (type === '<>') {
    812                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
    813                     ' L ' + (scr[1]) + ' ' + (scr[2] + size) +
    814                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
    815                     ' L ' + (scr[1]) + ' ' + (scr[2] - size) + ' Z ';
    816             } else if (type === '^') {
    817                 s = ' M ' + (scr[1]) + ' ' + (scr[2] - size) +
    818                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] + s05) +
    819                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] + s05) +
    820                     ' Z ';  // close path
    821             } else if (type === 'v') {
    822                 s = ' M ' + (scr[1]) + ' ' + (scr[2] + size) +
    823                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] - s05) +
    824                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] - s05) +
    825                     ' Z ';
    826             } else if (type === '>') {
    827                 s = ' M ' + (scr[1] + size) + ' ' + (scr[2]) +
    828                     ' L ' + (scr[1] - s05) + ' ' + (scr[2] - sqrt32) +
    829                     ' L ' + (scr[1] - s05) + ' ' + (scr[2] + sqrt32) +
    830                     ' Z ';
    831             } else if (type === '<') {
    832                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
    833                     ' L ' + (scr[1] + s05) + ' ' + (scr[2] - sqrt32) +
    834                     ' L ' + (scr[1] + s05) + ' ' + (scr[2] + sqrt32) +
    835                     ' Z ';
    836             }
    837             return s;
    838         },
    839
    840         // Already documented in JXG.AbstractRenderer
    841         updatePathStringPrim: function (el) {
    842             var i, scr, len,
    843                 symbm = ' M ',
    844                 symbl = ' L ',
    845                 symbc = ' C ',
    846                 nextSymb = symbm,
    847                 maxSize = 5000.0,
    848                 pStr = '';
    849
    850             if (el.numberPoints <= 0) {
    851                 return '';
    852             }
    853
    854             len = Math.min(el.points.length, el.numberPoints);
    855
    856             if (el.bezierDegree === 1) {
    857                 for (i = 0; i < len; i++) {
    858                     scr = el.points[i].scrCoords;
    859                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
    860                         nextSymb = symbm;
    861                     } else {
    862                         // Chrome has problems with values being too far away.
    863                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
    864                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
    865
    866                         // Attention: first coordinate may be inaccurate if far way
    867                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
    868                         pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
    869                         nextSymb = symbl;
    870                     }
    871                 }
    872             } else if (el.bezierDegree === 3) {
    873                 i = 0;
    874                 while (i < len) {
    875                     scr = el.points[i].scrCoords;
    876                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
    877                         nextSymb = symbm;
    878                     } else {
    879                         pStr += nextSymb + scr[1] + ' ' + scr[2];
    880                         if (nextSymb === symbc) {
    881                             i += 1;
    882                             scr = el.points[i].scrCoords;
    883                             pStr += ' ' + scr[1] + ' ' + scr[2];
    884                             i += 1;
    885                             scr = el.points[i].scrCoords;
    886                             pStr += ' ' + scr[1] + ' ' + scr[2];
    887                         }
    888                         nextSymb = symbc;
    889                     }
    890                     i += 1;
    891                 }
    892             }
    893             return pStr;
    894         },
    895
    896         // Already documented in JXG.AbstractRenderer
    897         updatePathStringBezierPrim: function (el) {
    898             var i, j, k, scr, lx, ly, len,
    899                 symbm = ' M ',
    900                 symbl = ' C ',
    901                 nextSymb = symbm,
    902                 maxSize = 5000.0,
    903                 pStr = '',
    904                 f = Type.evaluate(el.visProp.strokewidth),
    905                 isNoPlot = (Type.evaluate(el.visProp.curvetype) !== 'plot');
    906
    907             if (el.numberPoints <= 0) {
    908                 return '';
    909             }
    910
    911             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
    912                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
    913             }
    914
    915             len = Math.min(el.points.length, el.numberPoints);
    916             for (j = 1; j < 3; j++) {
    917                 nextSymb = symbm;
    918                 for (i = 0; i < len; i++) {
    919                     scr = el.points[i].scrCoords;
    920
    921                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
    922                         nextSymb = symbm;
    923                     } else {
    924                         // Chrome has problems with values being too far away.
    925                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
    926                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
    927
    928                         // Attention: first coordinate may be inaccurate if far way
    929                         if (nextSymb === symbm) {
    930                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
    931                             pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
    932                         } else {
    933                             k = 2 * j;
    934                             pStr += [nextSymb,
    935                                 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)), ' ',
    936                                 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)), ' ',
    937                                 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)), ' ',
    938                                 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)), ' ',
    939                                 scr[1], ' ', scr[2]].join('');
    940                         }
    941
    942                         nextSymb = symbl;
    943                         lx = scr[1];
    944                         ly = scr[2];
    945                     }
    946                 }
    947             }
    948             return pStr;
    949         },
    950
    951         // Already documented in JXG.AbstractRenderer
    952         updatePolygonPrim: function (node, el) {
    953             var i,
    954                 pStr = '',
    955                 scrCoords,
    956                 len = el.vertices.length;
    957
    958             node.setAttributeNS(null, 'stroke', 'none');
    959             if (el.elType === 'polygonalchain') {
    960                 len++;
    961             }
    962
    963             for (i = 0; i < len - 1; i++) {
    964                 if (el.vertices[i].isReal) {
    965                     scrCoords = el.vertices[i].coords.scrCoords;
    966                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
    967                 } else {
    968                     node.setAttributeNS(null, 'points', '');
    969                     return;
    970                 }
    971
    972                 if (i < len - 2) {
    973                     pStr += " ";
    974                 }
    975             }
    976             if (pStr.indexOf('NaN') === -1) {
    977                 node.setAttributeNS(null, 'points', pStr);
    978             }
    979         },
    980
    981         // Already documented in JXG.AbstractRenderer
    982         updateRectPrim: function (node, x, y, w, h) {
    983             node.setAttributeNS(null, 'x', x);
    984             node.setAttributeNS(null, 'y', y);
    985             node.setAttributeNS(null, 'width', w);
    986             node.setAttributeNS(null, 'height', h);
    987         },
    988
    989         /* **************************
    990          *  Set Attributes
    991          * **************************/
    992
    993         // documented in JXG.AbstractRenderer
    994         setPropertyPrim: function (node, key, val) {
    995             if (key === 'stroked') {
    996                 return;
    997             }
    998             node.setAttributeNS(null, key, val);
    999         },
    1000
    1001         display: function (el, val) {
    1002             var node;
    1003
    1004             if (el && el.rendNode) {
    1005                 el.visPropOld.visible = val;
    1006                 node = el.rendNode;
    1007                 if (val) {
    1008                     node.setAttributeNS(null, 'display', 'inline');
    1009                     node.style.visibility = "inherit";
    1010                 } else {
    1011                     node.setAttributeNS(null, 'display', 'none');
    1012                     node.style.visibility = "hidden";
    1013                 }
    1014             }
    1015         },
    1016
    1017         // documented in JXG.AbstractRenderer
    1018         show: function (el) {
    1019             JXG.deprecated('Board.renderer.show()', 'Board.renderer.display()');
    1020             this.display(el, true);
    1021             // var node;
    1022             //
    1023             // if (el && el.rendNode) {
    1024             //     node = el.rendNode;
    1025             //     node.setAttributeNS(null, 'display', 'inline');
    1026             //     node.style.visibility = "inherit";
    1027             // }
    1028         },
    1029
    1030         // documented in JXG.AbstractRenderer
    1031         hide: function (el) {
    1032             JXG.deprecated('Board.renderer.hide()', 'Board.renderer.display()');
    1033             this.display(el, false);
    1034             // var node;
    1035             //
    1036             // if (el && el.rendNode) {
    1037             //     node = el.rendNode;
    1038             //     node.setAttributeNS(null, 'display', 'none');
    1039             //     node.style.visibility = "hidden";
    1040             // }
    1041         },
    1042
    1043         // documented in JXG.AbstractRenderer
    1044         setBuffering: function (el, type) {
    1045             el.rendNode.setAttribute('buffered-rendering', type);
    1046         },
    1047
    1048         // documented in JXG.AbstractRenderer
    1049         setDashStyle: function (el) {
    1050             var dashStyle = Type.evaluate(el.visProp.dash),
    1051                 node = el.rendNode;
    1052
    1053             if (dashStyle > 0) {
    1054                 node.setAttributeNS(null, 'stroke-dasharray', this.dashArray[dashStyle - 1]);
    1055             } else {
    1056                 if (node.hasAttributeNS(null, 'stroke-dasharray')) {
    1057                     node.removeAttributeNS(null, 'stroke-dasharray');
    1058                 }
    1059             }
    1060         },
    1061
    1062         // documented in JXG.AbstractRenderer
    1063         setGradient: function (el) {
    1064             var fillNode = el.rendNode,
    1065                 node, node2, node3,
    1066                 ev_g = Type.evaluate(el.visProp.gradient);
    1067
    1068             if (ev_g === 'linear' || ev_g === 'radial') {
    1069                 node = this.createPrim(ev_g + 'Gradient', el.id + '_gradient');
    1070                 node2 = this.createPrim('stop', el.id + '_gradient1');
    1071                 node3 = this.createPrim('stop', el.id + '_gradient2');
    1072                 node.appendChild(node2);
    1073                 node.appendChild(node3);
    1074                 this.defs.appendChild(node);
    1075                 fillNode.setAttributeNS(null, 'style', 'fill:url(#' + this.container.id + '_' + el.id + '_gradient)');
    1076                 el.gradNode1 = node2;
    1077                 el.gradNode2 = node3;
    1078                 el.gradNode = node;
    1079             } else {
    1080                 fillNode.removeAttributeNS(null, 'style');
    1081             }
    1082         },
    1083
    1084         /**
    1085          * Set the gradient angle for linear color gradients.
    1086          *
    1087          * @private
    1088          * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
    1089          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
    1090          */
    1091         updateGradientAngle: function(node, radians) {
    1092             // Angles:
    1093             // 0: ->
    1094             // 90: down
    1095             // 180: <-
    1096             // 90: up
    1097             var f = 1.0,
    1098                 co = Math.cos(radians),
    1099                 si = Math.sin(radians);
    1100
    1101             if (Math.abs(co) > Math.abs(si)) {
    1102                 f /= Math.abs(co);
    1103             } else {
    1104                 f /= Math.abs(si);
    1105             }
    1106
    1107             if (co >= 0) {
    1108                 node.setAttributeNS(null, 'x1', 0);
    1109                 node.setAttributeNS(null, 'x2', co * f);
    1110             } else {
    1111                 node.setAttributeNS(null, 'x1', -co * f);
    1112                 node.setAttributeNS(null, 'x2', 0);
    1113             }
    1114             if (si >= 0) {
    1115                 node.setAttributeNS(null, 'y1', 0);
    1116                 node.setAttributeNS(null, 'y2', si * f);
    1117             } else {
    1118                 node.setAttributeNS(null, 'y1', -si * f);
    1119                 node.setAttributeNS(null, 'y2', 0);
    1120             }
    1121         },
    1122
    1123         /**
    1124          * Set circles for radial color gradients.
    1125          *
    1126          * @private
    1127          * @param {SVGnode} node SVG gradient node
    1128          * @param {Number} cx SVG value cx (value between 0 and 1)
    1129          * @param {Number} cy  SVG value cy (value between 0 and 1)
    1130          * @param {Number} r  SVG value r (value between 0 and 1)
    1131          * @param {Number} fx  SVG value fx (value between 0 and 1)
    1132          * @param {Number} fy  SVG value fy (value between 0 and 1)
    1133          * @param {Number} fr  SVG value fr (value between 0 and 1)
    1134          */
    1135         updateGradientCircle: function(node, cx, cy, r, fx, fy, fr) {
    1136             node.setAttributeNS(null, 'cx', cx * 100 + '%');   // Center first color
    1137             node.setAttributeNS(null, 'cy', cy * 100 + '%');
    1138             node.setAttributeNS(null, 'r', r * 100 + '%');
    1139             node.setAttributeNS(null, 'fx', fx * 100 + '%');   // Center second color / focal point
    1140             node.setAttributeNS(null, 'fy', fy * 100 + '%');
    1141             node.setAttributeNS(null, 'fr', fr * 100 + '%');
    1142         },
    1143
    1144         // documented in JXG.AbstractRenderer
    1145         updateGradient: function (el) {
    1146             var col, op,
    1147                 node2 = el.gradNode1,
    1148                 node3 = el.gradNode2,
    1149                 ev_g = Type.evaluate(el.visProp.gradient);
    1150
    1151             if (!Type.exists(node2) || !Type.exists(node3)) {
    1152                 return;
    1153             }
    1154
    1155             op = Type.evaluate(el.visProp.fillopacity);
    1156             op = (op > 0) ? op : 0;
    1157             col = Type.evaluate(el.visProp.fillcolor);
    1158
    1159             node2.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
    1160             node3.setAttributeNS(null, 'style',
    1161                     'stop-color:'    + Type.evaluate(el.visProp.gradientsecondcolor) +
    1162                     ';stop-opacity:' + Type.evaluate(el.visProp.gradientsecondopacity)
    1163                 );
    1164             node2.setAttributeNS(null, 'offset', Type.evaluate(el.visProp.gradientstartoffset) * 100 + '%');
    1165             node3.setAttributeNS(null, 'offset', Type.evaluate(el.visProp.gradientendoffset) * 100 + '%');
    1166             if (ev_g === 'linear') {
    1167                 this.updateGradientAngle(el.gradNode, Type.evaluate(el.visProp.gradientangle));
    1168             } else if (ev_g === 'radial') {
    1169                 this.updateGradientCircle(el.gradNode,
    1170                     Type.evaluate(el.visProp.gradientcx),
    1171                     Type.evaluate(el.visProp.gradientcy),
    1172                     Type.evaluate(el.visProp.gradientr),
    1173                     Type.evaluate(el.visProp.gradientfx),
    1174                     Type.evaluate(el.visProp.gradientfy),
    1175                     Type.evaluate(el.visProp.gradientfr)
    1176                 );
    1177             }
    1178         },
    1179
    1180         // documented in JXG.AbstractRenderer
    1181         setObjectTransition: function (el, duration) {
    1182             var node, transitionStr,
    1183                 i, len,
    1184                 nodes = ['rendNode',
    1185                     'rendNodeTriangleStart',
    1186                     'rendNodeTriangleEnd'];
    1187
    1188             if (duration === undefined) {
    1189                 duration = Type.evaluate(el.visProp.transitionduration);
    1190             }
    1191
    1192             if (duration === el.visPropOld.transitionduration) {
    1193                 return;
    1194             }
    1195
    1196             if (el.elementClass === Const.OBJECT_CLASS_TEXT &&
    1197                 Type.evaluate(el.visProp.display) === 'html') {
    1198                 transitionStr = ' color ' + duration + 'ms,' +
    1199                     ' opacity ' + duration + 'ms';
    1200             } else {
    1201                 transitionStr = ' fill ' + duration + 'ms,' +
    1202                     ' fill-opacity ' + duration + 'ms,' +
    1203                     ' stroke ' + duration + 'ms,' +
    1204                     ' stroke-opacity ' + duration + 'ms';
    1205             }
    1206
    1207             len = nodes.length;
    1208             for (i = 0; i < len; ++i) {
    1209                 if (el[nodes[i]]) {
    1210                     node = el[nodes[i]];
    1211                     node.style.transition = transitionStr;
    1212                 }
    1213             }
    1214
    1215             el.visPropOld.transitionduration = duration;
    1216         },
    1217
    1218         /**
    1219          * Call user-defined function to set visual attributes.
    1220          * If "testAttribute" is the empty string, the function
    1221          * is called immediately, otherwise it is called in a timeOut.
    1222          *
    1223          * This is necessary to realize smooth transitions but avoid transitions
    1224          * when first creating the objects.
    1225          *
    1226          * Usually, the string in testAttribute is the visPropOld attribute
    1227          * of the values which are set.
    1228          *
    1229          * @param {Function} setFunc       Some function which usually sets some attributes
    1230          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
    1231          *                               otherwise it is called in a setImeout.
    1232          * @see JXG.SVGRenderer#setObjectFillColor
    1233          * @see JXG.SVGRenderer#setObjectStrokeColor
    1234          * @see JXG.SVGRenderer#_setArrowColor
    1235          * @private
    1236          */
    1237         _setAttribute: function (setFunc, testAttribute) {
    1238             if (testAttribute === '') {
    1239                 setFunc();
    1240             } else {
    1241                 window.setTimeout(setFunc, 1);
    1242             }
    1243         },
    1244
    1245         // documented in JXG.AbstractRenderer
    1246         setObjectFillColor: function (el, color, opacity, rendNode) {
    1247             var node, c, rgbo, oo,
    1248                 rgba = Type.evaluate(color),
    1249                 o = Type.evaluate(opacity),
    1250                 grad = Type.evaluate(el.visProp.gradient);
    1251
    1252             o = (o > 0) ? o : 0;
    1253
    1254             // TODO  save gradient and gradientangle
    1255             if (el.visPropOld.fillcolor === rgba && el.visPropOld.fillopacity === o && grad === null) {
    1256                 return;
    1257             }
    1258
    1259             if (Type.exists(rgba) && rgba !== false) {
    1260                 if (rgba.length !== 9) {          // RGB, not RGBA
    1261                     c = rgba;
    1262                     oo = o;
    1263                 } else {                       // True RGBA, not RGB
    1264                     rgbo = Color.rgba2rgbo(rgba);
    1265                     c = rgbo[0];
    1266                     oo = o * rgbo[1];
    1267                 }
    1268
    1269                 if (rendNode === undefined) {
    1270                     node = el.rendNode;
    1271                 } else {
    1272                     node = rendNode;
    1273                 }
    1274
    1275                 if (c !== 'none') {
    1276                     this._setAttribute(function () {
    1277                         node.setAttributeNS(null, 'fill', c);
    1278                     }, el.visPropOld.fillcolor);
    1279                 }
    1280
    1281                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
    1282                     this._setAttribute(function () {
    1283                         node.setAttributeNS(null, 'opacity', oo);
    1284                     }, el.visPropOld.fillopacity);
    1285                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
    1286                 } else {
    1287                     if (c === 'none') {  // This is done only for non-images
    1288                         // because images have no fill color.
    1289                         oo = 0;
    1290                         // This is necessary if there is a foreignObject below.
    1291                         node.setAttributeNS(null, 'pointer-events', 'visibleStroke');
    1292                     } else {
    1293                         // This is the default
    1294                         node.setAttributeNS(null, 'pointer-events', 'visiblePainted');
    1295                     }
    1296                     this._setAttribute(function () {
    1297                         node.setAttributeNS(null, 'fill-opacity', oo);
    1298                     }, el.visPropOld.fillopacity);
    1299                 }
    1300
    1301                 if (grad === 'linear' || grad === 'radial') {
    1302                     this.updateGradient(el);
    1303                 }
    1304             }
    1305             el.visPropOld.fillcolor = rgba;
    1306             el.visPropOld.fillopacity = o;
    1307         },
    1308
    1309         // documented in JXG.AbstractRenderer
    1310         setObjectStrokeColor: function (el, color, opacity) {
    1311             var rgba = Type.evaluate(color), c, rgbo,
    1312                 o = Type.evaluate(opacity), oo,
    1313                 node;
    1314
    1315             o = (o > 0) ? o : 0;
    1316
    1317             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
    1318                 return;
    1319             }
    1320
    1321             if (Type.exists(rgba) && rgba !== false) {
    1322                 if (rgba.length !== 9) {          // RGB, not RGBA
    1323                     c = rgba;
    1324                     oo = o;
    1325                 } else {                       // True RGBA, not RGB
    1326                     rgbo = Color.rgba2rgbo(rgba);
    1327                     c = rgbo[0];
    1328                     oo = o * rgbo[1];
    1329                 }
    1330
    1331                 node = el.rendNode;
    1332
    1333                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
    1334                     if (Type.evaluate(el.visProp.display) === 'html') {
    1335                         this._setAttribute(function () {
    1336                             node.style.color = c;
    1337                             node.style.opacity = oo;
    1338                         }, el.visPropOld.strokecolor);
    1339
    1340                     } else {
    1341                         this._setAttribute(function () {
    1342                             node.setAttributeNS(null, "style", "fill:" + c);
    1343                             node.setAttributeNS(null, "style", "fill-opacity:" + oo);
    1344                         }, el.visPropOld.strokecolor);
    1345                     }
    1346                 } else {
    1347                     this._setAttribute(function () {
    1348                         node.setAttributeNS(null, 'stroke', c);
    1349                         node.setAttributeNS(null, 'stroke-opacity', oo);
    1350                     }, el.visPropOld.strokecolor);
    1351                 }
    1352
    1353                 if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
    1354                     el.elementClass === Const.OBJECT_CLASS_LINE) {
    1355                     if (Type.evaluate(el.visProp.firstarrow)) {
    1356                         this._setArrowColor(el.rendNodeTriangleStart, c, oo, el, el.visPropCalc.typeFirst);
    1357                     }
    1358
    1359                     if (Type.evaluate(el.visProp.lastarrow)) {
    1360                         this._setArrowColor(el.rendNodeTriangleEnd, c, oo, el, el.visPropCalc.typeLast);
    1361                     }
    1362                 }
    1363             }
    1364
    1365             el.visPropOld.strokecolor = rgba;
    1366             el.visPropOld.strokeopacity = o;
    1367         },
    1368
    1369         // documented in JXG.AbstractRenderer
    1370         setObjectStrokeWidth: function (el, width) {
    1371             var node,
    1372                 w = Type.evaluate(width);
    1373
    1374             if (isNaN(w) || el.visPropOld.strokewidth === w) {
    1375                 return;
    1376             }
    1377
    1378             node = el.rendNode;
    1379             this.setPropertyPrim(node, 'stroked', 'true');
    1380             if (Type.exists(w)) {
    1381                 this.setPropertyPrim(node, 'stroke-width', w + 'px');
    1382
    1383                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
    1384                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
    1385                 //     if (Type.evaluate(el.visProp.firstarrow)) {
    1386                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
    1387                 //     }
    1388                 //
    1389                 //     if (Type.evaluate(el.visProp.lastarrow)) {
    1390                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
    1391                 //     }
    1392                 // }
    1393             }
    1394             el.visPropOld.strokewidth = w;
    1395         },
    1396
    1397         // documented in JXG.AbstractRenderer
    1398         setLineCap: function (el) {
    1399             var capStyle = Type.evaluate(el.visProp.linecap);
    1400
    1401             if (capStyle === undefined || capStyle === '' || el.visPropOld.linecap === capStyle ||
    1402                 !Type.exists(el.rendNode)) {
    1403                 return;
    1404             }
    1405
    1406             this.setPropertyPrim(el.rendNode, 'stroke-linecap', capStyle);
    1407             el.visPropOld.linecap = capStyle;
    1408
    1409         },
    1410
    1411         // documented in JXG.AbstractRenderer
    1412         setShadow: function (el) {
    1413             var ev_s = Type.evaluate(el.visProp.shadow);
    1414             if (el.visPropOld.shadow === ev_s) {
    1415                 return;
    1416             }
    1417
    1418             if (Type.exists(el.rendNode)) {
    1419                 if (ev_s) {
    1420                     el.rendNode.setAttributeNS(null, 'filter', 'url(#' + this.container.id + '_' + 'f1)');
    1421                 } else {
    1422                     el.rendNode.removeAttributeNS(null, 'filter');
    1423                 }
    1424             }
    1425             el.visPropOld.shadow = ev_s;
    1426         },
    1427
    1428         /* **************************
    1429          * renderer control
    1430          * **************************/
    1431
    1432         // documented in JXG.AbstractRenderer
    1433         suspendRedraw: function () {
    1434             // It seems to be important for the Linux version of firefox
    1435             //this.suspendHandle = this.svgRoot.suspendRedraw(10000);
    1436         },
    1437
    1438         // documented in JXG.AbstractRenderer
    1439         unsuspendRedraw: function () {
    1440             //this.svgRoot.unsuspendRedraw(this.suspendHandle);
    1441             //this.svgRoot.unsuspendRedrawAll();
    1442             //this.svgRoot.forceRedraw();
    1443         },
    1444
    1445         // documented in AbstractRenderer
    1446         resize: function (w, h) {
    1447             // this.svgRoot.style.width  = parseFloat(w) + 'px';
    1448             // this.svgRoot.style.height = parseFloat(h) + 'px';
    1449
    1450             this.svgRoot.setAttribute('width',  parseFloat(w));
    1451             this.svgRoot.setAttribute('height', parseFloat(h));
    1452             // this.svgRoot.setAttribute('width',  '100%');
    1453             // this.svgRoot.setAttribute('height', '100%');
    1454         },
    1455
    1456         // documented in JXG.AbstractRenderer
    1457         createTouchpoints: function (n) {
    1458             var i, na1, na2, node;
    1459             this.touchpoints = [];
    1460             for (i = 0; i < n; i++) {
    1461                 na1 = 'touchpoint1_' + i;
    1462                 node = this.createPrim('path', na1);
    1463                 this.appendChildPrim(node, 19);
    1464                 node.setAttributeNS(null, 'd', 'M 0 0');
    1465                 this.touchpoints.push(node);
    1466
    1467                 this.setPropertyPrim(node, 'stroked', 'true');
    1468                 this.setPropertyPrim(node, 'stroke-width', '1px');
    1469                 node.setAttributeNS(null, 'stroke', '#000000');
    1470                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
    1471                 node.setAttributeNS(null, 'display', 'none');
    1472
    1473                 na2 = 'touchpoint2_' + i;
    1474                 node = this.createPrim('ellipse', na2);
    1475                 this.appendChildPrim(node, 19);
    1476                 this.updateEllipsePrim(node, 0, 0, 0, 0);
    1477                 this.touchpoints.push(node);
    1478
    1479                 this.setPropertyPrim(node, 'stroked', 'true');
    1480                 this.setPropertyPrim(node, 'stroke-width', '1px');
    1481                 node.setAttributeNS(null, 'stroke', '#000000');
    1482                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
    1483                 node.setAttributeNS(null, 'fill', '#ffffff');
    1484                 node.setAttributeNS(null, 'fill-opacity', 0.0);
    1485
    1486                 node.setAttributeNS(null, 'display', 'none');
    1487             }
    1488         },
    1489
    1490         // documented in JXG.AbstractRenderer
    1491         showTouchpoint: function (i) {
    1492             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
    1493                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'inline');
    1494                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'inline');
    1495             }
    1496         },
    1497
    1498         // documented in JXG.AbstractRenderer
    1499         hideTouchpoint: function (i) {
    1500             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
    1501                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'none');
    1502                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'none');
    1503             }
    1504         },
    1505
    1506         // documented in JXG.AbstractRenderer
    1507         updateTouchpoint: function (i, pos) {
    1508             var x, y,
    1509                 d = 37;
    1510
    1511             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
    1512                 x = pos[0];
    1513                 y = pos[1];
    1514
    1515                 this.touchpoints[2 * i].setAttributeNS(null, 'd', 'M ' + (x - d) + ' ' + y + ' ' +
    1516                     'L ' + (x + d) + ' ' + y + ' ' +
    1517                     'M ' + x + ' ' + (y - d) + ' ' +
    1518                     'L ' + x + ' ' + (y + d));
    1519                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
    1520             }
    1521         },
    1522
    1523         /**
    1524          * Walk recursively through the DOM subtree of a node and collect all
    1525          * value attributes together with the id of that node.
    1526          * <b>Attention:</b> Only values of nodes having a valid id are taken.
    1527          * @param  {Node} node   root node of DOM subtree that will be searched recursively.
    1528          * @return {Array}      Array with entries of the form [id, value]
    1529          * @private
    1530          */
    1531         _getValuesOfDOMElements: function (node) {
    1532             var values = [];
    1533             if (node.nodeType === 1) {
    1534                 node = node.firstChild;
    1535                 while (node) {
    1536                     if (node.id !== undefined && node.value !== undefined) {
    1537                         values.push([node.id, node.value]);
    1538                     }
    1539                     values = values.concat(this._getValuesOfDOMElements(node));
    1540                     node = node.nextSibling;
    1541                 }
    1542             }
    1543             return values;
    1544         },
    1545
    1546         _getDataUri: function (url, callback) {
    1547             var image = new Image();
    1548
    1549             image.onload = function () {
    1550                 var canvas = document.createElement('canvas');
    1551                 canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
    1552                 canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
    1553
    1554                 canvas.getContext('2d').drawImage(this, 0, 0);
    1555
    1556                 callback(canvas.toDataURL('image/png'));
    1557                 canvas.remove();
    1558             };
    1559
    1560             image.src = url;
    1561         },
    1562
    1563         _getImgDataURL: function(svgRoot) {
    1564             var images, len, canvas, ctx,
    1565                 ur, i;
    1566
    1567             images = svgRoot.getElementsByTagName("image");
    1568             len = images.length;
    1569             if (len > 0) {
    1570                 canvas = document.createElement('canvas');
    1571                 //img = new Image();
    1572                 for (i = 0; i < len; i++) {
    1573                     images[i].setAttribute("crossorigin", "anonymous");
    1574                     //img.src = images[i].href;
    1575                     //img.onload = function() {
    1576                     // img.crossOrigin = "anonymous";
    1577                     ctx = canvas.getContext('2d');
    1578                     canvas.width = images[i].getAttribute("width");
    1579                     canvas.height = images[i].getAttribute("height");
    1580                     try {
    1581                         ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
    1582
    1583                         // If the image is not png, the format must be specified here
    1584                         ur = canvas.toDataURL();
    1585                         images[i].setAttribute("xlink:href", ur);
    1586                     } catch (err) {
    1587                         console.log("CORS problem! Image can not be used", err);
    1588                     }
    1589                 }
    1590                 //canvas.remove();
    1591             }
    1592             return true;
    1593         },
    1594
    1595         /**
    1596          * Return a data URI of the SVG code representeing the construction.
    1597          * The SVG code of the construction is base64 encoded. The return string starts
    1598          * with "data:image/svg+xml;base64,...".
    1599          *
    1600          * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
    1601          * This is necessary for older versions of Safari. Default: false
    1602          * @returns {String}  data URI string
    1603          */
    1604         dumpToDataURI: function (ignoreTexts) {
    1605             var svgRoot = this.svgRoot,
    1606                 btoa = window.btoa || Base64.encode,
    1607                 svg,
    1608                 virtualNode, doc,
    1609                 i, len,
    1610                 values = [];
    1611
    1612             // Move all HTML tags (beside the SVG root) of the container
    1613             // to the foreignObject element inside of the svgRoot node
    1614             // Problem:
    1615             // input values are not copied. This can be verified by looking at an innerHTML output
    1616             // of an input element. Therefore, we do it "by hand".
    1617             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
    1618                 if (!ignoreTexts) {
    1619                     this.foreignObjLayer.setAttribute('display', 'inline');
    1620                 }
    1621                 while (svgRoot.nextSibling) {
    1622
    1623                     // Copy all value attributes
    1624                     values = values.concat(this._getValuesOfDOMElements(svgRoot.nextSibling));
    1625
    1626                     this.foreignObjLayer.appendChild(svgRoot.nextSibling);
    1627                 }
    1628             }
    1629
    1630             this._getImgDataURL(svgRoot);
    1631
    1632             // Convert the SVG graphic into a string containing SVG code
    1633             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    1634             svg = new XMLSerializer().serializeToString(svgRoot);
    1635
    1636             if (ignoreTexts !== true) {
    1637                 // Handle SVG texts
    1638                 // Insert all value attributes back into the svg string
    1639                 len = values.length;
    1640                 for (i = 0; i < len; i++) {
    1641                     svg = svg.replace('id="' + values[i][0] + '"', 'id="' + values[i][0] + '" value="' + values[i][1] + '"');
    1642                 }
    1643             }
    1644
    1645             // if (false) {
    1646             //     // Debug: use example svg image
    1647             //     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>';
    1648             // }
    1649
    1650             // In IE we have to remove the namespace again.
    1651             if ((svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1) {
    1652                 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, '');
    1653             }
    1654
    1655             // Safari fails if the svg string contains a " "
    1656             // Obsolete with Safari 12+
    1657             svg = svg.replace(/ /g, ' ');
    1658
    1659             // Move all HTML tags back from
    1660             // the foreignObject element to the container
    1661             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
    1662                 // Restore all HTML elements
    1663                 while (this.foreignObjLayer.firstChild) {
    1664                     this.container.appendChild(this.foreignObjLayer.firstChild);
    1665                 }
    1666                 this.foreignObjLayer.setAttribute("display", "none");
    1667             }
    1668
    1669             return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
    1670         },
    1671
    1672         /**
    1673          * Convert the SVG construction into an HTML canvas image.
    1674          * This works for all SVG supporting browsers. Implemented as Promise.
    1675          * <p>
    1676          * For IE, it is realized as function.
    1677          * It works from version 9, with the exception that HTML texts
    1678          * are ignored on IE. The drawing is done with a delay of
    1679          * 200 ms. Otherwise there would be problems with IE.
    1680          *
    1681          * @param {String} canvasId Id of an HTML canvas element
    1682          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
    1683          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
    1684          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
    1685          * This is necessary for older versions of Safari. Default: false
    1686          * @returns {Promise}  Promise object
    1687          *
    1688          * @example
    1689          * 	board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
    1690          *
    1691          * @example
    1692          *  // IE 11 example:
    1693          * 	board.renderer.dumpToCanvas('canvas');
    1694          * 	setTimeout(function() { console.log('done'); }, 400);
    1695          */
    1696         dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
    1697             var svg, tmpImg, cv, ctx;
    1698
    1699             // Prepare the canvas element
    1700             cv = document.getElementById(canvasId);
    1701
    1702             // Clear the canvas
    1703             /* eslint-disable no-self-assign */
    1704             cv.width = cv.width;
    1705             /* eslint-enable no-self-assign */
    1706
    1707             ctx = cv.getContext("2d");
    1708             if (w !== undefined && h !== undefined) {
    1709                 cv.style.width = parseFloat(w) + 'px';
    1710                 cv.style.height = parseFloat(h) + 'px';
    1711                 // Scale twice the CSS size to make the image crisp
    1712                 // cv.setAttribute('width', 2 * parseFloat(wOrg));
    1713                 // cv.setAttribute('height', 2 * parseFloat(hOrg));
    1714                 // ctx.scale(2 * wOrg / w, 2 * hOrg / h);
    1715                 cv.setAttribute('width', parseFloat(w));
    1716                 cv.setAttribute('height', parseFloat(h));
    1717             }
    1718
    1719             // Display the SVG string as data-uri in an HTML img.
    1720             tmpImg = new Image();
    1721             svg = this.dumpToDataURI(ignoreTexts);
    1722             tmpImg.src = svg;
    1723
    1724             // Finally, draw the HTML img in the canvas.
    1725             if (!('Promise' in window)) {
    1726                 tmpImg.onload = function () {
    1727                     // IE needs a pause...
    1728                     // Seems to be broken
    1729                     window.setTimeout(function() {
    1730                         try {
    1731                             ctx.drawImage(tmpImg, 0, 0, w, h);
    1732                         } catch (err) {
    1733                             console.log("screenshots not longer supported on IE");
    1734                         }
    1735                     }, 200);
    1736                 };
    1737                 return this;
    1738             }
    1739
    1740             return new Promise(function(resolve, reject) {
    1741                 try {
    1742                     tmpImg.onload = function () {
    1743                         ctx.drawImage(tmpImg, 0, 0, w, h);
    1744                         resolve();
    1745                     };
    1746                 } catch (e) {
    1747                     reject(e);
    1748                 }
    1749             });
    1750
    1751         },
    1752
    1753         /**
    1754          * Display SVG image in html img-tag which enables
    1755          * easy download for the user.
    1756          *
    1757          * Support:
    1758          * <ul>
    1759          * <li> IE: No
    1760          * <li> Edge: full
    1761          * <li>Firefox: full
    1762          * <li> Chrome: full
    1763          * <li> Safari: full (No text support in versions prior to 12).
    1764          * </ul>
    1765          *
    1766          * @param {JXG.Board} board Link to the board.
    1767          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
    1768          * the screenshot is copied to this img object. The width and height will be set to the values of the
    1769          * JSXGraph container.
    1770          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
    1771          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
    1772          * @return {Object}       the svg renderer object
    1773          */
    1774         screenshot: function (board, imgId, ignoreTexts) {
    1775             var node,
    1776                 doc = this.container.ownerDocument,
    1777                 parent = this.container.parentNode,
    1778                 cPos,
    1779                 canvas, id,
    1780                 img,
    1781                 button, buttonText,
    1782                 w, h,
    1783                 bas = board.attr.screenshot,
    1784                 zbar, zbarDisplay, cssTxt,
    1785                 newImg = false,
    1786                 _copyCanvasToImg,
    1787                 isDebug = false;
    1788
    1789             if (this.type === 'no') {
    1790                 return this;
    1791             }
    1792
    1793             w = bas.scale * this.container.getBoundingClientRect().width;
    1794             h = bas.scale * this.container.getBoundingClientRect().height;
    1795
    1796             if (imgId === undefined || imgId === '') {
    1797                 newImg = true;
    1798                 img = new Image(); //doc.createElement('img');
    1799                 img.style.width = w + 'px';
    1800                 img.style.height = h + 'px';
    1801             } else {
    1802                 newImg = false;
    1803                 img = doc.getElementById(imgId);
    1804             }
    1805             // img.crossOrigin = 'anonymous';
    1806
    1807             // Create div which contains canvas element and close button
    1808             if (newImg) {
    1809                 node = doc.createElement('div');
    1810                 node.style.cssText = bas.css;
    1811                 node.style.width = (w) + 'px';
    1812                 node.style.height = (h) + 'px';
    1813                 node.style.zIndex = this.container.style.zIndex + 120;
    1814
    1815                 // Try to position the div exactly over the JSXGraph board
    1816                 node.style.position = 'absolute';
    1817                 node.style.top = this.container.offsetTop + 'px';
    1818                 node.style.left = this.container.offsetLeft + 'px';
    1819             }
    1820
    1821             if (!isDebug) {
    1822                 // Create canvas element and add it to the DOM
    1823                 // It will be removed after the image has been stored.
    1824                 canvas = doc.createElement('canvas');
    1825                 id = Math.random().toString(36).substr(2, 5);
    1826                 canvas.setAttribute('id', id);
    1827                 canvas.setAttribute('width', w);
    1828                 canvas.setAttribute('height', h);
    1829                 canvas.style.width = w + 'px';
    1830                 canvas.style.height = w + 'px';
    1831                 canvas.style.display = 'none';
    1832                 parent.appendChild(canvas);
    1833             } else {
    1834                 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
    1835                 id = 'jxgbox_canvas';
    1836                 canvas = document.getElementById(id);
    1837             }
    1838
    1839             if (newImg) {
    1840                 // Create close button
    1841                 button = doc.createElement('span');
    1842                 buttonText = doc.createTextNode('\u2716');
    1843                 button.style.cssText = bas.cssButton;
    1844                 button.appendChild(buttonText);
    1845                 button.onclick = function () {
    1846                     node.parentNode.removeChild(node);
    1847                 };
    1848
    1849                 // Add all nodes
    1850                 node.appendChild(img);
    1851                 node.appendChild(button);
    1852                 parent.insertBefore(node, this.container.nextSibling);
    1853             }
    1854
    1855             // Hide navigation bar in board
    1856             zbar = document.getElementById(this.container.id + '_navigationbar');
    1857             if (Type.exists(zbar)) {
    1858                 zbarDisplay = zbar.style.display;
    1859                 zbar.style.display = 'none';
    1860             }
    1861
    1862             _copyCanvasToImg = function() {
    1863                 // Show image in img tag
    1864                 img.src = canvas.toDataURL('image/png');
    1865
    1866                 // Remove canvas node
    1867                 if (!isDebug) {
    1868                     parent.removeChild(canvas);
    1869                 }
    1870             };
    1871
    1872             // Create screenshot in image element
    1873             if ('Promise' in window) {
    1874                 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
    1875             } else {
    1876                 // IE
    1877                 this.dumpToCanvas(id, w, h, ignoreTexts);
    1878                 window.setTimeout(_copyCanvasToImg, 200);
    1879             }
    1880
    1881             // Show navigation bar in board
    1882             if (Type.exists(zbar)) {
    1883                 zbar.style.display = zbarDisplay;
    1884             }
    1885
    1886             return this;
    1887         }
    1888
    1889     });
    1890
    1891     return JXG.SVGRenderer;
    1892 });
    1893