/**
 * vivus - JavaScript library to make drawing animation on SVG
 * @version v0.4.2
 * @link https://github.com/maxwellito/vivus
 * @license MIT
 */

'use strict';

(function () {

    'use strict';

    /**
     * Pathformer
     * Beta version
     *
     * Take any SVG version 1.1 and transform
     * child elements to 'path' elements
     *
     * This code is purely forked from
     * https://github.com/Waest/SVGPathConverter
     */

    /**
     * Class constructor
     *
     * @param {DOM|String} element Dom element of the SVG or id of it
     */
    function Pathformer(element) {
        // Test params
        if (typeof element === 'undefined') {
            throw new Error('Pathformer [constructor]: "element" parameter is required');
        }

        // Set the element
        if (element.constructor === String) {
            element = document.getElementById(element);
            if (!element) {
                throw new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID');
            }
        }
        if (element instanceof window.SVGElement ||
            element instanceof window.SVGGElement ||
            /^svg$/i.test(element.nodeName)) {
            this.el = element;
        } else {
            throw new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement');
        }

        // Start
        this.scan(element);
    }

    /**
     * List of tags which can be transformed
     * to path elements
     *
     * @type {Array}
     */
    Pathformer.prototype.TYPES = ['line', 'ellipse', 'circle', 'polygon', 'polyline', 'rect'];

    /**
     * List of attribute names which contain
     * data. This array list them to check if
     * they contain bad values, like percentage.
     *
     * @type {Array}
     */
    Pathformer.prototype.ATTR_WATCH = ['cx', 'cy', 'points', 'r', 'rx', 'ry', 'x', 'x1', 'x2', 'y', 'y1', 'y2'];

    /**
     * Finds the elements compatible for transform
     * and apply the liked method
     *
     * @param  {object} options Object from the constructor
     */
    Pathformer.prototype.scan = function (svg) {
        var fn, element, pathData, pathDom,
            elements = svg.querySelectorAll(this.TYPES.join(','));

        for (var i = 0; i < elements.length; i++) {
            element = elements[i];
            fn = this[element.tagName.toLowerCase() + 'ToPath'];
            pathData = fn(this.parseAttr(element.attributes));
            pathDom = this.pathMaker(element, pathData);
            element.parentNode.replaceChild(pathDom, element);
        }
    };


    /**
     * Read `line` element to extract and transform
     * data, to make it ready for a `path` object.
     *
     * @param  {DOMelement} element Line element to transform
     * @return {object}             Data for a `path` element
     */
    Pathformer.prototype.lineToPath = function (element) {
        var newElement = {},
            x1 = element.x1 || 0,
            y1 = element.y1 || 0,
            x2 = element.x2 || 0,
            y2 = element.y2 || 0;

        newElement.d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2;
        return newElement;
    };

    /**
     * Read `rect` element to extract and transform
     * data, to make it ready for a `path` object.
     * The radius-border is not taken in charge yet.
     * (your help is more than welcomed)
     *
     * @param  {DOMelement} element Rect element to transform
     * @return {object}             Data for a `path` element
     */
    Pathformer.prototype.rectToPath = function (element) {
        var newElement = {},
            x      = parseFloat(element.x)      || 0,
            y      = parseFloat(element.y)      || 0,
            width  = parseFloat(element.width)  || 0,
            height = parseFloat(element.height) || 0;

        if (element.rx || element.ry) {
            var rx = parseInt(element.rx, 10) || -1,
                ry = parseInt(element.ry, 10) || -1;
            rx = Math.min(Math.max(rx < 0 ? ry : rx, 0), width/2);
            ry = Math.min(Math.max(ry < 0 ? rx : ry, 0), height/2);

            newElement.d = 'M ' + (x + rx) + ',' + y + ' ' +
                'L ' + (x + width - rx) + ',' + y + ' ' +
                'A ' + rx + ',' + ry + ',0,0,1,' + (x + width) + ',' + (y + ry) + ' ' +
                'L ' + (x + width) + ',' + (y + height - ry) + ' ' +
                'A ' + rx + ',' + ry + ',0,0,1,' + (x + width - rx) + ',' + (y + height) + ' ' +
                'L ' + (x + rx) + ',' + (y + height) + ' ' +
                'A ' + rx + ',' + ry + ',0,0,1,' + x + ',' + (y + height - ry) + ' ' +
                'L ' + x + ',' + (y + ry) + ' ' +
                'A ' + rx + ',' + ry + ',0,0,1,' + (x + rx) + ',' + y;
        }
        else {
            newElement.d = 'M' + x + ' ' + y + ' ' +
                'L' + (x + width) + ' ' + y + ' ' +
                'L' + (x + width) + ' ' + (y + height) + ' ' +
                'L' + x + ' ' + (y + height) + ' Z';
        }
        return newElement;
    };

    /**
     * Read `polyline` element to extract and transform
     * data, to make it ready for a `path` object.
     *
     * @param  {DOMelement} element Polyline element to transform
     * @return {object}             Data for a `path` element
     */
    Pathformer.prototype.polylineToPath = function (element) {
        var newElement = {},
            points = element.points.trim().split(' '),
            i, path;

        // Reformatting if points are defined without commas
        if (element.points.indexOf(',') === -1) {
            var formattedPoints = [];
            for (i = 0; i < points.length; i+=2) {
                formattedPoints.push(points[i] + ',' + points[i+1]);
            }
            points = formattedPoints;
        }

        // Generate the path.d value
        path = 'M' + points[0];
        for(i = 1; i < points.length; i++) {
            if (points[i].indexOf(',') !== -1) {
                path += 'L' + points[i];
            }
        }
        newElement.d = path;
        return newElement;
    };

    /**
     * Read `polygon` element to extract and transform
     * data, to make it ready for a `path` object.
     * This method rely on polylineToPath, because the
     * logic is similar. The path created is just closed,
     * so it needs an 'Z' at the end.
     *
     * @param  {DOMelement} element Polygon element to transform
     * @return {object}             Data for a `path` element
     */
    Pathformer.prototype.polygonToPath = function (element) {
        var newElement = Pathformer.prototype.polylineToPath(element);

        newElement.d += 'Z';
        return newElement;
    };

    /**
     * Read `ellipse` element to extract and transform
     * data, to make it ready for a `path` object.
     *
     * @param  {DOMelement} element ellipse element to transform
     * @return {object}             Data for a `path` element
     */
    Pathformer.prototype.ellipseToPath = function (element) {
        var newElement = {},
            rx = parseFloat(element.rx) || 0,
            ry = parseFloat(element.ry) || 0,
            cx = parseFloat(element.cx) || 0,
            cy = parseFloat(element.cy) || 0,
            startX = cx - rx,
            startY = cy,
            endX = parseFloat(cx) + parseFloat(rx),
            endY = cy;

        newElement.d = 'M' + startX + ',' + startY +
            'A' + rx + ',' + ry + ' 0,1,1 ' + endX + ',' + endY +
            'A' + rx + ',' + ry + ' 0,1,1 ' + startX + ',' + endY;
        return newElement;
    };

    /**
     * Read `circle` element to extract and transform
     * data, to make it ready for a `path` object.
     *
     * @param  {DOMelement} element Circle element to transform
     * @return {object}             Data for a `path` element
     */
    Pathformer.prototype.circleToPath = function (element) {
        var newElement = {},
            r  = parseFloat(element.r)  || 0,
            cx = parseFloat(element.cx) || 0,
            cy = parseFloat(element.cy) || 0,
            startX = cx - r,
            startY = cy,
            endX = parseFloat(cx) + parseFloat(r),
            endY = cy;

        newElement.d =  'M' + startX + ',' + startY +
            'A' + r + ',' + r + ' 0,1,1 ' + endX + ',' + endY +
            'A' + r + ',' + r + ' 0,1,1 ' + startX + ',' + endY;
        return newElement;
    };

    /**
     * Create `path` elements form original element
     * and prepared objects
     *
     * @param  {DOMelement} element  Original element to transform
     * @param  {object} pathData     Path data (from `toPath` methods)
     * @return {DOMelement}          Path element
     */
    Pathformer.prototype.pathMaker = function (element, pathData) {
        var i, attr, pathTag = document.createElementNS('http://www.w3.org/2000/svg','path');
        for(i = 0; i < element.attributes.length; i++) {
            attr = element.attributes[i];
            if (this.ATTR_WATCH.indexOf(attr.name) === -1) {
                pathTag.setAttribute(attr.name, attr.value);
            }
        }
        for(i in pathData) {
            pathTag.setAttribute(i, pathData[i]);
        }
        return pathTag;
    };

    /**
     * Parse attributes of a DOM element to
     * get an object of attribute => value
     *
     * @param  {NamedNodeMap} attributes Attributes object from DOM element to parse
     * @return {object}                  Object of attributes
     */
    Pathformer.prototype.parseAttr = function (element) {
        var attr, output = {};
        for (var i = 0; i < element.length; i++) {
            attr = element[i];
            // Check if no data attribute contains '%', or the transformation is impossible
            if (this.ATTR_WATCH.indexOf(attr.name) !== -1 && attr.value.indexOf('%') !== -1) {
                throw new Error('Pathformer [parseAttr]: a SVG shape got values in percentage. This cannot be transformed into \'path\' tags. Please use \'viewBox\'.');
            }
            output[attr.name] = attr.value;
        }
        return output;
    };

    'use strict';

    var setupEnv, requestAnimFrame, cancelAnimFrame, parsePositiveInt;

    /**
     * Vivus
     * Beta version
     *
     * Take any SVG and make the animation
     * to give give the impression of live drawing
     *
     * This in more than just inspired from codrops
     * At that point, it's a pure fork.
     */

    /**
     * Class constructor
     * option structure
     *   type: 'delayed'|'sync'|'oneByOne'|'script' (to know if the items must be drawn synchronously or not, default: delayed)
     *   duration: <int> (in frames)
     *   start: 'inViewport'|'manual'|'autostart' (start automatically the animation, default: inViewport)
     *   delay: <int> (delay between the drawing of first and last path)
     *   dashGap <integer> whitespace extra margin between dashes
     *   pathTimingFunction <function> timing animation function for each path element of the SVG
     *   animTimingFunction <function> timing animation function for the complete SVG
     *   forceRender <boolean> force the browser to re-render all updated path items
     *   selfDestroy <boolean> removes all extra styling on the SVG, and leaves it as original
     *
     * The attribute 'type' is by default on 'delayed'.
     *  - 'delayed'
     *    all paths are draw at the same time but with a
     *    little delay between them before start
     *  - 'sync'
     *    all path are start and finish at the same time
     *  - 'oneByOne'
     *    only one path is draw at the time
     *    the end of the first one will trigger the draw
     *    of the next one
     *
     * All these values can be overwritten individually
     * for each path item in the SVG
     * The value of frames will always take the advantage of
     * the duration value.
     * If you fail somewhere, an error will be thrown.
     * Good luck.
     *
     * @constructor
     * @this {Vivus}
     * @param {DOM|String}   element  Dom element of the SVG or id of it
     * @param {Object}       options  Options about the animation
     * @param {Function}     callback Callback for the end of the animation
     */
    function Vivus (element, options, callback) {

        setupEnv();

        // Setup
        this.isReady = false;
        this.setElement(element, options);
        this.setOptions(options);
        this.setCallback(callback);

        if (this.isReady) {
            this.init();
        }
    }

    /**
     * Timing functions
     **************************************
     *
     * Default functions to help developers.
     * It always take a number as parameter (between 0 to 1) then
     * return a number (between 0 and 1)
     */
    Vivus.LINEAR          = function (x) {return x;};
    Vivus.EASE            = function (x) {return -Math.cos(x * Math.PI) / 2 + 0.5;};
    Vivus.EASE_OUT        = function (x) {return 1 - Math.pow(1-x, 3);};
    Vivus.EASE_IN         = function (x) {return Math.pow(x, 3);};
    Vivus.EASE_OUT_BOUNCE = function (x) {
        var base = -Math.cos(x * (0.5 * Math.PI)) + 1,
            rate = Math.pow(base,1.5),
            rateR = Math.pow(1 - x, 2),
            progress = -Math.abs(Math.cos(rate * (2.5 * Math.PI) )) + 1;
        return (1- rateR) + (progress * rateR);
    };


    /**
     * Setters
     **************************************
     */

    /**
     * Check and set the element in the instance
     * The method will not return anything, but will throw an
     * error if the parameter is invalid
     *
     * @param {DOM|String}   element  SVG Dom element or id of it
     */
    Vivus.prototype.setElement = function (element, options) {
        // Basic check
        if (typeof element === 'undefined') {
            throw new Error('Vivus [constructor]: "element" parameter is required');
        }

        // Set the element
        if (element.constructor === String) {
            element = document.getElementById(element);
            if (!element) {
                throw new Error('Vivus [constructor]: "element" parameter is not related to an existing ID');
            }
        }
        this.parentEl = element;

        // Create the object element if the property `file` exists in the options object
        if (options && options.file) {
            var objElm = document.createElement('object');
            objElm.setAttribute('type', 'image/svg+xml');
            objElm.setAttribute('data', options.file);
            objElm.setAttribute('built-by-vivus', 'true');
            element.appendChild(objElm);
            element = objElm;
        }

        switch (element.constructor) {
            case window.SVGSVGElement:
            case window.SVGElement:
            case window.SVGGElement:
                this.el = element;
                this.isReady = true;
                break;

            case window.HTMLObjectElement:
                // If we have to wait for it
                var onLoad, self;

                self = this;
                onLoad = function (e) {
                    if (self.isReady) {
                        return;
                    }
                    self.el = element.contentDocument && element.contentDocument.querySelector('svg');
                    if (!self.el && e) {
                        throw new Error('Vivus [constructor]: object loaded does not contain any SVG');
                    }
                    else if (self.el) {
                        if (element.getAttribute('built-by-vivus')) {
                            self.parentEl.insertBefore(self.el, element);
                            self.parentEl.removeChild(element);
                            self.el.setAttribute('width', '100%');
                            self.el.setAttribute('height', '100%');
                        }
                        self.isReady = true;
                        self.init();
                        return true;
                    }
                };

                if (!onLoad()) {
                    element.addEventListener('load', onLoad);
                }
                break;

            default:
                throw new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)');
        }
    };

    /**
     * Set up user option to the instance
     * The method will not return anything, but will throw an
     * error if the parameter is invalid
     *
     * @param  {object} options Object from the constructor
     */
    Vivus.prototype.setOptions = function (options) {
        var allowedTypes = ['delayed', 'sync', 'async', 'nsync', 'oneByOne', 'scenario', 'scenario-sync'];
        var allowedStarts =  ['inViewport', 'manual', 'autostart'];

        // Basic check
        if (options !== undefined && options.constructor !== Object) {
            throw new Error('Vivus [constructor]: "options" parameter must be an object');
        }
        else {
            options = options || {};
        }

        // Set the animation type
        if (options.type && allowedTypes.indexOf(options.type) === -1) {
            throw new Error('Vivus [constructor]: ' + options.type + ' is not an existing animation `type`');
        }
        else {
            this.type = options.type || allowedTypes[0];
        }

        // Set the start type
        if (options.start && allowedStarts.indexOf(options.start) === -1) {
            throw new Error('Vivus [constructor]: ' + options.start + ' is not an existing `start` option');
        }
        else {
            this.start = options.start || allowedStarts[0];
        }

        this.isIE         = (window.navigator.userAgent.indexOf('MSIE') !== -1 || window.navigator.userAgent.indexOf('Trident/') !== -1 || window.navigator.userAgent.indexOf('Edge/') !== -1 );
        this.duration     = parsePositiveInt(options.duration, 120);
        this.delay        = parsePositiveInt(options.delay, null);
        this.dashGap      = parsePositiveInt(options.dashGap, 1);
        this.forceRender  = options.hasOwnProperty('forceRender') ? !!options.forceRender : this.isIE;
        this.reverseStack = !!options.reverseStack;
        this.selfDestroy  = !!options.selfDestroy;
        this.onReady      = options.onReady;
        this.map          = [];
        this.frameLength  = this.currentFrame = this.delayUnit = this.speed = this.handle = null;

        this.ignoreInvisible = options.hasOwnProperty('ignoreInvisible') ? !!options.ignoreInvisible : false;

        this.animTimingFunction = options.animTimingFunction || Vivus.LINEAR;
        this.pathTimingFunction = options.pathTimingFunction || Vivus.LINEAR;

        if (this.delay >= this.duration) {
            throw new Error('Vivus [constructor]: delay must be shorter than duration');
        }
    };

    /**
     * Set up callback to the instance
     * The method will not return enything, but will throw an
     * error if the parameter is invalid
     *
     * @param  {Function} callback Callback for the animation end
     */
    Vivus.prototype.setCallback = function (callback) {
        // Basic check
        if (!!callback && callback.constructor !== Function) {
            throw new Error('Vivus [constructor]: "callback" parameter must be a function');
        }
        this.callback = callback || function () {};
    };


    /**
     * Core
     **************************************
     */

    /**
     * Map the svg, path by path.
     * The method return nothing, it just fill the
     * `map` array. Each item in this array represent
     * a path element from the SVG, with informations for
     * the animation.
     *
     * ```
     * [
     *   {
 *     el: <DOMobj> the path element
 *     length: <number> length of the path line
 *     startAt: <number> time start of the path animation (in frames)
 *     duration: <number> path animation duration (in frames)
 *   },
     *   ...
     * ]
     * ```
     *
     */
    Vivus.prototype.mapping = function () {
        var i, paths, path, pAttrs, pathObj, totalLength, lengthMeter, timePoint;
        timePoint = totalLength = lengthMeter = 0;
        paths = this.el.querySelectorAll('path');

        for (i = 0; i < paths.length; i++) {
            path = paths[i];
            if (this.isInvisible(path)) {
                continue;
            }
            pathObj = {
                el: path,
                length: Math.ceil(path.getTotalLength())
            };
            // Test if the path length is correct
            if (isNaN(pathObj.length)) {
                if (window.console && console.warn) {
                    console.warn('Vivus [mapping]: cannot retrieve a path element length', path);
                }
                continue;
            }
            this.map.push(pathObj);
            path.style.strokeDasharray  = pathObj.length + ' ' + (pathObj.length + this.dashGap * 2);
            path.style.strokeDashoffset = pathObj.length + this.dashGap;
            pathObj.length += this.dashGap;
            totalLength += pathObj.length;

            this.renderPath(i);
        }

        totalLength = totalLength === 0 ? 1 : totalLength;
        this.delay = this.delay === null ? this.duration / 3 : this.delay;
        this.delayUnit = this.delay / (paths.length > 1 ? paths.length - 1 : 1);

        // Reverse stack if asked
        if (this.reverseStack) {
            this.map.reverse();
        }

        for (i = 0; i < this.map.length; i++) {
            pathObj = this.map[i];

            switch (this.type) {
                case 'delayed':
                    pathObj.startAt = this.delayUnit * i;
                    pathObj.duration = this.duration - this.delay;
                    break;

                case 'oneByOne':
                    pathObj.startAt = lengthMeter / totalLength * this.duration;
                    pathObj.duration = pathObj.length / totalLength * this.duration;
                    break;

                case 'sync':
                case 'async':
                case 'nsync':
                    pathObj.startAt = 0;
                    pathObj.duration = this.duration;
                    break;

                case 'scenario-sync':
                    path = pathObj.el;
                    pAttrs = this.parseAttr(path);
                    pathObj.startAt = timePoint + (parsePositiveInt(pAttrs['data-delay'], this.delayUnit) || 0);
                    pathObj.duration = parsePositiveInt(pAttrs['data-duration'], this.duration);
                    timePoint = pAttrs['data-async'] !== undefined ? pathObj.startAt : pathObj.startAt + pathObj.duration;
                    this.frameLength = Math.max(this.frameLength, (pathObj.startAt + pathObj.duration));
                    break;

                case 'scenario':
                    path = pathObj.el;
                    pAttrs = this.parseAttr(path);
                    pathObj.startAt = parsePositiveInt(pAttrs['data-start'], this.delayUnit) || 0;
                    pathObj.duration = parsePositiveInt(pAttrs['data-duration'], this.duration);
                    this.frameLength = Math.max(this.frameLength, (pathObj.startAt + pathObj.duration));
                    break;
            }
            lengthMeter += pathObj.length;
            this.frameLength = this.frameLength || this.duration;
        }
    };

    /**
     * Interval method to draw the SVG from current
     * position of the animation. It update the value of
     * `currentFrame` and re-trace the SVG.
     *
     * It use this.handle to store the requestAnimationFrame
     * and clear it one the animation is stopped. So this
     * attribute can be used to know if the animation is
     * playing.
     *
     * Once the animation at the end, this method will
     * trigger the Vivus callback.
     *
     */
    Vivus.prototype.drawer = function () {
        var self = this;
        this.currentFrame += this.speed;

        if (this.currentFrame <= 0) {
            this.stop();
            this.reset();
        } else if (this.currentFrame >= this.frameLength) {
            this.stop();
            this.currentFrame = this.frameLength;
            this.trace();
            if (this.selfDestroy) {
                this.destroy();
            }
        } else {
            this.trace();
            this.handle = requestAnimFrame(function () {
                self.drawer();
            });
            return;
        }

        this.callback(this);
        if (this.instanceCallback) {
            this.instanceCallback(this);
            this.instanceCallback = null;
        }
    };

    /**
     * Draw the SVG at the current instant from the
     * `currentFrame` value. Here is where most of the magic is.
     * The trick is to use the `strokeDashoffset` style property.
     *
     * For optimisation reasons, a new property called `progress`
     * is added in each item of `map`. This one contain the current
     * progress of the path element. Only if the new value is different
     * the new value will be applied to the DOM element. This
     * method save a lot of resources to re-render the SVG. And could
     * be improved if the animation couldn't be played forward.
     *
     */
    Vivus.prototype.trace = function () {
        var i, progress, path, currentFrame;
        currentFrame = this.animTimingFunction(this.currentFrame / this.frameLength) * this.frameLength;
        for (i = 0; i < this.map.length; i++) {
            path = this.map[i];
            progress = (currentFrame - path.startAt) / path.duration;
            progress = this.pathTimingFunction(Math.max(0, Math.min(1, progress)));
            if (path.progress !== progress) {
                path.progress = progress;
                path.el.style.strokeDashoffset = Math.floor(path.length * (1 - progress));
                this.renderPath(i);
            }
        }
    };

    /**
     * Method forcing the browser to re-render a path element
     * from it's index in the map. Depending on the `forceRender`
     * value.
     * The trick is to replace the path element by it's clone.
     * This practice is not recommended because it's asking more
     * ressources, too much DOM manupulation..
     * but it's the only way to let the magic happen on IE.
     * By default, this fallback is only applied on IE.
     *
     * @param  {Number} index Path index
     */
    Vivus.prototype.renderPath = function (index) {
        if (this.forceRender && this.map && this.map[index]) {
            var pathObj = this.map[index],
                newPath = pathObj.el.cloneNode(true);
            pathObj.el.parentNode.replaceChild(newPath, pathObj.el);
            pathObj.el = newPath;
        }
    };

    /**
     * When the SVG object is loaded and ready,
     * this method will continue the initialisation.
     *
     * This this mainly due to the case of passing an
     * object tag in the constructor. It will wait
     * the end of the loading to initialise.
     *
     */
    Vivus.prototype.init = function () {
        // Set object variables
        this.frameLength = 0;
        this.currentFrame = 0;
        this.map = [];

        // Start
        new Pathformer(this.el);
        this.mapping();
        this.starter();

        if (this.onReady) {
            this.onReady(this);
        }
    };

    /**
     * Trigger to start of the animation.
     * Depending on the `start` value, a different script
     * will be applied.
     *
     * If the `start` value is not valid, an error will be thrown.
     * Even if technically, this is impossible.
     *
     */
    Vivus.prototype.starter = function () {
        switch (this.start) {
            case 'manual':
                return;

            case 'autostart':
                this.play();
                break;

            case 'inViewport':
                var self = this,
                    listener = function () {
                        if (self.isInViewport(self.parentEl, 1)) {
                            self.play();
                            window.removeEventListener('scroll', listener);
                        }
                    };
                window.addEventListener('scroll', listener);
                listener();
                break;
        }
    };


    /**
     * Controls
     **************************************
     */

    /**
     * Get the current status of the animation between
     * three different states: 'start', 'progress', 'end'.
     * @return {string} Instance status
     */
    Vivus.prototype.getStatus = function () {
        return this.currentFrame === 0 ? 'start' : this.currentFrame === this.frameLength ? 'end' : 'progress';
    };

    /**
     * Reset the instance to the initial state : undraw
     * Be careful, it just reset the animation, if you're
     * playing the animation, this won't stop it. But just
     * make it start from start.
     *
     */
    Vivus.prototype.reset = function () {
        return this.setFrameProgress(0);
    };

    /**
     * Set the instance to the final state : drawn
     * Be careful, it just set the animation, if you're
     * playing the animation on rewind, this won't stop it.
     * But just make it start from the end.
     *
     */
    Vivus.prototype.finish = function () {
        return this.setFrameProgress(1);
    };

    /**
     * Set the level of progress of the drawing.
     *
     * @param {number} progress Level of progress to set
     */
    Vivus.prototype.setFrameProgress = function (progress) {
        progress = Math.min(1, Math.max(0, progress));
        this.currentFrame = Math.round(this.frameLength * progress);
        this.trace();
        return this;
    };

    /**
     * Play the animation at the desired speed.
     * Speed must be a valid number (no zero).
     * By default, the speed value is 1.
     * But a negative value is accepted to go forward.
     *
     * And works with float too.
     * But don't forget we are in JavaScript, se be nice
     * with him and give him a 1/2^x value.
     *
     * @param  {number} speed Animation speed [optional]
     */
    Vivus.prototype.play = function (speed, callback) {
        this.instanceCallback = null;

        if (speed && typeof speed === 'function') {
            this.instanceCallback = speed; // first parameter is actually the callback function
            speed = null;
        }
        else if (speed && typeof speed !== 'number') {
            throw new Error('Vivus [play]: invalid speed');
        }
        // if the first parameter wasn't the callback, check if the seconds was
        if (callback && typeof(callback) === 'function' && !this.instanceCallback) {
            this.instanceCallback = callback;
        }


        this.speed = speed || 1;
        if (!this.handle) {
            this.drawer();
        }
        return this;
    };

    /**
     * Stop the current animation, if on progress.
     * Should not trigger any error.
     *
     */
    Vivus.prototype.stop = function () {
        if (this.handle) {
            cancelAnimFrame(this.handle);
            this.handle = null;
        }
        return this;
    };

    /**
     * Destroy the instance.
     * Remove all bad styling attributes on all
     * path tags
     *
     */
    Vivus.prototype.destroy = function () {
        this.stop();
        var i, path;
        for (i = 0; i < this.map.length; i++) {
            path = this.map[i];
            path.el.style.strokeDashoffset = null;
            path.el.style.strokeDasharray = null;
            this.renderPath(i);
        }
    };


    /**
     * Utils methods
     * include methods from Codrops
     **************************************
     */

    /**
     * Method to best guess if a path should added into
     * the animation or not.
     *
     * 1. Use the `data-vivus-ignore` attribute if set
     * 2. Check if the instance must ignore invisible paths
     * 3. Check if the path is visible
     *
     * For now the visibility checking is unstable.
     * It will be used for a beta phase.
     *
     * Other improvments are planned. Like detecting
     * is the path got a stroke or a valid opacity.
     */
    Vivus.prototype.isInvisible = function (el) {
        var rect,
            ignoreAttr = el.getAttribute('data-ignore');

        if (ignoreAttr !== null) {
            return ignoreAttr !== 'false';
        }

        if (this.ignoreInvisible) {
            rect = el.getBoundingClientRect();
            return !rect.width && !rect.height;
        }
        else {
            return false;
        }
    };

    /**
     * Parse attributes of a DOM element to
     * get an object of {attributeName => attributeValue}
     *
     * @param  {object} element DOM element to parse
     * @return {object}         Object of attributes
     */
    Vivus.prototype.parseAttr = function (element) {
        var attr, output = {};
        if (element && element.attributes) {
            for (var i = 0; i < element.attributes.length; i++) {
                attr = element.attributes[i];
                output[attr.name] = attr.value;
            }
        }
        return output;
    };

    /**
     * Reply if an element is in the page viewport
     *
     * @param  {object} el Element to observe
     * @param  {number} h  Percentage of height
     * @return {boolean}
     */
    Vivus.prototype.isInViewport = function (el, h) {
        var scrolled   = this.scrollY(),
            viewed       = scrolled + this.getViewportH(),
            elBCR        = el.getBoundingClientRect(),
            elHeight     = elBCR.height,
            elTop        = scrolled + elBCR.top,
            elBottom     = elTop + elHeight;

        // if 0, the element is considered in the viewport as soon as it enters.
        // if 1, the element is considered in the viewport only when it's fully inside
        // value in percentage (1 >= h >= 0)
        h = h || 0;

        return (elTop + elHeight * h) <= viewed && (elBottom) >= scrolled;
    };


    /**
     * Get the viewport height in pixels
     *
     * @return {integer} Viewport height
     */
    Vivus.prototype.getViewportH = function () {
        var client = this.docElem.clientHeight,
            inner = window.innerHeight;

        if (client < inner) {
            return inner;
        }
        else {
            return client;
        }
    };

    /**
     * Get the page Y offset
     *
     * @return {integer} Page Y offset
     */
    Vivus.prototype.scrollY = function () {
        return window.pageYOffset || this.docElem.scrollTop;
    };

    setupEnv = function () {

        if (Vivus.prototype.docElem) {
            return;
        }

        /**
         * Alias for document element
         *
         * @type {DOMelement}
         */
        Vivus.prototype.docElem = window.document.documentElement;

        /**
         * Alias for `requestAnimationFrame` or
         * `setTimeout` function for deprecated browsers.
         *
         */
        requestAnimFrame = (function () {
            return (
                window.requestAnimationFrame       ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame    ||
                window.oRequestAnimationFrame      ||
                window.msRequestAnimationFrame     ||
                function(/* function */ callback){
                    return window.setTimeout(callback, 1000 / 60);
                }
            );
        })();

        /**
         * Alias for `cancelAnimationFrame` or
         * `cancelTimeout` function for deprecated browsers.
         *
         */
        cancelAnimFrame = (function () {
            return (
                window.cancelAnimationFrame       ||
                window.webkitCancelAnimationFrame ||
                window.mozCancelAnimationFrame    ||
                window.oCancelAnimationFrame      ||
                window.msCancelAnimationFrame     ||
                function(id){
                    return window.clearTimeout(id);
                }
            );
        })();
    };

    /**
     * Parse string to integer.
     * If the number is not positive or null
     * the method will return the default value
     * or 0 if undefined
     *
     * @param {string} value String to parse
     * @param {*} defaultValue Value to return if the result parsed is invalid
     * @return {number}
     *
     */
    parsePositiveInt = function (value, defaultValue) {
        var output = parseInt(value, 10);
        return (output >= 0) ? output : defaultValue;
    };


    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], function() {
            return Vivus;
        });
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = Vivus;
    } else {
        // Browser globals
        window.Vivus = Vivus;
    }

}());