auto-animate applies styles via stylesheet to avoid changing the slide dom

This commit is contained in:
Hakim El Hattab 2020-02-05 15:55:08 +01:00
parent b6b94739e2
commit 4802a2b7f4
3 changed files with 151 additions and 131 deletions

View file

@ -990,17 +990,6 @@ $controlsArrowAngleActive: 36deg;
} }
/*********************************************
* AUTO ANIMATE
*********************************************/
.reveal section[data-auto-animate] .auto-animate-target {
transition-property: all;
transform-origin: top left;
}
/********************************************* /*********************************************
* PAUSED MODE * PAUSED MODE
*********************************************/ *********************************************/

View file

@ -190,6 +190,11 @@
// Can be used to globally disable auto-animation // Can be used to globally disable auto-animation
autoAnimate: true, autoAnimate: true,
// Optionally provide a custom element matcher function,
// the function needs to return an array where each value is
// an array of animation pairs [fromElement, toElement]
autoAnimateMatcher: null,
// Default settings for or auto-animate transitions, can be // Default settings for or auto-animate transitions, can be
// overridden per-slide via data arguments // overridden per-slide via data arguments
autoAnimateEasing: 'ease', autoAnimateEasing: 'ease',
@ -197,16 +202,19 @@
// CSS styles that auto-animations will animate between // CSS styles that auto-animations will animate between
autoAnimateStyles: [ autoAnimateStyles: [
'opacity', { property: 'opacity' },
'color', { property: 'color' },
'backgroundColor', { property: 'backgroundColor' },
'font-size', { property: 'padding', defaultValue: 'computed' },
'line-height', { property: 'font-size', defaultValue: 'computed' },
'letter-spacing', { property: 'line-height', defaultValue: 'computed' },
'border-top-left-radius', { property: 'letter-spacing', defaultValue: 'computed' },
'border-top-right-radius', { property: 'border-width', defaultValue: 'computed' },
'border-bottom-left-radius', { property: 'border-color' },
'border-bottom-right-radius' { property: 'border-top-left-radius' },
{ property: 'border-top-right-radius' },
{ property: 'border-bottom-left-radius' },
{ property: 'border-bottom-right-radius' }
], ],
// Controls automatic progression to the next slide // Controls automatic progression to the next slide
@ -383,9 +391,8 @@
// Flags if the interaction event listeners are bound // Flags if the interaction event listeners are bound
eventsAreBound = false, eventsAreBound = false,
// A list of all elements that we have animated through // <style> element used to apply auto-animations
// auto-animations autoAnimateStyleSheet,
autoAnimatedRollbacks = [],
// The current auto-slide duration // The current auto-slide duration
autoSlide = 0, autoSlide = 0,
@ -1417,10 +1424,16 @@
} }
// Reset all auto animated elements // Reset all auto animated elements
autoAnimatedRollbacks.forEach( function( rollback ) { toArray( dom.wrapper.querySelectorAll( '.slides .auto-animate-start' ) ).forEach( function( element ) {
rollback(); element.classList.remove( 'auto-animate-start' );
} ); } );
autoAnimatedRollbacks = []; toArray( dom.wrapper.querySelectorAll( '[data-auto-animate-id]' ) ).forEach( function( element ) {
element.removeAttribute( 'data-auto-animate-id' );
} );
if( autoAnimateStyleSheet && autoAnimateStyleSheet.parentNode ) {
autoAnimateStyleSheet.parentNode.removeChild( autoAnimateStyleSheet );
autoAnimateStyleSheet = null;
}
// Remove existing auto-slide controls // Remove existing auto-slide controls
if( autoSlidePlayer ) { if( autoSlidePlayer ) {
@ -3036,8 +3049,8 @@
currentSlide.style.transition = 'none'; currentSlide.style.transition = 'none';
setTimeout( function() { setTimeout( function() {
previousSlide.style.transition = ''; if( previousSlide ) previousSlide.style.transition = '';
currentSlide.style.transition = ''; if( currentSlide ) currentSlide.style.transition = '';
}, 0 ); }, 0 );
} }
@ -3813,7 +3826,13 @@
if( config.autoAnimate ) { if( config.autoAnimate ) {
var options = { // Lazily create the auto-animate stylesheet
if( !autoAnimateStyleSheet ) {
autoAnimateStyleSheet = document.createElement( 'style' );
document.querySelector( 'head' ).appendChild( autoAnimateStyleSheet );
}
var animationOptions = {
easing: config.autoAnimateEasing, easing: config.autoAnimateEasing,
duration: config.autoAnimateDuration, duration: config.autoAnimateDuration,
offsetY: 0 offsetY: 0
@ -3823,22 +3842,34 @@
// account for their difference in position when // account for their difference in position when
// calculating deltas for animated elements // calculating deltas for animated elements
if( config.center ) { if( config.center ) {
options.offsetY = fromSlide.offsetTop - toSlide.offsetTop; animationOptions.offsetY = fromSlide.offsetTop - toSlide.offsetTop;
} }
// Check if easing is overriden // Check if easing is overriden
if( toSlide.hasAttribute( 'data-auto-animate-easing' ) ) { if( toSlide.hasAttribute( 'data-auto-animate-easing' ) ) {
options.easing = toSlide.getAttribute( 'data-auto-animate-easing' ); animationOptions.easing = toSlide.getAttribute( 'data-auto-animate-easing' );
} }
// Check if the duration is overriden // Check if the duration is overriden
if( toSlide.hasAttribute( 'data-auto-animate-duration' ) ) { if( toSlide.hasAttribute( 'data-auto-animate-duration' ) ) {
options.duration = parseFloat( toSlide.getAttribute( 'data-auto-animate-duration' ) ); animationOptions.duration = parseFloat( toSlide.getAttribute( 'data-auto-animate-duration' ) );
} }
getAutoAnimatableElements( fromSlide, toSlide ).forEach( function( elements ) { // Reset any prior animation
autoAnimateElement( elements[0], elements[1], options ); fromSlide.classList.remove( 'auto-animate-start' );
} ); toSlide.classList.remove( 'auto-animate-start' );
autoAnimateStyleSheet.innerHTML = '';
// Generate and write out custom auto-animate styles to the DOM
autoAnimateStyleSheet.innerHTML = getAutoAnimatableElements( fromSlide, toSlide ).map( function( elements, i ) {
return getAutoAnimateCSS( elements[0], elements[1], elements[2] || {}, animationOptions, i );
} ).join( '' );
// Start the animation next cycle
setTimeout( function() {
toSlide.classList.add( 'auto-animate-start' );
}, 0 );
} }
@ -3850,41 +3881,60 @@
* *
* @param {HTMLElement} from * @param {HTMLElement} from
* @param {HTMLElement} to * @param {HTMLElement} to
* @param {Object} options * @param {Object} options Optional settings for this specific pair
* @param {Object} animationOptions Options that apply to all
* elements in this transition
*/ */
function autoAnimateElement( from, to, options ) { function getAutoAnimateCSS( from, to, options, animationOptions, id ) {
var fromProps = getAutoAnimatableProperties( from ), // Each element gets a unique auto-animate ID
toProps = getAutoAnimatableProperties( to ); to.setAttribute( 'data-auto-animate-id', id );
var delta = { var fromProps = getAutoAnimatableProperties( 'from', from, options ),
x: fromProps.x - toProps.x, toProps = getAutoAnimatableProperties( 'to', to, options );
y: fromProps.y - toProps.y + options.offsetY,
scaleX: fromProps.width / toProps.width,
scaleY: fromProps.height / toProps.height
};
to.style.transition = 'none'; // Instantly move to the 'from' state
to.style.transform = 'translate('+delta.x+'px, '+delta.y+'px) scale('+delta.scaleX+','+delta.scaleY+')'; fromProps.styles.push([ 'transition', 'none' ]);
to.classList.add( 'auto-animate-target' );
config.autoAnimateStyles.forEach( function( propertyName ) { // transition to the 'to' state
to.style[propertyName] = fromProps[propertyName]; toProps.styles.push([ 'transition', 'all '+ animationOptions.duration +'s '+ animationOptions.easing ]);
} );
setTimeout( function() { // If translation and/or scalin are enabled, offset the
// 'to' element so that it starts out at the same position
// and scale as the 'from' element
if( options.translate !== false || options.scale !== false ) {
// Run the FLIP animation var delta = {
to.style.transition = ''; x: fromProps.x - toProps.x,
to.style.transitionTimingFunction = options.easing; y: fromProps.y - toProps.y + animationOptions.offsetY,
to.style.transitionDuration = options.duration + 's'; scaleX: fromProps.width / toProps.width,
to.style.transform = ''; scaleY: fromProps.height / toProps.height
};
config.autoAnimateStyles.forEach( function( propertyName ) { var transform = [];
to.style[propertyName] = toProps[propertyName];
} );
}, 0 ); if( options.translate !== false ) transform.push( 'translate('+delta.x+'px, '+delta.y+'px)' );
if( options.scale !== false ) transform.push( 'scale('+delta.scaleX+','+delta.scaleY+')' );
fromProps.styles.push([ 'transform', transform.join( ' ' ) ]);
fromProps.styles.push([ 'transformOrigin', 'top left' ]);
toProps.styles.push([ 'transform', 'none' ]);
}
// Build up our custom CSS. We need to override inline styles
// so we need to make our styles vErY IMPORTANT!1!!
var fromCSS = fromProps.styles.map( function( style ) {
return style[0] + ': ' + style[1] + ' !important;';
} ).join( '' );
var toCSS = toProps.styles.map( function( style ) {
return style[0] + ': ' + style[1] + ' !important;';
} ).join( '' );
return '.reveal [data-auto-animate-id="'+ id +'"] {\n'+ fromCSS +'\n}\n\n' +
'.reveal .auto-animate-start [data-auto-animate-id="'+ id +'"] {\n'+ toCSS +'\n}\n\n';
} }
@ -3893,53 +3943,52 @@
* that can be auto-animated for the given element * that can be auto-animated for the given element
* and their respective values. * and their respective values.
*/ */
function getAutoAnimatableProperties( element ) { function getAutoAnimatableProperties( direction, element, options ) {
var properties = element._animatableProperties; var properties = { styles: [] };
if( !properties ) { // Position and size
if( options.translate !== false || options.scale !== false ) {
properties = {};
// Position and size
properties.x = element.offsetLeft; properties.x = element.offsetLeft;
properties.y = element.offsetTop; properties.y = element.offsetTop;
properties.width = element.offsetWidth; properties.width = element.offsetWidth;
properties.height = element.offsetHeight; properties.height = element.offsetHeight;
// Styles
config.autoAnimateStyles.forEach( function( propertyName ) {
properties[propertyName] = element.style[propertyName];
} );
// Cache the list of properties
element._animatableProperties = properties;
// Provide a method for rolling back all changes made to this
// element as part of auto-animating it
autoAnimatedRollbacks.push( function( originalStyleAttribute ) {
element.classList.remove( 'auto-animate-target' );
element.style.transitionTimingFunction = '';
element.style.transitionDuration = '';
if( typeof originalStyleAttribute === 'string' ) {
element.setAttribute( 'style', originalStyleAttribute );
}
else {
element.removeAttribute( 'style' );
}
delete element._animatableProperties;
}.bind( null, element.getAttribute( 'style' ) ) );
} }
var computedStyles;
// CSS styles
( options.styles || config.autoAnimateStyles ).forEach( function( style ) {
var value;
if( typeof style.from !== 'undefined' && direction === 'from' ) {
value = style.from;
}
else if( typeof style.to !== 'undefined' && direction === 'to' ) {
value = style.to;
}
else {
value = element.style[style.property];
if( value === '' && style.defaultValue === 'computed' ) {
computedStyles = computedStyles || window.getComputedStyle( element );
value = computedStyles[style.property];
}
}
if( value !== '' ) {
properties.styles.push([ style.property, value ]);
}
} );
return properties; return properties;
} }
/** /**
* [getAutoAnimatableElements description] * Get a list of all element pairs that we can animate
* between the given slides.
*
* @param {HTMLElement} fromSlide * @param {HTMLElement} fromSlide
* @param {HTMLElement} toSlide * @param {HTMLElement} toSlide
* *
@ -3949,26 +3998,25 @@
*/ */
function getAutoAnimatableElements( fromSlide, toSlide ) { function getAutoAnimatableElements( fromSlide, toSlide ) {
var pairs = findImplicitAutoAnimatePairs( fromSlide, toSlide ) var matcher = typeof config.autoAnimateMatcher === 'function' ? config.autoAnimateMatcher : findAutoAnimatePairs;
.concat( findExplicitAutoAnimatePairs( fromSlide, toSlide ) );
var pairs = matcher( fromSlide, toSlide );
// Remove duplicate pairs // Remove duplicate pairs
pairs = pairs.filter( function( pair, index ) { return pairs.filter( function( pair, index ) {
return index === pairs.findIndex( function( comparePair ) { return index === pairs.findIndex( function( comparePair ) {
return pair[0] === comparePair[0] && pair[1] === comparePair[1]; return pair[0] === comparePair[0] && pair[1] === comparePair[1];
} ); } );
} ); } );
return pairs;
} }
/** /**
* Returns an array of auto-animate element pairs * Identifies matching elements between slides. You can specify
* discovered through implicing means such as matching * a custom matcher function by using the autoAnimateMatcher
* text content. * config option.
*/ */
function findImplicitAutoAnimatePairs( fromSlide, toSlide ) { function findAutoAnimatePairs( fromSlide, toSlide ) {
var pairs = []; var pairs = [];
@ -3991,6 +4039,11 @@
}; };
// Eplicit matches via data-id
findMatches( '[data-id]', function( node ) {
return node.nodeName + ':::' + node.getAttribute( 'data-id' );
} );
// Text // Text
findMatches( 'h1, h2, h3, h4, h5, h6, p, li, span', function( node ) { findMatches( 'h1, h2, h3, h4, h5, h6, p, li, span', function( node ) {
return node.nodeName + ':::' + node.innerText; return node.nodeName + ':::' + node.innerText;
@ -4012,29 +4065,6 @@
} }
/**
* Returns explicitly ID-matched auto-animate elements.
*/
function findExplicitAutoAnimatePairs( fromSlide, toSlide ) {
var pairs = [];
var fromHash = {};
toArray( fromSlide.querySelectorAll( '[data-id]' ) ).forEach( function( fromElement ) {
fromHash[ fromElement.getAttribute( 'data-id' ) ] = fromElement;
} );
toArray( toSlide.querySelectorAll( '[data-id]' ) ).forEach( function( toElement ) {
var fromElement = fromHash[ toElement.getAttribute( 'data-id' ) ];
if( fromElement ) {
pairs.push([ fromElement, toElement ]);
}
} );
return pairs;
}
/** /**
* Should the given element be preloaded? * Should the given element be preloaded?
* Decides based on local element attributes and global config. * Decides based on local element attributes and global config.

View file

@ -21,7 +21,7 @@
<section data-auto-animate> <section data-auto-animate>
<h3>Auto-Matched Content (no IDs)</h3> <h3>Auto-Matched Content (no IDs)</h3>
<h3>This will fade out</h3> <p>This will fade out</p>
<img src="assets/image1.png" style="height: 100px;"> <img src="assets/image1.png" style="height: 100px;">
<pre><code class="hljs"> <pre><code class="hljs">
function Example() { function Example() {
@ -29,9 +29,10 @@ function Example() {
} }
</code></pre> </code></pre>
</section> </section>
<section data-auto-animate> <section data-auto-animate data-auto-animate-unmatched="fade">
<h3>Auto-Matched Content (no IDs)</h3> <h3>Auto-Matched Content (no IDs)</h3>
<h3 style="opacity: 0.2; margin-top: 200px;">This will fade out</h3> <p style="opacity: 0.2; margin-top: 200px;">This will fade out</p>
<p>This element is unmatched</p>
<img src="assets/image1.png" style="height: 100px;"> <img src="assets/image1.png" style="height: 100px;">
<pre><code class="hljs"> <pre><code class="hljs">
function Example() { function Example() {