// Helper vars and functions.
    function extend( a, b ) {
        for( var key in b ) {
            if( b.hasOwnProperty( key ) ) {
                a[key] = b[key];
            }
        }
        return a;
    }

    // From https://davidwalsh.name/javascript-debounce-function.
    function debounce(func, wait, immediate) {
        var timeout;
        return function() {
            var context = this, args = arguments;
            var later = function() {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    };

    // Check if clip-path is supported. From http://stackoverflow.com/a/30041538.
    function areClipPathShapesSupported() {
        var base = 'clipPath',
            prefixes = [ 'webkit', 'moz', 'ms', 'o' ],
            properties = [ base ],
            testElement = document.createElement( 'testelement' ),
            attribute = 'polygon(50% 0%, 0% 100%, 100% 100%)';

        // Push the prefixed properties into the array of properties.
        for ( var i = 0, l = prefixes.length; i < l; i++ ) {
            var prefixedProperty = prefixes[i] + base.charAt( 0 ).toUpperCase() + base.slice( 1 ); // remember to capitalize!
            properties.push( prefixedProperty );
        }

        // Iterate over the properties and see if they pass two tests.
        for ( var i = 0, l = properties.length; i < l; i++ ) {
            var property = properties[i];

            // First, they need to support clip-path (IE <= 11 does not)...
            if ( testElement.style[property] === '' ) {

                // Second, we need to see what happens when we try to create a CSS shape...
                testElement.style[property] = attribute;
                if ( testElement.style[property] !== '' ) {
                    return true;
                }
            }
        }
        return false;
    };

    // From http://www.quirksmode.org/js/events_properties.html#position
    function getMousePos(e) {
        var posx = 0, posy = 0;
        if (!e) var e = window.event;
        if (e.pageX || e.pageY) 	{
            posx = e.pageX;
            posy = e.pageY;
        }
        else if (e.clientX || e.clientY) 	{
            posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
            posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
        }
        return { x : posx, y : posy }
    }

    // Returns a random number between min and max (inclusive).
    function getRandom(min, max) {
        return Math.random() * (max - min + 1) + min;
    }

    // Check for clip-path support.
    const isClipPathSupported = areClipPathShapesSupported();

    /**
     * FragmentsFx obj.
     */
    function FragmentsFx(el, options) {
        this.el = el;
        this.options = extend({}, this.options);
        extend(this.options, options);
        // Preload the element´s background image.
        var self = this;
        imagesLoaded(this.el, { background: true }, function() { self._init(); });
    }

    /**
     * FragmentsFx default options.
     */
    FragmentsFx.prototype.options = {
        // Number of fragments.
        fragments: 25,
        // The boundaries of the fragment translation (pixel values).
        boundaries: {x1: 100, x2: 100, y1: 50, y2: 50},
        // The area of the fragments in percentage values (clip-path).
        // We can also use random values by setting options.area to "random".
        area: 'random',
        /* example with 4 fragments (percentage values)
         [{top: 80, left: 10, width: 3, height: 20},{top: 2, left: 2, width: 4, height: 40},{top: 30, left: 60, width: 3, height: 60},{top: 10, left: 20, width: 50, height: 6}]
         */
        // If using area:"random", we can define the area´s minimum and maximum values for the clip-path. (percentage values)
        randomIntervals: {
            top: {min: 0,max: 90},
            left: {min: 0,max: 90},
            // Either the width or the height will be selected with a fixed value (+- 0.1) for the other dimension (percentage values).
            dimension: {
                width: {min: 10,max: 60, fixedHeight: 1.1},
                height: {min: 10,max: 60, fixedWidth: 1.1}
            }
        },
        parallax: false,
        // Range of movement for the parallax effect (pixel values).
        randomParallax: {min: 10, max: 150}
    };

    /**
     * Init. Create the layout and initialize/bind events.
     */
    FragmentsFx.prototype._init = function() {
        // The dimensions of the main element.
        this.dimensions = {width: this.el.offsetWidth, height: this.el.offsetHeight};
        // The source of the main image.
        this.imgsrc = this.el.style.backgroundImage.replace('url(','').replace(')','').replace(/\"/gi, "");
        // Render all the fragments defined in the options.
        this._layout();
        // Init/Bind events
        this._initEvents();
    };

    /**
     * Init/Bind events.
     */
    FragmentsFx.prototype._initEvents = function() {
        const self = this;

        // Parallax movement.
        if( this.options.parallax ) {
            this.mousemoveFn = function(ev) {
                requestAnimationFrame(function() {
                    // Mouse position relative to the document.
                    const mousepos = getMousePos(ev),
                        // Document scrolls.
                        docScrolls = {left : document.body.scrollLeft + document.documentElement.scrollLeft, top : document.body.scrollTop + document.documentElement.scrollTop},
                        bounds = self.el.getBoundingClientRect(),
                        // Mouse position relative to the main element (this.el).
                        relmousepos = { x : mousepos.x - bounds.left - docScrolls.left, y : mousepos.y - bounds.top - docScrolls.top };

                    // Movement settings for the animatable elements.
                    for(var i = 0, len = self.fragments.length; i <= len-1; ++i) {
                        const fragment = self.fragments[i],
                            t = fragment.getAttribute('data-parallax'),
                            transX = t/(self.dimensions.width)*relmousepos.x - t/2,
                            transY = t/(self.dimensions.height)*relmousepos.y - t/2;

                        fragment.style.transform = fragment.style.WebkitTransform = 'translate3d(' + transX + 'px,' + transY + 'px,0)';
                    }
                });
            };
            this.el.addEventListener('mousemove', this.mousemoveFn);

            this.mouseleaveFn = function(ev) {
                requestAnimationFrame(function() {
                    // Movement settings for the animatable elements.
                    for(var i = 0, len = self.fragments.length; i <= len-1; ++i) {
                        const fragment = self.fragments[i];
                        fragment.style.transform = fragment.style.WebkitTransform = 'translate3d(0,0,0)';
                    }
                });
            };
            this.el.addEventListener('mouseleave', this.mouseleaveFn);
        }

        // Window resize - Recalculate clip values and translations.
        this.debounceResize = debounce(function(ev) {
            // total elements/configuration
            const areasTotal = self.options.area.length;
            // Recalculate dimensions.
            self.dimensions = {width: self.el.offsetWidth, height: self.el.offsetHeight};
            // recalculate the clip/clip-path and translations
            for(var i = 0, len = self.fragments.length; i <= len-1; ++i) {
                self._positionFragment(i, self.fragments[i].querySelector('.fragment__piece'));
            }
        }, 10);
        window.addEventListener('resize', this.debounceResize);
    };

    /**
     * Renders all the fragments defined in the FragmentsFx.prototype.options
     */
    FragmentsFx.prototype._layout = function() {
        // Create the fragments and add them to the DOM (append it to the main element).
        this.fragments = [];
        for (var i = 0, len = this.options.fragments; i < len; ++i) {
            const fragment = this._createFragment(i);
            this.fragments.push(fragment);
        }
    };

    /**
     * Create a fragment.
     */
    FragmentsFx.prototype._createFragment = function(pos) {
        var fragment = document.createElement('div');
        fragment.className = 'fragment';
        // Set up a random number for the translation of the fragment when using parallax (mousemove).
        if( this.options.parallax ) {
            fragment.setAttribute('data-parallax', getRandom(this.options.randomParallax.min,this.options.randomParallax.max));
        }
        // Create the fragment "piece" on which we define the clip-path configuration and the background image.
        var piece = document.createElement('div');
        piece.style.backgroundImage = 'url(' + this.imgsrc + ')';
        piece.className = 'fragment__piece';
        piece.style.backgroundImage = 'url(' + this.imgsrc + ')';
        this._positionFragment(pos, piece);
        fragment.appendChild(piece);
        this.el.appendChild(fragment);

        return fragment;
    };

    FragmentsFx.prototype._positionFragment = function(pos, piece) {
        const isRandom = this.options.area === 'random',
            data = this.options.area[pos],
            top = isRandom ? getRandom(this.options.randomIntervals.top.min,this.options.randomIntervals.top.max) : data.top,
            left = isRandom ? getRandom(this.options.randomIntervals.left.min,this.options.randomIntervals.left.max) : data.left;

        // Select either the width or the height with a fixed value for the other dimension.
        var width, height;

        if( isRandom ) {
            if(!!Math.round(getRandom(0,1))) {
                width = getRandom(this.options.randomIntervals.dimension.width.min,this.options.randomIntervals.dimension.width.max);
                height = getRandom(Math.max(this.options.randomIntervals.dimension.width.fixedHeight-0.1,0.1), this.options.randomIntervals.dimension.width.fixedHeight+0.1);
            }
            else {
                height = getRandom(this.options.randomIntervals.dimension.width.min,this.options.randomIntervals.dimension.width.max);
                width = getRandom(Math.max(this.options.randomIntervals.dimension.height.fixedWidth-0.1,0.1), this.options.randomIntervals.dimension.height.fixedWidth+0.1);
            }
        }
        else {
            width = data.width;
            height = data.height;
        }

        if( !isClipPathSupported ) {
            const clipTop = top/100 * this.dimensions.height,
                clipLeft = left/100 * this.dimensions.width,
                clipRight = width/100 * this.dimensions.width + clipLeft,
                clipBottom = height/100 * this.dimensions.height + clipTop;

            piece.style.clip = 'rect(' + clipTop + 'px,' + clipRight + 'px,' + clipBottom + 'px,' + clipLeft + 'px)';
        }
        else {
            piece.style.WebkitClipPath = piece.style.clipPath = 'polygon(' + left + '% ' + top + '%, ' + (left + width) + '% ' + top + '%, ' + (left + width) + '% ' + (top + height) + '%, ' + left + '% ' + (top + height) + '%)';
        }

        // Translate the piece.
        // The translation has to respect the boundaries defined in the options.
        const translation = {
            x: getRandom(-1 * left/100 * this.dimensions.width - this.options.boundaries.x1, this.dimensions.width - left/100 * this.dimensions.width + this.options.boundaries.x2 - width/100 * this.dimensions.width),
            y: getRandom(-1 * top/100 * this.dimensions.height - this.options.boundaries.y1, this.dimensions.height - top/100 * this.dimensions.height + this.options.boundaries.y2 - height/100 * this.dimensions.height)
        };

        piece.style.WebkitTransform = piece.style.transform = 'translate3d(' + translation.x + 'px,' + translation.y +'px,0)';
    };

    window.FragmentsFx = FragmentsFx;