Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Wide Gamut (DisplayP3) Colors to React Native #738

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
68432e9
initial add support for CSS color() function proposal
ryanlntn Nov 20, 2023
48beb66
fill in more details
ryanlntn Nov 21, 2023
72dbafa
some android details
ryanlntn Nov 21, 2023
8844a67
add some more details
ryanlntn Nov 21, 2023
b5c4963
add more details
ryanlntn Nov 21, 2023
db607fd
fix some things
ryanlntn Nov 21, 2023
9e88917
a note about docs update
ryanlntn Nov 21, 2023
260b482
rename RFC
ryanlntn Nov 27, 2023
51f32e1
update summary
ryanlntn Nov 27, 2023
e9dd026
add inline style example
ryanlntn Nov 27, 2023
154fab1
improve basic example
ryanlntn Nov 27, 2023
9b06e73
add line to motivation regarding app quality and mission
ryanlntn Nov 27, 2023
156971b
add more details in how we teach this
ryanlntn Nov 27, 2023
abd3d74
update with more details regarding RCTConvert changes
ryanlntn Nov 28, 2023
c0fe689
add color space to color components and add more details to conversions
ryanlntn Nov 28, 2023
6fbff36
add more detail to ReactActivity changes
ryanlntn Nov 28, 2023
74b1792
add some more detail to ColorUtil and ColorPropConverter updates
ryanlntn Nov 28, 2023
efa99cd
add some lines about why we shouldn't break
ryanlntn Nov 28, 2023
dd5cd3e
update alternatives
ryanlntn Nov 29, 2023
33af21f
update filename
ryanlntn Nov 29, 2023
2085088
move Android implementation question to implementation section
ryanlntn Nov 29, 2023
06c4c25
add question about images and setLayerPaint
ryanlntn Nov 29, 2023
54e263a
improve formatting
ryanlntn Nov 30, 2023
f3de1d4
create srgb color directly as well
ryanlntn Nov 30, 2023
79b88c3
android wide color gamut should be opt-in/disabled by default
ryanlntn Nov 30, 2023
0e6d251
add feature flag to basic example
ryanlntn Dec 18, 2023
d447250
update implementation to use feature flag
ryanlntn Dec 18, 2023
77d6e67
update flag implementation
ryanlntn Dec 20, 2023
3abd3ec
update ios implementation for global flag
ryanlntn Jan 3, 2024
882b6c2
Merge branch 'main' into add-support-for-display-p3-colors
jamonholmgren May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
202 changes: 202 additions & 0 deletions proposals/0000-add-support-for-display-p3-colors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
---
title: Add Support for Wide Gamut (DisplayP3) Colors to React Native
author:
- Ryan Linton
- Yulian Glukhenko
date: 2023-11-20
---

# RFC0000: Add Support for Wide Gamut (DisplayP3) Colors to React Native

## Summary

React Native does [not currently support](https://github.com/facebook/react-native/issues/41517) wide gamut color spaces (i.e. display-p3). This proposal discusses adding support for this color space in React Native, covering:

- JS implementation
- Platform changes

## Basic example

Add a DisplayP3 background color and text color using color() function syntax per the [W3C CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/#color-function) spec.

### Using StyleSheet

```js
StyleSheet.create({
view: { backgroundColor: "color(display-p3 1 0.5 0)" },
text: { color: "color(display-p3 0 0.5 1)" },
});
```

### Using inline styles

```jsx
const MyComp = () => (
<View style={{ backgroundColor: "color(display-p3 1 0.5 0)" }}>
<Text style={{ color: "color(display-p3 0 0.5 1)" }}>
</View>
)
```

## Motivation

Since most new devices support wider gamut color spaces, React Native should support them as well. The DisplayP3 color space has had native support since Android 8.0 and iOS 9.3. The `color()` function was introduced with [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/#color-function), much of which is already implemented in React Native. Flutter also [recently added support](https://github.com/flutter/flutter/issues/55092) for DisplayP3 on iOS with plans to follow up with support for Android and then the framework itself. Support for DisplayP3 colors can improve the quality of React Native apps by providing developers a wider range of available colors to use and is aligned with React Native's mission to provide the same look and feel as the underlying native platforms.

## Detailed design

### JS Changes

1. Parse the color() function in [normalizeColor](https://github.com/facebook/react-native/blob/63213712125795ac082597dad2716258b90cdcd5/packages/normalize-color/index.js#L235)

```js

Choose a reason for hiding this comment

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

The motivation and API here make sense. How close is this towards a PR?

Copy link
Author

Choose a reason for hiding this comment

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

I haven't done any implementation work yet. Riccardo has been helping me with this so we were planning on leaving this open for feedback for a couple weeks before starting on implementation when he gets back.

const COLOR_SPACE = /display-p3|srgb/

function getMatchers() {
if (cachedMatchers === undefined) {
cachedMatchers = {
color: new RegExp(
'color(' +
call(COLOR_SPACE, NUMBER, NUMBER, NUMBER) +
'|' +
callWithSlashSeparator(COLOR_SPACE, NUMBER, NUMBER, NUMBER, NUMBER) +
')'
),
```

2. Include the color space in the return value of [StyleSheet processColor](https://github.com/facebook/react-native/blob/63213712125795ac082597dad2716258b90cdcd5/packages/react-native/Libraries/StyleSheet/processColor.js) and [Animated processColor](https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Animated/nodes/AnimatedColor.js)

```js
return { ["display-p3"]: true, red, green, blue, alpha };
```

### iOS Changes

1. Update [RCTConvert](https://github.com/facebook/react-native/blob/781b637db4268ad7f5f3910d99ebb5203467840b/packages/react-native/React/Base/RCTConvert.m#L881) to handle setting new color values for color space. If color space is not included preserve existing behavior.

```objc
+ (UIColor *)UIColor:(id)json
{
if (!json) {
return nil;
}
if ([json isKindOfClass:[NSArray class]]) {
// ...
} else if ([json isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = json;
id value = nil;

// handle json with srgb color space specified
if ((value = [dictionary objectForKey:@"srgb"])) {
return [UIColor colorWithRed:value.red green:value.green blue:value.blue alpha:value.alpha];

// handle json with display-p3 color space specified
} else if ((value = [dictionary objectForKey:@"display-p3"])) {
return [UIColor colorWithDisplayP3Red:value.red green:value.green blue:value.blue alpha:value.alpha];

// ...
```

2. Update [ColorComponents.h](https://github.com/facebook/react-native/blob/528f97152b7e0a7465c5b5c02e96c2c4306c78fe/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.h) to include color space.

```cpp
enum class ColorSpace {
sRGB,
DisplayP3
};

struct ColorComponents {
float red{0};
float green{0};
float blue{0};
float alpha{0};
ColorSpace colorSpace{ColorSpace::sRGB}; // Default to sRGB
};
```

3. Update [RCTConversions](https://github.com/facebook/react-native/blob/16ad818d21773cdf25156642fae83592352ae534/packages/react-native/React/Fabric/RCTConversions.h#L37) to handle setting new color values for color space. If color space is not included preserve existing behavior.

4. Update [RCTTextPrimitivesConversions](https://github.com/facebook/react-native/blob/ac1cdaa71620d5bb4860237cafb108f6aeae9aef/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h#L116) to handle setting new color values for color space. If color space is not included preserve existing behavior.

```cpp
auto components = facebook::react::colorComponentsFromColor(sharedColor);
if (components.colorSpace == ColorSpace::DisplayP3) {
return [UIColor colorWithDisplayP3Red:components.red green:components.green blue:components.blue alpha:components.alpha];
} else {
return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha];
}
```

### Android Changes

1. Enable the wide color gamut in [ReactActivity](https://github.com/facebook/react-native/blob/7625a502960e6b107e77542ff0d6f40fbf957322/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java#L22) if available.

```java
Window window = getWindow();
boolean isScreenWideColorGamut = window.getDecorView().isScreenWideColorGamut()
if (isScreenWideColorGamut) {
window.setColorMode(PixelFormat.COLOR_MODE_WIDE_COLOR_GAMUT)
}
```

This is prerequisite per [Android documentation](https://developer.android.com/training/wide-color-gamut) but they note:

> When wide color gamut mode is enabled, the activity's window uses more memory and GPU processing for screen composition. Before enabling wide color gamut mode, you should carefully consider if the activity truly benefits from it. For example, an activity that displays photos in fullscreen is a good candidate for wide color gamut mode, but an activity that shows small thumbnails is not.

So we'll likely want to leave this disabled by default on Android allow React Native developers to opt-in via a feature flag.

This won't impact any existing 32-bit integer colors since those will continue to be in sRGB color space. To actually use DisplayP3 colors it is necessary to pack the color space and 4 color components into a 64-bit long value.

2. Update [ColorUtil](https://github.com/facebook/react-native/blob/a6964b36294c3bfea09c0cdd65c5d0e3949f2dae/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java#L17) and [ColorPropConverter](https://github.com/facebook/react-native/blob/781b637db4268ad7f5f3910d99ebb5203467840b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java#L27) to return new color values for color space as longs.

```java
if (value instanceof ReadableMap) {
ReadableMap map = (ReadableMap) value;
float red = (float) map.getDouble("red");
float green = (float) map.getDouble("green");
float blue = (float) map.getDouble("blue");
float alpha = (float) map.getDouble("alpha");
int colorSpace = map.getInt("colorSpace")

if (colorSpace === 1) {
@ColorLong long p3 = pack(red, green, blue, alpha, ColorSpace.Named.DISPLAY_P3);
return Color.valueOf(p3);
} else {
// ...
}
}
```

3. Determine how best to utilize wide gamut color in Android. It's not clear if [Android View](<https://developer.android.com/reference/android/view/View#setBackgroundColor(int)>)'s actually support it directly. It is supported by [Paint](<https://developer.android.com/reference/android/graphics/Paint#setColor(long)>) so perhaps it's possible via [setLayerPaint](<https://developer.android.com/reference/android/view/View#setLayerPaint(android.graphics.Paint)>)?

### Docs

Update the [Color Reference](https://github.com/facebook/react-native-website/blob/main/docs/colors.md) to document color function usage in React Native.

## Drawbacks

There should be no breaking changes for users but the color implementation will be a bit more complicated than before. On iOS colors specified using existing methods will still use the same code paths and UIColor constructor. On Android colors specified using existing methods will still default to sRGB and only if DisplayP3 is specified will they be packed into a ColorLong.

## Alternatives

1. **Alternatives to CSS Color Module Level 4:** <br/> We could use a different standard for displaying wide gamut color. For example we could follow the [Color.js](https://github.com/color-js/color.js) API: `new Color({space: "p3", coords: [0, 1, 0], alpha: .9})`. However this deviates from the existing color implementation which mostly implements [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/#color-function) and would be surprising to existing React Native developers.

2. **Utilizing Swizzling on iOS:** <br/> As an alternative to the proposed iOS changes, swizzling could be considered similar to [RN #41517](https://github.com/facebook/react-native/issues/41517). This has a major downside of being an all or nothing breaking change forcing all React Native users into the DisplayP3 color space as well as being a more confusing and less maintainable solution.

3. **Full switch to DisplayP3:** <br/> In a similar vein we could just update the constructors directly without preserving existing behavior. While this would be a simpler implementation it would be a breaking change. Any existing visual regression tests would likely break and React Native users would now be required to specify all colors in a wider gamut.
Copy link

Choose a reason for hiding this comment

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

What do you think about a way to optionally fully opt-in to use DisplayP3?

The current approach would take a lot of extra steps to ensure every color has ["display-p3"]: true

Choose a reason for hiding this comment

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

This seems like a good idea.

Many libraries have predefined colors in them; I'd like to see how everything looks with display-p3 across the board. Usually, the differences should be slight and mainly positive, with colors being more vibrant.

Copy link
Author

Choose a reason for hiding this comment

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

What do you think about a way to optionally fully opt-in to use DisplayP3?

I can definitely see the utility of this. I'm not really sure what the API for that should look like though. AFAIK the APIs on Android, iOS, and web all only allow specifying color space per instance. Since color properties already mostly implement CSS Color Module Level 4 we were mainly planning on extending that further with this proposed API.

Choose a reason for hiding this comment

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

I agree that it could be useful.
The way I think this should be implemented is by factoring out the constructor functions in a single factory method so we can decide which initializer we want to use as default. At that point, we can specify a static feature flag that controls the default behavior.

For Android things are a little bit more convoluted because Android uses int to represent sRGB colors and long to represent WideGamut colors. So, not only we need to update how a color is created but we also have to overload several methods to achieve what we need.

But it is probably doable.

@ryanlntn could you update the proposal to add a feature flag that will control the default init behavior?


## Adoption strategy

This is intended to be a non-breaking change with zero impact to existing users. Adoption is entirely opt-in by using the color function in styles to start using display-p3 colors.

## How we teach this

Developers familiar with [color() on web](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color) should find this feature familiar and straightforward to use. To ensure that this feature is easily accessible to all developers we should do the following:

1. Document the inclusion of the color function in the [official color reference](https://reactnative.dev/docs/colors).
1. Partner with the Release Crew to make sure the new feature is included in a release blog post.
1. Publish a blog post on the [Infinite Red blog](https://shift.infinite.red/)

## Unresolved questions

- Do we need to do additional work to support interpolating colors for animations?
- Is there additional work we need to do to support wide gamut color in images?