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