-
Notifications
You must be signed in to change notification settings - Fork 127
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
base: main
Are you sure you want to change the base?
Changes from all commits
68432e9
48beb66
72dbafa
8844a67
b5c4963
db607fd
9e88917
260b482
51f32e1
e9dd026
154fab1
9b06e73
156971b
abd3d74
c0fe689
6fbff36
74b1792
efa99cd
dd5cd3e
33af21f
2085088
06c4c25
54e263a
f3de1d4
79b88c3
0e6d251
d447250
77d6e67
3abd3ec
882b6c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
--- | ||
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> | ||
) | ||
``` | ||
|
||
### Using a feature flag | ||
|
||
Opt-in to using DisplayP3 as the default color space by using a feature flag. | ||
|
||
```kt | ||
// MyMainApplication.kt | ||
|
||
override val reactNativeHost: ReactNativeHost = | ||
object : DefaultReactNativeHost(this) { | ||
// ... | ||
override val defaultColorSpace: RCTColorSpace = RCTColorSpaceDisplayP3 | ||
} | ||
``` | ||
|
||
```objc | ||
// AppDelegate.mm | ||
|
||
#import <React/RCTConvert.h> | ||
|
||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions | ||
{ | ||
// ... | ||
RCTSetDefaultColorSpace(RCTColorSpaceDisplayP3) | ||
// ... | ||
} | ||
``` | ||
|
||
Then specify colors as before but now they will be in the DisplayP3 color space. | ||
|
||
```jsx | ||
StyleSheet.create({ | ||
view: { backgroundColor: "rgb(255, 128, 0)" }, | ||
text: { color: "#0080FF" }, | ||
}); | ||
|
||
const MyComp = () => ( | ||
<View style={{ backgroundColor: "#FF8000" }}> | ||
<Text style={{ color: "rgb(0, 128, 255)" }}> | ||
</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 | ||
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 { space: "display-p3", 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 | ||
typedef NS_ENUM(NSInteger, RCTColorSpace) { | ||
RCTColorSpaceSRGB, | ||
RCTColorSpaceDisplayP3, | ||
}; | ||
|
||
static RCTColorSpace defaultColorSpace = (RCTColorSpace)facebook::react::defaultColorSpace; | ||
RCTColorSpace RCTGetDefaultColorSpace(void) | ||
{ | ||
return (RCTColorSpace)facebook::react::defaultColorSpace; | ||
} | ||
void RCTSetDefaultColorSpace(RCTColorSpace colorSpace) | ||
{ | ||
facebook::react::setDefaultColorSpace((facebook::react::ColorSpace)colorSpace); | ||
} | ||
|
||
+ (UIColor *)createColorFrom:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a | ||
{ | ||
RCTColorSpace space = RCTGetDefaultColorSpace(); | ||
return [self createColorFrom:r green:g blue:b alpha:a andColorSpace:space]; | ||
} | ||
+ (UIColor *)createColorFrom:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha andColorSpace:(RCTColorSpace)colorSpace | ||
{ | ||
if (colorSpace == RCTColorSpaceDisplayP3) { | ||
return [UIColor colorWithDisplayP3Red:red green:green blue:blue alpha:alpha]; | ||
} | ||
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; | ||
} | ||
|
||
+ (RCTColorSpace)colorSpaceFromString:(NSString *)colorSpace { | ||
if ([colorSpace isEqualToString:@"display-p3"]) { | ||
return RCTColorSpaceDisplayP3; | ||
} else if ([colorSpace isEqualToString:@"srgb"]) { | ||
return RCTColorSpaceSRGB; | ||
} | ||
return RCTGetDefaultColorSpace(); | ||
} | ||
|
||
+ (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; | ||
NSString *rawColorSpace = [dictionary objectForKey: @"space"]; | ||
if ([@[@"display-p3", @"srgb"] containsObject:rawColorSpace]) { | ||
CGFloat r = [[dictionary objectForKey:@"r"] floatValue]; | ||
CGFloat g = [[dictionary objectForKey:@"g"] floatValue]; | ||
CGFloat b = [[dictionary objectForKey:@"b"] floatValue]; | ||
CGFloat a = [[dictionary objectForKey:@"a"] floatValue]; | ||
RCTColorSpace colorSpace = [self colorSpaceFromString: rawColorSpace]; | ||
return [self createColorFrom:r green:g blue:b alpha:a andColorSpace:colorSpace]; | ||
// ... | ||
``` | ||
|
||
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 }; | ||
|
||
static ColorSpace defaultColorSpace = ColorSpace::sRGB; | ||
ColorSpace getDefaultColorSpace() { | ||
return defaultColorSpace; | ||
} | ||
void setDefaultColorSpace(ColorSpace newColorSpace) { | ||
defaultColorSpace = newColorSpace; | ||
} | ||
|
||
struct ColorComponents { | ||
float red{0}; | ||
float green{0}; | ||
float blue{0}; | ||
float alpha{0}; | ||
ColorSpace colorSpace{getDefaultColorSpace()}; | ||
}; | ||
``` | ||
|
||
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); | ||
return [RCTConvert createColorFrom:components.red | ||
green:components.green | ||
blue:components.blue | ||
alpha:components.alpha | ||
andColorSpace:(RCTColorSpace)components.colorSpace]; | ||
``` | ||
|
||
### 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The current approach would take a lot of extra steps to ensure every color has There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that it could be useful. For Android things are a little bit more convoluted because Android uses 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? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.