/cody/learning-zone/website-templates/johndoe-portfolio-resume-bootstrap-template/js/skrollr.js
/*! * skrollr core * * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr * * Free to use under terms of MIT license */(function(window, document, undefined) {	'use strict';
	/*	 * Global api.	 */	var skrollr = window.skrollr = {		get: function() {			return _instance;		},		//Main entry point.		init: function(options) {			return _instance || new Skrollr(options);		},		VERSION: '0.6.21'	};
	//Minify optimization.	var hasProp = Object.prototype.hasOwnProperty;	var Math = window.Math;	var getStyle = window.getComputedStyle;
	//They will be filled when skrollr gets initialized.	var documentElement;	var body;
	var EVENT_TOUCHSTART = 'touchstart';	var EVENT_TOUCHMOVE = 'touchmove';	var EVENT_TOUCHCANCEL = 'touchcancel';	var EVENT_TOUCHEND = 'touchend';
	var SKROLLABLE_CLASS = 'skrollable';	var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before';	var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between';	var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after';
	var SKROLLR_CLASS = 'skrollr';	var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS;	var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop';	var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile';
	var DEFAULT_EASING = 'linear';	var DEFAULT_DURATION = 1000;//ms	var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms²
	var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms
	var ANCHOR_START = 'start';	var ANCHOR_END = 'end';	var ANCHOR_CENTER = 'center';	var ANCHOR_BOTTOM = 'bottom';
	//The property which will be added to the DOM element to hold the ID of the skrollable.	var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id';
	var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i;
	var rxTrim = /^\s+|\s+$/g;
	//Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor].	var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/;
	var rxPropValue = /\s*([\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi;
	//Easing function names follow the property in square brackets.	var rxPropEasing = /^([a-z\-]+)\[(\w+)\]$/;
	var rxCamelCase = /-([a-z])/g;	var rxCamelCaseFn = function(str, letter) {		return letter.toUpperCase();	};
	//Numeric values with optional sign.	var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g;
	//Used to replace occurences of {?} with a number.	var rxInterpolateString = /\{\?\}/g;
	//Finds rgb(a) colors, which don't use the percentage notation.	var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g;
	//Finds all gradients.	var rxGradient = /[a-z\-]+-gradient/g;
	//Vendor prefix. Will be set once skrollr gets initialized.	var theCSSPrefix = '';	var theDashedCSSPrefix = '';
	//Will be called once (when skrollr gets initialized).	var detectCSSPrefix = function() {		//Only relevant prefixes. May be extended.		//Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.		var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;
		//Detect prefix for current browser by finding the first property using a prefix.		if(!getStyle) {			return;		}
		var style = getStyle(body, null);
		for(var k in style) {			//We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy.			theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes)));
			if(theCSSPrefix) {				break;			}		}
		//Did we even detect a prefix?		if(!theCSSPrefix) {			theCSSPrefix = theDashedCSSPrefix = '';
			return;		}
		theCSSPrefix = theCSSPrefix[0];
		//We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.		if(theCSSPrefix.slice(0,1) === '-') {			theDashedCSSPrefix = theCSSPrefix;
			//There's no logic behind these. Need a look up.			theCSSPrefix = ({				'-webkit-': 'webkit',				'-moz-': 'Moz',				'-ms-': 'ms',				'-o-': 'O'			})[theCSSPrefix];		} else {			theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-';		}	};
	var polyfillRAF = function() {		var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame'];
		var lastTime = _now();
		if(_isMobile || !requestAnimFrame) {			requestAnimFrame = function(callback) {				//How long did it take to render?				var deltaTime = _now() - lastTime;				var delay = Math.max(0, 1000 / 60 - deltaTime);
				return window.setTimeout(function() {					lastTime = _now();					callback();				}, delay);			};		}
		return requestAnimFrame;	};
	var polyfillCAF = function() {		var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame'];
		if(_isMobile || !cancelAnimFrame) {			cancelAnimFrame = function(timeout) {				return window.clearTimeout(timeout);			};		}
		return cancelAnimFrame;	};
	//Built-in easing functions.	var easings = {		begin: function() {			return 0;		},		end: function() {			return 1;		},		linear: function(p) {			return p;		},		quadratic: function(p) {			return p * p;		},		cubic: function(p) {			return p * p * p;		},		swing: function(p) {			return (-Math.cos(p * Math.PI) / 2) + 0.5;		},		sqrt: function(p) {			return Math.sqrt(p);		},		outCubic: function(p) {			return (Math.pow((p - 1), 3) + 1);		},		//see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this		bounce: function(p) {			var a;
			if(p <= 0.5083) {				a = 3;			} else if(p <= 0.8489) {				a = 9;			} else if(p <= 0.96208) {				a = 27;			} else if(p <= 0.99981) {				a = 91;			} else {				return 1;			}
			return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a);		}	};
	/**	 * Constructor.	 */	function Skrollr(options) {		documentElement = document.documentElement;		body = document.body;
		detectCSSPrefix();
		_instance = this;
		options = options || {};
		_constants = options.constants || {};
		//We allow defining custom easings or overwrite existing.		if(options.easing) {			for(var e in options.easing) {				easings[e] = options.easing[e];			}		}
		_edgeStrategy = options.edgeStrategy || 'set';
		_listeners = {			//Function to be called right before rendering.			beforerender: options.beforerender,
			//Function to be called right after finishing rendering.			render: options.render		};
		//forceHeight is true by default		_forceHeight = options.forceHeight !== false;
		if(_forceHeight) {			_scale = options.scale || 1;		}
		_mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION;
		_smoothScrollingEnabled = options.smoothScrolling !== false;		_smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION;
		//Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated.		_smoothScrolling = {			targetTop: _instance.getScrollTop()		};
		//A custom check function may be passed.		_isMobile = ((options.mobileCheck || function() {			return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera);		})());
		if(_isMobile) {			_skrollrBody = document.getElementById('skrollr-body');
			//Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body).			if(_skrollrBody) {				_detect3DTransforms();			}
			_initMobile();			_updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]);		} else {			_updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]);		}
		//Triggers parsing of elements and a first reflow.		_instance.refresh();
		_addEvent(window, 'resize orientationchange', function() {			var width = documentElement.clientWidth;			var height = documentElement.clientHeight;
			//Only reflow if the size actually changed (#271).			if(height !== _lastViewportHeight || width !== _lastViewportWidth) {				_lastViewportHeight = height;				_lastViewportWidth = width;
				_requestReflow = true;			}		});
		var requestAnimFrame = polyfillRAF();
		//Let's go.		(function animloop(){			_render();			_animFrame = requestAnimFrame(animloop);		}());
		return _instance;	}
	/**	 * (Re)parses some or all elements.	 */	Skrollr.prototype.refresh = function(elements) {		var elementIndex;		var elementsLength;		var ignoreID = false;
		//Completely reparse anything without argument.		if(elements === undefined) {			//Ignore that some elements may already have a skrollable ID.			ignoreID = true;
			_skrollables = [];			_skrollableIdCounter = 0;
			elements = document.getElementsByTagName('*');		} else {			//We accept a single element or an array of elements.			elements = [].concat(elements);		}
		elementIndex = 0;		elementsLength = elements.length;
		for(; elementIndex < elementsLength; elementIndex++) {			var el = elements[elementIndex];			var anchorTarget = el;			var keyFrames = [];
			//If this particular element should be smooth scrolled.			var smoothScrollThis = _smoothScrollingEnabled;
			//The edge strategy for this particular element.			var edgeStrategy = _edgeStrategy;
			if(!el.attributes) {				continue;			}
			//Iterate over all attributes and search for key frame attributes.			var attributeIndex = 0;			var attributesLength = el.attributes.length;
			for (; attributeIndex < attributesLength; attributeIndex++) {				var attr = el.attributes[attributeIndex];
				if(attr.name === 'data-anchor-target') {					anchorTarget = document.querySelector(attr.value);
					if(anchorTarget === null) {						throw 'Unable to find anchor target "' + attr.value + '"';					}
					continue;				}
				//Global smooth scrolling can be overridden by the element attribute.				if(attr.name === 'data-smooth-scrolling') {					smoothScrollThis = attr.value !== 'off';
					continue;				}
				//Global edge strategy can be overridden by the element attribute.				if(attr.name === 'data-edge-strategy') {					edgeStrategy = attr.value;
					continue;				}
				var match = attr.name.match(rxKeyframeAttribute);
				if(match === null) {					continue;				}
				var kf = {					props: attr.value,					//Point back to the element as well.					element: el				};
				keyFrames.push(kf);
				var constant = match[1];
				if(constant) {					//Strip the underscore prefix.					kf.constant = constant.substr(1);				}
				//Get the key frame offset.				var offset = match[2];
				//Is it a percentage offset?				if(/p$/.test(offset)) {					kf.isPercentage = true;					kf.offset = (offset.slice(0, -1) | 0) / 100;				} else {					kf.offset = (offset | 0);				}
				var anchor1 = match[3];
				//If second anchor is not set, the first will be taken for both.				var anchor2 = match[4] || anchor1;
				//"absolute" (or "classic") mode, where numbers mean absolute scroll offset.				if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) {					kf.mode = 'absolute';
					//data-end needs to be calculated after all key frames are known.					if(anchor1 === ANCHOR_END) {						kf.isEnd = true;					} else if(!kf.isPercentage) {						//For data-start we can already set the key frame w/o calculations.						//#59: "scale" options should only affect absolute mode.						kf.offset = kf.offset * _scale;					}				}				//"relative" mode, where numbers are relative to anchors.				else {					kf.mode = 'relative';					kf.anchors = [anchor1, anchor2];				}			}
			//Does this element have key frames?			if(!keyFrames.length) {				continue;			}
			//Will hold the original style and class attributes before we controlled the element (see #80).			var styleAttr, classAttr;
			var id;
			if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {				//We already have this element under control. Grab the corresponding skrollable id.				id = el[SKROLLABLE_ID_DOM_PROPERTY];				styleAttr = _skrollables[id].styleAttr;				classAttr = _skrollables[id].classAttr;			} else {				//It's an unknown element. Asign it a new skrollable id.				id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++);				styleAttr = el.style.cssText;				classAttr = _getClass(el);			}
			_skrollables[id] = {				element: el,				styleAttr: styleAttr,				classAttr: classAttr,				anchorTarget: anchorTarget,				keyFrames: keyFrames,				smoothScrolling: smoothScrollThis,				edgeStrategy: edgeStrategy			};
			_updateClass(el, [SKROLLABLE_CLASS], []);		}
		//Reflow for the first time.		_reflow();
		//Now that we got all key frame numbers right, actually parse the properties.		elementIndex = 0;		elementsLength = elements.length;
		for(; elementIndex < elementsLength; elementIndex++) {			var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]];
			if(sk === undefined) {				continue;			}
			//Parse the property string to objects			_parseProps(sk);
			//Fill key frames with missing properties from left and right			_fillProps(sk);		}
		return _instance;	};
	/**	 * Transform "relative" mode to "absolute" mode.	 * That is, calculate anchor position and offset of element.	 */	Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) {		var viewportHeight = documentElement.clientHeight;		var box = element.getBoundingClientRect();		var absolute = box.top;
		//#100: IE doesn't supply "height" with getBoundingClientRect.		var boxHeight = box.bottom - box.top;
		if(viewportAnchor === ANCHOR_BOTTOM) {			absolute -= viewportHeight;		} else if(viewportAnchor === ANCHOR_CENTER) {			absolute -= viewportHeight / 2;		}
		if(elementAnchor === ANCHOR_BOTTOM) {			absolute += boxHeight;		} else if(elementAnchor === ANCHOR_CENTER) {			absolute += boxHeight / 2;		}
		//Compensate scrolling since getBoundingClientRect is relative to viewport.		absolute += _instance.getScrollTop();
		return (absolute + 0.5) | 0;	};
	/**	 * Animates scroll top to new position.	 */	Skrollr.prototype.animateTo = function(top, options) {		options = options || {};
		var now = _now();		var scrollTop = _instance.getScrollTop();
		//Setting this to a new value will automatically cause the current animation to stop, if any.		_scrollAnimation = {			startTop: scrollTop,			topDiff: top - scrollTop,			targetTop: top,			duration: options.duration || DEFAULT_DURATION,			startTime: now,			endTime: now + (options.duration || DEFAULT_DURATION),			easing: easings[options.easing || DEFAULT_EASING],			done: options.done		};
		//Don't queue the animation if there's nothing to animate.		if(!_scrollAnimation.topDiff) {			if(_scrollAnimation.done) {				_scrollAnimation.done.call(_instance, false);			}
			_scrollAnimation = undefined;		}
		return _instance;	};
	/**	 * Stops animateTo animation.	 */	Skrollr.prototype.stopAnimateTo = function() {		if(_scrollAnimation && _scrollAnimation.done) {			_scrollAnimation.done.call(_instance, true);		}
		_scrollAnimation = undefined;	};
	/**	 * Returns if an animation caused by animateTo is currently running.	 */	Skrollr.prototype.isAnimatingTo = function() {		return !!_scrollAnimation;	};
	Skrollr.prototype.setScrollTop = function(top, force) {		_forceRender = (force === true);
		if(_isMobile) {			_mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame);		} else {			window.scrollTo(0, top);		}
		return _instance;	};
	Skrollr.prototype.getScrollTop = function() {		if(_isMobile) {			return _mobileOffset;		} else {			return window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0;		}	};
	Skrollr.prototype.getMaxScrollTop = function() {		return _maxKeyFrame;	};
	Skrollr.prototype.on = function(name, fn) {		_listeners[name] = fn;
		return _instance;	};
	Skrollr.prototype.off = function(name) {		delete _listeners[name];
		return _instance;	};
	Skrollr.prototype.destroy = function() {		var cancelAnimFrame = polyfillCAF();		cancelAnimFrame(_animFrame);		_removeAllEvents();
		_updateClass(documentElement, [NO_SKROLLR_CLASS], [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]);
		var skrollableIndex = 0;		var skrollablesLength = _skrollables.length;
		for(; skrollableIndex < skrollablesLength; skrollableIndex++) {			_reset(_skrollables[skrollableIndex].element);		}
		documentElement.style.overflow = body.style.overflow = 'auto';		documentElement.style.height = body.style.height = 'auto';
		if(_skrollrBody) {			skrollr.setStyle(_skrollrBody, 'transform', 'none');		}
		_instance = undefined;		_skrollrBody = undefined;		_listeners = undefined;		_forceHeight = undefined;		_maxKeyFrame = 0;		_scale = 1;		_constants = undefined;		_mobileDeceleration = undefined;		_direction = 'down';		_lastTop = -1;		_lastViewportWidth = 0;		_lastViewportHeight = 0;		_requestReflow = false;		_scrollAnimation = undefined;		_smoothScrollingEnabled = undefined;		_smoothScrollingDuration = undefined;		_smoothScrolling = undefined;		_forceRender = undefined;		_skrollableIdCounter = 0;		_edgeStrategy = undefined;		_isMobile = false;		_mobileOffset = 0;		_translateZ = undefined;	};
	/*		Private methods.	*/
	var _initMobile = function() {		var initialElement;		var initialTouchY;		var initialTouchX;		var currentElement;		var currentTouchY;		var currentTouchX;		var lastTouchY;		var deltaY;
		var initialTouchTime;		var currentTouchTime;		var lastTouchTime;		var deltaTime;
		_addEvent(documentElement, [EVENT_TOUCHSTART, EVENT_TOUCHMOVE, EVENT_TOUCHCANCEL, EVENT_TOUCHEND].join(' '), function(e) {			var touch = e.changedTouches[0];
			currentElement = e.target;
			//We don't want text nodes.			while(currentElement.nodeType === 3) {				currentElement = currentElement.parentNode;			}
			currentTouchY = touch.clientY;			currentTouchX = touch.clientX;			currentTouchTime = e.timeStamp;
			if(!rxTouchIgnoreTags.test(currentElement.tagName)) {				e.preventDefault();			}
			switch(e.type) {				case EVENT_TOUCHSTART:					//The last element we tapped on.					if(initialElement) {						initialElement.blur();					}
					_instance.stopAnimateTo();
					initialElement = currentElement;
					initialTouchY = lastTouchY = currentTouchY;					initialTouchX = currentTouchX;					initialTouchTime = currentTouchTime;
					break;				case EVENT_TOUCHMOVE:					//Prevent default event on touchIgnore elements in case they don't have focus yet.					if(rxTouchIgnoreTags.test(currentElement.tagName) && document.activeElement !== currentElement) {						e.preventDefault();					}
					deltaY = currentTouchY - lastTouchY;					deltaTime = currentTouchTime - lastTouchTime;
					_instance.setScrollTop(_mobileOffset - deltaY, true);
					lastTouchY = currentTouchY;					lastTouchTime = currentTouchTime;					break;				default:				case EVENT_TOUCHCANCEL:				case EVENT_TOUCHEND:					var distanceY = initialTouchY - currentTouchY;					var distanceX = initialTouchX - currentTouchX;					var distance2 = distanceX * distanceX + distanceY * distanceY;
					//Check if it was more like a tap (moved less than 7px).					if(distance2 < 49) {						if(!rxTouchIgnoreTags.test(initialElement.tagName)) {							initialElement.focus();
							//It was a tap, click the element.							var clickEvent = document.createEvent('MouseEvents');							clickEvent.initMouseEvent('click', true, true, e.view, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null);							initialElement.dispatchEvent(clickEvent);						}
						return;					}
					initialElement = undefined;
					var speed = deltaY / deltaTime;
					//Cap speed at 3 pixel/ms.					speed = Math.max(Math.min(speed, 3), -3);
					var duration = Math.abs(speed / _mobileDeceleration);					var targetOffset = speed * duration + 0.5 * _mobileDeceleration * duration * duration;					var targetTop = _instance.getScrollTop() - targetOffset;
					//Relative duration change for when scrolling above bounds.					var targetRatio = 0;
					//Change duration proportionally when scrolling would leave bounds.					if(targetTop > _maxKeyFrame) {						targetRatio = (_maxKeyFrame - targetTop) / targetOffset;
						targetTop = _maxKeyFrame;					} else if(targetTop < 0) {						targetRatio = -targetTop / targetOffset;
						targetTop = 0;					}
					duration = duration * (1 - targetRatio);
					_instance.animateTo((targetTop + 0.5) | 0, {easing: 'outCubic', duration: duration});					break;			}		});
		//Just in case there has already been some native scrolling, reset it.		window.scrollTo(0, 0);		documentElement.style.overflow = body.style.overflow = 'hidden';	};
	/**	 * Updates key frames which depend on others / need to be updated on resize.	 * That is "end" in "absolute" mode and all key frames in "relative" mode.	 * Also handles constants, because they may change on resize.	 */	var _updateDependentKeyFrames = function() {		var viewportHeight = documentElement.clientHeight;		var processedConstants = _processConstants();		var skrollable;		var element;		var anchorTarget;		var keyFrames;		var keyFrameIndex;		var keyFramesLength;		var kf;		var skrollableIndex;		var skrollablesLength;		var offset;		var constantValue;
		//First process all relative-mode elements and find the max key frame.		skrollableIndex = 0;		skrollablesLength = _skrollables.length;
		for(; skrollableIndex < skrollablesLength; skrollableIndex++) {			skrollable = _skrollables[skrollableIndex];			element = skrollable.element;			anchorTarget = skrollable.anchorTarget;			keyFrames = skrollable.keyFrames;
			keyFrameIndex = 0;			keyFramesLength = keyFrames.length;
			for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {				kf = keyFrames[keyFrameIndex];
				offset = kf.offset;				constantValue = processedConstants[kf.constant] || 0;
				kf.frame = offset;
				if(kf.isPercentage) {					//Convert the offset to percentage of the viewport height.					offset = offset * viewportHeight;
					//Absolute + percentage mode.					kf.frame = offset;				}
				if(kf.mode === 'relative') {					_reset(element);
					kf.frame = _instance.relativeToAbsolute(anchorTarget, kf.anchors[0], kf.anchors[1]) - offset;
					_reset(element, true);				}
				kf.frame += constantValue;
				//Only search for max key frame when forceHeight is enabled.				if(_forceHeight) {					//Find the max key frame, but don't use one of the data-end ones for comparison.					if(!kf.isEnd && kf.frame > _maxKeyFrame) {						_maxKeyFrame = kf.frame;					}				}			}		}
		//#133: The document can be larger than the maxKeyFrame we found.		_maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight());
		//Now process all data-end keyframes.		skrollableIndex = 0;		skrollablesLength = _skrollables.length;
		for(; skrollableIndex < skrollablesLength; skrollableIndex++) {			skrollable = _skrollables[skrollableIndex];			keyFrames = skrollable.keyFrames;
			keyFrameIndex = 0;			keyFramesLength = keyFrames.length;
			for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {				kf = keyFrames[keyFrameIndex];
				constantValue = processedConstants[kf.constant] || 0;
				if(kf.isEnd) {					kf.frame = _maxKeyFrame - kf.offset + constantValue;				}			}
			skrollable.keyFrames.sort(_keyFrameComparator);		}	};
	/**	 * Calculates and sets the style properties for the element at the given frame.	 * @param fakeFrame The frame to render at when smooth scrolling is enabled.	 * @param actualFrame The actual frame we are at.	 */	var _calcSteps = function(fakeFrame, actualFrame) {		//Iterate over all skrollables.		var skrollableIndex = 0;		var skrollablesLength = _skrollables.length;
		for(; skrollableIndex < skrollablesLength; skrollableIndex++) {			var skrollable = _skrollables[skrollableIndex];			var element = skrollable.element;			var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame;			var frames = skrollable.keyFrames;			var firstFrame = frames[0].frame;			var lastFrame = frames[frames.length - 1].frame;			var beforeFirst = frame < firstFrame;			var afterLast = frame > lastFrame;			var firstOrLastFrame = frames[beforeFirst ? 0 : frames.length - 1];			var key;			var value;
			//If we are before/after the first/last frame, set the styles according to the given edge strategy.			if(beforeFirst || afterLast) {				//Check if we already handled this edge case last time.				//Note: using setScrollTop it's possible that we jumped from one edge to the other.				if(beforeFirst && skrollable.edge === -1 || afterLast && skrollable.edge === 1) {					continue;				}
				//Add the skrollr-before or -after class.				_updateClass(element, [beforeFirst ? SKROLLABLE_BEFORE_CLASS : SKROLLABLE_AFTER_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_BETWEEN_CLASS, SKROLLABLE_AFTER_CLASS]);
				//Remember that we handled the edge case (before/after the first/last keyframe).				skrollable.edge = beforeFirst ? -1 : 1;
				switch(skrollable.edgeStrategy) {					case 'reset':						_reset(element);						continue;					case 'ease':						//Handle this case like it would be exactly at first/last keyframe and just pass it on.						frame = firstOrLastFrame.frame;						break;					default:					case 'set':						var props = firstOrLastFrame.props;
						for(key in props) {							if(hasProp.call(props, key)) {								value = _interpolateString(props[key].value);
								skrollr.setStyle(element, key, value);							}						}
						continue;				}			} else {				//Did we handle an edge last time?				if(skrollable.edge !== 0) {					_updateClass(element, [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]);					skrollable.edge = 0;				}			}
			//Find out between which two key frames we are right now.			var keyFrameIndex = 0;			var framesLength = frames.length - 1;
			for(; keyFrameIndex < framesLength; keyFrameIndex++) {				if(frame >= frames[keyFrameIndex].frame && frame <= frames[keyFrameIndex + 1].frame) {					var left = frames[keyFrameIndex];					var right = frames[keyFrameIndex + 1];
					for(key in left.props) {						if(hasProp.call(left.props, key)) {							var progress = (frame - left.frame) / (right.frame - left.frame);
							//Transform the current progress using the given easing function.							progress = left.props[key].easing(progress);
							//Interpolate between the two values							value = _calcInterpolation(left.props[key].value, right.props[key].value, progress);
							value = _interpolateString(value);
							skrollr.setStyle(element, key, value);						}					}
					break;				}			}		}	};
	/**	 * Renders all elements.	 */	var _render = function() {		if(_requestReflow) {			_requestReflow = false;			_reflow();		}
		//We may render something else than the actual scrollbar position.		var renderTop = _instance.getScrollTop();
		//If there's an animation, which ends in current render call, call the callback after rendering.		var afterAnimationCallback;		var now = _now();		var progress;
		//Before actually rendering handle the scroll animation, if any.		if(_scrollAnimation) {			//It's over			if(now >= _scrollAnimation.endTime) {				renderTop = _scrollAnimation.targetTop;				afterAnimationCallback = _scrollAnimation.done;				_scrollAnimation = undefined;			} else {				//Map the current progress to the new progress using given easing function.				progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration);
				renderTop = (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0;			}
			_instance.setScrollTop(renderTop, true);		}		//Smooth scrolling only if there's no animation running and if we're not forcing the rendering.		else if(!_forceRender) {			var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop;
			//The user scrolled, start new smooth scrolling.			if(smoothScrollingDiff) {				_smoothScrolling = {					startTop: _lastTop,					topDiff: renderTop - _lastTop,					targetTop: renderTop,					startTime: _lastRenderCall,					endTime: _lastRenderCall + _smoothScrollingDuration				};			}
			//Interpolate the internal scroll position (not the actual scrollbar).			if(now <= _smoothScrolling.endTime) {				//Map the current progress to the new progress using easing function.				progress = easings.sqrt((now - _smoothScrolling.startTime) / _smoothScrollingDuration);
				renderTop = (_smoothScrolling.startTop + progress * _smoothScrolling.topDiff) | 0;			}		}
		//That's were we actually "scroll" on mobile.		if(_isMobile && _skrollrBody) {			//Set the transform ("scroll it").			skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ);		}
		//Did the scroll position even change?		if(_forceRender || _lastTop !== renderTop) {			//Remember in which direction are we scrolling?			_direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction);
			_forceRender = false;
			var listenerParams = {				curTop: renderTop,				lastTop: _lastTop,				maxTop: _maxKeyFrame,				direction: _direction			};
			//Tell the listener we are about to render.			var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams);
			//The beforerender listener function is able the cancel rendering.			if(continueRendering !== false) {				//Now actually interpolate all the styles.				_calcSteps(renderTop, _instance.getScrollTop());
				//Remember when we last rendered.				_lastTop = renderTop;
				if(_listeners.render) {					_listeners.render.call(_instance, listenerParams);				}			}
			if(afterAnimationCallback) {				afterAnimationCallback.call(_instance, false);			}		}
		_lastRenderCall = now;	};
	/**	 * Parses the properties for each key frame of the given skrollable.	 */	var _parseProps = function(skrollable) {		//Iterate over all key frames		var keyFrameIndex = 0;		var keyFramesLength = skrollable.keyFrames.length;
		for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {			var frame = skrollable.keyFrames[keyFrameIndex];			var easing;			var value;			var prop;			var props = {};
			var match;
			while((match = rxPropValue.exec(frame.props)) !== null) {				prop = match[1];				value = match[2];
				easing = prop.match(rxPropEasing);
				//Is there an easing specified for this prop?				if(easing !== null) {					prop = easing[1];					easing = easing[2];				} else {					easing = DEFAULT_EASING;				}
				//Exclamation point at first position forces the value to be taken literal.				value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)];
				//Save the prop for this key frame with his value and easing function				props[prop] = {					value: value,					easing: easings[easing]				};			}
			frame.props = props;		}	};
	/**	 * Parses a value extracting numeric values and generating a format string	 * for later interpolation of the new values in old string.	 *	 * @param val The CSS value to be parsed.	 * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7]	 * where the first element is the format string later used	 * and all following elements are the numeric value.	 */	var _parseProp = function(val) {		var numbers = [];
		//One special case, where floats don't work.		//We replace all occurences of rgba colors		//which don't use percentage notation with the percentage notation.		rxRGBAIntegerColor.lastIndex = 0;		val = val.replace(rxRGBAIntegerColor, function(rgba) {			return rgba.replace(rxNumericValue, function(n) {				return n / 255 * 100 + '%';			});		});
		//Handle prefixing of "gradient" values.		//For now only the prefixed value will be set. Unprefixed isn't supported anyway.		if(theDashedCSSPrefix) {			rxGradient.lastIndex = 0;			val = val.replace(rxGradient, function(s) {				return theDashedCSSPrefix + s;			});		}
		//Now parse ANY number inside this string and create a format string.		val = val.replace(rxNumericValue, function(n) {			numbers.push(+n);			return '{?}';		});
		//Add the formatstring as first value.		numbers.unshift(val);
		return numbers;	};
	/**	 * Fills the key frames with missing left and right hand properties.	 * If key frame 1 has property X and key frame 2 is missing X,	 * but key frame 3 has X again, then we need to assign X to key frame 2 too.	 *	 * @param sk A skrollable.	 */	var _fillProps = function(sk) {		//Will collect the properties key frame by key frame		var propList = {};		var keyFrameIndex;		var keyFramesLength;
		//Iterate over all key frames from left to right		keyFrameIndex = 0;		keyFramesLength = sk.keyFrames.length;
		for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {			_fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);		}
		//Now do the same from right to fill the last gaps
		propList = {};
		//Iterate over all key frames from right to left		keyFrameIndex = sk.keyFrames.length - 1;
		for(; keyFrameIndex >= 0; keyFrameIndex--) {			_fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);		}	};
	var _fillPropForFrame = function(frame, propList) {		var key;
		//For each key frame iterate over all right hand properties and assign them,		//but only if the current key frame doesn't have the property by itself		for(key in propList) {			//The current frame misses this property, so assign it.			if(!hasProp.call(frame.props, key)) {				frame.props[key] = propList[key];			}		}
		//Iterate over all props of the current frame and collect them		for(key in frame.props) {			propList[key] = frame.props[key];		}	};
	/**	 * Calculates the new values for two given values array.	 */	var _calcInterpolation = function(val1, val2, progress) {		var valueIndex;		var val1Length = val1.length;
		//They both need to have the same length		if(val1Length !== val2.length) {			throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"';		}
		//Add the format string as first element.		var interpolated = [val1[0]];
		valueIndex = 1;
		for(; valueIndex < val1Length; valueIndex++) {			//That's the line where the two numbers are actually interpolated.			interpolated[valueIndex] = val1[valueIndex] + ((val2[valueIndex] - val1[valueIndex]) * progress);		}
		return interpolated;	};
	/**	 * Interpolates the numeric values into the format string.	 */	var _interpolateString = function(val) {		var valueIndex = 1;
		rxInterpolateString.lastIndex = 0;
		return val[0].replace(rxInterpolateString, function() {			return val[valueIndex++];		});	};
	/**	 * Resets the class and style attribute to what it was before skrollr manipulated the element.	 * Also remembers the values it had before reseting, in order to undo the reset.	 */	var _reset = function(elements, undo) {		//We accept a single element or an array of elements.		elements = [].concat(elements);
		var skrollable;		var element;		var elementsIndex = 0;		var elementsLength = elements.length;
		for(; elementsIndex < elementsLength; elementsIndex++) {			element = elements[elementsIndex];			skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]];
			//Couldn't find the skrollable for this DOM element.			if(!skrollable) {				continue;			}
			if(undo) {				//Reset class and style to the "dirty" (set by skrollr) values.				element.style.cssText = skrollable.dirtyStyleAttr;				_updateClass(element, skrollable.dirtyClassAttr);			} else {				//Remember the "dirty" (set by skrollr) class and style.				skrollable.dirtyStyleAttr = element.style.cssText;				skrollable.dirtyClassAttr = _getClass(element);
				//Reset class and style to what it originally was.				element.style.cssText = skrollable.styleAttr;				_updateClass(element, skrollable.classAttr);			}		}	};
	/**	 * Detects support for 3d transforms by applying it to the skrollr-body.	 */	var _detect3DTransforms = function() {		_translateZ = 'translateZ(0)';		skrollr.setStyle(_skrollrBody, 'transform', _translateZ);
		var computedStyle = getStyle(_skrollrBody);		var computedTransform = computedStyle.getPropertyValue('transform');		var computedTransformWithPrefix = computedStyle.getPropertyValue(theDashedCSSPrefix + 'transform');		var has3D = (computedTransform && computedTransform !== 'none') || (computedTransformWithPrefix && computedTransformWithPrefix !== 'none');
		if(!has3D) {			_translateZ = '';		}	};
	/**	 * Set the CSS property on the given element. Sets prefixed properties as well.	 */	skrollr.setStyle = function(el, prop, val) {		var style = el.style;
		//Camel case.		prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace('-', '');
		//Make sure z-index gets a <integer>.		//This is the only <integer> case we need to handle.		if(prop === 'zIndex') {			if(isNaN(val)) {				//If it's not a number, don't touch it.				//It could for example be "auto" (#351).				style[prop] = val;			} else {				//Floor the number.				style[prop] = '' + (val | 0);			}		}		//#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE.		else if(prop === 'float') {			style.styleFloat = style.cssFloat = val;		}		else {			//Need try-catch for old IE.			try {				//Set prefixed property if there's a prefix.				if(theCSSPrefix) {					style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val;				}
				//Set unprefixed.				style[prop] = val;			} catch(ignore) {}		}	};
	/**	 * Cross browser event handling.	 */	var _addEvent = skrollr.addEvent = function(element, names, callback) {		var intermediate = function(e) {			//Normalize IE event stuff.			e = e || window.event;
			if(!e.target) {				e.target = e.srcElement;			}
			if(!e.preventDefault) {				e.preventDefault = function() {					e.returnValue = false;				};			}
			return callback.call(this, e);		};
		names = names.split(' ');
		var name;		var nameCounter = 0;		var namesLength = names.length;
		for(; nameCounter < namesLength; nameCounter++) {			name = names[nameCounter];
			if(element.addEventListener) {				element.addEventListener(name, callback, false);			} else {				element.attachEvent('on' + name, intermediate);			}
			//Remember the events to be able to flush them later.			_registeredEvents.push({				element: element,				name: name,				listener: callback			});		}	};
	var _removeEvent = skrollr.removeEvent = function(element, names, callback) {		names = names.split(' ');
		var nameCounter = 0;		var namesLength = names.length;
		for(; nameCounter < namesLength; nameCounter++) {			if(element.removeEventListener) {				element.removeEventListener(names[nameCounter], callback, false);			} else {				element.detachEvent('on' + names[nameCounter], callback);			}		}	};
	var _removeAllEvents = function() {		var eventData;		var eventCounter = 0;		var eventsLength = _registeredEvents.length;
		for(; eventCounter < eventsLength; eventCounter++) {			eventData = _registeredEvents[eventCounter];
			_removeEvent(eventData.element, eventData.name, eventData.listener);		}
		_registeredEvents = [];	};
	var _reflow = function() {		var pos = _instance.getScrollTop();
		//Will be recalculated by _updateDependentKeyFrames.		_maxKeyFrame = 0;
		if(_forceHeight && !_isMobile) {			//un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216).			body.style.height = 'auto';		}
		_updateDependentKeyFrames();
		if(_forceHeight && !_isMobile) {			//"force" the height.			body.style.height = (_maxKeyFrame + documentElement.clientHeight) + 'px';		}
		//The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom).		if(_isMobile) {			_instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame));		} else {			//Remember and reset the scroll pos (#217).			_instance.setScrollTop(pos, true);		}
		_forceRender = true;	};
	/*	 * Returns a copy of the constants object where all functions and strings have been evaluated.	 */	var _processConstants = function() {		var viewportHeight = documentElement.clientHeight;		var copy = {};		var prop;		var value;
		for(prop in _constants) {			value = _constants[prop];
			if(typeof value === 'function') {				value = value.call(_instance);			}			//Percentage offset.			else if((/p$/).test(value)) {				value = (value.slice(0, -1) / 100) * viewportHeight;			}
			copy[prop] = value;		}
		return copy;	};
	/*	 * Returns the height of the document.	 */	var _getDocumentHeight = function() {		var skrollrBodyHeight = (_skrollrBody && _skrollrBody.offsetHeight || 0);		var bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight);
		return bodyHeight - documentElement.clientHeight;	};
	/**	 * Returns a string of space separated classnames for the current element.	 * Works with SVG as well.	 */	var _getClass = function(element) {		var prop = 'className';
		//SVG support by using className.baseVal instead of just className.		if(window.SVGElement && element instanceof window.SVGElement) {			element = element[prop];			prop = 'baseVal';		}
		return element[prop];	};
	/**	 * Adds and removes a CSS classes.	 * Works with SVG as well.	 * add and remove are arrays of strings,	 * or if remove is ommited add is a string and overwrites all classes.	 */	var _updateClass = function(element, add, remove) {		var prop = 'className';
		//SVG support by using className.baseVal instead of just className.		if(window.SVGElement && element instanceof window.SVGElement) {			element = element[prop];			prop = 'baseVal';		}
		//When remove is ommited, we want to overwrite/set the classes.		if(remove === undefined) {			element[prop] = add;			return;		}
		//Cache current classes. We will work on a string before passing back to DOM.		var val = element[prop];
		//All classes to be removed.		var classRemoveIndex = 0;		var removeLength = remove.length;
		for(; classRemoveIndex < removeLength; classRemoveIndex++) {			val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' ');		}
		val = _trim(val);
		//All classes to be added.		var classAddIndex = 0;		var addLength = add.length;
		for(; classAddIndex < addLength; classAddIndex++) {			//Only add if el not already has class.			if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) {				val += ' ' + add[classAddIndex];			}		}
		element[prop] = _trim(val);	};
	var _trim = function(a) {		return a.replace(rxTrim, '');	};
	/**	 * Adds a space before and after the string.	 */	var _untrim = function(a) {		return ' ' + a + ' ';	};
	var _now = Date.now || function() {		return +new Date();	};
	var _keyFrameComparator = function(a, b) {		return a.frame - b.frame;	};
	/*	 * Private variables.	 */
	//Singleton	var _instance;
	/*		A list of all elements which should be animated associated with their the metadata.		Exmaple skrollable with two key frames animating from 100px width to 20px:
		skrollable = {			element: <the DOM element>,			styleAttr: <style attribute of the element before skrollr>,			classAttr: <class attribute of the element before skrollr>,			keyFrames: [				{					frame: 100,					props: {						width: {							value: ['{?}px', 100],							easing: <reference to easing function>						}					},					mode: "absolute"				},				{					frame: 200,					props: {						width: {							value: ['{?}px', 20],							easing: <reference to easing function>						}					},					mode: "absolute"				}			]		};	*/	var _skrollables;
	var _skrollrBody;
	var _listeners;	var _forceHeight;	var _maxKeyFrame = 0;
	var _scale = 1;	var _constants;
	var _mobileDeceleration;
	//Current direction (up/down).	var _direction = 'down';
	//The last top offset value. Needed to determine direction.	var _lastTop = -1;
	//The last time we called the render method (doesn't mean we rendered!).	var _lastRenderCall = _now();
	//For detecting if it actually resized (#271).	var _lastViewportWidth = 0;	var _lastViewportHeight = 0;
	var _requestReflow = false;
	//Will contain data about a running scrollbar animation, if any.	var _scrollAnimation;
	var _smoothScrollingEnabled;
	var _smoothScrollingDuration;
	//Will contain settins for smooth scrolling if enabled.	var _smoothScrolling;
	//Can be set by any operation/event to force rendering even if the scrollbar didn't move.	var _forceRender;
	//Each skrollable gets an unique ID incremented for each skrollable.	//The ID is the index in the _skrollables array.	var _skrollableIdCounter = 0;
	var _edgeStrategy;

	//Mobile specific vars. Will be stripped by UglifyJS when not in use.	var _isMobile = false;
	//The virtual scroll offset when using mobile scrolling.	var _mobileOffset = 0;
	//If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise).	var _translateZ;
	//Will contain data about registered events by skrollr.	var _registeredEvents = [];
	//Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported).	var _animFrame;}(window, document));