UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

171 lines (154 loc) 4.89 kB
'use strict'; 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 }; }