react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
380 lines (350 loc) • 9.71 kB
text/typescript
import {
blue,
green,
hsvToColor,
opacity,
processColor,
red,
rgbaColor,
RGBtoHSV,
} from './Colors';
import type { SharedValue } from './commonTypes';
import { makeMutable } from './core';
import culori from './culori';
import { ReanimatedError } from './errors';
import { useSharedValue } from './hook/useSharedValue';
import { Extrapolation, interpolate } from './interpolation';
/** @deprecated Please use Extrapolation instead */
export const Extrapolate = Extrapolation;
/**
* Options for color interpolation.
*
* @param gamma - Gamma value used in gamma correction. Defaults to `2.2`.
* @param useCorrectedHSVInterpolation - Whether to reduce the number of colors
* the interpolation has to go through. Defaults to `true`.
*/
export type InterpolationOptions = {
gamma?: number;
useCorrectedHSVInterpolation?: boolean;
};
const interpolateColorsHSV = (
value: number,
inputRange: readonly number[],
colors: InterpolateHSV,
options: InterpolationOptions
) => {
'worklet';
let h = 0;
const { useCorrectedHSVInterpolation = true } = options;
if (useCorrectedHSVInterpolation) {
// if the difference between hues in a range is > 180 deg
// then move the hue at the right end of the range +/- 360 deg
// and add the next point in the original place + 0.00001 with original hue
// to not break the next range
const correctedInputRange = [inputRange[0]];
const originalH = colors.h;
const correctedH = [originalH[0]];
for (let i = 1; i < originalH.length; ++i) {
const d = originalH[i] - originalH[i - 1];
if (originalH[i] > originalH[i - 1] && d > 0.5) {
correctedInputRange.push(inputRange[i]);
correctedInputRange.push(inputRange[i] + 0.00001);
correctedH.push(originalH[i] - 1);
correctedH.push(originalH[i]);
} else if (originalH[i] < originalH[i - 1] && d < -0.5) {
correctedInputRange.push(inputRange[i]);
correctedInputRange.push(inputRange[i] + 0.00001);
correctedH.push(originalH[i] + 1);
correctedH.push(originalH[i]);
} else {
correctedInputRange.push(inputRange[i]);
correctedH.push(originalH[i]);
}
}
h =
(interpolate(
value,
correctedInputRange,
correctedH,
Extrapolation.CLAMP
) +
1) %
1;
} else {
h = interpolate(value, inputRange, colors.h, Extrapolation.CLAMP);
}
const s = interpolate(value, inputRange, colors.s, Extrapolation.CLAMP);
const v = interpolate(value, inputRange, colors.v, Extrapolation.CLAMP);
const a = interpolate(value, inputRange, colors.a, Extrapolation.CLAMP);
return hsvToColor(h, s, v, a);
};
const toLinearSpace = (x: number[], gamma: number): number[] => {
'worklet';
return x.map((v) => Math.pow(v / 255, gamma));
};
const toGammaSpace = (x: number, gamma: number): number => {
'worklet';
return Math.round(Math.pow(x, 1 / gamma) * 255);
};
const interpolateColorsRGB = (
value: number,
inputRange: readonly number[],
colors: InterpolateRGB,
options: InterpolationOptions
) => {
'worklet';
const { gamma = 2.2 } = options;
let { r: outputR, g: outputG, b: outputB } = colors;
if (gamma !== 1) {
outputR = toLinearSpace(outputR, gamma);
outputG = toLinearSpace(outputG, gamma);
outputB = toLinearSpace(outputB, gamma);
}
const r = interpolate(value, inputRange, outputR, Extrapolation.CLAMP);
const g = interpolate(value, inputRange, outputG, Extrapolation.CLAMP);
const b = interpolate(value, inputRange, outputB, Extrapolation.CLAMP);
const a = interpolate(value, inputRange, colors.a, Extrapolation.CLAMP);
if (gamma === 1) {
return rgbaColor(r, g, b, a);
}
return rgbaColor(
toGammaSpace(r, gamma),
toGammaSpace(g, gamma),
toGammaSpace(b, gamma),
a
);
};
const interpolateColorsLAB = (
value: number,
inputRange: readonly number[],
colors: InterpolateLAB,
_options: InterpolationOptions
) => {
'worklet';
const l = interpolate(value, inputRange, colors.l, Extrapolation.CLAMP);
const a = interpolate(value, inputRange, colors.a, Extrapolation.CLAMP);
const b = interpolate(value, inputRange, colors.b, Extrapolation.CLAMP);
const alpha = interpolate(
value,
inputRange,
colors.alpha,
Extrapolation.CLAMP
);
const {
r: _r,
g: _g,
b: _b,
alpha: _alpha,
} = culori.oklab.convert.toRgb({ l, a, b, alpha });
return rgbaColor(_r, _g, _b, _alpha);
};
const _splitColorsIntoChannels = (
colors: readonly (string | number)[],
convFromRgb: (color: { r: number; g: number; b: number }) => {
ch1: number;
ch2: number;
ch3: number;
}
): {
ch1: number[];
ch2: number[];
ch3: number[];
alpha: number[];
} => {
'worklet';
const ch1: number[] = [];
const ch2: number[] = [];
const ch3: number[] = [];
const alpha: number[] = [];
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
const processedColor = processColor(color);
if (typeof processedColor === 'number') {
const convertedColor = convFromRgb({
r: red(processedColor),
g: green(processedColor),
b: blue(processedColor),
});
ch1.push(convertedColor.ch1);
ch2.push(convertedColor.ch2);
ch3.push(convertedColor.ch3);
alpha.push(opacity(processedColor));
}
}
return {
ch1,
ch2,
ch3,
alpha,
};
};
export interface InterpolateRGB {
r: number[];
g: number[];
b: number[];
a: number[];
}
const getInterpolateRGB = (
colors: readonly (string | number)[]
): InterpolateRGB => {
'worklet';
const { ch1, ch2, ch3, alpha } = _splitColorsIntoChannels(
colors,
(color) => ({
ch1: color.r,
ch2: color.g,
ch3: color.b,
})
);
return {
r: ch1,
g: ch2,
b: ch3,
a: alpha,
};
};
export interface InterpolateHSV {
h: number[];
s: number[];
v: number[];
a: number[];
}
const getInterpolateHSV = (
colors: readonly (string | number)[]
): InterpolateHSV => {
'worklet';
const { ch1, ch2, ch3, alpha } = _splitColorsIntoChannels(colors, (color) => {
const hsvColor = RGBtoHSV(color.r, color.g, color.b);
return {
ch1: hsvColor.h,
ch2: hsvColor.s,
ch3: hsvColor.v,
};
});
return {
h: ch1,
s: ch2,
v: ch3,
a: alpha,
};
};
export interface InterpolateLAB {
l: number[];
a: number[];
b: number[];
alpha: number[];
}
const getInterpolateLAB = (
colors: readonly (string | number)[]
): InterpolateLAB => {
'worklet';
const { ch1, ch2, ch3, alpha } = _splitColorsIntoChannels(colors, (color) => {
const labColor = culori.oklab.convert.fromRgb(color);
return {
ch1: labColor.l,
ch2: labColor.a,
ch3: labColor.b,
};
});
return {
l: ch1,
a: ch2,
b: ch3,
alpha,
};
};
/**
* Lets you map a value from a range of numbers to a range of colors using
* linear interpolation.
*
* @param value - A number from the `input` range that is going to be mapped to
* the color in the `output` range.
* @param inputRange - An array of numbers specifying the input range of the
* interpolation.
* @param outputRange - An array of output colors values (eg. "red", "#00FFCC",
* "rgba(255, 0, 0, 0.5)").
* @param colorSpace - The color space to use for interpolation. Defaults to
* 'RGB'.
* @param options - Additional options for interpolation -
* {@link InterpolationOptions}.
* @returns The color after interpolation from within the output range in
* rgba(r, g, b, a) format.
* @see https://docs.swmansion.com/react-native-reanimated/docs/utilities/interpolateColor
*/
export function interpolateColor(
value: number,
inputRange: readonly number[],
outputRange: readonly string[],
colorSpace?: 'RGB' | 'HSV' | 'LAB',
options?: InterpolationOptions
): string;
export function interpolateColor(
value: number,
inputRange: readonly number[],
outputRange: readonly number[],
colorSpace?: 'RGB' | 'HSV' | 'LAB',
options?: InterpolationOptions
): number;
export function interpolateColor(
value: number,
inputRange: readonly number[],
outputRange: readonly (string | number)[],
colorSpace: 'RGB' | 'HSV' | 'LAB' = 'RGB',
options: InterpolationOptions = {}
): string | number {
'worklet';
if (colorSpace === 'HSV') {
return interpolateColorsHSV(
value,
inputRange,
getInterpolateHSV(outputRange),
options
);
} else if (colorSpace === 'RGB') {
return interpolateColorsRGB(
value,
inputRange,
getInterpolateRGB(outputRange),
options
);
} else if (colorSpace === 'LAB') {
return interpolateColorsLAB(
value,
inputRange,
getInterpolateLAB(outputRange),
options
);
}
throw new ReanimatedError(
`Invalid color space provided: ${
colorSpace as string
}. Supported values are: ['RGB', 'HSV', 'LAB'].`
);
}
export enum ColorSpace {
RGB = 0,
HSV = 1,
LAB = 2,
}
export interface InterpolateConfig {
inputRange: readonly number[];
outputRange: readonly (string | number)[];
colorSpace: ColorSpace;
cache: SharedValue<InterpolateRGB | InterpolateHSV | null>;
options: InterpolationOptions;
}
export function useInterpolateConfig(
inputRange: readonly number[],
outputRange: readonly (string | number)[],
colorSpace = ColorSpace.RGB,
options: InterpolationOptions = {}
): SharedValue<InterpolateConfig> {
return useSharedValue<InterpolateConfig>({
inputRange,
outputRange,
colorSpace,
cache: makeMutable<InterpolateRGB | InterpolateHSV | null>(null),
options,
});
}
;