      1 /*
      2     Copyright 2008-2018
      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/env', 'utils/color', 'utils/base64', 'math/numerics'
     48 ], function (JXG, Options, AbstractRenderer, Const, Type, Env, 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.AbstractRenderer
     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
    107         this.resize(dim.width, dim.height);
    108
    109         //this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision');
    110
    111         this.container.appendChild(this.svgRoot);
    112
    113         /**
    114          * The <tt>defs</tt> element is a container element to reference reusable SVG elements.
    115          * @type Node
    116          * @see http://www.w3.org/TR/SVG/struct.html#DefsElement
    117          */
    118         this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, 'defs');
    119         this.svgRoot.appendChild(this.defs);
    120
    121         /**
    122          * Filters are used to apply shadows.
    123          * @type Node
    124          * @see http://www.w3.org/TR/SVG/filters.html#FilterElement
    125          */
    126         this.filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter');
    127         this.filter.setAttributeNS(null, 'id', this.container.id + '_' + 'f1');
    128         /*
    129         this.filter.setAttributeNS(null, 'x', '-100%');
    130         this.filter.setAttributeNS(null, 'y', '-100%');
    131         this.filter.setAttributeNS(null, 'width', '400%');
    132         this.filter.setAttributeNS(null, 'height', '400%');
    133         //this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
    134         */
    135         this.filter.setAttributeNS(null, 'width', '300%');
    136         this.filter.setAttributeNS(null, 'height', '300%');
    137         this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
    138
    139         this.feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
    140         this.feOffset.setAttributeNS(null, 'result', 'offOut');
    141         this.feOffset.setAttributeNS(null, 'in', 'SourceAlpha');
    142         this.feOffset.setAttributeNS(null, 'dx', '5');
    143         this.feOffset.setAttributeNS(null, 'dy', '5');
    144         this.filter.appendChild(this.feOffset);
    145
    146         this.feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
    147         this.feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
    148         this.feGaussianBlur.setAttributeNS(null, 'in', 'offOut');
    149         this.feGaussianBlur.setAttributeNS(null, 'stdDeviation', '3');
    150         this.filter.appendChild(this.feGaussianBlur);
    151
    152         this.feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
    153         this.feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
    154         this.feBlend.setAttributeNS(null, 'in2', 'blurOut');
    155         this.feBlend.setAttributeNS(null, 'mode', 'normal');
    156         this.filter.appendChild(this.feBlend);
    157
    158         this.defs.appendChild(this.filter);
    159
    160         /**
    161          * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
    162          * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
    163          * there, too. The higher the number, the "more on top" are the elements on this layer.
    164          * @type Array
    165          */
    166         this.layer = [];
    167         for (i = 0; i < Options.layer.numlayers; i++) {
    168             this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
    169             this.svgRoot.appendChild(this.layer[i]);
    170         }
    171
    172         // already documented in JXG.AbstractRenderer
    173         this.supportsForeignObject = document.implementation.hasFeature("http://w3.org/TR/SVG11/feature#Extensibility", "1.1");
    174
    175         if (this.supportsForeignObject) {
    176             this.foreignObjLayer = this.container.ownerDocument.createElementNS(this.svgNamespace, 'foreignObject');
    177             this.foreignObjLayer.setAttribute("x",0);
    178             this.foreignObjLayer.setAttribute("y",0);
    179             this.foreignObjLayer.setAttribute("width","100%");
    180             this.foreignObjLayer.setAttribute("height","100%");
    181             this.foreignObjLayer.setAttribute('id', this.container.id + '_foreignObj');
    182             this.svgRoot.appendChild(this.foreignObjLayer);
    183         }
    184
    185         /**
    186          * Defines dash patterns. Defined styles are: <ol>
    187          * <li value="-1"> 2px dash, 2px space</li>
    188          * <li> 5px dash, 5px space</li>
    189          * <li> 10px dash, 10px space</li>
    190          * <li> 20px dash, 20px space</li>
    191          * <li> 20px dash, 10px space, 10px dash, 10px dash</li>
    192          * <li> 20px dash, 5px space, 10px dash, 5px space</li></ol>
    193          * @type Array
    194          * @default ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5']
    195          * @see http://www.w3.org/TR/SVG/painting.html#StrokeProperties
    196          */
    197         this.dashArray = ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5'];
    198     };
    199
    200     JXG.SVGRenderer.prototype = new AbstractRenderer();
    201
    202     JXG.extend(JXG.SVGRenderer.prototype, /** @lends JXG.SVGRenderer.prototype */ {
    203
    204         /**
    205          * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
    206          * @private
    207          * @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached.
    208          * @param {String} [idAppendix=''] A string that is added to the node's id.
    209          * @returns {Node} Reference to the node added to the DOM.
    210          */
    211         _createArrowHead: function (el, idAppendix) {
    212             var node2, node3,
    213                 id = el.id + 'Triangle',
    214                 type = null,
    215                 w, s,
    216                 ev_fa = Type.evaluate(el.visProp.firstarrow),
    217                 ev_la = Type.evaluate(el.visProp.lastarrow);
    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                The arrow head is an isosceles triangle with base length 10 and height 10.
    236                This 10 units are scaled to strokeWidth * arrowSize pixels, see
    237                this._setArrowWidth().
    238
    239                See also abstractRenderer.updateLine() where the line path is shortened accordingly.
    240
    241                Changes here are also necessary in setArrowWidth().
    242             */
    243             node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'path');
    244             if (idAppendix === 'End') {
    245                 // First arrow
    246                 if (JXG.exists(ev_fa.type)) {
    247                     type = Type.evaluate(ev_fa.type);
    248                 }
    249
    250                 node2.setAttributeNS(null, 'refY', 5);
    251                 if (type === 2) {
    252                     node2.setAttributeNS(null, 'refX', 4.9);
    253                     node3.setAttributeNS(null, 'd', 'M 10,0 L 0,5 L 10,10 L 5,5 z');
    254                 } else if (type === 3) {
    255                         node2.setAttributeNS(null, 'refX', 3.33);
    256                         node3.setAttributeNS(null, 'd', 'M 0,0 L 3.33,0 L 3.33,10 L 0,10 z');
    257                 } else {
    258                     node2.setAttributeNS(null, 'refX', 9.9);
    259                     node3.setAttributeNS(null, 'd', 'M 10,0 L 0,5 L 10,10 z');
    260                 }
    261             } else {
    262                 // Last arrow
    263                 if (JXG.exists(ev_la.type)) {
    264                     type = Type.evaluate(ev_la.type);
    265                 }
    266
    267                 node2.setAttributeNS(null, 'refY', 5);
    268                 if (type === 2) {
    269                     node2.setAttributeNS(null, 'refX', 5.1);
    270                     node3.setAttributeNS(null, 'd', 'M 0,0 L 10,5 L 0,10 L 5,5 z');
    271                 } else if (type === 3) {
    272                     node2.setAttributeNS(null, 'refX', 0.1);
    273                     node3.setAttributeNS(null, 'd', 'M 0,0 L 3.33,0 L 3.33,10 L 0,10 z');
    274                 } else {
    275                     node2.setAttributeNS(null, 'refX', 0.1);
    276                     node3.setAttributeNS(null, 'd', 'M 0,0 L 10,5 L 0,10 z');
    277                 }
    278             }
    279
    280             node2.appendChild(node3);
    281             return node2;
    282         },
    283
    284         /**
    285          * Updates color of an arrow DOM node.
    286          * @param {Node} node The arrow node.
    287          * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green.
    288          * @param {Number} opacity
    289          * @param {JXG.GeometryElement} el The element the arrows are to be attached to
    290          */
    291         _setArrowColor: function (node, color, opacity, el) {
    292             var s, d;
    293
    294             if (node) {
    295                 if (Type.isString(color)) {
    296                     this._setAttribute(function() {
    297                         node.setAttributeNS(null, 'stroke', color);
    298                         node.setAttributeNS(null, 'fill', color);
    299                         node.setAttributeNS(null, 'stroke-opacity', opacity);
    300                         node.setAttributeNS(null, 'fill-opacity', opacity);
    301                     }, el.visPropOld.fillcolor);
    302                 }
    303
    304                 if (this.isIE) {
    305                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
    306                 }
    307             }
    308
    309         },
    310
    311         // already documented in JXG.AbstractRenderer
    312         _setArrowWidth: function (node, width, parentNode, size) {
    313             var s, d;
    314
    315             if (node) {
    316                 if (width === 0) {
    317                     node.setAttributeNS(null, 'display', 'none');
    318                 } else {
    319                     s = width;
    320                     d = s * size;
    321                     node.setAttributeNS(null, 'viewBox', (0) + ' ' + (0) + ' ' + (s * 10) + ' ' + (s * 10));
    322                     node.setAttributeNS(null, 'markerHeight', d);
    323                     node.setAttributeNS(null, 'markerWidth', d);
    324                     node.setAttributeNS(null, 'display', 'inherit');
    325                 }
    326
    327                 if (this.isIE) {
    328                     parentNode.parentNode.insertBefore(parentNode, parentNode);
    329                 }
    330             }
    331         },
    332
    333         /* ******************************** *
    334          *  This renderer does not need to
    335          *  override draw/update* methods
    336          *  since it provides draw/update*Prim
    337          *  methods except for some cases like
    338          *  internal texts or images.
    339          * ******************************** */
    340
    341         /* **************************
    342          *    Lines
    343          * **************************/
    344
    345         // documented in AbstractRenderer
    346         updateTicks: function (ticks) {
    347             var i, c, node, x, y,
    348                 tickStr = '',
    349                 len = ticks.ticks.length;
    350
    351             for (i = 0; i < len; i++) {
    352                 c = ticks.ticks[i];
    353                 x = c[0];
    354                 y = c[1];
    355
    356                 if (Type.isNumber(x[0]) && Type.isNumber(x[1])) {
    357                     tickStr += "M " + (x[0]) + " " + (y[0]) + " L " + (x[1]) + " " + (y[1]) + " ";
    358                 }
    359             }
    360
    361             node = ticks.rendNode;
    362
    363             if (!Type.exists(node)) {
    364                 node = this.createPrim('path', ticks.id);
    365                 this.appendChildPrim(node, Type.evaluate(ticks.visProp.layer));
    366                 ticks.rendNode = node;
    367             }
    368
    369             node.setAttributeNS(null, 'stroke', Type.evaluate(ticks.visProp.strokecolor));
    370             node.setAttributeNS(null, 'stroke-opacity', Type.evaluate(ticks.visProp.strokeopacity));
    371             node.setAttributeNS(null, 'stroke-width', Type.evaluate(ticks.visProp.strokewidth));
    372             this.updatePathPrim(node, tickStr, ticks.board);
    373         },
    374
    375         /* **************************
    376          *    Text related stuff
    377          * **************************/
    378
    379         // already documented in JXG.AbstractRenderer
    380         displayCopyright: function (str, fontsize) {
    381             var node = this.createPrim('text', 'licenseText'),
    382                 t;
    383             node.setAttributeNS(null, 'x', '20px');
    384             node.setAttributeNS(null, 'y', (2 + fontsize) + 'px');
    385             node.setAttributeNS(null, "style", "font-family:Arial,Helvetica,sans-serif; font-size:" + fontsize + "px; fill:#356AA0;  opacity:0.3;");
    386             t = this.container.ownerDocument.createTextNode(str);
    387             node.appendChild(t);
    388             this.appendChildPrim(node, 0);
    389         },
    390
    391         // already documented in JXG.AbstractRenderer
    392         drawInternalText: function (el) {
    393             var node = this.createPrim('text', el.id);
    394
    395             //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
    396             // Preserve spaces
    397             //node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve");
    398             node.style.whiteSpace = 'nowrap';
    399
    400             el.rendNodeText = this.container.ownerDocument.createTextNode('');
    401             node.appendChild(el.rendNodeText);
    402             this.appendChildPrim(node,  Type.evaluate(el.visProp.layer));
    403
    404             return node;
    405         },
    406
    407         // already documented in JXG.AbstractRenderer
    408         updateInternalText: function (el) {
    409             var content = el.plaintext, v,
    410                 ev_ax = el.getAnchorX(),
    411                 ev_ay = el.getAnchorY();
    412
    413             if (el.rendNode.getAttributeNS(null, "class") !== el.visProp.cssclass) {
    414                 el.rendNode.setAttributeNS(null, "class", Type.evaluate(el.visProp.cssclass));
    415                 el.needsSizeUpdate = true;
    416             }
    417
    418             if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
    419                 // Horizontal
    420                 v = el.coords.scrCoords[1];
    421                 if (el.visPropOld.left !== (ev_ax + v)) {
    422                     el.rendNode.setAttributeNS(null, 'x', v + 'px');
    423
    424                     if (ev_ax === 'left') {
    425                         el.rendNode.setAttributeNS(null, 'text-anchor', 'start');
    426                     } else if (ev_ax === 'right') {
    427                         el.rendNode.setAttributeNS(null, 'text-anchor', 'end');
    428                     } else if (ev_ax === 'middle') {
    429                         el.rendNode.setAttributeNS(null, 'text-anchor', 'middle');
    430                     }
    431                     el.visPropOld.left = ev_ax + v;
    432                 }
    433
    434                 // Vertical
    435                 v = el.coords.scrCoords[2];
    436                 if (el.visPropOld.top !== (ev_ay + v)) {
    437                     el.rendNode.setAttributeNS(null, 'y', (v + this.vOffsetText * 0.5) + 'px');
    438
    439                     if (ev_ay === 'bottom') {
    440                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-after-edge');
    441                     } else if (ev_ay === 'top') {
    442                         el.rendNode.setAttributeNS(null, 'dy', '1.6ex');
    443                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge'); // Not supported by IE, edge
    444                     } else if (ev_ay === 'middle') {
    445                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
    446                         el.rendNode.setAttributeNS(null, 'dy', '0.6ex');
    447                     }
    448                     el.visPropOld.top = ev_ay + v;
    449                 }
    450             }
    451             if (el.htmlStr !== content) {
    452                 el.rendNodeText.data = content;
    453                 el.htmlStr = content;
    454             }
    455             this.transformImage(el, el.transformations);
    456         },
    457
    458         /**
    459          * Set color and opacity of internal texts.
    460          * SVG needs its own version.
    461          * @private
    462          * @see JXG.AbstractRenderer#updateTextStyle
    463          * @see JXG.AbstractRenderer#updateInternalTextStyle
    464          */
    465         updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) {
    466             this.setObjectFillColor(el, strokeColor, strokeOpacity);
    467         },
    468
    469         /* **************************
    470          *    Image related stuff
    471          * **************************/
    472
    473         // already documented in JXG.AbstractRenderer
    474         drawImage: function (el) {
    475             var node = this.createPrim('image', el.id);
    476
    477             node.setAttributeNS(null, 'preserveAspectRatio', 'none');
    478             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
    479             el.rendNode = node;
    480
    481             this.updateImage(el);
    482         },
    483
    484         // already documented in JXG.AbstractRenderer
    485         transformImage: function (el, t) {
    486             var s, m,
    487                 node = el.rendNode,
    488                 str = "",
    489                 len = t.length;
    490
    491             if (len > 0) {
    492                 m = this.joinTransforms(el, t);
    493                 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(',');
    494                 str += ' matrix(' + s + ') ';
    495                 node.setAttributeNS(null, 'transform', str);
    496             }
    497         },
    498
    499         // already documented in JXG.AbstractRenderer
    500         updateImageURL: function (el) {
    501             var url = Type.evaluate(el.url);
    502
    503             el.rendNode.setAttributeNS(this.xlinkNamespace, 'xlink:href', url);
    504         },
    505
    506         // already documented in JXG.AbstractRenderer
    507         updateImageStyle: function (el, doHighlight) {
    508             var css = Type.evaluate(doHighlight ? el.visProp.highlightcssclass : el.visProp.cssclass);
    509
    510             el.rendNode.setAttributeNS(null, 'class', css);
    511         },
    512
    513         /* **************************
    514          * Render primitive objects
    515          * **************************/
    516
    517         // already documented in JXG.AbstractRenderer
    518         appendChildPrim: function (node, level) {
    519             if (!Type.exists(level)) { // trace nodes have level not set
    520                 level = 0;
    521             } else if (level >= Options.layer.numlayers) {
    522                 level = Options.layer.numlayers - 1;
    523             }
    524
    525             this.layer[level].appendChild(node);
    526
    527             return node;
    528         },
    529
    530         // already documented in JXG.AbstractRenderer
    531         createPrim: function (type, id) {
    532             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
    533             node.setAttributeNS(null, 'id', this.container.id + '_' + id);
    534             node.style.position = 'absolute';
    535             if (type === 'path') {
    536                 node.setAttributeNS(null, 'stroke-linecap', 'round');
    537                 node.setAttributeNS(null, 'stroke-linejoin', 'round');
    538             }
    539             return node;
    540         },
    541
    542         // already documented in JXG.AbstractRenderer
    543         remove: function (shape) {
    544             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
    545                 shape.parentNode.removeChild(shape);
    546             }
    547         },
    548
    549         // already documented in JXG.AbstractRenderer
    550         makeArrows: function (el) {
    551             var node2,
    552                 ev_fa = Type.evaluate(el.visProp.firstarrow),
    553                 ev_la = Type.evaluate(el.visProp.lastarrow);
    554
    555             if (el.visPropOld.firstarrow === ev_fa &&
    556                 el.visPropOld.lastarrow === ev_la) {
    557                 if (this.isIE && el.visPropCalc.visible &&
    558                     (ev_fa || ev_la)) {
    559                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
    560                 }
    561                 return;
    562             }
    563
    564             if (ev_fa) {
    565                 node2 = el.rendNodeTriangleStart;
    566                 if (!Type.exists(node2)) {
    567                     node2 = this._createArrowHead(el, 'End');
    568                     this.defs.appendChild(node2);
    569                     el.rendNodeTriangleStart = node2;
    570                     el.rendNode.setAttributeNS(null, 'marker-start', 'url(#' + this.container.id + '_' + el.id + 'TriangleEnd)');
    571                 } else {
    572                     this.defs.appendChild(node2);
    573                 }
    574             } else {
    575                 node2 = el.rendNodeTriangleStart;
    576                 if (Type.exists(node2)) {
    577                     this.remove(node2);
    578                 }
    579             }
    580             if (ev_la) {
    581                 node2 = el.rendNodeTriangleEnd;
    582                 if (!Type.exists(node2)) {
    583                     node2 = this._createArrowHead(el, 'Start');
    584                     this.defs.appendChild(node2);
    585                     el.rendNodeTriangleEnd = node2;
    586                     el.rendNode.setAttributeNS(null, 'marker-end', 'url(#' + this.container.id + '_' + el.id + 'TriangleStart)');
    587                 } else {
    588                     this.defs.appendChild(node2);
    589                 }
    590             } else {
    591                 node2 = el.rendNodeTriangleEnd;
    592                 if (Type.exists(node2)) {
    593                     this.remove(node2);
    594                 }
    595             }
    596             el.visPropOld.firstarrow = ev_fa;
    597             el.visPropOld.lastarrow = ev_la;
    598         },
    599
    600         // already documented in JXG.AbstractRenderer
    601         updateEllipsePrim: function (node, x, y, rx, ry) {
    602             var huge = 1000000;
    603
    604             huge = 200000; // IE
    605             // webkit does not like huge values if the object is dashed
    606             // iE doesn't like huge values above 216000
    607             x = Math.abs(x) < huge ? x : huge * x / Math.abs(x);
    608             y = Math.abs(y) < huge ? y : huge * y / Math.abs(y);
    609             rx = Math.abs(rx) < huge ? rx : huge * rx / Math.abs(rx);
    610             ry = Math.abs(ry) < huge ? ry : huge * ry / Math.abs(ry);
    611
    612             node.setAttributeNS(null, 'cx', x);
    613             node.setAttributeNS(null, 'cy', y);
    614             node.setAttributeNS(null, 'rx', Math.abs(rx));
    615             node.setAttributeNS(null, 'ry', Math.abs(ry));
    616         },
    617
    618         // already documented in JXG.AbstractRenderer
    619         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
    620             var huge = 1000000;
    621
    622             huge = 200000; //IE
    623             if (!isNaN(p1x + p1y + p2x + p2y)) {
    624                 // webkit does not like huge values if the object is dashed
    625                 // IE doesn't like huge values above 216000
    626                 p1x = Math.abs(p1x) < huge ? p1x : huge * p1x / Math.abs(p1x);
    627                 p1y = Math.abs(p1y) < huge ? p1y : huge * p1y / Math.abs(p1y);
    628                 p2x = Math.abs(p2x) < huge ? p2x : huge * p2x / Math.abs(p2x);
    629                 p2y = Math.abs(p2y) < huge ? p2y : huge * p2y / Math.abs(p2y);
    630
    631                 node.setAttributeNS(null, 'x1', p1x);
    632                 node.setAttributeNS(null, 'y1', p1y);
    633                 node.setAttributeNS(null, 'x2', p2x);
    634                 node.setAttributeNS(null, 'y2', p2y);
    635             }
    636         },
    637
    638         // already documented in JXG.AbstractRenderer
    639         updatePathPrim: function (node, pointString) {
    640             if (pointString === '') {
    641                 pointString = 'M 0 0';
    642             }
    643             node.setAttributeNS(null, 'd', pointString);
    644         },
    645
    646         // already documented in JXG.AbstractRenderer
    647         updatePathStringPoint: function (el, size, type) {
    648             var s = '',
    649                 scr = el.coords.scrCoords,
    650                 sqrt32 = size * Math.sqrt(3) * 0.5,
    651                 s05 = size * 0.5;
    652
    653             if (type === 'x') {
    654                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2] - size) +
    655                     ' L ' + (scr[1] + size) + ' ' + (scr[2] + size) +
    656                     ' M ' + (scr[1] + size) + ' ' + (scr[2] - size) +
    657                     ' L ' + (scr[1] - size) + ' ' + (scr[2] + size);
    658             } else if (type === '+') {
    659                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
    660                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
    661                     ' M ' + (scr[1])        + ' ' + (scr[2] - size) +
    662                     ' L ' + (scr[1])        + ' ' + (scr[2] + size);
    663             } else if (type === '<>') {
    664                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
    665                     ' L ' + (scr[1])        + ' ' + (scr[2] + size) +
    666                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
    667                     ' L ' + (scr[1])        + ' ' + (scr[2] - size) + ' Z ';
    668             } else if (type === '^') {
    669                 s = ' M ' + (scr[1])          + ' ' + (scr[2] - size) +
    670                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] + s05) +
    671                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] + s05) +
    672                     ' Z ';  // close path
    673             } else if (type === 'v') {
    674                 s = ' M ' + (scr[1])          + ' ' + (scr[2] + size) +
    675                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] - s05) +
    676                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] - s05) +
    677                     ' Z ';
    678             } else if (type === '>') {
    679                 s = ' M ' + (scr[1] + size) + ' ' + (scr[2]) +
    680                     ' L ' + (scr[1] - s05)  + ' ' + (scr[2] - sqrt32) +
    681                     ' L ' + (scr[1] - s05)  + ' ' + (scr[2] + sqrt32) +
    682                     ' Z ';
    683             } else if (type === '<') {
    684                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
    685                     ' L ' + (scr[1] + s05)  + ' ' + (scr[2] - sqrt32) +
    686                     ' L ' + (scr[1] + s05)  + ' ' + (scr[2] + sqrt32) +
    687                     ' Z ';
    688             }
    689             return s;
    690         },
    691
    692         // already documented in JXG.AbstractRenderer
    693         updatePathStringPrim: function (el) {
    694             var i, scr, len,
    695                 symbm = ' M ',
    696                 symbl = ' L ',
    697                 symbc = ' C ',
    698                 nextSymb = symbm,
    699                 maxSize = 5000.0,
    700                 pStr = '';
    701
    702             if (el.numberPoints <= 0) {
    703                 return '';
    704             }
    705
    706             len = Math.min(el.points.length, el.numberPoints);
    707
    708             if (el.bezierDegree === 1) {
    709                 for (i = 0; i < len; i++) {
    710                     scr = el.points[i].scrCoords;
    711                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
    712                         nextSymb = symbm;
    713                     } else {
    714                         // Chrome has problems with values being too far away.
    715                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
    716                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
    717
    718                         // Attention: first coordinate may be inaccurate if far way
    719                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
    720                         pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
    721                         nextSymb = symbl;
    722                     }
    723                 }
    724             } else if (el.bezierDegree === 3) {
    725                 i = 0;
    726                 while (i < len) {
    727                     scr = el.points[i].scrCoords;
    728                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
    729                         nextSymb = symbm;
    730                     } else {
    731                         pStr += nextSymb + scr[1] + ' ' + scr[2];
    732                         if (nextSymb === symbc) {
    733                             i += 1;
    734                             scr = el.points[i].scrCoords;
    735                             pStr += ' ' + scr[1] + ' ' + scr[2];
    736                             i += 1;
    737                             scr = el.points[i].scrCoords;
    738                             pStr += ' ' + scr[1] + ' ' + scr[2];
    739                         }
    740                         nextSymb = symbc;
    741                     }
    742                     i += 1;
    743                 }
    744             }
    745             return pStr;
    746         },
    747
    748         // already documented in JXG.AbstractRenderer
    749         updatePathStringBezierPrim: function (el) {
    750             var i, j, k, scr, lx, ly, len,
    751                 symbm = ' M ',
    752                 symbl = ' C ',
    753                 nextSymb = symbm,
    754                 maxSize = 5000.0,
    755                 pStr = '',
    756                 f = Type.evaluate(el.visProp.strokewidth),
    757                 isNoPlot = (Type.evaluate(el.visProp.curvetype) !== 'plot');
    758
    759             if (el.numberPoints <= 0) {
    760                 return '';
    761             }
    762
    763             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
    764                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
    765             }
    766
    767             len = Math.min(el.points.length, el.numberPoints);
    768             for (j = 1; j < 3; j++) {
    769                 nextSymb = symbm;
    770                 for (i = 0; i < len; i++) {
    771                     scr = el.points[i].scrCoords;
    772
    773                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
    774                         nextSymb = symbm;
    775                     } else {
    776                         // Chrome has problems with values being too far away.
    777                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
    778                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
    779
    780                         // Attention: first coordinate may be inaccurate if far way
    781                         if (nextSymb === symbm) {
    782                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
    783                             pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
    784                         } else {
    785                             k = 2 * j;
    786                             pStr += [nextSymb,
    787                                 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)), ' ',
    788                                 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)), ' ',
    789                                 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)), ' ',
    790                                 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)), ' ',
    791                                 scr[1], ' ', scr[2]].join('');
    792                         }
    793
    794                         nextSymb = symbl;
    795                         lx = scr[1];
    796                         ly = scr[2];
    797                     }
    798                 }
    799             }
    800             return pStr;
    801         },
    802
    803         // already documented in JXG.AbstractRenderer
    804         updatePolygonPrim: function (node, el) {
    805             var i,
    806                 pStr = '',
    807                 scrCoords,
    808                 len = el.vertices.length;
    809
    810             node.setAttributeNS(null, 'stroke', 'none');
    811
    812             for (i = 0; i < len - 1; i++) {
    813                 if (el.vertices[i].isReal) {
    814                     scrCoords = el.vertices[i].coords.scrCoords;
    815                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
    816                 } else {
    817                     node.setAttributeNS(null, 'points', '');
    818                     return;
    819                 }
    820
    821                 if (i < len - 2) {
    822                     pStr += " ";
    823                 }
    824             }
    825             if (pStr.indexOf('NaN') === -1) {
    826                 node.setAttributeNS(null, 'points', pStr);
    827             }
    828         },
    829
    830         // already documented in JXG.AbstractRenderer
    831         updateRectPrim: function (node, x, y, w, h) {
    832             node.setAttributeNS(null, 'x', x);
    833             node.setAttributeNS(null, 'y', y);
    834             node.setAttributeNS(null, 'width', w);
    835             node.setAttributeNS(null, 'height', h);
    836         },
    837
    838         /* **************************
    839          *  Set Attributes
    840          * **************************/
    841
    842         // documented in JXG.AbstractRenderer
    843         setPropertyPrim: function (node, key, val) {
    844             if (key === 'stroked') {
    845                 return;
    846             }
    847             node.setAttributeNS(null, key, val);
    848         },
    849
    850         display: function(el, val) {
    851             var node;
    852
    853             if (el && el.rendNode) {
    854                 el.visPropOld.visible = val;
    855                 node = el.rendNode;
    856                 if (val) {
    857                     node.setAttributeNS(null, 'display', 'inline');
    858                     node.style.visibility = "inherit";
    859                 } else {
    860                     node.setAttributeNS(null, 'display', 'none');
    861                     node.style.visibility = "hidden";
    862                 }
    863             }
    864         },
    865
    866         // documented in JXG.AbstractRenderer
    867         show: function (el) {
    868             JXG.deprecated('Board.renderer.show()', 'Board.renderer.display()');
    869             this.display(el, true);
    870             // var node;
    871             //
    872             // if (el && el.rendNode) {
    873             //     node = el.rendNode;
    874             //     node.setAttributeNS(null, 'display', 'inline');
    875             //     node.style.visibility = "inherit";
    876             // }
    877         },
    878
    879         // documented in JXG.AbstractRenderer
    880         hide: function (el) {
    881             JXG.deprecated('Board.renderer.hide()', 'Board.renderer.display()');
    882             this.display(el, false);
    883             // var node;
    884             //
    885             // if (el && el.rendNode) {
    886             //     node = el.rendNode;
    887             //     node.setAttributeNS(null, 'display', 'none');
    888             //     node.style.visibility = "hidden";
    889             // }
    890         },
    891
    892         // documented in JXG.AbstractRenderer
    893         setBuffering: function (el, type) {
    894             el.rendNode.setAttribute('buffered-rendering', type);
    895         },
    896
    897         // documented in JXG.AbstractRenderer
    898         setDashStyle: function (el) {
    899             var dashStyle = Type.evaluate(el.visProp.dash),
    900                 node = el.rendNode;
    901
    902             if (dashStyle > 0) {
    903                 node.setAttributeNS(null, 'stroke-dasharray', this.dashArray[dashStyle - 1]);
    904             } else {
    905                 if (node.hasAttributeNS(null, 'stroke-dasharray')) {
    906                     node.removeAttributeNS(null, 'stroke-dasharray');
    907                 }
    908             }
    909         },
    910
    911         // documented in JXG.AbstractRenderer
    912         setGradient: function (el) {
    913             var fillNode = el.rendNode, col, op,
    914                 node, node2, node3, x1, x2, y1, y2,
    915                 ev_g = Type.evaluate(el.visProp.gradient);
    916
    917             op = Type.evaluate(el.visProp.fillopacity);
    918             op = (op > 0) ? op : 0;
    919             col = Type.evaluate(el.visProp.fillcolor);
    920
    921             if (ev_g === 'linear') {
    922                 node = this.createPrim('linearGradient', el.id + '_gradient');
    923                 x1 = '0%';
    924                 x2 = '100%';
    925                 y1 = '0%';
    926                 y2 = '0%';
    927
    928                 node.setAttributeNS(null, 'x1', x1);
    929                 node.setAttributeNS(null, 'x2', x2);
    930                 node.setAttributeNS(null, 'y1', y1);
    931                 node.setAttributeNS(null, 'y2', y2);
    932                 node2 = this.createPrim('stop', el.id + '_gradient1');
    933                 node2.setAttributeNS(null, 'offset', '0%');
    934                 node2.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
    935                 node3 = this.createPrim('stop', el.id + '_gradient2');
    936                 node3.setAttributeNS(null, 'offset', '100%');
    937                 node3.setAttributeNS(null, 'style', 'stop-color:' + Type.evaluate(el.visProp.gradientsecondcolor) +
    938                             ';stop-opacity:' + Type.evaluate(el.visProp.gradientsecondopacity));
    939                 node.appendChild(node2);
    940                 node.appendChild(node3);
    941                 this.defs.appendChild(node);
    942                 fillNode.setAttributeNS(null, 'style', 'fill:url(#' + this.container.id + '_' + el.id + '_gradient)');
    943                 el.gradNode1 = node2;
    944                 el.gradNode2 = node3;
    945             } else if (ev_g === 'radial') {
    946                 node = this.createPrim('radialGradient', el.id + '_gradient');
    947
    948                 node.setAttributeNS(null, 'cx', '50%');
    949                 node.setAttributeNS(null, 'cy', '50%');
    950                 node.setAttributeNS(null, 'r', '50%');
    951                 node.setAttributeNS(null, 'fx', Type.evaluate(el.visProp.gradientpositionx) * 100 + '%');
    952                 node.setAttributeNS(null, 'fy', Type.evaluate(el.visProp.gradientpositiony) * 100 + '%');
    953
    954                 node2 = this.createPrim('stop', el.id + '_gradient1');
    955                 node2.setAttributeNS(null, 'offset', '0%');
    956                 node2.setAttributeNS(null, 'style', 'stop-color:' + Type.evaluate(el.visProp.gradientsecondcolor) +
    957                                 ';stop-opacity:' + Type.evaluate(el.visProp.gradientsecondopacity));
    958                 node3 = this.createPrim('stop', el.id + '_gradient2');
    959                 node3.setAttributeNS(null, 'offset', '100%');
    960                 node3.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
    961
    962                 node.appendChild(node2);
    963                 node.appendChild(node3);
    964                 this.defs.appendChild(node);
    965                 fillNode.setAttributeNS(null, 'style', 'fill:url(#' + this.container.id + '_' + el.id + '_gradient)');
    966                 el.gradNode1 = node2;
    967                 el.gradNode2 = node3;
    968             } else {
    969                 fillNode.removeAttributeNS(null, 'style');
    970             }
    971         },
    972
    973         // documented in JXG.AbstractRenderer
    974         updateGradient: function (el) {
    975             var col, op,
    976                 node2 = el.gradNode1,
    977                 node3 = el.gradNode2,
    978                 ev_g = Type.evaluate(el.visProp.gradient);
    979
    980             if (!Type.exists(node2) || !Type.exists(node3)) {
    981                 return;
    982             }
    983
    984             op = Type.evaluate(el.visProp.fillopacity);
    985             op = (op > 0) ? op : 0;
    986             col = Type.evaluate(el.visProp.fillcolor);
    987
    988             if (ev_g === 'linear') {
    989                 node2.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
    990                 node3.setAttributeNS(null, 'style', 'stop-color:' + Type.evaluate(el.visProp.gradientsecondcolor) +
    991                         ';stop-opacity:' + Type.evaluate(el.visProp.gradientsecondopacity));
    992             } else if (ev_g === 'radial') {
    993                 node2.setAttributeNS(null, 'style', 'stop-color:' + Type.evaluate(el.visProp.gradientsecondcolor) +
    994                         ';stop-opacity:' + Type.evaluate(el.visProp.gradientsecondopacity));
    995                 node3.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
    996             }
    997         },
    998
    999         // documented in JXG.AbstractRenderer
    1000         setObjectTransition: function (el, duration) {
    1001             var node, transitionStr,
    1002                 i, len,
    1003                 nodes = ['rendNode',
    1004                          'rendNodeTriangleStart',
    1005                          'rendNodeTriangleEnd'];
    1006
    1007             if (duration === undefined) {
    1008                 duration = Type.evaluate(el.visProp.transitionduration);
    1009             }
    1010
    1011             if (duration === el.visPropOld.transitionduration) {
    1012                 return;
    1013             }
    1014
    1015             if (el.elementClass === Const.OBJECT_CLASS_TEXT &&
    1016                 Type.evaluate(el.visProp.display) === 'html') {
    1017                 transitionStr = ' color ' + duration + 'ms,' +
    1018                             ' opacity ' + duration + 'ms';
    1019             } else {
    1020                 transitionStr = ' fill ' + duration + 'ms,' +
    1021                             ' fill-opacity ' + duration + 'ms,' +
    1022                             ' stroke ' + duration + 'ms,' +
    1023                             ' stroke-opacity ' + duration + 'ms';
    1024             }
    1025
    1026             len = nodes.length;
    1027             for (i = 0; i < len; ++i) {
    1028                 if (el[nodes[i]]) {
    1029                     node = el[nodes[i]];
    1030                     node.style.transition = transitionStr;
    1031                 }
    1032             }
    1033
    1034             el.visPropOld.transitionduration = duration;
    1035         },
    1036
    1037         /**
    1038          * Call user-defined function to set visual attributes.
    1039          * If "testAttribute" is the empty string, the function
    1040          * is called immediately, otherwise it is called in a timeOut.
    1041          *
    1042          * This is necessary to realize smooth transitions buit avoid transistions
    1043          * when first creating the objects.
    1044          *
    1045          * Usually, the string in testAttribute is the visPropOld attribute
    1046          * of the values which are set.
    1047          *
    1048          * @param {Function} setFunc       Some function which usually sets some attributes
    1049          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
    1050          *                               otherwise it is called in a setImeout.
    1051          * @see JXG.SVGRenderer#setObjectFillColor
    1052          * @see JXG.SVGRenderer#setObjectStrokeColor
    1053          * @see JXG.SVGRenderer#_setArrowColor
    1054          * @private
    1055          */
    1056         _setAttribute: function(setFunc, testAttribute) {
    1057             if (testAttribute === '') {
    1058                 setFunc();
    1059             } else {
    1060                 setTimeout(setFunc, 1);
    1061             }
    1062         },
    1063
    1064         // documented in JXG.AbstractRenderer
    1065         setObjectFillColor: function (el, color, opacity, rendNode) {
    1066             var node, c, rgbo, oo, t,
    1067                 rgba = Type.evaluate(color),
    1068                 o = Type.evaluate(opacity);
    1069
    1070             o = (o > 0) ? o : 0;
    1071
    1072             if (el.visPropOld.fillcolor === rgba && el.visPropOld.fillopacity === o) {
    1073                 return;
    1074             }
    1075
    1076             if (Type.exists(rgba) && rgba !== false) {
    1077                 if (rgba.length !== 9) {          // RGB, not RGBA
    1078                     c = rgba;
    1079                     oo = o;
    1080                 } else {                       // True RGBA, not RGB
    1081                     rgbo = Color.rgba2rgbo(rgba);
    1082                     c = rgbo[0];
    1083                     oo = o * rgbo[1];
    1084                 }
    1085
    1086                 if (rendNode === undefined) {
    1087                     node = el.rendNode;
    1088                 } else {
    1089                     node = rendNode;
    1090                 }
    1091
    1092                 if (c !== 'none') {
    1093                     this._setAttribute(function() {
    1094                             node.setAttributeNS(null, 'fill', c);
    1095                         }, el.visPropOld.fillcolor);
    1096                 }
    1097
    1098                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
    1099                     this._setAttribute(function() {
    1100                             node.setAttributeNS(null, 'opacity', oo);
    1101                         }, el.visPropOld.fillopacity);
    1102                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
    1103                 } else {
    1104                     if (c === 'none') {  // This is done only for non-images
    1105                                          // because images have no fill color.
    1106                         oo = 0;
    1107                     }
    1108                     this._setAttribute(function() {
    1109                             node.setAttributeNS(null, 'fill-opacity', oo);
    1110                         }, el.visPropOld.fillopacity);
    1111                 }
    1112
    1113                 if (Type.exists(el.visProp.gradient)) {
    1114                     this.updateGradient(el);
    1115                 }
    1116             }
    1117             el.visPropOld.fillcolor = rgba;
    1118             el.visPropOld.fillopacity = o;
    1119         },
    1120
    1121         // documented in JXG.AbstractRenderer
    1122         setObjectStrokeColor: function (el, color, opacity) {
    1123             var rgba = Type.evaluate(color), c, rgbo,
    1124                 o = Type.evaluate(opacity), oo,
    1125                 node;
    1126
    1127             o = (o > 0) ? o : 0;
    1128
    1129             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
    1130                 return;
    1131             }
    1132
    1133             if (Type.exists(rgba) && rgba !== false) {
    1134                 if (rgba.length !== 9) {          // RGB, not RGBA
    1135                     c = rgba;
    1136                     oo = o;
    1137                 } else {                       // True RGBA, not RGB
    1138                     rgbo = Color.rgba2rgbo(rgba);
    1139                     c = rgbo[0];
    1140                     oo = o * rgbo[1];
    1141                 }
    1142
    1143                 node = el.rendNode;
    1144
    1145                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
    1146                     if (Type.evaluate(el.visProp.display) === 'html') {
    1147                         this._setAttribute(function() {
    1148                                 node.style.color = c;
    1149                                 node.style.opacity = oo;
    1150                             }, el.visPropOld.strokecolor);
    1151
    1152                     } else {
    1153                         this._setAttribute(function() {
    1154                                 node.setAttributeNS(null, "style", "fill:" + c);
    1155                                 node.setAttributeNS(null, "style", "fill-opacity:" + oo);
    1156                             }, el.visPropOld.strokecolor);
    1157                     }
    1158                 } else {
    1159                     this._setAttribute(function() {
    1160                             node.setAttributeNS(null, 'stroke', c);
    1161                             node.setAttributeNS(null, 'stroke-opacity', oo);
    1162                         }, el.visPropOld.strokecolor);
    1163                 }
    1164
    1165                 if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
    1166                     el.elementClass === Const.OBJECT_CLASS_LINE) {
    1167                     if (Type.evaluate(el.visProp.firstarrow)) {
    1168                         this._setArrowColor(el.rendNodeTriangleStart, c, oo, el);
    1169                     }
    1170
    1171                     if (Type.evaluate(el.visProp.lastarrow)) {
    1172                         this._setArrowColor(el.rendNodeTriangleEnd, c, oo, el);
    1173                     }
    1174                 }
    1175             }
    1176
    1177             el.visPropOld.strokecolor = rgba;
    1178             el.visPropOld.strokeopacity = o;
    1179         },
    1180
    1181         // documented in JXG.AbstractRenderer
    1182         setObjectStrokeWidth: function (el, width) {
    1183             var node,
    1184                 w = Type.evaluate(width),
    1185                 rgba, c, rgbo, o, oo;
    1186
    1187             if (isNaN(w) || el.visPropOld.strokewidth === w) {
    1188                 return;
    1189             }
    1190
    1191             node = el.rendNode;
    1192             this.setPropertyPrim(node, 'stroked', 'true');
    1193             if (Type.exists(w)) {
    1194                 this.setPropertyPrim(node, 'stroke-width', w + 'px');
    1195
    1196                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
    1197                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
    1198                 //     if (Type.evaluate(el.visProp.firstarrow)) {
    1199                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
    1200                 //     }
    1201                 //
    1202                 //     if (Type.evaluate(el.visProp.lastarrow)) {
    1203                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
    1204                 //     }
    1205                 // }
    1206              }
    1207             el.visPropOld.strokewidth = w;
    1208         },
    1209
    1210         // documented in JXG.AbstractRenderer
    1211         setLineCap: function (el) {
    1212             var capStyle = Type.evaluate(el.visProp.linecap);
    1213
    1214             if (capStyle === undefined || capStyle === '' || el.visPropOld.linecap === capStyle ||
    1215                 !Type.exists(el.rendNode)) {
    1216                 return;
    1217             }
    1218
    1219             this.setPropertyPrim(el.rendNode, 'stroke-linecap', capStyle);
    1220             el.visPropOld.linecap = capStyle;
    1221
    1222         },
    1223
    1224         // documented in JXG.AbstractRenderer
    1225         setShadow: function (el) {
    1226             var ev_s = Type.evaluate(el.visProp.shadow);
    1227             if (el.visPropOld.shadow === ev_s) {
    1228                 return;
    1229             }
    1230
    1231             if (Type.exists(el.rendNode)) {
    1232                 if (ev_s) {
    1233                     el.rendNode.setAttributeNS(null, 'filter', 'url(#' + this.container.id + '_' + 'f1)');
    1234                 } else {
    1235                     el.rendNode.removeAttributeNS(null, 'filter');
    1236                 }
    1237             }
    1238             el.visPropOld.shadow = ev_s;
    1239         },
    1240
    1241         /* **************************
    1242          * renderer control
    1243          * **************************/
    1244
    1245         // documented in JXG.AbstractRenderer
    1246         suspendRedraw: function () {
    1247             // It seems to be important for the Linux version of firefox
    1248             //this.suspendHandle = this.svgRoot.suspendRedraw(10000);
    1249         },
    1250
    1251         // documented in JXG.AbstractRenderer
    1252         unsuspendRedraw: function () {
    1253             //this.svgRoot.unsuspendRedraw(this.suspendHandle);
    1254             //this.svgRoot.unsuspendRedrawAll();
    1255             //this.svgRoot.forceRedraw();
    1256         },
    1257
    1258         // documented in AbstractRenderer
    1259         resize: function (w, h) {
    1260             this.svgRoot.style.width = parseFloat(w) + 'px';
    1261             this.svgRoot.style.height = parseFloat(h) + 'px';
    1262             this.svgRoot.setAttribute("width", parseFloat(w));
    1263             this.svgRoot.setAttribute("height", parseFloat(h));
    1264         },
    1265
    1266         // documented in JXG.AbstractRenderer
    1267         createTouchpoints: function (n) {
    1268             var i, na1, na2, node;
    1269             this.touchpoints = [];
    1270             for (i = 0; i < n; i++) {
    1271                 na1 = 'touchpoint1_' + i;
    1272                 node = this.createPrim('path', na1);
    1273                 this.appendChildPrim(node, 19);
    1274                 node.setAttributeNS(null, 'd', 'M 0 0');
    1275                 this.touchpoints.push(node);
    1276
    1277                 this.setPropertyPrim(node, 'stroked', 'true');
    1278                 this.setPropertyPrim(node, 'stroke-width', '1px');
    1279                 node.setAttributeNS(null, 'stroke', '#000000');
    1280                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
    1281                 node.setAttributeNS(null, 'display', 'none');
    1282
    1283                 na2 = 'touchpoint2_' + i;
    1284                 node = this.createPrim('ellipse', na2);
    1285                 this.appendChildPrim(node, 19);
    1286                 this.updateEllipsePrim(node, 0, 0, 0, 0);
    1287                 this.touchpoints.push(node);
    1288
    1289                 this.setPropertyPrim(node, 'stroked', 'true');
    1290                 this.setPropertyPrim(node, 'stroke-width', '1px');
    1291                 node.setAttributeNS(null, 'stroke', '#000000');
    1292                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
    1293                 node.setAttributeNS(null, 'fill', '#ffffff');
    1294                 node.setAttributeNS(null, 'fill-opacity', 0.0);
    1295
    1296                 node.setAttributeNS(null, 'display', 'none');
    1297             }
    1298         },
    1299
    1300         // documented in JXG.AbstractRenderer
    1301         showTouchpoint: function (i) {
    1302             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
    1303                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'inline');
    1304                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'inline');
    1305             }
    1306         },
    1307
    1308         // documented in JXG.AbstractRenderer
    1309         hideTouchpoint: function (i) {
    1310             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
    1311                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'none');
    1312                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'none');
    1313             }
    1314         },
    1315
    1316         // documented in JXG.AbstractRenderer
    1317         updateTouchpoint: function (i, pos) {
    1318             var x, y,
    1319                 d = 37;
    1320
    1321             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
    1322                 x = pos[0];
    1323                 y = pos[1];
    1324
    1325                 this.touchpoints[2 * i].setAttributeNS(null, 'd', 'M ' + (x - d) + ' ' + y + ' ' +
    1326                     'L ' + (x + d) + ' ' + y + ' ' +
    1327                     'M ' + x + ' ' + (y - d) + ' ' +
    1328                     'L ' + x + ' ' + (y + d));
    1329                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
    1330             }
    1331         },
    1332
    1333         /**
    1334          * Convert the SVG construction into an HTML canvas image.
    1335          * This works for all SVG supporting browsers.
    1336          * For IE it works from version 9, with the execption that HTML texts
    1337          * are ignored on IE. The drawing is done with a delay of
    1338          * 200 ms. Otherwise there would be problems with IE.
    1339          *
    1340          *
    1341          * @param {String} canvasId Id of an HTML canvas element
    1342          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
    1343          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
    1344          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
    1345          * This is necessary for Safari. Default: false
    1346          * @returns {Object}          the svg renderer object.
    1347          *
    1348          * @example
    1349          * 	board.renderer.dumpToCanvas('canvas');
    1350          */
    1351         dumpToCanvas: function(canvasId, w, h, ignoreTexts) {
    1352             var svgRoot = this.svgRoot,
    1353                 btoa = window.btoa || Base64.encode,
    1354                 svg, tmpImg, cv, ctx,
    1355                 wOrg, hOrg,
    1356                 // uriPayload,
    1357                 // DOMURL, svgBlob, url,
    1358                 virtualNode, doc;
    1359
    1360             // Move all HTML tags (beside the SVG root) of the container
    1361             // to the foreignObject element inside of the svgRoot node
    1362             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
    1363                 while (svgRoot.nextSibling) {
    1364                     this.foreignObjLayer.appendChild(svgRoot.nextSibling);
    1365                 }
    1366                 if (ignoreTexts === true) {
    1367                     doc = this.container.ownerDocument;
    1368                     virtualNode = doc.createElement('div');
    1369                     virtualNode.appendChild(this.foreignObjLayer);
    1370                 }
    1371             }
    1372
    1373             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    1374             wOrg = svgRoot.getAttribute('width');
    1375             hOrg = svgRoot.getAttribute('height');
    1376
    1377             svg = new XMLSerializer().serializeToString(svgRoot);
    1378
    1379             if (false) {
    1380                 // Debug: example svg image
    1381                 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>';
    1382             }
    1383
    1384             // In IE we have to remove the namespace again.
    1385             if ((svg.match(/xmlns=\"http:\/\/www.w3.org\/2000\/svg\"/g) || []).length > 1) {
    1386                 svg = svg.replace(/xmlns=\"http:\/\/www.w3.org\/2000\/svg\"/g, '');
    1387             }
    1388
    1389             // Safari fails if the svg string contains a " "
    1390             svg = svg.replace(/ /g, ' ');
    1391
    1392             cv = document.getElementById(canvasId);
    1393             // Clear the canvas
    1394             cv.width = cv.width;
    1395
    1396             ctx = cv.getContext("2d");
    1397             if (w !== undefined && h !== undefined) {
    1398                 // Scale twice the CSS size to make the image crisp
    1399                 cv.style.width = parseFloat(w) + 'px';
    1400                 cv.style.height = parseFloat(h) + 'px';
    1401                 cv.setAttribute('width',  2 * parseFloat(wOrg));
    1402                 cv.setAttribute('height', 2 * parseFloat(hOrg));
    1403                 ctx.scale(2 * wOrg / w, 2 * hOrg / h);
    1404             }
    1405
    1406             tmpImg = new Image();
    1407             if (true) {
    1408                 tmpImg.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
    1409                 // uriPayload = encodeURIComponent(svg.replace(/\n+/g, '')) // remove newlines // encode URL-unsafe characters
    1410                 //             .replace(/%20/g, ' ') // put spaces back in
    1411                 //             .replace(/%3D/g, '=') // ditto equals signs
    1412                 //             .replace(/%3A/g, ':') // ditto colons
    1413                 //             .replace(/%2F/g, '/') // ditto slashes
    1414                 //             .replace(/%22/g, "'");
    1415                 // tmpImg.src = 'data:image/svg+xml,' + uriPayload;
    1416
    1417                 tmpImg.onload = function () {
    1418                     // IE needs a pause...
    1419                     setTimeout(function(){
    1420                         ctx.drawImage(tmpImg, 0, 0, w, h);
    1421                     }, 200);
    1422                 };
    1423             } else {
    1424                 // // Alternative version
    1425                 // DOMURL = window.URL || window.webkitURL || window;
    1426                 // svgBlob = new Blob([svg], {type: 'image/svg+xml'});
    1427                 // url = DOMURL.createObjectURL(svgBlob);
    1428                 // tmpImg.src = url;
    1429                 //
    1430                 // tmpImg.onload = function () {
    1431                 //     // IE needs a pause...
    1432                 //     setTimeout(function(){
    1433                 //         ctx.drawImage(tmpImg, 0, 0, w, h);
    1434                 //     }, 200);
    1435                 //     DOMURL.revokeObjectURL(url);
    1436                 // };
    1437             }
    1438
    1439             // Move all HTML tags back from
    1440             // the foreignObject element to the container
    1441             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
    1442                 if (ignoreTexts === true) {
    1443                     svgRoot.appendChild(this.foreignObjLayer);
    1444                 }
    1445                 while (this.foreignObjLayer.firstChild) {
    1446                     this.container.appendChild(this.foreignObjLayer.firstChild);
    1447                 }
    1448             }
    1449
    1450             return this;
    1451         },
    1452
    1453         /**
    1454          * Display SVG image in html img-tag which enables
    1455          * easy download for the user.
    1456          *
    1457          * Support:
    1458          * <ul>
    1459          * <li> IE: No
    1460          * <li> Edge: full
    1461          * <li>Firefox: full
    1462          * <li> Chrome: full
    1463          * <li> Safari: supported, but no texts (to be precise, no foreignObject-element is allowed in SVG)
    1464          * </ul>
    1465          *
    1466          * @param {JXG.Board} board Link to the board.
    1467          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
    1468          * the screenshot is copied to this img object. The width and height will be set to the values of the
    1469          * JSXGraph container.
    1470          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
    1471          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
    1472          * @return {Object}       the svg renderer object
    1473          */
    1474         screenshot: function(board, imgId, ignoreTexts) {
    1475             var node,
    1476                 doc = this.container.ownerDocument,
    1477                 parent = this.container.parentNode,
    1478                 cPos,
    1479                 canvas, id,
    1480                 img,
    1481                 button, buttonText,
    1482                 w, h,
    1483                 bas = board.attr.screenshot,
    1484                 zbar, zbarDisplay, cssTxt,
    1485                 newImg = false,
    1486                 isDebug = false;
    1487
    1488             if (this.type === 'no') {
    1489                 return this;
    1490             }
    1491
    1492             w = bas.scale * parseFloat(this.container.style.width);
    1493             h = bas.scale * parseFloat(this.container.style.height);
    1494
    1495             if (imgId === undefined || imgId === '') {
    1496                 newImg = true;
    1497                 img = new Image(); //doc.createElement('img');
    1498                 img.style.width = w + 'px';
    1499                 img.style.height = h + 'px';
    1500             } else {
    1501                 newImg = false;
    1502                 img = doc.getElementById(imgId);
    1503             }
    1504             // img.crossOrigin = 'anonymous';
    1505
    1506             // Create div which contains canvas element and close button
    1507             if (newImg) {
    1508                 node = doc.createElement('div');
    1509                 node.style.cssText = bas.css;
    1510                 node.style.width = (w) + 'px';
    1511                 node.style.height = (h) + 'px';
    1512                 node.style.zIndex = this.container.style.zIndex + 120;
    1513
    1514                 // Position the div exactly over the JSXGraph board
    1515                 cPos = board.getCoordsTopLeftCorner();
    1516                 node.style.position= 'absolute';
    1517                 node.style.left = (cPos[0]) + 'px';
    1518                 node.style.top = (cPos[1]) + 'px';
    1519             }
    1520
    1521             if (!isDebug) {
    1522                 // Create canvas element and add it to the DOM
    1523                 // It will be removed after the image has been stored.
    1524                 canvas = doc.createElement('canvas');
    1525                 id = Math.random().toString(36).substr(2, 5);
    1526                 canvas.setAttribute('id', id);
    1527                 canvas.setAttribute('width', w);
    1528                 canvas.setAttribute('height', h);
    1529                 canvas.style.width = w + 'px';
    1530                 canvas.style.height = w + 'px';
    1531                 canvas.style.display = 'none';
    1532                 parent.append(canvas);
    1533             } else {
    1534                 // Debug: use canvas element
    1535                 // 'jxgbox_canvas' from jsxdev/dump.html
    1536                 id = 'jxgbox_canvas';
    1537                 canvas = document.getElementById(id);
    1538             }
    1539
    1540             if (newImg) {
    1541                 // Create close button
    1542                 button = doc.createElement('span');
    1543                 buttonText = doc.createTextNode('\u2716');
    1544                 button.style.cssText = bas.cssButton;
    1545                 button.appendChild(buttonText);
    1546                 button.onclick = function() {
    1547                     node.parentNode.removeChild(node);
    1548                 };
    1549
    1550                 // Add all nodes
    1551                 node.appendChild(img);
    1552                 node.appendChild(button);
    1553                 parent.appendChild(node);
    1554             }
    1555
    1556             // Hide navigation bar in board
    1557             zbar = document.getElementById(this.container.id + '_navigationbar');
    1558             if (Type.exists(zbar)) {
    1559                 zbarDisplay = zbar.style.display;
    1560                 zbar.style.display = 'none';
    1561             }
    1562
    1563             // Create screenshot in canvas
    1564             this.dumpToCanvas(id, w, h, ignoreTexts);
    1565
    1566             // Show image in img tag
    1567             setTimeout(function() {
    1568                 //console.log(canvas.toDataURL('image/png'));
    1569                 img.src = canvas.toDataURL('image/png');
    1570
    1571                 // Remove canvas node
    1572                 if (!isDebug) {
    1573                     parent.removeChild(canvas);
    1574                 }
    1575             }, 400);
    1576
    1577             // Show navigation bar in board
    1578             if (Type.exists(zbar)) {
    1579                 zbar.style.display = zbarDisplay;
    1580             }
    1581
    1582             return this;
    1583         }
    1584
    1585     });
    1586
    1587     return JXG.SVGRenderer;
    1588 });
    1589