App-revealup

 view release on metacpan or  search on metacpan

share/revealjs/js/controllers/autoanimate.js  view on Meta::CPAN

import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js'
import { FRAGMENT_STYLE_REGEX } from '../utils/constants.js'

// Counter used to generate unique IDs for auto-animated elements
let autoAnimateCounter = 0;

/**
 * Automatically animates matching elements across
 * slides with the [data-auto-animate] attribute.
 */
export default class AutoAnimate {

	constructor( Reveal ) {

		this.Reveal = Reveal;

	}

	/**
	 * Runs an auto-animation between the given slides.
	 *
	 * @param  {HTMLElement} fromSlide
	 * @param  {HTMLElement} toSlide
	 */
	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 both slides are auto-animate targets with the same data-auto-animate-id value
		// (including null if absent on both) and that data-auto-animate-restart isn't set on the
		// physically latter slide (independent of slide direction)
		if( 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',

share/revealjs/js/controllers/autoanimate.js  view on Meta::CPAN

		}

	}

	/**
	 * Rolls back all changes that we've made to the DOM so
	 * that as part of animating.
	 */
	reset() {

		// Reset slides
		queryAll( this.Reveal.getRevealElement(), '[data-auto-animate]:not([data-auto-animate=""])' ).forEach( element => {
			element.dataset.autoAnimate = '';
		} );

		// Reset elements
		queryAll( this.Reveal.getRevealElement(), '[data-auto-animate-target]' ).forEach( element => {
			delete element.dataset.autoAnimateTarget;
		} );

		// Remove the animation sheet
		if( this.autoAnimateStyleSheet && this.autoAnimateStyleSheet.parentNode ) {
			this.autoAnimateStyleSheet.parentNode.removeChild( this.autoAnimateStyleSheet );
			this.autoAnimateStyleSheet = null;
		}

	}

	/**
	 * Creates a FLIP animation where the `to` element starts out
	 * in the `from` element position and animates to its original
	 * state.
	 *
	 * @param {HTMLElement} from
	 * @param {HTMLElement} to
	 * @param {Object} elementOptions Options for this element pair
	 * @param {Object} animationOptions Options set at the slide level
	 * @param {String} id Unique ID that we can use to identify this
	 * auto-animate element in the DOM
	 */
	autoAnimateElements( from, to, elementOptions, animationOptions, id ) {

		// 'from' elements are given a data-auto-animate-target with no value,
		// 'to' elements are are given a data-auto-animate-target with an ID
		from.dataset.autoAnimateTarget = '';
		to.dataset.autoAnimateTarget = id;

		// Each element may override any of the auto-animate options
		// like transition easing, duration and delay via data-attributes
		let options = this.getAutoAnimateOptions( to, animationOptions );

		// If we're using a custom element matcher the element options
		// may contain additional transition overrides
		if( typeof elementOptions.delay !== 'undefined' ) options.delay = elementOptions.delay;
		if( typeof elementOptions.duration !== 'undefined' ) options.duration = elementOptions.duration;
		if( typeof elementOptions.easing !== 'undefined' ) options.easing = elementOptions.easing;

		let fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ),
			toProps = this.getAutoAnimatableProperties( 'to', to, elementOptions );

		// Maintain fragment visibility for matching elements when
		// we're navigating forwards, this way the viewer won't need
		// to step through the same fragments twice
		if( to.classList.contains( 'fragment' ) ) {

			// Don't auto-animate the opacity of fragments to avoid
			// conflicts with fragment animations
			delete toProps.styles['opacity'];

			if( from.classList.contains( 'fragment' ) ) {

				let fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
				let toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];

				// Only skip the fragment if the fragment animation style
				// remains unchanged
				if( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) {
					to.classList.add( 'visible', 'disabled' );
				}

			}

		}

		// If translation and/or scaling are enabled, css transform
		// the 'to' element so that it matches the position and size
		// of the 'from' element
		if( elementOptions.translate !== false || elementOptions.scale !== false ) {

			let presentationScale = this.Reveal.getScale();

			let delta = {
				x: ( fromProps.x - toProps.x ) / presentationScale,
				y: ( fromProps.y - toProps.y ) / presentationScale,
				scaleX: fromProps.width / toProps.width,
				scaleY: fromProps.height / toProps.height
			};

			// Limit decimal points to avoid 0.0001px blur and stutter
			delta.x = Math.round( delta.x * 1000 ) / 1000;
			delta.y = Math.round( delta.y * 1000 ) / 1000;
			delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
			delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;

			let translate = elementOptions.translate !== false && ( delta.x !== 0 || delta.y !== 0 ),
				scale = elementOptions.scale !== false && ( delta.scaleX !== 0 || delta.scaleY !== 0 );

			// No need to transform if nothing's changed
			if( translate || scale ) {

				let transform = [];

				if( translate ) transform.push( `translate(${delta.x}px, ${delta.y}px)` );
				if( scale ) transform.push( `scale(${delta.scaleX}, ${delta.scaleY})` );

				fromProps.styles['transform'] = transform.join( ' ' );
				fromProps.styles['transform-origin'] = 'top left';

				toProps.styles['transform'] = 'none';

			}

		}

		// Delete all unchanged 'to' styles
		for( let propertyName in toProps.styles ) {
			const toValue = toProps.styles[propertyName];
			const fromValue = fromProps.styles[propertyName];

			if( toValue === fromValue ) {
				delete toProps.styles[propertyName];
			}
			else {
				// If these property values were set via a custom matcher providing
				// an explicit 'from' and/or 'to' value, we always inject those values.



( run in 0.840 second using v1.01-cache-2.11-cpan-b16cb0d3907 )