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 
 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      * @class JXG.AbstractRenderer
 60      * @augments JXG.AbstractRenderer
 61      * @param {Node} container Reference to a DOM node containing the board.
 62      * @param {Object} dim The dimensions of the board
 63      * @param {Number} dim.width
 64      * @param {Number} dim.height
 65      * @see JXG.AbstractRenderer
 66      */
 67     JXG.CanvasRenderer = function (container, dim) {
 68         var i;
 69 
 70         this.type = 'canvas';
 71 
 72         this.canvasRoot = null;
 73         this.suspendHandle = null;
 74         this.canvasId = UUID.genUUID();
 75 
 76         this.canvasNamespace = null;
 77 
 78         if (Env.isBrowser) {
 79             this.container = container;
 80             this.container.style.MozUserSelect = 'none';
 81             this.container.style.userSelect = 'none';
 82 
 83             this.container.style.overflow = 'hidden';
 84             if (this.container.style.position === '') {
 85                 this.container.style.position = 'relative';
 86             }
 87 
 88             this.container.innerHTML = ['<canvas id="', this.canvasId,
 89                 '" width="', dim.width,
 90                 'px" height="', dim.height,
 91                 'px"><', '/canvas>'].join('');
 92             this.canvasRoot = this.container.ownerDocument.getElementById(this.canvasId);
 93             this.context =  this.canvasRoot.getContext('2d');
 94         } else if (Env.isNode()) {
 95             this.canvasId = (typeof module === 'object' ? module.require('canvas') : require('canvas'));
 96             this.canvasRoot = new this.canvasId(500, 500);
 97             this.context = this.canvasRoot.getContext('2d');
 98         }
 99 
100         this.dashArray = [[2, 2], [5, 5], [10, 10], [20, 20], [20, 10, 10, 10], [20, 5, 10, 5]];
101     };
102 
103     JXG.CanvasRenderer.prototype = new AbstractRenderer();
104 
105     JXG.extend(JXG.CanvasRenderer.prototype, /** @lends JXG.CanvasRenderer.prototype */ {
106 
107         /* **************************
108          *   private methods only used
109          *   in this renderer. Should
110          *   not be called from outside.
111          * **************************/
112 
113         /**
114          * Draws a filled polygon.
115          * @param {Array} shape A matrix presented by a two dimensional array of numbers.
116          * @see JXG.AbstractRenderer#makeArrows
117          * @private
118          */
119         _drawFilledPolygon: function (shape) {
120             var i, len = shape.length,
121                 context = this.context;
122 
123             if (len > 0) {
124                 context.beginPath();
125                 context.moveTo(shape[0][0], shape[0][1]);
126                 for (i = 0; i < len; i++) {
127                     if (i > 0) {
128                         context.lineTo(shape[i][0], shape[i][1]);
129                     }
130                 }
131                 context.lineTo(shape[0][0], shape[0][1]);
132                 context.fill();
133             }
134         },
135 
136         /**
137          * Sets the fill color and fills an area.
138          * @param {JXG.GeometryElement} el An arbitrary JSXGraph element, preferably one with an area.
139          * @private
140          */
141         _fill: function (el) {
142             var context = this.context;
143 
144             context.save();
145             if (this._setColor(el, 'fill')) {
146                 context.fill();
147             }
148             context.restore();
149         },
150 
151         /**
152          * Rotates a point around <tt>(0, 0)</tt> by a given angle.
153          * @param {Number} angle An angle, given in rad.
154          * @param {Number} x X coordinate of the point.
155          * @param {Number} y Y coordinate of the point.
156          * @returns {Array} An array containing the x and y coordinate of the rotated point.
157          * @private
158          */
159         _rotatePoint: function (angle, x, y) {
160             return [
161                 (x * Math.cos(angle)) - (y * Math.sin(angle)),
162                 (x * Math.sin(angle)) + (y * Math.cos(angle))
163             ];
164         },
165 
166         /**
167          * Rotates an array of points around <tt>(0, 0)</tt>.
168          * @param {Array} shape An array of array of point coordinates.
169          * @param {Number} angle The angle in rad the points are rotated by.
170          * @returns {Array} Array of array of two dimensional point coordinates.
171          * @private
172          */
173         _rotateShape: function (shape, angle) {
174             var i, rv = [], len = shape.length;
175 
176             if (len <= 0) {
177                 return shape;
178             }
179 
180             for (i = 0; i < len; i++) {
181                 rv.push(this._rotatePoint(angle, shape[i][0], shape[i][1]));
182             }
183 
184             return rv;
185         },
186 
187         /**
188          * Sets color and opacity for filling and stroking.
189          * type is the attribute from visProp and targetType the context[targetTypeStyle].
190          * This is necessary, because the fill style of a text is set by the stroke attributes of the text element.
191          * @param {JXG.GeometryElement} el Any JSXGraph element.
192          * @param {String} [type='stroke'] Either <em>fill</em> or <em>stroke</em>.
193          * @param {String} [targetType=type] (optional) Either <em>fill</em> or <em>stroke</em>.
194          * @returns {Boolean} If the color could be set, <tt>true</tt> is returned.
195          * @private
196          */
197         _setColor: function (el, type, targetType) {
198             var hasColor = true, isTrace = false,
199                 ev = el.visProp, hl, sw,
200                 rgba, rgbo, c, o, oo;
201 
202             type = type || 'stroke';
203             targetType = targetType || type;
204 
205             if (!Type.exists(el.board) || !Type.exists(el.board.highlightedObjects)) {
206                 // This case handles trace elements.
207                 // To make them work, we simply neglect highlighting.
208                 isTrace = true;
209             }
210 
211             if (!isTrace && Type.exists(el.board.highlightedObjects[el.id])) {
212                 hl = 'highlight';
213             } else {
214                 hl = '';
215             }
216 
217             // type is equal to 'fill' or 'stroke'
218             rgba = Type.evaluate(ev[hl + type + 'color']);
219             if (rgba !== 'none' && rgba !== false) {
220                 o = Type.evaluate(ev[hl + type + 'opacity']);
221                 o = (o > 0) ? o : 0;
222 
223                 // RGB, not RGBA
224                 if (rgba.length !== 9) {
225                     c = rgba;
226                     oo = o;
227                 // True RGBA, not RGB
228                 } else {
229                     rgbo = Color.rgba2rgbo(rgba);
230                     c = rgbo[0];
231                     oo = o * rgbo[1];
232                 }
233                 this.context.globalAlpha = oo;
234 
235                 this.context[targetType + 'Style'] = c;
236 
237             } else {
238                 hasColor = false;
239             }
240 
241             sw = parseFloat(Type.evaluate(ev.strokewidth));
242             if (type === 'stroke' && !isNaN(sw)) {
243                 if (sw === 0) {
244                     this.context.globalAlpha = 0;
245                 } else {
246                     this.context.lineWidth = sw;
247                 }
248             }
249 
250             if (type === 'stroke' && ev.linecap !== undefined && ev.linecap !== '') {
251                 this.context.lineCap = ev.linecap;
252             }
253 
254             return hasColor;
255         },
256 
257         /**
258          * Sets color and opacity for drawing paths and lines and draws the paths and lines.
259          * @param {JXG.GeometryElement} el An JSXGraph element with a stroke.
260          * @private
261          */
262         _stroke: function (el) {
263             var context = this.context,
264                 ev_dash = Type.evaluate(el.visProp.dash);
265 
266             context.save();
267 
268             if (ev_dash > 0) {
269                 if (context.setLineDash) {
270                     context.setLineDash(this.dashArray[ev_dash]);
271                 }
272             } else {
273                 this.context.lineDashArray = [];
274             }
275 
276             if (this._setColor(el, 'stroke')) {
277                 context.stroke();
278             }
279 
280             context.restore();
281         },
282 
283         /**
284          * Translates a set of points.
285          * @param {Array} shape An array of point coordinates.
286          * @param {Number} x Translation in X direction.
287          * @param {Number} y Translation in Y direction.
288          * @returns {Array} An array of translated point coordinates.
289          * @private
290          */
291         _translateShape: function (shape, x, y) {
292             var i, rv = [], len = shape.length;
293 
294             if (len <= 0) {
295                 return shape;
296             }
297 
298             for (i = 0; i < len; i++) {
299                 rv.push([ shape[i][0] + x, shape[i][1] + y ]);
300             }
301 
302             return rv;
303         },
304 
305         /* ******************************** *
306          *    Point drawing and updating    *
307          * ******************************** */
308 
309         // documented in AbstractRenderer
310         drawPoint: function (el) {
311             var f = Type.evaluate(el.visProp.face),
312                 size = Type.evaluate(el.visProp.size),
313                 scr = el.coords.scrCoords,
314                 sqrt32 = size * Math.sqrt(3) * 0.5,
315                 s05 = size * 0.5,
316                 stroke05 = parseFloat(Type.evaluate(el.visProp.strokewidth)) / 2.0,
317                 context = this.context;
318 
319             if (!el.visPropCalc.visible) {
320                 return;
321             }
322 
323             switch (f) {
324             case 'cross':  // x
325             case 'x':
326                 context.beginPath();
327                 context.moveTo(scr[1] - size, scr[2] - size);
328                 context.lineTo(scr[1] + size, scr[2] + size);
329                 context.moveTo(scr[1] + size, scr[2] - size);
330                 context.lineTo(scr[1] - size, scr[2] + size);
331                 context.lineCap = 'round';
332                 context.lineJoin = 'round';
333                 context.closePath();
334                 this._stroke(el);
335                 break;
336             case 'circle': // dot
337             case 'o':
338                 context.beginPath();
339                 context.arc(scr[1], scr[2], size + 1 + stroke05, 0, 2 * Math.PI, false);
340                 context.closePath();
341                 this._fill(el);
342                 this._stroke(el);
343                 break;
344             case 'square':  // rectangle
345             case '[]':
346                 if (size <= 0) {
347                     break;
348                 }
349 
350                 context.save();
351                 if (this._setColor(el, 'stroke', 'fill')) {
352                     context.fillRect(scr[1] - size - stroke05, scr[2] - size - stroke05, size * 2 + 3 * stroke05, size * 2 + 3 * stroke05);
353                 }
354                 context.restore();
355                 context.save();
356                 this._setColor(el, 'fill');
357                 context.fillRect(scr[1] - size + stroke05, scr[2] - size + stroke05, size * 2 - stroke05, size * 2 - stroke05);
358                 context.restore();
359                 break;
360             case 'plus':  // +
361             case '+':
362                 context.beginPath();
363                 context.moveTo(scr[1] - size, scr[2]);
364                 context.lineTo(scr[1] + size, scr[2]);
365                 context.moveTo(scr[1], scr[2] - size);
366                 context.lineTo(scr[1], scr[2] + size);
367                 context.lineCap = 'round';
368                 context.lineJoin = 'round';
369                 context.closePath();
370                 this._stroke(el);
371                 break;
372             case 'diamond':   // <>
373             case '<>':
374                 context.beginPath();
375                 context.moveTo(scr[1] - size, scr[2]);
376                 context.lineTo(scr[1], scr[2] + size);
377                 context.lineTo(scr[1] + size, scr[2]);
378                 context.lineTo(scr[1], scr[2] - size);
379                 context.closePath();
380                 this._fill(el);
381                 this._stroke(el);
382                 break;
383             case 'triangleup':
384             case 'a':
385             case '^':
386                 context.beginPath();
387                 context.moveTo(scr[1], scr[2] - size);
388                 context.lineTo(scr[1] - sqrt32, scr[2] + s05);
389                 context.lineTo(scr[1] + sqrt32, scr[2] + s05);
390                 context.closePath();
391                 this._fill(el);
392                 this._stroke(el);
393                 break;
394             case 'triangledown':
395             case 'v':
396                 context.beginPath();
397                 context.moveTo(scr[1], scr[2] + size);
398                 context.lineTo(scr[1] - sqrt32, scr[2] - s05);
399                 context.lineTo(scr[1] + sqrt32, scr[2] - s05);
400                 context.closePath();
401                 this._fill(el);
402                 this._stroke(el);
403                 break;
404             case 'triangleleft':
405             case '<':
406                 context.beginPath();
407                 context.moveTo(scr[1] - size, scr[2]);
408                 context.lineTo(scr[1] + s05, scr[2] - sqrt32);
409                 context.lineTo(scr[1] + s05, scr[2] + sqrt32);
410                 context.closePath();
411                 this.fill(el);
412                 this._stroke(el);
413                 break;
414             case 'triangleright':
415             case '>':
416                 context.beginPath();
417                 context.moveTo(scr[1] + size, scr[2]);
418                 context.lineTo(scr[1] - s05, scr[2] - sqrt32);
419                 context.lineTo(scr[1] - s05, scr[2] + sqrt32);
420                 context.closePath();
421                 this._fill(el);
422                 this._stroke(el);
423                 break;
424             }
425         },
426 
427         // documented in AbstractRenderer
428         updatePoint: function (el) {
429             this.drawPoint(el);
430         },
431 
432         /* ******************************** *
433          *           Lines                  *
434          * ******************************** */
435 
436         // documented in AbstractRenderer
437         drawLine: function (el) {
438             var obj,
439                 c1 = new Coords(Const.COORDS_BY_USER, el.point1.coords.usrCoords, el.board),
440                 c2 = new Coords(Const.COORDS_BY_USER, el.point2.coords.usrCoords, el.board),
441                 margin = null,
442                 ev_fa = Type.evaluate(el.visProp.firstarrow),
443                 ev_la = Type.evaluate(el.visProp.lastarrow);
444 
445             if (!el.visPropCalc.visible) {
446                 return;
447             }
448 
449             if (ev_fa || ev_la) {
450                 margin = -4;
451             }
452             Geometry.calcStraight(el, c1, c2, margin);
453 
454             obj = this.getPositionArrowHead(el, c1, c2);
455 
456             this.context.beginPath();
457             this.context.moveTo(obj.c1.scrCoords[1] + obj.d1x, obj.c1.scrCoords[2] + obj.d1y);
458             this.context.lineTo(obj.c2.scrCoords[1] - obj.d2x, obj.c2.scrCoords[2] - obj.d2y);
459             this._stroke(el);
460 
461             if ((ev_fa && obj.sFirst > 0) ||
462                 (ev_la && obj.sLast > 0)) {
463                 this.makeArrows(el, obj.c1, obj.c2);
464             }
465         },
466 
467         // documented in AbstractRenderer
468         updateLine: function (el) {
469             this.drawLine(el);
470         },
471 
472         // documented in AbstractRenderer
473         drawTicks: function () {
474             // this function is supposed to initialize the svg/vml nodes in the SVG/VMLRenderer.
475             // but in canvas there are no such nodes, hence we just do nothing and wait until
476             // updateTicks is called.
477         },
478 
479         // documented in AbstractRenderer
480         updateTicks: function (ticks) {
481             var i, c, x, y,
482                 len = ticks.ticks.length,
483                 context = this.context;
484 
485             context.beginPath();
486             for (i = 0; i < len; i++) {
487                 c = ticks.ticks[i];
488                 x = c[0];
489                 y = c[1];
490                 context.moveTo(x[0], y[0]);
491                 context.lineTo(x[1], y[1]);
492             }
493             // Labels
494             for (i = 0; i < len; i++) {
495                 c = ticks.ticks[i].scrCoords;
496                 if (ticks.ticks[i].major &&
497                         (ticks.board.needsFullUpdate || ticks.needsRegularUpdate) &&
498                         ticks.labels[i] &&
499                         ticks.labels[i].visPropCalc.visible) {
500                     this.updateText(ticks.labels[i]);
501                 }
502             }
503             context.lineCap = 'round';
504             this._stroke(ticks);
505         },
506 
507         /* **************************
508          *    Curves
509          * **************************/
510 
511         // documented in AbstractRenderer
512         drawCurve: function (el) {
513             if (Type.evaluate(el.visProp.handdrawing)) {
514                 this.updatePathStringBezierPrim(el);
515             } else {
516                 this.updatePathStringPrim(el);
517             }
518             if (el.numberPoints > 1) {
519                 this.makeArrows(el);
520             }
521         },
522 
523         // documented in AbstractRenderer
524         updateCurve: function (el) {
525             this.drawCurve(el);
526         },
527 
528         /* **************************
529          *    Circle related stuff
530          * **************************/
531 
532         // documented in AbstractRenderer
533         drawEllipse: function (el) {
534             var m1 = el.center.coords.scrCoords[1],
535                 m2 = el.center.coords.scrCoords[2],
536                 sX = el.board.unitX,
537                 sY = el.board.unitY,
538                 rX = 2 * el.Radius(),
539                 rY = 2 * el.Radius(),
540                 aWidth = rX * sX,
541                 aHeight = rY * sY,
542                 aX = m1 - aWidth / 2,
543                 aY = m2 - aHeight / 2,
544                 hB = (aWidth / 2) * 0.5522848,
545                 vB = (aHeight / 2) * 0.5522848,
546                 eX = aX + aWidth,
547                 eY = aY + aHeight,
548                 mX = aX + aWidth / 2,
549                 mY = aY + aHeight / 2,
550                 context = this.context;
551 
552             if (rX > 0.0 && rY > 0.0 && !isNaN(m1 + m2)) {
553                 context.beginPath();
554                 context.moveTo(aX, mY);
555                 context.bezierCurveTo(aX, mY - vB, mX - hB, aY, mX, aY);
556                 context.bezierCurveTo(mX + hB, aY, eX, mY - vB, eX, mY);
557                 context.bezierCurveTo(eX, mY + vB, mX + hB, eY, mX, eY);
558                 context.bezierCurveTo(mX - hB, eY, aX, mY + vB, aX, mY);
559                 context.closePath();
560                 this._fill(el);
561                 this._stroke(el);
562             }
563         },
564 
565         // documented in AbstractRenderer
566         updateEllipse: function (el) {
567             return this.drawEllipse(el);
568         },
569 
570         /* **************************
571          *    Polygon
572          * **************************/
573 
574         // nothing here, using AbstractRenderer implementations
575 
576         /* **************************
577          *    Text related stuff
578          * **************************/
579 
580         // already documented in JXG.AbstractRenderer
581         displayCopyright: function (str, fontSize) {
582             var context = this.context;
583 
584             // this should be called on EVERY update, otherwise it won't be shown after the first update
585             context.save();
586             context.font = fontSize + 'px Arial';
587             context.fillStyle = '#aaa';
588             context.lineWidth = 0.5;
589             context.fillText(str, 10, 2 + fontSize);
590             context.restore();
591         },
592 
593         // already documented in JXG.AbstractRenderer
594         drawInternalText: function (el) {
595             var ev_fs = Type.evaluate(el.visProp.fontsize),
596                 ev_ax = el.getAnchorX(),
597                 ev_ay = el.getAnchorY(),
598                 context = this.context;
599 
600             context.save();
601             if (this._setColor(el, 'stroke', 'fill') &&
602                     !isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
603                 context.font = (ev_fs > 0 ? ev_fs : 0) + 'px Arial';
604 
605                 this.transformImage(el, el.transformations);
606                 if (ev_ax === 'left') {
607                     context.textAlign = 'left';
608                 } else if (ev_ax === 'right') {
609                     context.textAlign = 'right';
610                 } else if (ev_ax === 'middle') {
611                     context.textAlign = 'center';
612                 }
613                 if (ev_ay === 'bottom') {
614                     context.textBaseline = 'bottom';
615                 } else if (ev_ay === 'top') {
616                     context.textBaseline = 'top';
617                 } else if (ev_ay === 'middle') {
618                     context.textBaseline = 'middle';
619                 }
620                 context.fillText(el.plaintext, el.coords.scrCoords[1], el.coords.scrCoords[2]);
621             }
622             context.restore();
623             return null;
624         },
625 
626         // already documented in JXG.AbstractRenderer
627         updateInternalText: function (el) {
628             this.drawInternalText(el);
629         },
630 
631         // documented in JXG.AbstractRenderer
632         // Only necessary for texts
633         setObjectStrokeColor: function (el, color, opacity) {
634             var rgba = Type.evaluate(color), c, rgbo,
635                 o = Type.evaluate(opacity), oo,
636                 node;
637 
638             o = (o > 0) ? o : 0;
639 
640             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
641                 return;
642             }
643 
644             // Check if this could be merged with _setColor
645 
646             if (Type.exists(rgba) && rgba !== false) {
647                 // RGB, not RGBA
648                 if (rgba.length !== 9) {
649                     c = rgba;
650                     oo = o;
651                 // True RGBA, not RGB
652                 } else {
653                     rgbo = Color.rgba2rgbo(rgba);
654                     c = rgbo[0];
655                     oo = o * rgbo[1];
656                 }
657                 node = el.rendNode;
658                 if (el.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(el.visProp.display) === 'html') {
659                     node.style.color = c;
660                     node.style.opacity = oo;
661                 }
662             }
663 
664             el.visPropOld.strokecolor = rgba;
665             el.visPropOld.strokeopacity = o;
666         },
667 
668         /* **************************
669          *    Image related stuff
670          * **************************/
671 
672         // already documented in JXG.AbstractRenderer
673         drawImage: function (el) {
674             el.rendNode = new Image();
675             // Store the file name of the image.
676             // Before, this was done in el.rendNode.src
677             // But there, the file name is expanded to
678             // the full url. This may be different from
679             // the url computed in updateImageURL().
680             el._src = '';
681             this.updateImage(el);
682         },
683 
684         // already documented in JXG.AbstractRenderer
685         updateImage: function (el) {
686             var context = this.context,
687                 o = Type.evaluate(el.visProp.fillopacity),
688                 paintImg = Type.bind(function () {
689                     el.imgIsLoaded = true;
690                     if (el.size[0] <= 0 || el.size[1] <= 0) {
691                         return;
692                     }
693                     context.save();
694                     context.globalAlpha = o;
695                     // If det(el.transformations)=0, FireFox 3.6. breaks down.
696                     // This is tested in transformImage
697                     this.transformImage(el, el.transformations);
698                     context.drawImage(el.rendNode,
699                         el.coords.scrCoords[1],
700                         el.coords.scrCoords[2] - el.size[1],
701                         el.size[0],
702                         el.size[1]);
703                     context.restore();
704                 }, this);
705 
706             if (this.updateImageURL(el)) {
707                 el.rendNode.onload = paintImg;
708             } else {
709                 if (el.imgIsLoaded) {
710                     paintImg();
711                 }
712             }
713         },
714 
715         // already documented in JXG.AbstractRenderer
716         transformImage: function (el, t) {
717             var m, len = t.length,
718                 ctx = this.context;
719 
720             if (len > 0) {
721                 m = this.joinTransforms(el, t);
722                 if (Math.abs(Numerics.det(m)) >= Mat.eps) {
723                     ctx.transform(m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]);
724                 }
725             }
726         },
727 
728         // already documented in JXG.AbstractRenderer
729         updateImageURL: function (el) {
730             var url;
731 
732             url = Type.evaluate(el.url);
733             if (el._src !== url) {
734                 el.imgIsLoaded = false;
735                 el.rendNode.src = url;
736                 el._src = url;
737                 return true;
738             }
739 
740             return false;
741         },
742 
743         /* **************************
744          * Render primitive objects
745          * **************************/
746 
747         // documented in AbstractRenderer
748         remove: function (shape) {
749             // sounds odd for a pixel based renderer but we need this for html texts
750             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
751                 shape.parentNode.removeChild(shape);
752             }
753         },
754 
755         // documented in AbstractRenderer
756         makeArrows: function (el, scr1, scr2) {
757             // not done yet for curves and arcs.
758             var x1, y1, x2, y2, ang,
759                 size,
760                 w = Type.evaluate(el.visProp.strokewidth),
761                 arrowHead,
762                 arrowTail,
763                 context = this.context,
764                 type,
765                 ev_fa = Type.evaluate(el.visProp.firstarrow),
766                 ev_la = Type.evaluate(el.visProp.lastarrow);
767 
768             if (Type.evaluate(el.visProp.strokecolor) !== 'none' &&
769                     (ev_fa || ev_la)) {
770                 if (el.elementClass === Const.OBJECT_CLASS_LINE) {
771                     x1 = scr1.scrCoords[1];
772                     y1 = scr1.scrCoords[2];
773                     x2 = scr2.scrCoords[1];
774                     y2 = scr2.scrCoords[2];
775                 } else {
776                     x1 = el.points[0].scrCoords[1];
777                     y1 = el.points[0].scrCoords[2];
778                     x2 = el.points[el.points.length - 1].scrCoords[1];
779                     y2 = el.points[el.points.length - 1].scrCoords[2];
780                     return;
781                 }
782 
783                 if (ev_fa &&
784                     Type.exists(ev_fa.type)) {
785 
786                     if (Type.exists(ev_fa.size)) {
787                         size = Type.evaluate(ev_fa.size);
788                     } else {
789                         size = 3;
790                     }
791                     w *= size;
792 
793                     type = Type.evaluate(ev_fa.type);
794                     if (type === 2) {
795                         arrowTail = [
796                                 [ w,      -w * 0.5],
797                                 [ 0.0,         0.0],
798                                 [ w,       w * 0.5],
799                                 [ w * 0.5,     0.0],
800                             ];
801                     } else if (type === 3) {
802                         arrowTail = [
803                                 [ w / 3.0,   -w * 0.5],
804                                 [ 0.0,       -w * 0.5],
805                                 [ 0.0,        w * 0.5],
806                                 [ w / 3.0,    w * 0.5]
807                             ];
808                     } else {
809                         arrowTail = [
810                             [ w,   -w * 0.5],
811                             [ 0.0,      0.0],
812                             [ w,    w * 0.5]
813                         ];
814                     }
815 
816                 }
817                 if (ev_la &&
818                     Type.exists(ev_la.type)) {
819 
820                     if (Type.exists(ev_la.size)) {
821                         size = Type.evaluate(ev_la.size);
822                     } else {
823                         size = 3;
824                     }
825                     w *= size;
826 
827                     type = Type.evaluate(ev_la.type);
828                     if (type === 2) {
829                         arrowHead = [
830                             [ -w, -w * 0.5],
831                             [ 0.0,     0.0],
832                             [ -w,  w * 0.5],
833                             [ -w * 0.5, 0.0]
834                         ];
835                     } else if (type === 3) {
836                         arrowHead = [
837                                 [-w / 3.0,   -w * 0.5],
838                                 [ 0.0,       -w * 0.5],
839                                 [ 0.0,        w * 0.5],
840                                 [-w / 3.0,    w * 0.5]
841                             ];
842                     } else {
843                         arrowHead = [
844                             [ -w, -w * 0.5],
845                             [ 0.0,     0.0],
846                             [ -w,  w * 0.5]
847                         ];
848                     }
849                 }
850 
851                 context.save();
852                 if (this._setColor(el, 'stroke', 'fill')) {
853                     ang = Math.atan2(y2 - y1, x2 - x1);
854                     if (ev_la) {
855                         this._drawFilledPolygon(this._translateShape(this._rotateShape(arrowHead, ang), x2, y2));
856                     }
857 
858                     if (ev_fa) {
859                         this._drawFilledPolygon(this._translateShape(this._rotateShape(arrowTail, ang), x1, y1));
860                     }
861                 }
862                 context.restore();
863             }
864         },
865 
866         // documented in AbstractRenderer
867         updatePathStringPrim: function (el) {
868             var i, scr, scr1, scr2, len,
869                 symbm = 'M',
870                 symbl = 'L',
871                 symbc = 'C',
872                 nextSymb = symbm,
873                 maxSize = 5000.0,
874                 context = this.context;
875 
876             if (el.numberPoints <= 0) {
877                 return;
878             }
879 
880             len = Math.min(el.points.length, el.numberPoints);
881             context.beginPath();
882 
883             if (el.bezierDegree === 1) {
884                 /*
885                 if (isNotPlot && el.board.options.curve.RDPsmoothing) {
886                     el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
887                 }
888                 */
889 
890                 for (i = 0; i < len; i++) {
891                     scr = el.points[i].scrCoords;
892 
893                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
894                         nextSymb = symbm;
895                     } else {
896                         // Chrome has problems with values  being too far away.
897                         if (scr[1] > maxSize) {
898                             scr[1] = maxSize;
899                         } else if (scr[1] < -maxSize) {
900                             scr[1] = -maxSize;
901                         }
902 
903                         if (scr[2] > maxSize) {
904                             scr[2] = maxSize;
905                         } else if (scr[2] < -maxSize) {
906                             scr[2] = -maxSize;
907                         }
908 
909                         if (nextSymb === symbm) {
910                             context.moveTo(scr[1], scr[2]);
911                         } else {
912                             context.lineTo(scr[1], scr[2]);
913                         }
914                         nextSymb = symbl;
915                     }
916                 }
917             } else if (el.bezierDegree === 3) {
918                 i = 0;
919                 while (i < len) {
920                     scr = el.points[i].scrCoords;
921                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
922                         nextSymb = symbm;
923                     } else {
924                         if (nextSymb === symbm) {
925                             context.moveTo(scr[1], scr[2]);
926                         } else {
927                             i += 1;
928                             scr1 = el.points[i].scrCoords;
929                             i += 1;
930                             scr2 = el.points[i].scrCoords;
931                             context.bezierCurveTo(scr[1], scr[2], scr1[1], scr1[2], scr2[1], scr2[2]);
932                         }
933                         nextSymb = symbc;
934                     }
935                     i += 1;
936                 }
937             }
938             context.lineCap = 'round';
939             this._fill(el);
940             this._stroke(el);
941         },
942 
943         // already documented in JXG.AbstractRenderer
944         updatePathStringBezierPrim: function (el) {
945             var i, j, k, scr, lx, ly, len,
946                 symbm = 'M',
947                 symbl = 'C',
948                 nextSymb = symbm,
949                 maxSize = 5000.0,
950                 f = Type.evaluate(el.visProp.strokewidth),
951                 isNoPlot = (Type.evaluate(el.visProp.curvetype) !== 'plot'),
952                 context = this.context;
953 
954             if (el.numberPoints <= 0) {
955                 return;
956             }
957 
958             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
959                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
960             }
961 
962             len = Math.min(el.points.length, el.numberPoints);
963             context.beginPath();
964 
965             for (j = 1; j < 3; j++) {
966                 nextSymb = symbm;
967                 for (i = 0; i < len; i++) {
968                     scr = el.points[i].scrCoords;
969 
970                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
971                         nextSymb = symbm;
972                     } else {
973                         // Chrome has problems with values  being too far away.
974                         if (scr[1] > maxSize) {
975                             scr[1] = maxSize;
976                         } else if (scr[1] < -maxSize) {
977                             scr[1] = -maxSize;
978                         }
979 
980                         if (scr[2] > maxSize) {
981                             scr[2] = maxSize;
982                         } else if (scr[2] < -maxSize) {
983                             scr[2] = -maxSize;
984                         }
985 
986                         if (nextSymb === symbm) {
987                             context.moveTo(scr[1], scr[2]);
988                         } else {
989                             k = 2 * j;
990                             context.bezierCurveTo(
991                                 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)),
992                                 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)),
993                                 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)),
994                                 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)),
995                                 scr[1],
996                                 scr[2]
997                             );
998                         }
999                         nextSymb = symbl;
1000                         lx = scr[1];
1001                         ly = scr[2];
1002                     }
1003                 }
1004             }
1005             context.lineCap = 'round';
1006             this._fill(el);
1007             this._stroke(el);
1008         },
1009 
1010         // documented in AbstractRenderer
1011         updatePolygonPrim: function (node, el) {
1012             var scrCoords, i, j,
1013                 len = el.vertices.length,
1014                 context = this.context,
1015                 isReal = true;
1016 
1017             if (len <= 0 || !el.visPropCalc.visible) {
1018                 return;
1019             }
1020 
1021             context.beginPath();
1022             i = 0;
1023             while (!el.vertices[i].isReal && i < len - 1) {
1024                 i++;
1025                 isReal = false;
1026             }
1027             scrCoords = el.vertices[i].coords.scrCoords;
1028             context.moveTo(scrCoords[1], scrCoords[2]);
1029 
1030             for (j = i; j < len - 1; j++) {
1031                 if (!el.vertices[j].isReal) {
1032                     isReal = false;
1033                 }
1034                 scrCoords = el.vertices[j].coords.scrCoords;
1035                 context.lineTo(scrCoords[1], scrCoords[2]);
1036             }
1037             context.closePath();
1038 
1039             if (isReal) {
1040                 this._fill(el);    // The edges of a polygon are displayed separately (as segments).
1041             }
1042         },
1043 
1044         // **************************  Set Attributes *************************
1045 
1046          // already documented in JXG.AbstractRenderer
1047          display: function(el, val) {
1048              if (el && el.rendNode) {
1049                  el.visPropOld.visible = val;
1050                  if (val) {
1051                      el.rendNode.style.visibility = "inherit";
1052                  } else {
1053                      el.rendNode.style.visibility = "hidden";
1054                  }
1055              }
1056          },
1057 
1058         // documented in AbstractRenderer
1059         show: function (el) {
1060             JXG.deprecated('Board.renderer.show()', 'Board.renderer.display()');
1061 
1062             if (Type.exists(el.rendNode)) {
1063                 el.rendNode.style.visibility = "inherit";
1064             }
1065         },
1066 
1067         // documented in AbstractRenderer
1068         hide: function (el) {
1069             JXG.deprecated('Board.renderer.hide()', 'Board.renderer.display()');
1070 
1071             if (Type.exists(el.rendNode)) {
1072                 el.rendNode.style.visibility = "hidden";
1073             }
1074         },
1075 
1076         // documented in AbstractRenderer
1077         setGradient: function (el) {
1078             var col, op;
1079 
1080             op = Type.evaluate(el.visProp.fillopacity);
1081             op = (op > 0) ? op : 0;
1082 
1083             col = Type.evaluate(el.visProp.fillcolor);
1084         },
1085 
1086         // documented in AbstractRenderer
1087         setShadow: function (el) {
1088             if (el.visPropOld.shadow === el.visProp.shadow) {
1089                 return;
1090             }
1091 
1092             // not implemented yet
1093             // we simply have to redraw the element
1094             // probably the best way to do so would be to call el.updateRenderer(), i think.
1095 
1096             el.visPropOld.shadow = el.visProp.shadow;
1097         },
1098 
1099         // documented in AbstractRenderer
1100         highlight: function (obj) {
1101             if (obj.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(obj.visProp.display) === 'html') {
1102                 this.updateTextStyle(obj, true);
1103             } else {
1104                 obj.board.prepareUpdate();
1105                 obj.board.renderer.suspendRedraw(obj.board);
1106                 obj.board.updateRenderer();
1107                 obj.board.renderer.unsuspendRedraw();
1108             }
1109             return this;
1110         },
1111 
1112         // documented in AbstractRenderer
1113         noHighlight: function (obj) {
1114             if (obj.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(obj.visProp.display) === 'html') {
1115                 this.updateTextStyle(obj, false);
1116             } else {
1117                 obj.board.prepareUpdate();
1118                 obj.board.renderer.suspendRedraw(obj.board);
1119                 obj.board.updateRenderer();
1120                 obj.board.renderer.unsuspendRedraw();
1121             }
1122             return this;
1123         },
1124 
1125         /* **************************
1126          * renderer control
1127          * **************************/
1128 
1129         // documented in AbstractRenderer
1130         suspendRedraw: function (board) {
1131             this.context.save();
1132             this.context.clearRect(0, 0, this.canvasRoot.width, this.canvasRoot.height);
1133 
1134             if (board && board.attr.showcopyright) {
1135                 this.displayCopyright(JXG.licenseText, 12);
1136             }
1137         },
1138 
1139         // documented in AbstractRenderer
1140         unsuspendRedraw: function () {
1141             this.context.restore();
1142         },
1143 
1144         // document in AbstractRenderer
1145         resize: function (w, h) {
1146             if (this.container) {
1147                 this.canvasRoot.style.width = parseFloat(w) + 'px';
1148                 this.canvasRoot.style.height = parseFloat(h) + 'px';
1149 
1150                 this.canvasRoot.setAttribute('width', (2 * parseFloat(w)) + 'px');
1151                 this.canvasRoot.setAttribute('height',(2 * parseFloat(h)) + 'px');
1152             } else {
1153                 this.canvasRoot.width = 2 * parseFloat(w);
1154                 this.canvasRoot.height = 2 * parseFloat(h);
1155             }
1156             this.context = this.canvasRoot.getContext('2d');
1157             // The width and height of the canvas is set to twice the CSS values,
1158             // followed by an appropiate scaling.
1159             // See http://stackoverflow.com/questions/22416462/canvas-element-with-blurred-lines
1160             this.context.scale(2, 2);
1161         },
1162 
1163         removeToInsertLater: function () {
1164             return function () {};
1165         }
1166     });
1167 
1168     return JXG.CanvasRenderer;
1169 });
1170