From e2a2c2c022c7b3676afb243782be36d15e251854 Mon Sep 17 00:00:00 2001 From: Hakim El Hattab Date: Fri, 28 Feb 2020 16:13:28 +0100 Subject: [PATCH] refactored and improved auto-animate matcher, supports line-by-line code animations --- css/reveal.css | 9 ++ css/reveal.scss | 11 +++ js/reveal.js | 163 ++++++++++++++++++++++++-------- test/examples/auto-animate.html | 35 ++++++- 4 files changed, 179 insertions(+), 39 deletions(-) diff --git a/css/reveal.css b/css/reveal.css index 7af66ac..34ace93 100644 --- a/css/reveal.css +++ b/css/reveal.css @@ -1233,6 +1233,9 @@ body { /********************************************* * AUTO ANIMATE *********************************************/ +.reveal [data-auto-animate-target="unmatched"] { + will-change: opacity; } + .reveal section[data-auto-animate]:not(.stack):not([data-auto-animate="running"]) [data-auto-animate-target="unmatched"] { opacity: 0; } @@ -1515,6 +1518,12 @@ body { width: 100%; box-sizing: border-box; } +.reveal pre[data-auto-animate-target] { + overflow: hidden; } + +.reveal pre[data-auto-animate-target] code { + height: 100%; } + /********************************************* * ROLLING LINKS *********************************************/ diff --git a/css/reveal.scss b/css/reveal.scss index 51c7a9e..2bb7656 100644 --- a/css/reveal.scss +++ b/css/reveal.scss @@ -1318,6 +1318,10 @@ $controlsArrowAngleActive: 36deg; * AUTO ANIMATE *********************************************/ +.reveal [data-auto-animate-target="unmatched"] { + will-change: opacity; +} + .reveal section[data-auto-animate]:not(.stack):not([data-auto-animate="running"]) [data-auto-animate-target="unmatched"] { opacity: 0; } @@ -1663,6 +1667,13 @@ $controlsArrowAngleActive: 36deg; box-sizing: border-box; } +.reveal pre[data-auto-animate-target] { + overflow: hidden; +} +.reveal pre[data-auto-animate-target] code { + height: 100%; +} + /********************************************* * ROLLING LINKS diff --git a/js/reveal.js b/js/reveal.js index 6ec56dc..d0e7394 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -3058,7 +3058,7 @@ cueAutoSlide(); // Auto-animation - if( slideChanged && previousSlide && currentSlide ) { + if( slideChanged && previousSlide && currentSlide && !isOverview() ) { // Skip the slide transition between our two slides // when auto-animating individual elements @@ -4028,6 +4028,7 @@ // Animate towards the 'to' state toProps.styles['transition'] = 'all '+ options.duration +'s '+ options.easing + ' ' + options.delay + 's'; toProps.styles['transition-property'] = toStyleProperties.join( ', ' ); + toProps.styles['will-change'] = toStyleProperties.join( ', ' ); // Build up our custom CSS. We need to override inline styles // so we need to make our styles vErY IMPORTANT!1!! @@ -4162,15 +4163,18 @@ */ function getAutoAnimatableElements( fromSlide, toSlide ) { - var matcher = typeof config.autoAnimateMatcher === 'function' ? config.autoAnimateMatcher : findAutoAnimatePairs; + var matcher = typeof config.autoAnimateMatcher === 'function' ? config.autoAnimateMatcher : getAutoAnimatePairs; var pairs = matcher( fromSlide, toSlide ); + var reserved = []; + // Remove duplicate pairs return pairs.filter( function( pair, index ) { - return index === pairs.findIndex( function( comparePair ) { - return pair.from === comparePair.from && pair.to === comparePair.to; - } ); + if( reserved.indexOf( pair.to ) === -1 ) { + reserved.push( pair.to ); + return true; + } } ); } @@ -4181,64 +4185,67 @@ * You can specify a custom matcher function by using * the `autoAnimateMatcher` config option. */ - function findAutoAnimatePairs( fromSlide, toSlide ) { + function getAutoAnimatePairs( fromSlide, toSlide ) { var pairs = []; - var findMatches = function( selector, serializer, transformer ) { - - var fromHash = {}; - - toArray( fromSlide.querySelectorAll( selector ) ).forEach( function( element ) { - if( typeof transformer === 'function' ) element = transformer( element ); - fromHash[ serializer( element ) ] = element; - } ); - - toArray( toSlide.querySelectorAll( selector ) ).forEach( function( element ) { - if( typeof transformer === 'function' ) element = transformer( element ); - var fromElement = fromHash[ serializer( element ) ]; - if( fromElement ) { - pairs.push({ from: fromElement, to: element }); - } - } ); - - }; - - var textNodes = 'h1, h2, h3, h4, h5, h6, p, li, span'; + var codeNodes = 'pre'; + var textNodes = 'h1, h2, h3, h4, h5, h6, p, li'; var mediaNodes = 'img, video, iframe'; // Eplicit matches via data-id - findMatches( '[data-id]', function( node ) { + findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', function( node ) { return node.nodeName + ':::' + node.getAttribute( 'data-id' ); } ); // Text - findMatches( textNodes, function( node ) { + findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, function( node ) { return node.nodeName + ':::' + node.innerText; - }, null ); + } ); // Media - findMatches( mediaNodes, function( node ) { + findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, function( node ) { return node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) ); } ); // Code - findMatches( 'pre>code', function( node ) { + findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, function( node ) { return node.nodeName + ':::' + node.innerText; - }, function( element ) { - return element.parentNode; } ); pairs.forEach( function( pair ) { - var fromElement = pair.from; - var matchesMethod = fromElement.matches || fromElement.matchesSelector || fromElement.msMatchesSelector; - // Disable scale transformations on text nodes, we transiition // each individual text property instead - if( matchesMethod.call( fromElement, textNodes ) ) { + if( pair.from.matches( textNodes ) ) { pair.options = { scale: false }; } + // Animate individual lines of code + else if( pair.from.matches( codeNodes ) ) { + + // Transition the code block's width and height instead of scaling + // to prevent its content from being squished + pair.options = { scale: false, styles: [ 'width', 'height' ] }; + + // Lines of code + findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', function( node ) { + return node.textContent; + }, { + scale: false, + styles: [], + measure: getLocalBoundingBox + } ); + + // Line numbers + findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-line[data-line-number]', function( node ) { + return node.getAttribute( 'data-line-number' ); + }, { + scale: false, + styles: [ 'width' ], + measure: getLocalBoundingBox + } ); + + } } ); @@ -4246,6 +4253,88 @@ } + /** + * Helper method which returns a bounding box based on + * the given elements offset coordinates. + * + * @param {HTMLElement} element + * @return {Object} x, y, width, height + */ + function getLocalBoundingBox( element ) { + + var presentationScale = Reveal.getScale(); + + return { + x: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100, + y: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100, + width: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100, + height: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100 + }; + + } + + /** + * Finds matching elements between two slides. + * + * @param {Array} pairs List of pairs to push matches to + * @param {HTMLElement} fromScope Scope within the from element exists + * @param {HTMLElement} toScope Scope within the to element exists + * @param {String} selector CSS selector of the element to match + * @param {Function} serializer A function that accepts an element and returns + * a stringified ID based on its contents + * @param {Object} animationOptions Optional config options for this pair + */ + function findAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) { + + var fromMatches = {}; + var toMatches = {}; + + [].slice.call( fromScope.querySelectorAll( selector ) ).forEach( function( element, i ) { + var key = serializer( element ); + if( typeof key === 'string' && key.length ) { + fromMatches[key] = fromMatches[key] || []; + fromMatches[key].push( element ); + } + } ); + + [].slice.call( toScope.querySelectorAll( selector ) ).forEach( function( element, i ) { + var key = serializer( element ); + toMatches[key] = toMatches[key] || []; + toMatches[key].push( element ); + + var fromElement; + + // Retrieve the 'from' element + if( fromMatches[key] ) { + var pimaryIndex = toMatches[key].length - 1; + var secondaryIndex = fromMatches[key].length - 1; + + // If there are multiple identical from elements, retrieve + // the one at the same index as our to-element. + if( fromMatches[key][ pimaryIndex ] ) { + fromElement = fromMatches[key][ pimaryIndex ]; + fromMatches[key][ pimaryIndex ] = null; + } + // If there are no matching from-elements at the same index, + // use the last one. + else if( fromMatches[key][ secondaryIndex ] ) { + fromElement = fromMatches[key][ secondaryIndex ]; + fromMatches[key][ secondaryIndex ] = null; + } + } + + // If we've got a matching pair, push it to the list of pairs + if( fromElement ) { + pairs.push({ + from: fromElement, + to: element, + options: animationOptions + }); + } + } ); + + } + /** * Returns a all elements within the given scope that should * be considered unmatched in an auto-animate transition. If diff --git a/test/examples/auto-animate.html b/test/examples/auto-animate.html index c81b949..c353748 100644 --- a/test/examples/auto-animate.html +++ b/test/examples/auto-animate.html @@ -23,7 +23,7 @@

Auto-Animate Example

This will fade out

-

+					

 						function Example() {
 						  const [count, setCount] = useState(0);
 						}
@@ -34,8 +34,9 @@
 					

This will fade out

This element is unmatched

-

+					

 						function Example() {
+						  New line!
 						  const [count, setCount] = useState(0);
 						}
 					
@@ -48,6 +49,36 @@

Line Height & Letter Spacing

+
+

+						import React, { useState } from 'react';
+
+						function Example() {
+						  const [count, setCount] = useState(0);
+
+						  return (
+						    ...
+						  );
+						}
+					
+
+
+

+						function Example() {
+						  const [count, setCount] = useState(0);
+
+						  return (
+						    <div>
+						      <p>You clicked {count} times</p>
+						      <button onClick={() => setCount(count + 1)}>
+						        Click me
+						      </button>
+						    </div>
+						  );
+						}
+					
+
+

Swapping list items