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