# SVG

#####

property
MathLib.SVG

The SVG renderer for 2D plotting

    MathLib.SVG = {

Applies the current transformations to the screen

    applyTransformation: function () {
        var m = this.transformation;
        this.layer.forEach(function (l) {
            l.ctx.setAttribute('transform', 'matrix(' + 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, prop, opts, svgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');

        svgCircle.setAttribute('cx', circle.center[0]);
        svgCircle.setAttribute('cy', circle.center[1]);
        svgCircle.setAttribute('r', circle.radius);

        if (options) {
            svgCircle.setAttribute('stroke-width', (options.lineWidth || 4) / (screen.scale.x - screen.scale.y) + '');
            opts = MathLib.SVG.convertOptions(options);
            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    svgCircle.setAttribute(prop, opts[prop]);
                }
            }
        }

        this.ctx.appendChild(svgCircle);

        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) {
        layer.ctx.textContent = '';
    },

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

Converts the options to the SVG options format

    convertOptions: function (options) {
        var convertedOptions = {};
        if ('fillColor' in options) {
            convertedOptions.fill = MathLib.colorConvert(options.fillColor);
        } else if ('color' in options) {
            convertedOptions.fill = 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.stroke = MathLib.colorConvert(options.lineColor);
        } else if ('color' in options) {
            convertedOptions.stroke = MathLib.colorConvert(options.color);
        }

        if ('dash' in options && options.dash.length !== 0) {
            convertedOptions['stroke-dasharray'] = options.dash;
        }

        if ('dashOffset' in options && options.dashOffset !== 0) {
            convertedOptions['stroke-dashoffset'] = options.dashOffset;
        }

        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, prop, opts, svgLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');

        // 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);
        }

        svgLine.setAttribute('x1', points[0][0]);
        svgLine.setAttribute('y1', points[0][1]);
        svgLine.setAttribute('x2', points[1][0]);
        svgLine.setAttribute('y2', points[1][1]);

        if (options) {
            svgLine.setAttribute('stroke-width', (options.lineWidth || 4) / (screen.scale.x - screen.scale.y) + '');
            opts = MathLib.SVG.convertOptions(options);
            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    svgLine.setAttribute(prop, opts[prop]);
                }
            }
        }

        this.ctx.appendChild(svgLine);

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

        return this;
    },

Option name | Type | Description
--- | --- | ---
curve | any | 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, svgPathStroke = document.createElementNS('http://www.w3.org/2000/svg', 'path'), svgPathFill = document.createElementNS('http://www.w3.org/2000/svg', 'path'), step = 2 / (screen.scale.x - screen.scale.y), pathStringFill, pathStringStroke, from, to, prop, opts, x, y, i, path, paths = [], fx, fxold;

        // 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
                // For more info see the corresponding function for canvas
                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;
        }

        pathStringFill = 'M' + from + ' 0 ' + paths.reduce(function (previ, path) {
            return previ + ' L ' + path[0][0] + ' 0 ' + path.reduce(function (prev, cur) {
                return prev + ' L ' + cur.join(' ');
            }, '') + ' L ' + path[path.length - 1][0] + ' 0 ';
        }, '');

        pathStringStroke = paths.reduce(function (previ, path) {
            return previ + ' M ' + path[0].join(' ') + path.reduce(function (prev, cur) {
                return prev + ' L ' + cur.join(' ');
            }, '');
        }, '');

        if (pathStringFill !== '') {
            svgPathFill.setAttribute('d', pathStringFill);
        }
        if (pathStringStroke) {
            svgPathStroke.setAttribute('d', pathStringStroke);
        }

        svgPathStroke.setAttribute('stroke-width', (options.lineWidth || 4) / (screen.scale.x - screen.scale.y) + '');

        if (options) {
            opts = MathLib.SVG.convertOptions(options);
            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    svgPathFill.setAttribute(prop, opts[prop]);
                    svgPathStroke.setAttribute(prop, opts[prop]);
                }
            }
        }

        svgPathFill.setAttribute('stroke', 'transparent');
        svgPathStroke.setAttribute('fill', 'transparent');

        this.ctx.appendChild(svgPathFill);
        this.ctx.appendChild(svgPathStroke);

        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, canvas = document.createElement('canvas'), canvasCtx = canvas.getContext('2d'), svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image'), svgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'g'), x, y, i, pxl, m = screen.transformation;

        canvas.width = screen.width;
        canvas.height = screen.height;
        canvasCtx.setTransform(m[0][0], m[1][0], m[0][1], m[1][1], m[0][2], m[1][2]);

        svgContainer.setAttribute('transform', 'matrix(' + 1 / m[0][0] + ', 0, 0, ' + 1 / m[1][1] + ', -' + m[0][2] / m[0][0] + ', ' + -m[1][2] / m[1][1] + ')');
        svgImage.setAttribute('width', screen.width + 'px');
        svgImage.setAttribute('height', screen.height + 'px');
        svgImage.setAttribute('x', '0');
        svgImage.setAttribute('y', '0');

        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 = (tPxl - bPxl), imgData = canvasCtx.createImageData(w, h);

        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];
            }
        }

        canvasCtx.putImageData(imgData, 0, 0);

        svgImage.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', canvas.toDataURL());

        svgContainer.appendChild(svgImage);
        this.ctx.appendChild(svgContainer);

        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, prop, opts, dist, svgPoint = document.createElementNS('http://www.w3.org/2000/svg', 'circle');

        svgPoint.setAttribute('cx', point[0] / point[2] + '');
        svgPoint.setAttribute('cy', point[1] / point[2] + '');
        svgPoint.setAttribute('r', (options.size || 10) / (screen.scale.x - screen.scale.y) + '');

        if (options) {
            svgPoint.setAttribute('stroke-width', (options.lineWidth || 4) / (screen.scale.x - screen.scale.y) + '');
            opts = MathLib.SVG.convertOptions(options);

            if (!('fillOpacity' in options)) {
                opts['fill-opacity'] = '1';
            }

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

            for (prop in opts) {
                if (opts.hasOwnProperty(prop)) {
                    svgPoint.setAttribute(prop, opts[prop]);
                }
            }
        }

        if (options.moveable) {
            svgPoint.setAttribute('cursor', 'move');

            // mousedown
            svgPoint.addEventListener('mousedown', function () {
                screen.options.interaction.type = 'move';
                var invTransformation = screen.transformation.inverse();

                var move = function (evt) {
                    evt.stopPropagation();

                    var evtPoint = invTransformation.times(screen.getEventPoint(evt));
                    point[0] = evtPoint[0];
                    point[1] = evtPoint[1];
                    screen.draw();
                }, up = function () {
                    screen.options.interaction.type = '';

                    document.body.removeEventListener('mousemove', move);
                    document.body.removeEventListener('mouseup', up);
                };

                // mousemove
                document.body.addEventListener('mousemove', move);

                // mouseup
                document.body.addEventListener('mouseup', up);
            });
        }

        this.ctx.appendChild(svgPoint);

        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);
        }

        svgPoint.addEventListener('contextmenu', function () {
            screen.options.interaction.type = 'contextmenu';
            var x = svgPoint.cx.baseVal.value, y = svgPoint.cy.baseVal.value;

            screen.contextMenu.innerHTML = '<div class="MathLib_menuItem MathLib_temporaryMenuItem MathLib_is_disabled MathLib_is_centered">Point</div>' + '<div class="MathLib_menuItem MathLib_temporaryMenuItem MathLib_hasSubmenu">Coordinates' + '<menu class="MathLib_menu MathLib_submenu">' + '<div class="MathLib_menuItem">cartesian: <span class="MathLib_is_selectable MathLib_is_right">(' + x.toFixed(3) + ', ' + y.toFixed(3) + ')</span></div>' + '<div class="MathLib_menuItem">polar: <span class="MathLib_is_selectable MathLib_is_right">(' + MathLib.hypot(x, y).toFixed(3) + ', ' + Math.atan2(y, x).toFixed(3) + ')</span></div>' + '</menu>' + '</div>' + '<div class="MathLib_menuItem MathLib_temporaryMenuItem MathLib_hasSubmenu">Options' + '<menu class="MathLib_menu MathLib_submenu">' + '<div class="MathLib_menuItem">Moveable:' + '<input type="checkbox" class="MathLib_is_right">' + '</div>' + '<div class="MathLib_menuItem">Size:' + '<input type="spinner" class="MathLib_is_right">' + '</div>' + '<div class="MathLib_menuItem">Fill color:' + '<input type="color" class="MathLib_is_right">' + '</div>' + '<div class="MathLib_menuItem">Line color:' + '<input type="color" class="MathLib_is_right">' + '</div>' + '</menu>' + '</div>' + '<hr class="MathLib_separator MathLib_temporaryMenuItem">' + screen.contextMenu.innerHTML;
        });

        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
redraw | boolean | Indicates if the current draw call is happening during a redraw

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)'
        }, opts, screen = this.screen, svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text'), svgTspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');

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

        svgTspan.textContent = str;
        svgTspan.setAttribute('x', x * screen.scale.x + '');
        svgTspan.setAttribute('y', y * screen.scale.y + '');
        svgText.setAttribute('transform', 'matrix(' + 1 / screen.scale.x + ', 0, 0, ' + 1 / screen.scale.y + ', 0, 0)');
        svgText.setAttribute('font-family', opts.font);
        svgText.setAttribute('font-size', opts.fontSize);
        svgText.setAttribute('fill', MathLib.colorConvert(options.textColor || options.color) || defaults.textColor);
        svgText.setAttribute('fill-opacity', '1');
        svgText.setAttribute('stroke', MathLib.colorConvert(options.textColor || options.color) || defaults.textColor);
        svgText.setAttribute('text-anchor', 'middle');

        // alignment-baseline isn't defined for text elements,
        // only for ‘tspan’, ‘tref’, ‘altGlyph’, ‘textPath’ elements.
        // see the [Specification](http://www.w3.org/TR/SVG/text.html#AlignmentBaselineProperty)
        // But it works for text elements, so we don't need an additional tspan element.
        svgTspan.setAttribute('alignment-baseline', 'middle');

        svgText.appendChild(svgTspan);
        this.ctx.appendChild(svgText);

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

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