react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
250 lines (217 loc) • 7.09 kB
text/typescript
;
'worklet';
import type {
DynamicColorIOS as RNDynamicColorIOS,
OpaqueColorValue,
} from 'react-native';
import { ColorProperties, processColorInitially } from '../../../Colors';
import type { StyleProps } from '../../../commonTypes';
import { IS_ANDROID, IS_IOS } from '../../constants';
import { ReanimatedError } from '../../errors';
import { type ValueProcessorContext, ValueProcessorTarget } from '../../types';
import { isRecord } from '../../utils';
/**
* Copied from:
* https://github.com/facebook/react-native/blob/v0.81.0/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts
*/
export function PlatformColor(...names: string[]): OpaqueColorValue {
return (IS_IOS
? { semantic: names }
: // eslint-disable-next-line camelcase
{ resource_paths: names }) as unknown as OpaqueColorValue;
}
type PlatformColorObject =
| { semantic: Array<string>; resource_paths?: never }
| { semantic?: never; resource_paths?: Array<string> };
function isPlatformColorObject(value: unknown): value is PlatformColorObject {
return (
isRecord(value) &&
(Array.isArray(value.semantic) || Array.isArray(value.resource_paths))
);
}
/* copied from:
* https://github.com/facebook/react-native/blob/v0.81.0/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesIOS.d.ts
*/
type DynamicColorIOSTuple = Parameters<typeof RNDynamicColorIOS>[0];
export function DynamicColorIOS(tuple: DynamicColorIOSTuple): OpaqueColorValue {
return {
dynamic: {
light: tuple.light,
dark: tuple.dark,
highContrastLight: tuple.highContrastLight,
highContrastDark: tuple.highContrastDark,
},
} as unknown as OpaqueColorValue;
}
type DynamicColorObjectIOS = {
dynamic: DynamicColorIOSTuple;
};
function isDynamicColorObjectIOS(
value: unknown
): value is DynamicColorObjectIOS {
return (
isRecord(value) &&
isRecord(value.dynamic) &&
'light' in value.dynamic &&
'dark' in value.dynamic
);
}
export const ERROR_MESSAGES = {
invalidColor: (color: unknown) =>
`Invalid color value: ${JSON.stringify(color)}`,
invalidProcessedColor: (color: unknown) =>
`Invalid processed color value: ${JSON.stringify(color)}`,
dynamicNotAvailableOnPlatform: () =>
'DynamicColorIOS is not available on this platform.',
};
export function processColorNumber(value: unknown): number | null {
let normalizedColor = processColorInitially(value);
if (IS_ANDROID && typeof normalizedColor == 'number') {
// Android use 32 bit *signed* integer to represent the color
// We utilize the fact that bitwise operations in JS also operates on
// signed 32 bit integers, so that we can use those to convert from
// *unsigned* to *signed* 32bit int that way.
normalizedColor = normalizedColor | 0x0;
}
return normalizedColor;
}
function unprocessColorNumber(value: number): string {
const a = value >>> 24;
const r = (value << 8) >>> 24;
const g = (value << 16) >>> 24;
const b = (value << 24) >>> 24;
return `rgba(${r},${g},${b},${a})`;
}
export type ProcessedDynamicColorObjectIOS = {
dynamic: {
light: number;
dark: number;
highContrastLight?: number;
highContrastDark?: number;
};
};
const DynamicColorIOSProperties = [
'light',
'dark',
'highContrastLight',
'highContrastDark',
] as const;
function processDynamicColorObjectIOS(
value: DynamicColorObjectIOS
): ProcessedDynamicColorObjectIOS | null {
const result = {} as ProcessedDynamicColorObjectIOS['dynamic'];
for (const property of DynamicColorIOSProperties) {
if (value.dynamic[property] === undefined) {
continue;
}
const processed = processColorNumber(value.dynamic[property]);
if (processed === null) {
return null;
}
result[property] = processed;
}
return {
dynamic: result,
};
}
function unprocessDynamicColorObjectIOS(
value: ProcessedDynamicColorObjectIOS
): DynamicColorObjectIOS {
const result = {} as DynamicColorIOSTuple;
for (const property of DynamicColorIOSProperties) {
if (value.dynamic[property] !== undefined) {
result[property] = unprocessColorNumber(value.dynamic[property]);
}
}
return {
dynamic: result,
};
}
type ProcessedColor =
| number
| PlatformColorObject
| ProcessedDynamicColorObjectIOS;
/**
* Processes a color value and returns a normalized color representation.
*
* @param value - The color value to process (string, number, or ColorValue)
* @param context - Optional for target-specific processing context (e.g. CSS)
* @returns The processed color value - `number` for valid colors, `false` for
* transparent colors
*/
export function processColor(
value: string | number,
context?: ValueProcessorContext
): number;
export function processColor(
value: unknown,
context?: ValueProcessorContext
): ProcessedColor;
export function processColor(
value: unknown,
context?: ValueProcessorContext
): ProcessedColor {
let result: ProcessedColor | null = processColorNumber(value); // try to convert to a number first (most common case)
if (result) {
return result;
}
if (result === 0) {
if (
context?.target === ValueProcessorTarget.CSS &&
value === 'transparent'
) {
// For CSS, we have to return `false` to distinguish the true 'transparent' from the 0x00000000 color
// and properly interpolate between the transparent and the non-transparent color.
return false as unknown as number; // TODO - figure out a better way to handle this instead of type casting
}
return result;
}
if (isPlatformColorObject(value)) {
return value;
}
if (isDynamicColorObjectIOS(value)) {
if (!IS_IOS) {
throw new ReanimatedError(ERROR_MESSAGES.dynamicNotAvailableOnPlatform());
}
result = processDynamicColorObjectIOS(value);
}
if (result === null) {
throw new ReanimatedError(ERROR_MESSAGES.invalidColor(value));
}
return result;
}
export function unprocessColor(
value: ProcessedColor
): string | PlatformColorObject | DynamicColorObjectIOS {
if (typeof value === 'number') {
return unprocessColorNumber(value);
}
if (isPlatformColorObject(value)) {
return value;
}
if (isDynamicColorObjectIOS(value)) {
if (!IS_IOS) {
throw new ReanimatedError(ERROR_MESSAGES.dynamicNotAvailableOnPlatform());
}
return unprocessDynamicColorObjectIOS(value);
}
throw new ReanimatedError(ERROR_MESSAGES.invalidProcessedColor(value));
}
export function processColorsInProps(props: StyleProps) {
for (const key in props) {
if (!ColorProperties.includes(key)) continue;
const value = props[key];
props[key] = Array.isArray(value)
? value.map((c) => processColor(c))
: processColor(value);
}
}
export function unprocessColorsInProps(props: StyleProps) {
for (const key in props) {
if (!ColorProperties.includes(key)) continue;
const value = props[key];
props[key] = Array.isArray(value)
? value.map((c) => unprocessColor(c))
: unprocessColor(value);
}
}