Skip to content

Commit f125d56

Browse files
ios radial gradiant with object syntax
1 parent 1c45a66 commit f125d56

File tree

8 files changed

+650
-200
lines changed

8 files changed

+650
-200
lines changed

packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,4 +935,25 @@ describe('processBackgroundImage', () => {
935935
y: 100,
936936
});
937937
});
938+
939+
it('should process default values for radial gradient object syntax', () => {
940+
const input = [
941+
{
942+
type: 'radialGradient',
943+
colorStops: [
944+
{color: 'red', positions: null},
945+
{color: 'blue', positions: null},
946+
],
947+
},
948+
];
949+
const result = processBackgroundImage(input);
950+
expect(result[0].colorStops).toEqual([
951+
{color: processColor('red'), position: null},
952+
{color: processColor('blue'), position: null},
953+
]);
954+
expect(result[0].position).toEqual({x: '50%', y: '50%'});
955+
expect(result[0].size).toEqual('farthest-corner');
956+
expect(result[0].shape).toEqual('ellipse');
957+
expect(result[0].type).toEqual('radialGradient');
958+
});
938959
});

packages/react-native/Libraries/StyleSheet/processBackgroundImage.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,13 @@ export default function processBackgroundImage(
141141
let size: RadialGradientSize = DEFAULT_RADIAL_SIZE;
142142
let position: RadialGradientPosition = DEFAULT_RADIAL_POSITION;
143143

144-
if (
145-
bgImage.shape != null &&
146-
(bgImage.shape === 'circle' || bgImage.shape === 'ellipse')
147-
) {
148-
shape = bgImage.shape;
149-
} else {
150-
// If the shape is invalid, return an empty array and do not apply any gradient. Same as web.
151-
return [];
144+
if (bgImage.shape != null) {
145+
if (bgImage.shape === 'circle' || bgImage.shape === 'ellipse') {
146+
shape = bgImage.shape;
147+
} else {
148+
// If the shape is invalid, return an empty array and do not apply any gradient. Same as web.
149+
return [];
150+
}
152151
}
153152

154153
if (bgImage.size != null) {

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#import <React/RCTBoxShadow.h>
1818
#import <React/RCTConversions.h>
1919
#import <React/RCTLinearGradient.h>
20+
#import <React/RCTRadialGradient.h>
2021
#import <React/RCTLocalizedString.h>
2122
#import <React/RCTViewFinder.h>
2223
#import <react/featureflags/ReactNativeFeatureFlags.h>
@@ -1026,6 +1027,15 @@ - (void)invalidateLayer
10261027
backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
10271028
[self.layer addSublayer:backgroundImageLayer];
10281029
[_backgroundImageLayers addObject:backgroundImageLayer];
1030+
} else if (std::holds_alternative<RadialGradient>(backgroundImage)) {
1031+
const auto &radialGradient = std::get<RadialGradient>(backgroundImage);
1032+
CALayer *backgroundImageLayer = [RCTRadialGradient gradientLayerWithSize:self.layer.bounds.size
1033+
gradient:radialGradient];
1034+
[self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics];
1035+
backgroundImageLayer.masksToBounds = YES;
1036+
backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
1037+
[self.layer addSublayer:backgroundImageLayer];
1038+
[_backgroundImageLayers addObject:backgroundImageLayer];
10291039
}
10301040
}
10311041
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <react/renderer/graphics/ColorStop.h>
9+
#import <vector>
10+
11+
using namespace facebook::react;
12+
13+
NS_ASSUME_NONNULL_BEGIN
14+
15+
@interface RCTGradientUtils : NSObject
16+
17+
+ (std::vector<ProcessedColorStop>)getFixedColorStops:(const std::vector<ColorStop> &)colorStops gradientLineLength:(CGFloat)gradientLineLength;
18+
+ (std::vector<ProcessedColorStop>)processColorTransitionHints:(const std::vector<ProcessedColorStop> &)originalStops;
19+
@end
20+
21+
NS_ASSUME_NONNULL_END
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTGradientUtils.h"
9+
#import <vector>
10+
#import <React/RCTConversions.h>
11+
#import <react/utils/FloatComparison.h>
12+
#import <React/RCTAnimationUtils.h>
13+
14+
using namespace facebook::react;
15+
16+
static std::optional<Float> resolveColorStopPosition(ValueUnit position, CGFloat gradientLineLength)
17+
{
18+
if (position.unit == UnitType::Point) {
19+
return position.resolve(0.0f) / gradientLineLength;
20+
}
21+
22+
if (position.unit == UnitType::Percent) {
23+
return position.resolve(1.0f);
24+
}
25+
26+
return std::nullopt;
27+
}
28+
29+
@implementation RCTGradientUtils
30+
// https://drafts.csswg.org/css-images-4/#color-stop-fixup
31+
+ (std::vector<ProcessedColorStop>)getFixedColorStops:(const std::vector<ColorStop> &)colorStops gradientLineLength:(CGFloat)gradientLineLength
32+
{
33+
std::vector<ProcessedColorStop> fixedColorStops(colorStops.size());
34+
bool hasNullPositions = false;
35+
auto maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength);
36+
if (!maxPositionSoFar.has_value()) {
37+
maxPositionSoFar = 0.0f;
38+
}
39+
40+
for (size_t i = 0; i < colorStops.size(); i++) {
41+
const auto &colorStop = colorStops[i];
42+
auto newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength);
43+
44+
if (!newPosition.has_value()) {
45+
// Step 1:
46+
// If the first color stop does not have a position,
47+
// set its position to 0%. If the last color stop does not have a position,
48+
// set its position to 100%.
49+
if (i == 0) {
50+
newPosition = 0.0f;
51+
} else if (i == colorStops.size() - 1) {
52+
newPosition = 1.0f;
53+
}
54+
}
55+
56+
// Step 2:
57+
// If a color stop or transition hint has a position
58+
// that is less than the specified position of any color stop or transition hint
59+
// before it in the list, set its position to be equal to the
60+
// largest specified position of any color stop or transition hint before it.
61+
if (newPosition.has_value()) {
62+
newPosition = std::max(newPosition.value(), maxPositionSoFar.value());
63+
fixedColorStops[i] = ProcessedColorStop{colorStop.color, newPosition};
64+
maxPositionSoFar = newPosition;
65+
} else {
66+
hasNullPositions = true;
67+
}
68+
}
69+
70+
// Step 3:
71+
// If any color stop still does not have a position,
72+
// then, for each run of adjacent color stops without positions,
73+
// set their positions so that they are evenly spaced between the preceding and
74+
// following color stops with positions.
75+
if (hasNullPositions) {
76+
size_t lastDefinedIndex = 0;
77+
for (size_t i = 1; i < fixedColorStops.size(); i++) {
78+
auto endPosition = fixedColorStops[i].position;
79+
if (endPosition.has_value()) {
80+
size_t unpositionedStops = i - lastDefinedIndex - 1;
81+
if (unpositionedStops > 0) {
82+
auto startPosition = fixedColorStops[lastDefinedIndex].position;
83+
if (startPosition.has_value()) {
84+
auto increment = (endPosition.value() - startPosition.value()) / (unpositionedStops + 1);
85+
for (size_t j = 1; j <= unpositionedStops; j++) {
86+
fixedColorStops[lastDefinedIndex + j] =
87+
ProcessedColorStop{colorStops[lastDefinedIndex + j].color, startPosition.value() + increment * j};
88+
}
89+
}
90+
}
91+
lastDefinedIndex = i;
92+
}
93+
}
94+
}
95+
96+
return fixedColorStops;
97+
}
98+
99+
100+
// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section)
101+
// Browsers add 9 intermediate color stops when a transition hint is present
102+
// Algorithm is referred from Blink engine
103+
// [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240).
104+
+ (std::vector<ProcessedColorStop>)processColorTransitionHints:(const std::vector<ProcessedColorStop> &)originalStops
105+
{
106+
auto colorStops = std::vector<ProcessedColorStop>(originalStops);
107+
int indexOffset = 0;
108+
109+
for (size_t i = 1; i < originalStops.size() - 1; ++i) {
110+
// Skip if not a color hint
111+
if (originalStops[i].color) {
112+
continue;
113+
}
114+
115+
size_t x = i + indexOffset;
116+
if (x < 1) {
117+
continue;
118+
}
119+
120+
auto offsetLeft = colorStops[x - 1].position.value();
121+
auto offsetRight = colorStops[x + 1].position.value();
122+
auto offset = colorStops[x].position.value();
123+
auto leftDist = offset - offsetLeft;
124+
auto rightDist = offsetRight - offset;
125+
auto totalDist = offsetRight - offsetLeft;
126+
SharedColor leftSharedColor = colorStops[x - 1].color;
127+
SharedColor rightSharedColor = colorStops[x + 1].color;
128+
129+
if (facebook::react::floatEquality(leftDist, rightDist)) {
130+
colorStops.erase(colorStops.begin() + x);
131+
--indexOffset;
132+
continue;
133+
}
134+
135+
if (facebook::react::floatEquality(leftDist, .0f)) {
136+
colorStops[x].color = rightSharedColor;
137+
continue;
138+
}
139+
140+
if (facebook::react::floatEquality(rightDist, .0f)) {
141+
colorStops[x].color = leftSharedColor;
142+
continue;
143+
}
144+
145+
std::vector<ProcessedColorStop> newStops;
146+
newStops.reserve(9);
147+
148+
// Position the new color stops
149+
if (leftDist > rightDist) {
150+
for (int y = 0; y < 7; ++y) {
151+
ProcessedColorStop newStop{SharedColor(), offsetLeft + leftDist * ((7.0f + y) / 13.0f)};
152+
newStops.push_back(newStop);
153+
}
154+
ProcessedColorStop stop1{SharedColor(), offset + rightDist * (1.0f / 3.0f)};
155+
ProcessedColorStop stop2{SharedColor(), offset + rightDist * (2.0f / 3.0f)};
156+
newStops.push_back(stop1);
157+
newStops.push_back(stop2);
158+
} else {
159+
ProcessedColorStop stop1{SharedColor(), offsetLeft + leftDist * (1.0f / 3.0f)};
160+
ProcessedColorStop stop2{SharedColor(), offsetLeft + leftDist * (2.0f / 3.0f)};
161+
newStops.push_back(stop1);
162+
newStops.push_back(stop2);
163+
for (int y = 0; y < 7; ++y) {
164+
ProcessedColorStop newStop{SharedColor(), offset + rightDist * (y / 13.0f)};
165+
newStops.push_back(newStop);
166+
}
167+
}
168+
169+
// calculate colors for the new color hints.
170+
// The color weighting for the new color stops will be
171+
// pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)).
172+
auto hintRelativeOffset = leftDist / totalDist;
173+
const auto logRatio = log(0.5) / log(hintRelativeOffset);
174+
auto leftColor = RCTUIColorFromSharedColor(leftSharedColor);
175+
auto rightColor = RCTUIColorFromSharedColor(rightSharedColor);
176+
NSArray<NSNumber *> *inputRange = @[ @0.0, @1.0 ];
177+
NSArray<UIColor *> *outputRange = @[ leftColor, rightColor ];
178+
179+
for (auto &newStop : newStops) {
180+
auto pointRelativeOffset = (newStop.position.value() - offsetLeft) / totalDist;
181+
auto weighting = pow(pointRelativeOffset, logRatio);
182+
183+
if (!std::isfinite(weighting) || std::isnan(weighting)) {
184+
continue;
185+
}
186+
187+
auto interpolatedColor = RCTInterpolateColorInRange(weighting, inputRange, outputRange);
188+
189+
auto alpha = (interpolatedColor >> 24) & 0xFF;
190+
auto red = (interpolatedColor >> 16) & 0xFF;
191+
auto green = (interpolatedColor >> 8) & 0xFF;
192+
auto blue = interpolatedColor & 0xFF;
193+
194+
newStop.color = facebook::react::colorFromRGBA(red, green, blue, alpha);
195+
}
196+
197+
// Replace the color hint with new color stops
198+
colorStops.erase(colorStops.begin() + x);
199+
colorStops.insert(colorStops.begin() + x, newStops.begin(), newStops.end());
200+
indexOffset += 8;
201+
}
202+
203+
return colorStops;
204+
}
205+
206+
@end
207+

0 commit comments

Comments
 (0)