(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
		typeof define === 'function' && define.amd ? define(['exports'], factory) :
			(factory((global.SpriteSpin = {})));
}(this, (function (exports) { 'use strict';

	/**
	 * @internal
	 */
	var Api = /** @class */ (function () {
		function Api(data) {
			this.data = data;
		}
		return Api;
	}());
	/**
	 * Adds methods to the SpriteSpin api
	 *
	 * @public
	 */
	function extendApi(methods) {
		var api = Api.prototype;
		for (var key in methods) {
			if (methods.hasOwnProperty(key)) {
				if (api[key]) {
					throw new Error('API method is already defined: ' + key);
				}
				else {
					api[key] = methods[key];
				}
			}
		}
		return api;
	}

	var $$1 = window.jQuery || window.$;

	function getCursorPosition(event) {
		var touches = event.touches;
		var source = event;
		// jQuery Event normalization does not preserve the 'event.touches'
		// try to grab touches from the original event
		if (event.touches === undefined && event.originalEvent !== undefined) {
			touches = event.originalEvent.touches;
		}
		// get current touch or mouse position
		if (touches !== undefined && touches.length > 0) {
			source = touches[0];
		}
		return {
			x: source.clientX || 0,
			y: source.clientY || 0
		};
	}

	var canvas;
	var context;
	function detectionContext() {
		if (context) {
			return context;
		}
		if (!canvas) {
			canvas = document.createElement('canvas');
		}
		if (!canvas || !canvas.getContext) {
			return null;
		}
		context = canvas.getContext('2d');
		return context;
	}
	/**
	 * Idea taken from https://github.com/stomita/ios-imagefile-megapixel
	 * Detects whether the image has been sub sampled by the browser and does not have its original dimensions.
	 * This method unfortunately does not work for images that have transparent background.
	 */
	function detectSubsampling(img, width, height) {
		if (!detectionContext()) {
			return false;
		}
		// sub sampling happens on images above 1 megapixel
		if (width * height <= 1024 * 1024) {
			return false;
		}
		// set canvas to 1x1 pixel size and fill it with magenta color
		canvas.width = canvas.height = 1;
		context.fillStyle = '#FF00FF';
		context.fillRect(0, 0, 1, 1);
		// render the image with a negative offset to the left so that it would
		// fill the canvas pixel with the top right pixel of the image.
		context.drawImage(img, -width + 1, 0);
		// check color value to confirm image is covering edge pixel or not.
		// if color still magenta, the image is assumed to be sub sampled.
		try {
			var dat = context.getImageData(0, 0, 1, 1).data;
			return (dat[0] === 255) && (dat[1] === 0) && (dat[2] === 255);
		}
		catch (err) {
			// avoids cross origin exception for chrome when code runs without a server
			return false;
		}
	}

	/**
	 *
	 */
	function getOuterSize(data) {
		var width = Math.floor(data.width || data.frameWidth || data.target.innerWidth());
		var height = Math.floor(data.height || data.frameHeight || data.target.innerHeight());
		return {
			aspect: width / height,
			height: height,
			width: width
		};
	}
	function getComputedSize(data) {
		var size = getOuterSize(data);
		if (typeof window.getComputedStyle !== 'function') {
			return size;
		}
		var style = window.getComputedStyle(data.target[0]);
		if (!style.width) {
			return size;
		}
		size.width = Math.floor(Number(style.width.replace('px', '')));
		size.height = Math.floor(size.width / size.aspect);
		return size;
	}
	/**
	 *
	 */
	function getInnerSize(data) {
		var width = Math.floor(data.frameWidth || data.width || data.target.innerWidth());
		var height = Math.floor(data.frameHeight || data.height || data.target.innerHeight());
		return {
			aspect: width / height,
			height: height,
			width: width
		};
	}
	/**
	 *
	 */
	function getInnerLayout(mode, inner, outer) {
		// get mode
		var isFit = mode === 'fit';
		var isFill = mode === 'fill';
		var isMatch = mode === 'stretch';
		// resulting layout
		var layout = {
			width: '100%',
			height: '100%',
			top: 0,
			left: 0,
			bottom: 0,
			right: 0,
			position: 'absolute',
			overflow: 'hidden'
		};
		// no calculation here
		if (!mode || isMatch) {
			return layout;
		}
		// get size and aspect
		var aspectIsGreater = inner.aspect >= outer.aspect;
		// mode == original
		var width = inner.width;
		var height = inner.height;
		// keep aspect ratio but fit/fill into container
		if (isFit && aspectIsGreater || isFill && !aspectIsGreater) {
			width = outer.width;
			height = outer.width / inner.aspect;
		}
		if (isFill && aspectIsGreater || isFit && !aspectIsGreater) {
			height = outer.height;
			width = outer.height * inner.aspect;
		}
		// floor the numbers
		width = Math.floor(width);
		height = Math.floor(height);
		// position in center
		layout.width = width;
		layout.height = height;
		layout.top = Math.floor((outer.height - height) / 2);
		layout.left = Math.floor((outer.width - width) / 2);
		layout.right = layout.left;
		layout.bottom = layout.top;
		return layout;
	}

	var img;
	/**
	 * gets the original width and height of an image element
	 */
	function naturalSize(image) {
		// for browsers that support naturalWidth and naturalHeight properties
		if (image.naturalWidth) {
			return {
				height: image.naturalHeight,
				width: image.naturalWidth
			};
		}
		// browsers that do not support naturalWidth and naturalHeight properties have to fall back to the width and
		// height properties. However, the image might have a css style applied so width and height would return the
		// css size. To avoid thet create a new Image object that is free of css rules and grab width and height
		// properties
		//
		// assume that the src has already been downloaded, so no onload callback is needed.
		img = img || new Image();
		img.src = image.src;
		return {
			height: img.height,
			width: img.width
		};
	}

	/**
	 * Measures the image frames that are used in the given data object
	 */
	function measure(images, options) {
		if (images.length === 1) {
			return [measureSheet(images[0], options)];
		}
		else if (options.framesX && options.framesY) {
			return measureMutipleSheets(images, options);
		}
		else {
			return measureFrames(images, options);
		}
	}
	function measureSheet(image, options) {
		var result = { id: 0, sprites: [] };
		measureImage(image, options, result);
		var frames = options.frames;
		var framesX = Number(options.framesX) || frames;
		var framesY = Math.ceil(frames / framesX);
		var frameWidth = Math.floor(result.width / framesX);
		var frameHeight = Math.floor(result.height / framesY);
		var divisor = result.isSubsampled ? 2 : 1;
		for (var i = 0; i < frames; i++) {
			var x = (i % framesX) * frameWidth;
			var y = Math.floor(i / framesX) * frameHeight;
			result.sprites.push({
				id: i,
				x: x, y: y,
				width: frameWidth,
				height: frameHeight,
				sampledX: x / divisor,
				sampledY: y / divisor,
				sampledWidth: frameWidth / divisor,
				sampledHeight: frameHeight / divisor
			});
		}
		return result;
	}
	function measureFrames(images, options) {
		var result = [];
		for (var id = 0; id < images.length; id++) {
			// TODO: optimize
			// dont measure images with same size twice
			var sheet = measureSheet(images[id], { frames: 1, framesX: 1, detectSubsampling: options.detectSubsampling });
			sheet.id = id;
			result.push(sheet);
		}
		return result;
	}
	function measureMutipleSheets(images, options) {
		var result = [];
		for (var id = 0; id < images.length; id++) {
			// TODO: optimize
			// dont measure images with same size twice
			var sheet = measureSheet(images[id], {
				frames: undefined,
				framesX: options.framesX,
				framesY: options.framesY,
				detectSubsampling: options.detectSubsampling
			});
			sheet.id = id;
			result.push(sheet);
		}
		return result;
	}
	function measureImage(image, options, result) {
		var size = naturalSize(image);
		result.isSubsampled = options.detectSubsampling && detectSubsampling(image, size.width, size.height);
		result.width = size.width;
		result.height = size.height;
		result.sampledWidth = size.width / (result.isSubsampled ? 2 : 1);
		result.sampledHeight = size.height / (result.isSubsampled ? 2 : 1);
		return result;
	}
	function findSpecs(metrics, frames, frame, lane) {
		var spriteId = lane * frames + frame;
		var sheetId = 0;
		var sprite = null;
		var sheet = null;
		while (true) {
			sheet = metrics[sheetId];
			if (!sheet) {
				break;
			}
			if (spriteId >= sheet.sprites.length) {
				spriteId -= sheet.sprites.length;
				sheetId++;
				continue;
			}
			sprite = sheet.sprites[spriteId];
			break;
		}
		return { sprite: sprite, sheet: sheet };
	}

	function indexOf(element, arr) {
		for (var i = 0; i < arr.length; i++) {
			if (arr[i] === element) {
				return i;
			}
		}
	}
	function noop() {
		//
	}
	function preload(opts) {
		var src;
		var input = opts.source;
		src = typeof input === 'string' ? [input] : input;
		// const src: string[] =  ? [opts.source] : opts.source
		var images = [];
		var targetCount = (opts.preloadCount || src.length);
		var onInitiated = opts.initiated || noop;
		var onProgress = opts.progress || noop;
		var onComplete = opts.complete || noop;
		var count = 0;
		var completed = false;
		var firstLoaded = false;
		var tick = function () {
			count += 1;
			onProgress({
				index: indexOf(this, images),
				loaded: count,
				total: src.length,
				percent: Math.round((count / src.length) * 100)
			});
			firstLoaded = firstLoaded || (this === images[0]);
			if (firstLoaded && !completed && (count >= targetCount)) {
				completed = true;
				onComplete(images);
			}
		};
		for (var _i = 0, src_1 = src; _i < src_1.length; _i++) {
			var url = src_1[_i];
			var img = new Image();
			// push result
			images.push(img);
			// bind logic, dont care about abort/errors
			img.onload = img.onabort = img.onerror = tick;
			// begin load
			img.src = url;
		}
		onInitiated(images);
	}

	function padNumber(num, length, pad) {
		var result = String(num);
		while (result.length < length) {
			result = String(pad) + result;
		}
		return result;
	}
	/**
	 * Generates an array of source strings
	 *
	 * @remarks
	 * Takes a template string and generates an array of strings by interpolating {lane} and {frame} placeholders.
	 *
	 * ```
	 * sourceArray('http://example.com/image_{frame}.jpg, { frame: [1, 3], digits: 2 })
	 * // gives:
	 * // [ 'http://example.com/image_01.jpg', 'http://example.com/image_02.jpg', 'http://example.com/image_03.jpg' ]
	 *
	 * sourceArray('http://example.com/image_FRAME.jpg, { frame: [1, 3], digits: 2, framePlacer: 'FRAME' })
	 * // gives:
	 * // [ 'http://example.com/image_01.jpg', 'http://example.com/image_02.jpg', 'http://example.com/image_03.jpg' ]
	 * ```
	 *
	 * @param template - The template string
	 * @param opts - Interpolation options
	 *
	 * @public
	 */
	function sourceArray(template, opts) {
		var digits = opts.digits || 2;
		var lPlacer = opts.lanePlacer || '{lane}';
		var fPlacer = opts.framePlacer || '{frame}';
		var fStart = 0;
		var fEnd = 0;
		if (opts.frame) {
			fStart = opts.frame[0];
			fEnd = opts.frame[1];
		}
		var lStart = 0;
		var lEnd = 0;
		if (opts.lane) {
			lStart = opts.lane[0];
			lEnd = opts.lane[1];
		}
		var result = [];
		for (var lane = lStart; lane <= lEnd; lane += 1) {
			for (var frame = fStart; frame <= fEnd; frame += 1) {
				result.push(template
					.replace(lPlacer, padNumber(lane, digits, '0'))
					.replace(fPlacer, padNumber(frame, digits, '0')));
			}
		}
		return result;
	}

	/**
	 * The namespace that is used to bind functions to DOM events and store the data object
	 */
	var namespace = 'spritespin';
	/**
	 * Event names that are recognized by SpriteSpin. A module can implement any of these and they will be bound
	 * to the target element on which the plugin is called.
	 */
	var eventNames = [
		'mousedown',
		'mousemove',
		'mouseup',
		'mouseenter',
		'mouseover',
		'mouseleave',
		'mousewheel',
		'wheel',
		'click',
		'dblclick',
		'touchstart',
		'touchmove',
		'touchend',
		'touchcancel',
		'selectstart',
		'gesturestart',
		'gesturechange',
		'gestureend'
	];
	/**
	 *
	 */
	var callbackNames = [
		'onInit',
		'onProgress',
		'onLoad',
		'onFrameChanged',
		'onFrame',
		'onDraw',
		'onComplete',
		'onDestroy'
	];
	/**
	 * Names of events for that the default behavior should be prevented.
	 */
	var eventsToPrevent = [
		'dragstart'
	];
	/**
	 * Default set of SpriteSpin options. This also represents the majority of data attributes that are used during the
	 * lifetime of a SpriteSpin instance. The data is stored inside the target DOM element on which the plugin is called.
	 */
	var defaults = {
		source: undefined,
		width: undefined,
		height: undefined,
		frames: undefined,
		framesX: undefined,
		lanes: 1,
		sizeMode: undefined,
		renderer: 'canvas',
		lane: 0,
		frame: 0,
		frameTime: 40,
		animate: true,
		retainAnimate: false,
		reverse: false,
		loop: true,
		stopFrame: 0,
		wrap: true,
		wrapLane: false,
		sense: 1,
		senseLane: undefined,
		orientation: 'horizontal',
		detectSubsampling: true,
		preloadCount: undefined,
		touchScrollTimer: [200, 1500],
		responsive: undefined,
		plugins: undefined
	};

	function noop$1() {
		// noop
	}
	function wrapConsole(type) {
		return console && console[type] ? function () {
			var args = [];
			for (var _i = 0; _i < arguments.length; _i++) {
				args[_i] = arguments[_i];
			}
			return console.log.apply(console, args);
		} : noop$1;
	}
	var log = wrapConsole('log');
	var warn = wrapConsole('warn');
	var error = wrapConsole('error');
	function toArray(value) {
		return Array.isArray(value) ? value : [value];
	}
	/**
	 * clamps the given value by the given min and max values
	 */
	function clamp(value, min, max) {
		return (value > max ? max : (value < min ? min : value));
	}
	/**
	 *
	 */
	function wrap(value, min, max, size) {
		while (value > max) {
			value -= size;
		}
		while (value < min) {
			value += size;
		}
		return value;
	}
	/**
	 * prevents default action on the given event
	 */
	function prevent(e) {
		e.preventDefault();
		return false;
	}
	/**
	 * Binds on the given target and event the given function.
	 * The SpriteSpin namespace is attached to the event name
	 */
	function bind(target, event, func) {
		if (func) {
			target.bind(event + '.' + namespace, function (e) {
				func.apply(target, [e, target.spritespin('data')]);
			});
		}
	}
	/**
	 * Unbinds all SpriteSpin events from given target element
	 */
	function unbind(target) {
		target.unbind('.' + namespace);
	}
	/**
	 * Checks if given object is a function
	 */
	function isFunction(fn) {
		return typeof fn === 'function';
	}
	function pixelRatio(context) {
		var devicePixelRatio = window.devicePixelRatio || 1;
		var backingStoreRatio = context.webkitBackingStorePixelRatio ||
		                        context.mozBackingStorePixelRatio ||
		                        context.msBackingStorePixelRatio ||
		                        context.oBackingStorePixelRatio ||
		                        context.backingStorePixelRatio || 1;
		return devicePixelRatio / backingStoreRatio;
	}



	var _Utils = Object.freeze({
		$: $$1,
		getCursorPosition: getCursorPosition,
		detectSubsampling: detectSubsampling,
		getOuterSize: getOuterSize,
		getComputedSize: getComputedSize,
		getInnerSize: getInnerSize,
		getInnerLayout: getInnerLayout,
		measure: measure,
		findSpecs: findSpecs,
		naturalSize: naturalSize,
		preload: preload,
		sourceArray: sourceArray,
		noop: noop$1,
		log: log,
		warn: warn,
		error: error,
		toArray: toArray,
		clamp: clamp,
		wrap: wrap,
		prevent: prevent,
		bind: bind,
		unbind: unbind,
		isFunction: isFunction,
		pixelRatio: pixelRatio
	});

	/**
	 * Applies css attributes to layout the SpriteSpin containers.
	 *
	 * @internal
	 */
	function applyLayout(data) {
		// disable selection
		data.target
		    .attr('unselectable', 'on')
		    .css({
			    width: '',
			    height: '',
			    '-ms-user-select': 'none',
			    '-moz-user-select': 'none',
			    '-khtml-user-select': 'none',
			    '-webkit-user-select': 'none',
			    'user-select': 'none'
		    });
		var size = data.responsive ? getComputedSize(data) : getOuterSize(data);
		var layout = getInnerLayout(data.sizeMode, getInnerSize(data), size);
		// apply layout on target
		data.target.css({
			width: size.width,
			height: size.height,
			position: 'relative',
			overflow: 'hidden'
		});
		// apply layout on stage
		data.stage
		    .css(layout)
		    .hide();
		if (!data.canvas) {
			return;
		}
		// apply layout on canvas
		data.canvas.css(layout).hide();
		// apply pixel ratio on canvas
		data.canvasRatio = data.canvasRatio || pixelRatio(data.context);
		if (typeof layout.width === 'number' && typeof layout.height === 'number') {
			data.canvas[0].width = (layout.width * data.canvasRatio) || size.width;
			data.canvas[0].height = (layout.height * data.canvasRatio) || size.height;
		}
		else {
			data.canvas[0].width = (size.width * data.canvasRatio);
			data.canvas[0].height = (size.height * data.canvasRatio);
		}
		// width and height must be set before calling scale
		data.context.scale(data.canvasRatio, data.canvasRatio);
	}

	/**
	 * Gets a state object by name.
	 * @internal
	 * @param data - The SpriteSpin instance data
	 * @param name - The name of the state object
	 */
	function getState(data, name) {
		data.state = data.state || {};
		data.state[name] = data.state[name] || {};
		return data.state[name];
	}
	/**
	 * Gets a plugin state object by name.
	 *
	 * @remarks
	 * Plugins should use this method to get or create a state object where they can
	 * store any instance variables.
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 * @param name - The name of the plugin
	 */
	function getPluginState(data, name) {
		var state = getState(data, 'plugin');
		state[name] = state[name] || {};
		return state[name];
	}
	/**
	 * Checks whether a flag is set. See {@link flag}.
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 * @param key - The name of the flag
	 */
	function is(data, key) {
		return !!getState(data, 'flags')[key];
	}
	/**
	 * Sets a flag value. See {@link is}.
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 * @param key - The name of the flag
	 * @param value - The value to set
	 */
	function flag(data, key, value) {
		getState(data, 'flags')[key] = !!value;
	}

	/**
	 * Gets the playback state
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 */
	function getPlaybackState(data) {
		return getState(data, 'playback');
	}
	function updateLane(data, lane) {
		data.lane = data.wrapLane
			? wrap(lane, 0, data.lanes - 1, data.lanes)
			: clamp(lane, 0, data.lanes - 1);
	}
	function updateAnimationFrame(data) {
		data.frame += (data.reverse ? -1 : 1);
		// wrap the frame value to fit in range [0, data.frames)
		data.frame = wrap(data.frame, 0, data.frames - 1, data.frames);
		// stop animation if loop is disabled and the stopFrame is reached
		if (!data.loop && (data.frame === data.stopFrame)) {
			stopAnimation(data);
		}
	}
	function updateInputFrame(data, frame) {
		data.frame = Number(frame);
		data.frame = data.wrap
			? wrap(data.frame, 0, data.frames - 1, data.frames)
			: clamp(data.frame, 0, data.frames - 1);
	}
	function updateAnimation(data) {
		var state = getPlaybackState(data);
		if (state.handler) {
			updateBefore(data);
			updateAnimationFrame(data);
			updateAfter(data);
		}
	}
	function updateBefore(data) {
		var state = getPlaybackState(data);
		state.lastFrame = data.frame;
		state.lastLane = data.lane;
	}
	function updateAfter(data) {
		var state = getPlaybackState(data);
		if (state.lastFrame !== data.frame || state.lastLane !== data.lane) {
			data.target.trigger('onFrameChanged.' + namespace, data);
		}
		data.target.trigger('onFrame.' + namespace, data);
		data.target.trigger('onDraw.' + namespace, data);
	}
	/**
	 * Updates the frame or lane number of the SpriteSpin data.
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 * @param frame - The frame number to set
	 * @param lane - The lane number to set
	 */
	function updateFrame(data, frame, lane) {
		updateBefore(data);
		if (frame != null) {
			updateInputFrame(data, frame);
		}
		if (lane != null) {
			updateLane(data, lane);
		}
		updateAfter(data);
	}
	/**
	 * Stops the running animation.
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 */
	function stopAnimation(data) {
		data.animate = false;
		var state = getPlaybackState(data);
		if (state.handler != null) {
			window.clearInterval(state.handler);
			state.handler = null;
		}
	}
	/**
	 * Starts animation playback if needed.
	 *
	 * @remarks
	 * Starts animation playback if `animate` property is `true` and the animation is not yet running.
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 */
	function applyAnimation(data) {
		var state = getPlaybackState(data);
		if (state.handler && (!data.animate || state.frameTime !== data.frameTime)) {
			stopAnimation(data);
		}
		if (data.animate && !state.handler) {
			state.frameTime = data.frameTime;
			state.handler = window.setInterval(function () { return updateAnimation(data); }, state.frameTime);
		}
	}
	/**
	 * Starts the animation playback
	 *
	 * @remarks
	 * Starts the animation playback and also sets the `animate` property to `true`
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 */
	function startAnimation(data) {
		data.animate = true;
		applyAnimation(data);
	}

	var plugins = {};
	/**
	 * Registers a plugin.
	 *
	 * @remarks
	 * Use this to add custom Rendering or Updating modules that can be addressed with the 'module' option.
	 *
	 * @public
	 * @param name - The name of the plugin
	 * @param plugin - The plugin implementation
	 */
	function registerPlugin(name, plugin) {
		if (plugins[name]) {
			error("Plugin name \"" + name + "\" is already taken");
			return;
		}
		plugin = plugin || {};
		plugins[name] = plugin;
		return plugin;
	}
	/**
	 * Registers a plugin.
	 *
	 * @public
	 * @deprecated Use {@link registerPlugin} instead
	 * @param name - The name of the plugin
	 * @param plugin - The plugin implementation
	 */
	function registerModule(name, plugin) {
		warn('"registerModule" is deprecated, use "registerPlugin" instead');
		registerPlugin(name, plugin);
	}
	/**
	 * Gets an active plugin by name
	 *
	 * @internal
	 * @param name - The name of the plugin
	 */
	function getPlugin(name) {
		return plugins[name];
	}
	/**
	 * Replaces module names on given SpriteSpin data and replaces them with actual implementations.
	 * @internal
	 */
	function applyPlugins(data) {
		fixPlugins(data);
		for (var i = 0; i < data.plugins.length; i += 1) {
			var name_1 = data.plugins[i];
			if (typeof name_1 !== 'string') {
				continue;
			}
			var plugin = getPlugin(name_1);
			if (!plugin) {
				error('No plugin found with name ' + name_1);
				continue;
			}
			data.plugins[i] = plugin;
		}
	}
	function fixPlugins(data) {
		// tslint:disable no-string-literal
		if (data['mods']) {
			warn('"mods" option is deprecated, use "plugins" instead');
			data.plugins = data['mods'];
			delete data['mods'];
		}
		if (data['behavior']) {
			warn('"behavior" option is deprecated, use "plugins" instead');
			data.plugins.push(data['behavior']);
			delete data['behavior'];
		}
		if (data['module']) {
			warn('"module" option is deprecated, use "plugins" instead');
			data.plugins.push(data['module']);
			delete data['module'];
		}
	}

	var $$2 = $$1;
	var counter = 0;
	/**
	 * Collection of all SpriteSpin instances
	 */
	var instances = {};
	function pushInstance(data) {
		counter += 1;
		data.id = String(counter);
		instances[data.id] = data;
	}
	function popInstance(data) {
		delete instances[data.id];
	}
	function eachInstance(cb) {
		for (var id in instances) {
			if (instances.hasOwnProperty(id)) {
				cb(instances[id]);
			}
		}
	}
	var lazyinit = function () {
		// replace function with a noop
		// this logic must run only once
		lazyinit = function () { };
		function onEvent(eventName, e) {
			eachInstance(function (data) {
				for (var _i = 0, _a = data.plugins; _i < _a.length; _i++) {
					var module_1 = _a[_i];
					if (typeof module_1[eventName] === 'function') {
						module_1[eventName].apply(data.target, [e, data]);
					}
				}
			});
		}
		function onResize() {
			eachInstance(function (data) {
				if (data.responsive) {
					boot(data);
				}
			});
		}
		var _loop_1 = function (eventName) {
			$$2(window.document).bind(eventName + '.' + namespace, function (e) {
				onEvent('document' + eventName, e);
			});
		};
		for (var _i = 0, eventNames_1 = eventNames; _i < eventNames_1.length; _i++) {
			var eventName = eventNames_1[_i];
			_loop_1(eventName);
		}
		var resizeTimeout = null;
		$$2(window).on('resize', function () {
			window.clearTimeout(resizeTimeout);
			resizeTimeout = window.setTimeout(onResize, 100);
		});
	};
	/**
	 * (re)binds all spritespin events on the target element
	 *
	 * @internal
	 */
	function applyEvents(data) {
		var target = data.target;
		// Clear all SpriteSpin events on the target element
		unbind(target);
		// disable all default browser behavior on the following events
		// mainly prevents image drag operation
		for (var _i = 0, eventsToPrevent_1 = eventsToPrevent; _i < eventsToPrevent_1.length; _i++) {
			var eName = eventsToPrevent_1[_i];
			bind(target, eName, prevent);
		}
		// Bind module functions to SpriteSpin events
		for (var _a = 0, _b = data.plugins; _a < _b.length; _a++) {
			var plugin = _b[_a];
			for (var _c = 0, eventNames_2 = eventNames; _c < eventNames_2.length; _c++) {
				var eName = eventNames_2[_c];
				bind(target, eName, plugin[eName]);
			}
			for (var _d = 0, callbackNames_1 = callbackNames; _d < callbackNames_1.length; _d++) {
				var eName = callbackNames_1[_d];
				bind(target, eName, plugin[eName]);
			}
		}
		// bind auto start function to load event.
		bind(target, 'onLoad', function (e, d) {
			applyAnimation(d);
		});
		// bind all user events that have been passed on initialization
		for (var _e = 0, callbackNames_2 = callbackNames; _e < callbackNames_2.length; _e++) {
			var eName = callbackNames_2[_e];
			bind(target, eName, data[eName]);
		}
	}
	function applyMetrics(data) {
		if (!data.images) {
			data.metrics = [];
		}
		data.metrics = measure(data.images, data);
		var spec = findSpecs(data.metrics, data.frames, 0, 0);
		if (spec.sprite) {
			// TODO: try to remove frameWidth/frameHeight
			data.frameWidth = spec.sprite.width;
			data.frameHeight = spec.sprite.height;
		}
	}
	/**
	 * Runs the boot process.
	 *
	 * @remarks
	 * (re)initializes plugins, (re)initializes the layout, (re)binds events and loads source images.
	 *
	 * @internal
	 */
	function boot(data) {
		applyPlugins(data);
		applyEvents(data);
		applyLayout(data);
		data.source = toArray(data.source);
		data.loading = true;
		data.target
		    .addClass('loading')
		    .trigger('onInit.' + namespace, data);
		preload({
			source: data.source,
			preloadCount: data.preloadCount,
			progress: function (progress) {
				data.progress = progress;
				data.target.trigger('onProgress.' + namespace, data);
			},
			complete: function (images) {
				data.images = images;
				data.loading = false;
				data.frames = data.frames || images.length;
				applyMetrics(data);
				applyLayout(data);
				data.stage.show();
				data.target
				    .removeClass('loading')
				    .trigger('onLoad.' + namespace, data)
				    .trigger('onFrame.' + namespace, data)
				    .trigger('onDraw.' + namespace, data)
				    .trigger('onComplete.' + namespace, data);
			}
		});
	}
	/**
	 * Creates a new SpriteSpin instance
	 *
	 * @public
	 */
	function create(options) {
		var _this = this;
		var target = options.target;
		// SpriteSpin is not initialized
		// Create default settings object and extend with given options
		var data = $$2.extend(true, {}, defaults, options);
		// ensure source is set
		data.source = data.source || [];
		// ensure plugins are set
		data.plugins = data.plugins || [
			'360',
			'drag'
		];
		// if image tags are contained inside this DOM element
		// use these images as the source files
		target.find('img').each(function () {
			if (!Array.isArray(data.source)) {
				data.source = [];
			}
			data.source.push($$2(_this).attr('src'));
		});
		// build inner html
		// <div>
		//   <div class='spritespin-stage'></div>
		//   <canvas class='spritespin-canvas'></canvas>
		// </div>
		target
			.empty()
			.addClass('spritespin-instance')
			.append("<div class='spritespin-stage'></div>");
		// add the canvas element if canvas rendering is enabled and supported
		if (data.renderer === 'canvas') {
			var canvas = document.createElement('canvas');
			if (!!(canvas.getContext && canvas.getContext('2d'))) {
				data.canvas = $$2(canvas).addClass('spritespin-canvas');
				data.context = canvas.getContext('2d');
				target.append(data.canvas);
				target.addClass('with-canvas');
			}
			else {
				// fallback to image rendering mode
				data.renderer = 'image';
			}
		}
		// setup references to DOM elements
		data.target = target;
		data.stage = target.find('.spritespin-stage');
		// store the data
		target.data(namespace, data);
		pushInstance(data);
		return data;
	}
	/**
	 * Creates a new SpriteSpin instance, or updates an existing one
	 *
	 * @public
	 */
	function createOrUpdate(options) {
		lazyinit();
		var data = options.target.data(namespace);
		if (!data) {
			data = create(options);
		}
		else {
			$$2.extend(data, options);
		}
		boot(data);
		return data;
	}
	/**
	 * Destroys the SpriteSpin instance
	 *
	 * @remarks
	 * - stops running animation
	 * - unbinds all events
	 * - deletes the data on the target element
	 *
	 * @public
	 */
	function destroy(data) {
		popInstance(data);
		stopAnimation(data);
		data.target
		    .trigger('onDestroy', data)
		    .html(null)
		    .attr('style', null)
		    .attr('unselectable', null)
		    .removeClass(['spritespin-instance', 'with-canvas']);
		unbind(data.target);
		data.target.removeData(namespace);
	}

	/**
	 * Gets the current input state
	 *
	 * @public
	 * @param data - The SpriteSpin instance data
	 */
	function getInputState(data) {
		return getState(data, 'input');
	}
	/**
	 * Updates the input state using a mous or touch event.
	 *
	 * @public
	 * @param e - The input event
	 * @param data - The SpriteSpin instance data
	 */
	function updateInput(e, data) {
		var cursor = getCursorPosition(e);
		var state = getInputState(data);
		// cache positions from previous frame
		state.oldX = state.currentX;
		state.oldY = state.currentY;
		state.currentX = cursor.x;
		state.currentY = cursor.y;
		// Fix old position.
		if (state.oldX === undefined || state.oldY === undefined) {
			state.oldX = state.currentX;
			state.oldY = state.currentY;
		}
		// Cache the initial click/touch position and store the frame number at which the click happened.
		// Useful for different behavior implementations. This must be restored when the click/touch is released.
		if (state.startX === undefined || state.startY === undefined) {
			state.startX = state.currentX;
			state.startY = state.currentY;
			state.clickframe = data.frame;
			state.clicklane = data.lane;
		}
		// Calculate the vector from start position to current pointer position.
		state.dX = state.currentX - state.startX;
		state.dY = state.currentY - state.startY;
		// Calculate the vector from last frame position to current pointer position.
		state.ddX = state.currentX - state.oldX;
		state.ddY = state.currentY - state.oldY;
		// Normalize vectors to range [-1:+1]
		state.ndX = state.dX / data.target.innerWidth();
		state.ndY = state.dY / data.target.innerHeight();
		state.nddX = state.ddX / data.target.innerWidth();
		state.nddY = state.ddY / data.target.innerHeight();
	}
	/**
	 * Resets the input state.
	 *
	 * @public
	 */
	function resetInput(data) {
		var input = getInputState(data);
		input.startX = input.startY = undefined;
		input.currentX = input.currentY = undefined;
		input.oldX = input.oldY = undefined;
		input.dX = input.dY = 0;
		input.ddX = input.ddY = 0;
		input.ndX = input.ndY = 0;
		input.nddX = input.nddY = 0;
	}

	function extension(option, value) {
		var $target = $$1(this);
		if (option === 'data') {
			return $target.data(namespace);
		}
		if (option === 'api') {
			var data = $target.data(namespace);
			data.api = data.api || new Api(data);
			return data.api;
		}
		if (option === 'destroy') {
			return $target.each(function () {
				var data = $target.data(namespace);
				if (data) {
					destroy(data);
				}
			});
		}
		if (arguments.length === 2 && typeof option === 'string') {
			option = (_a = {}, _a[option] = value, _a);
		}
		if (typeof option === 'object') {
			return createOrUpdate($$1.extend(true, { target: $target }, option)).target;
		}
		throw new Error('Invalid call to spritespin');
		var _a;
	}
	$$1.fn[namespace] = extension;

// tslint:disable:object-literal-shorthand
// tslint:disable:only-arrow-functions
	extendApi({
		// Gets a value indicating whether the animation is currently running.
		isPlaying: function () {
			return getPlaybackState(this.data).handler != null;
		},
		// Gets a value indicating whether the animation looping is enabled.
		isLooping: function () {
			return this.data.loop;
		},
		// Starts/Stops the animation playback
		toggleAnimation: function () {
			if (this.isPlaying()) {
				this.stopAnimation();
			}
			else {
				this.startAnimation();
			}
		},
		// Stops animation playback
		stopAnimation: function () {
			this.data.animate = false;
			stopAnimation(this.data);
		},
		// Starts animation playback
		startAnimation: function () {
			this.data.animate = true;
			applyAnimation(this.data);
		},
		// Sets a value indicating whether the animation should be looped or not.
		// This might start the animation (if the 'animate' data attribute is set to true)
		loop: function (value) {
			this.data.loop = value;
			applyAnimation(this.data);
			return this;
		},
		// Gets the current frame number
		currentFrame: function () {
			return this.data.frame;
		},
		// Updates SpriteSpin to the specified frame.
		updateFrame: function (frame) {
			updateFrame(this.data, frame);
			return this;
		},
		// Skips the given number of frames
		skipFrames: function (step) {
			var data = this.data;
			updateFrame(data, data.frame + (data.reverse ? -step : +step));
			return this;
		},
		// Updates SpriteSpin so that the next frame is shown
		nextFrame: function () {
			return this.skipFrames(1);
		},
		// Updates SpriteSpin so that the previous frame is shown
		prevFrame: function () {
			return this.skipFrames(-1);
		},
		// Starts the animations that will play until the given frame number is reached
		// options:
		//   force [boolean] starts the animation, even if current frame is the target frame
		//   nearest [boolean] animates to the direction with minimum distance to the target frame
		playTo: function (frame, options) {
			var data = this.data;
			options = options || {};
			if (!options.force && data.frame === frame) {
				return;
			}
			if (options.nearest) {
				// distance to the target frame
				var a = frame - data.frame;
				// distance to last frame and the to target frame
				var b = frame > data.frame ? a - data.frames : a + data.frames;
				// minimum distance
				var c = Math.abs(a) < Math.abs(b) ? a : b;
				data.reverse = c < 0;
			}
			data.animate = true;
			data.loop = false;
			data.stopFrame = frame;
			applyAnimation(data);
			return this;
		}
	});

	function pick(target, names) {
		for (var _i = 0, names_1 = names; _i < names_1.length; _i++) {
			var name_1 = names_1[_i];
			if (target[name_1] || name_1 in target) {
				return name_1;
			}
		}
		return names[0];
	}
	var browser = {
		requestFullscreen: pick(document.documentElement, [
			'requestFullscreen',
			'webkitRequestFullScreen',
			'mozRequestFullScreen',
			'msRequestFullscreen'
		]),
		exitFullscreen: pick(document, [
			'exitFullscreen',
			'webkitExitFullscreen',
			'webkitCancelFullScreen',
			'mozCancelFullScreen',
			'msExitFullscreen'
		]),
		fullscreenElement: pick(document, [
			'fullscreenElement',
			'webkitFullscreenElement',
			'webkitCurrentFullScreenElement',
			'mozFullScreenElement',
			'msFullscreenElement'
		]),
		fullscreenEnabled: pick(document, [
			'fullscreenEnabled',
			'webkitFullscreenEnabled',
			'mozFullScreenEnabled',
			'msFullscreenEnabled'
		]),
		fullscreenchange: pick(document, [
			'onfullscreenchange',
			'onwebkitfullscreenchange',
			'onmozfullscreenchange',
			'onMSFullscreenChange'
		]).replace(/^on/, ''),
		fullscreenerror: pick(document, [
			'onfullscreenerror',
			'onwebkitfullscreenerror',
			'onmozfullscreenerror',
			'onMSFullscreenError'
		]).replace(/^on/, '')
	};
	var changeEvent = browser.fullscreenchange + '.' + namespace + '-fullscreen';
	function unbindChangeEvent() {
		$$1(document).unbind(changeEvent);
	}
	function bindChangeEvent(callback) {
		unbindChangeEvent();
		$$1(document).bind(changeEvent, callback);
	}
	var orientationEvent = 'orientationchange.' + namespace + '-fullscreen';
	function unbindOrientationEvent() {
		$$1(window).unbind(orientationEvent);
	}
	function bindOrientationEvent(callback) {
		unbindOrientationEvent();
		$$1(window).bind(orientationEvent, callback);
	}
	function requestFullscreenNative(e) {
		e = e || document.documentElement;
		e[browser.requestFullscreen]();
	}
	function exitFullscreen() {
		return document[browser.exitFullscreen]();
	}
	function fullscreenEnabled() {
		return document[browser.fullscreenEnabled];
	}
	function fullscreenElement() {
		return document[browser.fullscreenElement];
	}
	function isFullscreen() {
		return !!fullscreenElement();
	}
	function toggleFullscreen(data, opts) {
		if (isFullscreen()) {
			this.apiRequestFullscreen(opts);
		}
		else {
			this.exitFullscreen();
		}
	}
	function requestFullscreen(data, opts) {
		opts = opts || {};
		var oWidth = data.width;
		var oHeight = data.height;
		var oSource = data.source;
		var oSize = data.sizeMode;
		var oResponsive = data.responsive;
		var enter = function () {
			data.width = window.screen.width;
			data.height = window.screen.height;
			data.source = (opts.source || oSource);
			data.sizeMode = opts.sizeMode || 'fit';
			data.responsive = false;
			boot(data);
		};
		var exit = function () {
			data.width = oWidth;
			data.height = oHeight;
			data.source = oSource;
			data.sizeMode = oSize;
			data.responsive = oResponsive;
			boot(data);
		};
		bindChangeEvent(function () {
			if (isFullscreen()) {
				enter();
				bindOrientationEvent(enter);
			}
			else {
				unbindChangeEvent();
				unbindOrientationEvent();
				exit();
			}
		});
		requestFullscreenNative(data.target[0]);
	}
	extendApi({
		fullscreenEnabled: fullscreenEnabled,
		fullscreenElement: fullscreenElement,
		exitFullscreen: exitFullscreen,
		toggleFullscreen: function (opts) {
			toggleFullscreen(this.data, opts);
		},
		requestFullscreen: function (opts) {
			requestFullscreen(this.data, opts);
		}
	});

	(function () {
		var NAME = 'click';
		function click(e, data) {
			if (data.loading || !data.stage.is(':visible')) {
				return;
			}
			updateInput(e, data);
			var input = getInputState(data);
			var half, pos;
			var target = data.target, offset = target.offset();
			if (data.orientation === 'horizontal') {
				half = target.innerWidth() / 2;
				pos = input.currentX - offset.left;
			}
			else {
				half = target.innerHeight() / 2;
				pos = input.currentY - offset.top;
			}
			updateFrame(data, data.frame + (pos > half ? 1 : -1));
		}
		registerPlugin(NAME, {
			name: NAME,
			mouseup: click,
			touchend: click
		});
	})();

	(function () {
		var NAME = 'drag';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function getAxis(data) {
			if (typeof data.orientation === 'number') {
				return data.orientation * Math.PI / 180;
			}
			if (data.orientation === 'horizontal') {
				return 0;
			}
			return Math.PI / 2;
		}
		function onInit(e, data) {
			var state = getState$$1(data);
			var d = [200, 1500];
			var t = data.touchScrollTimer || d;
			state.minTime = t[0] || d[0];
			state.maxTime = t[1] || d[1];
		}
		function dragStart(e, data) {
			var state = getState$$1(data);
			if (data.loading || is(data, 'dragging') || data['zoomPinFrame'] && !data.stage.is(':visible')) {
				return;
			}
			// Touch scroll can only be disabled by cancelling the 'touchstart' event.
			// If we would try to cancel the 'touchmove' event during a scroll
			// chrome browser raises an error
			//
			// When a user interacts with sprite spin, we dont know whether the intention
			// is to scroll the page or to roll the spin.
			//
			// On first interaction with SpriteSpin the scroll is not disabled
			// On double tap within 200ms the scroll is not disabled
			// Scroll is only disabled if there was an interaction with SpriteSpin in the past 1500ms
			var now = new Date().getTime();
			if (state.endAt && (now - state.endAt > state.maxTime)) {
				// reset timer if the user has no interaction with spritespin within 1500ms
				state.startAt = null;
				state.endAt = null;
			}
			if (state.startAt && (now - state.startAt > state.minTime)) {
				// disable scroll only if there was already an interaction with spritespin
				// however, allow scrolling on double tab within 200ms
				e.preventDefault();
			}
			state.startAt = now;
			state.wasPlaying = !!getPlaybackState(data).handler;
			state.frame = data.frame || 0;
			state.lane = data.lane || 0;
			flag(data, 'dragging', true);
			updateInput(e, data);
		}
		function dragEnd(e, data) {
			if (is(data, 'dragging')) {
				getState$$1(data).endAt = new Date().getTime();
				flag(data, 'dragging', false);
				resetInput(data);
				if (data.retainAnimate && getState$$1(data).wasPlaying) {
					startAnimation(data);
				}
			}
		}
		function drag(e, data) {
			var state = getState$$1(data);
			var input = getInputState(data);
			if (!is(data, 'dragging')) {
				return;
			}
			updateInput(e, data);
			var rad = getAxis(data);
			var sn = Math.sin(rad);
			var cs = Math.cos(rad);
			var x = ((input.nddX * cs - input.nddY * sn) * data.sense) || 0;
			var y = ((input.nddX * sn + input.nddY * cs) * (data.senseLane || data.sense)) || 0;
			// accumulate
			state.frame += data.frames * x;
			state.lane += data.lanes * y;
			// update spritespin
			var oldFrame = data.frame;
			var oldLane = data.lane;
			updateFrame(data, Math.floor(state.frame), Math.floor(state.lane));
			stopAnimation(data);
		}
		function mousemove(e, data) {
			dragStart(e, data);
			drag(e, data);
		}
		registerPlugin('drag', {
			name: 'drag',
			onInit: onInit,
			mousedown: dragStart,
			mousemove: drag,
			mouseup: dragEnd,
			documentmousemove: drag,
			documentmouseup: dragEnd,
			touchstart: dragStart,
			touchmove: drag,
			touchend: dragEnd,
			touchcancel: dragEnd
		});
		registerPlugin('move', {
			name: 'move',
			onInit: onInit,
			mousemove: mousemove,
			mouseleave: dragEnd,
			touchstart: dragStart,
			touchmove: drag,
			touchend: dragEnd,
			touchcancel: dragEnd
		});
	})();

	(function () {
		var NAME = 'hold';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function rememberOptions(data) {
			var state = getState$$1(data);
			state.frameTime = data.frameTime;
			state.animate = data.animate;
			state.reverse = data.reverse;
		}
		function restoreOptions(data) {
			var state = getState$$1(data);
			data.frameTime = state.frameTime;
			data.animate = state.animate;
			data.reverse = state.reverse;
		}
		function start(e, data) {
			if (is(data, 'loading') || is(data, 'dragging') || !data.stage.is(':visible')) {
				return;
			}
			rememberOptions(data);
			updateInput(e, data);
			flag(data, 'dragging', true);
			data.animate = true;
			applyAnimation(data);
		}
		function stop(e, data) {
			flag(data, 'dragging', false);
			resetInput(data);
			stopAnimation(data);
			restoreOptions(data);
			applyAnimation(data);
		}
		function update(e, data) {
			if (!is(data, 'dragging')) {
				return;
			}
			updateInput(e, data);
			var input = getInputState(data);
			var half, delta;
			var target = data.target, offset = target.offset();
			if (data.orientation === 'horizontal') {
				half = target.innerWidth() / 2;
				delta = (input.currentX - offset.left - half) / half;
			}
			else {
				half = (data.height / 2);
				delta = (input.currentY - offset.top - half) / half;
			}
			data.reverse = delta < 0;
			delta = delta < 0 ? -delta : delta;
			data.frameTime = 80 * (1 - delta) + 20;
			if (((data.orientation === 'horizontal') && (input.dX < input.dY)) ||
			    ((data.orientation === 'vertical') && (input.dX < input.dY))) {
				e.preventDefault();
			}
		}
		function onFrame(e, data) {
			data.animate = true;
			applyAnimation(data);
		}
		registerPlugin(NAME, {
			name: NAME,
			mousedown: start,
			mousemove: update,
			mouseup: stop,
			mouseleave: stop,
			touchstart: start,
			touchmove: update,
			touchend: stop,
			touchcancel: stop,
			onFrame: onFrame
		});
	})();

	(function () {
		var NAME = 'swipe';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function getOption(data, name, fallback) {
			return data[name] || fallback;
		}
		function init(e, data) {
			var state = getState$$1(data);
			state.fling = getOption(data, 'swipeFling', 10);
			state.snap = getOption(data, 'swipeSnap', 0.50);
		}
		function start(e, data) {
			if (!data.loading && !is(data, 'dragging')) {
				updateInput(e, data);
				flag(data, 'dragging', true);
			}
		}
		function update(e, data) {
			if (!is(data, 'dragging')) {
				return;
			}
			updateInput(e, data);
			var frame = data.frame;
			var lane = data.lane;
			updateFrame(data, frame, lane);
		}
		function end(e, data) {
			if (!is(data, 'dragging')) {
				return;
			}
			flag(data, 'dragging', false);
			var state = getState$$1(data);
			var input = getInputState(data);
			var frame = data.frame;
			var lane = data.lane;
			var snap = state.snap;
			var fling = state.fling;
			var dS, dF;
			if (data.orientation === 'horizontal') {
				dS = input.ndX;
				dF = input.ddX;
			}
			else {
				dS = input.ndY;
				dF = input.ddY;
			}
			if (dS >= snap || dF >= fling) {
				frame = data.frame - 1;
			}
			else if (dS <= -snap || dF <= -fling) {
				frame = data.frame + 1;
			}
			resetInput(data);
			updateFrame(data, frame, lane);
			stopAnimation(data);
		}
		registerPlugin(NAME, {
			name: NAME,
			onLoad: init,
			mousedown: start,
			mousemove: update,
			mouseup: end,
			mouseleave: end,
			touchstart: start,
			touchmove: update,
			touchend: end,
			touchcancel: end
		});
	})();

	(function () {
		var NAME = 'wheel';
		function wheel(e, data) {
			if (!data.loading && data.stage.is(':visible')) {
				e.preventDefault();
				var we = e.originalEvent;
				var signX = we.deltaX === 0 ? 0 : we.deltaX > 0 ? 1 : -1;
				var signY = we.deltaY === 0 ? 0 : we.deltaY > 0 ? 1 : -1;
				updateFrame(data, data.frame + signY, data.lane + signX);
			}
		}
		registerPlugin(NAME, {
			name: NAME,
			wheel: wheel
		});
	})();

	(function () {
		var template = "\n<div class='spritespin-progress'>\n  <div class='spritespin-progress-label'></div>\n  <div class='spritespin-progress-bar'></div>\n</div>\n";
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		var NAME = 'progress';
		function onInit(e, data) {
			var state = getState$$1(data);
			if (!state.stage) {
				state.stage = $$1(template);
				state.stage.appendTo(data.target);
			}
			state.stage.find('.spritespin-progress-label')
			     .text("0%")
			     .css({ 'text-align': 'center' });
			state.stage.find('.spritespin-progress-bar').css({
				width: "0%"
			});
			state.stage.hide().fadeIn();
		}
		function onProgress(e, data) {
			var state = getState$$1(data);
			state.stage.find('.spritespin-progress-label')
			     .text(data.progress.percent + "%")
			     .css({ 'text-align': 'center' });
			state.stage.find('.spritespin-progress-bar').css({
				width: data.progress.percent + "%"
			});
		}
		function onLoad(e, data) {
			$$1(getState$$1(data).stage).fadeOut();
		}
		function onDestroy(e, data) {
			$$1(getState$$1(data).stage).remove();
		}
		registerPlugin(NAME, {
			name: NAME,
			onInit: onInit,
			onProgress: onProgress,
			onLoad: onLoad,
			onDestroy: onDestroy
		});
	})();

	(function () {
		var NAME = '360';
		function onLoad(e, data) {
			data.stage.find('.spritespin-frames').detach();
			if (data.renderer === 'image') {
				$(data.images).addClass('spritespin-frames').appendTo(data.stage);
			}
		}
		function onDraw(e, data) {
			var specs = findSpecs(data.metrics, data.frames, data.frame, data.lane);
			var sheet = specs.sheet;
			var sprite = specs.sprite;
			if (!sheet || !sprite) {
				return;
			}
			var src = data.source[sheet.id];
			var image = data.images[sheet.id];
			if (data.renderer === 'canvas') {
				data.canvas.show();
				var w = data.canvas[0].width / data.canvasRatio;
				var h = data.canvas[0].height / data.canvasRatio;
				data.context.clearRect(0, 0, w, h);
				data.context.drawImage(image, sprite.sampledX, sprite.sampledY, sprite.sampledWidth, sprite.sampledHeight, 0, 0, w, h);
				return;
			}
			var scaleX = data.stage.innerWidth() / sprite.sampledWidth;
			var scaleY = data.stage.innerHeight() / sprite.sampledHeight;
			var top = Math.floor(-sprite.sampledY * scaleY);
			var left = Math.floor(-sprite.sampledX * scaleX);
			var width = Math.floor(sheet.sampledWidth * scaleX);
			var height = Math.floor(sheet.sampledHeight * scaleY);
			if (data.renderer === 'background') {
				data.stage.css({
					'background-image': "url('" + src + "')",
					'background-position': left + "px " + top + "px",
					'background-repeat': 'no-repeat',
					// set custom background size to enable responsive rendering
					'-webkit-background-size': width + "px " + height + "px",
					'-moz-background-size': width + "px " + height + "px",
					'-o-background-size': width + "px " + height + "px",
					'background-size': width + "px " + height + "px" /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */
				});
				return;
			}
			$(data.images).hide();
			$(image).show().css({
				position: 'absolute',
				top: top,
				left: left,
				'max-width': 'initial',
				width: width,
				height: height
			});
		}
		registerPlugin(NAME, {
			name: NAME,
			onLoad: onLoad,
			onDraw: onDraw
		});
	})();

	(function () {
		var NAME = 'blur';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function getOption(data, name, fallback) {
			return data[name] || fallback;
		}
		function init(e, data) {
			var state = getState$$1(data);
			state.canvas = state.canvas || $$1("<canvas class='blur-layer'></canvas>");
			state.context = state.context || state.canvas[0].getContext('2d');
			state.steps = state.steps || [];
			state.fadeTime = Math.max(getOption(data, 'blurFadeTime', 200), 1);
			state.frameTime = Math.max(getOption(data, 'blurFrameTime', data.frameTime), 16);
			state.trackTime = null;
			state.cssBlur = !!getOption(data, 'blurCss', false);
			var inner = getInnerSize(data);
			var outer = data.responsive ? getComputedSize(data) : getOuterSize(data);
			var css = getInnerLayout(data.sizeMode, inner, outer);
			state.canvas[0].width = data.width * data.canvasRatio;
			state.canvas[0].height = data.height * data.canvasRatio;
			state.canvas.css(css).show();
			state.context.scale(data.canvasRatio, data.canvasRatio);
			data.target.append(state.canvas);
		}
		function onFrame(e, data) {
			var state = getState$$1(data);
			trackFrame(data);
			if (state.timeout == null) {
				loop(data);
			}
		}
		function trackFrame(data) {
			var state = getState$$1(data);
			var ani = getPlaybackState(data);
			// distance between frames
			var d = Math.abs(data.frame - ani.lastFrame);
			// shortest distance
			d = d >= data.frames / 2 ? data.frames - d : d;
			state.steps.unshift({
				frame: data.frame,
				lane: data.lane,
				live: 1,
				step: state.frameTime / state.fadeTime,
				d: d,
				alpha: 0
			});
		}
		var toRemove = [];
		function removeOldFrames(frames) {
			toRemove.length = 0;
			for (var i = 0; i < frames.length; i += 1) {
				if (frames[i].alpha <= 0) {
					toRemove.push(i);
				}
			}
			for (var _i = 0, toRemove_1 = toRemove; _i < toRemove_1.length; _i++) {
				var item = toRemove_1[_i];
				frames.splice(item, 1);
			}
		}
		function loop(data) {
			var state = getState$$1(data);
			state.timeout = window.setTimeout(function () { tick(data); }, state.frameTime);
		}
		function killLoop(data) {
			var state = getState$$1(data);
			window.clearTimeout(state.timeout);
			state.timeout = null;
		}
		function applyCssBlur(canvas, d) {
			var amount = Math.min(Math.max((d / 2) - 4, 0), 2.5);
			var blur = "blur(" + amount + "px)";
			canvas.css({
				'-webkit-filter': blur,
				filter: blur
			});
		}
		function clearFrame(data, state) {
			state.canvas.show();
			var w = state.canvas[0].width / data.canvasRatio;
			var h = state.canvas[0].height / data.canvasRatio;
			// state.context.clearRect(0, 0, w, h)
		}
		function drawFrame(data, state, step) {
			if (step.alpha <= 0) {
				return;
			}
			var specs = findSpecs(data.metrics, data.frames, step.frame, step.lane);
			var sheet = specs.sheet;
			var sprite = specs.sprite;
			if (!sheet || !sprite) {
				return;
			}
			var src = data.source[sheet.id];
			var image = data.images[sheet.id];
			if (image.complete === false) {
				return;
			}
			state.canvas.show();
			var w = state.canvas[0].width / data.canvasRatio;
			var h = state.canvas[0].height / data.canvasRatio;
			state.context.globalAlpha = step.alpha;
			state.context.drawImage(image, sprite.sampledX, sprite.sampledY, sprite.sampledWidth, sprite.sampledHeight, 0, 0, w, h);
		}
		function tick(data) {
			var state = getState$$1(data);
			killLoop(data);
			if (!state.context) {
				return;
			}
			var d = 0;
			clearFrame(data, state);
			state.context.clearRect(0, 0, data.width, data.height);
			for (var _i = 0, _a = state.steps; _i < _a.length; _i++) {
				var step = _a[_i];
				step.live = Math.max(step.live - step.step, 0);
				step.alpha = Math.max(step.live - 0.25, 0);
				drawFrame(data, state, step);
				d += step.alpha + step.d;
			}
			if (state.cssBlur) {
				applyCssBlur(state.canvas, d);
			}
			removeOldFrames(state.steps);
			if (state.steps.length) {
				loop(data);
			}
		}
		registerPlugin(NAME, {
			name: NAME,
			onLoad: init,
			onFrameChanged: onFrame
		});
	})();

	(function () {
		var max = Math.max;
		var min = Math.min;
		var NAME = 'ease';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function getOption(data, name, fallback) {
			return data[name] || fallback;
		}
		function init(e, data) {
			var state = getState$$1(data);
			state.maxSamples = max(getOption(data, 'easeMaxSamples', 5), 0);
			state.damping = max(min(getOption(data, 'easeDamping', 0.9), 0.999), 0);
			state.abortTime = max(getOption(data, 'easeAbortTime', 250), 16);
			state.updateTime = max(getOption(data, 'easeUpdateTime', data.frameTime), 16);
			state.samples = [];
			state.steps = [];
		}
		function update(e, data) {
			if (is(data, 'dragging')) {
				killLoop(data);
				sampleInput(data);
			}
		}
		function end(e, data) {
			var state = getState$$1(data);
			var samples = state.samples;
			var last;
			var lanes = 0;
			var frames = 0;
			var time = 0;
			for (var _i = 0, samples_1 = samples; _i < samples_1.length; _i++) {
				var sample = samples_1[_i];
				if (!last) {
					last = sample;
					continue;
				}
				var dt = sample.time - last.time;
				if (dt > state.abortTime) {
					lanes = frames = time = 0;
					return killLoop(data);
				}
				frames += sample.frame - last.frame;
				lanes += sample.lane - last.lane;
				time += dt;
				last = sample;
			}
			samples.length = 0;
			if (!time) {
				return;
			}
			state.lane = data.lane;
			state.lanes = 0;
			state.laneStep = lanes / time * state.updateTime;
			state.frame = data.frame;
			state.frames = 0;
			state.frameStep = frames / time * state.updateTime;
			loop(data);
		}
		function sampleInput(data) {
			var state = getState$$1(data);
			// add a new sample
			state.samples.push({
				time: new Date().getTime(),
				frame: data.frame,
				lane: data.lane
			});
			// drop old samples
			while (state.samples.length > state.maxSamples) {
				state.samples.shift();
			}
		}
		function killLoop(data) {
			var state = getState$$1(data);
			if (state.handler != null) {
				window.clearTimeout(state.handler);
				state.handler = null;
			}
		}
		function loop(data) {
			var state = getState$$1(data);
			state.handler = window.setTimeout(function () { tick(data); }, state.updateTime);
		}
		function tick(data) {
			var state = getState$$1(data);
			state.lanes += state.laneStep;
			state.frames += state.frameStep;
			state.laneStep *= state.damping;
			state.frameStep *= state.damping;
			var frame = Math.floor(state.frame + state.frames);
			var lane = Math.floor(state.lane + state.lanes);
			updateFrame(data, frame, lane);
			if (is(data, 'dragging')) {
				killLoop(data);
			}
			else if (Math.abs(state.frameStep) > 0.005 || Math.abs(state.laneStep) > 0.005) {
				loop(data);
			}
			else {
				killLoop(data);
			}
		}
		registerPlugin(NAME, {
			name: NAME,
			onLoad: init,
			mousemove: update,
			mouseup: end,
			mouseleave: end,
			touchmove: update,
			touchend: end,
			touchcancel: end
		});
	})();

	(function () {
		var NAME = 'gallery';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function getOption(data, name, fallback) {
			return data[name] || fallback;
		}
		function load(e, data) {
			var state = getState$$1(data);
			state.images = [];
			state.offsets = [];
			state.frame = data.frame;
			state.speed = getOption(data, 'gallerySpeed', 500);
			state.opacity = getOption(data, 'galleryOpacity', 0.25);
			state.stage = getOption(data, 'galleryStage', $$1('<div></div>'));
			state.stage.empty().addClass('gallery-stage').prependTo(data.stage);
			var size = 0;
			for (var _i = 0, _a = data.images; _i < _a.length; _i++) {
				var image = _a[_i];
				var naturalSize$$1 = naturalSize(image);
				var scale = data.height / naturalSize$$1.height;
				var img = $$1(image);
				state.stage.append(img);
				state.images.push(img);
				state.offsets.push(-size + (data.width - image.width * scale) / 2);
				size += data.width;
				img.css({
					'max-width': 'initial',
					opacity: state.opacity,
					width: data.width,
					height: data.height
				});
			}
			var innerSize = getInnerSize(data);
			var outerSize = data.responsive ? getComputedSize(data) : getOuterSize(data);
			var layout = getInnerLayout(data.sizeMode, innerSize, outerSize);
			state.stage.css(layout).css({ width: size, left: state.offsets[state.frame] });
			state.images[state.frame].animate({ opacity: 1 }, { duration: state.speed });
		}
		function draw(e, data) {
			var state = getState$$1(data);
			var input = getInputState(data);
			var isDragging = is(data, 'dragging');
			if (state.frame !== data.frame && !isDragging) {
				state.stage.stop(true, false).animate({ left: state.offsets[data.frame] }, { duration: state.speed });
				state.images[state.frame].animate({ opacity: state.opacity }, { duration: state.speed });
				state.frame = data.frame;
				state.images[state.frame].animate({ opacity: 1 }, { duration: state.speed });
				state.stage.animate({ left: state.offsets[state.frame] });
			}
			else if (isDragging || state.dX !== input.dX) {
				state.dX = input.dX;
				state.ddX = input.ddX;
				state.stage.stop(true, true).css({ left: state.offsets[state.frame] + state.dX });
			}
		}
		registerPlugin(NAME, {
			name: NAME,
			onLoad: load,
			onDraw: draw
		});
	})();

	(function () {
		var NAME = 'panorama';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function onLoad(e, data) {
			var state = getState$$1(data);
			var sprite = data.metrics[0];
			if (!sprite) {
				return;
			}
			if (data.orientation === 'horizontal') {
				state.scale = data.target.innerHeight() / sprite.sampledHeight;
				data.frames = sprite.sampledWidth;
			}
			else {
				state.scale = data.target.innerWidth() / sprite.sampledWidth;
				data.frames = sprite.sampledHeight;
			}
			var width = Math.floor(sprite.sampledWidth * state.scale);
			var height = Math.floor(sprite.sampledHeight * state.scale);
			data.stage.css({
				'background-image': "url(" + data.source[sprite.id] + ")",
				'background-repeat': 'repeat-both',
				// set custom background size to enable responsive rendering
				'-webkit-background-size': width + "px " + height + "px",
				'-moz-background-size': width + "px " + height + "px",
				'-o-background-size': width + "px " + height + "px",
				'background-size': width + "px " + height + "px" /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */
			});
		}
		function onDraw(e, data) {
			var state = getState$$1(data);
			var px = data.orientation === 'horizontal' ? 1 : 0;
			var py = px ? 0 : 1;
			var offset = data.frame % data.frames;
			var left = Math.round(px * offset * state.scale);
			var top = Math.round(py * offset * state.scale);
			data.stage.css({ 'background-position': left + "px " + top + "px" });
		}
		registerPlugin(NAME, {
			name: NAME,
			onLoad: onLoad,
			onDraw: onDraw
		});
	})();

	(function () {
		var NAME = 'zoom';
		function getState$$1(data) {
			return getPluginState(data, NAME);
		}
		function getOption(data, name, fallback) {
			return name in data ? data[name] : fallback;
		}
		function onInit(e, data) {
			var state = getState$$1(data);
			state.source = getOption(data, 'zoomSource', data.source);
			state.useWheel = getOption(data, 'zoomUseWheel', false);
			state.useClick = getOption(data, 'zoomUseClick', true);
			state.pinFrame = getOption(data, 'zoomPinFrame', true);
			state.doubleClickTime = getOption(data, 'zoomDoubleClickTime', 500);
			state.stage = state.stage || $$1("<div class='zoom-stage'></div>");
			state.stage.css({
				width: '100%',
				height: '100%',
				top: 0,
				left: 0,
				bottom: 0,
				right: 0,
				position: 'absolute'
			})
			     .appendTo(data.target)
			     .hide();
		}
		function onDestroy(e, data) {
			var state = getState$$1(data);
			if (state.stage) {
				state.stage.remove();
				delete state.stage;
			}
		}
		function updateInput$$1(e, data) {
			var state = getState$$1(data);
			if (!state.stage.is(':visible')) {
				return;
			}
			e.preventDefault();
			if (state.pinFrame) {
				// hack into drag/move module and disable dragging
				// prevents frame change during zoom mode
				flag(data, 'dragging', false);
			}
			// grab touch/cursor position
			var cursor = getCursorPosition(e);
			// normalize cursor position into [0:1] range
			var x = cursor.x / data.width;
			var y = cursor.y / data.height;
			if (state.oldX == null) {
				state.oldX = x;
				state.oldY = y;
			}
			if (state.currentX == null) {
				state.currentX = x;
				state.currentY = y;
			}
			// calculate move delta since last frame and remember current position
			var dx = x - state.oldX;
			var dy = y - state.oldY;
			state.oldX = x;
			state.oldY = y;
			// invert drag direction for touch events to enable 'natural' scrolling
			if (e.type.match(/touch/)) {
				dx = -dx;
				dy = -dy;
			}
			// accumulate display coordinates
			state.currentX = clamp(state.currentX + dx, 0, 1);
			state.currentY = clamp(state.currentY + dy, 0, 1);
			updateFrame(data, data.frame, data.lane);
		}
		function onClick(e, data) {
			var state = getState$$1(data);
			if (!state.useClick) {
				return;
			}
			e.preventDefault();
			// simulate double click
			var clickTime = new Date().getTime();
			if (!state.clickTime) {
				// on first click
				state.clickTime = clickTime;
				return;
			}
			// on second click
			var timeDelta = clickTime - state.clickTime;
			if (timeDelta > state.doubleClickTime) {
				// took too long, back to first click
				state.clickTime = clickTime;
				return;
			}
			// on valid double click
			state.clickTime = undefined;
			if (toggleZoom(data)) {
				updateInput$$1(e, data);
			}
		}
		function onMove(e, data) {
			var state = getState$$1(data);
			if (state.stage.is(':visible')) {
				updateInput$$1(e, data);
			}
		}
		function onDraw(e, data) {
			var state = getState$$1(data);
			// calculate the frame index
			var index = data.lane * data.frames + data.frame;
			// get the zoom image. Use original frames as fallback. This won't work for spritesheets
			var source = state.source[index];
			var spec = findSpecs(data.metrics, data.frames, data.frame, data.lane);
			// get display position
			var x = state.currentX;
			var y = state.currentY;
			// fallback to centered position
			if (x == null) {
				x = state.currentX = 0.5;
				y = state.currentY = 0.5;
			}
			if (source) {
				// scale up from [0:1] to [0:100] range
				x = Math.floor(x * 100);
				y = Math.floor(y * 100);
				// update background image and position
				state.stage.css({
					'background-repeat': 'no-repeat',
					'background-image': "url('" + source + "')",
					'background-position': x + "% " + y + "%"
				});
			}
			else if (spec.sheet && spec.sprite) {
				var sprite = spec.sprite;
				var sheet = spec.sheet;
				var src = data.source[sheet.id];
				var left = -Math.floor(sprite.sampledX + x * (sprite.sampledWidth - data.width));
				var top_1 = -Math.floor(sprite.sampledY + y * (sprite.sampledHeight - data.height));
				var width = sheet.sampledWidth;
				var height = sheet.sampledHeight;
				state.stage.css({
					'background-image': "url('" + src + "')",
					'background-position': left + "px " + top_1 + "px",
					'background-repeat': 'no-repeat',
					// set custom background size to enable responsive rendering
					'-webkit-background-size': width + "px " + height + "px",
					'-moz-background-size': width + "px " + height + "px",
					'-o-background-size': width + "px " + height + "px",
					'background-size': width + "px " + height + "px" /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */
				});
			}
		}
		function toggleZoom(data) {
			var state = getState$$1(data);
			if (!state.stage) {
				throw new Error('zoom module is not initialized or is not available.');
			}
			if (state.stage.is(':visible')) {
				showZoom(data);
			}
			else {
				hideZoom(data);
				return true;
			}
			return false;
		}
		function showZoom(data) {
			var state = getState$$1(data);
			state.stage.fadeOut();
			data.stage.fadeIn();
		}
		function hideZoom(data) {
			var state = getState$$1(data);
			state.stage.fadeIn();
			data.stage.fadeOut();
		}
		function wheel(e, data) {
			var state = getState$$1(data);
			if (!data.loading && state.useWheel) {
				var we = e.originalEvent;
				var signY = we.deltaY === 0 ? 0 : we.deltaY > 0 ? 1 : -1;
				if (typeof state.useWheel === 'number') {
					signY *= state.useWheel;
				}
				if (state.stage.is(':visible') && signY > 0) {
					e.preventDefault();
					showZoom(data);
				}
				if (!state.stage.is(':visible') && signY < 0) {
					e.preventDefault();
					hideZoom(data);
				}
			}
		}
		registerPlugin(NAME, {
			name: NAME,
			mousedown: onClick,
			touchstart: onClick,
			mousemove: onMove,
			touchmove: onMove,
			wheel: wheel,
			onInit: onInit,
			onDestroy: onDestroy,
			onDraw: onDraw
		});
		extendApi({
			toggleZoom: function () { toggleZoom(this.data); } // tslint:disable-line
		});
	})();

	var Utils = _Utils;

	exports.Utils = Utils;
	exports.sourceArray = sourceArray;
	exports.Api = Api;
	exports.extendApi = extendApi;
	exports.instances = instances;
	exports.applyEvents = applyEvents;
	exports.boot = boot;
	exports.create = create;
	exports.createOrUpdate = createOrUpdate;
	exports.destroy = destroy;
	exports.namespace = namespace;
	exports.eventNames = eventNames;
	exports.callbackNames = callbackNames;
	exports.eventsToPrevent = eventsToPrevent;
	exports.defaults = defaults;
	exports.getInputState = getInputState;
	exports.updateInput = updateInput;
	exports.resetInput = resetInput;
	exports.applyLayout = applyLayout;
	exports.getPlaybackState = getPlaybackState;
	exports.updateFrame = updateFrame;
	exports.stopAnimation = stopAnimation;
	exports.applyAnimation = applyAnimation;
	exports.startAnimation = startAnimation;
	exports.registerPlugin = registerPlugin;
	exports.registerModule = registerModule;
	exports.getPlugin = getPlugin;
	exports.applyPlugins = applyPlugins;
	exports.getState = getState;
	exports.getPluginState = getPluginState;
	exports.is = is;
	exports.flag = flag;

	Object.defineProperty(exports, '__esModule', { value: true });

})));
//# sourceMappingURL=spritespin.js.map
