# Canvas

#####

property
MathLib.Canvas

The Canvas renderer for 2D plotting

    MathLib.Canvas = {

Applies the current transformations to the screen

    applyTransformation: function () {
        var m = this.transformation;
        this.layer.forEach(function (l) {
            l.ctx.setTransform(m[0][0], m[1][0], m[0][1], m[1][1], m[0][2], m[1][2]);
        });
    },

Option name | Type | Description
--- | --- | ---
circle | Circle | The circle to be drawn
options | object | Optional drawing options
redraw | boolean | Indicates if the current draw call is happening during a redraw

Draws a circle on the screen.

    circle: function (circle, options, redraw) {
        if (typeof options === "undefined") { options = {}; }
        if (typeof redraw === "undefined") { redraw = false; }
        var screen = this.screen, ctx = this.ctx, prop, opts;

        ctx.save();
        ctx.lineWidth = (options.lineWidth || 4) / (screen.scale.x - screen.scale.y);

        // Set the drawing options
        if (options) {
            opts = MathLib.Canvas.convertOptions(options);
            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    ctx[prop] = opts[prop];
                }
            }

            if ('setLineDash' in ctx) {
                ctx.setLineDash(('dash' in options ? options.dash : []));
            }
            if ('lineDashOffset' in ctx) {
                ctx.lineDashOffset = ('dashOffset' in options ? options.dashOffset : 0);
            }
        }

        // Draw the circle
        ctx.beginPath();
        ctx.arc(circle.center[0], circle.center[1], circle.radius, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.restore();

        if (!redraw) {
            this.stack.push({
                type: 'circle',
                object: circle,
                options: options
            });
        }

        return this;
    },

Option name | Type | Description
--- | --- | ---
layer | Layer | The layer to be cleared

Clears a given Layer.

    clear: function (layer) {
        var screen = layer.screen, left = -screen.translation.x / screen.scale.x, top = -screen.translation.y / screen.scale.y, width = screen.width / screen.scale.x, height = screen.height / screen.scale.y;

        layer.ctx.clearRect(left, top, width, height);
    },

Option name | Type | Description
--- | --- | ---
options | object | The drawing options

Converts the options to the Canvas options format

    convertOptions: function (options) {
        var convertedOptions = {};
        if ('fillColor' in options) {
            convertedOptions.fillStyle = MathLib.colorConvert(options.fillColor);
        } else if ('color' in options) {
            convertedOptions.fillStyle = MathLib.colorConvert(options.color);
        }

        if ('font' in options) {
            convertedOptions.font = options.font;
        }

        if ('fontSize' in options) {
            convertedOptions.fontSize = options.fontSize;
        }

        if ('lineColor' in options) {
            convertedOptions.strokeStyle = MathLib.colorConvert(options.lineColor);
        } else if ('color' in options) {
            convertedOptions.strokeStyle = MathLib.colorConvert(options.color);
        }

        return convertedOptions;
    },

Option name | Type | Description
--- | --- | ---
line | Line | The line to be drawn
options | object | Optional drawing options
redraw | boolean | Indicates if the current draw call is happening during a redraw

Draws a line on the screen.

    line: function (line, options, redraw) {
        if (typeof options === "undefined") { options = {}; }
        if (typeof redraw === "undefined") { redraw = false; }
        var screen = this.screen, points, ctx = this.ctx, prop, opts;

        ctx.save();
        ctx.lineWidth = (options.lineWidth || 4) / (screen.scale.x - screen.scale.y);

        // Don't try to draw the line at infinity
        if (line.type === 'line' && MathLib.isZero(line[0]) && MathLib.isZero(line[1])) {
            return this;
        } else {
            points = this.screen.getLineEndPoints(line);
        }

        // Set the drawing options
        if (options) {
            opts = MathLib.Canvas.convertOptions(options);
            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    ctx[prop] = opts[prop];
                }
            }

            if ('setLineDash' in ctx) {
                ctx.setLineDash(('dash' in options ? options.dash : []));
            }
            if ('lineDashOffset' in ctx) {
                ctx.lineDashOffset = ('dashOffset' in options ? options.dashOffset : 0);
            }
        }

        // Draw the line
        ctx.beginPath();
        ctx.moveTo(points[0][0], points[0][1]);
        ctx.lineTo(points[1][0], points[1][1]);
        ctx.stroke();
        ctx.closePath();
        ctx.restore();

        if (!redraw) {
            this.stack.push({
                type: 'line',
                object: line,
                options: options
            });
        }

        return this;
    },

Option name | Type | Description
--- | --- | ---
curve | Path | The path to be drawn
options | object | Optional drawing options
redraw | boolean | Indicates if the current draw call is happening during a redraw

Draws a path on the screen.

    path: function (curve, options, redraw) {
        if (typeof options === "undefined") { options = {}; }
        if (typeof redraw === "undefined") { redraw = false; }
        var screen = this.screen, ctx = this.ctx, prop, opts, path, paths = [], x, y, i, fx, fxold, step = 2 / (screen.scale.x - screen.scale.y), from, to;

        ctx.save();
        ctx.lineWidth = (options.lineWidth || 4) / (screen.scale.x - screen.scale.y);

        // Set the drawing options
        if (options) {
            opts = MathLib.Canvas.convertOptions(options);
            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    ctx[prop] = opts[prop];
                }
            }

            if ('setLineDash' in ctx) {
                ctx.setLineDash(('dash' in options ? options.dash : []));
            }
            if ('lineDashOffset' in ctx) {
                ctx.lineDashOffset = ('dashOffset' in options ? options.dashOffset : 0);
            }
        }

        // If curve is a function f, the path will be (x, f(x))
        if (typeof curve === 'function') {
            path = [];
            from = ('from' in options ? options.from : (-screen.translation.x) / screen.scale.x) - step;
            to = ('to' in options ? options.to : (screen.width - screen.translation.x) / screen.scale.x) + step;

            for (i = from; i <= to; i += step) {
                fx = curve(i);

                // Inline NaN test and disontinuity test
                // Check if we are drawing a (nearly) vertical line, which should not be there.
                // i.e the vertical lines at π/2 for the tangent function
                // TODO: Find a better check if there is a discontinuity.
                if (fx !== fx || (MathLib.abs((fxold - fx) / step) >= 1e2 && (fx - curve(i - step / 2)) * (fxold - curve(i - step / 2)) >= 0)) {
                    // Don't add empty subpaths
                    if (path.length) {
                        paths.push(path);
                        path = [];
                    }
                } else {
                    path.push([i, fx]);
                }

                fxold = fx;
            }
            if (path.length) {
                paths.push(path);
            }
        } else if (typeof curve[0] === 'function') {
            path = [];
            x = curve[0];
            y = curve[1];
            from = ('from' in options ? options.from : 0) - step;
            to = ('to' in options ? options.to : 2 * Math.PI) + step;
            for (i = from; i <= to; i += step) {
                path.push([x(i), y(i)]);
            }
            paths.push(path);
        } else {
            path = curve;
        }

        // Draw the path
        // Till now I haven't found a way to stroke and fill the path in one go.
        // The problem is basically, that moveTo creates a new subpath
        // and every subpath is filled on its own.
        if (options.fillColor || options.fillColor !== 'transparent') {
            ctx.beginPath();
            ctx.lineTo(from, 0);
            paths.forEach(function (path) {
                // The following line (and the line four lines afterwards) fixes the fill at holes in the path.
                ctx.lineTo(path[0][0], 0);
                path.forEach(function (x) {
                    ctx.lineTo(x[0], x[1]);
                });
                ctx.lineTo(path[path.length - 1][0], 0);
            });
            ctx.fill();
            //		ctx.closePath();
        }

        if (options.lineColor || options.lineColor !== 'transparent') {
            ctx.beginPath();
            paths.forEach(function (path) {
                ctx.moveTo(path[0][0], path[0][1]);
                path.forEach(function (x) {
                    ctx.lineTo(x[0], x[1]);
                });
            });
            ctx.stroke();
            //		ctx.closePath();
        }

        ctx.restore();

        if (!redraw) {
            if (options.conic) {
                this.stack.push({
                    type: 'conic',
                    object: options.conic,
                    options: options
                });
            } else {
                this.stack.push({
                    type: 'path',
                    object: curve,
                    options: options
                });
            }
        }

        return this;
    },

Option name | Type | Description
--- | --- | ---
f | function | The pixel function
t | number | The top coordinate of the draw rectangle
r | number | The right coordinate of the draw rectangle
b | number | The bottom coordinate of the draw rectangle
l | number | The left coordinate of the draw rectangle
options | object | Optional drawing options
redraw | boolean | Indicates if the current draw call is happening during a redraw

Draws pixel on the screen.

    pixel: function (f, t, r, b, l, options, redraw) {
        if (typeof options === "undefined") { options = {}; }
        if (typeof redraw === "undefined") { redraw = false; }
        var screen = this.screen, top = (-screen.translation.y) / screen.scale.y, bottom = (screen.height - screen.translation.y) / screen.scale.y, left = (-screen.translation.x) / screen.scale.x, right = (screen.width - screen.translation.x) / screen.scale.x, ctx = this.ctx, x, y, i;

        t = Math.min(top, t);
        r = Math.min(right, r);
        b = Math.max(bottom, b);
        l = Math.max(left, l);

        var tPxl = Math.floor(-t * screen.scale.y), rPxl = Math.floor(r * screen.scale.x), bPxl = Math.floor(-b * screen.scale.y), lPxl = Math.floor(l * screen.scale.x), w = (rPxl - lPxl), h = (bPxl - tPxl), imgData = ctx.createImageData(w, h), pxl;

        for (y = tPxl, i = 0; y > bPxl; y--) {
            for (x = lPxl; x < rPxl; x++, i++) {
                pxl = f(x / screen.scale.x, y / screen.scale.y);
                imgData.data[4 * i] = pxl[0];
                imgData.data[4 * i + 1] = pxl[1];
                imgData.data[4 * i + 2] = pxl[2];
                imgData.data[4 * i + 3] = pxl[3];
            }
        }

        ctx.putImageData(imgData, (left - l) * screen.scale.x, (t - top) * screen.scale.y);

        if (!redraw) {
            this.stack.push({
                type: 'pixel',
                object: f,
                t: t,
                r: r,
                b: b,
                l: l,
                options: options
            });
        }

        return this;
    },

Option name | Type | Description
--- | --- | ---
point | Point | The point to be drawn
options | object | Optional drawing options
redraw | boolean | Indicates if the current draw call is happening during a redraw

Draws a point on the screen.

    point: function (point, options, redraw) {
        if (typeof options === "undefined") { options = {}; }
        if (typeof redraw === "undefined") { redraw = false; }
        var screen = this.screen, ctx = this.ctx, prop, opts, dist;

        ctx.save();
        ctx.lineWidth = (options.lineWidth || 4) / (screen.scale.x - screen.scale.y);

        // Set the drawing options
        if (options) {
            opts = MathLib.Canvas.convertOptions(options);

            if (!('fillColor' in options) && !('color' in options)) {
                opts.fillStyle = 'black';
            }

            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    ctx[prop] = opts[prop];
                }
            }

            if ('setLineDash' in ctx) {
                ctx.setLineDash(('dash' in options ? options.dash : []));
            }
            if ('lineDashOffset' in ctx) {
                ctx.lineDashOffset = ('dashOffset' in options ? options.dashOffset : 0);
            }
        }

        // Draw the point
        ctx.beginPath();
        ctx.arc(point[0] / point[2], point[1] / point[2], (options.size || 10) / (screen.scale.x - screen.scale.y), 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.restore();

        if (options.label) {
            dist = 1.75 * (options.size || 10) + 0.75 * (options.lineWidth || 4);
            screen.text(options.label, point[0] / point[2] + dist / (screen.scale.x - screen.scale.y), point[1] / point[2] + dist / (screen.scale.x - screen.scale.y), options, true);
        }

        if (!redraw) {
            this.stack.push({
                type: 'point',
                object: point,
                options: options
            });
        }

        return this;
    },

Option name | Type | Description
--- | --- | ---
str | string | The string to be drawn
x | number | The x coordinate
y | number | The y coordinate
options | object | Optional drawing options

Writes text on the screen.

    text: function (str, x, y, options, redraw) {
        if (typeof options === "undefined") { options = {}; }
        if (typeof redraw === "undefined") { redraw = false; }
        var defaults = {
            font: 'Helvetica',
            fontSize: 12,
            //				lineWidth:  0.05,
            textColor: 'rgba(0, 0, 0, 1)'
        }, ctx, prop, opts;

        ctx = this.ctx;

        opts = MathLib.Canvas.convertOptions(MathLib.extendObject(defaults, options));

        for (prop in opts) {
            if (opts.hasOwnProperty(prop)) {
                ctx[prop] = opts[prop];
            }
        }

        ctx.fillStyle = MathLib.colorConvert(options.textColor || options.color || defaults.textColor);
        ctx.strokeStyle = MathLib.colorConvert(options.textColor || options.color || defaults.textColor);

        ctx.font = opts.fontSize + 'px ' + opts.font;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';

        var tf = this.screen.transformation;

        ctx.save();
        ctx.transform(1 / tf[0][0], 0, 0, 1 / tf[1][1], 0, 0);
        ctx.fillText(str, tf[0][0] * x, tf[1][1] * y);
        ctx.restore();

        if (!redraw) {
            this.stack.push({
                type: 'text',
                object: str,
                x: x,
                y: y,
                options: options
            });
        }

        return this;
    }
        };
    })(MathLib || (MathLib = {}));