Skip to content

Conversation

@MatiPl01
Copy link
Member

@MatiPl01 MatiPl01 commented Oct 15, 2025

Summary

This PR fixes the interpolateColor interpolation between non-transparent colors and 'transparent' (keyword) color. The previous implementation was incorrect as it converted 'transparent' to the 0x00000000 (black transparent), but the 'transparent' keyword is not just a black transparent color. Because of that, the interpolation from/to the transparent color always changed the color to black (the difference can be seen on the recordings below).

Example recordings

If you pause recordings and compare, you can see that the Before one goes through a dirty yellow to transparent because it becomes more and more black on each animation step.

Before After
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2025-10-15.at.20.03.31.mp4
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2025-10-15.at.19.56.50.mp4
Example source code
import React, { useEffect } from 'react';
import { StyleSheet } from 'react-native';
import Animated, {
  interpolateColor,
  useAnimatedStyle,
  useSharedValue,
  withRepeat,
  withTiming,
} from 'react-native-reanimated';

export default function EmptyExample() {
  const sv = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => ({
    backgroundColor: interpolateColor(
      sv.value,
      [0, 1],
      ['yellow', 'transparent']
    ),
  }));

  useEffect(() => {
    sv.value = 0;
    sv.value = withRepeat(withTiming(1, { duration: 3000 }), -1, true);
  }, [sv]);

  return <Animated.View style={[styles.container, animatedStyle]} />;
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

@MatiPl01 MatiPl01 self-assigned this Oct 15, 2025
Base automatically changed from @matipl01/binary-search-interpolate to main October 16, 2025 09:51
@MatiPl01 MatiPl01 force-pushed the @matipl01/fix-transparent-color-interpolation-css branch from fa1a973 to d5538d1 Compare October 16, 2025 14:50
@software-mansion software-mansion deleted a comment Oct 16, 2025
@software-mansion software-mansion deleted a comment Oct 16, 2025
Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that much code necessary? Since the only interface for the user is interpolateColor, can't we just convert transparent in that function to a relevant color in the given color space and leave color interpolators unchanged?

@MatiPl01
Copy link
Member Author

MatiPl01 commented Oct 16, 2025

Is that much code necessary? Since the only interface for the user is interpolateColor, can't we just convert 'transparent' in that function to a relevant color in the given color space and leave color interpolators unchanged?

If you have an idea how to represent such a relevant color, then I am open to suggestions and can change the implementation.

I personally think that such representation doesn't exist as the RGB channels depend on the other color used in the interpolation pair. For example, when interpolating from (122, 235, 52, 255) to 'transparent', we have to convert the 'transparent' string to (122, 235, 52, 0). We can't do this without knowing the second color of the interpolation pair.

Also consider this example:

interpolateColor(
  progress,
  [0, 0.5, 1],
  ['rgb(122, 235, 52)', 'transparent', 'rgba(52, 58, 235, 100)']
)

In this case, the 'transparent' color cannot be replaced with a single value because it will be different when interpolating between these 2 values rgb(122, 235, 52), 'transparent' and when interpolating between this pair 'transparent', rgba(52, 58, 235, 100).

  • rgb(122, 235, 52) to 'transparent' --> rgba(122, 235, 52, 255) to rgba(122, 235, 52, 0)
  • 'transparent' to rgba(52, 58, 235, 100) --> rgba(52, 58, 235, 0) to rgba(52, 58, 235, 100)

Copy link
Member

@piaskowyk piaskowyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the visual effect 👍

@tjzel
Copy link
Collaborator

tjzel commented Oct 20, 2025

If you have an idea how to represent such a relevant color, then I am open to suggestions and can change the implementation.

I think we could artificially extend inputs and outputs.

When receiving (i.e. in RGB)

interpolateColor(
  progress,
  [0, 0.5, 1],
  ['#ff0000ff' /* red */, 'transparent', '#0000ffff' /* blue */]
)

we could effectively transform it as if the user invoked

interpolateColor(
  progress,
  [0, 0.5, 0.5, 1],
  ['#ff0000ff' /* red */, '#ff000000' /* transparent red */, '#0000ff00' /* transparent blue */, '#0000ffff' /* blue */]
)

This is a super simple, working PoC for RGB colors:

// interpolateColor.ts

function processTransparency(
  inputRange: number[],
  outputRange: (string | number)[]
): void {
  'worklet';

  for (let i = 0; i < outputRange.length; i++) {
    const color = outputRange[i];
    if (color !== 'transparent') {
      continue;
    }

    const previousColor = processColor(outputRange[i - 1]);

    const redPrev = red(previousColor!);
    const greenPrev = green(previousColor!);
    const bluePrev = blue(previousColor!);
    const transparentPrev = rgbaColor(redPrev, greenPrev, bluePrev, 0);

    const nextColor = processColor(outputRange[i + 1]);

    const redNext = red(nextColor!);
    const greenNext = green(nextColor!);
    const blueNext = blue(nextColor!);

    const transparentNext = rgbaColor(redNext, greenNext, blueNext, 0);

    outputRange.splice(i, 1, transparentPrev, transparentNext);
    inputRange.splice(i, 1, inputRange[i], inputRange[i]);
  }
}

diff to try it out:

diff --git a/apps/common-app/src/apps/reanimated/examples/ColorInterpolationExample.tsx b/apps/common-app/src/apps/reanimated/examples/ColorInterpolationExample.tsx
index 0cd8ef57d4..ec8f4c37cb 100644
--- a/apps/common-app/src/apps/reanimated/examples/ColorInterpolationExample.tsx
+++ b/apps/common-app/src/apps/reanimated/examples/ColorInterpolationExample.tsx
@@ -108,9 +108,15 @@ function ColorInterpolation({
 
 function rgbInterpolation(color1: string, color2: string, progress: number) {
   'worklet';
-  return interpolateColor(progress, [0, 1], [color1, color2], 'RGB', {
-    gamma: 1,
-  });
+  return interpolateColor(
+    progress,
+    [0, 0.5, 1],
+    ['#ff0000ff', 'transparent', '#0000ffff'],
+    'RGB',
+    {
+      gamma: 1,
+    }
+  );
 }
 
 function rgbGammaInterpolation(
@@ -236,4 +242,8 @@ const styles = StyleSheet.create({
     color: 'black',
     fontWeight: 'bold',
   },
+  // eslint-disable-next-line react-native/no-unused-styles
+  dupsko: {
+    backgroundColor: 'transparent',
+  },
 });
diff --git a/packages/react-native-reanimated/src/interpolateColor.ts b/packages/react-native-reanimated/src/interpolateColor.ts
index d4cbcd3261..0a3627c46a 100644
--- a/packages/react-native-reanimated/src/interpolateColor.ts
+++ b/packages/react-native-reanimated/src/interpolateColor.ts
@@ -276,6 +276,38 @@ const getInterpolateLAB = (
   };
 };
 
+function processTransparency(
+  inputRange: number[],
+  outputRange: (string | number)[]
+): void {
+  'worklet';
+
+  for (let i = 0; i < outputRange.length; i++) {
+    const color = outputRange[i];
+    if (color !== 'transparent') {
+      continue;
+    }
+
+    const previousColor = processColor(outputRange[i - 1]);
+
+    const redPrev = red(previousColor!);
+    const greenPrev = green(previousColor!);
+    const bluePrev = blue(previousColor!);
+    const transparentPrev = rgbaColor(redPrev, greenPrev, bluePrev, 0);
+
+    const nextColor = processColor(outputRange[i + 1]);
+
+    const redNext = red(nextColor!);
+    const greenNext = green(nextColor!);
+    const blueNext = blue(nextColor!);
+
+    const transparentNext = rgbaColor(redNext, greenNext, blueNext, 0);
+
+    outputRange.splice(i, 1, transparentPrev, transparentNext);
+    inputRange.splice(i, 1, inputRange[i], inputRange[i]);
+  }
+}
+
 /**
  * Lets you map a value from a range of numbers to a range of colors using
  * linear interpolation.
@@ -326,6 +358,8 @@ export function interpolateColor(
       options
     );
   } else if (colorSpace === 'RGB') {
+    processTransparency(inputRange, outputRange);
+
     return interpolateColorsRGB(
       value,
       inputRange,

Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, left some minor comments.

@MatiPl01 MatiPl01 requested a review from tjzel October 21, 2025 21:39
@MatiPl01 MatiPl01 added this pull request to the merge queue Oct 22, 2025
Merged via the queue into main with commit e0595d8 Oct 22, 2025
12 checks passed
@MatiPl01 MatiPl01 deleted the @matipl01/fix-transparent-color-interpolation-css branch October 22, 2025 07:15
MatiPl01 added a commit that referenced this pull request Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants