Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 116 additions & 6 deletions packages/react-native-reanimated/__tests__/InterpolateColor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,22 @@ describe('colors interpolation', () => {
test('interpolates semi-transparent colors', () => {
const colors = ['#10506050', '#60902070'];
let interpolatedColor = interpolateColor(0.5, [0, 1], colors);
expect(interpolatedColor).toBe(`rgba(71, 117, 73, ${96 / 255})`);
expect(interpolatedColor).toBe('rgba(71, 117, 73, 0.376)');

interpolatedColor = interpolateColor(0, [0, 1], colors);
expect(interpolatedColor).toBe(`rgba(16, 80, 96, ${80 / 255})`);
expect(interpolatedColor).toBe('rgba(16, 80, 96, 0.314)');

interpolatedColor = interpolateColor(1, [0, 1], colors);
expect(interpolatedColor).toBe(`rgba(96, 144, 32, ${112 / 255})`);
expect(interpolatedColor).toBe('rgba(96, 144, 32, 0.439)');

interpolatedColor = interpolateColor(0.5, [0, 1], colors, 'HSV');
expect(interpolatedColor).toBe(`rgba(23, 120, 54, ${96 / 255})`);
expect(interpolatedColor).toBe('rgba(23, 120, 54, 0.376)');

interpolatedColor = interpolateColor(0, [0, 1], colors, 'HSV');
expect(interpolatedColor).toBe(`rgba(16, 80, 96, ${80 / 255})`);
expect(interpolatedColor).toBe('rgba(16, 80, 96, 0.314)');

interpolatedColor = interpolateColor(1, [0, 1], colors, 'HSV');
expect(interpolatedColor).toBe(`rgba(96, 144, 32, ${112 / 255})`);
expect(interpolatedColor).toBe('rgba(96, 144, 32, 0.439)');
});

test('handles tiny values', () => {
Expand All @@ -132,6 +132,116 @@ describe('colors interpolation', () => {
expect(interpolatedColor).toBe(`rgba(4, 2, 0, 0)`);
});

describe('simple transparent to color interpolation', () => {
const cases = [
{
name: 'transparent to color at midpoint',
value: 0.5,
inputRange: [0, 1],
outputRange: ['transparent', '#ff0000'],
expected: 'rgba(255, 0, 0, 0.5)',
},
{
name: 'transparent at start position',
value: 0,
inputRange: [0, 1],
outputRange: ['transparent', '#ff0000'],
expected: 'rgba(255, 0, 0, 0)',
},
{
name: 'color at end position',
value: 1,
inputRange: [0, 1],
outputRange: ['transparent', '#ff0000'],
expected: 'rgba(255, 0, 0, 1)',
},
{
name: 'transparent to transparent',
value: 0.5,
inputRange: [0, 1],
outputRange: ['transparent', 'transparent'],
expected: 'rgba(0, 0, 0, 0)',
},
];

const colorSpaces: Array<{
colorSpace: 'RGB' | 'HSV' | 'LAB';
options?: Record<string, unknown>;
eps?: number;
}> = [
{ colorSpace: 'RGB' },
{ colorSpace: 'RGB', options: { gamma: 1 } },
{ colorSpace: 'HSV' },
{ colorSpace: 'HSV', options: { useCorrectedHSVInterpolation: false } },
// LAB may produce slightly different results, but the differences are usually small
{ colorSpace: 'LAB', eps: 1e-5 },
];

colorSpaces.forEach(({ colorSpace, options, eps }) => {
test.each(cases)(
`$name using ${colorSpace}${options ? ` with options ${JSON.stringify(options)}` : ''}`,
({ value, inputRange, outputRange, expected }) => {
const result = interpolateColor(
value,
inputRange,
outputRange,
colorSpace,
options
);

if (eps) {
const getChannels = (color: string) =>
color
.replace('rgba(', '')
.replace(')', '')
.split(',')
.map((v) => parseFloat(v.trim()));

getChannels(result).forEach((v, i) => {
expect(v).toBeCloseTo(getChannels(expected)[i], eps);
});
} else {
expect(result).toBe(expected);
}
}
);
});
});

describe('color interpolation with multiple transparent colors', () => {
const inputRange = [0, 0.2, 0.4, 0.6, 0.8, 1];
const outputRange = [
'transparent',
'transparent',
'red',
'transparent',
'blue',
'#00ff00',
];

const cases: [number, string][] = [
[0.1, 'rgba(255, 0, 0, 0)'], // red transparent
[0.2, 'rgba(255, 0, 0, 0)'], // red transparent
[0.3, 'rgba(255, 0, 0, 0.5)'], // between transparent red and red
[0.4, 'rgba(255, 0, 0, 1)'], // red
[0.5, 'rgba(255, 0, 0, 0.5)'], // between red and transparent red
[0.6, 'rgba(255, 0, 0, 0)'], // red transparent
[0.6000001, 'rgba(0, 0, 255, 0)'], // blue transparent
[0.7, 'rgba(0, 0, 255, 0.5)'], // between transparent blue and blue
[0.8, 'rgba(0, 0, 255, 1)'], // blue
[0.9, 'rgba(0, 127.5, 127.5, 1)'], // between blue and green
[1, 'rgba(0, 255, 0, 1)'], // green
];

test.each(cases)(`for value %s, the result is %s`, (value, expected) => {
expect(
interpolateColor(value, inputRange, outputRange, 'RGB', {
gamma: 1,
})
).toBe(expected);
});
});

function TestComponent() {
const color = useSharedValue('#105060');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ describe('Test `normalizeColor` function', () => {
describe('Test colors a colorName string', () => {
test.each([
['red', 0xff0000ff],
['transparent', 0x00000000],
['transparent', undefined], // Transparent cannot be represented as a number
['peachpuff', 0xffdab9ff],
['peachPuff', null],
['PeachPuff', null],
Expand Down
17 changes: 7 additions & 10 deletions packages/react-native-reanimated/src/Colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ export function clampRGBA(RGBA: ParsedColorArray): void {
}
}

const names: Record<string, number> = {
transparent: 0x00000000,
const names: Record<string, number | undefined> = {
transparent: undefined,

/* spell-checker: disable */
// http://www.w3.org/TR/css3-color/#svg-color
Expand Down Expand Up @@ -364,7 +364,7 @@ export const DynamicColorIOSProperties = [
'highContrastDark',
] as const;

export function normalizeColor(color: unknown): number | null {
export function normalizeColor(color: unknown): number | null | undefined {
'worklet';

if (typeof color === 'number') {
Expand All @@ -385,7 +385,7 @@ export function normalizeColor(color: unknown): number | null {
return Number.parseInt(match[1] + 'ff', 16) >>> 0;
}

if (names[color] !== undefined) {
if (color in names) {
return names[color];
}

Expand Down Expand Up @@ -538,8 +538,8 @@ export const rgbaColor = (
alpha = 1
): number | string => {
'worklet';
// Replace tiny values like 1.234e-11 with 0:
const safeAlpha = alpha < 0.001 ? 0 : alpha;
// Round alpha to 3 decimal places to avoid floating point precision issues
const safeAlpha = Math.round(alpha * 1000) / 1000;
return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
};

Expand Down Expand Up @@ -646,12 +646,9 @@ export function processColorInitially(
colorNumber = color;
} else {
const normalizedColor = normalizeColor(color);
if (normalizedColor === null || normalizedColor === undefined) {
return undefined;
}

if (typeof normalizedColor !== 'number') {
return null;
return normalizedColor;
}

colorNumber = normalizedColor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,9 @@ function isDynamicColorObject(value: any): boolean {

export function processColor(color: unknown): number | null | undefined {
let normalizedColor = processColorInitially(color);
if (normalizedColor === null || normalizedColor === undefined) {
return undefined;
}

if (typeof normalizedColor !== 'number') {
return null;
return normalizedColor;
}

if (IS_ANDROID) {
Expand Down
Loading