diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedInterpolation-benchmark-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedInterpolation-benchmark-itest.js new file mode 100644 index 000000000000..0544f3675657 --- /dev/null +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedInterpolation-benchmark-itest.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {InterpolationConfigType} from '../nodes/AnimatedInterpolation'; + +import AnimatedInterpolation from '../nodes/AnimatedInterpolation'; +import * as Fantom from '@react-native/fantom'; + +const CALLS = 1_000_000; + +function createInterpolation( + config: InterpolationConfigType, +): number => string { + let parentValue = 0; + const interpolation = new AnimatedInterpolation( + // $FlowFixMe[incompatible-type] + {__getValue: () => parentValue}, + config, + ); + return input => { + parentValue = input; + return interpolation.__getValue(); + }; +} + +function run(interpolation: number => string) { + for (let i = 0; i < CALLS; i++) { + interpolation(i / CALLS); + } +} + +const rotate = createInterpolation({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], +}); +const shadow = createInterpolation({ + inputRange: [0, 1], + outputRange: ['0px 0px 0px', '-10.5px 20px 4px'], +}); +const color = createInterpolation({ + inputRange: [0, 1], + outputRange: ['#FF9500', 'rgba(50, 150, 250, 0.4)'], +}); + +Fantom.unstable_benchmark + .suite('AnimatedInterpolation', {minIterations: 10}) + .test(`string output, 1 number (${CALLS} calls)`, () => { + run(rotate); + }) + .test(`string output, 3 numbers (${CALLS} calls)`, () => { + run(shadow); + }) + .test(`color output (${CALLS} calls)`, () => { + run(color); + }); diff --git a/packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js b/packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js index a2d7f506915d..da1ed8489130 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js @@ -311,6 +311,28 @@ describe('Interpolation', () => { expect(interpolation(1)).toBe('100deg'); }); + it('should work with multiple numeric components in string ranges', () => { + const interpolation = createInterpolation({ + inputRange: [0, 1], + outputRange: ['0px -4.5px 10%', '10px 5.5px 0%'], + }); + + expect(interpolation(0)).toBe('0px -4.5px 10%'); + expect(interpolation(0.5)).toBe('5px 0.5px 5%'); + expect(interpolation(1)).toBe('10px 5.5px 0%'); + }); + + it('should keep the non-numeric prefix and suffix of string ranges', () => { + const interpolation = createInterpolation({ + inputRange: [0, 1], + outputRange: ['translate(0px, -10.5px)', 'translate(20px, 10px)'], + }); + + expect(interpolation(0)).toBe('translate(0px, -10.5px)'); + expect(interpolation(0.5)).toBe('translate(10px, -0.25px)'); + expect(interpolation(1)).toBe('translate(20px, 10px)'); + }); + it('should crash when chaining an interpolation that returns a string', () => { const interpolation = createInterpolation({ inputRange: [0, 1], diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js index 701a5701b8c4..9db7fbd564b0 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -277,22 +277,25 @@ function createStringInterpolation( }), ); if (!isColor) { + const components = outputRange[0].components; return input => { - const values = interpolations.map(interpolation => interpolation(input)); + let result = ''; let i = 0; - return outputRange[0].components - .map(c => (typeof c === 'number' ? values[i++] : c)) - .join(''); + for (let j = 0; j < components.length; j++) { + const c = components[j]; + result += typeof c === 'number' ? interpolations[i++](input) : c; + } + return result; }; } else { return input => { - const result = interpolations.map((interpolation, i) => { - const value = interpolation(input); - // rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to - // round the opacity (4th column). - return i < 3 ? Math.round(value) : Math.round(value * 1000) / 1000; - }); - return `rgba(${result[0]}, ${result[1]}, ${result[2]}, ${result[3]})`; + // rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to + // round the opacity (4th column). + const r = Math.round(interpolations[0](input)); + const g = Math.round(interpolations[1](input)); + const b = Math.round(interpolations[2](input)); + const a = Math.round(interpolations[3](input) * 1000) / 1000; + return `rgba(${r}, ${g}, ${b}, ${a})`; }; } }