Skip to content

Commit 953b379

Browse files
authored
chore: Unify processColor function implementation (#8433)
## Summary This PR removes the separate `processColor` function implementation used in the CSS animations and merges it with the `processColor` function used in worklet-based animations. It also standarizes values returned by the `processColor` - `null` for `'transparent'` string, which cannot be converted to number, `undefined` for all invalid values.
1 parent 3380da1 commit 953b379

File tree

12 files changed

+130
-118
lines changed

12 files changed

+130
-118
lines changed

packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,11 @@ CSSColor::CSSColor(const folly::dynamic &value)
100100
}
101101

102102
bool CSSColor::canConstruct(jsi::Runtime &rt, const jsi::Value &jsiValue) {
103-
return jsiValue.isNumber() || jsiValue.isUndefined() ||
104-
(jsiValue.isString() && jsiValue.getString(rt).utf8(rt) == "transparent");
103+
return jsiValue.isNumber() || jsiValue.isNull();
105104
}
106105

107106
bool CSSColor::canConstruct(const folly::dynamic &value) {
108-
return value.isNumber() || value.empty() ||
109-
(value.isString() && value.getString() == "transparent");
107+
return value.isNumber() || value.isNull();
110108
}
111109

112110
folly::dynamic CSSColor::toDynamic() const {

packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.cpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ SVGBrush::SVGBrush(const folly::dynamic &value)
2424
bool SVGBrush::canConstruct(jsi::Runtime &rt, const jsi::Value &jsiValue) {
2525
return jsiValue.isNumber() || jsiValue.isUndefined() ||
2626
(jsiValue.isString() &&
27-
isValidColorString(jsiValue.getString(rt).utf8(rt)));
27+
jsiValue.getString(rt).utf8(rt) == "currentColor");
2828
}
2929

3030
bool SVGBrush::canConstruct(const folly::dynamic &value) {
3131
return value.isNumber() || value.empty() ||
32-
isValidColorString(value.asString());
32+
value.asString() == "currentColor";
3333
}
3434

3535
folly::dynamic SVGBrush::toDynamic() const {
@@ -78,8 +78,4 @@ std::ostream &operator<<(std::ostream &os, const SVGBrush &colorValue) {
7878

7979
#endif // NDEBUG
8080

81-
bool SVGBrush::isValidColorString(const std::string &value) {
82-
return value == "transparent" || value == "currentColor";
83-
}
84-
8581
} // namespace reanimated::css

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

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('Test `normalizeColor` function', () => {
1515
'rgb(1, 2, 3,)',
1616
'rgb(1, 2, 3',
1717
].forEach((color) => {
18-
expect(normalizeColor(color)).toBe(null);
18+
expect(normalizeColor(color)).toBeUndefined();
1919
});
2020
});
2121

@@ -33,7 +33,7 @@ describe('Test `normalizeColor` function', () => {
3333
'hsl(1%, 2, 3)',
3434
'rg b( 1%, 2%, 3%)',
3535
].forEach((color) => {
36-
expect(normalizeColor(color)).toBe(null);
36+
expect(normalizeColor(color)).toBeUndefined();
3737
});
3838
});
3939

@@ -50,7 +50,7 @@ describe('Test `normalizeColor` function', () => {
5050
'rgb(-1, -2, -3)',
5151
'rgba(0, 0, 0, 1)',
5252
].forEach((color) => {
53-
expect(normalizeColor(color)).not.toBe(null);
53+
expect(normalizeColor(color)).toBeDefined();
5454
});
5555
});
5656
});
@@ -65,36 +65,36 @@ describe('Test `normalizeColor` function', () => {
6565
[0x123456, 0x123456],
6666
[0x1234567, 0x1234567],
6767
[0x12345678, 0x12345678],
68-
[0x123456789, null],
68+
[0x123456789, undefined],
6969
[11, 11],
7070
[158, 158],
71-
[300.78, null],
72-
[-300, null],
71+
[300.78, undefined],
72+
[-300, undefined],
7373
[0, 0],
74-
[NaN, null],
75-
[Infinity, null],
76-
[-Infinity, null],
74+
[NaN, undefined],
75+
[Infinity, undefined],
76+
[-Infinity, undefined],
7777
])('normalizeColor(%d) = %p', (color, expectedColor) => {
7878
expect(normalizeColor(color)).toBe(expectedColor);
7979
});
8080
});
8181

8282
describe('Test colors being a number as string', () => {
8383
test.each([
84-
['0x1', null],
85-
['0x12', null],
86-
['0x123', null],
87-
['0x1234', null],
88-
['0x12345', null],
89-
['0x123456', null],
90-
['0x1234567', null],
91-
['0x12345678', null],
92-
['0x123456789', null],
93-
['11', null],
94-
['158', null],
95-
['300.78', null],
96-
['-300', null],
97-
['0', null],
84+
['0x1', undefined],
85+
['0x12', undefined],
86+
['0x123', undefined],
87+
['0x1234', undefined],
88+
['0x12345', undefined],
89+
['0x123456', undefined],
90+
['0x1234567', undefined],
91+
['0x12345678', undefined],
92+
['0x123456789', undefined],
93+
['11', undefined],
94+
['158', undefined],
95+
['300.78', undefined],
96+
['-300', undefined],
97+
['0', undefined],
9898
])('normalizeColor("%s") = %p', (color, expectedColor) => {
9999
expect(normalizeColor(color)).toBe(expectedColor);
100100
});
@@ -160,26 +160,26 @@ describe('Test `normalizeColor` function', () => {
160160

161161
describe('Test invalid hex', () => {
162162
test.each([
163-
['#12345', null],
164-
['#12345g', null],
165-
['#1234567', null],
166-
['#abcde', null],
167-
['#abcdeff', null],
168-
['#abcde', null],
169-
['#abcdeff', null],
170-
['#abcde', null],
171-
['#abcdeff', null],
163+
['#12345', undefined],
164+
['#12345g', undefined],
165+
['#1234567', undefined],
166+
['#abcde', undefined],
167+
['#abcdeff', undefined],
168+
['#abcde', undefined],
169+
['#abcdeff', undefined],
170+
['#abcde', undefined],
171+
['#abcdeff', undefined],
172172
])('normalizeColor(%s) = %p', (color, expectedColor) => {
173173
expect(normalizeColor(color)).toBe(expectedColor);
174174
});
175175
});
176176
});
177177
describe('Test colors being a rgb string', () => {
178178
test.each([
179-
['rgb (0,0,0)', null],
180-
['rgb(50,200,150, 45)', null],
181-
['RGB(50,200,150)', null],
182-
['rgb(50,200,150, 0.45)', null],
179+
['rgb (0,0,0)', undefined],
180+
['rgb(50,200,150, 45)', undefined],
181+
['RGB(50,200,150)', undefined],
182+
['rgb(50,200,150, 0.45)', undefined],
183183
['rgb(0, 0, 255)', 0x0000ffff],
184184
['rgb(0 0 255)', 0x0000ffff],
185185
['rgb(100, 15, 69)', 0x640f45ff],
@@ -205,8 +205,8 @@ describe('Test `normalizeColor` function', () => {
205205

206206
describe('Test colors being a rgba string', () => {
207207
test.each([
208-
['RGBA(100 ,255 ,50 ,50 )', null],
209-
['rgba (100,255,50,.5)', null],
208+
['RGBA(100 ,255 ,50 ,50 )', undefined],
209+
['rgba (100,255,50,.5)', undefined],
210210
['rgba(0, 0, 0, .5)', 0x00000080],
211211
['rgba(0, 0, 0, 0.0)', 0x00000000],
212212
['rgba(0, 0, 0, 0)', 0x00000000],
@@ -232,10 +232,10 @@ describe('Test `normalizeColor` function', () => {
232232

233233
describe('Test colors being a hsl string', () => {
234234
test.each([
235-
['HSL(0,100%,50%)', null],
236-
['hsl(120 ,0.99, 0.1 )', null],
237-
['hsl(0,100,50)', null],
238-
['hsl(0,100%,50%, 0.5)', null],
235+
['HSL(0,100%,50%)', undefined],
236+
['hsl(120 ,0.99, 0.1 )', undefined],
237+
['hsl(0,100,50)', undefined],
238+
['hsl(0,100%,50%, 0.5)', undefined],
239239
['hsl(0, 0%, 0%)', 0x000000ff],
240240
['hsl(360, 100%, 100%)', 0xffffffff],
241241
['hsl(180, 50%, 50%)', 0x40bfbfff],
@@ -271,7 +271,7 @@ describe('Test `normalizeColor` function', () => {
271271
['hsla(360 100% 100% / 1)', 0xffffffff],
272272
['hsla(360 100% 100% / 0)', 0xffffff00],
273273
['hsla(180 50% 50% / 0.2)', 0x40bfbf33],
274-
['HSLA(0,100%,50%,0.5)', null],
274+
['HSLA(0,100%,50%,0.5)', undefined],
275275
['hsla(0,100%,50%,0.5)', 0xff000080],
276276
['hsla(120,100%,50%, 0.5)', 0x00ff0080],
277277
['hsla(120,100%,50%, 1)', 0x00ff00ff],
@@ -292,7 +292,7 @@ describe('Test `normalizeColor` function', () => {
292292
['hwb(360, 100%, 100%)', 0x808080ff],
293293
['hwb(0 0% 0%)', 0xff0000ff],
294294
['hwb(70 50% 0%)', 0xeaff80ff],
295-
['HWB(0,100%,50%)', null],
295+
['HWB(0,100%,50%)', undefined],
296296
['hwb(0,67%, 33%)', 0xabababff],
297297
['hwb(0,67% , 33%)', 0xabababff],
298298
['hwb(48, 38%, 6%)', 0xf0d361ff],
@@ -304,10 +304,10 @@ describe('Test `normalizeColor` function', () => {
304304
describe('Test colors a colorName string', () => {
305305
test.each([
306306
['red', 0xff0000ff],
307-
['transparent', undefined], // Transparent cannot be represented as a number
307+
['transparent', null], // Transparent cannot be represented as a number
308308
['peachpuff', 0xffdab9ff],
309-
['peachPuff', null],
310-
['PeachPuff', null],
309+
['peachPuff', undefined],
310+
['PeachPuff', undefined],
311311
])('normalizeColor(%s) = %p', (color, expectedColor) => {
312312
expect(normalizeColor(color)).toBe(expectedColor);
313313
});

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

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

171-
const names: Record<string, number | undefined> = {
172-
transparent: undefined,
171+
const names: Record<string, number | null> = {
172+
transparent: null,
173173

174174
/* spell-checker: disable */
175175
// http://www.w3.org/TR/css3-color/#svg-color
@@ -371,11 +371,11 @@ export function normalizeColor(color: unknown): number | null | undefined {
371371
if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) {
372372
return color;
373373
}
374-
return null;
374+
return undefined;
375375
}
376376

377377
if (typeof color !== 'string') {
378-
return null;
378+
return undefined;
379379
}
380380

381381
let match: RegExpExecArray | null | undefined;
@@ -508,7 +508,7 @@ export function normalizeColor(color: unknown): number | null | undefined {
508508
);
509509
}
510510

511-
return null;
511+
return undefined;
512512
}
513513

514514
export const opacity = (c: number): number => {
@@ -637,7 +637,7 @@ export function processColorInitially(
637637
): number | null | undefined {
638638
'worklet';
639639
if (color === null || color === undefined) {
640-
return color;
640+
return undefined;
641641
}
642642

643643
let colorNumber: number;
@@ -662,7 +662,7 @@ export function isColor(value: unknown): boolean {
662662
if (typeof value !== 'string') {
663663
return false;
664664
}
665-
return processColorInitially(value) != null;
665+
return processColorInitially(value) != undefined;
666666
}
667667

668668
export type ParsedColorArray = [number, number, number, number];
Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
2-
import { ReanimatedError } from '../../../../../common';
3-
import { ERROR_MESSAGES, processColor } from '../colors';
2+
import { ReanimatedError } from '../../errors';
3+
import { ERROR_MESSAGES, processColor, processColorsInProps } from '../colors';
44

5-
describe(processColor, () => {
6-
describe('converts color strings to numbers for all color props', () => {
5+
describe(processColorsInProps, () => {
6+
describe('properly converts colors in props', () => {
77
test.each([
88
['backgroundColor', 'red', 0xff0000ff],
99
['color', 'rgb(255, 200, 0)', 0xffc800ff],
@@ -23,7 +23,46 @@ describe(processColor, () => {
2323
['overlayColor', 'hsla(360, 100%, 50%, 0.75)', 0xff0000bf],
2424
['tintColor', 'hsl(180, 100%, 25%)', 0x007f80ff],
2525
])('converts %p with value %p to %p', (key, value, expected) => {
26+
// convert from RGBA to ARGB format
2627
const argb = ((expected << 24) | (expected >>> 8)) >>> 0;
28+
const props = { [key]: value };
29+
30+
processColorsInProps(props);
31+
32+
expect(props).toEqual({ [key]: argb });
33+
});
34+
});
35+
36+
describe('does not convert non-color properties', () => {
37+
test.each([
38+
['width', 'red'],
39+
['height', 'blue'],
40+
['margin', 0x000000ff],
41+
['padding', '#ff0000'],
42+
])('does not convert %p', (key, value) => {
43+
const props = { [key]: value };
44+
45+
processColorsInProps(props);
46+
47+
expect(props).toEqual({ [key]: value });
48+
});
49+
});
50+
});
51+
52+
describe(processColor, () => {
53+
describe('properly converts colors', () => {
54+
test.each([
55+
['red', 0xff0000ff],
56+
['rgb(255, 200, 0)', 0xffc800ff],
57+
['rgba(50, 100, 150, 0.6)', 0x32649699],
58+
['#34a', 0x3344aaff],
59+
['hsl(240, 100%, 50%)', 0x0000ffff],
60+
['hsla(120, 50%, 50%, 0.5)', 0x40bf4080],
61+
['hwb(0, 0%, 0%)', 0xff0000ff],
62+
['transparent', null],
63+
])('converts %p to %p', (value, expected) => {
64+
// convert from RGBA to ARGB format if not null
65+
const argb = expected && ((expected << 24) | (expected >>> 8)) >>> 0;
2766
expect(processColor(value)).toEqual(argb);
2867
});
2968
});

packages/react-native-reanimated/src/common/processors/colors.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,32 @@ function isDynamicColorObject(value: any): boolean {
6060
);
6161
}
6262

63-
export function processColor(color: unknown): number | null | undefined {
64-
let normalizedColor = processColorInitially(color);
63+
export const ERROR_MESSAGES = {
64+
invalidColor: (color: unknown) => `Invalid color value: ${String(color)}`,
65+
};
6566

66-
if (typeof normalizedColor !== 'number') {
67-
return normalizedColor;
68-
}
67+
/**
68+
* Processes a color value and returns a normalized color representation.
69+
*
70+
* @param value - The color value to process (string, number, or ColorValue)
71+
* @returns The processed color value as a number for valid colors, null for
72+
* transparent colors, or undefined for invalid colors
73+
*/
74+
export function processColor(value: unknown): number | null {
75+
let normalizedColor = processColorInitially(value);
6976

70-
if (IS_ANDROID) {
77+
if (IS_ANDROID && typeof normalizedColor == 'number') {
7178
// Android use 32 bit *signed* integer to represent the color
7279
// We utilize the fact that bitwise operations in JS also operates on
7380
// signed 32 bit integers, so that we can use those to convert from
7481
// *unsigned* to *signed* 32bit int that way.
7582
normalizedColor = normalizedColor | 0x0;
7683
}
7784

85+
if (normalizedColor === undefined) {
86+
throw new ReanimatedError(ERROR_MESSAGES.invalidColor(value));
87+
}
88+
7889
return normalizedColor;
7990
}
8091

@@ -85,7 +96,7 @@ export function processColorsInProps(props: StyleProps) {
8596
const value = props[key];
8697

8798
if (Array.isArray(value)) {
88-
props[key] = value.map((c: unknown) => processColor(c));
99+
props[key] = value.map((c) => processColor(c));
89100
} else if (isDynamicColorObject(value)) {
90101
if (!IS_IOS) {
91102
throw new ReanimatedError(
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
'use strict';
2-
export * from './colors';
3-
export * from './shadows';
4-
export * from './transformOrigin';
2+
export {
3+
DynamicColorIOS,
4+
PlatformColor,
5+
processColor,
6+
processColorsInProps,
7+
} from './colors';
8+
export { processBoxShadowNative, processBoxShadowWeb } from './shadows';
9+
export { processTransformOrigin } from './transformOrigin';

0 commit comments

Comments
 (0)