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