Skip to content

Commit

Permalink
Fix immutable color cache (software-mansion#2796)
Browse files Browse the repository at this point in the history
## Description

The color cache was mutable only from the JS side. I decided to remove the cache and instead of this I created a new faster function with a better cache mechanism without previous limitations.

Performance comparation between `interpolateColor` and `interpolateSharableColor`:
```
old 2894ms - new 1741ms // 39.8% faster
old 2720ms - new 1697ms // 37.6% faster
old 2739ms - new 1650ms // 39.7% faster
old 2738ms - new 1781ms // 34.9% faster
old 2727ms - new 1662ms // 39.0% faster
```
Performance test code:
```js
const interpolateConfig = useInterpolateConfig([0, 350], ['#ff0000', '#00ff00']);
const v1 = interpolateSharableColor(randomWidth.value, interpolateConfig);
const iterations = 10000;
const t1_start = Date.now();
for(let i = 0; i < iterations; i++) {
  const v = interpolateSharableColor(randomWidth.value, interpolateConfig);
}
const t1_end = Date.now();
const t2_start = Date.now();
for(let i = 0; i < iterations; i++) {
  const v = interpolateColor(randomWidth.value, [0, 350], ['#ff0000', '#00ff00']);
}
const t2_end = Date.now();
console.log(t1_end - t1_start, t2_end - t2_start);
```

## Usage
```js
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  interpolateSharableColor,
  useInterpolateConfig,
} from 'react-native-reanimated';
import { View, Button } from 'react-native';
import React from 'react';

function AnimatedStyleUpdateExample(): React.ReactElement {
  const randomWidth = useSharedValue(10);
  const interpolateConfig = useInterpolateConfig([0, 350], ['#ff0000', '#00ff00']); // <- here

  const style = useAnimatedStyle(() => {
    return {
      width: randomWidth.value,
      backgroundColor: interpolateSharableColor(randomWidth.value, interpolateConfig) // <- here
    };
  });

  return (
    <View style={{ flex: 1, flexDirection: 'column', }}>
      <Animated.View
        style={[ 
          { width: 100, height: 80, backgroundColor: 'black', margin: 30 }, 
          style 
        ]}
      />
      <Button
        title="toggle"
        onPress={() => { randomWidth.value = withTiming(Math.random() * 350) }}
      />
    </View>
  );
}

export default AnimatedStyleUpdateExample;
```

Fixes software-mansion#2329 software-mansion#2739
  • Loading branch information
piaskowyk authored Jan 27, 2022
1 parent 6222491 commit 2c48937
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 54 deletions.
36 changes: 36 additions & 0 deletions react-native-reanimated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,42 @@ declare module 'react-native-reanimated' {
colorSpace?: 'RGB' | 'HSV'
): string | number;

export enum ColorSpace {
RGB = 0,
HSV = 1,
}

export interface InterpolateRGB {
r: number[];
g: number[];
b: number[];
a: number[];
}

export interface InterpolateHSV {
h: number[];
s: number[];
v: number[];
}

export interface InterpolateConfig {
inputRange: readonly number[];
outputRange: readonly (string | number)[];
colorSpace: ColorSpace;
cache: SharedValue<InterpolateRGB | InterpolateHSV>;
}

export function useInterpolateConfig(
inputRange: readonly number[],
outputRange: readonly (string | number)[],
colorSpace?: ColorSpace
): SharedValue<InterpolateConfig>;

export function interpolateSharableColor(
value: number,
interpolateConfig: SharedValue<InterpolateConfig>
): string | number;

export function makeMutable<T>(initialValue: T): SharedValue<T>;

type DependencyList = ReadonlyArray<any>;
Expand Down
131 changes: 77 additions & 54 deletions src/reanimated2/Colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

/* eslint no-bitwise: 0 */
import { Platform } from 'react-native';
import { makeRemote, makeShareable, isConfigured } from './core';
import { makeRemote, makeShareable, isConfigured, makeMutable } from './core';
import { interpolate } from './interpolation';
// @ts-ignore JS file
import { Extrapolate } from '../reanimated1/derived';
import { SharedValue } from './commonTypes';
import { useSharedValue } from './hook/useSharedValue';

interface RBG {
interface RGB {
r: number;
g: number;
b: number;
Expand Down Expand Up @@ -475,7 +477,7 @@ export const rgbaColor = (
* 0 <= r, g, b <= 255
* returns 0 <= h, s, v <= 1
*/
function RGBtoHSV(rgb: RBG): HSV;
function RGBtoHSV(rgb: RGB): HSV;
function RGBtoHSV(r: number, g: number, b: number): HSV;
function RGBtoHSV(r: any, g?: any, b?: any): HSV {
'worklet';
Expand Down Expand Up @@ -528,8 +530,8 @@ function RGBtoHSV(r: any, g?: any, b?: any): HSV {
* 0 <= h, s, v <= 1
* returns 0 <= r, g, b <= 255
*/
function HSVtoRGB(hsv: HSV): RBG;
function HSVtoRGB(h: number, s: number, v: number): RBG;
function HSVtoRGB(hsv: HSV): RGB;
function HSVtoRGB(h: number, s: number, v: number): RGB;
function HSVtoRGB(h: any, s?: any, v?: any) {
'worklet';
/* eslint-disable */
Expand Down Expand Up @@ -656,7 +658,7 @@ export function toRGBA(HSVA: ParsedColorArray): string {
const interpolateColorsHSV = (
value: number,
inputRange: readonly number[],
colors: InterpolateCacheHSV
colors: InterpolateHSV
) => {
'worklet';
const h = interpolate(value, inputRange, colors.h, Extrapolate.CLAMP);
Expand All @@ -668,7 +670,7 @@ const interpolateColorsHSV = (
const interpolateColorsRGB = (
value: number,
inputRange: readonly number[],
colors: InterpolateCacheRGBA
colors: InterpolateRGB
) => {
'worklet';
const r = interpolate(value, inputRange, colors.r, Extrapolate.CLAMP);
Expand All @@ -678,27 +680,17 @@ const interpolateColorsRGB = (
return rgbaColor(r, g, b, a);
};

interface InterpolateCacheRGBA {
interface InterpolateRGB {
r: number[];
g: number[];
b: number[];
a: number[];
}

const BUFFER_SIZE = 200;
const hashOrderRGBA: any = new ArrayBuffer(BUFFER_SIZE);
let curentHashIndexRGBA = 0;
const interpolateCacheRGBA: { [name: string]: InterpolateCacheRGBA } = {};

const getInterpolateCacheRGBA = (
const getInterpolateRGB = (
colors: readonly (string | number)[]
): InterpolateCacheRGBA => {
): InterpolateRGB => {
'worklet';
const hash = colors.join('');
const cache = interpolateCacheRGBA[hash];
if (cache !== undefined) {
return cache;
}

const r = [];
const g = [];
Expand All @@ -715,37 +707,19 @@ const getInterpolateCacheRGBA = (
a.push(opacity(proocessedColor));
}
}
const newCache = { r, g, b, a };
const overrideHash = hashOrderRGBA[curentHashIndexRGBA];
if (overrideHash) {
delete interpolateCacheRGBA[overrideHash];
}
interpolateCacheRGBA[hash] = newCache;
hashOrderRGBA[curentHashIndexRGBA] = hash;
curentHashIndexRGBA = (curentHashIndexRGBA + 1) % BUFFER_SIZE;
return newCache;
return { r, g, b, a };
};

interface InterpolateCacheHSV {
interface InterpolateHSV {
h: number[];
s: number[];
v: number[];
}

const hashOrderHSV: any = new ArrayBuffer(BUFFER_SIZE);
let curentHashIndexHSV = 0;
const interpolateCacheHSV: { [name: string]: InterpolateCacheHSV } = {};

const getInterpolateCacheHSV = (
const getInterpolateHSV = (
colors: readonly (string | number)[]
): InterpolateCacheHSV => {
): InterpolateHSV => {
'worklet';
const hash = colors.join('');
const cache = interpolateCacheHSV[hash];
if (cache !== undefined) {
return cache;
}

const h = [];
const s = [];
const v = [];
Expand All @@ -758,15 +732,7 @@ const getInterpolateCacheHSV = (
v.push(proocessedColor.v);
}
}
const newCache = { h, s, v };
const overrideHash = hashOrderHSV[curentHashIndexHSV];
if (overrideHash) {
delete interpolateCacheHSV[overrideHash];
}
interpolateCacheHSV[hash] = newCache;
hashOrderHSV[curentHashIndexHSV] = hash;
curentHashIndexHSV = (curentHashIndexHSV + 1) % BUFFER_SIZE;
return newCache;
return { h, s, v };
};

export const interpolateColor = (
Expand All @@ -780,16 +746,73 @@ export const interpolateColor = (
return interpolateColorsHSV(
value,
inputRange,
getInterpolateCacheHSV(outputRange)
getInterpolateHSV(outputRange)
);
} else if (colorSpace === 'RGB') {
return interpolateColorsRGB(
value,
inputRange,
getInterpolateCacheRGBA(outputRange)
getInterpolateRGB(outputRange)
);
}
throw new Error(
`Invalid color space provided: ${colorSpace}. Supported values are: ['RGB', 'HSV']`
);
};

export enum ColorSpace {
RGB = 0,
HSV = 1,
}

export interface InterpolateConfig {
inputRange: readonly number[];
outputRange: readonly (string | number)[];
colorSpace: ColorSpace;
cache: SharedValue<InterpolateRGB | InterpolateHSV>;
}

export function useInterpolateConfig(
inputRange: readonly number[],
outputRange: readonly (string | number)[],
colorSpace = ColorSpace.RGB
): SharedValue<InterpolateConfig> {
return useSharedValue({
inputRange,
outputRange,
colorSpace,
cache: makeMutable(null),
});
}

export const interpolateSharableColor = (
value: number,
interpolateConfig: SharedValue<InterpolateConfig>
): string | number => {
'worklet';
let colors = interpolateConfig.value.cache.value;
if (interpolateConfig.value.colorSpace === ColorSpace.RGB) {
if (!colors) {
colors = getInterpolateRGB(interpolateConfig.value.outputRange);
interpolateConfig.value.cache.value = colors;
}
return interpolateColorsRGB(
value,
interpolateConfig.value.inputRange,
colors as InterpolateRGB
);
} else if (interpolateConfig.value.colorSpace === ColorSpace.HSV) {
if (!colors) {
colors = getInterpolateHSV(interpolateConfig.value.outputRange);
interpolateConfig.value.cache.value = colors;
}
return interpolateColorsHSV(
value,
interpolateConfig.value.inputRange,
colors as InterpolateHSV
);
}
throw new Error(
`invalid color space provided: ${colorSpace}. Supported values are: ['RGB', 'HSV']`
`Invalid color space provided: ${interpolateConfig.value.colorSpace}. Supported values are: ['RGB', 'HSV']`
);
};

0 comments on commit 2c48937

Please sign in to comment.