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