1 /*
  2     Copyright 2008-2021
  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 
 33 /*global JXG: true, define: true, AMprocessNode: true, document: true, Image: true, module: true, require: true */
 34 /*jslint nomen: true, plusplus: true, newcap:true*/
 35 
 36 /* depends:
 37  jxg
 38  renderer/abstract
 39  base/constants
 40  utils/env
 41  utils/type
 42  utils/uuid
 43  utils/color
 44  base/coords
 45  math/math
 46  math/geometry
 47  math/numerics
 48 */
 49 
 50 define([
 51     'jxg', 'renderer/abstract', 'base/constants', 'utils/env', 'utils/type', 'utils/uuid', 'utils/color',
 52     'base/coords', 'math/math', 'math/geometry', 'math/numerics'
 53 ], function (JXG, AbstractRenderer, Const, Env, Type, UUID, Color, Coords, Mat, Geometry, Numerics) {
 54 
 55     "use strict";
 56 
 57     /**
 58      * Uses HTML Canvas to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
 59      * 
 60      * @class JXG.CanvasRenderer
 61      * @augments JXG.AbstractRenderer
 62      * @param {Node} container Reference to a DOM node containing the board.
 63      * @param {Object} dim The dimensions of the board
 64      * @param {Number} dim.width
 65      * @param {Number} dim.height
 66      * @see JXG.AbstractRenderer
 67      */
 68     JXG.CanvasRenderer = function (container, dim) {
 69         this.type = 'canvas';
 70 
 71         this.canvasRoot = null;
 72         this.suspendHandle = null;
 73         this.canvasId = UUID.genUUID();
 74 
 75         this.canvasNamespace = null;
 76 
 77         if (Env.isBrowser) {
 78             this.container = container;
 79             this.container.style.MozUserSelect = 'none';
 80             this.container.style.userSelect = 'none';
 81 
 82             this.container.style.overflow = 'hidden';
 83             if (this.container.style.position === '') {
 84                 this.container.style.position = 'relative';
 85             }
 86 
 87             this.container.innerHTML = ['<canvas id="', this.canvasId,
 88                 '" width="', dim.width,
 89                 'px" height="', dim.height,
 90                 'px"><', '/canvas>'].join('');
 91             this.canvasRoot = this.container.ownerDocument.getElementById(this.canvasId);
 92             this.context =  this.canvasRoot.getContext('2d');
 93             this.canvasRoot.style.display = 'block';
 94 
 95         } else if (Env.isNode()) {
 96             this.canvasId = (typeof module === 'object' ? module.require('canvas') : require('canvas'));
 97             this.canvasRoot = new this.canvasId(500, 500);
 98             this.context = this.canvasRoot.getContext('2d');
 99         }
100 
101         this.dashArray = [[2, 2], [5, 5], [10, 10], [20, 20], [20, 10, 10, 10], [20, 5, 10, 5]];
102     };
103 
104     JXG.CanvasRenderer.prototype = new AbstractRenderer();
105 
106     JXG.extend(JXG.CanvasRenderer.prototype, /** @lends JXG.CanvasRenderer.prototype */ {
107 
108         /* **************************
109          *   private methods only used
110          *   in this renderer. Should
111          *   not be called from outside.
112          * **************************/
113 
114         /**
115          * Draws a filled polygon.
116          * @param {Array} shape A matrix presented by a two dimensional array of numbers.
117          * @see JXG.AbstractRenderer#drawArrows
118          * @private
119          */
120         _drawPolygon: function (shape, degree, doFill) {
121             var i, len = shape.length,
122                 context = this.context;
123 
124             if (len > 0) {
125                 if (doFill) {
126                     context.lineWidth = 0;
127                 }
128                 context.beginPath();
129                 context.moveTo(shape[0][0], shape[0][1]);
130                 if (degree === 1) {
131                     for (i = 1; i < len; i++) {
132                         context.lineTo(shape[i][0], shape[i][1]);
133                     }
134                 } else {
135                     for (i = 1; i < len; i += 3) {
136                         context.bezierCurveTo(shape[i][0], shape[i][1], shape[i + 1][0], shape[i + 1][1], shape[i + 2][0], shape[i + 2][1]);
137                     }
138                 }
139                 if (doFill) {
140                     context.lineTo(shape[0][0], shape[0][1]);
141                     context.closePath();
142                     context.fill();
143                 } else {
144                     context.stroke();
145                 }
146             }
147         },
148 
149         /**
150          * Sets the fill color and fills an area.
151          * @param {JXG.GeometryElement} el An arbitrary JSXGraph element, preferably one with an area.
152          * @private
153          */
154         _fill: function (el) {
155             var context = this.context;
156 
157             context.save();
158             if (this._setColor(el, 'fill')) {
159                 context.fill();
160             }
161             context.restore();
162         },
163 
164         /**
165          * Rotates a point around <tt>(0, 0)</tt> by a given angle.
166          * @param {Number} angle An angle, given in rad.
167          * @param {Number} x X coordinate of the point.
168          * @param {Number} y Y coordinate of the point.
169          * @returns {Array} An array containing the x and y coordinate of the rotated point.
170          * @private
171          */
172         _rotatePoint: function (angle, x, y) {
173             return [
174                 (x * Math.cos(angle)) - (y * Math.sin(angle)),
175                 (x * Math.sin(angle)) + (y * Math.cos(angle))
176             ];
177         },
178 
179         /**
180          * Rotates an array of points around <tt>(0, 0)</tt>.
181          * @param {Array} shape An array of array of point coordinates.
182          * @param {Number} angle The angle in rad the points are rotated by.
183          * @returns {Array} Array of array of two dimensional point coordinates.
184          * @private
185          */
186         _rotateShape: function (shape, angle) {
187             var i, rv = [], len = shape.length;
188 
189             if (len <= 0) {
190                 return shape;
191             }
192 
193             for (i = 0; i < len; i++) {
194                 rv.push(this._rotatePoint(angle, shape[i][0], shape[i][1]));
195             }
196 
197             return rv;
198         },
199 
200         /**
201          * Set the gradient angle for linear color gradients.
202          *
203          * @private
204          * @param {JXG.GeometryElement} node An arbitrary JSXGraph element, preferably one with an area.
205          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
206          */
207         updateGradientAngle: function(el, radians) {
208             // Angles:
209             // 0: ->
210             // 90: down
211             // 180: <-
212             // 90: up
213             var f = 1.0,
214                 co = Math.cos(-radians),
215                 si = Math.sin(-radians),
216                 bb = el.getBoundingBox(),
217                 c1, c2, x1, x2, y1, y2, x1s, x2s, y1s, y2s, dx, dy;
218 
219             if (Math.abs(co) > Math.abs(si)) {
220                 f /= Math.abs(co);
221             } else {
222                 f /= Math.abs(si);
223             }
224             if (co >= 0) {
225                 x1 = 0;
226                 x2 = co * f;
227             } else {
228                 x1 = -co * f;
229                 x2 = 0;
230             }
231             if (si >= 0) {
232                 y1 = 0;
233                 y2 = si * f;
234             } else {
235                 y1 = -si * f;
236                 y2 = 0;
237             }
238 
239             c1 = new Coords(Const.COORDS_BY_USER, [bb[0], bb[1]], el.board);
240             c2 = new Coords(Const.COORDS_BY_USER, [bb[2], bb[3]], el.board);
241             dx = c2.scrCoords[1] - c1.scrCoords[1];
242             dy = c2.scrCoords[2] - c1.scrCoords[2];
243             x1s = c1.scrCoords[1] + dx * x1;
244             y1s = c1.scrCoords[2] + dy * y1;
245             x2s = c1.scrCoords[1] + dx * x2;
246             y2s = c1.scrCoords[2] + dy * y2;
247 
248             return this.context.createLinearGradient(x1s, y1s, x2s, y2s);
249         },
250 
251         /**
252          * Set circles for radial color gradients.
253          *
254          * @private
255          * @param {SVGnode} node SVG gradient node
256          * @param {Number} cx Canvas value x1 (but value between 0 and 1)
257          * @param {Number} cy  Canvas value y1 (but value between 0 and 1)
258          * @param {Number} r  Canvas value r1 (but value between 0 and 1)
259          * @param {Number} fx  Canvas value x0 (but value between 0 and 1)
260          * @param {Number} fy  Canvas value x1 (but value between 0 and 1)
261          * @param {Number} fr  Canvas value r0 (but value between 0 and 1)
262          */
263         updateGradientCircle: function(el, cx, cy, r, fx, fy, fr) {
264             var bb = el.getBoundingBox(),
265                 c1, c2, cxs, cys, rs, fxs, fys, frs, dx, dy;
266 
267             c1 = new Coords(Const.COORDS_BY_USER, [bb[0], bb[1]], el.board);
268             c2 = new Coords(Const.COORDS_BY_USER, [bb[2], bb[3]], el.board);
269             dx = c2.scrCoords[1] - c1.scrCoords[1];
270             dy = c1.scrCoords[2] - c2.scrCoords[2];
271 
272             cxs = c1.scrCoords[1] + dx * cx;
273             cys = c2.scrCoords[2] + dy * cy;
274             fxs = c1.scrCoords[1] + dx * fx;
275             fys = c2.scrCoords[2] + dy * fy;
276             rs = r * (dx + dy) * 0.5;
277             frs = fr * (dx + dy) * 0.5;
278 
279             return this.context.createRadialGradient(fxs, fys, frs, cxs, cys, rs);
280         },
281 
282         // documented in JXG.AbstractRenderer
283         updateGradient: function(el) {
284             var col, op,
285                 ev_g = Type.evaluate(el.visProp.gradient),
286                 gradient;
287 
288             op = Type.evaluate(el.visProp.fillopacity);
289             op = (op > 0) ? op : 0;
290             col = Type.evaluate(el.visProp.fillcolor);
291 
292             if (ev_g === 'linear') {
293                 gradient = this.updateGradientAngle(el, Type.evaluate(el.visProp.gradientangle));
294             } else if (ev_g === 'radial') {
295                 gradient = this.updateGradientCircle(el,
296                     Type.evaluate(el.visProp.gradientcx),
297                     Type.evaluate(el.visProp.gradientcy),
298                     Type.evaluate(el.visProp.gradientr),
299                     Type.evaluate(el.visProp.gradientfx),
300                     Type.evaluate(el.visProp.gradientfy),
301                     Type.evaluate(el.visProp.gradientfr)
302                 );
303             }
304             gradient.addColorStop(Type.evaluate(el.visProp.gradientstartoffset), col);
305             gradient.addColorStop(Type.evaluate(el.visProp.gradientendoffset),
306                                   Type.evaluate(el.visProp.gradientsecondcolor));
307         return gradient;
308         },
309 
310         /**
311          * Sets color and opacity for filling and stroking.
312          * type is the attribute from visProp and targetType the context[targetTypeStyle].
313          * This is necessary, because the fill style of a text is set by the stroke attributes of the text element.
314          * @param {JXG.GeometryElement} el Any JSXGraph element.
315          * @param {String} [type='stroke'] Either <em>fill</em> or <em>stroke</em>.
316          * @param {String} [targetType=type] (optional) Either <em>fill</em> or <em>stroke</em>.
317          * @returns {Boolean} If the color could be set, <tt>true</tt> is returned.
318          * @private
319          */
320         _setColor: function (el, type, targetType) {
321             var hasColor = true,
322                 ev = el.visProp, hl, sw,
323                 rgba, rgbo, c, o, oo,
324                 grad;
325 
326             type = type || 'stroke';
327             targetType = targetType || type;
328 
329             hl = this._getHighlighted(el);
330 
331             grad = Type.evaluate(el.visProp.gradient);
332             if (grad === 'linear' || grad === 'radial') {
333                 // TODO: opacity
334                 this.context[targetType + 'Style'] = this.updateGradient(el);
335                 return hasColor;
336             }
337 
338             // type is equal to 'fill' or 'stroke'
339             rgba = Type.evaluate(ev[hl + type + 'color']);
340             if (rgba !== 'none' && rgba !== false) {
341                 o = Type.evaluate(ev[hl + type + 'opacity']);
342                 o = (o > 0) ? o : 0;
343 
344                 // RGB, not RGBA
345                 if (rgba.length !== 9) {
346                     c = rgba;
347                     oo = o;
348                 // True RGBA, not RGB
349                 } else {
350                     rgbo = Color.rgba2rgbo(rgba);
351                     c = rgbo[0];
352                     oo = o * rgbo[1];
353                 }
354                 this.context.globalAlpha = oo;
355 
356                 this.context[targetType + 'Style'] = c;
357 
358             } else {
359                 hasColor = false;
360             }
361 
362             sw = parseFloat(Type.evaluate(ev[hl + 'strokewidth']));
363             if (type === 'stroke' && !isNaN(sw)) {
364                 if (sw === 0) {
365                     this.context.globalAlpha = 0;
366                 } else {
367                     this.context.lineWidth = sw;
368                 }
369             }
370 
371             if (type === 'stroke' && ev.linecap !== undefined && ev.linecap !== '') {
372                 this.context.lineCap = ev.linecap;
373             }
374 
375             return hasColor;
376         },
377 
378         /**
379          * Sets color and opacity for drawing paths and lines and draws the paths and lines.
380          * @param {JXG.GeometryElement} el An JSXGraph element with a stroke.
381          * @private
382          */
383         _stroke: function (el) {
384             var context = this.context,
385                 ev_dash = Type.evaluate(el.visProp.dash);
386 
387             context.save();
388 
389             if (ev_dash > 0) {
390                 if (context.setLineDash) {
391                     context.setLineDash(this.dashArray[ev_dash]);
392                 }
393             } else {
394                 this.context.lineDashArray = [];
395             }
396 
397             if (this._setColor(el, 'stroke')) {
398                 context.stroke();
399             }
400 
401             context.restore();
402         },
403 
404         /**
405          * Translates a set of points.
406          * @param {Array} shape An array of point coordinates.
407          * @param {Number} x Translation in X direction.
408          * @param {Number} y Translation in Y direction.
409          * @returns {Array} An array of translated point coordinates.
410          * @private
411          */
412         _translateShape: function (shape, x, y) {
413             var i, rv = [], len = shape.length;
414 
415             if (len <= 0) {
416                 return shape;
417             }
418 
419             for (i = 0; i < len; i++) {
420                 rv.push([ shape[i][0] + x, shape[i][1] + y ]);
421             }
422 
423             return rv;
424         },
425 
426         /* ******************************** *
427          *    Point drawing and updating    *
428          * ******************************** */
429 
430         // documented in AbstractRenderer
431         drawPoint: function (el) {
432             var f = Type.evaluate(el.visProp.face),
433                 size = Type.evaluate(el.visProp.size),
434                 scr = el.coords.scrCoords,
435                 sqrt32 = size * Math.sqrt(3) * 0.5,
436                 s05 = size * 0.5,
437                 stroke05 = parseFloat(Type.evaluate(el.visProp.strokewidth)) / 2.0,
438                 context = this.context;
439 
440             if (!el.visPropCalc.visible) {
441                 return;
442             }
443 
444             switch (f) {
445             case 'cross':  // x
446             case 'x':
447                 context.beginPath();
448                 context.moveTo(scr[1] - size, scr[2] - size);
449                 context.lineTo(scr[1] + size, scr[2] + size);
450                 context.moveTo(scr[1] + size, scr[2] - size);
451                 context.lineTo(scr[1] - size, scr[2] + size);
452                 context.lineCap = 'round';
453                 context.lineJoin = 'round';
454                 context.closePath();
455                 this._stroke(el);
456                 break;
457             case 'circle': // dot
458             case 'o':
459                 context.beginPath();
460                 context.arc(scr[1], scr[2], size + 1 + stroke05, 0, 2 * Math.PI, false);
461                 context.closePath();
462                 this._fill(el);
463                 this._stroke(el);
464                 break;
465             case 'square':  // rectangle
466             case '[]':
467                 if (size <= 0) {
468                     break;
469                 }
470 
471                 context.save();
472                 if (this._setColor(el, 'stroke', 'fill')) {
473                     context.fillRect(scr[1] - size - stroke05, scr[2] - size - stroke05, size * 2 + 3 * stroke05, size * 2 + 3 * stroke05);
474                 }
475                 context.restore();
476                 context.save();
477                 this._setColor(el, 'fill');
478                 context.fillRect(scr[1] - size + stroke05, scr[2] - size + stroke05, size * 2 - stroke05, size * 2 - stroke05);
479                 context.restore();
480                 break;
481             case 'plus':  // +
482             case '+':
483                 context.beginPath();
484                 context.moveTo(scr[1] - size, scr[2]);
485                 context.lineTo(scr[1] + size, scr[2]);
486                 context.moveTo(scr[1], scr[2] - size);
487                 context.lineTo(scr[1], scr[2] + size);
488                 context.lineCap = 'round';
489                 context.lineJoin = 'round';
490                 context.closePath();
491                 this._stroke(el);
492                 break;
493             case 'diamond':   // <>
494             case '<>':
495                 context.beginPath();
496                 context.moveTo(scr[1] - size, scr[2]);
497                 context.lineTo(scr[1], scr[2] + size);
498                 context.lineTo(scr[1] + size, scr[2]);
499                 context.lineTo(scr[1], scr[2] - size);
500                 context.closePath();
501                 this._fill(el);
502                 this._stroke(el);
503                 break;
504             case 'triangleup':
505             case 'a':
506             case '^':
507                 context.beginPath();
508                 context.moveTo(scr[1], scr[2] - size);
509                 context.lineTo(scr[1] - sqrt32, scr[2] + s05);
510                 context.lineTo(scr[1] + sqrt32, scr[2] + s05);
511                 context.closePath();
512                 this._fill(el);
513                 this._stroke(el);
514                 break;
515             case 'triangledown':
516             case 'v':
517                 context.beginPath();
518                 context.moveTo(scr[1], scr[2] + size);
519                 context.lineTo(scr[1] - sqrt32, scr[2] - s05);
520                 context.lineTo(scr[1] + sqrt32, scr[2] - s05);
521                 context.closePath();
522                 this._fill(el);
523                 this._stroke(el);
524                 break;
525             case 'triangleleft':
526             case '<':
527                 context.beginPath();
528                 context.moveTo(scr[1] - size, scr[2]);
529                 context.lineTo(scr[1] + s05, scr[2] - sqrt32);
530                 context.lineTo(scr[1] + s05, scr[2] + sqrt32);
531                 context.closePath();
532                 this._fill(el);
533                 this._stroke(el);
534                 break;
535             case 'triangleright':
536             case '>':
537                 context.beginPath();
538                 context.moveTo(scr[1] + size, scr[2]);
539                 context.lineTo(scr[1] - s05, scr[2] - sqrt32);
540                 context.lineTo(scr[1] - s05, scr[2] + sqrt32);
541                 context.closePath();
542                 this._fill(el);
543                 this._stroke(el);
544                 break;
545             }
546         },
547 
548         // documented in AbstractRenderer
549         updatePoint: function (el) {
550             this.drawPoint(el);
551         },
552 
553         /* ******************************** *
554          *           Lines                  *
555          * ******************************** */
556 
557         /**
558          * Draws arrows of an element (usually a line) in canvas renderer.
559          * @param {JXG.GeometryElement} el Line to be drawn.
560          * @param {Array} scr1 Screen coordinates of the start position of the line or curve.
561          * @param {Array} scr2 Screen coordinates of the end position of the line or curve.
562          * @param {String} hl String which carries information if the element is highlighted. Used for getting the correct attribute.
563          * @private
564          */
565         drawArrows: function (el, scr1, scr2, hl, a) {
566              var x1, y1, x2, y2,
567                  w0, w,
568                  arrowHead,
569                  arrowTail,
570                  context = this.context,
571                  size = 6,
572                  type = 1,
573                  type_fa, type_la,
574                  degree_fa = 1,
575                  degree_la = 1,
576                  doFill,
577                  i, len,
578                  d1x, d1y, d2x, d2y, last,
579                  ang1, ang2,
580                  ev_fa = a.evFirst,
581                  ev_la = a.evLast;
582 
583             if (Type.evaluate(el.visProp.strokecolor) !== 'none' &&
584                      (ev_fa || ev_la)) {
585 
586                 if (el.elementClass === Const.OBJECT_CLASS_LINE) {
587                     x1 = scr1.scrCoords[1];
588                     y1 = scr1.scrCoords[2];
589                     x2 = scr2.scrCoords[1];
590                     y2 = scr2.scrCoords[2];
591                     ang1 = ang2 = Math.atan2(y2 - y1, x2 - x1);
592                 } else {
593                     x1 = el.points[0].scrCoords[1];
594                     y1 = el.points[0].scrCoords[2];
595 
596                     last = el.points.length - 1;
597                     if (last < 1) {
598                         // No arrows for curves consisting of 1 point
599                         return;
600                     }
601                     x2 = el.points[el.points.length - 1].scrCoords[1];
602                     y2 = el.points[el.points.length - 1].scrCoords[2];
603 
604                     d1x = el.points[1].scrCoords[1] - el.points[0].scrCoords[1];
605                     d1y = el.points[1].scrCoords[2] - el.points[0].scrCoords[2];
606                     d2x = el.points[last].scrCoords[1] - el.points[last - 1].scrCoords[1];
607                     d2y = el.points[last].scrCoords[2] - el.points[last - 1].scrCoords[2];
608                     if (ev_fa) {
609                         ang1 = Math.atan2(d1y, d1x);
610                     }
611                     if (ev_la) {
612                         ang2 = Math.atan2(d2y, d2x);
613                     }
614                 }
615 
616                 w0 = Type.evaluate(el.visProp[hl + 'strokewidth']);
617 
618                 if (ev_fa) {
619                     size = a.sizeFirst;
620 
621                     w = w0 * size;
622 
623                     type = a.typeFirst;
624                     type_fa = type;
625 
626                     if (type === 2) {
627                         arrowTail = [
628                                  [ w,      -w * 0.5],
629                                  [ 0.0,         0.0],
630                                  [ w,       w * 0.5],
631                                  [ w * 0.5,     0.0],
632                              ];
633                     } else if (type === 3) {
634                         arrowTail = [
635                                  [ w / 3.0,   -w * 0.5],
636                                  [ 0.0,       -w * 0.5],
637                                  [ 0.0,        w * 0.5],
638                                  [ w / 3.0,    w * 0.5]
639                              ];
640                     } else if (type === 4) {
641                         w /= 10;
642                         degree_fa = 3;
643                         arrowTail = [
644                             [10.00, 3.31],
645                             [6.47, 3.84],
646                             [2.87, 4.50],
647                             [0.00, 6.63],
648                             [0.67, 5.52],
649                             [1.33, 4.42],
650                             [2.00, 3.31],
651                             [1.33, 2.21],
652                             [0.67, 1.10],
653                             [0.00, 0.00],
654                             [2.87, 2.13],
655                             [6.47, 2.79],
656                             [10.00, 3.31]
657                         ];
658                         len = arrowTail.length;
659                         for (i = 0; i < len; i++) {
660                             arrowTail[i][0] *= -w;
661                             arrowTail[i][1] *= w;
662                             arrowTail[i][0] += 10 * w;
663                             arrowTail[i][1] -= 3.31 * w;
664                         }
665                     } else if (type === 5) {
666                         w /= 10;
667                         degree_fa = 3;
668                         arrowTail = [
669                             [10.00,3.28],
670                             [6.61,4.19],
671                             [3.19,5.07],
672                             [0.00,6.55],
673                             [0.62,5.56],
674                             [1.00,4.44],
675                             [1.00,3.28],
676                             [1.00,2.11],
677                             [0.62,0.99],
678                             [0.00,0.00],
679                             [3.19,1.49],
680                             [6.61,2.37],
681                             [10.00,3.28]
682                         ];
683                         len = arrowTail.length;
684                         for (i = 0; i < len; i++) {
685                             arrowTail[i][0] *= -w;
686                             arrowTail[i][1] *= w;
687                             arrowTail[i][0] += 10 * w;
688                             arrowTail[i][1] -= 3.28 * w;
689                         }
690                     } else if (type === 6) {
691                         w /= 10;
692                         degree_fa = 3;
693                         arrowTail = [
694                             [10.00,2.84],
695                             [6.61,3.59],
696                             [3.21,4.35],
697                             [0.00,5.68],
698                             [0.33,4.73],
699                             [0.67,3.78],
700                             [1.00,2.84],
701                             [0.67,1.89],
702                             [0.33,0.95],
703                             [0.00,0.00],
704                             [3.21,1.33],
705                             [6.61,2.09],
706                             [10.00,2.84]
707                         ];
708                         len = arrowTail.length;
709                         for (i = 0; i < len; i++) {
710                             arrowTail[i][0] *= -w;
711                             arrowTail[i][1] *= w;
712                             arrowTail[i][0] += 10 * w;
713                             arrowTail[i][1] -= 2.84 * w;
714                         }
715                     } else if (type === 7) {
716                         w = w0;
717                         degree_fa = 3;
718                         arrowTail = [
719                             [0.00,10.39],
720                             [2.01,6.92],
721                             [5.96,5.20],
722                             [10.00,5.20],
723                             [5.96,5.20],
724                             [2.01,3.47],
725                             [0.00,0.00]
726                         ];
727                         len = arrowTail.length;
728                         for (i = 0; i < len; i++) {
729                             arrowTail[i][0] *= -w;
730                             arrowTail[i][1] *= w;
731                             arrowTail[i][0] += 10 * w;
732                             arrowTail[i][1] -= 5.20 * w;
733                         }
734                     } else {
735                         arrowTail = [
736                              [ w,   -w * 0.5],
737                              [ 0.0,      0.0],
738                              [ w,    w * 0.5]
739                         ];
740                     }
741                 }
742 
743                 if (ev_la) {
744                     size = a.sizeLast;
745                     w = w0 * size;
746 
747                     type = a.typeLast;
748                     type_la = type;
749                     if (type === 2) {
750                         arrowHead = [
751                              [ -w, -w * 0.5],
752                              [ 0.0,     0.0],
753                              [ -w,  w * 0.5],
754                              [ -w * 0.5, 0.0]
755                         ];
756                     } else if (type === 3) {
757                         arrowHead = [
758                                  [-w / 3.0,   -w * 0.5],
759                                  [ 0.0,       -w * 0.5],
760                                  [ 0.0,        w * 0.5],
761                                  [-w / 3.0,    w * 0.5]
762                              ];
763                     } else if (type === 4) {
764                         w /= 10;
765                         degree_la = 3;
766                         arrowHead = [
767                             [10.00, 3.31],
768                             [6.47, 3.84],
769                             [2.87, 4.50],
770                             [0.00, 6.63],
771                             [0.67, 5.52],
772                             [1.33, 4.42],
773                             [2.00, 3.31],
774                             [1.33, 2.21],
775                             [0.67, 1.10],
776                             [0.00, 0.00],
777                             [2.87, 2.13],
778                             [6.47, 2.79],
779                             [10.00, 3.31]
780                         ];
781                         len = arrowHead.length;
782                         for (i = 0; i < len; i++) {
783                             arrowHead[i][0] *= w;
784                             arrowHead[i][1] *= w;
785                             arrowHead[i][0] -= 10 * w;
786                             arrowHead[i][1] -= 3.31 * w;
787 
788                         }
789                     } else if (type === 5) {
790                         w /= 10;
791                         degree_la = 3;
792                         arrowHead = [
793                             [10.00,3.28],
794                             [6.61,4.19],
795                             [3.19,5.07],
796                             [0.00,6.55],
797                             [0.62,5.56],
798                             [1.00,4.44],
799                             [1.00,3.28],
800                             [1.00,2.11],
801                             [0.62,0.99],
802                             [0.00,0.00],
803                             [3.19,1.49],
804                             [6.61,2.37],
805                             [10.00,3.28]
806                         ];
807                         len = arrowHead.length;
808                         for (i = 0; i < len; i++) {
809                             arrowHead[i][0] *= w;
810                             arrowHead[i][1] *= w;
811                             arrowHead[i][0] -= 10 * w;
812                             arrowHead[i][1] -= 3.28 * w;
813 
814                         }
815                     } else if (type === 6) {
816                         w /= 10;
817                         degree_la = 3;
818                         arrowHead = [
819                             [10.00,2.84],
820                             [6.61,3.59],
821                             [3.21,4.35],
822                             [0.00,5.68],
823                             [0.33,4.73],
824                             [0.67,3.78],
825                             [1.00,2.84],
826                             [0.67,1.89],
827                             [0.33,0.95],
828                             [0.00,0.00],
829                             [3.21,1.33],
830                             [6.61,2.09],
831                             [10.00,2.84]
832                         ];
833                         len = arrowHead.length;
834                         for (i = 0; i < len; i++) {
835                             arrowHead[i][0] *= w;
836                             arrowHead[i][1] *= w;
837                             arrowHead[i][0] -= 10 * w;
838                             arrowHead[i][1] -= 2.84 * w;
839 
840                         }
841 
842                     } else if (type === 7) {
843                         w = w0;
844                         degree_la = 3;
845                         arrowHead = [
846                             [0.00,10.39],
847                             [2.01,6.92],
848                             [5.96,5.20],
849                             [10.00,5.20],
850                             [5.96,5.20],
851                             [2.01,3.47],
852                             [0.00,0.00]
853                         ];
854                         len = arrowHead.length;
855                         for (i = 0; i < len; i++) {
856                             arrowHead[i][0] *= w;
857                             arrowHead[i][1] *= w;
858                             arrowHead[i][0] -= 10 * w;
859                             arrowHead[i][1] -= 5.20 * w;
860 
861                         }
862                     } else {
863                         arrowHead = [
864                              [ -w, -w * 0.5],
865                              [ 0.0,     0.0],
866                              [ -w,  w * 0.5]
867                          ];
868                     }
869                 }
870 
871                 context.save();
872                 if (this._setColor(el, 'stroke', 'fill')) {
873                     this._setColor(el, 'stroke');
874                     if (ev_fa) {
875                         if (type_fa === 7) {
876                             doFill = false;
877                         } else {
878                             doFill = true;
879                         }
880                         this._drawPolygon(this._translateShape(this._rotateShape(arrowTail, ang1), x1, y1), degree_fa, doFill);
881                     }
882                     if (ev_la) {
883                         if (type_la === 7) {
884                             doFill = false;
885                         } else {
886                             doFill = true;
887                         }
888                         this._drawPolygon(this._translateShape(this._rotateShape(arrowHead, ang2), x2, y2), degree_la, doFill);
889                     }
890                 }
891                 context.restore();
892             }
893         },
894 
895         // documented in AbstractRenderer
896         drawLine: function (el) {
897             var c1_org, c2_org,
898                 c1 = new Coords(Const.COORDS_BY_USER, el.point1.coords.usrCoords, el.board),
899                 c2 = new Coords(Const.COORDS_BY_USER, el.point2.coords.usrCoords, el.board),
900                 margin = null,
901                 hl, w, arrowData;
902 
903             if (!el.visPropCalc.visible) {
904                 return;
905             }
906 
907             hl = this._getHighlighted(el);
908             w = Type.evaluate(el.visProp[hl + 'strokewidth']);
909             arrowData = this.getArrowHeadData(el, w, hl);
910 
911             if (arrowData.evFirst || arrowData.evLast) {
912                 margin = -4;
913             }
914             Geometry.calcStraight(el, c1, c2, margin);
915             this.handleTouchpoints(el, c1, c2, arrowData);
916 
917             c1_org = new Coords(Const.COORDS_BY_USER, c1.usrCoords, el.board);
918             c2_org = new Coords(Const.COORDS_BY_USER, c2.usrCoords, el.board);
919 
920             this.getPositionArrowHead(el, c1, c2, arrowData);
921 
922             this.context.beginPath();
923             this.context.moveTo(c1.scrCoords[1], c1.scrCoords[2]);
924             this.context.lineTo(c2.scrCoords[1], c2.scrCoords[2]);
925             this._stroke(el);
926 
927             if ((arrowData.evFirst/* && obj.sFirst > 0*/) ||
928                 (arrowData.evLast/* && obj.sLast > 0*/)) {
929 
930                 this.drawArrows(el, c1_org, c2_org, hl, arrowData);
931             }
932         },
933 
934         // documented in AbstractRenderer
935         updateLine: function (el) {
936             this.drawLine(el);
937         },
938 
939         // documented in AbstractRenderer
940         drawTicks: function () {
941             // this function is supposed to initialize the svg/vml nodes in the SVG/VMLRenderer.
942             // but in canvas there are no such nodes, hence we just do nothing and wait until
943             // updateTicks is called.
944         },
945 
946         // documented in AbstractRenderer
947         updateTicks: function (ticks) {
948             var i, c, x, y,
949                 len = ticks.ticks.length,
950                 len2, j,
951                 context = this.context;
952 
953             context.beginPath();
954             for (i = 0; i < len; i++) {
955                 c = ticks.ticks[i];
956                 x = c[0];
957                 y = c[1];
958 
959                 // context.moveTo(x[0], y[0]);
960                 // context.lineTo(x[1], y[1]);
961                 len2 = x.length;
962                 context.moveTo(x[0], y[0]);
963                 for (j = 1; j < len2; ++j) {
964                     context.lineTo(x[j], y[j]);
965                 }
966 
967             }
968             // Labels
969             // for (i = 0; i < len; i++) {
970             //     c = ticks.ticks[i].scrCoords;
971             //     if (ticks.ticks[i].major &&
972             //             (ticks.board.needsFullUpdate || ticks.needsRegularUpdate) &&
973             //             ticks.labels[i] &&
974             //             ticks.labels[i].visPropCalc.visible) {
975             //         this.updateText(ticks.labels[i]);
976             //     }
977             // }
978             context.lineCap = 'round';
979             this._stroke(ticks);
980         },
981 
982         /* **************************
983          *    Curves
984          * **************************/
985 
986         // documented in AbstractRenderer
987         drawCurve: function (el) {
988             var hl, w, arrowData;
989 
990             if (Type.evaluate(el.visProp.handdrawing)) {
991                 this.updatePathStringBezierPrim(el);
992             } else {
993                 this.updatePathStringPrim(el);
994             }
995             if (el.numberPoints > 1) {
996                 hl = this._getHighlighted(el);
997                 w = Type.evaluate(el.visProp[hl + 'strokewidth']);
998                 arrowData = this.getArrowHeadData(el, w, hl);
999                 if ((arrowData.evFirst/* && obj.sFirst > 0*/) ||
1000                     (arrowData.evLast/* && obj.sLast > 0*/)) {
1001                     this.drawArrows(el, null, null, hl, arrowData);
1002                 }
1003             }
1004         },
1005 
1006         // documented in AbstractRenderer
1007         updateCurve: function (el) {
1008             this.drawCurve(el);
1009         },
1010 
1011         /* **************************
1012          *    Circle related stuff
1013          * **************************/
1014 
1015         // documented in AbstractRenderer
1016         drawEllipse: function (el) {
1017             var m1 = el.center.coords.scrCoords[1],
1018                 m2 = el.center.coords.scrCoords[2],
1019                 sX = el.board.unitX,
1020                 sY = el.board.unitY,
1021                 rX = 2 * el.Radius(),
1022                 rY = 2 * el.Radius(),
1023                 aWidth = rX * sX,
1024                 aHeight = rY * sY,
1025                 aX = m1 - aWidth / 2,
1026                 aY = m2 - aHeight / 2,
1027                 hB = (aWidth / 2) * 0.5522848,
1028                 vB = (aHeight / 2) * 0.5522848,
1029                 eX = aX + aWidth,
1030                 eY = aY + aHeight,
1031                 mX = aX + aWidth / 2,
1032                 mY = aY + aHeight / 2,
1033                 context = this.context;
1034 
1035             if (rX > 0.0 && rY > 0.0 && !isNaN(m1 + m2)) {
1036                 context.beginPath();
1037                 context.moveTo(aX, mY);
1038                 context.bezierCurveTo(aX, mY - vB, mX - hB, aY, mX, aY);
1039                 context.bezierCurveTo(mX + hB, aY, eX, mY - vB, eX, mY);
1040                 context.bezierCurveTo(eX, mY + vB, mX + hB, eY, mX, eY);
1041                 context.bezierCurveTo(mX - hB, eY, aX, mY + vB, aX, mY);
1042                 context.closePath();
1043                 this._fill(el);
1044                 this._stroke(el);
1045             }
1046         },
1047 
1048         // documented in AbstractRenderer
1049         updateEllipse: function (el) {
1050             return this.drawEllipse(el);
1051         },
1052 
1053         /* **************************
1054          *    Polygon
1055          * **************************/
1056 
1057         // nothing here, using AbstractRenderer implementations
1058 
1059         /* **************************
1060          *    Text related stuff
1061          * **************************/
1062 
1063         // Already documented in JXG.AbstractRenderer
1064         displayCopyright: function (str, fontSize) {
1065             var context = this.context;
1066 
1067             // this should be called on EVERY update, otherwise it won't be shown after the first update
1068             context.save();
1069             context.font = fontSize + 'px Arial';
1070             context.fillStyle = '#aaa';
1071             context.lineWidth = 0.5;
1072             context.fillText(str, 10, 2 + fontSize);
1073             context.restore();
1074         },
1075 
1076         // Already documented in JXG.AbstractRenderer
1077         drawInternalText: function (el) {
1078             var ev_fs = Type.evaluate(el.visProp.fontsize),
1079                 fontUnit = Type.evaluate(el.visProp.fontunit),
1080                 ev_ax = el.getAnchorX(),
1081                 ev_ay = el.getAnchorY(),
1082                 context = this.context;
1083 
1084             context.save();
1085             if (this._setColor(el, 'stroke', 'fill') &&
1086                     !isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
1087                 context.font = (ev_fs > 0 ? ev_fs : 0) + fontUnit + ' Arial';
1088 
1089                 this.transformImage(el, el.transformations);
1090                 if (ev_ax === 'left') {
1091                     context.textAlign = 'left';
1092                 } else if (ev_ax === 'right') {
1093                     context.textAlign = 'right';
1094                 } else if (ev_ax === 'middle') {
1095                     context.textAlign = 'center';
1096                 }
1097                 if (ev_ay === 'bottom') {
1098                     context.textBaseline = 'bottom';
1099                 } else if (ev_ay === 'top') {
1100                     context.textBaseline = 'top';
1101                 } else if (ev_ay === 'middle') {
1102                     context.textBaseline = 'middle';
1103                 }
1104                 context.fillText(el.plaintext, el.coords.scrCoords[1], el.coords.scrCoords[2]);
1105             }
1106             context.restore();
1107             return null;
1108         },
1109 
1110         // Already documented in JXG.AbstractRenderer
1111         updateInternalText: function (el) {
1112             this.drawInternalText(el);
1113         },
1114 
1115         // documented in JXG.AbstractRenderer
1116         // Only necessary for texts
1117         setObjectStrokeColor: function (el, color, opacity) {
1118             var rgba = Type.evaluate(color), c, rgbo,
1119                 o = Type.evaluate(opacity), oo,
1120                 node;
1121 
1122             o = (o > 0) ? o : 0;
1123 
1124             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
1125                 return;
1126             }
1127 
1128             // Check if this could be merged with _setColor
1129 
1130             if (Type.exists(rgba) && rgba !== false) {
1131                 // RGB, not RGBA
1132                 if (rgba.length !== 9) {
1133                     c = rgba;
1134                     oo = o;
1135                 // True RGBA, not RGB
1136                 } else {
1137                     rgbo = Color.rgba2rgbo(rgba);
1138                     c = rgbo[0];
1139                     oo = o * rgbo[1];
1140                 }
1141                 node = el.rendNode;
1142                 if (el.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(el.visProp.display) === 'html') {
1143                     node.style.color = c;
1144                     node.style.opacity = oo;
1145                 }
1146             }
1147 
1148             el.visPropOld.strokecolor = rgba;
1149             el.visPropOld.strokeopacity = o;
1150         },
1151 
1152         /* **************************
1153          *    Image related stuff
1154          * **************************/
1155 
1156         // Already documented in JXG.AbstractRenderer
1157         drawImage: function (el) {
1158             el.rendNode = new Image();
1159             // Store the file name of the image.
1160             // Before, this was done in el.rendNode.src
1161             // But there, the file name is expanded to
1162             // the full url. This may be different from
1163             // the url computed in updateImageURL().
1164             el._src = '';
1165             this.updateImage(el);
1166         },
1167 
1168         // Already documented in JXG.AbstractRenderer
1169         updateImage: function (el) {
1170             var context = this.context,
1171                 o = Type.evaluate(el.visProp.fillopacity),
1172                 paintImg = Type.bind(function () {
1173                     el.imgIsLoaded = true;
1174                     if (el.size[0] <= 0 || el.size[1] <= 0) {
1175                         return;
1176                     }
1177                     context.save();
1178                     context.globalAlpha = o;
1179                     // If det(el.transformations)=0, FireFox 3.6. breaks down.
1180                     // This is tested in transformImage
1181                     this.transformImage(el, el.transformations);
1182                     context.drawImage(el.rendNode,
1183                         el.coords.scrCoords[1],
1184                         el.coords.scrCoords[2] - el.size[1],
1185                         el.size[0],
1186                         el.size[1]);
1187                     context.restore();
1188                 }, this);
1189 
1190             if (this.updateImageURL(el)) {
1191                 el.rendNode.onload = paintImg;
1192             } else {
1193                 if (el.imgIsLoaded) {
1194                     paintImg();
1195                 }
1196             }
1197         },
1198 
1199         // Already documented in JXG.AbstractRenderer
1200         transformImage: function (el, t) {
1201             var m, len = t.length,
1202                 ctx = this.context;
1203 
1204             if (len > 0) {
1205                 m = this.joinTransforms(el, t);
1206                 if (Math.abs(Numerics.det(m)) >= Mat.eps) {
1207                     ctx.transform(m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]);
1208                 }
1209             }
1210         },
1211 
1212         // Already documented in JXG.AbstractRenderer
1213         updateImageURL: function (el) {
1214             var url;
1215 
1216             url = Type.evaluate(el.url);
1217             if (el._src !== url) {
1218                 el.imgIsLoaded = false;
1219                 el.rendNode.src = url;
1220                 el._src = url;
1221                 return true;
1222             }
1223 
1224             return false;
1225         },
1226 
1227         /* **************************
1228          * Render primitive objects
1229          * **************************/
1230 
1231         // documented in AbstractRenderer
1232         remove: function (shape) {
1233             // sounds odd for a pixel based renderer but we need this for html texts
1234             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
1235                 shape.parentNode.removeChild(shape);
1236             }
1237         },
1238 
1239         // documented in AbstractRenderer
1240         updatePathStringPrim: function (el) {
1241             var i, scr, scr1, scr2, len,
1242                 symbm = 'M',
1243                 symbl = 'L',
1244                 symbc = 'C',
1245                 nextSymb = symbm,
1246                 maxSize = 5000.0,
1247                 context = this.context;
1248 
1249             if (el.numberPoints <= 0) {
1250                 return;
1251             }
1252 
1253             len = Math.min(el.points.length, el.numberPoints);
1254             context.beginPath();
1255 
1256             if (el.bezierDegree === 1) {
1257                 /*
1258                 if (isNotPlot && el.board.options.curve.RDPsmoothing) {
1259                     el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
1260                 }
1261                 */
1262 
1263                 for (i = 0; i < len; i++) {
1264                     scr = el.points[i].scrCoords;
1265 
1266                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
1267                         nextSymb = symbm;
1268                     } else {
1269                         // Chrome has problems with values  being too far away.
1270                         if (scr[1] > maxSize) {
1271                             scr[1] = maxSize;
1272                         } else if (scr[1] < -maxSize) {
1273                             scr[1] = -maxSize;
1274                         }
1275 
1276                         if (scr[2] > maxSize) {
1277                             scr[2] = maxSize;
1278                         } else if (scr[2] < -maxSize) {
1279                             scr[2] = -maxSize;
1280                         }
1281 
1282                         if (nextSymb === symbm) {
1283                             context.moveTo(scr[1], scr[2]);
1284                         } else {
1285                             context.lineTo(scr[1], scr[2]);
1286                         }
1287                         nextSymb = symbl;
1288                     }
1289                 }
1290             } else if (el.bezierDegree === 3) {
1291                 i = 0;
1292                 while (i < len) {
1293                     scr = el.points[i].scrCoords;
1294                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
1295                         nextSymb = symbm;
1296                     } else {
1297                         if (nextSymb === symbm) {
1298                             context.moveTo(scr[1], scr[2]);
1299                         } else {
1300                             i += 1;
1301                             scr1 = el.points[i].scrCoords;
1302                             i += 1;
1303                             scr2 = el.points[i].scrCoords;
1304                             context.bezierCurveTo(scr[1], scr[2], scr1[1], scr1[2], scr2[1], scr2[2]);
1305                         }
1306                         nextSymb = symbc;
1307                     }
1308                     i += 1;
1309                 }
1310             }
1311             context.lineCap = 'round';
1312             this._fill(el);
1313             this._stroke(el);
1314         },
1315 
1316         // Already documented in JXG.AbstractRenderer
1317         updatePathStringBezierPrim: function (el) {
1318             var i, j, k, scr, lx, ly, len,
1319                 symbm = 'M',
1320                 symbl = 'C',
1321                 nextSymb = symbm,
1322                 maxSize = 5000.0,
1323                 f = Type.evaluate(el.visProp.strokewidth),
1324                 isNoPlot = (Type.evaluate(el.visProp.curvetype) !== 'plot'),
1325                 context = this.context;
1326 
1327             if (el.numberPoints <= 0) {
1328                 return;
1329             }
1330 
1331             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
1332                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
1333             }
1334 
1335             len = Math.min(el.points.length, el.numberPoints);
1336             context.beginPath();
1337 
1338             for (j = 1; j < 3; j++) {
1339                 nextSymb = symbm;
1340                 for (i = 0; i < len; i++) {
1341                     scr = el.points[i].scrCoords;
1342 
1343                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
1344                         nextSymb = symbm;
1345                     } else {
1346                         // Chrome has problems with values being too far away.
1347                         if (scr[1] > maxSize) {
1348                             scr[1] = maxSize;
1349                         } else if (scr[1] < -maxSize) {
1350                             scr[1] = -maxSize;
1351                         }
1352 
1353                         if (scr[2] > maxSize) {
1354                             scr[2] = maxSize;
1355                         } else if (scr[2] < -maxSize) {
1356                             scr[2] = -maxSize;
1357                         }
1358 
1359                         if (nextSymb === symbm) {
1360                             context.moveTo(scr[1], scr[2]);
1361                         } else {
1362                             k = 2 * j;
1363                             context.bezierCurveTo(
1364                                 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)),
1365                                 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)),
1366                                 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)),
1367                                 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)),
1368                                 scr[1],
1369                                 scr[2]
1370                             );
1371                         }
1372                         nextSymb = symbl;
1373                         lx = scr[1];
1374                         ly = scr[2];
1375                     }
1376                 }
1377             }
1378             context.lineCap = 'round';
1379             this._fill(el);
1380             this._stroke(el);
1381         },
1382 
1383         // documented in AbstractRenderer
1384         updatePolygonPrim: function (node, el) {
1385             var scrCoords, i, j,
1386                 len = el.vertices.length,
1387                 context = this.context,
1388                 isReal = true;
1389 
1390             if (len <= 0 || !el.visPropCalc.visible) {
1391                 return;
1392             }
1393             if (el.elType === 'polygonalchain') {
1394                 len++;
1395             }
1396 
1397             context.beginPath();
1398             i = 0;
1399             while (!el.vertices[i].isReal && i < len - 1) {
1400                 i++;
1401                 isReal = false;
1402             }
1403             scrCoords = el.vertices[i].coords.scrCoords;
1404             context.moveTo(scrCoords[1], scrCoords[2]);
1405 
1406             for (j = i; j < len - 1; j++) {
1407                 if (!el.vertices[j].isReal) {
1408                     isReal = false;
1409                 }
1410                 scrCoords = el.vertices[j].coords.scrCoords;
1411                 context.lineTo(scrCoords[1], scrCoords[2]);
1412             }
1413             context.closePath();
1414 
1415             if (isReal) {
1416                 this._fill(el);    // The edges of a polygon are displayed separately (as segments).
1417             }
1418         },
1419 
1420         // **************************  Set Attributes *************************
1421 
1422         // Already documented in JXG.AbstractRenderer
1423         display: function(el, val) {
1424              if (el && el.rendNode) {
1425                  el.visPropOld.visible = val;
1426                  if (val) {
1427                      el.rendNode.style.visibility = "inherit";
1428                  } else {
1429                      el.rendNode.style.visibility = "hidden";
1430                  }
1431              }
1432          },
1433 
1434         // documented in AbstractRenderer
1435         show: function (el) {
1436             JXG.deprecated('Board.renderer.show()', 'Board.renderer.display()');
1437 
1438             if (Type.exists(el.rendNode)) {
1439                 el.rendNode.style.visibility = "inherit";
1440             }
1441         },
1442 
1443         // documented in AbstractRenderer
1444         hide: function (el) {
1445             JXG.deprecated('Board.renderer.hide()', 'Board.renderer.display()');
1446 
1447             if (Type.exists(el.rendNode)) {
1448                 el.rendNode.style.visibility = "hidden";
1449             }
1450         },
1451 
1452         // documented in AbstractRenderer
1453         setGradient: function (el) {
1454             var // col,
1455                 op;
1456 
1457             op = Type.evaluate(el.visProp.fillopacity);
1458             op = (op > 0) ? op : 0;
1459 
1460             // col = Type.evaluate(el.visProp.fillcolor);
1461         },
1462 
1463         // documented in AbstractRenderer
1464         setShadow: function (el) {
1465             if (el.visPropOld.shadow === el.visProp.shadow) {
1466                 return;
1467             }
1468 
1469             // not implemented yet
1470             // we simply have to redraw the element
1471             // probably the best way to do so would be to call el.updateRenderer(), i think.
1472 
1473             el.visPropOld.shadow = el.visProp.shadow;
1474         },
1475 
1476         // documented in AbstractRenderer
1477         highlight: function (obj) {
1478             if (obj.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(obj.visProp.display) === 'html') {
1479                 this.updateTextStyle(obj, true);
1480             } else {
1481                 obj.board.prepareUpdate();
1482                 obj.board.renderer.suspendRedraw(obj.board);
1483                 obj.board.updateRenderer();
1484                 obj.board.renderer.unsuspendRedraw();
1485             }
1486             return this;
1487         },
1488 
1489         // documented in AbstractRenderer
1490         noHighlight: function (obj) {
1491             if (obj.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(obj.visProp.display) === 'html') {
1492                 this.updateTextStyle(obj, false);
1493             } else {
1494                 obj.board.prepareUpdate();
1495                 obj.board.renderer.suspendRedraw(obj.board);
1496                 obj.board.updateRenderer();
1497                 obj.board.renderer.unsuspendRedraw();
1498             }
1499             return this;
1500         },
1501 
1502         /* **************************
1503          * renderer control
1504          * **************************/
1505 
1506         // documented in AbstractRenderer
1507         suspendRedraw: function (board) {
1508             this.context.save();
1509             this.context.clearRect(0, 0, this.canvasRoot.width, this.canvasRoot.height);
1510 
1511             if (board && board.attr.showcopyright) {
1512                 this.displayCopyright(JXG.licenseText, 12);
1513             }
1514         },
1515 
1516         // documented in AbstractRenderer
1517         unsuspendRedraw: function () {
1518             this.context.restore();
1519         },
1520 
1521         // document in AbstractRenderer
1522         resize: function (w, h) {
1523             if (this.container) {
1524                 this.canvasRoot.style.width = parseFloat(w) + 'px';
1525                 this.canvasRoot.style.height = parseFloat(h) + 'px';
1526 
1527                 this.canvasRoot.setAttribute('width', (2 * parseFloat(w)) + 'px');
1528                 this.canvasRoot.setAttribute('height',(2 * parseFloat(h)) + 'px');
1529             } else {
1530                 this.canvasRoot.width = 2 * parseFloat(w);
1531                 this.canvasRoot.height = 2 * parseFloat(h);
1532             }
1533             this.context = this.canvasRoot.getContext('2d');
1534             // The width and height of the canvas is set to twice the CSS values,
1535             // followed by an appropiate scaling.
1536             // See http://stackoverflow.com/questions/22416462/canvas-element-with-blurred-lines
1537             this.context.scale(2, 2);
1538         },
1539 
1540         removeToInsertLater: function () {
1541             return function () {};
1542         }
1543     });
1544 
1545     return JXG.CanvasRenderer;
1546 });
1547