react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
171 lines (154 loc) • 4.89 kB
text/typescript
;
import type {
AnyRecord,
NativePropsBuilder,
UnknownRecord,
} from '../../../../common';
import {
getPropsBuilder,
getSeparatelyInterpolatedNestedProperties,
isDefined,
isNumber,
ReanimatedError,
} from '../../../../common';
import { PERCENTAGE_REGEX } from '../../../constants';
import type {
CSSAnimationKeyframes,
CSSAnimationKeyframeSelector,
CSSAnimationTimingFunction,
} from '../../../types';
import type {
NormalizedCSSAnimationKeyframesConfig,
NormalizedCSSKeyframeTimingFunctions,
PropsWithKeyframes,
} from '../../types';
import { normalizeTimingFunction } from '../common';
export const ERROR_MESSAGES = {
invalidOffsetType: (selector: CSSAnimationKeyframeSelector) =>
`Invalid keyframe selector "${selector}". Only numbers, percentages, "from", and "to" are supported.`,
invalidOffsetRange: (selector: CSSAnimationKeyframeSelector) =>
`Invalid keyframe selector "${selector}". Expected a number between 0 and 1 or a percentage between 0% and 100%.`,
};
export function normalizeKeyframeSelector(
keyframeSelector: CSSAnimationKeyframeSelector
): number[] {
const selectors =
typeof keyframeSelector === 'string'
? keyframeSelector.split(',').map((k) => k.trim())
: [keyframeSelector];
const offsets = selectors.map((selector) => {
if (selector === 'from') {
return 0;
}
if (selector === 'to') {
return 1;
}
let offset: number | undefined;
if (typeof selector === 'number' || !isNaN(+selector)) {
offset = +selector;
} else if (PERCENTAGE_REGEX.test(selector)) {
offset = parseFloat(selector) / 100;
}
if (!isNumber(offset)) {
throw new ReanimatedError(ERROR_MESSAGES.invalidOffsetType(selector));
}
if (offset < 0 || offset > 1) {
throw new ReanimatedError(ERROR_MESSAGES.invalidOffsetRange(selector));
}
return offset;
});
return offsets;
}
type ProcessedKeyframes = Array<{
offset: number;
props: UnknownRecord;
timingFunction?: CSSAnimationTimingFunction;
}>;
export function processKeyframes(
keyframes: CSSAnimationKeyframes,
propsBuilder: NativePropsBuilder
): ProcessedKeyframes {
return Object.entries(keyframes)
.flatMap(
([selector, { animationTimingFunction = undefined, ...props } = {}]) => {
const normalizedProps = propsBuilder.build(props);
if (!normalizedProps) {
return [];
}
return normalizeKeyframeSelector(selector).map((offset) => ({
offset,
props: normalizedProps,
...(animationTimingFunction && {
timingFunction: animationTimingFunction,
}),
}));
}
)
.sort((a, b) => a.offset - b.offset)
.reduce<ProcessedKeyframes>((acc, keyframe) => {
const lastKeyframe = acc[acc.length - 1];
if (lastKeyframe && lastKeyframe.offset === keyframe.offset) {
lastKeyframe.props = { ...lastKeyframe.props, ...keyframe.props };
lastKeyframe.timingFunction = keyframe.timingFunction;
} else {
acc.push(keyframe);
}
return acc;
}, []);
}
function processProps(
offset: number,
props: object,
keyframeProps: AnyRecord,
separatelyInterpolatedNestedProperties: ReadonlySet<string>
) {
Object.entries(props).forEach(([property, value]) => {
if (!isDefined(value)) {
return;
}
if (
/* this object type check is correct as it accepts records and arrays */
typeof value === 'object' &&
separatelyInterpolatedNestedProperties.has(property)
) {
if (!keyframeProps[property]) {
keyframeProps[property] = Array.isArray(value) ? [] : {};
}
processProps(
offset,
value,
keyframeProps[property],
separatelyInterpolatedNestedProperties
);
return;
}
if (!keyframeProps[property]) {
keyframeProps[property] = [];
}
keyframeProps[property].push({ offset, value });
});
}
export function normalizeAnimationKeyframes(
keyframes: CSSAnimationKeyframes,
compoundComponentName: string
): NormalizedCSSAnimationKeyframesConfig {
const propsBuilder = getPropsBuilder(compoundComponentName);
const separatelyInterpolatedNestedProperties =
getSeparatelyInterpolatedNestedProperties(compoundComponentName);
const propKeyframes: PropsWithKeyframes = {};
const timingFunctions: NormalizedCSSKeyframeTimingFunctions = {};
processKeyframes(keyframes, propsBuilder).forEach(
({ offset, props, timingFunction }) => {
processProps(
offset,
props,
propKeyframes,
separatelyInterpolatedNestedProperties
);
if (timingFunction && offset < 1) {
timingFunctions[offset] = normalizeTimingFunction(timingFunction);
}
}
);
return { propKeyframes, keyframeTimingFunctions: timingFunctions };
}