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