Skip to content

Commit 2f3b104

Browse files
- Use CAGradientLayer for linear gradient (#52096)
Summary: This PR replaces Core Graphics implementation with Core Animation for linear gradients. I came across a great [solution](https://stackoverflow.com/questions/38821631/cagradientlayer-diagonal-gradient/43176174#43176174) that makes the `CAGradientLayer`'s start and end point behaviour CSS spec compliant. This will make gradients much more performant. ## Changelog: [IOS] [CHANGED] - Optimised Linear Gradients. <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #52096 Test Plan: Non breaking change. Test Linear gradient example from RNTester. Compare results with web, android and iOS. Each platform should render the gradients identically. ## Note: I will be doing a PR to use `CAGradientLayer` for radial gradients as well. The next properties that I have locally working are `background-size`, `background-position` and `background-repeat`. These will be addressed in small PRs. Reviewed By: NickGerleman Differential Revision: D76905215 Pulled By: javache fbshipit-source-id: 0094bdf70869d619272d491dd496983316b0dbf0
1 parent 6a3116a commit 2f3b104

6 files changed

Lines changed: 253 additions & 55 deletions

File tree

packages/react-native/React/Fabric/Utils/RCTGradientUtils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ NS_ASSUME_NONNULL_BEGIN
1616
+ (std::vector<facebook::react::ProcessedColorStop>)getFixedColorStops:
1717
(const std::vector<facebook::react::ColorStop> &)colorStops
1818
gradientLineLength:(CGFloat)gradientLineLength;
19+
// CAGradientLayer linear gradient squishes the non-square gradient to square gradient.
20+
// This function fixes the "squished" effect.
21+
// See https://stackoverflow.com/a/43176174 for more information.
22+
+ (std::pair<CGPoint, CGPoint>)pointsForCAGradientLayerLinearGradient:(CGPoint)startPoint
23+
endPoint:(CGPoint)endPoint
24+
bounds:(CGSize)bounds;
1925
@end
2026

2127
NS_ASSUME_NONNULL_END

packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm

Lines changed: 179 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,140 @@
99
#import <React/RCTAnimationUtils.h>
1010
#import <React/RCTConversions.h>
1111
#import <react/utils/FloatComparison.h>
12+
#include <optional>
1213
#import <vector>
1314

1415
using namespace facebook::react;
1516

17+
namespace {
18+
19+
struct Line;
20+
21+
struct LineSegment {
22+
CGPoint p1;
23+
CGPoint p2;
24+
25+
LineSegment(CGPoint p1, CGPoint p2) : p1(p1), p2(p2) {}
26+
27+
LineSegment(CGPoint p1, CGFloat m, CGFloat distance);
28+
29+
CGFloat getLength() const
30+
{
31+
CGFloat dx = p2.x - p1.x;
32+
CGFloat dy = p2.y - p1.y;
33+
return sqrt(dx * dx + dy * dy);
34+
}
35+
36+
CGFloat getDistance() const
37+
{
38+
return p1.x <= p2.x ? getLength() : -getLength();
39+
}
40+
41+
CGPoint getMidpoint() const
42+
{
43+
return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
44+
}
45+
46+
CGFloat getSlope() const
47+
{
48+
CGFloat dx = p2.x - p1.x;
49+
if (floatEquality(dx, 0.0)) {
50+
return std::numeric_limits<CGFloat>::infinity();
51+
}
52+
return (p2.y - p1.y) / dx;
53+
}
54+
55+
CGFloat getPerpendicularSlope() const
56+
{
57+
CGFloat slope = getSlope();
58+
if (std::isinf(slope)) {
59+
return 0.0;
60+
}
61+
if (floatEquality(slope, 0.0)) {
62+
return -std::numeric_limits<CGFloat>::infinity();
63+
}
64+
return -1 / slope;
65+
}
66+
67+
Line toLine() const;
68+
69+
LineSegment perpendicularBisector() const
70+
{
71+
CGPoint midpoint = getMidpoint();
72+
CGFloat perpSlope = getPerpendicularSlope();
73+
CGFloat dist = getDistance();
74+
return {LineSegment(midpoint, perpSlope, -dist / 2).p2, LineSegment(midpoint, perpSlope, dist / 2).p2};
75+
}
76+
77+
LineSegment multiplied(CGSize multipliers) const
78+
{
79+
return {
80+
CGPointMake(p1.x * multipliers.width, p1.y * multipliers.height),
81+
CGPointMake(p2.x * multipliers.width, p2.y * multipliers.height)};
82+
}
83+
84+
LineSegment divided(CGSize divisors) const
85+
{
86+
return multiplied(CGSizeMake(1 / divisors.width, 1 / divisors.height));
87+
}
88+
};
89+
90+
struct Line {
91+
CGFloat m;
92+
CGFloat b;
93+
94+
Line(CGFloat m, CGFloat b) : m(m), b(b) {}
95+
Line(CGFloat m, CGPoint p) : m(m), b(p.y - m * p.x) {}
96+
Line(CGPoint p1, CGPoint p2) : m(LineSegment(p1, p2).getSlope()), b(p1.y - m * p1.x) {}
97+
98+
CGFloat y(CGFloat x) const
99+
{
100+
return m * x + b;
101+
}
102+
103+
CGPoint point(CGFloat x) const
104+
{
105+
return CGPointMake(x, y(x));
106+
}
107+
108+
std::optional<CGPoint> intersection(const Line &other) const
109+
{
110+
CGFloat n = other.m;
111+
CGFloat c = other.b;
112+
if (floatEquality(m, n)) {
113+
return std::nullopt;
114+
}
115+
CGFloat x = (c - b) / (m - n);
116+
return point(x);
117+
}
118+
};
119+
120+
LineSegment::LineSegment(CGPoint p1, CGFloat m, CGFloat distance) : p1(p1)
121+
{
122+
Line line(m, p1);
123+
CGPoint measuringPoint = line.point(p1.x + 1);
124+
LineSegment measuringSegment(p1, measuringPoint);
125+
CGFloat measuringDeltaH = measuringSegment.getDistance();
126+
CGFloat deltaX = !floatEquality(measuringDeltaH, 0.0) ? distance / measuringDeltaH : 0.0;
127+
p2 = line.point(p1.x + deltaX);
128+
}
129+
130+
Line LineSegment::toLine() const
131+
{
132+
return {p1, p2};
133+
}
134+
135+
CGSize calculateMultipliers(CGSize bounds)
136+
{
137+
if (bounds.height <= bounds.width) {
138+
return CGSizeMake(1, bounds.width / bounds.height);
139+
} else {
140+
return CGSizeMake(bounds.height / bounds.width, 1);
141+
}
142+
}
143+
144+
} // namespace
145+
16146
static std::optional<Float> resolveColorStopPosition(ValueUnit position, CGFloat gradientLineLength)
17147
{
18148
if (position.unit == UnitType::Point) {
@@ -52,21 +182,21 @@
52182
auto leftDist = offset - offsetLeft;
53183
auto rightDist = offsetRight - offset;
54184
auto totalDist = offsetRight - offsetLeft;
55-
SharedColor leftSharedColor = colorStops[x - 1].color;
56-
SharedColor rightSharedColor = colorStops[x + 1].color;
185+
const SharedColor &leftSharedColor = colorStops[x - 1].color;
186+
const SharedColor &rightSharedColor = colorStops[x + 1].color;
57187

58-
if (facebook::react::floatEquality(leftDist, rightDist)) {
188+
if (floatEquality(leftDist, rightDist)) {
59189
colorStops.erase(colorStops.begin() + x);
60190
--indexOffset;
61191
continue;
62192
}
63193

64-
if (facebook::react::floatEquality(leftDist, .0f)) {
194+
if (floatEquality(leftDist, 0.0)) {
65195
colorStops[x].color = rightSharedColor;
66196
continue;
67197
}
68198

69-
if (facebook::react::floatEquality(rightDist, .0f)) {
199+
if (floatEquality(rightDist, 0.0)) {
70200
colorStops[x].color = leftSharedColor;
71201
continue;
72202
}
@@ -120,7 +250,7 @@
120250
auto green = (interpolatedColor >> 8) & 0xFF;
121251
auto blue = interpolatedColor & 0xFF;
122252

123-
newStop.color = facebook::react::colorFromRGBA(red, green, blue, alpha);
253+
newStop.color = colorFromRGBA(red, green, blue, alpha);
124254
}
125255

126256
// Replace the color hint with new color stops
@@ -201,4 +331,47 @@ @implementation RCTGradientUtils
201331
}
202332
return processColorTransitionHints(fixedColorStops);
203333
}
334+
335+
// CAGradientLayer linear gradient squishes the non-square gradient to square gradient.
336+
// This function fixes the "squished" effect.
337+
// See https://stackoverflow.com/a/43176174 for more information.
338+
+ (std::pair<CGPoint, CGPoint>)pointsForCAGradientLayerLinearGradient:(CGPoint)startPoint
339+
endPoint:(CGPoint)endPoint
340+
bounds:(CGSize)bounds
341+
{
342+
if (floatEquality(startPoint.x, endPoint.x) || floatEquality(startPoint.y, endPoint.y)) {
343+
// Apple's implementation of horizontal and vertical gradients works just fine
344+
return {startPoint, endPoint};
345+
}
346+
347+
LineSegment startEnd(startPoint, endPoint);
348+
LineSegment ab = startEnd.multiplied({bounds.width, bounds.height});
349+
const CGPoint a = ab.p1;
350+
const CGPoint b = ab.p2;
351+
352+
LineSegment cd = ab.perpendicularBisector();
353+
354+
CGSize multipliers = calculateMultipliers(bounds);
355+
LineSegment lineSegmentCD = cd.multiplied(multipliers);
356+
357+
LineSegment lineSegmentEF = lineSegmentCD.perpendicularBisector();
358+
359+
LineSegment ef = lineSegmentEF.divided(multipliers);
360+
361+
Line efLine = ef.toLine();
362+
363+
Line aParallelLine(cd.getSlope(), a);
364+
Line bParallelLine(cd.getSlope(), b);
365+
366+
std::optional<CGPoint> g_opt = efLine.intersection(aParallelLine);
367+
std::optional<CGPoint> h_opt = efLine.intersection(bParallelLine);
368+
369+
if (g_opt && h_opt) {
370+
LineSegment gh(*g_opt, *h_opt);
371+
LineSegment result = gh.divided({bounds.width, bounds.height});
372+
return {result.p1, result.p2};
373+
}
374+
375+
return {startPoint, endPoint};
376+
}
204377
@end

packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,54 +19,52 @@ @implementation RCTLinearGradient
1919

2020
+ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient &)gradient
2121
{
22-
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size];
2322
const auto &direction = gradient.direction;
24-
UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) {
25-
CGPoint startPoint;
26-
CGPoint endPoint;
27-
28-
if (direction.type == GradientDirectionType::Angle) {
29-
CGFloat angle = std::get<Float>(direction.value);
30-
std::tie(startPoint, endPoint) = getPointsFromAngle(angle, size);
31-
} else if (direction.type == GradientDirectionType::Keyword) {
32-
auto keyword = std::get<GradientKeyword>(direction.value);
33-
CGFloat angle = getAngleForKeyword(keyword, size);
34-
std::tie(startPoint, endPoint) = getPointsFromAngle(angle, size);
35-
} else {
36-
// Default to top-to-bottom gradient
37-
startPoint = CGPointMake(0.0, 0.0);
38-
endPoint = CGPointMake(0.0, size.height);
39-
}
40-
41-
CGFloat dx = endPoint.x - startPoint.x;
42-
CGFloat dy = endPoint.y - startPoint.y;
43-
CGFloat gradientLineLength = sqrt(dx * dx + dy * dy);
44-
const auto colorStops = [RCTGradientUtils getFixedColorStops:gradient.colorStops
45-
gradientLineLength:gradientLineLength];
46-
47-
CGContextRef context = rendererContext.CGContext;
48-
NSMutableArray *colors = [NSMutableArray array];
49-
CGFloat locations[colorStops.size()];
50-
51-
for (size_t i = 0; i < colorStops.size(); ++i) {
52-
const auto &colorStop = colorStops[i];
53-
CGColorRef cgColor = RCTCreateCGColorRefFromSharedColor(colorStop.color);
54-
[colors addObject:(__bridge id)cgColor];
55-
locations[i] = std::max(std::min(colorStop.position.value(), 1.0), 0.0);
56-
}
57-
58-
CGGradientRef cgGradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
59-
60-
CGContextDrawLinearGradient(context, cgGradient, startPoint, endPoint, 0);
23+
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
24+
CGPoint startPoint;
25+
CGPoint endPoint;
26+
27+
if (direction.type == GradientDirectionType::Angle) {
28+
CGFloat angle = std::get<Float>(direction.value);
29+
std::tie(startPoint, endPoint) = getPointsFromAngle(angle, size);
30+
} else if (direction.type == GradientDirectionType::Keyword) {
31+
auto keyword = std::get<GradientKeyword>(direction.value);
32+
CGFloat angle = getAngleForKeyword(keyword, size);
33+
std::tie(startPoint, endPoint) = getPointsFromAngle(angle, size);
34+
} else {
35+
// Default to top-to-bottom gradient
36+
CGFloat centerX = size.width / 2;
37+
startPoint = CGPointMake(centerX, 0.0);
38+
endPoint = CGPointMake(centerX, size.height);
39+
}
6140

62-
for (id color in colors) {
63-
CGColorRelease((__bridge CGColorRef)color);
64-
}
65-
CGGradientRelease(cgGradient);
66-
}];
41+
CGFloat dx = endPoint.x - startPoint.x;
42+
CGFloat dy = endPoint.y - startPoint.y;
43+
CGFloat gradientLineLength = sqrt(dx * dx + dy * dy);
44+
const auto colorStops = [RCTGradientUtils getFixedColorStops:gradient.colorStops
45+
gradientLineLength:gradientLineLength];
46+
NSMutableArray<id> *colors = [NSMutableArray array];
47+
NSMutableArray<NSNumber *> *locations = [NSMutableArray array];
48+
CGPoint relativeStartPoint = CGPointMake(startPoint.x / size.width, startPoint.y / size.height);
49+
CGPoint relativeEndPoint = CGPointMake(endPoint.x / size.width, endPoint.y / size.height);
50+
51+
CGPoint fixedStartPoint;
52+
CGPoint fixedEndPoint;
53+
54+
std::tie(fixedStartPoint, fixedEndPoint) = [RCTGradientUtils pointsForCAGradientLayerLinearGradient:relativeStartPoint
55+
endPoint:relativeEndPoint
56+
bounds:size];
57+
58+
gradientLayer.startPoint = fixedStartPoint;
59+
gradientLayer.endPoint = fixedEndPoint;
60+
61+
for (const auto &colorStop : colorStops) {
62+
[colors addObject:(id)RCTUIColorFromSharedColor(colorStop.color).CGColor];
63+
[locations addObject:@(std::max(std::min(colorStop.position.value(), 1.0), 0.0))];
64+
}
6765

68-
CALayer *gradientLayer = [CALayer layer];
69-
gradientLayer.contents = (__bridge id)gradientImage.CGImage;
66+
gradientLayer.colors = colors;
67+
gradientLayer.locations = locations;
7068

7169
return gradientLayer;
7270
}

packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,11 @@ Transform Transform::Interpolate(
302302
}
303303

304304
bool Transform::isVerticalInversion(const Transform& transform) {
305-
return facebook::react::floatEquality(transform.at(1, 1), -1.0f);
305+
return floatEquality(transform.at(1, 1), static_cast<Float>(-1.0f));
306306
}
307307

308308
bool Transform::isHorizontalInversion(const Transform& transform) {
309-
return facebook::react::floatEquality(transform.at(0, 0), -1.0f);
309+
return floatEquality(transform.at(0, 0), static_cast<Float>(-1.0f));
310310
}
311311

312312
bool Transform::operator==(const Transform& rhs) const {

packages/react-native/ReactCommon/react/utils/FloatComparison.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77

88
#pragma once
99

10+
#include <cmath>
11+
1012
namespace facebook::react {
1113

1214
constexpr float kDefaultEpsilon = 0.005f;
1315

14-
inline bool floatEquality(float a, float b, float epsilon = kDefaultEpsilon) {
16+
template <typename T>
17+
inline bool
18+
floatEquality(T a, T b, T epsilon = static_cast<T>(kDefaultEpsilon)) {
1519
return (std::isnan(a) && std::isnan(b)) ||
16-
(!std::isnan(a) && !std::isnan(b) && fabs(a - b) < epsilon);
20+
(!std::isnan(a) && !std::isnan(b) && std::abs(a - b) < epsilon);
1721
}
1822

1923
} // namespace facebook::react

packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,21 @@ exports.examples = [
273273
);
274274
},
275275
},
276+
{
277+
title: 'Non-square multiple color stops',
278+
name: 'non-square-multiple-color-stops',
279+
render(): React.Node {
280+
return (
281+
<GradientBox
282+
testID="linear-gradient-non-square-multiple-color-stops"
283+
style={{
284+
experimental_backgroundImage:
285+
'linear-gradient(45deg, black 9%, red 20%, blue 30%, green 50%, black 90%, transparent)',
286+
width: 100,
287+
height: 200,
288+
}}
289+
/>
290+
);
291+
},
292+
},
276293
];

0 commit comments

Comments
 (0)