amis-rpc-design/node_modules/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js
2023-10-07 19:42:30 +08:00

416 lines
12 KiB
JavaScript

/**
* 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
* @format
*/
/* eslint no-bitwise: 0 */
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedNode from './AnimatedNode';
import normalizeColor from '../../StyleSheet/normalizeColor';
import processColor from '../../StyleSheet/processColor';
import Easing from '../Easing';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
export type InterpolationConfigType<OutputT: number | string> = $ReadOnly<{
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
easing?: (input: number) => number,
extrapolate?: ExtrapolateType,
extrapolateLeft?: ExtrapolateType,
extrapolateRight?: ExtrapolateType,
}>;
/**
* Very handy helper to map input ranges to output ranges with an easing
* function and custom behavior outside of the ranges.
*/
function createNumericInterpolation(
config: InterpolationConfigType<number>,
): (input: number) => number {
const outputRange: $ReadOnlyArray<number> = (config.outputRange: any);
const inputRange = config.inputRange;
const easing = config.easing || Easing.linear;
let extrapolateLeft: ExtrapolateType = 'extend';
if (config.extrapolateLeft !== undefined) {
extrapolateLeft = config.extrapolateLeft;
} else if (config.extrapolate !== undefined) {
extrapolateLeft = config.extrapolate;
}
let extrapolateRight: ExtrapolateType = 'extend';
if (config.extrapolateRight !== undefined) {
extrapolateRight = config.extrapolateRight;
} else if (config.extrapolate !== undefined) {
extrapolateRight = config.extrapolate;
}
return input => {
invariant(
typeof input === 'number',
'Cannot interpolation an input which is not a number',
);
const range = findRange(input, inputRange);
return (interpolate(
input,
inputRange[range],
inputRange[range + 1],
outputRange[range],
outputRange[range + 1],
easing,
extrapolateLeft,
extrapolateRight,
): any);
};
}
function interpolate(
input: number,
inputMin: number,
inputMax: number,
outputMin: number,
outputMax: number,
easing: (input: number) => number,
extrapolateLeft: ExtrapolateType,
extrapolateRight: ExtrapolateType,
) {
let result = input;
// Extrapolate
if (result < inputMin) {
if (extrapolateLeft === 'identity') {
return result;
} else if (extrapolateLeft === 'clamp') {
result = inputMin;
} else if (extrapolateLeft === 'extend') {
// noop
}
}
if (result > inputMax) {
if (extrapolateRight === 'identity') {
return result;
} else if (extrapolateRight === 'clamp') {
result = inputMax;
} else if (extrapolateRight === 'extend') {
// noop
}
}
if (outputMin === outputMax) {
return outputMin;
}
if (inputMin === inputMax) {
if (input <= inputMin) {
return outputMin;
}
return outputMax;
}
// Input Range
if (inputMin === -Infinity) {
result = -result;
} else if (inputMax === Infinity) {
result = result - inputMin;
} else {
result = (result - inputMin) / (inputMax - inputMin);
}
// Easing
result = easing(result);
// Output Range
if (outputMin === -Infinity) {
result = -result;
} else if (outputMax === Infinity) {
result = result + outputMin;
} else {
result = result * (outputMax - outputMin) + outputMin;
}
return result;
}
const numericComponentRegex = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
// Maps string inputs an RGBA color or an array of numeric components
function mapStringToNumericComponents(
input: string,
):
| {isColor: true, components: [number, number, number, number]}
| {isColor: false, components: $ReadOnlyArray<number | string>} {
let normalizedColor = normalizeColor(input);
invariant(
normalizedColor == null || typeof normalizedColor !== 'object',
'PlatformColors are not supported',
);
if (typeof normalizedColor === 'number') {
normalizedColor = normalizedColor || 0;
const r = (normalizedColor & 0xff000000) >>> 24;
const g = (normalizedColor & 0x00ff0000) >>> 16;
const b = (normalizedColor & 0x0000ff00) >>> 8;
const a = (normalizedColor & 0x000000ff) / 255;
return {isColor: true, components: [r, g, b, a]};
} else {
const components: Array<string | number> = [];
let lastMatchEnd = 0;
let match: RegExp$matchResult;
while ((match = (numericComponentRegex.exec(input): any)) != null) {
if (match.index > lastMatchEnd) {
components.push(input.substring(lastMatchEnd, match.index));
}
components.push(parseFloat(match[0]));
lastMatchEnd = match.index + match[0].length;
}
invariant(
components.length > 0,
'outputRange must contain color or value with numeric component',
);
if (lastMatchEnd < input.length) {
components.push(input.substring(lastMatchEnd, input.length));
}
return {isColor: false, components};
}
}
/**
* Supports string shapes by extracting numbers so new values can be computed,
* and recombines those values into new strings of the same shape. Supports
* things like:
*
* rgba(123, 42, 99, 0.36) // colors
* -45deg // values with units
*/
function createStringInterpolation(
config: InterpolationConfigType<string>,
): (input: number) => string {
invariant(config.outputRange.length >= 2, 'Bad output range');
const outputRange = config.outputRange.map(mapStringToNumericComponents);
const isColor = outputRange[0].isColor;
if (__DEV__) {
invariant(
outputRange.every(output => output.isColor === isColor),
'All elements of output range should either be a color or a string with numeric components',
);
const firstOutput = outputRange[0].components;
invariant(
outputRange.every(
output => output.components.length === firstOutput.length,
),
'All elements of output range should have the same number of components',
);
invariant(
outputRange.every(output =>
output.components.every(
(component, i) =>
// $FlowIgnoreMe[invalid-compare]
typeof component === 'number' || component === firstOutput[i],
),
),
'All elements of output range should have the same non-numeric components',
);
}
const numericComponents: $ReadOnlyArray<$ReadOnlyArray<number>> =
outputRange.map(output =>
isColor
? // $FlowIgnoreMe[incompatible-call]
output.components
: // $FlowIgnoreMe[incompatible-call]
output.components.filter(c => typeof c === 'number'),
);
const interpolations = numericComponents[0].map((_, i) =>
createNumericInterpolation({
...config,
outputRange: numericComponents.map(components => components[i]),
}),
);
if (!isColor) {
return input => {
const values = interpolations.map(interpolation => interpolation(input));
let i = 0;
return outputRange[0].components
.map(c => (typeof c === 'number' ? values[i++] : c))
.join('');
};
} 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]})`;
};
}
}
function findRange(input: number, inputRange: $ReadOnlyArray<number>) {
let i;
for (i = 1; i < inputRange.length - 1; ++i) {
if (inputRange[i] >= input) {
break;
}
}
return i - 1;
}
function checkValidRanges<OutputT: number | string>(
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
) {
checkInfiniteRange('outputRange', outputRange);
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);
invariant(
inputRange.length === outputRange.length,
'inputRange (' +
inputRange.length +
') and outputRange (' +
outputRange.length +
') must have the same length',
);
}
function checkValidInputRange(arr: $ReadOnlyArray<number>) {
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
const message =
'inputRange must be monotonically non-decreasing ' + String(arr);
for (let i = 1; i < arr.length; ++i) {
invariant(arr[i] >= arr[i - 1], message);
}
}
function checkInfiniteRange<OutputT: number | string>(
name: string,
arr: $ReadOnlyArray<OutputT>,
) {
invariant(arr.length >= 2, name + ' must have at least 2 elements');
invariant(
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
/* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression
* below this comment, one or both of the operands may be something that
* doesn't cleanly convert to a string, like undefined, null, and object,
* etc. If you really mean this implicit string conversion, you can do
* something like String(myThing) */
// $FlowFixMe[unsafe-addition]
name + 'cannot be ]-infinity;+infinity[ ' + arr,
);
}
export default class AnimatedInterpolation<
OutputT: number | string,
> extends AnimatedWithChildren {
_parent: AnimatedNode;
_config: InterpolationConfigType<OutputT>;
_interpolation: ?(input: number) => OutputT;
constructor(parent: AnimatedNode, config: InterpolationConfigType<OutputT>) {
super();
this._parent = parent;
this._config = config;
if (__DEV__) {
checkValidRanges(config.inputRange, config.outputRange);
// Create interpolation eagerly in dev, so we can signal errors faster
// even when using the native driver
this._getInterpolation();
}
}
_getInterpolation(): number => OutputT {
if (!this._interpolation) {
const config = this._config;
if (config.outputRange && typeof config.outputRange[0] === 'string') {
this._interpolation = (createStringInterpolation((config: any)): any);
} else {
this._interpolation = (createNumericInterpolation((config: any)): any);
}
}
return this._interpolation;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._parent.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): OutputT {
const parentValue: number = this._parent.__getValue();
invariant(
typeof parentValue === 'number',
'Cannot interpolate an input which is not a number.',
);
return this._getInterpolation()(parentValue);
}
interpolate<NewOutputT: number | string>(
config: InterpolationConfigType<NewOutputT>,
): AnimatedInterpolation<NewOutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._parent.__addChild(this);
}
__detach(): void {
this._parent.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
if (__DEV__) {
NativeAnimatedHelper.validateInterpolation(this._config);
}
// Only the `outputRange` can contain strings so we don't need to transform `inputRange` here
let outputRange = this._config.outputRange;
let outputType = null;
if (typeof outputRange[0] === 'string') {
// $FlowIgnoreMe[incompatible-cast]
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
const processedColor = processColor(value);
if (typeof processedColor === 'number') {
outputType = 'color';
return processedColor;
} else {
return NativeAnimatedHelper.transformDataType(value);
}
}): any);
}
return {
inputRange: this._config.inputRange,
outputRange,
outputType,
extrapolateLeft:
this._config.extrapolateLeft || this._config.extrapolate || 'extend',
extrapolateRight:
this._config.extrapolateRight || this._config.extrapolate || 'extend',
type: 'interpolation',
};
}
}