Skip to content

Commit 8fdbbc8

Browse files
linco95necolas
authored andcommitted
[fix] Animated interpolation of color values
Fix #2001 Close #2002
1 parent e826485 commit 8fdbbc8

File tree

2 files changed

+337
-2
lines changed

2 files changed

+337
-2
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @emails oncall+react_native
9+
*/
10+
11+
'use strict';
12+
13+
import Animated from '..';
14+
import Easing from '../../Easing';
15+
16+
const AnimatedInterpolation = Animated.Interpolation;
17+
18+
describe('Animated', () => {
19+
describe('Interpolation', () => {
20+
describe('color value', () => {
21+
test.each([[['#1E1E1E', '#1E1E1F']], [[0x1e1e1e, 0x1e1e1f]]])(
22+
'can interpolate %s',
23+
(outputRange) => {
24+
const config = {
25+
inputRange: [0, 1],
26+
outputRange
27+
};
28+
const animVal = new Animated.Value(0);
29+
const result = animVal.interpolate(config);
30+
expect(result).toBeInstanceOf(Animated.Interpolation);
31+
}
32+
);
33+
});
34+
35+
it('should work with defaults', () => {
36+
const interpolation = AnimatedInterpolation.__createInterpolation({
37+
inputRange: [0, 1],
38+
outputRange: [0, 1]
39+
});
40+
41+
expect(interpolation(0)).toBe(0);
42+
expect(interpolation(0.5)).toBe(0.5);
43+
expect(interpolation(0.8)).toBe(0.8);
44+
expect(interpolation(1)).toBe(1);
45+
});
46+
47+
it('should work with output range', () => {
48+
const interpolation = AnimatedInterpolation.__createInterpolation({
49+
inputRange: [0, 1],
50+
outputRange: [100, 200]
51+
});
52+
53+
expect(interpolation(0)).toBe(100);
54+
expect(interpolation(0.5)).toBe(150);
55+
expect(interpolation(0.8)).toBe(180);
56+
expect(interpolation(1)).toBe(200);
57+
});
58+
59+
it('should work with input range', () => {
60+
const interpolation = AnimatedInterpolation.__createInterpolation({
61+
inputRange: [100, 200],
62+
outputRange: [0, 1]
63+
});
64+
65+
expect(interpolation(100)).toBe(0);
66+
expect(interpolation(150)).toBe(0.5);
67+
expect(interpolation(180)).toBe(0.8);
68+
expect(interpolation(200)).toBe(1);
69+
});
70+
71+
it('should throw for non monotonic input ranges', () => {
72+
expect(() =>
73+
AnimatedInterpolation.__createInterpolation({
74+
inputRange: [0, 2, 1],
75+
outputRange: [0, 1, 2]
76+
})
77+
).toThrow();
78+
79+
expect(() =>
80+
AnimatedInterpolation.__createInterpolation({
81+
inputRange: [0, 1, 2],
82+
outputRange: [0, 3, 1]
83+
})
84+
).not.toThrow();
85+
});
86+
87+
it('should work with empty input range', () => {
88+
const interpolation = AnimatedInterpolation.__createInterpolation({
89+
inputRange: [0, 10, 10],
90+
outputRange: [1, 2, 3],
91+
extrapolate: 'extend'
92+
});
93+
94+
expect(interpolation(0)).toBe(1);
95+
expect(interpolation(5)).toBe(1.5);
96+
expect(interpolation(10)).toBe(2);
97+
expect(interpolation(10.1)).toBe(3);
98+
expect(interpolation(15)).toBe(3);
99+
});
100+
101+
it('should work with empty output range', () => {
102+
const interpolation = AnimatedInterpolation.__createInterpolation({
103+
inputRange: [1, 2, 3],
104+
outputRange: [0, 10, 10],
105+
extrapolate: 'extend'
106+
});
107+
108+
expect(interpolation(0)).toBe(-10);
109+
expect(interpolation(1.5)).toBe(5);
110+
expect(interpolation(2)).toBe(10);
111+
expect(interpolation(2.5)).toBe(10);
112+
expect(interpolation(3)).toBe(10);
113+
expect(interpolation(4)).toBe(10);
114+
});
115+
116+
it('should work with easing', () => {
117+
const interpolation = AnimatedInterpolation.__createInterpolation({
118+
inputRange: [0, 1],
119+
outputRange: [0, 1],
120+
easing: Easing.quad
121+
});
122+
123+
expect(interpolation(0)).toBe(0);
124+
expect(interpolation(0.5)).toBe(0.25);
125+
expect(interpolation(0.9)).toBe(0.81);
126+
expect(interpolation(1)).toBe(1);
127+
});
128+
129+
it('should work with extrapolate', () => {
130+
let interpolation = AnimatedInterpolation.__createInterpolation({
131+
inputRange: [0, 1],
132+
outputRange: [0, 1],
133+
extrapolate: 'extend',
134+
easing: Easing.quad
135+
});
136+
137+
expect(interpolation(-2)).toBe(4);
138+
expect(interpolation(2)).toBe(4);
139+
140+
interpolation = AnimatedInterpolation.__createInterpolation({
141+
inputRange: [0, 1],
142+
outputRange: [0, 1],
143+
extrapolate: 'clamp',
144+
easing: Easing.quad
145+
});
146+
147+
expect(interpolation(-2)).toBe(0);
148+
expect(interpolation(2)).toBe(1);
149+
150+
interpolation = AnimatedInterpolation.__createInterpolation({
151+
inputRange: [0, 1],
152+
outputRange: [0, 1],
153+
extrapolate: 'identity',
154+
easing: Easing.quad
155+
});
156+
157+
expect(interpolation(-2)).toBe(-2);
158+
expect(interpolation(2)).toBe(2);
159+
});
160+
161+
it('should work with keyframes with extrapolate', () => {
162+
const interpolation = AnimatedInterpolation.__createInterpolation({
163+
inputRange: [0, 10, 100, 1000],
164+
outputRange: [0, 5, 50, 500],
165+
extrapolate: true
166+
});
167+
168+
expect(interpolation(-5)).toBe(-2.5);
169+
expect(interpolation(0)).toBe(0);
170+
expect(interpolation(5)).toBe(2.5);
171+
expect(interpolation(10)).toBe(5);
172+
expect(interpolation(50)).toBe(25);
173+
expect(interpolation(100)).toBe(50);
174+
expect(interpolation(500)).toBe(250);
175+
expect(interpolation(1000)).toBe(500);
176+
expect(interpolation(2000)).toBe(1000);
177+
});
178+
179+
it('should work with keyframes without extrapolate', () => {
180+
const interpolation = AnimatedInterpolation.__createInterpolation({
181+
inputRange: [0, 1, 2],
182+
outputRange: [0.2, 1, 0.2],
183+
extrapolate: 'clamp'
184+
});
185+
186+
expect(interpolation(5)).toBeCloseTo(0.2);
187+
});
188+
189+
it('should throw for an infinite input range', () => {
190+
expect(() =>
191+
AnimatedInterpolation.__createInterpolation({
192+
inputRange: [-Infinity, Infinity],
193+
outputRange: [0, 1]
194+
})
195+
).toThrow();
196+
197+
expect(() =>
198+
AnimatedInterpolation.__createInterpolation({
199+
inputRange: [-Infinity, 0, Infinity],
200+
outputRange: [1, 2, 3]
201+
})
202+
).not.toThrow();
203+
});
204+
205+
it('should work with negative infinite', () => {
206+
const interpolation = AnimatedInterpolation.__createInterpolation({
207+
inputRange: [-Infinity, 0],
208+
outputRange: [-Infinity, 0],
209+
easing: Easing.quad,
210+
extrapolate: 'identity'
211+
});
212+
213+
expect(interpolation(-Infinity)).toBe(-Infinity);
214+
expect(interpolation(-100)).toBeCloseTo(-10000);
215+
expect(interpolation(-10)).toBeCloseTo(-100);
216+
expect(interpolation(0)).toBeCloseTo(0);
217+
expect(interpolation(1)).toBeCloseTo(1);
218+
expect(interpolation(100)).toBeCloseTo(100);
219+
});
220+
221+
it('should work with positive infinite', () => {
222+
const interpolation = AnimatedInterpolation.__createInterpolation({
223+
inputRange: [5, Infinity],
224+
outputRange: [5, Infinity],
225+
easing: Easing.quad,
226+
extrapolate: 'identity'
227+
});
228+
229+
expect(interpolation(-100)).toBeCloseTo(-100);
230+
expect(interpolation(-10)).toBeCloseTo(-10);
231+
expect(interpolation(0)).toBeCloseTo(0);
232+
expect(interpolation(5)).toBeCloseTo(5);
233+
expect(interpolation(6)).toBeCloseTo(5 + 1);
234+
expect(interpolation(10)).toBeCloseTo(5 + 25);
235+
expect(interpolation(100)).toBeCloseTo(5 + 95 * 95);
236+
expect(interpolation(Infinity)).toBe(Infinity);
237+
});
238+
239+
it('should work with output ranges as string', () => {
240+
const interpolation = AnimatedInterpolation.__createInterpolation({
241+
inputRange: [0, 1],
242+
outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.4)']
243+
});
244+
245+
expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)');
246+
expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.2)');
247+
expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.4)');
248+
});
249+
250+
it('should work with output ranges as short hex string', () => {
251+
const interpolation = AnimatedInterpolation.__createInterpolation({
252+
inputRange: [0, 1],
253+
outputRange: ['#024', '#9BF']
254+
});
255+
256+
expect(interpolation(0)).toBe('rgba(0, 34, 68, 1)');
257+
expect(interpolation(0.5)).toBe('rgba(77, 111, 162, 1)');
258+
expect(interpolation(1)).toBe('rgba(153, 187, 255, 1)');
259+
});
260+
261+
it('should work with output ranges as long hex string', () => {
262+
const interpolation = AnimatedInterpolation.__createInterpolation({
263+
inputRange: [0, 1],
264+
outputRange: ['#FF9500', '#87FC70']
265+
});
266+
267+
expect(interpolation(0)).toBe('rgba(255, 149, 0, 1)');
268+
expect(interpolation(0.5)).toBe('rgba(195, 201, 56, 1)');
269+
expect(interpolation(1)).toBe('rgba(135, 252, 112, 1)');
270+
});
271+
272+
it('should work with output ranges with mixed hex and rgba strings', () => {
273+
const interpolation = AnimatedInterpolation.__createInterpolation({
274+
inputRange: [0, 1],
275+
outputRange: ['rgba(100, 120, 140, .4)', '#87FC70']
276+
});
277+
278+
expect(interpolation(0)).toBe('rgba(100, 120, 140, 0.4)');
279+
expect(interpolation(0.5)).toBe('rgba(118, 186, 126, 0.7)');
280+
expect(interpolation(1)).toBe('rgba(135, 252, 112, 1)');
281+
});
282+
283+
it('should work with negative and decimal values in string ranges', () => {
284+
const interpolation = AnimatedInterpolation.__createInterpolation({
285+
inputRange: [0, 1],
286+
outputRange: ['-100.5deg', '100deg']
287+
});
288+
289+
expect(interpolation(0)).toBe('-100.5deg');
290+
expect(interpolation(0.5)).toBe('-0.25deg');
291+
expect(interpolation(1)).toBe('100deg');
292+
});
293+
294+
it('should crash when chaining an interpolation that returns a string', () => {
295+
const interpolation = AnimatedInterpolation.__createInterpolation({
296+
inputRange: [0, 1],
297+
outputRange: [0, 1]
298+
});
299+
expect(() => {
300+
interpolation('45rad');
301+
}).toThrow();
302+
});
303+
304+
it('should support a mix of color patterns', () => {
305+
const interpolation = AnimatedInterpolation.__createInterpolation({
306+
inputRange: [0, 1, 2],
307+
outputRange: ['rgba(0, 100, 200, 0)', 'rgb(50, 150, 250)', 'red']
308+
});
309+
310+
expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)');
311+
expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.5)');
312+
expect(interpolation(1.5)).toBe('rgba(153, 75, 125, 1)');
313+
expect(interpolation(2)).toBe('rgba(255, 0, 0, 1)');
314+
});
315+
316+
it('should crash when defining output range with different pattern', () => {
317+
expect(() =>
318+
AnimatedInterpolation.__createInterpolation({
319+
inputRange: [0, 1],
320+
outputRange: ['20deg', '30rad']
321+
})
322+
).toThrow();
323+
});
324+
325+
it('should round the alpha channel of a color to the nearest thousandth', () => {
326+
const interpolation = AnimatedInterpolation.__createInterpolation({
327+
inputRange: [0, 1],
328+
outputRange: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 1)']
329+
});
330+
331+
expect(interpolation(1e-12)).toBe('rgba(0, 0, 0, 0)');
332+
expect(interpolation(2 / 3)).toBe('rgba(0, 0, 0, 0.667)');
333+
});
334+
});
335+
});

packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedInterpolation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import AnimatedWithChildren from './AnimatedWithChildren';
1717
import NativeAnimatedHelper from '../NativeAnimatedHelper';
1818

1919
import invariant from 'fbjs/lib/invariant';
20-
import normalizeColor from '../../../../modules/normalizeColor';
20+
import normalizeColor from 'normalize-css-color';
2121

2222
const __DEV__ = process.env.NODE_ENV !== 'production';
2323

@@ -177,7 +177,7 @@ function colorToRgba(input: string): string {
177177
const g = (normalizedColor & 0x00ff0000) >>> 16;
178178
const b = (normalizedColor & 0x0000ff00) >>> 8;
179179
const a = (normalizedColor & 0x000000ff) / 255;
180-
180+
181181
return `rgba(${r}, ${g}, ${b}, ${a})`;
182182
}
183183

0 commit comments

Comments
 (0)