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

Commit 6ccdaf4

Browse files
author
Brandon Dail
committed
Improved merge heuristics
Merge now normalizes keyframe values so that animations that define keyframes that are within a specified delta of each other (e.g., 55% and 57.5%) can easily be merged. It normalizes the animation behavior a bit, but should allow for a greater range of animations to be merged together.
1 parent 6271993 commit 6ccdaf4

File tree

2 files changed

+110
-24
lines changed

2 files changed

+110
-24
lines changed

demo/app.jsx

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

39+
window.animations = animations
40+
3941

4042
animations.tadaFlip = merge(animations.tada, animations.flip);
4143
animations.jelloFadeDown = merge(animations.fadeInDown, animations.jello);
44+
animations.flashSwing = merge(animations.jello, animations.bounce);
4245

4346
const animationNames = [];
4447

src/merge.js

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
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+
}
11+
12+
// The default allowed delta for keyframe distance
13+
const keyframeDistance = 10;
14+
415
/**
516
* Merge lets you take two Animations and merge them together. It
617
* iterates through each animation and merges each keyframe. It
@@ -14,33 +25,105 @@ import type { Animation, Keyframe, CSSValue } from './types';
1425
* import { merge, tada, flip } from 'react-effects';
1526
* const tadaFlip = merge(tada, flip);
1627
*/
17-
export default function merge(a: Animation, b: Animation): Animation {
18-
const merged: Animation = {};
19-
// Copy all values from a
20-
for (let frame in a) {
21-
const sourceFrame: Keyframe = a[frame];
22-
const targetFrame: Keyframe = b[frame];
23-
const target: Keyframe = merged[frame] || (merged[frame] = {});
24-
for (let cssValue: string in sourceFrame) {
25-
if (cssValue === 'transform' && targetFrame && targetFrame[cssValue]) {
26-
var transforms = [sourceFrame[cssValue], targetFrame[cssValue]]
27-
target[cssValue] = transforms.filter(
28-
transform => transform !== 'none'
29-
)
30-
.join(' ')
31-
.trim();
28+
export default function merge(primary, secondary) {
29+
// A map used to track the normalized frame value in cases where
30+
// two animations contain frames that appear closely, but not exactly
31+
// 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+
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;
45+
// 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);
51+
52+
const normalizedPrimary = cacheNormalizedFrames(
53+
primary,
54+
normalizedFrames
55+
);
56+
57+
const normalizedSecondary = cacheNormalizedFrames(
58+
secondary,
59+
normalizedFrames
60+
);
61+
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.
76+
for (let frame in normalizedFrames) {
77+
const primaryFrame = normalizedPrimary[frame];
78+
const secondaryFrame = normalizedSecondary[frame];
79+
const target = merged[frame] || (merged[frame] = {});
80+
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+
]);
3291
} else {
33-
target[cssValue] = sourceFrame[cssValue]
92+
target[propertyName] = primaryFrame[propertyName];
93+
}
94+
}
95+
96+
for (let propertyName in secondaryFrame) {
97+
if (!target[propertyName]) {
98+
target[propertyName] = secondaryFrame[propertyName];
3499
}
35100
}
36-
}
37-
// Copy all values from b
38-
for (let frame in b) {
39-
const sourceFrame: Keyframe = b[frame];
40-
const target: Keyframe = merged[frame] || (merged[frame] = {});
41-
for (let cssValue in sourceFrame) {
42-
target[cssValue] = target[cssValue] || sourceFrame[cssValue];
43-
};
44101
}
45102
return merged;
46103
}
104+
105+
function mergeTransforms(transforms) {
106+
transforms = transforms.filter(
107+
transform => transform && transform !== 'none'
108+
)
109+
transforms = transforms.join(' ');
110+
return transforms.trim();
111+
}
112+
113+
114+
function parseBoundryTransform(animation) {
115+
return animation.from && animation.from.transform;
116+
}
117+
118+
119+
function cacheNormalizedFrames(source, cache) {
120+
const normalized = {};
121+
for (let frame in source) {
122+
const normalizedFrame = cache[frame] || (Math.round(
123+
parseFloat(frame) / keyframeDistance
124+
) * keyframeDistance) + '%';
125+
normalized[normalizedFrame] = source[frame];
126+
cache[normalizedFrame] = normalizedFrame;
127+
}
128+
return normalized;
129+
}

0 commit comments

Comments
 (0)