/*
 * Slim v4.0.2 - Image Cropping Made Easy
 * Copyright (c) 2016 Rik Schennink - http://slimimagecropper.com
 */
(function(angular){

	// Slim Lib
	var Slim = (function() {

// custom event polyfill for IE10
(function() {
	function CustomEvent(event, params) {
		params = params || { bubbles: false, cancelable: false, detail: undefined };
		var evt = document.createEvent('CustomEvent');
		evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
		return evt;
	}
	CustomEvent.prototype = window.CustomEvent.prototype;
	window.CustomEvent = CustomEvent;
})();
/*
* JavaScript Load Image
* https://github.com/blueimp/JavaScript-Load-Image
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/

/*global define, module, window, document, URL, webkitURL, FileReader */

// Loads an image for a given File object.
// Invokes the callback with an img or optional canvas
// element (if supported by the browser) as parameter:
var loadImage = function (file, callback, options) {
    var img = document.createElement('img')
    var url
    var oUrl
    img.onerror = callback
    img.onload = function () {
        if (oUrl && !(options && options.noRevoke)) {
            loadImage.revokeObjectURL(oUrl)
        }
        if (callback) {
            callback(loadImage.scale(img, options))
        }
    }
    if (loadImage.isInstanceOf('Blob', file) ||
        // Files are also Blob instances, but some browsers
        // (Firefox 3.6) support the File API but not Blobs:
        loadImage.isInstanceOf('File', file)) {
        url = oUrl = loadImage.createObjectURL(file)
        // Store the file type for resize processing:
        img._type = file.type
    } else if (typeof file === 'string') {
        url = file
        if (options && options.crossOrigin) {
            img.crossOrigin = options.crossOrigin
        }
    } else {
        return false
    }
    if (url) {
        img.src = url
        return img
    }
    return loadImage.readFile(file, function (e) {
        var target = e.target
        if (target && target.result) {
            img.src = target.result
        } else {
            if (callback) {
                callback(e)
            }
        }
    })
}
// The check for URL.revokeObjectURL fixes an issue with Opera 12,
// which provides URL.createObjectURL but doesn't properly implement it:
var urlAPI = (window.createObjectURL && window) ||
    (window.URL && URL.revokeObjectURL && URL) ||
    (window.webkitURL && webkitURL)

loadImage.isInstanceOf = function (type, obj) {
    // Cross-frame instanceof check
    return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}

// Transform image coordinates, allows to override e.g.
// the canvas orientation based on the orientation option,
// gets canvas, options passed as arguments:
loadImage.transformCoordinates = function () {
    return
}

// Returns transformed options, allows to override e.g.
// maxWidth, maxHeight and crop options based on the aspectRatio.
// gets img, options passed as arguments:
loadImage.getTransformedOptions = function (img, options) {
    var aspectRatio = options.aspectRatio
    var newOptions
    var i
    var width
    var height
    if (!aspectRatio) {
        return options
    }
    newOptions = {}
    for (i in options) {
        if (options.hasOwnProperty(i)) {
            newOptions[i] = options[i]
        }
    }
    newOptions.crop = true
    width = img.naturalWidth || img.width
    height = img.naturalHeight || img.height
    if (width / height > aspectRatio) {
        newOptions.maxWidth = height * aspectRatio
        newOptions.maxHeight = height
    } else {
        newOptions.maxWidth = width
        newOptions.maxHeight = width / aspectRatio
    }
    return newOptions
}

// Canvas render method, allows to implement a different rendering algorithm:
loadImage.renderImageToCanvas = function (
    canvas,
    img,
    sourceX,
    sourceY,
    sourceWidth,
    sourceHeight,
    destX,
    destY,
    destWidth,
    destHeight
) {
    canvas.getContext('2d').drawImage(
        img,
        sourceX,
        sourceY,
        sourceWidth,
        sourceHeight,
        destX,
        destY,
        destWidth,
        destHeight
    )
    return canvas
}

// This method is used to determine if the target image
// should be a canvas element:
loadImage.hasCanvasOption = function (options) {
    return options.canvas || options.crop || !!options.aspectRatio
}

// Scales and/or crops the given image (img or canvas HTML element)
// using the given options.
// Returns a canvas object if the browser supports canvas
// and the hasCanvasOption method returns true or a canvas
// object is passed as image, else the scaled image:
loadImage.scale = function (img, options) {
    options = options || {}
    var canvas = document.createElement('canvas')
    var useCanvas = img.getContext ||
        (loadImage.hasCanvasOption(options) && canvas.getContext)
    var width = img.naturalWidth || img.width
    var height = img.naturalHeight || img.height
    var destWidth = width
    var destHeight = height
    var maxWidth
    var maxHeight
    var minWidth
    var minHeight
    var sourceWidth
    var sourceHeight
    var sourceX
    var sourceY
    var pixelRatio
    var downsamplingRatio
    var tmp
    function scaleUp () {
        var scale = Math.max(
            (minWidth || destWidth) / destWidth,
            (minHeight || destHeight) / destHeight
        )
        if (scale > 1) {
            destWidth *= scale
            destHeight *= scale
        }
    }
    function scaleDown () {
        var scale = Math.min(
            (maxWidth || destWidth) / destWidth,
            (maxHeight || destHeight) / destHeight
        )
        if (scale < 1) {
            destWidth *= scale
            destHeight *= scale
        }
    }
    if (useCanvas) {
        options = loadImage.getTransformedOptions(img, options)
        sourceX = options.left || 0
        sourceY = options.top || 0
        if (options.sourceWidth) {
            sourceWidth = options.sourceWidth
            if (options.right !== undefined && options.left === undefined) {
                sourceX = width - sourceWidth - options.right
            }
        } else {
            sourceWidth = width - sourceX - (options.right || 0)
        }
        if (options.sourceHeight) {
            sourceHeight = options.sourceHeight
            if (options.bottom !== undefined && options.top === undefined) {
                sourceY = height - sourceHeight - options.bottom
            }
        } else {
            sourceHeight = height - sourceY - (options.bottom || 0)
        }
        destWidth = sourceWidth
        destHeight = sourceHeight
    }
    maxWidth = options.maxWidth
    maxHeight = options.maxHeight
    minWidth = options.minWidth
    minHeight = options.minHeight
    if (useCanvas && maxWidth && maxHeight && options.crop) {
        destWidth = maxWidth
        destHeight = maxHeight
        tmp = sourceWidth / sourceHeight - maxWidth / maxHeight
        if (tmp < 0) {
            sourceHeight = maxHeight * sourceWidth / maxWidth
            if (options.top === undefined && options.bottom === undefined) {
                sourceY = (height - sourceHeight) / 2
            }
        } else if (tmp > 0) {
            sourceWidth = maxWidth * sourceHeight / maxHeight
            if (options.left === undefined && options.right === undefined) {
                sourceX = (width - sourceWidth) / 2
            }
        }
    } else {
        if (options.contain || options.cover) {
            minWidth = maxWidth = maxWidth || minWidth
            minHeight = maxHeight = maxHeight || minHeight
        }
        if (options.cover) {
            scaleDown()
            scaleUp()
        } else {
            scaleUp()
            scaleDown()
        }
    }
    if (useCanvas) {
        pixelRatio = options.pixelRatio
        if (pixelRatio > 1) {
            canvas.style.width = destWidth + 'px'
            canvas.style.height = destHeight + 'px'
            destWidth *= pixelRatio
            destHeight *= pixelRatio
            canvas.getContext('2d').scale(pixelRatio, pixelRatio)
        }
        downsamplingRatio = options.downsamplingRatio
        if (downsamplingRatio > 0 && downsamplingRatio < 1 &&
            destWidth < sourceWidth && destHeight < sourceHeight) {
            while (sourceWidth * downsamplingRatio > destWidth) {
                canvas.width = sourceWidth * downsamplingRatio
                canvas.height = sourceHeight * downsamplingRatio
                loadImage.renderImageToCanvas(
                    canvas,
                    img,
                    sourceX,
                    sourceY,
                    sourceWidth,
                    sourceHeight,
                    0,
                    0,
                    canvas.width,
                    canvas.height
                )
                sourceWidth = canvas.width
                sourceHeight = canvas.height
                img = document.createElement('canvas')
                img.width = sourceWidth
                img.height = sourceHeight
                loadImage.renderImageToCanvas(
                    img,
                    canvas,
                    0,
                    0,
                    sourceWidth,
                    sourceHeight,
                    0,
                    0,
                    sourceWidth,
                    sourceHeight
                )
            }
        }
        canvas.width = destWidth
        canvas.height = destHeight
        loadImage.transformCoordinates(
            canvas,
            options
        )
        return loadImage.renderImageToCanvas(
            canvas,
            img,
            sourceX,
            sourceY,
            sourceWidth,
            sourceHeight,
            0,
            0,
            destWidth,
            destHeight
        )
    }
    img.width = destWidth
    img.height = destHeight
    return img
}

loadImage.createObjectURL = function (file) {
    return urlAPI ? urlAPI.createObjectURL(file) : false
}

loadImage.revokeObjectURL = function (url) {
    return urlAPI ? urlAPI.revokeObjectURL(url) : false
}

// Loads a given File object via FileReader interface,
// invokes the callback with the event object (load or error).
// The result can be read via event.target.result:
loadImage.readFile = function (file, callback, method) {
    if (window.FileReader) {
        var fileReader = new FileReader()
        fileReader.onload = fileReader.onerror = callback
        method = method || 'readAsDataURL'
        if (fileReader[method]) {
            fileReader[method](file)
            return fileReader
        }
    }
    return false
}

var originalHasCanvasOption = loadImage.hasCanvasOption
var originalTransformCoordinates = loadImage.transformCoordinates
var originalGetTransformedOptions = loadImage.getTransformedOptions

// This method is used to determine if the target image
// should be a canvas element:
loadImage.hasCanvasOption = function (options) {
    return !!options.orientation ||
        originalHasCanvasOption.call(loadImage, options)
}

// Transform image orientation based on
// the given EXIF orientation option:
loadImage.transformCoordinates = function (canvas, options) {
    originalTransformCoordinates.call(loadImage, canvas, options)
    var ctx = canvas.getContext('2d')
    var width = canvas.width
    var height = canvas.height
    var styleWidth = canvas.style.width
    var styleHeight = canvas.style.height
    var orientation = options.orientation
    if (!orientation || orientation > 8) {
        return
    }
    if (orientation > 4) {
        canvas.width = height
        canvas.height = width
        canvas.style.width = styleHeight
        canvas.style.height = styleWidth
    }
    switch (orientation) {
        case 2:
            // horizontal flip
            ctx.translate(width, 0)
            ctx.scale(-1, 1)
            break
        case 3:
            // 180° rotate left
            ctx.translate(width, height)
            ctx.rotate(Math.PI)
            break
        case 4:
            // vertical flip
            ctx.translate(0, height)
            ctx.scale(1, -1)
            break
        case 5:
            // vertical flip + 90 rotate right
            ctx.rotate(0.5 * Math.PI)
            ctx.scale(1, -1)
            break
        case 6:
            // 90° rotate right
            ctx.rotate(0.5 * Math.PI)
            ctx.translate(0, -height)
            break
        case 7:
            // horizontal flip + 90 rotate right
            ctx.rotate(0.5 * Math.PI)
            ctx.translate(width, -height)
            ctx.scale(-1, 1)
            break
        case 8:
            // 90° rotate left
            ctx.rotate(-0.5 * Math.PI)
            ctx.translate(-width, 0)
            break
    }
}

// Transforms coordinate and dimension options
// based on the given orientation option:
loadImage.getTransformedOptions = function (img, opts) {
    var options = originalGetTransformedOptions.call(loadImage, img, opts)
    var orientation = options.orientation
    var newOptions
    var i
    if (!orientation || orientation > 8 || orientation === 1) {
        return options
    }
    newOptions = {}
    for (i in options) {
        if (options.hasOwnProperty(i)) {
            newOptions[i] = options[i]
        }
    }
    switch (options.orientation) {
        case 2:
            // horizontal flip
            newOptions.left = options.right
            newOptions.right = options.left
            break
        case 3:
            // 180° rotate left
            newOptions.left = options.right
            newOptions.top = options.bottom
            newOptions.right = options.left
            newOptions.bottom = options.top
            break
        case 4:
            // vertical flip
            newOptions.top = options.bottom
            newOptions.bottom = options.top
            break
        case 5:
            // vertical flip + 90 rotate right
            newOptions.left = options.top
            newOptions.top = options.left
            newOptions.right = options.bottom
            newOptions.bottom = options.right
            break
        case 6:
            // 90° rotate right
            newOptions.left = options.top
            newOptions.top = options.right
            newOptions.right = options.bottom
            newOptions.bottom = options.left
            break
        case 7:
            // horizontal flip + 90 rotate right
            newOptions.left = options.bottom
            newOptions.top = options.right
            newOptions.right = options.top
            newOptions.bottom = options.left
            break
        case 8:
            // 90° rotate left
            newOptions.left = options.bottom
            newOptions.top = options.left
            newOptions.right = options.top
            newOptions.bottom = options.right
            break
    }
    if (options.orientation > 4) {
        newOptions.maxWidth = options.maxHeight
        newOptions.maxHeight = options.maxWidth
        newOptions.minWidth = options.minHeight
        newOptions.minHeight = options.minWidth
        newOptions.sourceWidth = options.sourceHeight
        newOptions.sourceHeight = options.sourceWidth
    }
    return newOptions
}

var hasblobSlice = window.Blob && (Blob.prototype.slice ||
    Blob.prototype.webkitSlice || Blob.prototype.mozSlice)

loadImage.blobSlice = hasblobSlice && function () {
        var slice = this.slice || this.webkitSlice || this.mozSlice
        return slice.apply(this, arguments)
    }

loadImage.metaDataParsers = {
    jpeg: {
        0xffe1: [] // APP1 marker
    }
}

// Parses image meta data and calls the callback with an object argument
// with the following properties:
// * imageHead: The complete image head as ArrayBuffer (Uint8Array for IE10)
// The options arguments accepts an object and supports the following properties:
// * maxMetaDataSize: Defines the maximum number of bytes to parse.
// * disableImageHead: Disables creating the imageHead property.
loadImage.parseMetaData = function (file, callback, options) {
    options = options || {}
    var that = this
    // 256 KiB should contain all EXIF/ICC/IPTC segments:
    var maxMetaDataSize = options.maxMetaDataSize || 262144
    var data = {}
    var noMetaData = !(window.DataView && file && file.size >= 12 &&
    file.type === 'image/jpeg' && loadImage.blobSlice)
    if (noMetaData || !loadImage.readFile(
            loadImage.blobSlice.call(file, 0, maxMetaDataSize),
            function (e) {
                if (e.target.error) {
                    // FileReader error
                    //console.log(e.target.error)
                    callback(data)
                    return
                }
                // Note on endianness:
                // Since the marker and length bytes in JPEG files are always
                // stored in big endian order, we can leave the endian parameter
                // of the DataView methods undefined, defaulting to big endian.
                var buffer = e.target.result
                var dataView = new DataView(buffer)
                var offset = 2
                var maxOffset = dataView.byteLength - 4
                var headLength = offset
                var markerBytes
                var markerLength
                var parsers
                var i
                // Check for the JPEG marker (0xffd8):
                if (dataView.getUint16(0) === 0xffd8) {
                    while (offset < maxOffset) {
                        markerBytes = dataView.getUint16(offset)
                        // Search for APPn (0xffeN) and COM (0xfffe) markers,
                        // which contain application-specific meta-data like
                        // Exif, ICC and IPTC data and text comments:
                        if ((markerBytes >= 0xffe0 && markerBytes <= 0xffef) ||
                            markerBytes === 0xfffe) {
                            // The marker bytes (2) are always followed by
                            // the length bytes (2), indicating the length of the
                            // marker segment, which includes the length bytes,
                            // but not the marker bytes, so we add 2:
                            markerLength = dataView.getUint16(offset + 2) + 2
                            if (offset + markerLength > dataView.byteLength) {
                                //console.log('Invalid meta data: Invalid segment size.')
                                break
                            }
                            parsers = loadImage.metaDataParsers.jpeg[markerBytes]
                            if (parsers) {
                                for (i = 0; i < parsers.length; i += 1) {
                                    parsers[i].call(
                                        that,
                                        dataView,
                                        offset,
                                        markerLength,
                                        data,
                                        options
                                    )
                                }
                            }
                            offset += markerLength
                            headLength = offset
                        } else {
                            // Not an APPn or COM marker, probably safe to
                            // assume that this is the end of the meta data
                            break
                        }
                    }
                    // Meta length must be longer than JPEG marker (2)
                    // plus APPn marker (2), followed by length bytes (2):
                    if (!options.disableImageHead && headLength > 6) {
                        if (buffer.slice) {
                            data.imageHead = buffer.slice(0, headLength)
                        } else {
                            // Workaround for IE10, which does not yet
                            // support ArrayBuffer.slice:
                            data.imageHead = new Uint8Array(buffer)
                                .subarray(0, headLength)
                        }
                    }
                } else {
                    //console.log('Invalid JPEG file: Missing JPEG marker.')
                }
                callback(data)
            },
            'readAsArrayBuffer'
        )) {
        callback(data)
    }
}

loadImage.ExifMap = function () {
    return this
}

loadImage.ExifMap.prototype.map = {
    'Orientation': 0x0112
}

loadImage.ExifMap.prototype.get = function (id) {
    return this[id] || this[this.map[id]]
}

loadImage.getExifThumbnail = function (dataView, offset, length) {
    var hexData,
        i,
        b
    if (!length || offset + length > dataView.byteLength) {
        //console.log('Invalid Exif data: Invalid thumbnail data.')
        return
    }
    hexData = []
    for (i = 0; i < length; i += 1) {
        b = dataView.getUint8(offset + i)
        hexData.push((b < 16 ? '0' : '') + b.toString(16))
    }
    return 'data:image/jpeg,%' + hexData.join('%')
}

loadImage.exifTagTypes = {
    // byte, 8-bit unsigned int:
    1: {
        getValue: function (dataView, dataOffset) {
            return dataView.getUint8(dataOffset)
        },
        size: 1
    },
    // ascii, 8-bit byte:
    2: {
        getValue: function (dataView, dataOffset) {
            return String.fromCharCode(dataView.getUint8(dataOffset))
        },
        size: 1,
        ascii: true
    },
    // short, 16 bit int:
    3: {
        getValue: function (dataView, dataOffset, littleEndian) {
            return dataView.getUint16(dataOffset, littleEndian)
        },
        size: 2
    },
    // long, 32 bit int:
    4: {
        getValue: function (dataView, dataOffset, littleEndian) {
            return dataView.getUint32(dataOffset, littleEndian)
        },
        size: 4
    },
    // rational = two long values, first is numerator, second is denominator:
    5: {
        getValue: function (dataView, dataOffset, littleEndian) {
            return dataView.getUint32(dataOffset, littleEndian) /
                dataView.getUint32(dataOffset + 4, littleEndian)
        },
        size: 8
    },
    // slong, 32 bit signed int:
    9: {
        getValue: function (dataView, dataOffset, littleEndian) {
            return dataView.getInt32(dataOffset, littleEndian)
        },
        size: 4
    },
    // srational, two slongs, first is numerator, second is denominator:
    10: {
        getValue: function (dataView, dataOffset, littleEndian) {
            return dataView.getInt32(dataOffset, littleEndian) /
                dataView.getInt32(dataOffset + 4, littleEndian)
        },
        size: 8
    }
}
// undefined, 8-bit byte, value depending on field:
loadImage.exifTagTypes[7] = loadImage.exifTagTypes[1]

loadImage.getExifValue = function (dataView, tiffOffset, offset, type, length, littleEndian) {
    var tagType = loadImage.exifTagTypes[type]
    var tagSize
    var dataOffset
    var values
    var i
    var str
    var c
    if (!tagType) {
        //console.log('Invalid Exif data: Invalid tag type.')
        return
    }
    tagSize = tagType.size * length
    // Determine if the value is contained in the dataOffset bytes,
    // or if the value at the dataOffset is a pointer to the actual data:
    dataOffset = tagSize > 4
        ? tiffOffset + dataView.getUint32(offset + 8, littleEndian)
        : (offset + 8)
    if (dataOffset + tagSize > dataView.byteLength) {
        //console.log('Invalid Exif data: Invalid data offset.')
        return
    }
    if (length === 1) {
        return tagType.getValue(dataView, dataOffset, littleEndian)
    }
    values = []
    for (i = 0; i < length; i += 1) {
        values[i] = tagType.getValue(dataView, dataOffset + i * tagType.size, littleEndian)
    }
    if (tagType.ascii) {
        str = ''
        // Concatenate the chars:
        for (i = 0; i < values.length; i += 1) {
            c = values[i]
            // Ignore the terminating NULL byte(s):
            if (c === '\u0000') {
                break
            }
            str += c
        }
        return str
    }
    return values
}

loadImage.parseExifTag = function (dataView, tiffOffset, offset, littleEndian, data) {
    var tag = dataView.getUint16(offset, littleEndian)
    data.exif[tag] = loadImage.getExifValue(
        dataView,
        tiffOffset,
        offset,
        dataView.getUint16(offset + 2, littleEndian), // tag type
        dataView.getUint32(offset + 4, littleEndian), // tag length
        littleEndian
    )
}

loadImage.parseExifTags = function (dataView, tiffOffset, dirOffset, littleEndian, data) {
    var tagsNumber,
        dirEndOffset,
        i
    if (dirOffset + 6 > dataView.byteLength) {
        //console.log('Invalid Exif data: Invalid directory offset.')
        return
    }
    tagsNumber = dataView.getUint16(dirOffset, littleEndian)
    dirEndOffset = dirOffset + 2 + 12 * tagsNumber
    if (dirEndOffset + 4 > dataView.byteLength) {
        //console.log('Invalid Exif data: Invalid directory size.')
        return
    }
    for (i = 0; i < tagsNumber; i += 1) {
        this.parseExifTag(
            dataView,
            tiffOffset,
            dirOffset + 2 + 12 * i, // tag offset
            littleEndian,
            data
        )
    }
    // Return the offset to the next directory:
    return dataView.getUint32(dirEndOffset, littleEndian)
}

loadImage.parseExifData = function (dataView, offset, length, data, options) {
    if (options.disableExif) {
        return
    }
    var tiffOffset = offset + 10
    var littleEndian
    var dirOffset
    var thumbnailData
    // Check for the ASCII code for "Exif" (0x45786966):
    if (dataView.getUint32(offset + 4) !== 0x45786966) {
        // No Exif data, might be XMP data instead
        return
    }
    if (tiffOffset + 8 > dataView.byteLength) {
        //console.log('Invalid Exif data: Invalid segment size.')
        return
    }
    // Check for the two null bytes:
    if (dataView.getUint16(offset + 8) !== 0x0000) {
        //console.log('Invalid Exif data: Missing byte alignment offset.')
        return
    }
    // Check the byte alignment:
    switch (dataView.getUint16(tiffOffset)) {
        case 0x4949:
            littleEndian = true
            break
        case 0x4D4D:
            littleEndian = false
            break
        default:
            //console.log('Invalid Exif data: Invalid byte alignment marker.')
            return
    }
    // Check for the TIFF tag marker (0x002A):
    if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002A) {
        //console.log('Invalid Exif data: Missing TIFF marker.')
        return
    }
    // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal:
    dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian)
    // Create the exif object to store the tags:
    data.exif = new loadImage.ExifMap()
    // Parse the tags of the main image directory and retrieve the
    // offset to the next directory, usually the thumbnail directory:
    dirOffset = loadImage.parseExifTags(
        dataView,
        tiffOffset,
        tiffOffset + dirOffset,
        littleEndian,
        data
    )
    if (dirOffset && !options.disableExifThumbnail) {
        thumbnailData = {exif: {}}
        dirOffset = loadImage.parseExifTags(
            dataView,
            tiffOffset,
            tiffOffset + dirOffset,
            littleEndian,
            thumbnailData
        )
        // Check for JPEG Thumbnail offset:
        if (thumbnailData.exif[0x0201]) {
            data.exif.Thumbnail = loadImage.getExifThumbnail(
                dataView,
                tiffOffset + thumbnailData.exif[0x0201],
                thumbnailData.exif[0x0202] // Thumbnail data length
            )
        }
    }
    // Check for Exif Sub IFD Pointer:
    if (data.exif[0x8769] && !options.disableExifSub) {
        loadImage.parseExifTags(
            dataView,
            tiffOffset,
            tiffOffset + data.exif[0x8769], // directory offset
            littleEndian,
            data
        )
    }
    // Check for GPS Info IFD Pointer:
    if (data.exif[0x8825] && !options.disableExifGps) {
        loadImage.parseExifTags(
            dataView,
            tiffOffset,
            tiffOffset + data.exif[0x8825], // directory offset
            littleEndian,
            data
        )
    }
}

// Registers the Exif parser for the APP1 JPEG meta data segment:
loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData)
var snabbt = (function() {

var tickRequests = [];
var runningAnimations = [];
var completedAnimations = [];
var transformProperty = 'transform';

// Find which vendor prefix to use
var styles = window.getComputedStyle(document.documentElement, '');
var vendorPrefix = (Array.prototype.slice
  .call(styles)
  .join('') 
  .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o'])
)[1];
if(vendorPrefix === 'webkit')
transformProperty = 'webkitTransform';

/* Entry point, only function to be called by user */
var snabbt = function(arg1, arg2, arg3) {

var elements = arg1;

// If argument is an Array or a NodeList or other list type that can be iterable.
// Loop through and start one animation for each element.
if(elements.length !== undefined) {
  var aggregateChainer = {
    chainers: [],
    then: function(opts) {
      return this.snabbt(opts);
    },
    snabbt: function(opts) {
      var len = this.chainers.length;
      this.chainers.forEach(function(chainer, index) {
        chainer.snabbt(preprocessOptions(opts, index, len));
      });
      return aggregateChainer;
    },
    setValue: function(value) {
      this.chainers.forEach(function(chainer) {
        chainer.setValue(value);
      });
      return aggregateChainer;
    },
    finish: function() {
      this.chainers.forEach(function(chainer) {
        chainer.finish();
      });
      return aggregateChainer;
    },
    rollback: function() {
      this.chainers.forEach(function(chainer) {
        chainer.rollback();
      });
      return aggregateChainer;
    }
  };

  for(var i=0, len=elements.length;i<len;++i) {
    if(typeof arg2 == 'string')
      aggregateChainer.chainers.push(snabbtSingleElement(elements[i], arg2, preprocessOptions(arg3, i, len)));
    else
      aggregateChainer.chainers.push(snabbtSingleElement(elements[i], preprocessOptions(arg2, i, len), arg3));
  }
  return aggregateChainer;
} else {
  if(typeof arg2 == 'string')
    return snabbtSingleElement(elements, arg2, preprocessOptions(arg3, 0, 1));
  else
    return snabbtSingleElement(elements, preprocessOptions(arg2, 0, 1), arg3);
}
};

var preprocessOptions = function(options, index, len) {
if(!options)
  return options;
var clone = cloneObject(options);

if(isFunction(options.delay)) {
  clone.delay = options.delay(index, len);
}

if(isFunction(options.callback)) {
  clone.complete = function() {
    options.callback.call(this, index, len);
  };
}

var hasAllDoneCallback = isFunction(options.allDone);
var hasCompleteCallback = isFunction(options.complete);

if(hasCompleteCallback || hasAllDoneCallback) {
  clone.complete = function() {
    if(hasCompleteCallback) {
      options.complete.call(this, index, len);
    }
    if(hasAllDoneCallback && (index == len - 1)) {
      options.allDone();
    }
  };
}

if(isFunction(options.valueFeeder)) {
  clone.valueFeeder = function(i, matrix) {
    return options.valueFeeder(i, matrix, index, len);
  };
}
if(isFunction(options.easing)) {
  clone.easing = function(i) {
    return options.easing(i, index, len);
  };
}

var properties = [
  'position',
  'rotation',
  'skew',
  'rotationPost',
  'scale',
  'width',
  'height',
  'opacity',
  'fromPosition',
  'fromRotation',
  'fromSkew',
  'fromRotationPost',
  'fromScale',
  'fromWidth',
  'fromHeight',
  'fromOpacity',
  'transformOrigin',
  'duration',
  'delay'
];

properties.forEach(function(property) {
  if(isFunction(options[property])) {
    clone[property] = options[property](index, len);
  }
});

return clone;
};

var snabbtSingleElement = function(element, arg2, arg3) {

if(arg2 === 'attention') {
  return setupAttentionAnimation(element, arg3);
}

if(arg2 === 'stop') {
  return stopAnimation(element);
}

if(arg2 === 'detach') {
  return detachChildren(element);
}

var options = arg2;

// Remove orphaned end states
clearOphanedEndStates();

// If there is a running or past completed animation with element, use that end state as start state
var currentState = currentAnimationState(element);
var start = currentState;
// from has precendance over current animation state
start = stateFromOptions(options, start, true);
var end = cloneObject(currentState);
end = stateFromOptions(options, end);

var animOptions = setupAnimationOptions(start, end, options);
var animation = createAnimation(animOptions);

runningAnimations.push([element, animation]);

animation.updateElement(element, true);
var queue = [];
var chainer = {
  snabbt: function(opts) {
    queue.unshift(preprocessOptions(opts, 0, 1));
    return chainer;
  },
  then: function(opts) {
    return this.snabbt(opts);
  }
};

function tick(time) {
  animation.tick(time);
  animation.updateElement(element);
  if(animation.isStopped())
    return;

  if(!animation.completed())
    return queueTick(tick);

  if(options.loop > 1 && !animation.isStopped()) {
    // Loop current animation
    options.loop -= 1;
    animation.restart();
    queueTick(tick);
  } else {
    if(options.complete) {
      options.complete.call(element);
    }

    // Start next animation in queue
    if(queue.length) {
      options = queue.pop();

      start = stateFromOptions(options, end, true);
      end = stateFromOptions(options, cloneObject(end));
      options = setupAnimationOptions(start, end, options);

      animation = createAnimation(options);
      runningAnimations.push([element, animation]);

      animation.tick(time);
      queueTick(tick);
    }
  }
}

queueTick(tick);
// Manual animations are not chainable, instead an animation controller object is returned
// with setValue, finish and rollback methods
if(options.manual)
  return animation;
return chainer;
};

var setupAttentionAnimation = function(element,  options) {
var movement = stateFromOptions(options, createState({}));
options.movement = movement;
var animation = createAttentionAnimation(options);

runningAnimations.push([element, animation]);
function tick(time) {
  animation.tick(time);
  animation.updateElement(element);
  if(!animation.completed()) {
    queueTick(tick);
  } else {
    if(options.callback) {
      options.callback(element);
    }
    if(options.loop && options.loop > 1) {
      options.loop--;
      animation.restart();
      queueTick(tick);
    }
  }
}
queueTick(tick);
};

var stopAnimation = function(element) {
for(var i= 0,len=runningAnimations.length;i<len;++i) {
  var currentAnimation = runningAnimations[i];
  var animatedElement = currentAnimation[0];
  var animation = currentAnimation[1];

  if(animatedElement === element) {
    animation.stop();
  }
}
};

var indexOfElement = function(arr, element) {
  for(var i=0,len=arr.length;i<len;++i) {
    if (arr[i][0]===element) {
      return i;
    }
  }
  return -1;
};

var detachChildren = function(element) {

  var elements = [];
  var animations = runningAnimations.concat(completedAnimations);
  var el;
  var i;
  var len = animations.length;

  for(i=0;i<len;++i) {
    el = animations[i][0];
    if (element.contains(el) || element === el) {
      elements.push(el);
    }
  }

  len=elements.length;
  for(i=0;i<len;++i) {
    detachElement(elements[i]);
  }

};

var detachElement = function(element) {

  // stop animations
  stopAnimation(element);

  // remove
  var index = indexOfElement(runningAnimations, element);
  if (index >= 0) {
    runningAnimations.splice(index,1);
  }

  index = indexOfElement(completedAnimations, element);
  if (index >= 0) {
    completedAnimations.splice(index,1);
  }

};

var findAnimationState = function(animationList, element) {
for(var i=0,len=animationList.length;i<len;++i) {
  var currentAnimation = animationList[i];
  var animatedElement = currentAnimation[0];
  var animation = currentAnimation[1];

  if(animatedElement === element) {
    var state = animation.getCurrentState();
    animation.stop();
    return state;
  }
}
};

var clearOphanedEndStates = function() {
completedAnimations = completedAnimations.filter(function(animation) {
  return (findUltimateAncestor(animation[0]).body);
});
};

var findUltimateAncestor = function(node) {
var ancestor = node;
while(ancestor.parentNode) {
  ancestor = ancestor.parentNode;
}
return ancestor;
};

/**
* Returns the current state of element if there is an ongoing or previously finished
* animation releated to it. Will also call stop on the animation.
* TODO: The stopping of the animation is better put somewhere else
*/
var currentAnimationState = function(element) {
// Check if a completed animation is stored for this element
var state = findAnimationState(runningAnimations, element);
if(state)
  return state;

return findAnimationState(completedAnimations, element);
};

/**
* Parses an animation configuration object and returns a State instance
*/
var stateFromOptions = function(options, state, useFromPrefix) {
if (!state) {
  state = createState({
    position: [0, 0, 0],
    rotation: [0, 0, 0],
    rotationPost: [0, 0, 0],
    scale: [1, 1],
    skew: [0, 0]
  });
}
var position = 'position';
var rotation = 'rotation';
var skew = 'skew';
var rotationPost = 'rotationPost';
var scale = 'scale';
var scalePost = 'scalePost';
var width = 'width';
var height = 'height';
var opacity = 'opacity';

if(useFromPrefix) {
  position = 'fromPosition';
  rotation = 'fromRotation';
  skew = 'fromSkew';
  rotationPost = 'fromRotationPost';
  scale = 'fromScale';
  scalePost = 'fromScalePost';
  width = 'fromWidth';
  height = 'fromHeight';
  opacity = 'fromOpacity';
}

state.position = optionOrDefault(options[position], state.position);
state.rotation = optionOrDefault(options[rotation], state.rotation);
state.rotationPost = optionOrDefault(options[rotationPost], state.rotationPost);
state.skew = optionOrDefault(options[skew], state.skew);
state.scale = optionOrDefault(options[scale], state.scale);
state.scalePost = optionOrDefault(options[scalePost], state.scalePost);
state.opacity = options[opacity];
state.width = options[width];
state.height = options[height];

return state;
};

var setupAnimationOptions = function(start, end, options) {
options.startState = start;
options.endState = end;
return options;
};

var polyFillrAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { return setTimeout(callback, 1000 / 60); }; 

var queueTick = function(func) {
if(tickRequests.length === 0)
  polyFillrAF(tickAnimations);
tickRequests.push(func);
};

var tickAnimations = function(time) {
var len = tickRequests.length;
for(var i=0;i<len;++i) {
  tickRequests[i](time);
}
tickRequests.splice(0, len);

var finishedAnimations = runningAnimations.filter(function(animation) {
  return animation[1].completed();
});

// See if there are any previously completed animations on the same element, if so, remove it before merging
completedAnimations = completedAnimations.filter(function(animation) {
  for(var i=0,len=finishedAnimations.length;i<len;++i) {
    if(animation[0] === finishedAnimations[i][0]) {
      return false;
    }
  }
  return true;
});

completedAnimations = completedAnimations.concat(finishedAnimations);

runningAnimations = runningAnimations.filter(function(animation) {
  return !animation[1].completed();
});

if(tickRequests.length !== 0)
  polyFillrAF(tickAnimations);
};


// Class for handling animation between two states
var createAnimation = function(options) {
var startState = options.startState;
var endState = options.endState;
var duration = optionOrDefault(options.duration, 500);
var delay = optionOrDefault(options.delay, 0);
var perspective = options.perspective;
var easing = createEaser(optionOrDefault(options.easing, 'linear'), options);
var currentState = duration === 0 ? endState.clone() : startState.clone();
var transformOrigin = options.transformOrigin;
currentState.transformOrigin = options.transformOrigin;

var startTime = 0;
var currentTime = 0;
var stopped = false;
var started = false;

// Manual related
var manual = options.manual;
var manualValue = 0;
var manualDelayFactor = delay / duration;
var manualCallback;

var tweener;
// Setup tweener
if(options.valueFeeder) {
  tweener = createValueFeederTweener(options.valueFeeder,
                                     startState,
                                     endState,
                                     currentState);
} else {
  tweener = createStateTweener(startState, endState, currentState);
}

// Public api
return {
  stop: function() {
    stopped = true;
  },
  isStopped: function() {
    return stopped;
  },

  finish: function(callback) {
    manual = false;
    var manualDuration = duration * manualValue;
    startTime = currentTime - manualDuration;
    manualCallback = callback;
    easing.resetFrom = manualValue;
  },

  rollback: function(callback) {
    manual = false;
    tweener.setReverse();
    var manualDuration = duration * (1 - manualValue);
    startTime = currentTime - manualDuration;
    manualCallback = callback;
    easing.resetFrom = manualValue;
  },

  restart: function() {
    // Restart timer
    startTime = undefined;
    easing.resetFrom(0);
  },

  tick: function(time) {
    if(stopped)
      return;

    if(manual) {
      currentTime = time;
      this.updateCurrentTransform();
      return;
    }

    // If first tick, set startTime
    if(!startTime) {
      startTime = time;
    }
    if(time - startTime > delay) {
      started = true;
      currentTime = time - delay;

      var curr = Math.min(Math.max(0.0, currentTime - startTime), duration);
      easing.tick(curr / duration);
      this.updateCurrentTransform();
      if(this.completed() && manualCallback) {
        manualCallback();
      }
    }
  },

  getCurrentState: function() {
    return currentState;
  },

  setValue: function(_manualValue) {
    started = true;
    manualValue = Math.min(Math.max(_manualValue, 0.0001), 1 + manualDelayFactor);
  },

  updateCurrentTransform: function() {
    var tweenValue = easing.getValue();
    if(manual) {
      var val = Math.max(0.00001, manualValue - manualDelayFactor);
      easing.tick(val);
      tweenValue = easing.getValue();
    }
    tweener.tween(tweenValue);
  },

  completed: function() {
    if(stopped)
      return true;
    if(startTime === 0) {
      return false;
    }
    return easing.completed();
  },

  updateElement: function(element, forceUpdate) {
    if(!started && !forceUpdate)
      return;
    var matrix = tweener.asMatrix();
    var properties = tweener.getProperties();
    updateElementTransform(element, matrix, perspective);
    updateElementProperties(element, properties);
  }
};
};

// ------------------------------
// End Time animation
// ------------------------------

// ------------------------
// -- AttentionAnimation --
// ------------------------

var createAttentionAnimation = function(options) {
var movement = options.movement;
options.initialVelocity = 0.1;
options.equilibriumPosition = 0;
var spring = createSpringEasing(options);
var stopped = false;
var tweenPosition = movement.position;
var tweenRotation = movement.rotation;
var tweenRotationPost = movement.rotationPost;
var tweenScale = movement.scale;
var tweenSkew = movement.skew;

var currentMovement = createState({
  position: tweenPosition ? [0, 0, 0] : undefined,
  rotation: tweenRotation ? [0, 0, 0] : undefined,
  rotationPost: tweenRotationPost ? [0, 0, 0] : undefined,
  scale: tweenScale ? [0, 0] : undefined,
  skew: tweenSkew ? [0, 0] : undefined,
});

// Public API
return {
  stop: function() {
    stopped = true;
  },

  isStopped: function(time) {
    return stopped;
  },

  tick: function(time) {
    if(stopped)
      return;
    if(spring.equilibrium)
      return;
    spring.tick();

    this.updateMovement();
  },

  updateMovement:function() {
    var value = spring.getValue();
    if(tweenPosition) {
      currentMovement.position[0] = movement.position[0] * value;
      currentMovement.position[1] = movement.position[1] * value;
      currentMovement.position[2] = movement.position[2] * value;
    }
    if(tweenRotation) {
      currentMovement.rotation[0] = movement.rotation[0] * value;
      currentMovement.rotation[1] = movement.rotation[1] * value;
      currentMovement.rotation[2] = movement.rotation[2] * value;
    }
    if(tweenRotationPost) {
      currentMovement.rotationPost[0] = movement.rotationPost[0] * value;
      currentMovement.rotationPost[1] = movement.rotationPost[1] * value;
      currentMovement.rotationPost[2] = movement.rotationPost[2] * value;
    }
    if(tweenScale) {
      currentMovement.scale[0] = 1 + movement.scale[0] * value;
      currentMovement.scale[1] = 1 + movement.scale[1] * value;
    }

    if(tweenSkew) {
      currentMovement.skew[0] = movement.skew[0] * value;
      currentMovement.skew[1] = movement.skew[1] * value;
    }
  },

  updateElement: function(element) {
    updateElementTransform(element, currentMovement.asMatrix());
    updateElementProperties(element, currentMovement.getProperties());
  },

  getCurrentState: function() {
    return currentMovement;
  },

  completed: function() {
    return spring.equilibrium || stopped;
  },

  restart: function() {
    // Restart spring
    spring = createSpringEasing(options);
  }
};
};


/**********
* Easings *
***********/

var linearEasing = function(value) {
return value;
};

var ease = function(value) {
return (Math.cos(value*Math.PI + Math.PI) + 1)/2;
};

var easeIn = function(value) {
return value*value;
};

var easeOut = function(value) {
return -Math.pow(value - 1, 2) + 1;
};

var createSpringEasing = function(options) {
var position = optionOrDefault(options.startPosition, 0);
var equilibriumPosition = optionOrDefault(options.equilibriumPosition, 1);
var velocity = optionOrDefault(options.initialVelocity, 0);
var springConstant = optionOrDefault(options.springConstant, 0.8);
var deceleration = optionOrDefault(options.springDeceleration, 0.9);
var mass = optionOrDefault(options.springMass, 10);

var equilibrium = false;

// Public API
return {

  tick: function(value) {
    if(value === 0.0)
      return;
    if(equilibrium)
      return;
    var springForce = -(position - equilibriumPosition) * springConstant;
    // f = m * a
    // a = f / m
    var a = springForce / mass;
    // s = v * t
    // t = 1 ( for now )
    velocity += a;
    position += velocity;

    // Deceleration
    velocity *= deceleration;

    if(Math.abs(position - equilibriumPosition) < 0.001 && Math.abs(velocity) < 0.001) {
      equilibrium = true;
    }
  },

  resetFrom: function(value) {
    position = value;
    velocity = 0;
  },


  getValue: function() {
    if(equilibrium)
      return equilibriumPosition;
    return position;
  },

  completed: function() {
    return equilibrium;
  }
};
};

var EASING_FUNCS = {
'linear': linearEasing,
'ease': ease,
'easeIn': easeIn,
'easeOut': easeOut,
};


var createEaser = function(easerName, options) {
if(easerName == 'spring') {
  return createSpringEasing(options);
}
var easeFunction = easerName;
if(!isFunction(easerName)) {
  easeFunction = EASING_FUNCS[easerName];
}

var easer = easeFunction;
var value = 0;
var lastValue;

// Public API
return {
  tick: function(v) {
    value = easer(v);
    lastValue = v;
  },

  resetFrom: function(value) {
    lastValue = 0;
  },

  getValue: function() {
    return value;
  },

  completed: function() {
    if(lastValue >= 1) {
      return lastValue;
    }
    return false;
  }
};
};

/***
* Matrix related
*/

var assignTranslate = function(matrix, x, y, z) {
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = 0;
matrix[5] = 1;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
matrix[12] = x;
matrix[13] = y;
matrix[14] = z;
matrix[15] = 1;
};

var assignRotateX = function(matrix, rad) {
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = 0;
matrix[5] = Math.cos(rad);
matrix[6] = -Math.sin(rad);
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = Math.sin(rad);
matrix[10] = Math.cos(rad);
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
};


var assignRotateY = function(matrix, rad) {
matrix[0] = Math.cos(rad);
matrix[1] = 0;
matrix[2] = Math.sin(rad);
matrix[3] = 0;
matrix[4] = 0;
matrix[5] = 1;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = -Math.sin(rad);
matrix[9] = 0;
matrix[10] = Math.cos(rad);
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
};

var assignRotateZ = function(matrix, rad) {
matrix[0] = Math.cos(rad);
matrix[1] = -Math.sin(rad);
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = Math.sin(rad);
matrix[5] = Math.cos(rad);
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
};

var assignSkew = function(matrix, ax, ay) {
matrix[0] = 1;
matrix[1] = Math.tan(ax);
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = Math.tan(ay);
matrix[5] = 1;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
};


var assignScale = function(matrix, x, y) {
matrix[0] = x;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = 0;
matrix[5] = y;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
};

var assignIdentity = function(matrix) {
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = 0;
matrix[5] = 1;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
};

var copyArray = function(a, b) {
b[0] = a[0];
b[1] = a[1];
b[2] = a[2];
b[3] = a[3];
b[4] = a[4];
b[5] = a[5];
b[6] = a[6];
b[7] = a[7];
b[8] = a[8];
b[9] = a[9];
b[10] = a[10];
b[11] = a[11];
b[12] = a[12];
b[13] = a[13];
b[14] = a[14];
b[15] = a[15];
};

var createMatrix = function() {
var data = new Float32Array(16);
var a = new Float32Array(16);
var b = new Float32Array(16);
assignIdentity(data);

return {
  data: data,

  asCSS: function() {
    var css = 'matrix3d(';
    for(var i=0;i<15;++i) {
      if(Math.abs(data[i]) < 0.0001)
        css += '0,';
      else
        css += data[i].toFixed(10) + ',';
    }
    if(Math.abs(data[15]) < 0.0001)
      css += '0)';
    else
      css += data[15].toFixed(10) + ')';
    return css;
  },

  clear: function() {
    assignIdentity(data);
  },

  translate: function(x, y, z) {
    copyArray(data, a);
    assignTranslate(b, x, y, z);
    assignedMatrixMultiplication(a, b, data);
    return this;
  },

  rotateX: function(radians) {
    copyArray(data, a);
    assignRotateX(b, radians);
    assignedMatrixMultiplication(a, b, data);
    return this;
  },

  rotateY: function(radians) {
    copyArray(data, a);
    assignRotateY(b, radians);
    assignedMatrixMultiplication(a, b, data);
    return this;
  },

  rotateZ: function(radians) {
    copyArray(data, a);
    assignRotateZ(b, radians);
    assignedMatrixMultiplication(a, b, data);
    return this;
  },

  scale: function(x, y) {
    copyArray(data, a);
    assignScale(b, x, y);
    assignedMatrixMultiplication(a, b, data);
    return this;
  },

  skew: function(ax, ay) {
    copyArray(data, a);
    assignSkew(b, ax, ay);
    assignedMatrixMultiplication(a, b, data);
    return this;
  }
};
};

var assignedMatrixMultiplication = function(a, b, res) {
// Unrolled loop
res[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12];
res[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13];
res[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14];
res[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15];

res[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12];
res[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13];
res[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14];
res[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15];

res[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12];
res[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13];
res[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14];
res[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15];

res[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12];
res[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13];
res[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14];
res[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15];

return res;
};

var createState = function(config) {
// Caching of matrix and properties so we don't have to create new ones everytime they are needed
var matrix = createMatrix();
var properties = {
  opacity: undefined,
  width: undefined,
  height: undefined
};

// Public API
return {
  position: config.position,
  rotation: config.rotation,
  rotationPost: config.rotationPost,
  skew: config.skew,
  scale: config.scale,
  scalePost: config.scalePost,
  opacity: config.opacity,
  width: config.width,
  height: config.height,


  clone: function() {
    return createState({
      position: this.position ? this.position.slice(0) : undefined,
      rotation: this.rotation ? this.rotation.slice(0) : undefined,
      rotationPost: this.rotationPost ? this.rotationPost.slice(0) : undefined,
      skew: this.skew ? this.skew.slice(0) : undefined,
      scale: this.scale ? this.scale.slice(0) : undefined,
      scalePost: this.scalePost ? this.scalePost.slice(0) : undefined,
      height: this.height,
      width: this.width,
      opacity: this.opacity
    });
  },

  asMatrix: function() {
    var m = matrix;
    m.clear();

    if(this.transformOrigin)
      m.translate(-this.transformOrigin[0], -this.transformOrigin[1], -this.transformOrigin[2]);

    if(this.scale) {
      m.scale(this.scale[0], this.scale[1]);
    }

    if(this.skew) {
      m.skew(this.skew[0], this.skew[1]);
    }

    if(this.rotation) {
      m.rotateX(this.rotation[0]);
      m.rotateY(this.rotation[1]);
      m.rotateZ(this.rotation[2]);
    }

    if(this.position) {
      m.translate(this.position[0], this.position[1], this.position[2]);
    }

    if(this.rotationPost) {
      m.rotateX(this.rotationPost[0]);
      m.rotateY(this.rotationPost[1]);
      m.rotateZ(this.rotationPost[2]);
    }

    if(this.scalePost) {
      m.scale(this.scalePost[0], this.scalePost[1]);
    }

    if(this.transformOrigin)
      m.translate(this.transformOrigin[0], this.transformOrigin[1], this.transformOrigin[2]);
    return m;
  },

  getProperties: function() {
    properties.opacity = this.opacity;
    properties.width = this.width + 'px';
    properties.height = this.height + 'px';
    return properties;
  }
};
};
// ------------------
// -- StateTweener -- 
// -------------------

var createStateTweener = function(startState, endState, resultState) {
var start = startState;
var end = endState;
var result = resultState;

var tweenPosition = end.position !== undefined;
var tweenRotation = end.rotation !== undefined;
var tweenRotationPost = end.rotationPost !== undefined;
var tweenScale = end.scale !== undefined;
var tweenSkew = end.skew !== undefined;
var tweenWidth = end.width !== undefined;
var tweenHeight = end.height !== undefined;
var tweenOpacity = end.opacity !== undefined;

// Public API
return {

  tween: function(tweenValue) {

    if(tweenPosition) {
      var dX = (end.position[0] - start.position[0]);
      var dY = (end.position[1] - start.position[1]);
      var dZ = (end.position[2] - start.position[2]);
      result.position[0] = start.position[0] + tweenValue*dX;
      result.position[1] = start.position[1] + tweenValue*dY;
      result.position[2] = start.position[2] + tweenValue*dZ;
    }

    if(tweenRotation) {
      var dAX = (end.rotation[0] - start.rotation[0]);
      var dAY = (end.rotation[1] - start.rotation[1]);
      var dAZ = (end.rotation[2] - start.rotation[2]);
      result.rotation[0] = start.rotation[0] + tweenValue*dAX;
      result.rotation[1] = start.rotation[1] + tweenValue*dAY;
      result.rotation[2] = start.rotation[2] + tweenValue*dAZ;
    }

    if(tweenRotationPost) {
      var dBX = (end.rotationPost[0] - start.rotationPost[0]);
      var dBY = (end.rotationPost[1] - start.rotationPost[1]);
      var dBZ = (end.rotationPost[2] - start.rotationPost[2]);
      result.rotationPost[0] = start.rotationPost[0] + tweenValue*dBX;
      result.rotationPost[1] = start.rotationPost[1] + tweenValue*dBY;
      result.rotationPost[2] = start.rotationPost[2] + tweenValue*dBZ;
    }

    if(tweenSkew) {
      var dSX = (end.scale[0] - start.scale[0]);
      var dSY = (end.scale[1] - start.scale[1]);

      result.scale[0] = start.scale[0] + tweenValue*dSX;
      result.scale[1] = start.scale[1] + tweenValue*dSY;
    }

    if(tweenScale) {
      var dSkewX = (end.skew[0] - start.skew[0]);
      var dSkewY = (end.skew[1] - start.skew[1]);

      result.skew[0] = start.skew[0] + tweenValue*dSkewX;
      result.skew[1] = start.skew[1] + tweenValue*dSkewY;
    }

    if(tweenWidth) {
      var dWidth = (end.width - start.width);
      result.width = start.width + tweenValue*dWidth;
    }


    if(tweenHeight) {
      var dHeight = (end.height - start.height);
      result.height = start.height + tweenValue*dHeight;
    }

    if(tweenOpacity) {
      var dOpacity = (end.opacity - start.opacity);
      result.opacity = start.opacity + tweenValue*dOpacity;
    }

  },

  asMatrix: function() {
    return result.asMatrix();
  },

  getProperties: function() {
    return result.getProperties();
  },

  setReverse: function() {
    var oldStart = start;
    start = end;
    end = oldStart;
  }
};
};

// ------------------------
// -- ValueFeederTweener -- 
// ------------------------

var createValueFeederTweener = function(valueFeeder, startState, endState, resultState) {
var currentMatrix = valueFeeder(0, createMatrix());
var start = startState;
var end = endState;
var result = resultState;
var reverse = false;


// Public API
return {

  tween: function(tweenValue) {
    if(reverse)
      tweenValue = 1 - tweenValue;
    currentMatrix.clear();
    currentMatrix = valueFeeder(tweenValue, currentMatrix);

    var dWidth = (end.width - start.width);
    var dHeight = (end.height - start.height);
    var dOpacity = (end.opacity - start.opacity);

    if(end.width !== undefined)
      result.width = start.width + tweenValue*dWidth;
    if(end.height !== undefined)
      result.height = start.height + tweenValue*dHeight;
    if(end.opacity !== undefined)
      result.opacity = start.opacity + tweenValue*dOpacity;
  },

  asMatrix: function() {
    return currentMatrix;
  },

  getProperties: function() {
    return result.getProperties();
  },

  setReverse: function() {
    reverse = true;
  }

};
};

var optionOrDefault = function(option, def) {
if(typeof option == 'undefined') {
  return def;
}
return option;
};

var updateElementTransform = function(element, matrix, perspective) {
var cssPerspective = '';
if(perspective) {
  cssPerspective = 'perspective(' + perspective + 'px) ';
}
var cssMatrix = matrix.asCSS();
element.style[transformProperty] = cssPerspective + cssMatrix;
};

var updateElementProperties = function(element, properties) {
for(var key in properties) {
  element.style[key] = properties[key];
}
};

var isFunction = function(object) {
return (typeof object === "function");
};

var cloneObject = function(object) {
if(!object)
  return object;
var clone = {};
for(var key in object) {
  clone[key] = object[key];
}
return clone;
};

snabbt.createMatrix = createMatrix;
snabbt.setElementTransform = updateElementTransform;
return snabbt;
}());
var stackBlur = (function(){

var mul_table = [
    512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
    454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
    482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
    437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
    497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
    320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
    446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
    329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,
    505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,
    399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,
    324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,
    268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,
    451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,
    385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,
    332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,
    289,287,285,282,280,278,275,273,271,269,267,265,263,261,259];


var shg_table = [
    9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
    17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
    19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
    20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
    21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
    21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
    22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
    22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
    23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
    23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
    23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
    23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ];

function getImageDataFromCanvas(canvas, top_x, top_y, width, height)
{
    if (typeof(canvas) == 'string')
        canvas  = document.getElementById(canvas);
    else if (!canvas instanceof HTMLCanvasElement)
        return;

    var context = canvas.getContext('2d');
    var imageData;

    try {
        try {
            imageData = context.getImageData(top_x, top_y, width, height);
        } catch(e) {
            throw new Error("unable to access local image data: " + e);
            return;
        }
    } catch(e) {
        throw new Error("unable to access image data: " + e);
    }

    return imageData;
}

function processCanvasRGBA(canvas, top_x, top_y, width, height, radius)
{
    if (isNaN(radius) || radius < 1) return;
    radius |= 0;

    var imageData = getImageDataFromCanvas(canvas, top_x, top_y, width, height);

    imageData = processImageDataRGBA(imageData, top_x, top_y, width, height, radius);

    canvas.getContext('2d').putImageData(imageData, top_x, top_y);
}

function processImageDataRGBA(imageData, top_x, top_y, width, height, radius)
{
    var pixels = imageData.data;

    var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum,
        r_out_sum, g_out_sum, b_out_sum, a_out_sum,
        r_in_sum, g_in_sum, b_in_sum, a_in_sum,
        pr, pg, pb, pa, rbs;

    var div = radius + radius + 1;
    var w4 = width << 2;
    var widthMinus1  = width - 1;
    var heightMinus1 = height - 1;
    var radiusPlus1  = radius + 1;
    var sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2;

    var stackStart = new BlurStack();
    var stack = stackStart;
    for (i = 1; i < div; i++)
    {
        stack = stack.next = new BlurStack();
        if (i == radiusPlus1) var stackEnd = stack;
    }
    stack.next = stackStart;
    var stackIn = null;
    var stackOut = null;

    yw = yi = 0;

    var mul_sum = mul_table[radius];
    var shg_sum = shg_table[radius];

    for (y = 0; y < height; y++)
    {
        r_in_sum = g_in_sum = b_in_sum = a_in_sum = r_sum = g_sum = b_sum = a_sum = 0;

        r_out_sum = radiusPlus1 * (pr = pixels[yi]);
        g_out_sum = radiusPlus1 * (pg = pixels[yi+1]);
        b_out_sum = radiusPlus1 * (pb = pixels[yi+2]);
        a_out_sum = radiusPlus1 * (pa = pixels[yi+3]);

        r_sum += sumFactor * pr;
        g_sum += sumFactor * pg;
        b_sum += sumFactor * pb;
        a_sum += sumFactor * pa;

        stack = stackStart;

        for (i = 0; i < radiusPlus1; i++)
        {
            stack.r = pr;
            stack.g = pg;
            stack.b = pb;
            stack.a = pa;
            stack = stack.next;
        }

        for (i = 1; i < radiusPlus1; i++)
        {
            p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2);
            r_sum += (stack.r = (pr = pixels[p])) * (rbs = radiusPlus1 - i);
            g_sum += (stack.g = (pg = pixels[p+1])) * rbs;
            b_sum += (stack.b = (pb = pixels[p+2])) * rbs;
            a_sum += (stack.a = (pa = pixels[p+3])) * rbs;

            r_in_sum += pr;
            g_in_sum += pg;
            b_in_sum += pb;
            a_in_sum += pa;

            stack = stack.next;
        }


        stackIn = stackStart;
        stackOut = stackEnd;
        for (x = 0; x < width; x++)
        {
            pixels[yi+3] = pa = (a_sum * mul_sum) >> shg_sum;
            if (pa != 0)
            {
                pa = 255 / pa;
                pixels[yi]   = ((r_sum * mul_sum) >> shg_sum) * pa;
                pixels[yi+1] = ((g_sum * mul_sum) >> shg_sum) * pa;
                pixels[yi+2] = ((b_sum * mul_sum) >> shg_sum) * pa;
            } else {
                pixels[yi] = pixels[yi+1] = pixels[yi+2] = 0;
            }

            r_sum -= r_out_sum;
            g_sum -= g_out_sum;
            b_sum -= b_out_sum;
            a_sum -= a_out_sum;

            r_out_sum -= stackIn.r;
            g_out_sum -= stackIn.g;
            b_out_sum -= stackIn.b;
            a_out_sum -= stackIn.a;

            p =  (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2;

            r_in_sum += (stackIn.r = pixels[p]);
            g_in_sum += (stackIn.g = pixels[p+1]);
            b_in_sum += (stackIn.b = pixels[p+2]);
            a_in_sum += (stackIn.a = pixels[p+3]);

            r_sum += r_in_sum;
            g_sum += g_in_sum;
            b_sum += b_in_sum;
            a_sum += a_in_sum;

            stackIn = stackIn.next;

            r_out_sum += (pr = stackOut.r);
            g_out_sum += (pg = stackOut.g);
            b_out_sum += (pb = stackOut.b);
            a_out_sum += (pa = stackOut.a);

            r_in_sum -= pr;
            g_in_sum -= pg;
            b_in_sum -= pb;
            a_in_sum -= pa;

            stackOut = stackOut.next;

            yi += 4;
        }
        yw += width;
    }


    for (x = 0; x < width; x++)
    {
        g_in_sum = b_in_sum = a_in_sum = r_in_sum = g_sum = b_sum = a_sum = r_sum = 0;

        yi = x << 2;
        r_out_sum = radiusPlus1 * (pr = pixels[yi]);
        g_out_sum = radiusPlus1 * (pg = pixels[yi+1]);
        b_out_sum = radiusPlus1 * (pb = pixels[yi+2]);
        a_out_sum = radiusPlus1 * (pa = pixels[yi+3]);

        r_sum += sumFactor * pr;
        g_sum += sumFactor * pg;
        b_sum += sumFactor * pb;
        a_sum += sumFactor * pa;

        stack = stackStart;

        for (i = 0; i < radiusPlus1; i++)
        {
            stack.r = pr;
            stack.g = pg;
            stack.b = pb;
            stack.a = pa;
            stack = stack.next;
        }

        yp = width;

        for (i = 1; i <= radius; i++)
        {
            yi = (yp + x) << 2;

            r_sum += (stack.r = (pr = pixels[yi])) * (rbs = radiusPlus1 - i);
            g_sum += (stack.g = (pg = pixels[yi+1])) * rbs;
            b_sum += (stack.b = (pb = pixels[yi+2])) * rbs;
            a_sum += (stack.a = (pa = pixels[yi+3])) * rbs;

            r_in_sum += pr;
            g_in_sum += pg;
            b_in_sum += pb;
            a_in_sum += pa;

            stack = stack.next;

            if(i < heightMinus1)
            {
                yp += width;
            }
        }

        yi = x;
        stackIn = stackStart;
        stackOut = stackEnd;
        for (y = 0; y < height; y++)
        {
            p = yi << 2;
            pixels[p+3] = pa = (a_sum * mul_sum) >> shg_sum;
            if (pa > 0)
            {
                pa = 255 / pa;
                pixels[p]   = ((r_sum * mul_sum) >> shg_sum) * pa;
                pixels[p+1] = ((g_sum * mul_sum) >> shg_sum) * pa;
                pixels[p+2] = ((b_sum * mul_sum) >> shg_sum) * pa;
            } else {
                pixels[p] = pixels[p+1] = pixels[p+2] = 0;
            }

            r_sum -= r_out_sum;
            g_sum -= g_out_sum;
            b_sum -= b_out_sum;
            a_sum -= a_out_sum;

            r_out_sum -= stackIn.r;
            g_out_sum -= stackIn.g;
            b_out_sum -= stackIn.b;
            a_out_sum -= stackIn.a;

            p = (x + (((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width)) << 2;

            r_sum += (r_in_sum += (stackIn.r = pixels[p]));
            g_sum += (g_in_sum += (stackIn.g = pixels[p+1]));
            b_sum += (b_in_sum += (stackIn.b = pixels[p+2]));
            a_sum += (a_in_sum += (stackIn.a = pixels[p+3]));

            stackIn = stackIn.next;

            r_out_sum += (pr = stackOut.r);
            g_out_sum += (pg = stackOut.g);
            b_out_sum += (pb = stackOut.b);
            a_out_sum += (pa = stackOut.a);

            r_in_sum -= pr;
            g_in_sum -= pg;
            b_in_sum -= pb;
            a_in_sum -= pa;

            stackOut = stackOut.next;

            yi += width;
        }
    }
    return imageData;
}

function BlurStack()
{
    this.r = 0;
    this.g = 0;
    this.b = 0;
    this.a = 0;
    this.next = null;
}

return processCanvasRGBA;

}());
// canvas to blob polyfill
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Polyfill
if (!HTMLCanvasElement.prototype.toBlob) {
	Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
		value: function (callback, type, quality) {

			var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
				len = binStr.length,
				arr = new Uint8Array(len);

			for (var i=0; i<len; i++ ) {
				arr[i] = binStr.charCodeAt(i);
			}

			callback( new Blob( [arr], {type: type || 'image/png'} ) );
		}
	});
}
'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var copyString = function copyString(str) {
	return str + '';
};

var getDataset = function getDataset(el) {

	if (typeof el.dataset === 'undefined') {
		var res = {};
		var attr;
		var attrName;
		var attrs = el.attributes;
		for (attr in attrs) {
			if (attrs.hasOwnProperty(attr) && attrs[attr].name && /^data-[a-z_\-\d]*$/i.test(attrs[attr].name)) {
				attrName = toCamelCase(attrs[attr].name.substr(5));
				res[attrName] = attrs[attr].value;
			}
		}
		return res;
	}
	return el.dataset;
};

var toCamelCase = function toCamelCase(str) {
	return str.replace(/\-./g, function (substr) {
		return substr.charAt(1).toUpperCase();
	});
};

var getElementAttributes = function getElementAttributes(el) {
	return Array.prototype.slice.call(el.attributes).map(function (attr) {
		return {
			'name': attr.name,
			'value': attr.value
		};
	});
};

// helper method
var getOffsetByEvent = function getOffsetByEvent(e) {
	return {
		x: typeof e.offsetX === 'undefined' ? e.layerX : e.offsetX,
		y: typeof e.offsetY === 'undefined' ? e.layerY : e.offsetY
	};
};

// merge two objects together
var mergeOptions = function mergeOptions(base, additives) {

	var key;
	var options = {};
	var optionsToMerge = additives || {};

	for (key in base) {
		if (!base.hasOwnProperty(key)) {
			continue;
		}
		options[key] = typeof optionsToMerge[key] === 'undefined' ? base[key] : optionsToMerge[key];
	}

	return options;
};

// keys
var Key = {
	ESC: 27,
	RETURN: 13
};

// pointer events
var Events = {
	DOWN: ['touchstart', 'pointerdown', 'mousedown'],
	MOVE: ['touchmove', 'pointermove', 'mousemove'],
	UP: ['touchend', 'touchcancel', 'pointerup', 'mouseup']
};

var MimeTypes = {
	'jpeg': 'image/jpeg',
	'jpg': 'image/jpeg',
	'jpe': 'image/jpeg',
	'png': 'image/png',
	'gif': 'image/gif',
	'bmp': 'image/bmp'
};

var ImageExtensionsRegex = /(\.png|\.bmp|\.gif|\.jpg|\.jpe|\.jpg|\.jpeg)$/;

var CanvasExportExtensions = /(jpe|jpg|jpeg|png)/;

// shortcuts
var create = function create(name, className) {
	var node = document.createElement(name);
	if (className) {
		node.className = className;
	}
	return node;
};

// events
var addEvents = function addEvents(obj, events, scope) {
	events.forEach(function (event) {
		obj.addEventListener(event, scope, false);
	});
};

var removeEvents = function removeEvents(obj, events, scope) {
	events.forEach(function (event) {
		obj.removeEventListener(event, scope, false);
	});
};

var getEventOffset = function getEventOffset(e) {

	var event = e.changedTouches ? e.changedTouches[0] : e;

	// no event found, quit!
	if (!event) {
		return;
	}

	// get offset from events
	return {
		x: event.pageX,
		y: event.pageY
	};
};

var getEventOffsetLocal = function getEventOffsetLocal(e, local) {

	var offset = getEventOffset(e);
	var rect = local.getBoundingClientRect();
	var top = window.pageYOffset || document.documentElement.scrollTop;
	var left = window.pageXOffset || document.documentElement.scrollLeft;

	return {
		x: offset.x - rect.left - left,
		y: offset.y - rect.top - top
	};
};

var lowercaseFirstLetter = function lowercaseFirstLetter(string) {
	return string.charAt(0).toLowerCase() + string.slice(1);
};

var capitalizeFirstLetter = function capitalizeFirstLetter(string) {
	return string.charAt(0).toUpperCase() + string.slice(1);
};

var last = function last(array) {
	return array[array.length - 1];
};

var limit = function limit(value, min, max) {
	return Math.max(min, Math.min(max, value));
};

var inArray = function inArray(needle, arr) {
	return arr.indexOf(needle) !== -1;
};

var send = function send(url, data, requestDecorator, progress, success, err) {

	var xhr = new XMLHttpRequest();

	// if progress callback defined handle progress events
	if (progress) {
		xhr.upload.addEventListener('progress', function (e) {
			progress(e.loaded, e.total);
		});
	}

	// open the request
	xhr.open('POST', url, true);

	// if request decorator defined pass XMLHttpRequest instance to decorator
	if (requestDecorator) {
		requestDecorator(xhr);
	}

	// handle state changes
	xhr.onreadystatechange = function () {

		if (xhr.readyState === 4 && xhr.status === 200) {

			var text = xhr.responseText;

			// if no data returned from server assume success
			if (!text.length) {
				success();
				return;
			}

			// catch possible PHP content length problem
			if (text.indexOf('Content-Length') !== -1) {
				err('file-too-big');
				return;
			}

			// if data returned it should be in suggested JSON format
			var obj = void 0;
			try {
				obj = JSON.parse(xhr.responseText);
			} catch (e) {}

			// if is failure response
			if ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj.status === 'failure') {
				err(obj.message);
				return;
			}

			success(obj || text);
		} else if (xhr.readyState === 4) {
			err('fail');
		}
	};

	// do request
	xhr.send(data);
};

var resetTransforms = function resetTransforms(element) {
	if (!element) {
		return;
	}
	element.style.webkitTransform = '';
	element.style.transform = '';
};

var bytesToMegaBytes = function bytesToMegaBytes(b) {
	return b / 1000000;
};

var megaBytesToBytes = function megaBytesToBytes(mb) {
	return mb * 1000000;
};

var getCommonMimeTypes = function getCommonMimeTypes() {
	var types = [];
	var type = void 0;
	var mimeType = void 0;
	for (type in MimeTypes) {
		if (!MimeTypes.hasOwnProperty(type)) {
			continue;
		}
		mimeType = MimeTypes[type];
		if (types.indexOf(mimeType) == -1) {
			types.push(mimeType);
		}
	}
	return types;
};

var isJPEGMimeType = function isJPEGMimeType(type) {
	return type === 'image/jpeg';
};

var getExtensionByMimeType = function getExtensionByMimeType(mimetype) {
	var type = void 0;
	for (type in MimeTypes) {
		if (!MimeTypes.hasOwnProperty(type)) {
			continue;
		}
		if (MimeTypes[type] === mimetype) {
			return type;
		}
	}
	return mimetype;
};

var getMimeTypeFromResponseType = function getMimeTypeFromResponseType(responseType) {
	var type = void 0;
	for (type in MimeTypes) {
		if (!MimeTypes.hasOwnProperty(type)) {
			continue;
		}
		if (responseType.indexOf(MimeTypes[type]) !== -1) {
			return MimeTypes[type];
		}
	}
	return null;
};

var getFileMimeType = function getFileMimeType(extension) {
	return MimeTypes[extension ? extension.toLowerCase() : extension] || 'unknown';
};

var getDataTransferItemType = function getDataTransferItemType(dataTransferItem) {
	if (!dataTransferItem) {
		return null;
	}
	var file = null;
	if (typeof dataTransferItem.getAsFile == 'function') {
		file = dataTransferItem.getAsFile();
	}
	return file ? file.type : dataTransferItem.type;
};

var getFileName = function getFileName(path) {
	return path.split('/').pop().split('?').shift();
};

var getFileNameWithoutExtension = function getFileNameWithoutExtension(path) {
	if (typeof path !== 'string') {
		return 'unknown';
	}
	var name = getFileName(path);
	return name.split('.').shift();
};

var blobToFile = function blobToFile(blob, name) {
	blob.lastModifiedDate = new Date();
	blob.name = name;
	return blob;
};

var resourceIsBase64Data = function resourceIsBase64Data(resource) {
	return (/^data:image/.test(resource)
	);
};

var loadURL = function loadURL(url, cb) {

	var xhr = new XMLHttpRequest();
	xhr.open('GET', url, true);
	xhr.responseType = 'blob';
	xhr.onload = function (e) {
		var name = getFileName(url);
		var type = getMimeTypeFromResponseType(this.response.type);
		if (!ImageExtensionsRegex.test(name)) {
			name += '.' + getExtensionByMimeType(type);
		}

		// get as file
		var file = blobToFile(this.response, name);

		// need to set correct type
		cb(cloneFile(file, type));
	};
	xhr.send();
};

var base64ToByteString = function base64ToByteString(dataURI) {

	// get data part of string (remove data:image/jpeg...,)
	var dataPart = dataURI.split(',')[1];

	// remove any whitespace as that causes InvalidCharacterError in IE
	var dataPartCleaned = dataPart.replace(/\s/g, '');

	// to bytestring
	return atob(dataPartCleaned);
};

var base64ToBlob = function base64ToBlob(dataURI) {

	var byteString = base64ToByteString(dataURI);
	var ab = new ArrayBuffer(byteString.length);
	var ia = new Uint8Array(ab);

	for (var i = 0; i < byteString.length; i++) {
		ia[i] = byteString.charCodeAt(i);
	}

	var mimeType = getMimeTypeFromDataURI(dataURI);
	return blobToFile(new Blob([ab], {
		type: mimeType
	}), 'unknown.' + getExtensionByMimeType(mimeType));
};

var getImageAsCanvas = function getImageAsCanvas(src, callback) {

	// only cross origin when it's not base64 data, to prevent errors in Safari
	// http://stackoverflow.com/questions/31643096/why-does-safari-throw-cors-error-when-setting-base64-data-on-a-crossorigin-an
	var crossOrigin = typeof src === 'string' ? src.indexOf('data:image') !== 0 : true;

	loadImage.parseMetaData(src, function (meta) {

		var options = {
			canvas: true,
			crossOrigin: crossOrigin
		};

		if (meta.exif) {
			options.orientation = meta.exif.get('Orientation');
		}

		loadImage(src, function (res) {

			if (res.type === 'error') {
				callback();
				return;
			}

			callback(res, meta);
		}, options);
	});
};

var getAutoCropRect = function getAutoCropRect(width, height, ratioOut) {

	var x,
	    y,
	    w,
	    h,
	    ratioIn = height / width;

	// if input is portrait and required is landscape
	// width is portrait width, height is width times outputRatio
	if (ratioIn < ratioOut) {
		h = height;
		w = h / ratioOut;
		x = (width - w) * .5;
		y = 0;
	}

	// if input is landscape and required is portrait
	// height is landscape height, width is height divided by outputRatio
	else {
			w = width;
			h = w * ratioOut;
			x = 0;
			y = (height - h) * .5;
		}

	return {
		x: x,
		y: y,
		height: h,
		width: w
	};
};

var transformCanvas = function transformCanvas(canvas) {
	var transforms = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
	var cb = arguments[2];


	var result = create('canvas');

	var rotation = transforms.rotation;
	var crop = transforms.crop;
	var size = transforms.size;

	// do crop transforms

	if (crop) {

		// do crop
		var isTilted = rotation % 180 !== 0;
		var space = {
			width: isTilted ? canvas.height : canvas.width,
			height: isTilted ? canvas.width : canvas.height
		};

		// crop offsets in percentages
		var px = crop.x / space.width;
		var py = crop.y / space.height;
		var pw = crop.width / space.width;
		var ph = crop.height / space.height;

		// limit crop to size of canvas else safari might return transparent image
		if (crop.y + crop.height > space.height) {
			crop.y = Math.max(0, space.height - crop.height);
		}

		if (crop.x + crop.width > space.width) {
			crop.x = Math.max(0, space.width - crop.width);
		}

		// resize canvas to the final crop result size
		result.width = crop.width;
		result.height = crop.height;

		// draw the crop
		var ctx = result.getContext('2d');

		if (rotation === 90) {

			ctx.translate(result.width * .5, result.height * .5);
			ctx.rotate(-90 * Math.PI / 180);
			ctx.drawImage(canvas,

			// source rectangle (crop area)
			(1 - py) * canvas.width - canvas.width * ph, crop.x, crop.height, crop.width,

			// target area (cover)
			-result.height * .5, -result.width * .5, result.height, result.width);
		} else if (rotation === 180) {

			ctx.translate(result.width * .5, result.height * .5);
			ctx.rotate(-180 * Math.PI / 180);
			ctx.drawImage(canvas,

			// source rectangle (crop area)
			(1 - (px + pw)) * space.width, (1 - (py + ph)) * space.height, pw * space.width, ph * space.height,

			// target area (cover)
			-result.width * .5, -result.height * .5, result.width, result.height);
		} else if (rotation === 270) {

			ctx.translate(result.width * .5, result.height * .5);
			ctx.rotate(-270 * Math.PI / 180);
			ctx.drawImage(canvas,

			// source rectangle (crop area)
			crop.y, (1 - px) * canvas.height - canvas.height * pw, crop.height, crop.width,

			// target area (cover)
			-result.height * .5, -result.width * .5, result.height, result.width);
		} else {

			ctx.drawImage(canvas,

			// source rectangle (crop area)
			crop.x, crop.y, crop.width, crop.height,

			// target area (cover)
			0, 0, result.width, result.height);
		}
	}

	// do size transforms
	if (size) {

		var scalarX = size.width / result.width;
		var scalarY = size.height / result.height;
		var scalar = Math.min(scalarX, scalarY);

		// pick smallest scalar
		scaleCanvas(result, scalar);
	}

	cb(result);
};

function scaleCanvas(canvas, scalar) {

	// if not scaling down, bail out
	if (scalar >= 1) {
		return;
	}

	var targetWidth = Math.round(canvas.width * scalar);
	var targetHeight = Math.round(canvas.height * scalar);

	var c = cloneCanvas(canvas);
	var ctx = void 0;
	var width = canvas.width;
	var height = canvas.height;

	// scale down in steps
	while (width > targetWidth) {

		width *= .5;
		height *= .5;

		if (width < targetWidth) {
			// skip to final draw
			break;
		}

		c = create('canvas');
		c.width = width;
		c.height = height;
		ctx = c.getContext('2d');
		ctx.drawImage(canvas, 0, 0, width, height);
	}

	// draw final result back to original canvas
	canvas.width = targetWidth;
	canvas.height = targetHeight;
	ctx = canvas.getContext('2d');
	ctx.drawImage(c, 0, 0, targetWidth, targetHeight);
}

var sizeDist = function sizeDist(rect, canvas) {

	var dx = Math.abs(rect.width - canvas.width);
	var dy = Math.abs(rect.height - canvas.height);

	return Math.max(dx, dy);
};

var cloneCanvas = function cloneCanvas(original) {
	return cloneCanvasScaled(original, 1);
};

var cloneCanvasScaled = function cloneCanvasScaled(original, scalar) {

	if (!original) {
		return null;
	}

	var duplicate = document.createElement('canvas');
	var ctx = duplicate.getContext('2d');
	duplicate.width = original.width;
	duplicate.height = original.height;
	ctx.drawImage(original, 0, 0);
	if (scalar > 0 && scalar != 1) {
		scaleCanvas(duplicate, scalar);
	}

	return duplicate;
};

var canvasHasDimensions = function canvasHasDimensions(canvas) {
	return canvas.width && canvas.height;
};

var copyCanvas = function copyCanvas(original, destination) {

	var ctx = destination.getContext('2d');
	if (canvasHasDimensions(destination)) {
		ctx.drawImage(original, 0, 0, destination.width, destination.height);
	} else {
		destination.width = original.width;
		destination.height = original.height;
		ctx.drawImage(original, 0, 0);
	}
};

var clearCanvas = function clearCanvas(canvas) {
	var ctx = canvas.getContext('2d');
	ctx.clearRect(0, 0, canvas.width, canvas.height);
};

var blurCanvas = function blurCanvas(canvas) {
	stackBlur(canvas, 0, 0, canvas.width, canvas.height, 3);
};

var downloadCanvas = function downloadCanvas(canvas, filename, mimeType) {

	canvas.toBlob(function (blob) {

		if ('msSaveBlob' in window.navigator) {
			window.navigator.msSaveBlob(blob, filename);
			return;
		}

		var url = URL.createObjectURL(blob);

		// setup hidden link
		var link = create('a');
		link.style.display = 'none';
		link.download = filename;
		link.href = url;

		// attach to DOM otherwise this does not work in Firefox
		document.body.appendChild(link);

		// fire click
		link.click();

		// delay on remove otherwise does not work in Firefox
		setTimeout(function () {
			document.body.removeChild(link);
			URL.revokeObjectURL(url);
		}, 0);
	}, mimeType);
};

var covers = function covers(image, rect) {
	return parseInt(image.width, 10) >= rect.width && parseInt(image.height, 10) >= rect.height;
};

var scaleRect = function scaleRect(rect, w, h) {
	return {
		x: rect.x * w,
		y: rect.y * h,
		width: rect.width * w,
		height: rect.height * h
	};
};

var divideRect = function divideRect(rect, w, h) {
	return {
		x: rect.x / w,
		y: rect.y / h,
		width: rect.width / w,
		height: rect.height / h
	};
};

var resetFileInput = function resetFileInput(input) {

	// no value, no need to reset
	if (!input || input.value === '') {
		return;
	}

	try {
		// for modern browsers
		input.value = '';
	} catch (err) {}

	// for IE10
	if (input.value) {

		// quickly append input to temp form and reset form
		var form = document.createElement('form');
		var parentNode = input.parentNode;
		var ref = input.nextSibling;
		form.appendChild(input);
		form.reset();

		// re-inject input where it originally was
		if (ref) {
			parentNode.insertBefore(input, ref);
		} else {
			parentNode.appendChild(input);
		}
	}
};

var clone = function clone(obj) {
	if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null) {
		return JSON.parse(JSON.stringify(obj));
	}
	return obj;
};

var cloneFile = function cloneFile(file) {
	var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];

	if (!file) {
		return null;
	}
	var dupe = file.slice(0, file.size, type || file.type);
	dupe.name = file.name;
	dupe.lastModifiedDate = new Date(file.lastModifiedDate);
	return dupe;
};

var cloneData = function cloneData(obj) {
	var dupe = clone(obj);
	dupe.input.file = cloneFile(obj.input.file);
	dupe.output.image = cloneCanvas(obj.output.image);
	return dupe;
};

/**
 * @param image
 * @param type
 * @param jpegCompression - value between 0 and 100 or undefined/null to use default compression
 * @returns {*}
 */
var toDataURL = function toDataURL(image, type, jpegCompression) {
	if (!image || !type) {
		return null;
	}
	return image.toDataURL(type, isJPEGMimeType(type) && typeof jpegCompression === 'number' ? jpegCompression / 100 : undefined);
};

var getMimeTypeFromDataURI = function getMimeTypeFromDataURI(dataUri) {
	if (!dataUri) {
		return null;
	}
	var matches = dataUri.substr(0, 16).match(/^.+;/);
	if (matches.length) {
		return matches[0].substring(5, matches[0].length - 1);
	}
	return null;
};

var flattenData = function flattenData(obj) {
	var props = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
	var jpegCompression = arguments[2];
	var forcedType = arguments[3];


	var data = {
		server: clone(obj.server),
		meta: clone(obj.meta),
		input: {
			name: obj.input.name,
			type: obj.input.type,
			size: obj.input.size,
			width: obj.input.width,
			height: obj.input.height,
			field: obj.input.field
		}
	};

	if (inArray('output', props)) {

		data.output = {
			name: forcedType ? getFileNameWithoutExtension(obj.input.name) + '.' + forcedType : obj.input.name,
			type: MimeTypes[forcedType] || obj.input.type,
			width: obj.output.width,
			height: obj.output.height
		};

		data.output.image = toDataURL(obj.output.image, data.output.type, jpegCompression);
		data.output.type = getMimeTypeFromDataURI(data.output.image);

		// browser problem:
		// if output is of type png and input was of type jpeg we need to fix extension of filename
		// so instead of testing the above situation we just always fix extension when handling PNGs
		if (data.output.type === 'image/png') {
			data.output.name = getFileNameWithoutExtension(data.input.name) + '.png';
		}
	}

	if (inArray('actions', props)) {
		data.actions = clone(obj.actions);
	}

	return data;
};

var toggleDisplayBySelector = function toggleDisplayBySelector(selector, enabled, root) {
	var node = root.querySelector(selector);
	if (!node) {
		return;
	}
	node.style.display = enabled ? '' : 'none';
};

var nodeListToArray = function nodeListToArray(nl) {
	return Array.prototype.slice.call(nl);
};

var removeElement = function removeElement(el) {
	el.parentNode.removeChild(el);
};

var wrap = function wrap(element) {
	var wrapper = create('div');
	if (element.parentNode) {
		if (element.nextSibling) {
			element.parentNode.insertBefore(wrapper, element.nextSibling);
		} else {
			element.parentNode.appendChild(wrapper);
		}
	}
	wrapper.appendChild(element);
	return wrapper;
};

var polarToCartesian = function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
	var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;

	return {
		x: centerX + radius * Math.cos(angleInRadians),
		y: centerY + radius * Math.sin(angleInRadians)
	};
};

var describeArc = function describeArc(x, y, radius, startAngle, endAngle) {

	var start = polarToCartesian(x, y, radius, endAngle);
	var end = polarToCartesian(x, y, radius, startAngle);

	var arcSweep = endAngle - startAngle <= 180 ? '0' : '1';

	var d = ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' ');

	return d;
};

var percentageArc = function percentageArc(x, y, radius, p) {
	return describeArc(x, y, radius, 0, p * 360);
};
var CropArea = function () {

	var resizers = {
		'n': function n(rect, offset, space, ratio) {

			var t, r, b, l, w, h, p, d;

			// bottom is fixed
			b = rect.y + rect.height;

			// intended top
			t = limit(offset.y, 0, b);

			// if is too small vertically
			if (b - t < space.min.height) {
				t = b - space.min.height;
			}

			// if should scale by ratio, pick width by ratio of new height
			w = ratio ? (b - t) / ratio : rect.width;

			// check if has fallen below min width or height
			if (w < space.min.width) {
				w = space.min.width;
				t = b - w * ratio;
			}

			// add half to left and half to right edge
			p = (w - rect.width) * .5;
			l = rect.x - p;
			r = rect.x + rect.width + p;

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (l < 0 || r > space.width) {

				// smallest distance to edge of space
				d = Math.min(rect.x, space.width - (rect.x + rect.width));

				// new left and right offsets
				l = rect.x - d;
				r = rect.x + rect.width + d;

				// resulting width
				w = r - l;

				// resulting height based on ratio
				h = w * ratio;

				// new top position
				t = b - h;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		's': function s(rect, offset, space, ratio) {

			var t, r, b, l, w, h, p, d;

			// top is fixed
			t = rect.y;

			// intended bottom
			b = limit(offset.y, t, space.height);

			// if is too small vertically
			if (b - t < space.min.height) {
				b = t + space.min.height;
			}

			// if should scale by ratio, pick width by ratio of new height
			w = ratio ? (b - t) / ratio : rect.width;

			// check if has fallen below min width or height
			if (w < space.min.width) {
				w = space.min.width;
				b = t + w * ratio;
			}

			// add half to left and half to right edge
			p = (w - rect.width) * .5;
			l = rect.x - p;
			r = rect.x + rect.width + p;

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (l < 0 || r > space.width) {

				// smallest distance to edge of space
				d = Math.min(rect.x, space.width - (rect.x + rect.width));

				// new left and right offsets
				l = rect.x - d;
				r = rect.x + rect.width + d;

				// resulting width
				w = r - l;

				// resulting height based on ratio
				h = w * ratio;

				// new bottom position
				b = t + h;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		'e': function e(rect, offset, space, ratio) {

			var t, r, b, l, w, h, p, d;

			// left is fixed
			l = rect.x;

			// intended right edge
			r = limit(offset.x, l, space.width);

			// if is too small vertically
			if (r - l < space.min.width) {
				r = l + space.min.width;
			}

			// if should scale by ratio, pick height by ratio of new width
			h = ratio ? (r - l) * ratio : rect.height;

			// check if has fallen below min width or height
			if (h < space.min.height) {
				h = space.min.height;
				r = l + h / ratio;
			}

			// add half to top and bottom
			p = (h - rect.height) * .5;
			t = rect.y - p;
			b = rect.y + rect.height + p;

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (t < 0 || b > space.height) {

				// smallest distance to edge of space
				d = Math.min(rect.y, space.height - (rect.y + rect.height));

				// new top and bottom offsets
				t = rect.y - d;
				b = rect.y + rect.height + d;

				// resulting height
				h = b - t;

				// resulting width based on ratio
				w = h / ratio;

				// new right position
				r = l + w;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		'w': function w(rect, offset, space, ratio) {

			var t, r, b, l, w, h, p, d;

			// right is fixed
			r = rect.x + rect.width;

			// intended left edge
			l = limit(offset.x, 0, r);

			// if is too small vertically
			if (r - l < space.min.width) {
				l = r - space.min.width;
			}

			// if should scale by ratio, pick height by ratio of new width
			h = ratio ? (r - l) * ratio : rect.height;

			// check if has fallen below min width or height
			if (h < space.min.height) {
				h = space.min.height;
				l = r - h / ratio;
			}

			// add half to top and bottom
			p = (h - rect.height) * .5;
			t = rect.y - p;
			b = rect.y + rect.height + p;

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (t < 0 || b > space.height) {

				// smallest distance to edge of space
				d = Math.min(rect.y, space.height - (rect.y + rect.height));

				// new top and bottom offsets
				t = rect.y - d;
				b = rect.y + rect.height + d;

				// resulting height
				h = b - t;

				// resulting width based on ratio
				w = h / ratio;

				// new right position
				l = r - w;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		'ne': function ne(rect, offset, space, ratio) {

			var t, r, b, l, w, h, d;

			// left and bottom are fixed
			l = rect.x;
			b = rect.y + rect.height;

			// intended right edge
			r = limit(offset.x, l, space.width);

			// if is too small vertically
			if (r - l < space.min.width) {
				r = l + space.min.width;
			}

			// if should scale by ratio, pick height by ratio of new width
			h = ratio ? (r - l) * ratio : limit(b - offset.y, space.min.height, b);

			// check if has fallen below min width or height
			if (h < space.min.height) {
				h = space.min.height;
				r = l + h / ratio;
			}

			// add height difference with original height to top
			t = rect.y - (h - rect.height);

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (t < 0 || b > space.height) {

				// smallest distance to edge of space
				d = Math.min(rect.y, space.height - (rect.y + rect.height));

				// new top and bottom offsets
				t = rect.y - d;

				// resulting height
				h = b - t;

				// resulting width based on ratio
				w = h / ratio;

				// new right position
				r = l + w;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		'se': function se(rect, offset, space, ratio) {

			var t, r, b, l, w, h, d;

			// left and top are fixed
			l = rect.x;
			t = rect.y;

			// intended right edge
			r = limit(offset.x, l, space.width);

			// if is too small vertically
			if (r - l < space.min.width) {
				r = l + space.min.width;
			}

			// if should scale by ratio, pick height by ratio of new width
			h = ratio ? (r - l) * ratio : limit(offset.y - rect.y, space.min.height, space.height - t);

			// check if has fallen below min width or height
			if (h < space.min.height) {
				h = space.min.height;
				r = l + h / ratio;
			}

			// add height difference with original height to bottom
			b = rect.y + rect.height + (h - rect.height);

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (t < 0 || b > space.height) {

				// smallest distance to edge of space
				d = Math.min(rect.y, space.height - (rect.y + rect.height));

				// new bottom offset
				b = rect.y + rect.height + d;

				// resulting height
				h = b - t;

				// resulting width based on ratio
				w = h / ratio;

				// new right position
				r = l + w;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		'sw': function sw(rect, offset, space, ratio) {

			var t, r, b, l, w, h, d;

			// right and top are fixed
			r = rect.x + rect.width;
			t = rect.y;

			// intended left edge
			l = limit(offset.x, 0, r);

			// if is too small vertically
			if (r - l < space.min.width) {
				l = r - space.min.width;
			}

			// if should scale by ratio, pick height by ratio of new width
			h = ratio ? (r - l) * ratio : limit(offset.y - rect.y, space.min.height, space.height - t);

			// check if has fallen below min width or height
			if (h < space.min.height) {
				h = space.min.height;
				l = r - h / ratio;
			}

			// add height difference with original height to bottom
			b = rect.y + rect.height + (h - rect.height);

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (t < 0 || b > space.height) {

				// smallest distance to edge of space
				d = Math.min(rect.y, space.height - (rect.y + rect.height));

				// new bottom offset
				b = rect.y + rect.height + d;

				// resulting height
				h = b - t;

				// resulting width based on ratio
				w = h / ratio;

				// new left position
				l = r - w;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		},
		'nw': function nw(rect, offset, space, ratio) {

			var t, r, b, l, w, h, d;

			// right and bottom are fixed
			r = rect.x + rect.width;
			b = rect.y + rect.height;

			// intended left edge
			l = limit(offset.x, 0, r);

			// if is too small vertically
			if (r - l < space.min.width) {
				l = r - space.min.width;
			}

			// if should scale by ratio, pick height by ratio of new width
			h = ratio ? (r - l) * ratio : limit(b - offset.y, space.min.height, b);

			// check if has fallen below min width or height
			if (h < space.min.height) {
				h = space.min.height;
				l = r - h / ratio;
			}

			// add height difference with original height to bottom
			t = rect.y - (h - rect.height);

			// check if any of the edges has moved out of the available space, if so,
			// set max size of rectangle from original position
			if (t < 0 || b > space.height) {

				// smallest distance to edge of space
				d = Math.min(rect.y, space.height - (rect.y + rect.height));

				// new bottom offset
				t = rect.y - d;

				// resulting height
				h = b - t;

				// resulting width based on ratio
				w = h / ratio;

				// new left position
				l = r - w;
			}

			return {
				x: l,
				y: t,
				width: r - l,
				height: b - t
			};
		}

	};

	/**
  * CropArea
  */
	return function () {
		function CropArea() {
			var element = arguments.length <= 0 || arguments[0] === undefined ? document.createElement('div') : arguments[0];

			_classCallCheck(this, CropArea);

			this._element = element;

			this._interaction = null;

			this._minWidth = 0;
			this._minHeight = 0;

			this._ratio = null;

			this._rect = {
				x: 0,
				y: 0,
				width: 0,
				height: 0
			};

			this._space = {
				width: 0,
				height: 0
			};

			this._rectChanged = false;

			this._init();
		}

		_createClass(CropArea, [{
			key: '_init',
			value: function _init() {

				this._element.className = 'slim-crop-area';

				// lines
				var lines = create('div', 'grid');
				this._element.appendChild(lines);

				// corner & edge resize buttons
				for (var handler in resizers) {
					if (!resizers.hasOwnProperty(handler)) {
						continue;
					}
					var _btn = create('button', handler);
					this._element.appendChild(_btn);
				}
				var btn = create('button', 'c');
				this._element.appendChild(btn);

				addEvents(document, Events.DOWN, this);
			}
		}, {
			key: 'reset',
			value: function reset() {

				this._interaction = null;

				this._rect = {
					x: 0,
					y: 0,
					width: 0,
					height: 0
				};

				this._rectChanged = true;

				this._redraw();

				this._element.dispatchEvent(new CustomEvent('change'));
			}
		}, {
			key: 'rescale',
			value: function rescale(scale) {

				// no rescale
				if (scale === 1) {
					return;
				}

				this._interaction = null;

				this._rectChanged = true;

				this._rect.x *= scale;
				this._rect.y *= scale;
				this._rect.width *= scale;
				this._rect.height *= scale;

				this._redraw();

				this._element.dispatchEvent(new CustomEvent('change'));
			}
		}, {
			key: 'limit',
			value: function limit(width, height) {
				this._space = {
					width: width,
					height: height
				};
			}
		}, {
			key: 'resize',
			value: function resize(x, y, width, height) {

				this._interaction = null;

				this._rect = {
					x: limit(x, 0, this._space.width - this._minWidth),
					y: limit(y, 0, this._space.height - this._minHeight),
					width: limit(width, this._minWidth, this._space.width),
					height: limit(height, this._minHeight, this._space.height)
				};

				this._rectChanged = true;

				this._redraw();

				this._element.dispatchEvent(new CustomEvent('change'));
			}
		}, {
			key: 'handleEvent',
			value: function handleEvent(e) {

				switch (e.type) {

					case 'touchstart':
					case 'pointerdown':
					case 'mousedown':
						{
							this._onStartDrag(e);
						}
						break;
					case 'touchmove':
					case 'pointermove':
					case 'mousemove':
						{
							this._onDrag(e);
						}
						break;
					case 'touchend':
					case 'touchcancel':
					case 'pointerup':
					case 'mouseup':
						{
							this._onStopDrag(e);
						}

				}
			}
		}, {
			key: '_onStartDrag',
			value: function _onStartDrag(e) {

				// is not my event?
				if (!this._element.contains(e.target)) {
					return;
				}

				e.preventDefault();

				// listen to drag related events
				addEvents(document, Events.MOVE, this);
				addEvents(document, Events.UP, this);

				// now interacting with control
				this._interaction = {
					type: e.target.className,
					offset: getEventOffsetLocal(e, this._element)
				};

				// now dragging
				this._element.setAttribute('data-dragging', 'true');

				// start the redraw update loop
				this._redraw();
			}
		}, {
			key: '_onDrag',
			value: function _onDrag(e) {

				e.preventDefault();

				// get local offset for this event
				var offset = getEventOffsetLocal(e, this._element.parentNode);
				var type = this._interaction.type;

				// drag
				if (type === 'c') {

					this._rect.x = limit(offset.x - this._interaction.offset.x, 0, this._space.width - this._rect.width);
					this._rect.y = limit(offset.y - this._interaction.offset.y, 0, this._space.height - this._rect.height);
				}
				// resize
				else if (resizers[type]) {

						this._rect = resizers[type](this._rect, offset, {
							x: 0,
							y: 0,
							width: this._space.width,
							height: this._space.height,
							min: {
								width: this._minWidth,
								height: this._minHeight
							}
						}, this._ratio);
					}

				this._rectChanged = true;

				// dispatch
				this._element.dispatchEvent(new CustomEvent('input'));
			}
		}, {
			key: '_onStopDrag',
			value: function _onStopDrag(e) {

				e.preventDefault();

				// stop listening to drag related events
				removeEvents(document, Events.MOVE, this);
				removeEvents(document, Events.UP, this);

				// no longer interacting, so no need to redraw
				this._interaction = null;

				// now dragging
				this._element.setAttribute('data-dragging', 'false');

				// fire change event
				this._element.dispatchEvent(new CustomEvent('change'));
			}
		}, {
			key: '_redraw',
			value: function _redraw() {
				var _this = this;

				if (this._rectChanged) {

					this._element.style.cssText = '\n\t\t\t\t\tleft:' + this._rect.x + 'px;\n\t\t\t\t\ttop:' + this._rect.y + 'px;\n\t\t\t\t\twidth:' + this._rect.width + 'px;\n\t\t\t\t\theight:' + this._rect.height + 'px;\n\t\t\t\t';

					this._rectChanged = false;
				}

				// if no longer interacting with crop area stop here
				if (!this._interaction) {
					return;
				}

				// redraw
				requestAnimationFrame(function () {
					return _this._redraw();
				});
			}
		}, {
			key: 'destroy',
			value: function destroy() {

				this._interaction = false;
				this._rectChanged = false;

				removeEvents(document, Events.DOWN, this);
				removeEvents(document, Events.MOVE, this);
				removeEvents(document, Events.UP, this);

				removeElement(this._element);
			}
		}, {
			key: 'element',
			get: function get() {
				return this._element;
			}
		}, {
			key: 'space',
			get: function get() {
				return this._space;
			}
		}, {
			key: 'area',
			get: function get() {

				var x = this._rect.x / this._space.width;
				var y = this._rect.y / this._space.height;
				var width = this._rect.width / this._space.width;
				var height = this._rect.height / this._space.height;

				return {
					x: x,
					y: y,
					width: width,
					height: height
				};
			}
		}, {
			key: 'dirty',
			get: function get() {
				return this._rect.x !== 0 || this._rect.y !== 0 || this._rect.width !== 0 || this._rect.height !== 0;
			}
		}, {
			key: 'minWidth',
			set: function set(value) {
				this._minWidth = value;
			}
		}, {
			key: 'minHeight',
			set: function set(value) {
				this._minHeight = value;
			}
		}, {
			key: 'ratio',
			set: function set(value) {
				this._ratio = value;
			}
		}]);

		return CropArea;
	}();
}();
var ImageEditor = function () {

	/**
  * ImageEditor
  * @param element
  * @param options
  * @constructor
  */

	var CropAreaEvents = ['input', 'change'];

	var ImageEditor = function () {
		function ImageEditor() {
			var element = arguments.length <= 0 || arguments[0] === undefined ? document.createElement('div') : arguments[0];
			var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];

			_classCallCheck(this, ImageEditor);

			this._element = element;
			this._options = mergeOptions(ImageEditor.options(), options);

			this._ratio = null;
			this._output = null;

			this._input = null;

			this._preview = null;
			this._previewBlurred = null;

			this._blurredPreview = false;

			this._cropper = null;
			this._previewWrapper = null;
			this._currentWindowSize = {};

			this._btnGroup = null;

			this._dirty = false;

			this._wrapperRotation = 0;
			this._wrapperScale = 1.0;

			this._init();
		}

		_createClass(ImageEditor, [{
			key: '_init',
			value: function _init() {
				var _this2 = this;

				this._element.className = 'slim-image-editor';

				// container
				this._container = create('div', 'slim-container');

				// wrapper
				this._wrapper = create('div', 'slim-wrapper');

				// photo crop mark container
				this._stage = create('div', 'slim-stage');
				this._container.appendChild(this._stage);

				// create crop marks
				this._cropper = new CropArea();
				CropAreaEvents.forEach(function (e) {
					_this2._cropper.element.addEventListener(e, _this2);
				});
				this._stage.appendChild(this._cropper.element);

				// canvas ghost
				this._previewWrapper = create('div', 'slim-image-editor-preview slim-crop-preview');
				this._previewBlurred = create('canvas', 'slim-crop-blur');
				this._previewWrapper.appendChild(this._previewBlurred);
				this._wrapper.appendChild(this._previewWrapper);

				this._preview = create('img', 'slim-crop');
				this._previewWrapper.appendChild(this._preview);

				// buttons
				this._btnGroup = create('div', 'slim-editor-btn-group');
				ImageEditor.Buttons.forEach(function (c) {
					var prop = capitalizeFirstLetter(c);
					var label = _this2._options['button' + prop + 'Label'];
					var title = _this2._options['button' + prop + 'Title'];
					var className = _this2._options['button' + prop + 'ClassName'];
					var btn = create('button', 'slim-editor-btn slim-btn-' + c + (className ? ' ' + className : ''));
					btn.innerHTML = label;
					btn.title = title || label;
					btn.type = 'button';
					btn.setAttribute('data-action', c);
					btn.addEventListener('click', _this2);
					_this2._btnGroup.appendChild(btn);
				});

				// utils
				this._utilsGroup = create('div', 'slim-editor-utils-group');

				// create rotation button
				var btn = create('button', 'slim-editor-utils-btn slim-btn-rotate' + (this._options.buttonRotateClassName ? ' ' + this._options.buttonRotateClassName : ''));
				btn.setAttribute('data-action', 'rotate');
				btn.addEventListener('click', this);
				btn.title = this._options.buttonRotateTitle;
				this._utilsGroup.appendChild(btn);

				this._container.appendChild(this._wrapper);

				this._element.appendChild(this._container);
				this._element.appendChild(this._utilsGroup);
				this._element.appendChild(this._btnGroup);
			}
		}, {
			key: 'dirty',
			value: function dirty() {
				this._dirty = true;
			}
		}, {
			key: 'handleEvent',
			value: function handleEvent(e) {

				switch (e.type) {
					case 'click':
						this._onClick(e);
						break;
					case 'change':
						this._onGridChange(e);
						break;
					case 'input':
						this._onGridInput(e);
						break;
					case 'keydown':
						this._onKeyDown(e);
						break;
					case 'resize':
						this._onResize(e);
						break;
				}
			}
		}, {
			key: '_onKeyDown',
			value: function _onKeyDown(e) {

				switch (e.keyCode) {
					case Key.RETURN:
						{
							this._confirm();
							break;
						}
					case Key.ESC:
						{
							this._cancel();
							break;
						}
				}
			}
		}, {
			key: '_onClick',
			value: function _onClick(e) {

				if (e.target.classList.contains('slim-btn-cancel')) {
					this._cancel();
				}

				if (e.target.classList.contains('slim-btn-confirm')) {
					this._confirm();
				}

				if (e.target.classList.contains('slim-btn-rotate')) {
					this._rotate();
				}
			}
		}, {
			key: '_onResize',
			value: function _onResize() {

				// remember window size
				this._currentWindowSize = {
					width: window.innerWidth,
					height: window.innerHeight
				};

				// redraw the image editor based on new dimensions
				this._redraw();

				this._redrawCropper(this._cropper.area);

				this._updateWrapperScale();

				// apply rotation and scale
				this._redrawWrapper();
			}
		}, {
			key: '_redrawWrapper',
			value: function _redrawWrapper() {
				var matrix = snabbt.createMatrix();
				matrix.scale(this._wrapperScale, this._wrapperScale);
				matrix.rotateZ(this._wrapperRotation * (Math.PI / 180));
				snabbt.setElementTransform(this._previewWrapper, matrix);
			}
		}, {
			key: '_onGridInput',
			value: function _onGridInput() {

				this._redrawCropMask();
			}
		}, {
			key: '_onGridChange',
			value: function _onGridChange() {

				this._redrawCropMask();
			}
		}, {
			key: '_updateWrapperRotation',
			value: function _updateWrapperRotation() {

				if (this._options.minSize.width > this._input.height || this._options.minSize.height > this._input.width) {
					this._wrapperRotation += 180;
				} else {
					this._wrapperRotation += 90;
				}
			}
		}, {
			key: '_updateWrapperScale',
			value: function _updateWrapperScale() {

				// test if is tilted
				var isTilted = this._wrapperRotation % 180 !== 0;

				// if tilted determine correct scale factor
				if (isTilted) {

					// maximum size
					var maxWidth = this._container.offsetWidth;
					var maxHeight = this._container.offsetHeight;

					// get wrapper size
					var wrapperWidth = this._wrapper.offsetHeight;
					var wrapperHeight = this._wrapper.offsetWidth;

					var scalar = maxWidth / wrapperWidth;
					if (scalar * wrapperHeight > maxHeight) {
						scalar = maxHeight / wrapperHeight;
					}

					this._wrapperScale = scalar;
				} else {
					this._wrapperScale = 1.0;
				}
			}

			/**
    * Action handlers
    */

		}, {
			key: '_cancel',
			value: function _cancel() {

				this._element.dispatchEvent(new CustomEvent('cancel'));
			}
		}, {
			key: '_confirm',
			value: function _confirm() {

				var isTilted = this._wrapperRotation % 180 !== 0;
				var area = this._cropper.area;

				var result = scaleRect(area, isTilted ? this._input.height : this._input.width, isTilted ? this._input.width : this._input.height);

				this._element.dispatchEvent(new CustomEvent('confirm', {
					detail: {
						rotation: this._wrapperRotation % 360,
						crop: result
					}
				}));
			}
		}, {
			key: '_rotate',
			value: function _rotate() {
				var _this3 = this;

				// rotate!
				this._updateWrapperRotation();

				// wrapper might also need to be scaled
				this._updateWrapperScale();

				// hide the mask
				this._clearCropMask();

				// hide the cropper
				this._hideCropper();

				// rotation effect
				snabbt(this._previewWrapper, {
					rotation: [0, 0, this._wrapperRotation * (Math.PI / 180)],
					scale: [this._wrapperScale, this._wrapperScale],
					easing: 'spring',
					springConstant: .8,
					springDeceleration: .65,
					complete: function complete() {

						// redraws auto cropper
						_this3._redrawCropper();

						// shows the cropper
						_this3._showCropper();
					}
				});
			}
		}, {
			key: '_showCropper',
			value: function _showCropper() {

				snabbt(this._stage, {
					easing: 'ease',
					duration: 250,
					fromOpacity: 0,
					opacity: 1.0
				});
			}
		}, {
			key: '_hideCropper',
			value: function _hideCropper() {

				snabbt(this._stage, {
					duration: 0,
					fromOpacity: 0,
					opacity: 0
				});
			}
		}, {
			key: '_clearCropMask',
			value: function _clearCropMask() {
				this._preview.style.clip = '';
			}
		}, {
			key: '_redrawCropMask',
			value: function _redrawCropMask() {
				var _this4 = this;

				// get rotation
				var rotation = this._wrapperRotation % 360;

				// get image size
				var canvas = {
					width: this._wrapper.offsetWidth,
					height: this._wrapper.offsetHeight
				};

				// get default mask cropper area
				var mask = this._cropper.area;
				var px = mask.x;
				var py = mask.y;
				var pw = mask.width;
				var ph = mask.height;

				// rotate cropper area
				if (rotation === 90) {
					mask.x = 1 - py - ph;
					mask.y = px;
					mask.width = ph;
					mask.height = pw;
				} else if (rotation === 180) {
					mask.x = 1 - (px + pw);
					mask.y = 1 - (py + ph);
					mask.width = pw;
					mask.height = ph;
				} else if (rotation === 270) {
					mask.x = py;
					mask.y = 1 - px - pw;
					mask.width = ph;
					mask.height = pw;
				}

				// scale rect to fit canvas
				mask = scaleRect(mask, canvas.width, canvas.height);

				// update on next frame (so it's in sync with crop area redraw)
				requestAnimationFrame(function () {

					// show only crop area of original image
					_this4._preview.style.clip = 'rect(\n\t\t\t\t\t' + mask.y + 'px,\n\t\t\t\t\t' + (mask.x + mask.width) + 'px,\n\t\t\t\t\t' + (mask.y + mask.height) + 'px,\n\t\t\t\t\t' + mask.x + 'px)\n\t\t\t\t';
				});
			}
		}, {
			key: 'open',
			value: function open(image, ratio, crop, rotation, complete) {
				var _this5 = this;

				// test if is same image
				if (this._input && !this._dirty) {
					complete();
					return;
				}

				// reset dirty value
				this._dirty = false;

				// reset rotation
				this._wrapperRotation = rotation || 0;

				// we'll always have to blur the preview of a newly opened image
				this._blurredPreview = false;

				// set ratio
				this._ratio = ratio;

				// hide editor
				this._element.style.opacity = '0';

				// set as new input image
				this._input = image;

				// calculate crop
				var tilted = rotation % 180 !== 0;
				var relativeCrop = divideRect(crop, tilted ? image.height : image.width, tilted ? image.width : image.height);

				// preview has now loaded
				this._preview.onload = function () {

					// reset onload listener
					_this5._preview.onload = null;

					// setup cropper
					_this5._cropper.ratio = _this5.ratio;

					// redraw view (for first time)
					_this5._redraw();

					// redraw cropper
					_this5._redrawCropper(relativeCrop);

					// done
					complete();

					// fade in
					_this5._element.style.opacity = '';
				};

				// load lower resolution preview image
				this._preview.src = cloneCanvasScaled(this._input, Math.min(this._container.offsetWidth / this._input.width, this._container.offsetHeight / this._input.height)).toDataURL();
			}
		}, {
			key: '_redrawCropper',
			value: function _redrawCropper(rect) {

				var isTilted = this._wrapperRotation % 180 !== 0;

				// image ratio
				var imageRatio = isTilted ? this._input.height / this._input.width : this._input.width / this._input.height;

				// get dimensions of image wrapper
				var width = this._wrapper.offsetWidth;
				var height = this._wrapper.offsetHeight;

				// get space
				var maxWidth = this._container.offsetWidth;
				var maxHeight = this._container.offsetHeight;

				// rescale wrapper
				this._updateWrapperScale();

				// position cropper container to fit wrapper
				var sw = this._wrapperScale * (isTilted ? height : width);
				var sh = this._wrapperScale * (isTilted ? width : height);
				var sx = isTilted ? (maxWidth - sw) * .5 : this._wrapper.offsetLeft;
				var sy = isTilted ? (maxHeight - sh) * .5 : this._wrapper.offsetTop;

				this._stage.style.cssText = '\n\t\t\t\tleft:' + sx + 'px;\n\t\t\t\ttop:' + sy + 'px;\n\t\t\t\twidth:' + sw + 'px;\n\t\t\t\theight:' + sh + 'px;\n\t\t\t';

				// set new size limit for crops
				// use image ratio so we have exact amount of pixels
				this._cropper.limit(sw, sw / imageRatio);

				// set min and max height of cropper
				this._cropper.minWidth = this._wrapperScale * this._options.minSize.width * this.scalar;
				this._cropper.minHeight = this._wrapperScale * this._options.minSize.height * this.scalar;

				// set crop rect
				var crop = null;
				if (rect) {
					crop = {
						x: rect.x * sw,
						y: rect.y * sh,
						width: rect.width * sw,
						height: rect.height * sh
					};
				} else {
					crop = getAutoCropRect(sw, sh, this._ratio || sh / sw);
				}

				this._cropper.resize(crop.x, crop.y, crop.width, crop.height);

				// resize of cropper will automatically trigger a change event which will update the blur mask
			}
		}, {
			key: '_redraw',
			value: function _redraw() {

				// image ratio
				var ratio = this._input.height / this._input.width;

				// determine rounded size
				var maxWidth = this._container.clientWidth;
				var maxHeight = this._container.clientHeight;

				var width = maxWidth;
				var height = width * ratio;

				if (height > maxHeight) {
					height = maxHeight;
					width = height / ratio;
				}

				width = Math.round(width);
				height = Math.round(height);

				// rescale and recenter wrapper
				var x = (maxWidth - width) / 2;
				var y = (maxHeight - height) / 2;

				this._wrapper.style.cssText = '\n\t\t\t\tleft:' + x + 'px;\n\t\t\t\ttop:' + y + 'px;\n\t\t\t\twidth:' + width + 'px;\n\t\t\t\theight:' + height + 'px;\n\t\t\t';

				// set image size based on container dimensions
				this._previewBlurred.style.cssText = '\n\t\t\t\twidth:' + width + 'px;\n\t\t\t\theight:' + height + 'px;\n\t\t\t';

				this._preview.width = width;
				this._preview.height = height;

				// scale and blur the blurry preview
				if (!this._blurredPreview) {

					this._previewBlurred.width = 300;
					this._previewBlurred.height = this._previewBlurred.width * ratio;

					copyCanvas(this._input, this._previewBlurred);

					blurCanvas(this._previewBlurred, 3);

					this._blurredPreview = true;
				}
			}
		}, {
			key: 'show',
			value: function show() {
				var callback = arguments.length <= 0 || arguments[0] === undefined ? function () {} : arguments[0];


				// test if window size has changed since previous showing
				if (this._currentWindowSize.width !== window.innerWidth || this._currentWindowSize.height !== window.innerHeight) {

					// if so, reposition elements
					this._redraw();

					// redraw cropper position
					this._redrawCropper(this._cropper.area);
				}

				// listen to keydown so we can close or confirm with RETURN / ESC
				document.addEventListener('keydown', this);

				// when window is scaled we want to resize the editor
				window.addEventListener('resize', this);

				// fade in preview
				var rotation = this._wrapperRotation * (Math.PI / 180);
				snabbt(this._previewWrapper, {
					fromRotation: [0, 0, rotation],
					rotation: [0, 0, rotation],
					fromPosition: [0, 0, 0],
					position: [0, 0, 0],
					fromOpacity: 0,
					opacity: .9999, // fixes IE render bug
					fromScale: [this._wrapperScale - .02, this._wrapperScale - .02],
					scale: [this._wrapperScale, this._wrapperScale],
					easing: 'spring',
					springConstant: .3,
					springDeceleration: .85,
					delay: 450,
					complete: function complete() {
						//resetTransforms(this);
					}
				});

				if (this._cropper.dirty) {

					// now show cropper
					snabbt(this._stage, {
						fromPosition: [0, 0, 0],
						position: [0, 0, 0],
						fromOpacity: 0,
						opacity: 1,
						duration: 250,
						delay: 550,
						complete: function complete() {
							resetTransforms(this);
							callback();
						}
					});
				} else {

					// now show cropper
					snabbt(this._stage, {
						fromPosition: [0, 0, 0],
						position: [0, 0, 0],
						fromOpacity: 0,
						opacity: 1,
						duration: 250,
						delay: 1000,
						complete: function complete() {
							resetTransforms(this);
						}
					});
				}

				// now show buttons
				snabbt(this._btnGroup.childNodes, {
					fromScale: [.9, .9],
					scale: [1, 1],
					fromOpacity: 0,
					opacity: 1,
					delay: function delay(i) {
						return 1000 + i * 100;
					},
					easing: 'spring',
					springConstant: .3,
					springDeceleration: .85,
					complete: function complete() {
						resetTransforms(this);
					}
				});

				snabbt(this._utilsGroup.childNodes, {
					fromScale: [.9, .9],
					scale: [1, 1],
					fromOpacity: 0,
					opacity: 1,
					easing: 'spring',
					springConstant: .3,
					springDeceleration: .85,
					delay: 1250,
					complete: function complete() {
						resetTransforms(this);
					}
				});
			}
		}, {
			key: 'hide',
			value: function hide() {
				var callback = arguments.length <= 0 || arguments[0] === undefined ? function () {} : arguments[0];


				// no more need to listen to keydown
				document.removeEventListener('keydown', this);

				// no more need to resize editor when window is scaled
				window.removeEventListener('resize', this);

				// fade out buttons
				snabbt(this._utilsGroup.childNodes, {
					fromOpacity: 1,
					opacity: 0,
					duration: 250
				});

				snabbt(this._btnGroup.childNodes, {
					fromOpacity: 1,
					opacity: 0,
					delay: 200,
					duration: 350
				});

				snabbt([this._stage, this._previewWrapper], {
					fromPosition: [0, 0, 0],
					position: [0, -250, 0],
					fromOpacity: 1,
					opacity: 0,
					easing: 'spring',
					springConstant: .3,
					springDeceleration: .75,
					delay: 250,
					allDone: function allDone() {

						callback();
					}
				});
			}
		}, {
			key: 'destroy',
			value: function destroy() {
				var _this6 = this;

				nodeListToArray(this._btnGroup.children).forEach(function (btn) {
					btn.removeEventListener('click', _this6);
				});

				CropAreaEvents.forEach(function (e) {
					_this6._cropper.element.removeEventListener(e, _this6);
				});

				this._cropper.destroy();

				removeElement(this._element);
			}
		}, {
			key: 'showRotateButton',
			set: function set(enabled) {
				if (enabled) {
					this._element.classList.remove('slim-rotation-disabled');
				} else {
					this._element.classList.add('slim-rotation-disabled');
				}
			}
		}, {
			key: 'element',
			get: function get() {
				return this._element;
			}
		}, {
			key: 'ratio',
			get: function get() {
				if (this._ratio === 'input') {
					return this._input.height / this._input.width;
				}
				return this._ratio;
			}
		}, {
			key: 'offset',
			get: function get() {
				return this._element.getBoundingClientRect();
			}
		}, {
			key: 'original',
			get: function get() {
				return this._input;
			}
		}, {
			key: 'scalar',
			get: function get() {
				return this._preview.width / this._input.width;
			}
		}], [{
			key: 'options',
			value: function options() {

				return {
					buttonCancelClassName: null,
					buttonConfirmClassName: null,
					buttonCancelLabel: 'Cancel',
					buttonConfirmLabel: 'Confirm',
					buttonCancelTitle: null,
					buttonConfirmTitle: null,

					buttonRotateTitle: 'Rotate',
					buttonRotateClassName: null,

					minSize: {
						width: 0,
						height: 0
					}
				};
			}
		}]);

		return ImageEditor;
	}();

	ImageEditor.Buttons = ['cancel', 'confirm'];

	return ImageEditor;
}(CropArea);
var FileHopper = function () {

	/**
  * FileHopper
  * @param element
  * @param options
  * @constructor
  */
	var DragDropEvents = ['dragover', 'dragleave', 'drop'];

	return function () {
		function FileHopper() {
			var element = arguments.length <= 0 || arguments[0] === undefined ? document.createElement('div') : arguments[0];

			_classCallCheck(this, FileHopper);

			this._element = element;
			this._accept = [];

			this._dragPath = null;

			this._init();
		}

		// public properties


		_createClass(FileHopper, [{
			key: 'areValidDataTransferItems',
			value: function areValidDataTransferItems(dataTransferItems) {

				if (this._accept.length && dataTransferItems) {
					return this._accept.indexOf(getDataTransferItemType(dataTransferItems[0])) !== -1;
				}
				return true;
			}

			// public methods

		}, {
			key: 'reset',
			value: function reset() {
				this._element.files = null;
			}

			// private

		}, {
			key: '_init',
			value: function _init() {
				var _this7 = this;

				this._element.className = 'slim-file-hopper';

				DragDropEvents.forEach(function (e) {
					_this7._element.addEventListener(e, _this7);
				});
			}

			// the input has changed

		}, {
			key: 'handleEvent',
			value: function handleEvent(e) {

				switch (e.type) {
					case 'dragover':
						this._onDragOver(e);
						break;
					case 'dragleave':
						this._onDragLeave(e);
						break;
					case 'drop':
						this._onDrop(e);
						break;
				}
			}
		}, {
			key: '_onDrop',
			value: function _onDrop(e) {

				// prevents browser from opening image
				e.preventDefault();

				// test type in older browsers
				if (!this.areValidDataTransferItems(e.dataTransfer.files)) {

					// invalid files, stop here
					this._element.dispatchEvent(new CustomEvent('file-invalid-drop'));

					// no longer hovering
					this._dragPath = null;
					return;
				}

				// store dropped files on element files property, so can be accessed by same function as file input handler
				this._element.files = e.dataTransfer.files;

				// file has been dropped
				this._element.dispatchEvent(new CustomEvent('file-drop', {
					detail: getOffsetByEvent(e)
				}));

				// file list has changed, let's notify others
				this._element.dispatchEvent(new CustomEvent('change'));

				// no longer hovering
				this._dragPath = null;
			}
		}, {
			key: '_onDragOver',
			value: function _onDragOver(e) {

				// prevents browser from opening image
				e.preventDefault();

				e.dataTransfer.dropEffect = 'copy';

				if (!this.areValidDataTransferItems(e.dataTransfer.items)) {

					// indicate drop mode to user
					e.dataTransfer.dropEffect = 'none';

					// invalid files, stop here
					this._element.dispatchEvent(new CustomEvent('file-invalid'));
					return;
				}

				// now hovering file over the area
				if (!this._dragPath) {
					this._dragPath = [];
				}

				// push location
				this._dragPath.push(getOffsetByEvent(e));

				// file is hovering over element
				this._element.dispatchEvent(new CustomEvent('file-over', {
					detail: {
						x: last(this._dragPath).x,
						y: last(this._dragPath).y
					}
				}));
			}
		}, {
			key: '_onDragLeave',
			value: function _onDragLeave(e) {

				// user has dragged file out of element area
				this._element.dispatchEvent(new CustomEvent('file-out', {
					detail: getOffsetByEvent(e)
				}));

				// now longer hovering file
				this._dragPath = null;
			}
		}, {
			key: 'destroy',
			value: function destroy() {
				var _this8 = this;

				DragDropEvents.forEach(function (e) {
					_this8._element.removeEventListener(e, _this8);
				});

				removeElement(this._element);

				this._element = null;
				this._dragPath = null;
				this._accept = null;
			}
		}, {
			key: 'element',
			get: function get() {
				return this._element;
			}
		}, {
			key: 'dragPath',
			get: function get() {
				return this._dragPath;
			}
		}, {
			key: 'enabled',
			get: function get() {
				return this._element.style.display === '';
			},
			set: function set(value) {
				this._element.style.display = value ? '' : 'none';
			}
		}, {
			key: 'accept',
			set: function set(mimetypes) {
				this._accept = mimetypes;
			},
			get: function get() {
				return this._accept;
			}
		}]);

		return FileHopper;
	}();
}();
var Popover = function () {

	/**
  * Popover
  */
	return function () {
		function Popover() {
			_classCallCheck(this, Popover);

			this._element = null;
			this._inner = null;

			this._init();
		}

		_createClass(Popover, [{
			key: '_init',
			value: function _init() {

				this._element = create('div', 'slim-popover');
				this._element.setAttribute('data-state', 'off');
				document.body.appendChild(this._element);
			}
		}, {
			key: 'show',
			value: function show() {
				var _this9 = this;

				var callback = arguments.length <= 0 || arguments[0] === undefined ? function () {} : arguments[0];


				// turn on
				this._element.setAttribute('data-state', 'on');

				// show and animate in
				snabbt(this._element, {
					fromOpacity: 0,
					opacity: 1,
					duration: 350,
					complete: function complete() {

						// clean up transforms
						resetTransforms(_this9._element);

						// ready!
						callback();
					}
				});
			}
		}, {
			key: 'hide',
			value: function hide() {
				var _this10 = this;

				var callback = arguments.length <= 0 || arguments[0] === undefined ? function () {} : arguments[0];


				// animate out and hide
				snabbt(this._element, {
					fromOpacity: 1,
					opacity: 0,
					duration: 500,
					complete: function complete() {

						// clean up transforms
						resetTransforms(_this10._element);

						// hide the editor
						_this10._element.setAttribute('data-state', 'off');

						// ready!
						callback();
					}
				});
			}
		}, {
			key: 'destroy',
			value: function destroy() {
				if (!this._element.parentNode) {
					return;
				}
				this._element.parentNode.removeChild(this._element);
				this._element = null;
				this._inner = null;
			}
		}, {
			key: 'inner',
			set: function set(element) {

				this._inner = element;
				if (this._element.firstChild) {
					this._element.removeChild(this._element.firstChild);
				}
				this._element.appendChild(this._inner);
			}
		}]);

		return Popover;
	}();
}();
var intSplit = function intSplit(v, c) {
	return v.split(c).map(function (v) {
		return parseInt(v, 10);
	});
};

var isWrapper = function isWrapper(element) {
	return element.nodeName === 'DIV';
};

var CropType = {
	AUTO: 'auto',
	INITIAL: 'initial',
	MANUAL: 'manual'
};

var Rect = ['x', 'y', 'width', 'height'];

var HopperEvents = ['file-invalid-drop', 'file-invalid', 'file-drop', 'file-over', 'file-out', 'click'];

var ImageEditorEvents = ['cancel', 'confirm'];

var SlimButtons = ['remove', 'edit', 'download', 'upload'];

var SlimPopover = null;
var SlimCount = 0;

var SlimLoaderHTML = '\n<div class="slim-loader">\n\t<svg>\n\t\t<path class="slim-loader-background" fill="none" stroke-width="3" />\n\t\t<path class="slim-loader-foreground" fill="none" stroke-width="3" />\n\t</svg>\n</div>\n';

var SlimUploadStatusHTML = '\n<div class="slim-upload-status"></div>\n';

var Slim = function () {
	function Slim(element) {
		var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];

		_classCallCheck(this, Slim);

		// create popover element if not already created
		if (!SlimPopover) {
			SlimPopover = new Popover();
		}

		// we create a new counter, we use this counter to determine if we need to clean up the popover
		this._uid = SlimCount++;

		// the options to use
		this._options = mergeOptions(Slim.options(), options);

		// test options correctness
		if (this._options.forceSize) {
			this._options.ratio = this._options.forceSize.width + ':' + this._options.forceSize.height;
			this._options.size = clone(this._options.forceSize);
		}

		// reference to original element so we can restore original situation
		this._originalElement = element;
		this._originalElementInner = element.innerHTML;
		this._originalElementAttributes = getElementAttributes(element);

		// should be file input, image or slim wrapper, if not wrapper, wrap
		if (!isWrapper(element)) {

			this._element = wrap(element);
			this._element.className = element.className;
			element.className = '';

			// ratio is used for CSS so let's set default attribute
			this._element.setAttribute('data-ratio', this._options.ratio);
		} else {
			this._element = element;
		}
		this._element.classList.add('slim');
		this._element.setAttribute('data-state', 'init');

		// internal timer array for async processes
		this._timers = [];

		// the source element (can either be an input or an img)
		this._input = null;

		// the source element unique name if is input type file
		this._inputReference = null;

		// the output element (hidden input that contains the resulting json object)
		this._output = null;

		// current image ratio
		this._ratio = null;

		// was required field
		this._isRequired = false;

		// components
		this._imageHopper = null;
		this._imageEditor = null;

		// progress path
		this._progressEnabled = true;

		// resulting data
		this._data = {};
		this._resetData();

		// editor state
		this._state = [];

		// the circle below the mouse hover point
		this._drip = null;

		// had initial image
		this._hasInitialImage = false;

		// initial crop
		this._initialCrop = this._options.crop;

		// set to true when destroy method is called, used to halt any timeouts or async processes
		this._isBeingDestroyed = false;

		// let's go!
		if (Slim.supported) {
			this._init();
		} else {
			this._fallback();
		}
	}

	_createClass(Slim, [{
		key: 'isAttachedTo',


		// methods
		// Test if this Slim object has been bound to the given element
		value: function isAttachedTo(element) {
			return this._element === element || this._originalElement === element;
		}
	}, {
		key: 'isDetached',
		value: function isDetached() {
			return this._element.parentNode === null;
		}
	}, {
		key: 'load',
		value: function load(src) {
			var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
			var callback = arguments[2];


			if (typeof options == 'function') {
				callback = options;
			} else {

				// store in options
				this._options.crop = options.crop;

				// set initial crop
				this._initialCrop = this._options.crop;
			}

			this._load(src, callback);
		}
	}, {
		key: 'upload',
		value: function upload(callback) {
			this._doUpload(callback);
		}
	}, {
		key: 'download',
		value: function download() {
			this._doDownload();
		}
	}, {
		key: 'remove',
		value: function remove() {
			return this._doRemove();
		}
	}, {
		key: 'destroy',
		value: function destroy() {
			this._doDestroy();
		}
	}, {
		key: 'edit',
		value: function edit() {
			this._doEdit();
		}
	}, {
		key: 'crop',
		value: function crop(rect, callback) {
			this._crop(rect.x, rect.y, rect.width, rect.height, callback);
		}

		/**
   * State Related
   */

	}, {
		key: '_canInstantEdit',
		value: function _canInstantEdit() {
			return this._options.instantEdit && !this._isInitialising;
		}
	}, {
		key: '_getFileInput',
		value: function _getFileInput() {
			return this._element.querySelector('input[type=file]');
		}
	}, {
		key: '_getInitialImage',
		value: function _getInitialImage() {
			return this._element.querySelector('img');
		}
	}, {
		key: '_getInputElement',
		value: function _getInputElement() {
			return this._getFileInput() || this._getInitialImage();
		}
	}, {
		key: '_getRatioSpacerElement',
		value: function _getRatioSpacerElement() {
			return this._element.children[0];
		}
	}, {
		key: '_isImageOnly',
		value: function _isImageOnly() {
			return this._input.nodeName !== 'INPUT';
		}
	}, {
		key: '_isFixedRatio',
		value: function _isFixedRatio() {
			return this._options.ratio.indexOf(':') !== -1;
		}
	}, {
		key: '_isAutoCrop',
		value: function _isAutoCrop() {
			return this._data.actions.crop.type === CropType.AUTO;
		}
	}, {
		key: '_toggleButton',
		value: function _toggleButton(action, state) {
			toggleDisplayBySelector('.slim-btn[data-action="' + action + '"]', state, this._element);
		}
	}, {
		key: '_clearState',
		value: function _clearState() {
			this._state = [];
			this._updateState();
		}
	}, {
		key: '_removeState',
		value: function _removeState(state) {
			this._state = this._state.filter(function (item) {
				return item !== state;
			});
			this._updateState();
		}
	}, {
		key: '_addState',
		value: function _addState(state) {
			if (inArray(state, this._state)) {
				return;
			}
			this._state.push(state);
			this._updateState();
		}
	}, {
		key: '_updateState',
		value: function _updateState() {
			if (!this._element) {
				return;
			}
			this._element.setAttribute('data-state', this._state.join(','));
		}
	}, {
		key: '_resetData',
		value: function _resetData() {

			// resets data object
			this._data = {
				server: null,
				meta: clone(this._options.meta),
				input: {
					field: this._inputReference,
					name: null,
					type: null,
					width: 0,
					height: 0,
					file: null
				},
				output: {
					image: null,
					width: 0,
					height: 0
				},
				actions: {
					rotation: null,
					crop: null,
					size: null
				}
			};

			// resets hidden input clone (has not yet been created when reset call in constructor, hence the check)
			if (this._output) {
				this._output.value = '';
			}

			// should reset file input
			resetFileInput(this._getFileInput());
		}

		/**
   * Initialisation
   */

	}, {
		key: '_init',
		value: function _init() {
			var _this11 = this;

			// busy initialising
			this._isInitialising = true;

			// cropper root is not file input
			this._addState('empty');

			// define input reference field name
			if (inArray('input', this._options.post)) {
				this._inputReference = 'slim_input_' + this._uid;
			}

			// get input element
			this._input = this._getInputElement();

			// if no input supplied we'll automatically create one
			if (!this._input) {
				this._input = create('input');
				this._input.type = 'file';
				this._element.appendChild(this._input);
			}

			// get required state of input element
			this._isRequired = this._input.required === true;

			// set or create output field
			this._output = this._element.querySelector('input[type=hidden]');

			// if no output element defined we'll create one automagically
			if (!this._output) {
				this._output = create('input');
				this._output.type = 'hidden';
				this._output.name = this._input.name || this._options.defaultInputName;
				this._element.appendChild(this._output);
			}

			// prevent the original file input field from posting (value will be duplicated to output field)
			this._input.removeAttribute('name');

			// create drop area
			var area = create('div', 'slim-area');

			// test if contains initial image
			var initialImage = this._getInitialImage();
			var initialImageSrc = (initialImage || {}).src;
			if (initialImageSrc) {
				this._hasInitialImage = true;
			} else {
				this._initialCrop = null;
			}

			var resultHTML = '\n\t\t<div class="slim-result">\n\t\t\t<img class="in" style="opacity:0" ' + (initialImageSrc ? 'src="' + initialImageSrc + '"' : '') + '><img><img style="opacity:0">\n\t\t</div>';

			// add drop overlay
			if (this._isImageOnly()) {
				area.innerHTML = '\n\t\t\t\t' + SlimLoaderHTML + '\n\t\t\t\t' + SlimUploadStatusHTML + '\n\t\t\t\t' + resultHTML + '\n\t\t\t';
			} else {

				// if should post input data
				if (inArray('input', this._options.post)) {
					this._data.input.field = this._inputReference;

					// if is sync post
					// and should post input data
					if (!this._options.service) {
						this._input.name = this._inputReference;
					}
				}

				// set common image mime type to the accept attribute
				var mimetypes = void 0;
				if (!this._input.hasAttribute('accept') || this._input.getAttribute('accept') === 'image/*') {
					mimetypes = getCommonMimeTypes();
					this._input.setAttribute('accept', mimetypes.join(','));
				} else {
					mimetypes = this._input.accept.split(',').map(function (mimetype) {
						return mimetype.trim();
					}).filter(function (mimetype) {
						return mimetype.length > 0;
					});
				}

				// setup hopper
				this._imageHopper = new FileHopper();
				this._imageHopper.accept = mimetypes;
				this._element.appendChild(this._imageHopper.element);
				HopperEvents.forEach(function (e) {
					_this11._imageHopper.element.addEventListener(e, _this11);
				});

				// render area
				area.innerHTML = '\n\t\t\t\t' + SlimLoaderHTML + '\n\t\t\t\t' + SlimUploadStatusHTML + '\n\t\t\t\t<div class="slim-drip"><span><span></span></span></div>\n\t\t\t\t<div class="slim-status"><div class="slim-label">' + (this._options.label || '') + '</div></div>\n\t\t\t\t' + resultHTML + '\n\t\t\t';

				// start listening for events so we can load image on file input change
				this._input.addEventListener('change', this);
			}

			// add area node
			this._element.appendChild(area);

			// add button group
			this._btnGroup = create('div', 'slim-btn-group');
			this._btnGroup.style.display = 'none';
			this._element.appendChild(this._btnGroup);
			SlimButtons.filter(function (c) {
				return _this11._isButtonAllowed(c);
			}).forEach(function (c) {
				var prop = capitalizeFirstLetter(c);
				var label = _this11._options['button' + prop + 'Label'];
				var title = _this11._options['button' + prop + 'Title'] || label;
				var className = _this11._options['button' + prop + 'ClassName'];
				var btn = create('button', 'slim-btn slim-btn-' + c + (className ? ' ' + className : ''));
				btn.innerHTML = label;
				btn.title = title;
				btn.type = 'button';
				btn.addEventListener('click', _this11);
				btn.setAttribute('data-action', c);
				_this11._btnGroup.appendChild(btn);
			});

			// add ratio padding
			if (this._isFixedRatio()) {
				var parts = intSplit(this._options.ratio, ':');
				this._ratio = parts[1] / parts[0];
				this._scaleDropArea(this._ratio);
			}

			// setup loader
			this._updateProgress(.5);

			// preload source
			if (initialImageSrc) {

				this._load(initialImageSrc, function () {
					_this11._onInit();
				});
			} else {
				this._onInit();
			}
		}
	}, {
		key: '_onInit',
		value: function _onInit() {
			var _this12 = this;

			// we're done initialising
			this._isInitialising = false;

			// done initialising now, else is only called after image load
			var done = function done() {
				// we call this async so the constructor of Slim has returned before the onInit is called, allowing clean immidiate destroy
				var timer = setTimeout(function () {
					_this12._options.didInit.apply(_this12, [_this12.data]);
				}, 0);
				_this12._timers.push(timer);
			};

			// save initial image
			if (this._options.saveInitialImage) {
				this._save(function () {
					done();
				}, false);
			} else {
				done();
			}
		}
	}, {
		key: '_updateProgress',
		value: function _updateProgress(progress) {

			if (!this._element) {
				return;
			}

			if (!this._progressEnabled) {
				return;
			}

			var loader = this._element.querySelector('.slim-loader');
			if (!loader) {
				return;
			}
			var rect = loader.getBoundingClientRect();
			var paths = loader.querySelectorAll('path');
			var ringWidth = parseInt(paths[0].getAttribute('stroke-width'), 10);

			paths[0].setAttribute('d', percentageArc(rect.width * .5, rect.height * .5, rect.width * .5 - ringWidth, .9999));

			paths[1].setAttribute('d', percentageArc(rect.width * .5, rect.height * .5, rect.width * .5 - ringWidth, progress));
		}
	}, {
		key: '_startProgress',
		value: function _startProgress() {
			var _this13 = this;

			if (!this._element) {
				return;
			}

			this._progressEnabled = false;

			var loader = this._element.querySelector('.slim-loader');
			if (!loader) {
				return;
			}
			var ring = loader.children[0];

			this._stopProgressLoop(function () {

				loader.removeAttribute('style');
				ring.removeAttribute('style');

				_this13._progressEnabled = true;

				_this13._updateProgress(0);

				_this13._progressEnabled = false;

				snabbt(ring, {

					fromOpacity: 0,
					opacity: 1,
					duration: 250,
					complete: function complete() {
						_this13._progressEnabled = true;
					}
				});
			});
		}
	}, {
		key: '_stopProgress',
		value: function _stopProgress() {
			var _this14 = this;

			if (!this._element) {
				return;
			}

			var loader = this._element.querySelector('.slim-loader');
			if (!loader) {
				return;
			}
			var ring = loader.children[0];

			this._updateProgress(1);

			snabbt(ring, {
				fromOpacity: 1,
				opacity: 0,
				duration: 250,
				complete: function complete() {

					loader.removeAttribute('style');
					ring.removeAttribute('style');

					_this14._updateProgress(.5);

					_this14._progressEnabled = false;
				}
			});
		}
	}, {
		key: '_startProgressLoop',
		value: function _startProgressLoop() {

			if (!this._element) {
				return;
			}

			// start loading animation
			var loader = this._element.querySelector('.slim-loader');
			if (!loader) {
				return;
			}
			var ring = loader.children[0];
			loader.removeAttribute('style');
			ring.removeAttribute('style');

			// set infinite load state
			this._updateProgress(.5);

			// repeat!
			var repeat = 1000;

			snabbt(loader, {
				rotation: [0, 0, -(Math.PI * 2) * repeat],
				easing: 'linear',
				duration: repeat * 1000
			});

			snabbt(ring, {
				fromOpacity: 0,
				opacity: 1,
				duration: 250
			});
		}
	}, {
		key: '_stopProgressLoop',
		value: function _stopProgressLoop(callback) {

			if (!this._element) {
				return;
			}

			var loader = this._element.querySelector('.slim-loader');
			if (!loader) {
				return;
			}
			var ring = loader.children[0];

			snabbt(ring, {
				fromOpacity: parseFloat(ring.style.opacity),
				opacity: 0,
				duration: 250,
				complete: function complete() {
					snabbt(loader, 'stop');
					if (callback) {
						callback();
					}
				}
			});
		}
	}, {
		key: '_isButtonAllowed',
		value: function _isButtonAllowed(button) {

			if (button === 'edit') {
				return this._options.edit;
			}

			if (button === 'download') {
				return this._options.download;
			}

			if (button === 'upload') {

				// if no service defined upload button makes no sense
				if (!this._options.service) {
					return false;
				}

				// if push mode is set, no need for upload button
				if (this._options.push) {
					return false;
				}

				// set upload button
				return true;
			}

			if (button === 'remove') {
				return !this._isImageOnly();
			}

			return true;
		}
	}, {
		key: '_fallback',
		value: function _fallback() {

			this._removeState('init');

			// create status area
			var area = create('div', 'slim-area');
			area.innerHTML = '\n\t\t\t<div class="slim-status"><div class="slim-label">' + (this._options.label || '') + '</div></div>\n\t\t';
			this._element.appendChild(area);

			this._throwError(this._options.statusNoSupport);
		}

		/**
   * Event routing
   */

	}, {
		key: 'handleEvent',
		value: function handleEvent(e) {

			switch (e.type) {
				case 'click':
					this._onClick(e);
					break;
				case 'change':
					this._onChange(e);
					break;
				case 'cancel':
					this._onCancel(e);
					break;
				case 'confirm':
					this._onConfirm(e);
					break;
				case 'file-over':
					this._onFileOver(e);
					break;
				case 'file-out':
					this._onFileOut(e);
					break;
				case 'file-drop':
					this._onDropFile(e);
					break;
				case 'file-invalid':
					this._onInvalidFile(e);
					break;
				case 'file-invalid-drop':
					this._onInvalidFileDrop(e);
					break;
			}
		}
	}, {
		key: '_getIntro',
		value: function _getIntro() {
			return this._element.querySelector('.slim-result .in');
		}
	}, {
		key: '_getOutro',
		value: function _getOutro() {
			return this._element.querySelector('.slim-result .out');
		}
	}, {
		key: '_getInOut',
		value: function _getInOut() {
			return this._element.querySelectorAll('.slim-result img');
		}
	}, {
		key: '_getDrip',
		value: function _getDrip() {
			if (!this._drip) {
				this._drip = this._element.querySelector('.slim-drip > span');
			}
			return this._drip;
		}

		// errors

	}, {
		key: '_throwError',
		value: function _throwError(error) {

			this._addState('error');

			this._element.querySelector('.slim-label').style.display = 'none';

			var node = this._element.querySelector('.slim-error');
			if (!node) {
				node = create('div', 'slim-error');
				this._element.querySelector('.slim-status').appendChild(node);
			}

			node.innerHTML = error;
		}
	}, {
		key: '_removeError',
		value: function _removeError() {

			this._removeState('error');
			this._element.querySelector('.slim-label').style.display = '';

			var error = this._element.querySelector('.slim-error');
			if (error) {
				error.parentNode.removeChild(error);
			}
		}
	}, {
		key: '_openFileDialog',
		value: function _openFileDialog() {
			this._removeError();
			this._input.click();
		}

		// drop area clicked

	}, {
		key: '_onClick',
		value: function _onClick(e) {
			var _this15 = this;

			var list = e.target.classList;
			var target = e.target;

			// no preview, so possible to drop file
			if (list.contains('slim-file-hopper')) {
				this._openFileDialog();
				return;
			}

			// decide what button was clicked
			switch (target.getAttribute('data-action')) {
				case 'remove':
					this._options.willRemove.apply(this, [this.data, function () {
						_this15._doRemove();
					}]);
					break;
				case 'edit':
					this._doEdit();
					break;
				case 'download':
					this._doDownload();
					break;
				case 'upload':
					this._doUpload();
					break;
			}
		}
	}, {
		key: '_onInvalidFileDrop',
		value: function _onInvalidFileDrop() {

			this._onInvalidFile();

			this._removeState('file-over');

			// animate out drip
			var drip = this._getDrip();
			snabbt(drip.firstChild, {
				fromScale: [.5, .5],
				scale: [0, 0],
				fromOpacity: .5,
				opacity: 0,
				duration: 150,
				complete: function complete() {

					// clean up transforms
					resetTransforms(drip.firstChild);
				}
			});
		}
	}, {
		key: '_onInvalidFile',
		value: function _onInvalidFile() {

			var types = this._imageHopper.accept.map(getExtensionByMimeType);
			var error = this._options.statusFileType.replace('$0', types.join(', '));
			this._throwError(error);
		}
	}, {
		key: '_onImageTooSmall',
		value: function _onImageTooSmall() {

			var error = this._options.statusImageTooSmall.replace('$0', this._options.minSize.width + ' \xD7 ' + this._options.minSize.height);
			this._throwError(error);
		}
	}, {
		key: '_onOverWeightFile',
		value: function _onOverWeightFile() {

			var error = this._options.statusFileSize.replace('$0', this._options.maxFileSize);
			this._throwError(error);
		}
	}, {
		key: '_onFileOver',
		value: function _onFileOver(e) {

			this._addState('file-over');
			this._removeError();

			// animations
			var drip = this._getDrip();

			// move around drip
			var matrix = snabbt.createMatrix();
			matrix.translate(e.detail.x, e.detail.y, 0);
			snabbt.setElementTransform(drip, matrix);

			// on first entry fade in blob
			if (this._imageHopper.dragPath.length == 1) {

				// show
				drip.style.opacity = 1;

				// animate in
				snabbt(drip.firstChild, {
					fromOpacity: 0,
					opacity: .5,
					fromScale: [0, 0],
					scale: [.5, .5],
					duration: 150
				});
			}
		}
	}, {
		key: '_onFileOut',
		value: function _onFileOut(e) {

			this._removeState('file-over');
			this._removeState('file-invalid');
			this._removeError();

			// move to last position
			var drip = this._getDrip();
			var matrix = snabbt.createMatrix();
			matrix.translate(e.detail.x, e.detail.y, 0);
			snabbt.setElementTransform(drip, matrix);

			// animate out
			snabbt(drip.firstChild, {
				fromScale: [.5, .5],
				scale: [0, 0],
				fromOpacity: .5,
				opacity: 0,
				duration: 150,
				complete: function complete() {

					// clean up transforms
					resetTransforms(drip.firstChild);
				}
			});
		}

		/**
   * When a file was literally dropped on the drop area
   * @param e
   * @private
   */

	}, {
		key: '_onDropFile',
		value: function _onDropFile(e) {
			var _this16 = this;

			this._removeState('file-over');

			// get drip node reference and set to last position
			var drip = this._getDrip();
			var matrix = snabbt.createMatrix();
			matrix.translate(e.detail.x, e.detail.y, 0);
			snabbt.setElementTransform(drip, matrix);

			// get element minimum 10 entries distant from the last entry so we can calculate velocity of movement
			var l = this._imageHopper.dragPath.length;
			var jump = this._imageHopper.dragPath[l - Math.min(10, l)];

			// velocity
			var dx = e.detail.x - jump.x;
			var dy = e.detail.y - jump.y;

			snabbt(drip, {
				fromPosition: [e.detail.x, e.detail.y, 0],
				position: [e.detail.x + dx, e.detail.y + dy, 0],
				duration: 200
			});

			// animate out drip
			snabbt(drip.firstChild, {

				fromScale: [.5, .5],
				scale: [2, 2],
				fromOpacity: 1,
				opacity: 0,
				duration: 200,

				complete: function complete() {

					// clean up transforms
					resetTransforms(drip.firstChild);

					// load dropped resource
					_this16._load(e.target.files[0]);
				}
			});
		}

		/**
   * When a file has been selected after a click on the drop area
   * @param e
   * @private
   */

	}, {
		key: '_onChange',
		value: function _onChange(e) {

			this._load(e.target.files[0]);
		}

		/**
   * Loads a resource (blocking operation)
   * @param resource
   * @param callback(err)
   * @private
   */

	}, {
		key: '_load',
		value: function _load(resource, callback) {
			var _this17 = this;

			// stop here
			if (this._isBeingDestroyed) {
				return;
			}

			// turn string based resources (url / base64) into file objects
			if (typeof resource === 'string') {

				if (resourceIsBase64Data(resource)) {
					// resource is base64 data, turn into file
					this._load(base64ToBlob(resource), callback);
				} else {
					// resource is url, load with XHR
					loadURL(resource, function (file) {
						// continue with file object
						_this17._load(file, callback);
					});
				}

				// don't continue, wait for load
				return;
			}

			// let's continue with file resource
			var file = resource;

			// re-test if is valid file type
			// in case of loading base64 data or url
			if (this._imageHopper && this._imageHopper.accept.indexOf(file.type) === -1) {
				this._onInvalidFile();
				if (callback) {
					callback.apply(this, ['file-invalid']);
				}
				return;
			}

			// test if too big
			if (file.size && this._options.maxFileSize && bytesToMegaBytes(file.size) > this._options.maxFileSize) {
				this._onOverWeightFile();
				if (callback) {
					callback.apply(this, ['file-too-big']);
				}
				return;
			}

			// no longer empty
			this._removeState('empty');
			this._addState('busy');

			// can't drop any other image
			if (this._imageHopper) {
				this._imageHopper.enabled = false;
			}

			// if has loaded image editor set to dirty
			if (this._imageEditor) {
				this._imageEditor.dirty();
			}

			// continue
			this._data.input.name = file.name;
			this._data.input.type = file.type;
			this._data.input.size = file.size;
			this._data.input.file = file;

			// start loading indicator
			this._startProgressLoop();

			// fetch resource
			getImageAsCanvas(file, function (image, meta) {

				var rewind = function rewind() {

					// rewind state
					if (_this17._imageHopper) {
						_this17._imageHopper.enabled = true;
					}

					_this17._removeState('busy');
					_this17._stopProgressLoop();
					_this17._resetData();
				};

				// if no image, something went wrong
				if (!image) {

					rewind();

					if (callback) {
						callback.apply(_this17, ['file-not-found']);
					}

					return;
				}

				// test if image is too small
				if (!covers(image, _this17._options.minSize)) {

					rewind();

					_this17._onImageTooSmall();

					if (callback) {
						callback.apply(_this17, ['image-too-small']);
					}

					return;
				}

				var status = _this17._options.didLoad.apply(_this17, [file, image, meta]);
				if (status !== true) {

					rewind();

					if (status !== false) {
						_this17._throwError(status);
					}

					if (callback) {
						callback.apply(_this17, [status]);
					}

					return;
				}

				// load the image
				_this17._loadCanvas(image, function () {

					// do intro stuff
					var intro = _this17._getIntro();

					// setup base animation
					var animation = {
						fromScale: [1.25, 1.25],
						scale: [1, 1],
						fromOpacity: 0,
						opacity: 1,
						complete: function complete() {

							resetTransforms(intro);

							intro.style.opacity = 1;

							// don't show buttons when instant editing
							// the buttons will be triggered by the closing of the popup
							if (!_this17._canInstantEdit()) {
								_this17._showButtons();
							}

							_this17._stopProgressLoop();
							_this17._removeState('busy');
							_this17._addState('preview');

							if (callback) {
								callback.apply(_this17, [null, _this17.data]);
							}
						}

					};

					// if not attached to DOM, don't animate
					if (_this17.isDetached()) {
						animation.duration = 1;
					} else {
						animation.easing = 'spring';
						animation.springConstant = .3;
						animation.springDeceleration = .7;
					}

					// if is instant edit mode don't zoom out but zoom in
					if (_this17._canInstantEdit()) {
						animation.delay = 500;
						animation.duration = 1;

						// instant edit mode just fire up the editor immidiately
						_this17._doEdit();
					}

					// reveal loaded image
					snabbt(intro, animation);
				});
			});
		}
	}, {
		key: '_loadCanvas',
		value: function _loadCanvas(image, ready) {
			var _this18 = this;

			// halt here if cropper is currently being destroyed
			if (this._isBeingDestroyed) {
				return;
			}

			// scales the drop area
			// if is 'input' or 'free' parameter
			if (!this._isFixedRatio()) {
				this._ratio = image.height / image.width;
				this._scaleDropArea(this._ratio);
			}

			// store raw data
			this._data.input.image = image;
			this._data.input.width = image.width;
			this._data.input.height = image.height;

			if (this._initialCrop) {

				// use initial supplied crop rectangle
				this._data.actions.crop = clone(this._initialCrop);
				this._data.actions.crop.type = CropType.INITIAL;

				// clear initial crop, it's no longer useful
				this._initialCrop = null;
			} else {
				// get automagical crop rectangle
				this._data.actions.crop = getAutoCropRect(image.width, image.height, this._ratio);
				this._data.actions.crop.type = CropType.AUTO;
			}

			// if max size set
			if (this._options.size) {
				this._data.actions.size = {
					width: this._options.size.width,
					height: this._options.size.height
				};
			}

			// do initial auto transform
			this._applyTransforms(image, function (transformedImage) {

				var intro = _this18._getIntro();
				var scalar = intro.offsetWidth / transformedImage.width;

				// store data, if has preview image this prevents initial load from pushing
				var willUpload = false;

				// can only do auto upload when service is defined and push is enabled...
				if (_this18._options.service && _this18._options.push) {

					// ...and is not transformation of initial image
					// + is not instant edit mode
					if (!_this18._hasInitialImage && !_this18._canInstantEdit()) {
						willUpload = true;
					}
				}

				// store data (possibly)
				_this18._save(function () {}, willUpload);

				// show intro animation
				intro.src = '';
				intro.src = cloneCanvasScaled(transformedImage, scalar).toDataURL();
				intro.onload = function () {

					intro.onload = null;

					// bail out if we've been cleaned up
					if (_this18._isBeingDestroyed) {
						return;
					}

					if (ready) {
						ready();
					}
				};
			});
		}
	}, {
		key: '_applyTransforms',
		value: function _applyTransforms(image, ready) {
			var _this19 = this;

			transformCanvas(image, this._data.actions, function (transformedImage) {

				var outputImage = transformedImage;

				// if should force/correct output size?
				// - is forced size set?
				// - is a discrepancy found between requested output size and transformed size
				if (_this19._options.forceSize || _this19._options.size && sizeDist(_this19._options.size, transformedImage) == 1) {
					outputImage = create('canvas');
					outputImage.width = _this19._options.size.width;
					outputImage.height = _this19._options.size.height;
					var ctx = outputImage.getContext('2d');
					ctx.drawImage(transformedImage, 0, 0, _this19._options.size.width, _this19._options.size.height);
				}

				// store output
				_this19._data.output.width = outputImage.width;
				_this19._data.output.height = outputImage.height;
				_this19._data.output.image = outputImage;

				_this19._onTransformCanvas(function (transformedData) {

					_this19._data = transformedData;

					_this19._options.didTransform.apply(_this19, [_this19.data]);

					ready(_this19._data.output.image);
				});
			});
		}
	}, {
		key: '_onTransformCanvas',
		value: function _onTransformCanvas(ready) {

			this._options.willTransform.apply(this, [this.data, ready]);
		}

		/**
   * Creates the editor nodes
   * @private
   */

	}, {
		key: '_appendEditor',
		value: function _appendEditor() {
			var _this20 = this;

			// we already have an editor
			if (this._imageEditor) {
				return;
			}

			// add editor
			this._imageEditor = new ImageEditor(create('div'), {
				minSize: this._options.minSize,

				buttonConfirmClassName: this._options.buttonConfirmClassName,
				buttonCancelClassName: this._options.buttonCancelClassName,
				buttonRotateClassName: this._options.buttonRotateClassName,

				buttonConfirmLabel: this._options.buttonConfirmLabel,
				buttonCancelLabel: this._options.buttonCancelLabel,
				buttonRotateLabel: this._options.buttonRotateLabel,

				buttonConfirmTitle: this._options.buttonConfirmTitle,
				buttonCancelTitle: this._options.buttonCancelTitle,
				buttonRotateTitle: this._options.buttonRotateTitle

			});

			// listen to events
			ImageEditorEvents.forEach(function (e) {
				_this20._imageEditor.element.addEventListener(e, _this20);
			});
		}
	}, {
		key: '_scaleDropArea',
		value: function _scaleDropArea(ratio) {
			var node = this._getRatioSpacerElement();
			if (!node || !this._element) {
				return;
			}
			node.style.marginBottom = ratio * 100 + '%';
			this._element.setAttribute('data-ratio', '1:' + ratio);
		}

		/**
   * Data Layer
   * @private
   */
		// image editor closed

	}, {
		key: '_onCancel',
		value: function _onCancel(e) {

			this._removeState('editor');

			this._showButtons();

			this._hideEditor();

			if (this._options.instantEdit && !this._hasInitialImage && this._isAutoCrop()) {
				this._doRemove();
			}
		}

		// user confirmed changes

	}, {
		key: '_onConfirm',
		value: function _onConfirm(e) {
			var _this21 = this;

			this._removeState('editor');

			this._startProgressLoop();
			this._addState('busy');

			// clear data
			this._output.value = '';

			// apply new action object to this._data
			this._data.actions.rotation = e.detail.rotation;
			this._data.actions.crop = e.detail.crop;
			this._data.actions.crop.type = CropType.MANUAL;

			// do transforms
			this._applyTransforms(this._data.input.image, function (transformedImage) {

				// set new image result
				var images = _this21._getInOut();
				var intro = images[0].className === 'out' ? images[0] : images[1];
				var outro = intro === images[0] ? images[1] : images[0];

				intro.className = 'in';
				intro.style.opacity = '0';
				intro.style.zIndex = '2';
				outro.className = 'out';
				outro.style.zIndex = '1';

				// new image get's
				intro.src = '';
				intro.src = cloneCanvasScaled(transformedImage, intro.offsetWidth / transformedImage.width).toDataURL();
				intro.onload = function () {

					intro.onload = null;

					// scale the dropzone
					if (_this21._options.ratio === 'free') {
						_this21._ratio = intro.naturalHeight / intro.naturalWidth;
						_this21._scaleDropArea(_this21._ratio);
					}

					// close the editor
					_this21._hideEditor();

					// wait a tiny bit so animations sync up nicely
					var timer = setTimeout(function () {

						// show the preview
						_this21._showPreview(intro, function () {

							// if
							// - service set
							// - and we are pushing
							// - and we don't instant edit
							// we will upload
							var willUpload = _this21._options.service && _this21._options.push;

							// save the data
							_this21._save(function (err, data, res) {

								// done!
								_this21._toggleButton('upload', true);

								_this21._stopProgressLoop();
								_this21._removeState('busy');

								_this21._showButtons();
							}, willUpload);
						});
					}, 250);

					_this21._timers.push(timer);
				};
			});
		}
	}, {
		key: '_crop',
		value: function _crop(x, y, width, height) {
			var _this22 = this;

			var callback = arguments.length <= 4 || arguments[4] === undefined ? function () {} : arguments[4];


			this._startProgressLoop();
			this._addState('busy');

			// clear data
			this._output.value = '';

			// apply new action object to this._data
			this._data.actions.crop = {
				x: x,
				y: y,
				width: width,
				height: height
			};
			this._data.actions.crop.type = CropType.MANUAL;

			// do transforms
			this._applyTransforms(this._data.input.image, function (transformedImage) {

				// set new image result
				var images = _this22._getInOut();
				var intro = images[0].className === 'out' ? images[0] : images[1];
				var outro = intro === images[0] ? images[1] : images[0];

				intro.className = 'in';
				intro.style.opacity = '1';
				intro.style.zIndex = '2';
				outro.className = 'out';
				outro.style.zIndex = '0';

				// new image
				intro.src = '';
				intro.src = cloneCanvasScaled(transformedImage, intro.offsetWidth / transformedImage.width).toDataURL();
				intro.onload = function () {
					intro.onload = null;

					// scale the dropzone
					if (_this22._options.ratio === 'free') {
						_this22._ratio = intro.naturalHeight / intro.naturalWidth;
						_this22._scaleDropArea(_this22._ratio);
					}

					// determine if will also upload
					var willUpload = _this22._options.service && _this22._options.push;

					// save the data
					_this22._save(function (err, data, res) {

						_this22._stopProgressLoop();
						_this22._removeState('busy');

						callback.apply(_this22, [_this22.data]);
					}, willUpload);
				};
			});
		}
	}, {
		key: '_save',
		value: function _save() {
			var _this23 = this;

			var callback = arguments.length <= 0 || arguments[0] === undefined ? function () {} : arguments[0];
			var allowUpload = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1];


			if (this._isBeingDestroyed) {
				return;
			}

			// flatten data also turns output canvas into data uri
			// removes input file object and image object
			var data = this.dataBase64;

			// decide if we need to
			// - A. Store the data in an output field
			// - B. Upload the data and store the response in output field

			// - we are not doing async uploading (in which case output is used for response)
			// - we are not initialising a replaceable image
			if (!this._options.service && !(this._isInitialising && !this._isImageOnly())) {

				this._options.willSave.apply(this, [data, function (data) {

					_this23._store(data);

					_this23._options.didSave.apply(_this23, [data]);
				}]);
			}

			if (this._isBeingDestroyed) {
				return;
			}

			// is remote service defined upload async
			if (this._options.service && allowUpload) {

				// allow user to modify the data
				this._options.willSave.apply(this, [data, function (data) {

					// do the actual uploading
					_this23._upload(data, function (err, res) {

						// store response
						if (!err) {
							_this23._storeServerResponse(res);
						}

						// we did upload data
						_this23._options.didUpload.apply(_this23, [err, data, res]);

						// done!
						callback(err, data, res);
					});
				}]);
			}

			// if no service, we're done here
			if (!this._options.service || !allowUpload) {
				callback();
			}
		}

		// stores active file information in hidden output field

	}, {
		key: '_storeServerResponse',
		value: function _storeServerResponse(data) {

			// remove required flag
			if (this._isRequired) {
				this._input.required = false;
			}

			// store data returned from server
			this._data.server = data;

			// sync with output value
			this._output.value = (typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object' ? JSON.stringify(this._data.server) : data;
		}

		// stores data in output field

	}, {
		key: '_store',
		value: function _store(data) {

			if (this._isRequired) {
				this._input.required = false;
			}

			this._output.value = JSON.stringify(data);
		}

		// uploads given data to server

	}, {
		key: '_upload',
		value: function _upload(data, callback) {
			var _this24 = this;

			var formData = new FormData();

			// output dataset
			formData.append(this._output.name, JSON.stringify(data));

			// if input should be posted along, append data to FormData object
			if (inArray('input', this._options.post)) {
				formData.append(this._inputReference, this._data.input.file, this._data.input.file.name);
			}

			var statusNode = this._element.querySelector('.slim-upload-status');

			var requestDecorator = this._options.willRequest;

			send(
			// url to service
			this._options.service,

			// data
			formData,

			// decorator (useful to add headers to request
			requestDecorator,

			// progress
			function (loaded, total) {

				_this24._updateProgress(loaded / total);
			},

			// success
			function (obj) {

				var timer = setTimeout(function () {

					// it's possible that Slim has been destroyed in the mean time.
					if (_this24._isBeingDestroyed) {
						return;
					}

					statusNode.innerHTML = _this24._options.statusUploadSuccess;
					statusNode.setAttribute('data-state', 'success');
					statusNode.style.opacity = 1;

					// hide status update after 2 seconds
					var timer = setTimeout(function () {
						statusNode.style.opacity = 0;
					}, 2000);

					_this24._timers.push(timer);
				}, 250);

				_this24._timers.push(timer);

				callback(null, obj);
			},

			// error
			function (status) {

				var html = '';
				if (status === 'file-too-big') {
					html = _this24._options.statusContentLength;
				} else {
					html = _this24._options.statusUnknownResponse;
				}

				// when an error occurs the status update is not automatically hidden
				var timer = setTimeout(function () {

					statusNode.innerHTML = html;
					statusNode.setAttribute('data-state', 'error');
					statusNode.style.opacity = 1;
				}, 250);

				_this24._timers.push(timer);

				callback(status);
			});
		}
	}, {
		key: '_showEditor',
		value: function _showEditor() {

			SlimPopover.show();

			this._imageEditor.show();
		}
	}, {
		key: '_hideEditor',
		value: function _hideEditor() {

			this._imageEditor.hide();

			var timer = setTimeout(function () {

				SlimPopover.hide();
			}, 250);

			this._timers.push(timer);
		}

		/**
   * Animations
   */

	}, {
		key: '_showPreview',
		value: function _showPreview(intro, callback) {

			snabbt(intro, {

				fromPosition: [0, 50, 0],
				position: [0, 0, 0],

				fromScale: [1.5, 1.5],
				scale: [1, 1],

				fromOpacity: 0,
				opacity: 1,

				easing: 'spring',
				springConstant: .3,
				springDeceleration: .7,

				complete: function complete() {

					resetTransforms(intro);

					if (callback) {
						callback();
					}
				}

			});
		}
	}, {
		key: '_hideResult',
		value: function _hideResult(callback) {

			var intro = this._getIntro();
			if (!intro) {
				return;
			}

			snabbt(intro, {

				fromScale: [1, 1],
				scale: [.5, .5],

				fromOpacity: 1,
				opacity: 0,

				easing: 'spring',
				springConstant: .3,
				springDeceleration: .75,

				complete: function complete() {
					resetTransforms(intro);
					if (callback) {
						callback();
					}
				}

			});
		}
	}, {
		key: '_showButtons',
		value: function _showButtons(callback) {

			if (!this._btnGroup) {
				return;
			}

			this._btnGroup.style.display = '';

			// setup animation
			var animation = {
				fromScale: [.5, .5],
				scale: [1, 1],
				fromPosition: [0, 10, 0],
				position: [0, 0, 0],
				fromOpacity: 0,
				opacity: 1,
				complete: function complete() {
					resetTransforms(this);
				},
				allDone: function allDone() {
					if (callback) {
						callback();
					}
				}
			};

			// don't animate when detached
			if (this.isDetached()) {
				animation.duration = 1;
			} else {
				animation.delay = function (i) {
					return 250 + i * 50;
				};
				animation.easing = 'spring';
				animation.springConstant = .3;
				animation.springDeceleration = .85;
			}

			snabbt(this._btnGroup.childNodes, animation);
		}
	}, {
		key: '_hideButtons',
		value: function _hideButtons(callback) {
			var _this25 = this;

			if (!this._btnGroup) {
				return;
			}

			var animation = {
				fromScale: [1, 1],
				scale: [.85, .85],
				fromOpacity: 1,
				opacity: 0,
				allDone: function allDone() {
					_this25._btnGroup.style.display = 'none';
					if (callback) {
						callback();
					}
				}
			};

			// don't animate when detached
			if (this.isDetached()) {
				animation.duration = 1;
			} else {
				animation.easing = 'spring';
				animation.springConstant = .3;
				animation.springDeceleration = .75;
			}

			// go hide the buttons
			snabbt(this._btnGroup.childNodes, animation);
		}
	}, {
		key: '_hideStatus',
		value: function _hideStatus() {

			var statusNode = this._element.querySelector('.slim-upload-status');
			statusNode.style.opacity = 0;
		}
	}, {
		key: '_doEdit',
		value: function _doEdit() {
			var _this26 = this;

			// if no input data available, can't edit anything
			if (!this._data.input.image) {
				return;
			}

			// now in editor mode
			this._addState('editor');

			// create editor (if not already created)
			if (!this._imageEditor) {
				this._appendEditor();
			}

			// hide or show rotate button
			this._imageEditor.showRotateButton = this._options.rotateButton;

			// append to popover
			SlimPopover.inner = this._imageEditor.element;

			// read the data
			this._imageEditor.open(

			// send copy of canvas to the editor
			cloneCanvas(this._data.input.image),

			// determine ratio
			this._options.ratio === 'free' ? null : this._ratio,

			// the initial crop to show
			this._data.actions.crop,

			// the initial rotation of the image
			this._data.actions.rotation,

			// handle editor load
			function () {

				_this26._showEditor();

				_this26._hideButtons();

				_this26._hideStatus();
			});
		}
	}, {
		key: '_doRemove',
		value: function _doRemove() {
			var _this27 = this;

			// cannot remove when is only one image
			if (this._isImageOnly()) {
				return;
			}

			this._clearState();
			this._addState('empty');

			this._hasInitialImage = false;
			this._imageHopper.enabled = true;

			if (this._isRequired) {
				this._input.required = true;
			}

			var out = this._getOutro();
			if (out) {
				out.style.opacity = '0';
			}

			// get public available clone of data
			var data = this.data;

			// now reset all data
			this._resetData();

			var timer = setTimeout(function () {

				if (_this27._isBeingDestroyed) {
					return;
				}

				_this27._hideButtons(function () {

					_this27._toggleButton('upload', true);
				});

				_this27._hideStatus();

				_this27._hideResult();

				_this27._options.didRemove.apply(_this27, [data]);
			}, this.isDetached() ? 0 : 250);

			this._timers.push(timer);

			return data;
		}
	}, {
		key: '_doUpload',
		value: function _doUpload(callback) {
			var _this28 = this;

			// if no input data available, can't upload anything
			if (!this._data.input.image) {
				return;
			}

			this._addState('upload');
			this._startProgress();

			this._hideButtons(function () {

				// block upload button
				_this28._toggleButton('upload', false);

				_this28._save(function (err, data, res) {

					_this28._removeState('upload');
					_this28._stopProgress();

					if (callback) {
						callback.apply(_this28, [err, data, res]);
					}

					if (err) {
						_this28._toggleButton('upload', true);
					}

					_this28._showButtons();
				});
			});
		}
	}, {
		key: '_doDownload',
		value: function _doDownload() {

			var image = this._data.output.image;
			if (!image) {
				return;
			}

			downloadCanvas(image, this._data.output.name, this._data.input.type);
		}
	}, {
		key: '_doDestroy',
		value: function _doDestroy() {
			var _this29 = this;

			// set destroy flag to halt any running functionality
			this._isBeingDestroyed = true;

			// clear timers
			this._timers.forEach(function (timer) {
				clearTimeout(timer);
			});
			this._timers = [];

			// clean up snabbt animations
			snabbt(this._element, 'detach');

			// this removes the image hopper if it's attached
			if (this._imageHopper) {
				HopperEvents.forEach(function (e) {
					_this29._imageHopper.element.removeEventListener(e, _this29);
				});
				this._imageHopper.destroy();
				this._imageHopper = null;
			}

			// this block removes the image editor
			if (this._imageEditor) {
				ImageEditorEvents.forEach(function (e) {
					_this29._imageEditor.element.removeEventListener(e, _this29);
				});
				this._imageEditor.destroy();
				this._imageEditor = null;
			}

			// remove button event listeners
			nodeListToArray(this._btnGroup.children).forEach(function (btn) {
				btn.removeEventListener('click', _this29);
			});

			// stop listening to input
			this._input.removeEventListener('change', this);

			// detect if was wrapped, if so, remove wrapping (needs to have parent node)
			if (this._element !== this._originalElement && this._element.parentNode) {
				this._element.parentNode.replaceChild(this._originalElement, this._element);
			}

			// restore HTML of original element
			this._originalElement.innerHTML = this._originalElementInner;

			// get current attributes and remove all, then add original attributes
			function matchesAttributeInList(a, attributes) {
				return attributes.filter(function (attr) {
					return a.name === attr.name && a.value === attr.value;
				}).length !== 0;
			}

			var attributes = getElementAttributes(this._originalElement);
			attributes.forEach(function (attribute) {

				// if attribute  is contained in original element attribute list and is the same, don't remove
				if (matchesAttributeInList(attribute, _this29._originalElementAttributes)) {
					return;
				}

				// else remove
				_this29._originalElement.removeAttribute(attribute.name);
			});

			this._originalElementAttributes.forEach(function (attribute) {

				// attribute was never removed
				if (matchesAttributeInList(attribute, attributes)) {
					return;
				}

				// add attribute
				_this29._originalElement.setAttribute(attribute.name, attribute.value);
			});

			// now destroyed this counter so the total Slim count can be lowered
			SlimCount = Math.max(0, SlimCount - 1);

			// if slim count has reached 0 it's time to clean up the popover
			if (SlimPopover && SlimCount === 0) {
				SlimPopover.destroy();
				SlimPopover = null;
			}

			this._originalElement = null;
			this._element = null;
			this._input = null;
			this._output = null;
			this._btnGroup = null;
			this._options = null;
		}
	}, {
		key: 'dataBase64',


		/**
   * Public API
   */
		// properties
		get: function get() {
			return flattenData(this._data, this._options.post, this._options.jpegCompression, this._options.forceType);
		}
	}, {
		key: 'data',
		get: function get() {
			return cloneData(this._data);
		}
	}, {
		key: 'element',
		get: function get() {
			return this._element;
		}
	}, {
		key: 'size',
		set: function set(dimensions) {

			if (!dimensions || !dimensions.width || !dimensions.height) {
				return;
			}
			this._options.size = clone(dimensions);
			this._data.actions.size = clone(dimensions);
		}
	}, {
		key: 'ratio',
		set: function set(ratio) {

			if (!ratio || typeof ratio !== 'string') {
				return;
			}

			this._options.ratio = ratio;

			if (this._isFixedRatio()) {
				var parts = intSplit(this._options.ratio, ':');
				this._ratio = parts[1] / parts[0];
				this._scaleDropArea(this._ratio);
			}
		}
	}], [{
		key: 'options',
		value: function options() {

			var defaults = {

				// edit button is enabled by default
				edit: true,

				// immidiately summons editor on load
				instantEdit: false,

				// metadata values
				meta: {},

				// ratio of crop by default is the same as input image ratio
				ratio: 'free',

				// dimensions to resize the resulting image to
				size: null,

				// initial crop settings for example: {x:0, y:0, width:100, height:100}
				crop: null,

				// post these values
				post: ['output', 'actions'],

				// call this service to submit cropped data
				service: null,

				// when service is set, and this is set to true, Soon will auto upload all crops (also auto crops)
				push: false,

				// default fallback name for field
				defaultInputName: 'slim[]',

				// minimum size of cropped area object with width and height property
				minSize: {
					width: 100,
					height: 100
				},

				// maximum file size in MB to upload
				maxFileSize: null,

				// compression of JPEG (between 0 and 100)
				jpegCompression: null,

				// render download link
				download: false,

				// save initially loaded image
				saveInitialImage: false,

				// the type to force (jpe|jpg|jpeg or png)
				forceType: false,

				// the forced output size of the image
				forceSize: null,

				// enable or disable rotation
				rotateButton: true,

				// label HTML to show inside drop area
				label: '<p>Drop your image here</p>',

				// error messages
				statusFileType: '<p>Invalid file type, expects: $0.</p>',
				statusFileSize: '<p>File is too big, maximum file size: $0 MB.</p>',
				statusNoSupport: '<p>Your browser does not support image cropping.</p>',
				statusImageTooSmall: '<p>Image is too small, minimum size is: $0 pixels.</p>',
				statusContentLength: '<span class="slim-upload-status-icon"></span> The file is probably too big',
				statusUnknownResponse: '<span class="slim-upload-status-icon"></span> An unknown error occurred',
				statusUploadSuccess: '<span class="slim-upload-status-icon"></span> Saved',

				// callback methods
				didInit: function didInit(data) {},
				didLoad: function didLoad(file, image, meta) {
					return true;
				},
				didSave: function didSave(data) {},
				didUpload: function didUpload(err, data, res) {},
				didRemove: function didRemove(data) {},
				didTransform: function didTransform(data) {},

				willTransform: function willTransform(data, cb) {
					cb(data);
				},
				willSave: function willSave(data, cb) {
					cb(data);
				},
				willRemove: function willRemove(data, cb) {
					cb();
				},
				willRequest: function willRequest(xhr) {}

			};

			// add default button labels
			SlimButtons.concat(ImageEditor.Buttons).concat('rotate').forEach(function (btn) {
				var capitalized = capitalizeFirstLetter(btn);
				defaults['button' + capitalized + 'ClassName'] = null;
				defaults['button' + capitalized + 'Label'] = capitalized;
				defaults['button' + capitalized + 'Title'] = capitalized;
			});

			return defaults;
		}
	}]);

	return Slim;
}();

/**
 * Slim Static Methods
 */


(function () {

	var instances = [];

	var indexOfElement = function indexOfElement(element) {
		var i = 0;
		var l = instances.length;
		for (; i < l; i++) {
			if (instances[i].isAttachedTo(element)) {
				return i;
			}
		}
		return -1;
	};

	function toLabel(v) {
		// if value set, use as label
		if (v) {
			return '<p>' + v + '</p>';
		}

		// else use default text
		return null;
	}

	function toFunctionReference(name) {
		var ref = window;
		var levels = name.split('.');
		levels.forEach(function (level, index) {
			if (!ref[levels[index]]) {
				return;
			}
			ref = ref[levels[index]];
		});
		return ref !== window ? ref : null;
	}

	var passThrough = function passThrough(v) {
		return v;
	};
	var defaultFalse = function defaultFalse(v) {
		return v === 'true';
	};
	var defaultTrue = function defaultTrue(v) {
		return v ? v === 'true' : true;
	};
	var defaultLabel = function defaultLabel(v) {
		return toLabel(v);
	};
	var defaultFunction = function defaultFunction(v) {
		return v ? toFunctionReference(v) : null;
	};
	var defaultSize = function defaultSize(v) {
		if (!v) {
			return null;
		}
		var parts = intSplit(v, ',');
		return {
			width: parts[0],
			height: parts[1]
		};
	};

	var toFloat = function toFloat(v) {
		if (!v) {
			return null;
		}
		return parseFloat(v);
	};

	var toInt = function toInt(v) {
		if (!v) {
			return null;
		}
		return parseInt(v, 10);
	};

	var toRect = function toRect(v) {
		if (!v) {
			return null;
		}
		var obj = {};
		v.split(',').map(function (p) {
			return parseInt(p, 10);
		}).forEach(function (v, i) {
			obj[Rect[i]] = v;
		});
		return obj;
	};

	var defaults = {

		// is user allowed to download the cropped image?
		'download': defaultFalse,

		// is user allowed to edit the cropped image?
		'edit': defaultTrue,

		// open editor immidiately on file drop
		'instantEdit': defaultFalse,

		// minimum crop size in pixels of original image
		'minSize': defaultSize,

		// the final bounding box of the output image
		'size': defaultSize,

		// the forced output size of the image
		'forceSize': defaultSize,

		// url to post to
		'service': function service(v) {
			return typeof v === 'undefined' ? null : v;
		},

		// set auto push mode
		'push': defaultFalse,

		// set crop rect
		'crop': toRect,

		// what to post
		'post': function post(v) {
			if (!v) {
				return null;
			}
			return v.split(',').map(function (item) {
				return item.trim();
			});
		},

		// default input name
		'defaultInputName': passThrough,

		// the ratio of the crop
		'ratio': function ratio(v) {
			if (!v) {
				return null;
			}
			return v;
		},

		// maximum file size
		'maxFileSize': toFloat,

		// jpeg compression
		'jpegCompression': toInt,

		// sets file type to force output to
		'forceType': passThrough,

		// bool determining if initial image should be saved
		'saveInitialImage': defaultFalse,

		// rotate button
		'rotateButton': defaultTrue,

		// default labels
		'label': defaultLabel

	};

	// labels
	['FileSize', 'FileType', 'NoSupport', 'ImageTooSmall'].forEach(function (status) {
		defaults['status' + status] = defaultLabel;
	});

	// status
	['ContentLength', 'UnknownResponse', 'UploadSuccess'].forEach(function (status) {
		defaults['status' + status] = passThrough;
	});

	// the did callbacks
	['Init', 'Load', 'Save', 'Upload', 'Remove', 'Transform'].forEach(function (cb) {
		defaults['did' + cb] = defaultFunction;
	});

	// the will callbacks
	['Transform', 'Save', 'Remove', 'Request'].forEach(function (cb) {
		defaults['will' + cb] = defaultFunction;
	});

	// button defaults
	var buttonOptions = ['ClassName', 'Label', 'Title'];
	SlimButtons.concat(ImageEditor.Buttons).concat('rotate').forEach(function (btn) {
		var capitalized = capitalizeFirstLetter(btn);
		buttonOptions.forEach(function (opt) {
			defaults['button' + capitalized + opt] = passThrough;
		});
	});

	Slim.supported = function () {
		return typeof window.FileReader !== 'undefined';
	}();

	Slim.parse = function (context) {
		var elements;
		var element;
		var i;
		var croppers = [];

		// find all crop elements and bind Crop behavior
		elements = context.querySelectorAll('.slim:not([data-state])');
		i = elements.length;

		while (i--) {
			element = elements[i];
			croppers.push(Slim.create(element, Slim.getOptionsFromAttributes(element)));
		}

		return croppers;
	};

	Slim.getOptionsFromAttributes = function (element) {

		var dataset = getDataset(element);

		var options = {
			meta: {}
		};

		for (var prop in dataset) {

			var valueTransformer = defaults[prop];
			var _value = dataset[prop];

			if (valueTransformer) {
				_value = valueTransformer(_value);
				_value = _value === null ? clone(Slim.options()[prop]) : _value;
				options[prop] = _value;
			} else if (prop.indexOf('meta') === 0) {
				options['meta'][lowercaseFirstLetter(prop.substr(4))] = _value;
			}
		}

		return options;
	};

	Slim.find = function (element) {
		var result = instances.filter(function (instance) {
			return instance.isAttachedTo(element);
		});
		return result ? result[0] : null;
	};

	Slim.create = function (element, options) {

		// if already in array, can't create another slim
		if (Slim.find(element)) {
			return;
		}

		// if no options supplied, try to get the options from the element
		if (!options) {
			options = Slim.getOptionsFromAttributes(element);
		}

		// instance
		var slim = new Slim(element, options);

		// add new slim
		instances.push(slim);

		// return the slim instance
		return slim;
	};

	Slim.destroy = function (element) {
		var index = indexOfElement(element);

		if (index < 0) {
			return false;
		}

		instances[index].destroy();
		instances.splice(index, 1);

		return true;
	};
})();
    return Slim;
}());

	// Create callback scope
	var callbacks = {};
	for (var key in Slim.options()) {
		if (/^(did|will)/.test(key)) {
			callbacks[key] = '&';
		}
	}

	// Angular Directive
	angular
		.module('slim', [])
		.directive('slim', function($timeout) {
			return {
				restrict: 'C',
				scope: callbacks,
				link: function(scope, element, attributes) {

					$timeout(function () {

						// get options
						var options = Slim.getOptionsFromAttributes(element[0]);

						// add callbacks
						for (var key in attributes) {
							if (/^(did|will)/.test(key)) {
								options[key] = scope[key]();
							}
						}

						// create slim
						Slim.create(element[0], options);

					});

				}
			};
		});

}(angular));