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