-
Notifications
You must be signed in to change notification settings - Fork 24.3k
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
fix: start and end points linear gradient algorithm #47003
base: main
Are you sure you want to change the base?
fix: start and end points linear gradient algorithm #47003
Conversation
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.
Do you have any plans to implement other backgroundImage
features in the near future? If so do give us a heads up!
@@ -123,25 +140,30 @@ function parseCSSLinearGradient( | |||
while ((match = linearGradientRegex.exec(cssString))) { | |||
const gradientContent = match[1]; | |||
const parts = gradientContent.split(','); | |||
let points = TO_BOTTOM_START_END_POINTS; | |||
let orientation: LinearGradientOrientation = DEFAULT_ORIENTATION; | |||
const trimmedDirection = parts[0].trim().toLowerCase(); | |||
const colorStopRegex = | |||
/\s*((?:(?:rgba?|hsla?)\s*\([^)]+\))|#[0-9a-fA-F]+|[a-zA-Z]+)(?:\s+(-?[0-9.]+%?)(?:\s+(-?[0-9.]+%?))?)?\s*/gi; |
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.
Can we add a comment explaining what this regex matches to and same with the linearGradientRegex?
eg from processFilter: // matches on functions with args and nested functions like "drop-shadow(10 10 10 rgba(0, 0, 0, 1))"
Or maybe a link to the syntax we are matching from the spec?
@@ -123,25 +140,30 @@ function parseCSSLinearGradient( | |||
while ((match = linearGradientRegex.exec(cssString))) { |
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.
It would be nice if we only call parseCSSLinearGradient
if we already know what we are parsing is a linearGradient. I'm thinking it can be a head start for if we add other backgroundImage
functions like radial gradients or even url
.
We did something similar for drop-shadow on processFilter.js
I'll leave this up to you since its not a blocker for this.
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.
@jorge-cab yes, will follow up on this when adding more support to backgroundImage
. First i want to implement color hints syntax support with linear gradients, which will be done in this PR. Thanks for reviewing!
Catching up on context here, was there previous work from you to implement the linear gradient? Or this is a longstanding thing that is being addressed? Do you have screenshots of the difference between the gradient? What's the con of using a non-standard algorithm today? |
@lunaleaps Sure. Linear gradient was done in these PRs (#45434) and #45433. It is still behind experimental flag like box shadow and filters. The con of using non standard algo is I used simple unit square for calculation of start and end points which doesn't work well for rectangle boxes when angles are cornered. Something I found last week while testing 🤦♂️ so made this PR. This PR adds standard algo which makes it match to the output of web. e.g. output of
|
Thanks @intergalacticspacehighway for the details and including the screenshots! |
@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
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.
Math looks good! Nice job figuring this out.
orientationIt->second.hasType<std::unordered_map<std::string, RawValue>>()) { | ||
auto orientationMap = static_cast<std::unordered_map<std::string, RawValue>>(orientationIt->second); | ||
|
||
auto typeIt = orientationMap.find("type"); |
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.
nit: this naming is shadowing we should rename
|
||
auto colorSpace = getDefaultColorSpace() == ColorSpace::sRGB ? CGColorSpaceCreateDeviceRGB() : CGColorSpaceCreateWithName(kCGColorSpaceDisplayP3); | ||
|
||
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations); |
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.
nit: This naming is shadowing, we should rename
UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { | ||
CGContextRef context = rendererContext.CGContext; | ||
NSMutableArray *colors = [NSMutableArray array]; | ||
CGFloat *locations = (CGFloat *)malloc(sizeof(CGFloat) * colorStops.size()); |
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.
We really want to avoid malloc can we do something like:
CGFloat *locations = (CGFloat *)malloc(sizeof(CGFloat) * colorStops.size()); | |
CGFloat locations[colorStops.size()]; |
backgroundImageLayer.borderWidth = layer.borderWidth; | ||
backgroundImageLayer.borderColor = layer.borderColor; |
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.
We don't need to set these two here, border layer will be composited on top of backgroundImage
backgroundImageLayer.borderWidth = layer.borderWidth; | |
backgroundImageLayer.borderColor = layer.borderColor; |
backgroundImageLayer.frame = layer.bounds; | ||
backgroundImageLayer.masksToBounds = YES; | ||
// border styling to work with gradient layers | ||
if (useCoreAnimationBorderRendering) { |
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.
This section should just be used to round the corners of the backgroundImage we don't really care about most of the things useCoreAnimationBorderRendering
checks for. We can focus on the border being uniform
if (useCoreAnimationBorderRendering) { | |
if (borderMetrics.borderRadii.isUniform()) { |
CAShapeLayer *maskLayer = [CAShapeLayer layer]; | ||
CGPathRef path = RCTPathCreateWithRoundedRect( | ||
self.bounds, | ||
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero), | ||
nil); | ||
maskLayer.path = path; | ||
CGPathRelease(path); | ||
backgroundImageLayer.mask = maskLayer; |
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.
Can we use createMaskLayer
from line ~1094 instead
maskLayer.path = path; | ||
CGPathRelease(path); | ||
backgroundImageLayer.mask = maskLayer; | ||
} |
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.
We need to reset corners to 0 in case of a state change backgroundImageLayer.cornerRadius = 0;
@@ -21,63 +20,46 @@ public class Gradient(gradient: ReadableMap?, context: Context) { | |||
} | |||
|
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.
Can we make this class internal
instead of public. We don' want people to start relying on this yet
import kotlin.math.atan | ||
import kotlin.math.tan | ||
|
||
public class LinearGradient( |
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.
public class LinearGradient( | |
internal class LinearGradient( |
private val orientation: Orientation | ||
|
||
init { | ||
orientation = when (val type = orientationMap.getString("type")) { | ||
"angle" -> { | ||
val angle = orientationMap.getDouble("value") | ||
Orientation.Angle(angle) | ||
} | ||
|
||
"direction" -> { | ||
val direction = orientationMap.getString("value") | ||
?: throw IllegalArgumentException("Direction value cannot be null") | ||
Orientation.Direction(direction) | ||
} | ||
|
||
else -> throw IllegalArgumentException("Invalid orientation type: $type") | ||
} | ||
} |
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.
nit: We should just assign this to the variable no need to use init
Pushed all fixes @jorge-cab. Thanks for the quick review! <3 |
@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
@intergalacticspacehighway I should mention that I'm starting to do work to implement the image features of background-image and potentially radial-gradient after so if you are interested in taking on radial-gradient lmk so that we don't step on each other. |
@jorge-cab That's amazing to hear, thanks for informing. i was thinking to pick radial gradients after finishing transition hint syntax with linear gradient. I can implement radial gradiant by next weekend. Is that alright? if not you can pick it |
Yeah, feel free to work on it. I don't even know when I'd start with radial gradient so no rush there haha |
@jorge-cab Is anything pending on this PR. can we get this merged? I want to do some follow up PRs (px and transition hint support) that might be based on this one. Thanks 🙏 btw i have started doing some analysis for radial gradients. Currently it's a bit challenging with CSS spec as both iOS and Android's APIs lack ellipse shape support, circles are easy. I am still looking if there's a way. |
) { | ||
const parsedDirection = getDirectionString(bgImage.direction); | ||
if (parsedDirection != null) { | ||
orientation = { |
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.
nit: if the single keyword variants are directlty convertible to angles, that might make sense to simplify during parsing
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 thought about this too but it seems keywords like to top left
(which means the angle should align with the view's corner) need information about the view which we can't do during parsing.
We could however preemptively parse to top
, to left
etc since those map to constant angle values. However naively I feel like we should avoid parsing in such a fragmented way
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 also had same thoughts as @jorge-cab about parsing in fragmented way but i am down to re-using angles and passing only corner styles, will make the change, lmk if sounds good to you @jorge-cab.
i was also thinking in the longer term that dimension-dependent styles like these could be part of layout nodes right before setting native view dimensions, lot of native styling logic could be moved there and code can be reused among platforms. e.g transform-origin, translate with %, gradients, also support for calc function etc. Is there an ongoing effort in that direction? i can also take a look, i know @NickGerleman is working on native CSS parser, but i haven't dug very deep into it yet.
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.
Updated here, loving it. It made it more concise and reusable.
if (parsedDirection != null) { | ||
orientation = { | ||
type: 'direction', | ||
value: parsedDirection, |
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.
Apart from those that can be simplified to angles, we have four corners this could point to, so it might be simpler to pass one of four well known values to native for these.
@@ -17,22 +17,37 @@ const processColor = require('./processColor').default; | |||
const DIRECTION_REGEX = |
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.
nit: I think this should also be case insensitive, since CSS keywords generally are
|
||
type LinearGradientOrientation = | ||
| {type: 'angle', value: number} | ||
| {type: 'direction', value: string}; |
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.
nit: consider keyword
or keywords
instead of direction, to map to spec, since angle is considered a direction in verbiage
[colors addObject:(id)color.CGColor]; | ||
[locations addObject:location]; | ||
// iterate in reverse to match CSS specification | ||
for (auto it = _props->backgroundImage.rbegin(); it != _props->backgroundImage.rend(); ++it) { |
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.
nit: I think we require min XCode which supports std::ranges::reverse_view
as well,
) { | ||
private sealed class Orientation { | ||
public data class Angle(val value: Double) : Orientation() | ||
public data class Direction(val value: String) : Orientation() |
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.
Any way we could parse this into one of four possible unique values as enum, instead of keeping string around?
|
||
backgroundImage.push_back(gradientValue); | ||
backgroundImage.push_back(BackgroundImage{BackgroundImageType::LinearGradient, linearGradient}); |
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.
backgroundImage.push_back(BackgroundImage{BackgroundImageType::LinearGradient, linearGradient}); | |
backgroundImage.push_back(BackgroundImage{BackgroundImageType::LinearGradient, std::move(linearGradient)}); |
bool operator==(const BackgroundImage& other) const { | ||
return type == other.type && value == other.value; | ||
} |
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.
Here and below, does not seem like we need to provide explicit equality operator
bool operator==(const BackgroundImage& other) const { | |
return type == other.type && value == other.value; | |
} | |
bool operator==(const BackgroundImage& other) const = default; |
struct ColorStop { | ||
bool operator==(const ColorStop& other) const = default; | ||
SharedColor color; | ||
Float position; |
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.
nit: provide explicit initial value for primitives
|
||
struct GradientOrientation { | ||
GradientOrientationType type; | ||
std::variant<Float, std::string> value; |
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.
Can we parse the Direction
/Keyword
version into enum as early as possible instead of keeping string around?
Left a whole bunch of comments, but I’m also about to be on PTO for two weeks, and want to make sure I don’t block anything while I’m gone 😅. I left a lot of nits/mechanical pieces that aren’t super critical, but the high level points I’m curious about are:
@intergalacticspacehighway to give you some more context on delays after import, imported PRs need approver outside of engineer who imports. Sometimes an imported PR has been reviewed thoroughly by owner/expert before importing, sometimes it has gone through some initial pass and needs more eyes, and other times it hasn’t been reviewed at all. This means that this review internally usually carries a similar weight as the external one. Larger changes usually spend longer on the review queue, especially those that touch multiple platforms (where say, someone with Android expertise might not feel comfortable signing off on a change with major iOS components). This is tricky, because a long latency from PR to commit encourages grouping more changes together. In general though, small/scoped changes usually get through the pipeline quicker though, where there are opportunities to split things up. Some of the tooling in Metas monorepo encouraging breaking up changes into smaller reviewable “stacked” commits, which also influences some of the culture of small commits. I’ve heard some folks on the React team end up using “ghstack” to make this pattern easier against GitHub, but haven’t used it myself to vouch for it. |
@NickGerleman have a good time on your PTO 😄. Thanks for reviewing the PR and explaining the process in depth. I pushed almost all the changes apart from these two:
|
Summary:
CAGradientLayer
which does not support spec compliant start and end points i.e. start and end point can be outside of rectangle bounds. This leads to inconsistent gradients on iOS for corner angles compared to web and android. So this PR replaces it withCGGradient
.Changelog:
[GENERAL] [FIXED] - Linear gradient start and end point algorithm.
Test Plan:
Aside
Took a while to understand the spec, but felt great after getting it. Gradients should be 100% identical on all platforms now. Sorry i missed testing cornered angles + rectangles earlier and I found out it is inconsistent on platforms just this weekend 😅