Skip to content
This repository was archived by the owner on Aug 19, 2022. It is now read-only.

Commit 9c8edf6

Browse files
author
Brandon Dail
committed
Simplify merge heuristics
Removes the idea of a 'boundry' transform. More explicit and documentated merging process.
1 parent 6ccdaf4 commit 9c8edf6

File tree

2 files changed

+127
-74
lines changed

2 files changed

+127
-74
lines changed

demo/app.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@ let aphroditeStyle = {
3636
},
3737
}
3838

39-
window.animations = animations
40-
41-
4239
animations.tadaFlip = merge(animations.tada, animations.flip);
4340
animations.jelloFadeDown = merge(animations.fadeInDown, animations.jello);
4441
animations.flashSwing = merge(animations.jello, animations.bounce);
42+
animations.zoomWobble = merge(animations.wobble, animations.zoomOut);
4543

4644
const animationNames = [];
4745

src/merge.js

Lines changed: 126 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
// flow
22
import 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
1310
const 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

Comments
 (0)