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