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