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
{ colorSpace: 'HSV' },
{ colorSpace: 'HSV', options: { useCorrectedHSVInterpolation: false } },
// LAB may produce slightly different results, but the differences are usually small
{ colorSpace: 'LAB', eps: 1e-5 },
Copy link
Member Author

Choose a reason for hiding this comment

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

LAB colorspace returned sth like rgba(255, 0.00001, 0, 0). I think that it is because of the conversion of the color between colorspaces.

test.each([
['red', 0xff0000ff],
['transparent', 0x00000000],
['transparent', undefined], // Transparent cannot be represented as a number
Copy link
Member Author

Choose a reason for hiding this comment

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

I decided to return undefined for the transparent color as the null value indicates that the value is invalid but the 'transparent' string is valid but cannot be converted to the number via the processColor function. That's why I decided to return undefined instead to differentiate this case.

const names: Record<string, number> = {
transparent: 0x00000000,
const names: Record<string, number | undefined> = {
transparent: undefined,
Copy link
Member Author

Choose a reason for hiding this comment

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

I explicitly store it as undefined in order not to return null. I prefer to return undefined as the color is valid but not convertible to number, so the number representation is not defined.


// This can happen while interpolating between a 'transparent' and a non-transparent color.
// We want to keep color channels unchanged but interpolate only the alpha channel.
return narrowedRange.leftEdgeOutput ?? narrowedRange.rightEdgeOutput ?? 0;
Copy link
Member Author

@MatiPl01 MatiPl01 Oct 15, 2025

Choose a reason for hiding this comment

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

When one of the interpolation pair values is undefined (i.e. when we interpolate from 'transparent' to a non-transparent color or from non-transparent to 'transparent'), we want to preserve color channels from the non-transparent one and just interpolate the alpha channel.

If both both are undefined (interpolation from 'transparent' to 'transparent'), then we can just use 0 (interpolation of any 2 fully transparent colors is the same, no matter what values are used for color channels).

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 👍

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.

3 participants