Skip to content

Commit 8a3f591

Browse files
committed
Implement correct transparent color interpolation
1 parent fd01fff commit 8a3f591

File tree

6 files changed

+331
-103
lines changed

6 files changed

+331
-103
lines changed

apps/common-app/src/apps/reanimated/examples/EmptyExample.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,57 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
3+
import Animated, {
4+
interpolateColor,
5+
useAnimatedStyle,
6+
useSharedValue,
7+
withRepeat,
8+
withTiming,
9+
} from 'react-native-reanimated';
310

411
export default function EmptyExample() {
12+
const sv = useSharedValue(0);
13+
14+
const eStyle = useAnimatedStyle(() => ({
15+
backgroundColor: interpolateColor(
16+
sv.value,
17+
[0, 1],
18+
['yellow', 'transparent']
19+
),
20+
}));
21+
22+
useEffect(() => {
23+
sv.value = 0;
24+
sv.value = withRepeat(withTiming(1, { duration: 3000 }), -1, true);
25+
}, [sv]);
26+
527
return (
628
<View style={styles.container}>
729
<Text>Hello world!</Text>
30+
<Animated.View
31+
style={[
32+
styles.box,
33+
eStyle,
34+
// {
35+
// animationName: {
36+
// from: { backgroundColor: 'yellow' },
37+
// to: { backgroundColor: 'transparent' },
38+
// },
39+
// animationDuration: '3000ms',
40+
// animationIterationCount: 'infinite',
41+
// animationDirection: 'alternate',
42+
// },
43+
]}
44+
/>
845
</View>
946
);
1047
}
1148

1249
const styles = StyleSheet.create({
50+
box: {
51+
width: 100,
52+
height: 100,
53+
backgroundColor: 'red',
54+
},
1355
container: {
1456
flex: 1,
1557
alignItems: 'center',

packages/react-native-reanimated/__tests__/InterpolateColor.test.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,82 @@ describe('colors interpolation', () => {
132132
expect(interpolatedColor).toBe(`rgba(4, 2, 0, 0)`);
133133
});
134134

135+
describe('transparent color interpolation', () => {
136+
const basicCases = [
137+
{
138+
name: 'transparent to color at midpoint',
139+
value: 0.5,
140+
inputRange: [0, 1],
141+
outputRange: ['transparent', '#ff0000'],
142+
expected: 'rgba(255, 0, 0, 0.5)',
143+
},
144+
{
145+
name: 'transparent at start position',
146+
value: 0,
147+
inputRange: [0, 1],
148+
outputRange: ['transparent', '#ff0000'],
149+
expected: 'rgba(255, 0, 0, 0)',
150+
},
151+
{
152+
name: 'color at end position',
153+
value: 1,
154+
inputRange: [0, 1],
155+
outputRange: ['transparent', '#ff0000'],
156+
expected: 'rgba(255, 0, 0, 1)',
157+
},
158+
{
159+
name: 'transparent to transparent',
160+
value: 0.5,
161+
inputRange: [0, 1],
162+
outputRange: ['transparent', 'transparent'],
163+
expected: 'rgba(0, 0, 0, 0)',
164+
},
165+
];
166+
167+
const colorSpaces: Array<{
168+
colorSpace: 'RGB' | 'HSV' | 'LAB';
169+
options?: Record<string, unknown>;
170+
eps?: number;
171+
}> = [
172+
{ colorSpace: 'RGB' },
173+
{ colorSpace: 'RGB', options: { gamma: 1 } },
174+
{ colorSpace: 'HSV' },
175+
{ colorSpace: 'HSV', options: { useCorrectedHSVInterpolation: false } },
176+
// LAB may produce slightly different results, but the differences are usually small
177+
{ colorSpace: 'LAB', eps: 1e-5 },
178+
];
179+
180+
colorSpaces.forEach(({ colorSpace, options, eps }) => {
181+
test.each(basicCases)(
182+
`$name using ${colorSpace}${options ? ` with options ${JSON.stringify(options)}` : ''}`,
183+
({ value, inputRange, outputRange, expected }) => {
184+
const result = interpolateColor(
185+
value,
186+
inputRange,
187+
outputRange,
188+
colorSpace,
189+
options
190+
);
191+
192+
if (eps) {
193+
const getChannels = (color: string) =>
194+
color
195+
.replace('rgba(', '')
196+
.replace(')', '')
197+
.split(',')
198+
.map((v) => parseFloat(v.trim()));
199+
200+
getChannels(result).forEach((v, i) => {
201+
expect(v).toBeCloseTo(getChannels(expected)[i], eps);
202+
});
203+
} else {
204+
expect(result).toBe(expected);
205+
}
206+
}
207+
);
208+
});
209+
});
210+
135211
function TestComponent() {
136212
const color = useSharedValue('#105060');
137213

packages/react-native-reanimated/src/Colors.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,6 @@ export function clampRGBA(RGBA: ParsedColorArray): void {
169169
}
170170

171171
const names: Record<string, number> = {
172-
transparent: 0x00000000,
173-
174172
/* spell-checker: disable */
175173
// http://www.w3.org/TR/css3-color/#svg-color
176174
aliceblue: 0xf0f8ffff,

packages/react-native-reanimated/src/css/native/style/processors/colors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@ export const processColor: ValueProcessor<
2828
throw new ReanimatedError(ERROR_MESSAGES.invalidColor(value));
2929
}
3030

31+
console.log('>>> normalizedColor', normalizedColor);
32+
3133
return normalizedColor;
3234
};

0 commit comments

Comments
 (0)