/* * Slim v4.19.0 - Image Cropping Made Easy * Copyright (c) 2018 Rik Schennink - http://slimimagecropper.com */ module.exports = (function() { // custom event polyfill for IE10 (function() { if ( typeof window.CustomEvent === 'function' ) return false; 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.Event.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 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= 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 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 1 && arguments[1] !== undefined ? arguments[1] : 'POST'; var data = arguments[2]; var requestDecorator = arguments[3]; var progress = arguments[4]; var success = arguments[5]; var err = arguments[6]; 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(method, url, true); // if request decorator defined pass XMLHttpRequest instance to decorator if (requestDecorator) { requestDecorator(xhr, data); } // handle state changes xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { 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) { var _obj = void 0; try { _obj = JSON.parse(xhr.responseText); } catch (e) {} // if is clean failure response if ((typeof _obj === 'undefined' ? 'undefined' : _typeof(_obj)) === 'object' && _obj.status === 'failure') { err(_obj.message); return; } 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 getFileName = function getFileName(path) { return path.split('/').pop().split('?').shift(); }; var leftPad = function leftPad(value) { var padding = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; return (padding + value).slice(-padding.length); }; var getDateString = function getDateString(date) { return date.getFullYear() + '-' + leftPad(date.getMonth() + 1, '00') + '-' + leftPad(date.getDate(), '00') + '_' + leftPad(date.getHours(), '00') + '-' + leftPad(date.getMinutes(), '00') + '-' + leftPad(date.getSeconds(), '00'); }; var getFileNameByFile = function getFileNameByFile(file) { if (typeof file.name === 'undefined') { return getDateString(new Date()) + '.' + getExtensionByMimeType(getFileTypeByFile(file)); } return file.name; }; var getFileTypeByFile = function getFileTypeByFile(file) { return file.type || 'image/jpeg'; }; var getFileNameWithoutExtension = function getFileNameWithoutExtension(path) { if (typeof path !== 'string') { return getDateString(new Date()); } var name = getFileName(path); return name.split('.').shift(); }; var blobToFile = function blobToFile(blob, name) { try { return new File([blob], name, { type: blob.type, lastModified: Date.now() }); } catch (e) { blob.lastModified = new Date(); blob.name = name; return blob; } }; var resourceIsFetchURL = function resourceIsFetchURL(resource) { return (/^fetch\//.test(resource) ); }; var resourceIsBase64Data = function resourceIsBase64Data(resource) { return (/^data:image/.test(resource) ); }; var loadRemoteURL = function loadRemoteURL(fetcher, fetchRequestDecorator, loadRequestDecorator, url, err, cb) { fetcher = '' + fetcher + (fetcher.indexOf('?') !== -1 ? '&' : '?') + 'url=' + url; var xhr = new XMLHttpRequest(); xhr.open('GET', fetcher, true); fetchRequestDecorator(xhr); xhr.responseType = 'json'; xhr.onload = function () { if (this.response.status === 'failure') { err(this.response.message); return; } loadURL(this.response.body, loadRequestDecorator, cb); }; xhr.send(); }; var loadURL = function loadURL(url, requestDecorator, cb, err) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); requestDecorator(xhr); xhr.responseType = 'blob'; xhr.onload = function (e) { if (xhr.status >= 200 && xhr.status < 300) { 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)); } else { err(xhr.status + ': ' + xhr.statusText); } }; xhr.onerror = function () { err(); }; 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 base64ToArrayBuffer = function base64ToArrayBuffer(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); } return ab; }; var base64ToBlob = function base64ToBlob(dataURI, filename) { 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); if (typeof filename === 'undefined') { filename = getDateString(new Date()) + '.' + getExtensionByMimeType(mimeType); } return blobToFile(createBlob(ab, mimeType), filename); }; var createBlob = function createBlob(data, mimeType) { var BB = window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; if (BB) { var bb = new BB(); bb.append(data); return bb.getBlob(mimeType); } return new Blob([data], { type: mimeType }); }; var arrayBufferConcat = function arrayBufferConcat(buffers) { var length = 0; var buffer = null; for (var i in buffers) { if (!_arguments.hasOwnProperty(i)) { continue; } buffer = buffers[i]; length += buffer.byteLength; } var joined = new Uint8Array(length); var offset = 0; for (var _i in buffers) { if (!_arguments.hasOwnProperty(_i)) { continue; } buffer = buffers[_i]; joined.set(new Uint8Array(buffer), offset); offset += buffer.byteLength; } return joined.buffer; }; var getImageAsCanvas = function getImageAsCanvas(src, size, 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 (size) { options.maxWidth = size.width; options.maxHeight = size.height; } 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) * 0.5; y = 0; } else { // if input is landscape and required is portrait // height is landscape height, width is height divided by outputRatio w = width; h = w * ratioOut; x = 0; y = (height - h) * 0.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, crop = transforms.crop, size = transforms.size, filters = transforms.filters, minSize = transforms.minSize; // 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 }; // limit crop to size of canvas else safari might return transparent image if (crop.x < 0) { crop.x = 0; } if (crop.y < 0) { crop.y = 0; } if (crop.width > space.width) { crop.width = space.width; } if (crop.height > space.height) { crop.height = space.height; } 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); } // 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; // 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 * 0.5, result.height * 0.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 * 0.5, -result.width * 0.5, result.height, result.width); } else if (rotation === 180) { ctx.translate(result.width * 0.5, result.height * 0.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 * 0.5, -result.height * 0.5, result.width, result.height); } else if (rotation === 270) { ctx.translate(result.width * 0.5, result.height * 0.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 * 0.5, -result.width * 0.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); scaleCanvas(result, scalar, size, minSize); // sharpen result if (filters.sharpen > 0) { filter(result, sharpen(filters.sharpen)); } } cb(result); }; function scaleCanvas(canvas, scalar, bounds, min) { // if not scaling down, bail out if (scalar >= 1) { return; } var w = canvas.width; var h = canvas.height; // calculate min target width and height var targetWidth = Math.max(min.width, Math.min(bounds.width, Math.round(canvas.width * scalar))); var targetHeight = Math.max(min.height, Math.min(bounds.height, Math.round(canvas.height * scalar))); var tmp = cloneCanvas(canvas); var c = void 0; var ctx = void 0; while (w > targetWidth && h > targetHeight) { c = document.createElement('canvas'); w = Math.round(tmp.width * 0.5); h = Math.round(tmp.height * 0.5); if (w < targetWidth) { w = targetWidth; } if (h < targetHeight) { h = targetHeight; } c.width = w; c.height = h; ctx = c.getContext('2d'); ctx.drawImage(tmp, 0, 0, w, h); tmp = cloneCanvas(c); } canvas.width = targetWidth; canvas.height = targetHeight; ctx = canvas.getContext('2d'); ctx.drawImage(tmp, 0, 0, targetWidth, targetHeight); } var getPixels = function getPixels(canvas) { var ctx = canvas.getContext('2d'); return ctx.getImageData(0, 0, canvas.width, canvas.height); }; var filter = function filter(canvas, _filter) { var ctx = canvas.getContext('2d'); ctx.putImageData(_filter(getPixels(canvas), canvas.width, canvas.height), 0, 0); }; var createImageData = function createImageData(w, h, pixels) { var c = document.createElement('canvas'); c.width = w; c.height = h; var ctx = c.getContext('2d'); var data = ctx.createImageData(c.width, c.height); if (pixels) { data.set(pixels.data); } return data; }; var sharpen = function sharpen(mix) { return function (pixels, w, h) { var weights = [0, -1, 0, -1, 5, -1, 0, -1, 0], katet = Math.round(Math.sqrt(weights.length)), half = katet * 0.5 | 0, dstData = createImageData(w, h), dstBuff = dstData.data, srcBuff = pixels.data, y = h, x = void 0; while (y--) { x = w; while (x--) { var sy = y, sx = x, dstOff = (y * w + x) * 4, r = 0, g = 0, b = 0, a = 0; for (var cy = 0; cy < katet; cy++) { for (var cx = 0; cx < katet; cx++) { var scy = sy + cy - half; var scx = sx + cx - half; if (scy >= 0 && scy < h && scx >= 0 && scx < w) { var srcOff = (scy * w + scx) * 4; var wt = weights[cy * katet + cx]; r += srcBuff[srcOff] * wt; g += srcBuff[srcOff + 1] * wt; b += srcBuff[srcOff + 2] * wt; a += srcBuff[srcOff + 3] * wt; } } } dstBuff[dstOff] = r * mix + srcBuff[dstOff] * (1 - mix); dstBuff[dstOff + 1] = g * mix + srcBuff[dstOff + 1] * (1 - mix); dstBuff[dstOff + 2] = b * mix + srcBuff[dstOff + 2] * (1 - mix); dstBuff[dstOff + 3] = srcBuff[dstOff + 3]; } } return dstData; }; }; 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, { width: Math.round(original.width * scalar), height: Math.round(original.height * scalar) }, { width: 0, height: 0 }); } 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 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 ? arguments[1] : null; if (!file) { return null; } var dupe = file.slice(0, file.size, type || file.type); dupe.name = file.name; dupe.lastModified = new Date(file.lastModified); 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 async = arguments[4]; 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('input', props) && !async) { data.input.image = toDataURL(obj.input.image, obj.input.type); } 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 downloadCanvas = function downloadCanvas(data, jpegCompression, forcedType) { var canvas = data.output.image; var filename = forcedType ? getFileNameWithoutExtension(data.input.name) + '.' + forcedType : data.input.name; var type = MimeTypes[forcedType] || data.input.type; // 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 (type === 'image/png') { filename = getFileNameWithoutExtension(data.input.name) + '.png'; } canvas.toBlob(function (blob) { if ('msSaveBlob' in window.navigator) { window.navigator.msSaveBlob(blob, filename); return; } var url = (window.URL || window.webkitURL).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); (window.URL || window.webkitURL).revokeObjectURL(url); }, 0); }, type, typeof jpegCompression === 'number' ? jpegCompression / 100 : undefined); }; 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) * 0.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 || Math.round(r) > Math.round(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) * 0.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 || Math.round(r) > Math.round(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) * 0.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 || Math.round(b) > Math.round(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) * 0.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 || Math.round(b) > Math.round(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 || Math.round(b) > Math.round(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 || Math.round(b) > Math.round(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 || Math.round(b) > Math.round(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 || Math.round(b) > Math.round(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 ? arguments[0] : document.createElement('div'); _classCallCheck(this, CropArea); this._element = element; this._interaction = null; this._minWidth = 1; this._minHeight = 1; 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; this._space.height = height; } }, { key: 'offset', value: function offset(x, y) { this._space.x = x; this._space.y = y; } }, { 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); this._interaction = { type: e.target.className, offset: getEventOffsetScroll(e) }; this._interaction.offset.x -= this._rect.x; this._interaction.offset.y -= this._rect.y; // 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 = getEventOffsetScroll(e); 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); } else if (resizers[type]) { // resize this._rect = resizers[type](this._rect, { x: offset.x - this._space.x, y: offset.y - this._space.y }, { 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) { var transform = 'translate(' + this._rect.x + 'px,' + this._rect.y + 'px);'; this._element.style.cssText = '\n\t\t\t\t\t-webkit-transform: ' + transform + ';\n\t\t\t\t\ttransform: ' + transform + ';\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 = Math.max(value, 1); } }, { key: 'minHeight', set: function set(value) { this._minHeight = Math.max(value, 1); } }, { 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 ? arguments[0] : document.createElement('div'); 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._rotating = false; this._input = null; this._preview = null; this._previewBlurred = null; this._blurredPreview = false; this._cropper = null; this._straightCrop = null; this._previewWrapper = null; this._currentWindowSize = {}; this._btnGroup = null; this._maskFrame = 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._previewMask = create('div', 'slim-crop-mask'); this._preview = create('img'); this._previewMask.appendChild(this._preview); this._cropper.element.appendChild(this._previewMask); // 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() { if (this._rotating) { return; } this._element.dispatchEvent(new CustomEvent('cancel')); } }, { key: '_confirm', value: function _confirm() { if (this._rotating) { return; } 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; if (this._rotating) { return; } this._rotating = true; // rotate! this._updateWrapperRotation(); // only pass current rect if is 1:1 or free var rect = this.ratio === 1 || this._ratio === null ? this._cropper.area : null; if (rect) { rotate(rect, 90); } // wrapper might also need to be scaled this._updateWrapperScale(); // 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: 0.8, springDeceleration: 0.65, complete: function complete() { // redraws auto cropper _this3._redrawCropper(rect); // shows the cropper _this3._showCropper(); // done rotating _this3._rotating = false; } }); } }, { 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: '_redrawCropMask', value: function _redrawCropMask() { var _this4 = this; // get rotation var rotation = this._wrapperRotation % 360; // get scale var scale = this._wrapperScale; // get image size var canvas = { width: this._wrapper.offsetWidth, height: this._wrapper.offsetHeight }; // get default mask cropper area var mask = this._cropper.area; var preview = { x: 0, y: 0 }; if (rotation === 0) { preview.x = -mask.x; preview.y = -mask.y; } else if (rotation === 90) { preview.x = -(1 - mask.y); preview.y = -mask.x; } else if (rotation === 180) { preview.x = -(1 - mask.x); preview.y = -(1 - mask.y); } else if (rotation === 270) { preview.x = -mask.y; preview.y = -(1 - mask.x); } // scale rect to fit canvas preview.x *= canvas.width; preview.y *= canvas.height; // update on next frame (so it's in sync with crop area redraw) cancelAnimationFrame(this._maskFrame); this._maskFrame = requestAnimationFrame(function () { var transform = 'scale(' + scale + ') rotate(' + -rotation + 'deg) translate(' + preview.x + 'px, ' + preview.y + 'px);'; _this4._preview.style.cssText = '\n\t\t\t\t\twidth: ' + _this4._previewSize.width + 'px;\n\t\t\t\t\theight: ' + _this4._previewSize.height + 'px;\n\t\t\t\t\t-webkit-transform: ' + transform + ';\n\t\t\t\t\ttransform: ' + transform + ';\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 && this._ratio === ratio) { complete(); return; } // remember current window size this._currentWindowSize = { width: window.innerWidth, height: window.innerHeight }; // 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; // reset preview size this._previewSize = null; // 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 = ''; this._preview.src = cloneCanvasScaled(this._input, Math.min(this._container.offsetWidth / this._input.width, this._container.offsetHeight / this._input.height) * this._options.devicePixelRatio).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) * 0.5 : this._wrapper.offsetLeft; var sy = isTilted ? (maxHeight - sh) * 0.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); this._cropper.offset(sx + this._element.offsetLeft, sy + this._element.offsetTop); // 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); } }, { 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.style.cssText = '\n\t\t\t\twidth:' + width + 'px;\n\t\t\t\theight:' + height + 'px;\n\t\t\t'; this._previewSize = { width: width, 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 ? arguments[0] : function () {}; // 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: 1, fromScale: [this._wrapperScale - 0.02, this._wrapperScale - 0.02], scale: [this._wrapperScale, this._wrapperScale], easing: 'spring', springConstant: 0.3, springDeceleration: 0.85, delay: 450, complete: function complete() { // don't reset transforms because we need rotation to stick } }); 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: 850, 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: [0.9, 0.9], scale: [1, 1], fromOpacity: 0, opacity: 1, delay: function delay(i) { return 1000 + i * 100; }, easing: 'spring', springConstant: 0.3, springDeceleration: 0.85, complete: function complete() { resetTransforms(this); } }); snabbt(this._utilsGroup.childNodes, { fromScale: [0.9, 0.9], scale: [1, 1], fromOpacity: 0, opacity: 1, easing: 'spring', springConstant: 0.3, springDeceleration: 0.85, delay: 1250, complete: function complete() { resetTransforms(this); } }); } }, { key: 'hide', value: function hide() { var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; // 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: 0.3, springDeceleration: 0.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(); // if still attached to DOM, detach if (this._element.parentNode) { 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._previewSize.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, devicePixelRatio: 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 = ['dragenter', 'dragover', 'dragleave', 'drop']; return function () { function FileHopper() { var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document.createElement('div'); _classCallCheck(this, FileHopper); this._element = element; this._accept = []; this._allowURLs = false; this._dragPath = null; this._init(); } // public properties _createClass(FileHopper, [{ key: 'isValidDataTransfer', value: function isValidDataTransfer(dataTransfer) { if (dataTransfer.files && dataTransfer.files.length) { return this.areValidDataTransferFiles(dataTransfer.files); } if (dataTransfer.items && dataTransfer.items.length) { return this.areValidDataTransferItems(dataTransfer.items); } return null; } }, { key: 'areValidDataTransferFiles', value: function areValidDataTransferFiles(files) { if (this._accept.length && files) { return this._accept.indexOf(files[0].type) !== -1; } return true; } }, { key: 'areValidDataTransferItems', value: function areValidDataTransferItems(items) { if (this._accept.length && items) { // is possibly dropping url if (this._allowURLs && items[0].kind === 'string') { return null; } // is http website so firefox will not return file type if (items[0].type && items[0].type.indexOf('application') === 0) { return null; } return this._accept.indexOf(items[0].type) !== -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 'dragenter': 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 if a url was dropped var remote = null; if (this._allowURLs) { var url = void 0; var meta = void 0; try { url = e.dataTransfer.getData('url'); meta = e.dataTransfer.getData('text/html'); } catch (e) { // nope nope nope (ie11 trouble) } if (meta && meta.length) { var parsed = meta.match(/src\s*=\s*"(.+?)"/); if (parsed) { remote = parsed[1]; } } else if (url && url.length) { remote = url; } } if (remote) { this._element.files = [{ remote: remote }]; } else { // nope, must have been a file // test type in older browsers var filesValidity = this.isValidDataTransfer(e.dataTransfer); if (!filesValidity) { // 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(); // set drop effect e.dataTransfer.dropEffect = 'copy'; // determin if is valid data var dataValidity = this.isValidDataTransfer(e.dataTransfer); // if validity is null is undetermined if (dataValidity !== null && !dataValidity) { // 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: 'allowURLs', set: function set(value) { this._allowURLs = value; } }, { 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); // prevent body scrolling on iOS 11.3 this._element.addEventListener('touchmove', function (e) { e.preventDefault(); }, true); } }, { key: 'show', value: function show() { var _this9 = this; var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; // 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 ? arguments[0] : function () {}; // 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); } }, { key: 'className', set: function set(value) { this._element.className = 'slim-popover' + (value === null ? '' : ' ' + value); } }]); 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' || element.nodeName === 'SPAN'; }; 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
\n\t\n\t\t\n\t\t\n\t\n
\n'; var SlimUploadStatusHTML = '\n
\n'; var stringToSize = function stringToSize(str) { var size = str.split(','); return { width: parseInt(size[0], 10), height: parseInt(size[1], 10) }; }; 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) { if (typeof this._options.forceSize === 'string') { this._options.forceSize = stringToSize(this._options.forceSize); } this._options.ratio = this._options.forceSize.width + ':' + this._options.forceSize.height; this._options.size = clone(this._options.forceSize); } // test for size set as string if (typeof this._options.size === 'string') { this._options.size = stringToSize(this._options.size); } // test for size set as string if (typeof this._options.minSize === 'string') { this._options.minSize = stringToSize(this._options.minSize); } // fix post formatting if is supplied as string if (typeof this._options.post === 'string') { this._options.post = this._options.post.split(',').map(function (part) { return part.trim(); }); } // 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'); // editor state this._state = []; // 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(); // the circle below the mouse hover point this._drip = null; // had initial image this._hasInitialImage = false; // initial crop this._initialCrop = this._options.crop; // initial rotation this._initialRotation = this._options.rotation && this._options.rotation % 90 === 0 ? this._options.rotation : null; // set to true when destroy method is called, used to halt any timeouts or async processes this._isBeingDestroyed = false; // stop here if not supported if (!Slim.supported) { this._fallback(); } else { // let's go! this._init(); } } _createClass(Slim, [{ key: 'setRotation', value: function setRotation(rotation, callback) { if (typeof rotation !== 'number' && rotation % 90 !== 0) { return; } this._data.actions.rotation = rotation; var isTilted = this._data.actions.rotation % 180 !== 0; if (this._data.input.image) { var w = isTilted ? this._data.input.image.height : this._data.input.image.width; var h = isTilted ? this._data.input.image.width : this._data.input.image.height; this._data.actions.crop = getAutoCropRect(w, h, this._ratio); this._data.actions.crop.type = CropType.AUTO; } if (this._data.input.image && callback) { this._manualTransform(callback); } } }, { key: 'setSize', value: function setSize(dimensions, callback) { if (typeof dimensions === 'string') { dimensions = stringToSize(dimensions); } if (!dimensions || !dimensions.width || !dimensions.height) { return; } this._options.size = clone(dimensions); this._data.actions.size = clone(dimensions); // if a crop area is set do re-crop to conform to size if (this._data.input.image && callback) { this._manualTransform(callback); } } }, { key: 'setForceSize', value: function setForceSize(dimensions, callback) { if (typeof dimensions === 'string') { dimensions = stringToSize(dimensions); } if (!dimensions || !dimensions.width || !dimensions.height) { return; } this._options.size = clone(dimensions); this._options.forceSize = clone(dimensions); this._data.actions.size = clone(dimensions); this.setRatio(this._options.forceSize.width + ':' + this._options.forceSize.height, callback); } }, { key: 'setRatio', value: function setRatio(ratio, callback) { var _this11 = this; 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]; if (this._data.input.image && callback) { this._cropAuto(function (data) { _this11._scaleDropArea(_this11._ratio); if (callback) { callback(data); } }); } else { if (this._data.input.image) { this._data.actions.crop = getAutoCropRect(this._data.input.image.width, this._data.input.image.height, this._ratio); this._data.actions.crop.type = CropType.AUTO; } this._scaleDropArea(this._ratio); if (callback) { callback(null); } } } } // methods // Test if this Slim object has been bound to the given element }, { key: 'isAttachedTo', 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 rotation this._options.rotation = options.rotation; // initial rotation this._initialRotation = options.rotation && options.rotation % 90 === 0 ? options.rotation : null; // set initial crop this._initialCrop = this._options.crop; } this._load(src, callback, { blockPush: options.blockPush }); } }, { 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); } }, { key: 'containsImage', value: function containsImage() { return this._data.input.name !== null; } /** * 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 _this12 = 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); } else { var initialData = null; try { initialData = JSON.parse(this._output.value); } catch (e) {} if (initialData) { var img = new Image(); img.src = initialData.output.image; img.setAttribute('data-filename', initialData.output.name); this._element.insertBefore(img, this._element.firstChild); } } // 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; var initialImageName = initialImage ? initialImage.getAttribute('data-filename') : null; if (initialImageSrc) { this._hasInitialImage = true; } else { this._initialCrop = null; this._initialRotation = null; } var resultHTML = '\n\t\t
\n\t\t\t\n\t\t
'; // 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\t
' + (this._options.labelLoading || '') + '
\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._imageHopper.allowURLs = typeof this._options.fetcher === 'string'; this._element.appendChild(this._imageHopper.element); HopperEvents.forEach(function (e) { _this12._imageHopper.element.addEventListener(e, _this12); }); // render area area.innerHTML = '\n\t\t\t\t' + SlimLoaderHTML + '\n\t\t\t\t' + SlimUploadStatusHTML + '\n\t\t\t\t
\n\t\t\t\t
' + (this._options.label || '') + '
' + (this._options.labelLoading || '') + '
\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 _this12._isButtonAllowed(c); }).forEach(function (c) { var prop = capitalizeFirstLetter(c); var label = _this12._options['button' + prop + 'Label']; var title = _this12._options['button' + prop + 'Title'] || label; var className = _this12._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', _this12); btn.setAttribute('data-action', c); _this12._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(0.5); // preload source if (initialImageSrc) { this._load(initialImageSrc, function () { _this12._onInit(); }, { name: initialImageName }); } else { this._onInit(); } } }, { key: '_onInit', value: function _onInit() { var _this13 = 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 () { _this13._options.didInit.apply(_this13, [_this13.data, _this13]); }, 0); _this13._timers.push(timer); }; // save initial image if (this._options.saveInitialImage && this.containsImage()) { if (!this._options.service) { this._save(function () { done(); }, false); } } else { // by default upload button is disabled for existing images if (this._options.service && this.containsImage()) { this._toggleButton('upload', false); } done(); } } }, { key: '_updateProgress', value: function _updateProgress(progress) { progress = Math.min(0.99999, progress); if (!this._element) { return; } if (!this._progressEnabled) { return; } var loader = this._element.querySelector('.slim-loader'); if (!loader) { return; } var size = loader.offsetWidth; var paths = loader.querySelectorAll('path'); var ringWidth = parseInt(paths[0].getAttribute('stroke-width'), 10); paths[0].setAttribute('d', percentageArc(size * 0.5, size * 0.5, size * 0.5 - ringWidth, 0.9999)); paths[1].setAttribute('d', percentageArc(size * 0.5, size * 0.5, size * 0.5 - ringWidth, progress)); } }, { key: '_startProgress', value: function _startProgress(cb) { var _this14 = 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'); _this14._progressEnabled = true; _this14._updateProgress(0); _this14._progressEnabled = false; snabbt(ring, { fromOpacity: 0, opacity: 1, duration: 250, complete: function complete() { _this14._progressEnabled = true; if (cb) { cb(); } } }); }); } }, { key: '_stopProgress', value: function _stopProgress() { var _this15 = 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'); _this15._updateProgress(0.5); _this15._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(0.5); // repeat! var repeat = 1000; snabbt(loader, 'stop'); 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'); loader.removeAttribute('style'); ring.removeAttribute('style'); 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() { // create status area var area = create('div', 'slim-area'); area.innerHTML = '\n\t\t\t
' + (this._options.label || '') + '
\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; this._options.didThrowError.apply(this, [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 _this16 = this; var list = e.target.classList; var target = e.target; // no preview, so possible to drop file if (list.contains('slim-file-hopper')) { e.preventDefault(); this._openFileDialog(); return; } // decide what button was clicked switch (target.getAttribute('data-action')) { case 'remove': this._options.willRemove.apply(this, [this.data, function () { _this16._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: [0.5, 0.5], scale: [0, 0], fromOpacity: 0.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: '_onLocalURLProblem', value: function _onLocalURLProblem(error) { this._throwError(this._options.statusLocalUrlProblem || error); } }, { key: '_onRemoteURLProblem', value: function _onRemoteURLProblem(error) { 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: 0.5, fromScale: [0, 0], scale: [0.5, 0.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: [0.5, 0.5], scale: [0, 0], fromOpacity: 0.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 _this17 = 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: [0.5, 0.5], scale: [2, 2], fromOpacity: 1, opacity: 0, duration: 200, complete: function complete() { // clean up transforms resetTransforms(drip.firstChild); // load dropped resource _this17._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) { if (e.target.files.length) { this._load(e.target.files[0]); } } /** * Loads a resource (blocking operation) * @param resource * @param callback(err) * @param options * @private */ }, { key: '_load', value: function _load(resource, callback) { var _this18 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; // stop here if (this._isBeingDestroyed) { return; } // if currently contains image, remove it if (this.containsImage()) { clearTimeout(this._replaceTimeout); this._doRemove(function () { _this18._replaceTimeout = setTimeout(function () { _this18._load(resource, callback, options); }, 100); }); return; } // no longer empty this._removeState('empty'); this._addState('busy'); // start loading indicator this._startProgressLoop(); // can't drop any other image while loading if (this._imageHopper) { this._imageHopper.enabled = false; } clearTimeout(this._loadTimeout); var load = function load() { clearTimeout(_this18._loadTimeout); _this18._loadTimeout = setTimeout(function () { if (_this18._isBeingDestroyed) { return; } _this18._addState('loading'); snabbt(_this18._element.querySelector('.slim-label-loading'), { fromOpacity: 0, opacity: 1, duration: 250 }); }, 500); }; // early exit var exit = function exit() { if (_this18._imageHopper) { _this18._imageHopper.enabled = true; } _this18._removeState('loading'); _this18._removeState('busy'); _this18._addState('empty'); _this18._stopProgressLoop(); }; // 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, options); } else { // will take a while, show loading indicator load(); // resource is url, load with XHR loadURL(resource, this._options.willLoad, function (file) { // continue with file object _this18._load(file, callback, options); }, function (error) { setTimeout(function () { exit(); _this18._onLocalURLProblem('

' + error + '

'); if (callback) { callback.apply(_this18, ['local-url-problem']); } }, 500); }); } // don't continue, wait for load return; } else if (typeof resource.remote !== 'undefined') { // is dropped link // test if happens to be base64 data if (resourceIsBase64Data(resource.remote)) { this._load(base64ToBlob(resource.remote), callback, options); return; } if (this._options.fetcher) { loadRemoteURL(this._options.fetcher, this._options.willFetch, this._options.willLoad, resource.remote, function (error) { exit(); _this18._onRemoteURLProblem('

' + error + '

'); if (callback) { callback.apply(_this18, ['remote-url-problem']); } }, function (file) { // continue with file object _this18._load(file, callback, options); }); } // don't continue wait for server fetch 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) { exit(); 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) { exit(); this._onOverWeightFile(); if (callback) { callback.apply(this, ['file-too-big']); } return; } // if has loaded image editor set to dirty if (this._imageEditor) { this._imageEditor.dirty(); } // continue this._data.input.name = options && options.name ? options.name : getFileNameByFile(file); this._data.input.type = getFileTypeByFile(file); this._data.input.size = file.size; this._data.input.file = file; // fetch resource getImageAsCanvas(file, this._options.internalCanvasSize, function (image, meta) { var rewind = function rewind() { // rewind state if (_this18._imageHopper) { _this18._imageHopper.enabled = true; } _this18._removeState('loading'); _this18._removeState('busy'); _this18._addState('empty'); _this18._stopProgressLoop(); _this18._resetData(); }; // if no image, something went wrong if (!image) { rewind(); if (callback) { callback.apply(_this18, ['file-not-found']); } return; } // test if image is too small if (!covers(image, _this18._options.minSize)) { rewind(); _this18._onImageTooSmall(); if (callback) { callback.apply(_this18, ['image-too-small']); } return; } var status = _this18._options.didLoad.apply(_this18, [file, image, meta, _this18]); if (status !== true) { rewind(); if (status !== false) { _this18._throwError(status); } if (callback) { callback.apply(_this18, [status]); } return; } // done loading file _this18._removeState('loading'); var revealCanvas = function revealCanvas(done) { // done, enable hopper if (_this18._imageHopper && _this18._options.dropReplace) { _this18._imageHopper.enabled = true; } // do intro stuff var intro = _this18._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; done(); } }; // if not attached to DOM, don't animate if (_this18.isDetached()) { animation.duration = 1; } else { animation.easing = 'spring'; animation.springConstant = 0.3; animation.springDeceleration = 0.7; } // if is instant edit mode don't zoom out but zoom in if (_this18._canInstantEdit()) { animation.delay = 500; animation.duration = 1; // instant edit mode just fire up the editor immidiately _this18._doEdit(); } // reveal loaded image snabbt(intro, animation); }; // load the image _this18._loadCanvas(image, // done loading the canvas function (isUploading) { _this18._addState('preview'); revealCanvas(function () { // don't show buttons when instant editing // the buttons will be triggered by the closing of the popup if (!_this18._canInstantEdit() && !isUploading) { _this18._showButtons(); } if (!isUploading) { _this18._stopProgressLoop(); _this18._removeState('busy'); } if (callback) { callback.apply(_this18, [null, _this18.data]); } }); }, // done uploading function () { // don't show buttons when instant editing if (!_this18._canInstantEdit()) { _this18._showButtons(); } _this18._removeState('busy'); }, // options for canvas load { blockPush: options.blockPush }); }); } }, { key: '_loadCanvas', value: function _loadCanvas(image, ready, complete, options) { var _this19 = this; // set default options object if not supplied if (!options) { options = {}; } // halt here if cropper is currently being destroyed if (this._isBeingDestroyed) { return; } // store raw data this._data.input.image = image; this._data.input.width = image.width; this._data.input.height = image.height; if (this._initialRotation) { this._data.actions.rotation = this._initialRotation; this._initialRotation = null; } var isTilted = this._data.actions.rotation % 180 !== 0; // scales the drop area // if is 'input' or 'free' parameter if (!this._isFixedRatio()) { if (this._initialCrop) { this._ratio = this._initialCrop.height / this._initialCrop.width; } else { this._ratio = isTilted ? image.width / image.height : image.height / image.width; } this._scaleDropArea(this._ratio); } var applyTransforms = function applyTransforms() { // if max size set if (_this19._options.size) { _this19._data.actions.size = { width: _this19._options.size.width, height: _this19._options.size.height }; } // do initial auto transform _this19._applyTransforms(image, function (transformedImage) { var intro = _this19._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 (_this19._options.service && _this19._options.push && !options.blockPush) { // ...and is not transformation of initial image // + is not instant edit mode if (!_this19._hasInitialImage && !_this19._canInstantEdit()) { willUpload = true; _this19._stopProgressLoop(function () { _this19._startProgress(function () { _this19._updateProgress(0.1); }); }); } } // no service set, and instant edit if (!_this19._canInstantEdit()) { // store data (possibly) _this19._save(function () { if (_this19._isBeingDestroyed) { return; } if (willUpload) { _this19._stopProgress(); complete(); } }, willUpload); } // scale ratio var ratio = _this19._options.devicePixelRatio === 'auto' ? window.devicePixelRatio : _this19._options.devicePixelRatio; // show intro animation intro.src = ''; intro.src = cloneCanvasScaled(transformedImage, scalar * ratio).toDataURL(); intro.onload = function () { intro.onload = null; // bail out if we've been cleaned up if (_this19._isBeingDestroyed) { return; } if (ready) { ready(willUpload); } }; }); }; 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; // go! applyTransforms(); } else { this._options.willCropInitial.apply(this, [this.data, function (rect) { if (rect) { // apply custom crop rectangle _this19._data.actions.crop = rect; _this19._data.actions.crop.type = CropType.INITIAL; } else { // get automagical crop rectangle _this19._data.actions.crop = getAutoCropRect(isTilted ? image.height : image.width, isTilted ? image.width : image.height, _this19._ratio); _this19._data.actions.crop.type = CropType.AUTO; } applyTransforms(); }, this]); } } }, { key: '_applyTransforms', value: function _applyTransforms(image, ready) { var _this20 = this; var actions = clone(this._data.actions); actions.filters = { sharpen: this._options.filterSharpen / 100 }; // if should force minimum size on output image if (this._options.forceMinSize) { actions.minSize = this._options.minSize; } else { actions.minSize = { width: 0, height: 0 }; } transformCanvas(image, 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 (_this20._options.forceSize || _this20._options.size && sizeDist(_this20._options.size, transformedImage) == 1) { outputImage = create('canvas'); outputImage.width = _this20._options.size.width; outputImage.height = _this20._options.size.height; var ctx = outputImage.getContext('2d'); ctx.drawImage(transformedImage, 0, 0, _this20._options.size.width, _this20._options.size.height); } // make sure min size is respected when size is equal to min size if (_this20._options.forceMinSize && _this20._options.size && _this20._options.minSize.width === _this20._options.size.width && _this20._options.minSize.height === _this20._options.size.height && (outputImage.width < _this20._options.minSize.width || outputImage.height < _this20._options.minSize.height)) { var w = Math.max(outputImage.width, _this20._options.minSize.width); var h = Math.max(outputImage.height, _this20._options.minSize.height); outputImage = create('canvas'); outputImage.width = w; outputImage.height = h; var _ctx = outputImage.getContext('2d'); _ctx.drawImage(transformedImage, 0, 0, w, h); } // prevent smaller square output image, this is a quick fix for square images, should be done a lot neater if (_this20._options.forceMinSize && _this20._ratio === 1 && (outputImage.width < _this20._options.minSize.width || outputImage.height < _this20._options.minSize.height)) { outputImage = create('canvas'); outputImage.width = _this20._options.minSize.width; outputImage.height = _this20._options.minSize.height; var _ctx2 = outputImage.getContext('2d'); _ctx2.drawImage(transformedImage, 0, 0, outputImage.width, outputImage.height); } // store output _this20._data.output.width = outputImage.width; _this20._data.output.height = outputImage.height; _this20._data.output.image = outputImage; _this20._onTransformCanvas(function (transformedData) { _this20._data = transformedData; _this20._options.didTransform.apply(_this20, [_this20.data, _this20]); ready(_this20._data.output.image); }); }); } }, { key: '_onTransformCanvas', value: function _onTransformCanvas(ready) { this._options.willTransform.apply(this, [this.data, ready, this]); } /** * Creates the editor nodes * @private */ }, { key: '_appendEditor', value: function _appendEditor() { var _this21 = this; // we already have an editor if (this._imageEditor) { return; } // add editor this._imageEditor = new ImageEditor(create('div'), { minSize: this._options.minSize, devicePixelRatio: this._options.devicePixelRatio, 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) { _this21._imageEditor.element.addEventListener(e, _this21); }); } }, { 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._options.didCancel.apply(this, [this]); this._showButtons(); this._hideEditor(); if (this._options.instantEdit && !this._hasInitialImage && this._isAutoCrop()) { this._doRemove(); } } // user confirmed changes }, { key: '_onConfirm', value: function _onConfirm(e) { var _this22 = this; // if // - service set // - and we are pushing // - and we don't instant edit // we will upload var willUpload = this._options.service && this._options.push; if (willUpload) { this._startProgress(function () { _this22._updateProgress(0.1); }); } else { this._startProgressLoop(); } this._removeState('editor'); 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) { // user confirmed the crop (and changes have been applied to data) _this22._options.didConfirm.apply(_this22, [_this22.data, _this22]); // 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 = '0'; intro.style.zIndex = '2'; outro.className = 'out'; outro.style.zIndex = '1'; // scale ratio var ratio = _this22._options.devicePixelRatio === 'auto' ? window.devicePixelRatio : _this22._options.devicePixelRatio; // new image get's intro.src = ''; intro.src = cloneCanvasScaled(transformedImage, intro.offsetWidth / transformedImage.width * ratio).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); } // close the editor _this22._hideEditor(); // wait a tiny bit so animations sync up nicely var timer = setTimeout(function () { // show the preview _this22._showPreview(intro, function () { // save the data _this22._save(function (err, data, res) { // done! _this22._toggleButton('upload', true); if (willUpload) { _this22._stopProgress(); } else { _this22._stopProgressLoop(); } _this22._removeState('busy'); _this22._showButtons(); }, willUpload); }); }, 250); _this22._timers.push(timer); }; }); } }, { key: '_cropAuto', value: function _cropAuto() { var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function (data) {}; var isTilted = this._data.actions.rotation % 180 !== 0; var rect = getAutoCropRect(isTilted ? this._data.input.image.height : this._data.input.image.width, isTilted ? this._data.input.image.width : this._data.input.image.height, this._ratio); this._crop(rect.x, rect.y, rect.width, rect.height, callback, CropType.AUTO); } }, { key: '_crop', value: function _crop(x, y, width, height) { var callback = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : function (data) {}; var cropType = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : CropType.MANUAL; // 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; this._manualTransform(callback); } }, { key: '_manualTransform', value: function _manualTransform(callback) { var _this23 = this; this._startProgressLoop(); this._addState('busy'); // do transforms this._applyTransforms(this._data.input.image, function (transformedImage) { // set new image result var images = _this23._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'; // scale ratio var pixelRatio = _this23._options.devicePixelRatio === 'auto' ? window.devicePixelRatio : _this23._options.devicePixelRatio; // new image intro.src = ''; intro.src = cloneCanvasScaled(transformedImage, intro.offsetWidth / transformedImage.width * pixelRatio).toDataURL(); intro.onload = function () { intro.onload = null; // scale the dropzone if (_this23._options.ratio === 'free') { _this23._ratio = intro.naturalHeight / intro.naturalWidth; _this23._scaleDropArea(_this23._ratio); } // determine if will also upload var willUpload = _this23._options.service && _this23._options.push; var save = function save() { // save the data _this23._save(function (err, data, res) { // stop loader if (!willUpload) { _this23._stopProgressLoop(); } _this23._removeState('busy'); callback.apply(_this23, [_this23.data]); }, willUpload); }; if (willUpload) { _this23._startProgress(save); } else { save(); } }; }); } }, { key: '_save', value: function _save() { var _this24 = this; var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; var allowUpload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 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) { _this24._store(data); _this24._options.didSave.apply(_this24, [data, _this24]); }, this]); } 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) { _this24._addState('upload'); if (_this24._imageHopper && _this24._options.dropReplace) { _this24._imageHopper.enabled = false; } // do the actual uploading _this24._upload(data, function (err, res) { if (_this24._imageHopper && _this24._options.dropReplace) { _this24._imageHopper.enabled = true; } // store response if (!err) { _this24._storeServerResponse(res); } // we did upload data _this24._options.didUpload.apply(_this24, [err, data, res, _this24]); _this24._removeState('upload'); // done! callback(err, data, res); }); }, this]); } // 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 // function(error) {} // error message (string), _this25 = this; this.requestOutput(function (fileData, formData) { var statusNode = _this25._element.querySelector('.slim-upload-status'); var requestDecorator = _this25._options.willRequest; // callback methods var onProgress = function onProgress(loaded, total) { _this25._updateProgress(Math.max(0.1, loaded / total)); }; var onSuccess = function onSuccess(obj) { var timer = setTimeout(function () { // it's possible that Slim has been destroyed in the mean time. if (_this25._isBeingDestroyed) { return; } statusNode.innerHTML = _this25._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); _this25._timers.push(timer); }, 250); _this25._timers.push(timer); callback(null, obj); }; var onError = function onError(status) { var html = ''; if (status === 'file-too-big') { html = _this25._options.statusContentLength; } else { html = _this25._options.didReceiveServerError.apply(_this25, [status, _this25._options.statusUnknownResponse, _this25]); } // 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); _this25._timers.push(timer); callback(status); }; // use default send method or custom implementation if (typeof _this25._options.service === 'string') { send(_this25._options.service, _this25._options.uploadMethod, formData, requestDecorator, onProgress, onSuccess, onError); } else if (typeof _this25._options.service === 'function') { _this25._options.service.apply(_this25, [_this25._options.serviceFormat === 'file' ? fileData : formData, onProgress, // function(loaded, total) {} // loaded bytes (number), total bytes (number) onSuccess, // function(response) {} // response (object or string) onError, _this25]); } }, data); } }, { key: 'requestOutput', value: function requestOutput(cb, data) { var _this26 = this; if (!this._data.input.file) { cb(null, null); return; } if (!data) { data = this.dataBase64; } // copy the meta data of the original file to the output loadImage.parseMetaData(this._data.input.file, // receives image data from input file function (imageData) { var fileData = []; var formData = new FormData(); // if input should be posted along, append data // to FormData object as file if (inArray('input', _this26._options.post)) { // add to data array fileData.push(_this26._data.input.file); // add to formdata formData.append(_this26._inputReference, _this26._data.input.file, _this26._data.input.file.name); } // if image data is defined, turn it into a file object (we can send files if we're uploading) if (inArray('output', _this26._options.post) && _this26._data.output.image !== null && _this26._options.uploadBase64 === false) { var output = base64ToBlob(data.output.image, data.output.name); // if image head available, inject in output if (imageData.imageHead && _this26._options.copyImageHead) { try { output = new Blob([imageData.imageHead, loadImage.blobSlice.call(output, 20)], { type: getMimeTypeFromDataURI(data.output.image) }); output = blobToFile(output, data.output.name); } catch (e) {} } // add to data array fileData.push(output); // add to formdata var field = 'slim_output_' + _this26._uid; data.output.image = null; // clear base64 data data.output.field = field; formData.append(field, output, data.output.name); } // output dataset formData.append(_this26._output.name, JSON.stringify(data)); // done cb(fileData, formData); }, { maxMetaDataSize: 262144, disableImageHead: false }); } }, { key: '_showEditor', value: function _showEditor() { SlimPopover.className = this._options.popoverClassName; 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: 0.3, springDeceleration: 0.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: [0.5, 0.5], fromOpacity: 1, opacity: 0, easing: 'spring', springConstant: 0.3, springDeceleration: 0.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: [0.5, 0.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 = 0.3; animation.springDeceleration = 0.85; } snabbt(this._btnGroup.childNodes, animation); } }, { key: '_hideButtons', value: function _hideButtons(callback) { var _this27 = this; if (!this._btnGroup) { return; } var animation = { fromScale: [1, 1], scale: [0.85, 0.85], fromOpacity: 1, opacity: 0, allDone: function allDone() { _this27._btnGroup.style.display = 'none'; if (callback) { callback(); } } }; // don't animate when detached if (this.isDetached()) { animation.duration = 1; } else { animation.easing = 'spring'; animation.springConstant = 0.3; animation.springDeceleration = 0.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 _this28 = 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 () { _this28._showEditor(); _this28._hideButtons(); _this28._hideStatus(); }); } }, { key: '_doRemove', value: function _doRemove(done) { var _this29 = this; // cannot remove when is only one image if (this._isImageOnly()) { return; } this._clearState(); this._addState('empty'); this._hasInitialImage = false; if (this._imageHopper) { 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 (_this29._isBeingDestroyed) { return; } _this29._hideButtons(function () { _this29._toggleButton('upload', true); }); _this29._hideStatus(); _this29._hideResult(); _this29._options.didRemove.apply(_this29, [data, _this29]); if (done) { done(); } }, this.isDetached() ? 0 : 250); this._timers.push(timer); return data; } }, { key: '_doUpload', value: function _doUpload(callback) { var _this30 = 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 _this30._toggleButton('upload', false); _this30._save(function (err, data, res) { _this30._removeState('upload'); _this30._stopProgress(); if (callback) { callback.apply(_this30, [err, data, res]); } if (err) { _this30._toggleButton('upload', true); } _this30._showButtons(); }); }); } }, { key: '_doDownload', value: function _doDownload() { var image = this._data.output.image; if (!image) { return; } downloadCanvas(this._data, this._options.jpegCompression, this._options.forceType); } }, { key: '_doDestroy', value: function _doDestroy() { var _this31 = 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) { _this31._imageHopper.element.removeEventListener(e, _this31); }); this._imageHopper.destroy(); this._imageHopper = null; } // this block removes the image editor if (this._imageEditor) { ImageEditorEvents.forEach(function (e) { _this31._imageEditor.element.removeEventListener(e, _this31); }); this._imageEditor.destroy(); this._imageEditor = null; } // remove button event listeners nodeListToArray(this._btnGroup.children).forEach(function (btn) { btn.removeEventListener('click', _this31); }); // 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, _this31._originalElementAttributes)) { return; } // else remove _this31._originalElement.removeAttribute(attribute.name); }); this._originalElementAttributes.forEach(function (attribute) { // attribute was never removed if (matchesAttributeInList(attribute, attributes)) { return; } // add attribute _this31._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, this._options.service !== null); } }, { key: 'data', get: function get() { return cloneData(this._data); } }, { key: 'element', get: function get() { return this._element; } }, { key: 'service', set: function set(service) { this._options.service = service; } }, { key: 'size', set: function set(dimensions) { this.setSize(dimensions, null); } }, { key: 'rotation', set: function set(rotation) { this.setRotation(rotation, null); } }, { key: 'forceSize', set: function set(dimensions) { this.setForceSize(dimensions, null); } }, { key: 'ratio', set: function set(ratio) { this.setRatio(ratio, null); } }], [{ key: 'options', value: function options() { var defaults = { // edit button is enabled by default edit: true, // immidiately summons editor on load instantEdit: false, // set to true to upload data as base64 string uploadBase64: false, // metadata values meta: {}, // ratio of crop by default is the same as input image ratio ratio: 'free', // use fix value for device pixel ratio devicePixelRatio: 1, // set to 'auto' to use device ratio // dimensions to resize the resulting image to size: null, // set initial rotation rotation: 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, serviceFormat: null, // sharpen filter value, really low values might improve image output filterSharpen: 0, // 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: 0, height: 0 }, // maximum file size in MB to upload maxFileSize: null, // compression of JPEG (between 0 and 100) jpegCompression: null, // upload method of request uploadMethod: 'POST', // 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, forceMinSize: true, // disable drop to replace dropReplace: true, // remote URL service fetcher: null, // set the internal canvas size internalCanvasSize: { width: 4096, height: 4096 }, // copies the input image meta data to the output image copyImageHead: false, // enable or disable rotation rotateButton: true, // popover classname popoverClassName: null, // label HTML to show inside drop area label: '

Drop your image here

', labelLoading: '

Loading image...

', // error messages statusFileType: '

Invalid file type, expects: $0.

', statusFileSize: '

File is too big, maximum file size: $0 MB.

', statusNoSupport: '

Your browser does not support image cropping.

', statusImageTooSmall: '

Image is too small, minimum size is: $0 pixels.

', statusContentLength: ' The file is probably too big', statusUnknownResponse: ' An unknown error occurred', statusUploadSuccess: ' Saved', statusLocalUrlProblem: null, // callback methods didInit: function didInit(data) {}, didLoad: function didLoad(file, image, meta) { return true; }, didSave: function didSave(data) {}, didUpload: function didUpload(err, data, res) {}, didReceiveServerError: function didReceiveServerError(err, defaultError) { return defaultError; }, didRemove: function didRemove(data) {}, didTransform: function didTransform(data) {}, didConfirm: function didConfirm(data) {}, didCancel: function didCancel() {}, didThrowError: function didThrowError() {}, willCropInitial: function willCropInitial(file, cb) { cb(null); }, willTransform: function willTransform(data, cb) { cb(data); }, willSave: function willSave(data, cb) { cb(data); }, willRemove: function willRemove(data, cb) { cb(); }, willRequest: function willRequest(xhr, data) {}, willFetch: function willFetch(xhr) {}, willLoad: function willLoad(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 '

' + v + '

'; } // 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, forceMinSize: defaultTrue, // the internal data canvas size internalCanvasSize: defaultSize, // url to post to service: function service(v) { if (typeof v === 'undefined') { return null; } var fn = toFunctionReference(v); if (fn) { return fn; } return v; }, // format of service data serviceFormat: function serviceFormat(v) { return typeof v === 'undefined' ? null : v; }, // url to fetch service fetcher: function fetcher(v) { return typeof v === 'undefined' ? null : v; }, // set auto push mode push: defaultFalse, // initial rotation rotation: function rotation(v) { return typeof v === 'undefined' ? null : parseInt(v, 10); }, // 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, // sharpen filter filterSharpen: toInt, // jpeg compression jpegCompression: toInt, // base64 data uploading uploadBase64: defaultFalse, // sets file type to force output to forceType: passThrough, // drop to replace dropReplace: defaultTrue, // bool determining if initial image should be saved saveInitialImage: defaultFalse, // copies input image head to output image copyImageHead: defaultFalse, // rotate button rotateButton: defaultTrue, // default labels label: defaultLabel, labelLoading: defaultLabel, // class name to put on popover element popoverClassName: passThrough, // the ratio for pixels devicePixelRatio: passThrough, // upload request method uploadMethod: passThrough }; // labels ['FileSize', 'FileType', 'NoSupport', 'ImageTooSmall'].forEach(function (status) { defaults['status' + status] = defaultLabel; }); // status ['ContentLength', 'UnknownResponse', 'UploadSuccess', 'localUrlProblem'].forEach(function (status) { defaults['status' + status] = passThrough; }); // the did callbacks ['Init', 'Load', 'Save', 'Upload', 'Remove', 'Transform', 'ReceiveServerError', 'Confirm', 'Cancel', 'ThrowError'].forEach(function (cb) { defaults['did' + cb] = defaultFunction; }); // the will callbacks ['CropInitial', 'Transform', 'Save', 'Remove', 'Request', 'Load', 'Fetch'].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 !( // is opera mini Object.prototype.toString.call(window.operamini) === '[object OperaMini]' || // no event listener support typeof window.addEventListener === 'undefined' || // no file reader support typeof window.FileReader === 'undefined' || // no blob slicing (can't dupe files) !('slice' in Blob.prototype) || // no .createObjectURL support, used by download method but also convenient to exclude Android 4.3 and lower // Android 4.3 and lower don't support XHR2 responseType blob typeof window.URL === 'undefined' || typeof window.URL.createObjectURL === '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; }());