run( fromSlide, toSlide ) {

		// Clean up after prior animations
		this.reset();

		let allSlides = this.Reveal.getSlides();
		let toSlideIndex = allSlides.indexOf( toSlide );
		let fromSlideIndex = allSlides.indexOf( fromSlide );

		// Ensure that;
		// 1. Both slides exist.
		// 2. Both slides are auto-animate targets with the same
		//    data-auto-animate-id value (including null if absent on both).
		// 3. data-auto-animate-restart isn't set on the physically latter
		//    slide (independent of slide direction).
		if( fromSlide && toSlide && fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )
				&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' ) 
				&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {

			// Create a new auto-animate sheet
			this.autoAnimateStyleSheet = this.autoAnimateStyleSheet || createStyleSheet();

			let animationOptions = this.getAutoAnimateOptions( toSlide );

			// Set our starting state
			fromSlide.dataset.autoAnimate = 'pending';
			toSlide.dataset.autoAnimate = 'pending';

			// Flag the navigation direction, needed for fragment buildup
			animationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward';

			// If the from-slide is hidden because it has moved outside
			// the view distance, we need to temporarily show it while
			// measuring
			let fromSlideIsHidden = fromSlide.style.display === 'none';
			if( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display;

			// Inject our auto-animate styles for this transition
			let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => {
				return this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ );
			} );

			if( fromSlideIsHidden ) fromSlide.style.display = 'none';

			// Animate unmatched elements, if enabled
			if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) {

				// Our default timings for unmatched elements
				let defaultUnmatchedDuration = animationOptions.duration * 0.8,
					defaultUnmatchedDelay = animationOptions.duration * 0.2;

				this.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => {

					let unmatchedOptions = this.getAutoAnimateOptions( unmatchedElement, animationOptions );
					let id = 'unmatched';

					// If there is a duration or delay set specifically for this
					// element our unmatched elements should adhere to those
					if( unmatchedOptions.duration !== animationOptions.duration || unmatchedOptions.delay !== animationOptions.delay ) {
						id = 'unmatched-' + autoAnimateCounter++;
						css.push( `[data-auto-animate="running"] [data-auto-animate-target="${id}"] { transition: opacity ${unmatchedOptions.duration}s ease ${unmatchedOptions.delay}s; }` );
					}

					unmatchedElement.dataset.autoAnimateTarget = id;

				}, this );

				// Our default transition for unmatched elements
				css.push( `[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${defaultUnmatchedDuration}s ease ${defaultUnmatchedDelay}s; }` );

			}

			// Setting the whole chunk of CSS at once is the most
			// efficient way to do this. Using sheet.insertRule
			// is multiple factors slower.
			this.autoAnimateStyleSheet.innerHTML = css.join( '' );

			// Start the animation next cycle
			requestAnimationFrame( () => {
				if( this.autoAnimateStyleSheet ) {
					// This forces our newly injected styles to be applied in Firefox
					getComputedStyle( this.autoAnimateStyleSheet ).fontWeight;

					toSlide.dataset.autoAnimate = 'running';
				}
			} );

			this.Reveal.dispatchEvent({
				type: 'autoanimate',
				data: {
					fromSlide,
					toSlide,
					sheet: this.autoAnimateStyleSheet
				}
			});

		}

	}
Runs an auto-animation between the given slides.

@param  {HTMLElement} fromSlide
@param  {HTMLElement} toSlide