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