11// flow
22import type { Animation , Keyframe , CSSValue } from './types' ;
33
4- // Keyframes that bound an animation
5- const boundryFrmes : { [ frame : string ] : boolean } = {
6- 'from' : true ,
7- '0%' : true ,
8- 'to' : true ,
9- '100%' : true ,
10- }
4+
5+ type FrameMap = {
6+ [ source : string ] : string
7+ } ;
118
129// The default allowed delta for keyframe distance
1310const keyframeDistance = 10 ;
1411
12+ const defaultNormalizedFrames : FrameMap = {
13+ 'from' : 'from' ,
14+ '0%' : 'from' ,
15+ 'to' : 'to' ,
16+ '100%' : 'to' ,
17+ }
18+
1519/**
1620 * Merge lets you take two Animations and merge them together. It
1721 * iterates through each animation and merges each keyframe. It
@@ -25,101 +29,152 @@ const keyframeDistance = 10;
2529 * import { merge, tada, flip } from 'react-effects';
2630 * const tadaFlip = merge(tada, flip);
2731 */
28- export default function merge ( primary , secondary ) {
32+ export default function merge (
33+ primary : Animation ,
34+ secondary : Animation
35+ ) : Animation {
2936 // A map used to track the normalized frame value in cases where
3037 // two animations contain frames that appear closely, but not exactly
3138 // at the same time (e.g., 50% and 52%)
32- const normalizedFrames = {
33- 'from' : 'from' ,
34- '0%' : 'from' ,
35- 'to' : 'to' ,
36- '100%' : 'to' ,
37- } ;
38-
39+ const normalizedFrames : FrameMap = { } ;
3940
40- // If we are dealing with an animation that appears to be
41- // a "boundry-specific" animation, meaning it only specifies
42- // a start and end position, we want to persist the start transform
43- // throughout.
44- let boundryTransform = null ;
4541 // We merge each frame into a new object and return it
46- const merged = { } ;
47- /* primary frame should control directional movement */
48- const primaryFrames = Object . keys ( primary ) ;
49- /* secondary frames should control orientation/size */
50- const secondaryFrames = Object . keys ( secondary ) ;
42+ const merged : Animation = { } ;
5143
52- const normalizedPrimary = cacheNormalizedFrames (
44+ const normalizedPrimary : Animation = cacheNormalizedFrames (
5345 primary ,
5446 normalizedFrames
5547 ) ;
5648
57- const normalizedSecondary = cacheNormalizedFrames (
49+ const normalizedSecondary : Animation = cacheNormalizedFrames (
5850 secondary ,
5951 normalizedFrames
6052 ) ;
6153
62- // We parse a boundry transform from either the primary
63- // or secondary animation, if either look to be bounded.
64- // Primary animation is given precedence.
65- if ( secondaryFrames . length <= 2 ) {
66- boundryTransform = parseBoundryTransform (
67- normalizedSecondary
68- ) ;
69- } else if ( primaryFrames . length <= 2 ) {
70- boundryTransform = parseBoundryTransform (
71- primaryFrames
72- ) ;
73- }
74-
75- // Iterate through all the cached, normalized animation frames.
54+ // Iterate all normalized frames
7655 for ( let frame in normalizedFrames ) {
7756 const primaryFrame = normalizedPrimary [ frame ] ;
7857 const secondaryFrame = normalizedSecondary [ frame ] ;
58+ // Create a new frame object if it doesn't exist.
7959 const target = merged [ frame ] || ( merged [ frame ] = { } ) ;
8060
81- for ( let propertyName in primaryFrame ) {
82- if ( propertyName === 'transform' && secondaryFrame ) {
83- // TODO we should only apply the boundry transform when we
84- // are in between boundry frames, not in them. We need to track
85- // what the actual start and end frame are.
86- target [ propertyName ] = mergeTransforms ( [
87- primaryFrame [ propertyName ] ,
88- secondaryFrame [ propertyName ] ,
89- boundryTransform
90- ] ) ;
91- } else {
92- target [ propertyName ] = primaryFrame [ propertyName ] ;
61+ // If both aniatmions define this frame, merge them carefully
62+ if ( primaryFrame && secondaryFrame ) {
63+ // Walk through all properties in the primary frame
64+ for ( let propertyName in primaryFrame ) {
65+ // Transform is special cased, as we want to combine both
66+ // transforms when posssible.
67+ if ( propertyName === 'transform' ) {
68+ // But we dont need to do anything if theres no other
69+ // transform to merge.
70+ if ( secondaryFrame [ propertyName ] ) {
71+ const newTransform = mergeTransforms ( [
72+ primaryFrame [ propertyName ] ,
73+ secondaryFrame [ propertyName ]
74+ ] ) ;
75+ // We make the assumption that animations use 'transform: none'
76+ // to terminate the keyframe. If we're combining two animations
77+ // that may terminate at separte frames, its safest to just
78+ // ignore this.
79+ if ( newTransform !== 'none' ) {
80+ target [ propertyName ] = newTransform ;
81+ }
82+ } else {
83+ const propertyValue = getDefined (
84+ primaryFrame [ propertyName ] ,
85+ secondaryFrame [ propertyName ]
86+ ) ;
87+ target [ propertyName ] = propertyValue ;
88+ }
89+ }
90+ // If the property is *not* 'transform' we just write it
91+ else {
92+ // Use a typeof check so we don't ignore falsy values like 0.
93+ const propertyValue = getDefined (
94+ primaryFrame [ propertyName ] ,
95+ secondaryFrame [ propertyName ]
96+ ) ;
97+ target [ propertyName ] = propertyValue ;
98+ }
99+ }
100+ // Walk through all properties in the secondary frame.
101+ // We should be able to assume that any property that
102+ // needed to be merged has already been merged when we
103+ // walked the primary frame.
104+ for ( let propertyName in secondaryFrame ) {
105+ const propertyValue = secondaryFrame [ propertyName ] ;
106+ // Again, ignore 'transform: none'
107+ if ( propertyName === 'transform' && propertyValue === 'none' ) {
108+ continue ;
109+ }
110+ target [ propertyName ] = target [ propertyName ] || propertyValue ;
93111 }
94112 }
95-
96- for ( let propertyName in secondaryFrame ) {
97- if ( ! target [ propertyName ] ) {
98- target [ propertyName ] = secondaryFrame [ propertyName ] ;
113+ // Otherwise just pick the frame that is defined.
114+ else {
115+ const definedFrame = primaryFrame || secondaryFrame ;
116+ const target = { } ;
117+ for ( let propertyName in definedFrame ) {
118+ const propertyValue = definedFrame [ propertyName ] ;
119+ // Again, ignore 'transform: none'
120+ if ( propertyName === 'transform' && propertyValue === 'none' ) {
121+ continue ;
122+ }
123+ target [ propertyName ] = propertyValue ;
124+ }
125+ // Only define a frame if there are actual styles to apply
126+ if ( Object . keys ( target ) . length ) {
127+ merged [ frame ] = target ;
99128 }
100129 }
101- }
102- return merged ;
103- }
130+ } ;
104131
105- function mergeTransforms ( transforms ) {
106- transforms = transforms . filter (
107- transform => transform && transform !== 'none'
108- )
109- transforms = transforms . join ( ' ' ) ;
110- return transforms . trim ( ) ;
132+ return merged ;
111133}
112134
113135
114- function parseBoundryTransform ( animation ) {
115- return animation . from && animation . from . transform ;
136+ /**
137+ * Takes an array of strings representing transform values and
138+ * merges them. Ignores duplicates and 'none'.
139+ * @private
140+ * @example
141+ * mergeTransforms([
142+ * 'translateX(10px)',
143+ * 'rotateX(120deg)',
144+ * 'translateX(10px)',
145+ * 'none',
146+ * ])
147+ * // -> 'translateX(10px) rotateX(120deg)'
148+ *
149+ */
150+ function mergeTransforms ( transforms : Array < string > ) : string {
151+ const filtered = transforms . filter ( ( transform , i ) =>
152+ transform !== 'none' && transforms . indexOf ( transform ) == i
153+ ) ;
154+ return filtered . join ( ' ' ) ;
116155}
117156
157+ /**
158+ * Returns whichever value is actually defined
159+ * @private
160+ */
161+ function getDefined ( primary : CSSValue , secondary : CSSValue ) : CSSValue {
162+ return typeof primary !== 'undefined' ? primary : secondary
163+ }
118164
119- function cacheNormalizedFrames ( source , cache ) {
120- const normalized = { } ;
165+ /**
166+ * Takes a source animation and the current cache, populating the
167+ * cache with the normalized keyframes and returning a copy of the
168+ * source animation with the normalized keyframes as well.
169+ *
170+ * It uses keyframeDistance to determine how much it should normalize
171+ * frames.
172+ * @private
173+ */
174+ function cacheNormalizedFrames ( source : Animation , cache : FrameMap ) : Animation {
175+ const normalized : Animation = { } ;
121176 for ( let frame in source ) {
122- const normalizedFrame = cache [ frame ] || ( Math . round (
177+ const normalizedFrame = defaultNormalizedFrames [ frame ] || ( Math . round (
123178 parseFloat ( frame ) / keyframeDistance
124179 ) * keyframeDistance ) + '%' ;
125180 normalized [ normalizedFrame ] = source [ frame ] ;
0 commit comments