From e57fde294f1a244fe29d62468a810df020afc659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Wed, 26 Mar 2025 02:58:01 +0530 Subject: [PATCH 1/3] radial gradient iOS changes --- .../View/RCTViewComponentView.mm | 10 + .../React/Fabric/Utils/RCTGradientUtils.h | 18 ++ .../React/Fabric/Utils/RCTGradientUtils.mm | 206 +++++++++++++++++ .../React/Fabric/Utils/RCTLinearGradient.mm | 196 +--------------- .../React/Fabric/Utils/RCTRadialGradient.h | 19 ++ .../React/Fabric/Utils/RCTRadialGradient.mm | 213 ++++++++++++++++++ .../renderer/components/view/conversions.h | 136 ++++++++--- .../react/renderer/graphics/BackgroundImage.h | 3 +- .../react/renderer/graphics/ColorStop.h | 28 +++ .../react/renderer/graphics/LinearGradient.h | 13 +- .../react/renderer/graphics/RadialGradient.h | 96 ++++++++ .../rn-tester/js/utils/RNTesterList.ios.js | 8 +- 12 files changed, 706 insertions(+), 240 deletions(-) create mode 100644 packages/react-native/React/Fabric/Utils/RCTGradientUtils.h create mode 100644 packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm create mode 100644 packages/react-native/React/Fabric/Utils/RCTRadialGradient.h create mode 100644 packages/react-native/React/Fabric/Utils/RCTRadialGradient.mm create mode 100644 packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h create mode 100644 packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 2679c8695f62e4..d0e86f3b3b33d9 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -17,6 +17,7 @@ #import #import #import +#import #import #import #import @@ -1026,6 +1027,15 @@ - (void)invalidateLayer backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; [self.layer addSublayer:backgroundImageLayer]; [_backgroundImageLayers addObject:backgroundImageLayer]; + } else if (std::holds_alternative(backgroundImage)) { + const auto &radialGradient = std::get(backgroundImage); + CALayer *backgroundImageLayer = [RCTRadialGradient gradientLayerWithSize:self.layer.bounds.size + gradient:radialGradient]; + [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics]; + backgroundImageLayer.masksToBounds = YES; + backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; + [self.layer addSublayer:backgroundImageLayer]; + [_backgroundImageLayers addObject:backgroundImageLayer]; } } } diff --git a/packages/react-native/React/Fabric/Utils/RCTGradientUtils.h b/packages/react-native/React/Fabric/Utils/RCTGradientUtils.h new file mode 100644 index 00000000000000..22f87681f695f9 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTGradientUtils.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTGradientUtils : NSObject + ++ (std::vector)getFixedColorStops:(const std::vector &)colorStops gradientLineLength:(CGFloat)gradientLineLength; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm b/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm new file mode 100644 index 00000000000000..571e43652f54f7 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm @@ -0,0 +1,206 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTGradientUtils.h" +#import +#import +#import +#import + +using namespace facebook::react; + +static std::optional resolveColorStopPosition(ValueUnit position, CGFloat gradientLineLength) +{ + if (position.unit == UnitType::Point) { + return position.resolve(0.0f) / gradientLineLength; + } + + if (position.unit == UnitType::Percent) { + return position.resolve(1.0f); + } + + return std::nullopt; +} + + +// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section) +// Browsers add 9 intermediate color stops when a transition hint is present +// Algorithm is referred from Blink engine +// [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240). +static std::vector processColorTransitionHints(const std::vector& originalStops) +{ + auto colorStops = std::vector(originalStops); + int indexOffset = 0; + + for (size_t i = 1; i < originalStops.size() - 1; ++i) { + // Skip if not a color hint + if (originalStops[i].color) { + continue; + } + + size_t x = i + indexOffset; + if (x < 1) { + continue; + } + + auto offsetLeft = colorStops[x - 1].position.value(); + auto offsetRight = colorStops[x + 1].position.value(); + auto offset = colorStops[x].position.value(); + auto leftDist = offset - offsetLeft; + auto rightDist = offsetRight - offset; + auto totalDist = offsetRight - offsetLeft; + SharedColor leftSharedColor = colorStops[x - 1].color; + SharedColor rightSharedColor = colorStops[x + 1].color; + + if (facebook::react::floatEquality(leftDist, rightDist)) { + colorStops.erase(colorStops.begin() + x); + --indexOffset; + continue; + } + + if (facebook::react::floatEquality(leftDist, .0f)) { + colorStops[x].color = rightSharedColor; + continue; + } + + if (facebook::react::floatEquality(rightDist, .0f)) { + colorStops[x].color = leftSharedColor; + continue; + } + + std::vector newStops; + newStops.reserve(9); + + // Position the new color stops + if (leftDist > rightDist) { + for (int y = 0; y < 7; ++y) { + ProcessedColorStop newStop{SharedColor(), offsetLeft + leftDist * ((7.0f + y) / 13.0f)}; + newStops.push_back(newStop); + } + ProcessedColorStop stop1{SharedColor(), offset + rightDist * (1.0f / 3.0f)}; + ProcessedColorStop stop2{SharedColor(), offset + rightDist * (2.0f / 3.0f)}; + newStops.push_back(stop1); + newStops.push_back(stop2); + } else { + ProcessedColorStop stop1{SharedColor(), offsetLeft + leftDist * (1.0f / 3.0f)}; + ProcessedColorStop stop2{SharedColor(), offsetLeft + leftDist * (2.0f / 3.0f)}; + newStops.push_back(stop1); + newStops.push_back(stop2); + for (int y = 0; y < 7; ++y) { + ProcessedColorStop newStop{SharedColor(), offset + rightDist * (y / 13.0f)}; + newStops.push_back(newStop); + } + } + + // calculate colors for the new color hints. + // The color weighting for the new color stops will be + // pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)). + auto hintRelativeOffset = leftDist / totalDist; + const auto logRatio = log(0.5) / log(hintRelativeOffset); + auto leftColor = RCTUIColorFromSharedColor(leftSharedColor); + auto rightColor = RCTUIColorFromSharedColor(rightSharedColor); + NSArray *inputRange = @[ @0.0, @1.0 ]; + NSArray *outputRange = @[ leftColor, rightColor ]; + + for (auto &newStop : newStops) { + auto pointRelativeOffset = (newStop.position.value() - offsetLeft) / totalDist; + auto weighting = pow(pointRelativeOffset, logRatio); + + if (!std::isfinite(weighting) || std::isnan(weighting)) { + continue; + } + + auto interpolatedColor = RCTInterpolateColorInRange(weighting, inputRange, outputRange); + + auto alpha = (interpolatedColor >> 24) & 0xFF; + auto red = (interpolatedColor >> 16) & 0xFF; + auto green = (interpolatedColor >> 8) & 0xFF; + auto blue = interpolatedColor & 0xFF; + + newStop.color = facebook::react::colorFromRGBA(red, green, blue, alpha); + } + + // Replace the color hint with new color stops + colorStops.erase(colorStops.begin() + x); + colorStops.insert(colorStops.begin() + x, newStops.begin(), newStops.end()); + indexOffset += 8; + } + + return colorStops; +} + + +@implementation RCTGradientUtils +// https://drafts.csswg.org/css-images-4/#color-stop-fixup ++ (std::vector)getFixedColorStops:(const std::vector &)colorStops gradientLineLength:(CGFloat)gradientLineLength +{ + std::vector fixedColorStops(colorStops.size()); + bool hasNullPositions = false; + auto maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength); + if (!maxPositionSoFar.has_value()) { + maxPositionSoFar = 0.0f; + } + + for (size_t i = 0; i < colorStops.size(); i++) { + const auto &colorStop = colorStops[i]; + auto newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength); + + if (!newPosition.has_value()) { + // Step 1: + // If the first color stop does not have a position, + // set its position to 0%. If the last color stop does not have a position, + // set its position to 100%. + if (i == 0) { + newPosition = 0.0f; + } else if (i == colorStops.size() - 1) { + newPosition = 1.0f; + } + } + + // Step 2: + // If a color stop or transition hint has a position + // that is less than the specified position of any color stop or transition hint + // before it in the list, set its position to be equal to the + // largest specified position of any color stop or transition hint before it. + if (newPosition.has_value()) { + newPosition = std::max(newPosition.value(), maxPositionSoFar.value()); + fixedColorStops[i] = ProcessedColorStop{colorStop.color, newPosition}; + maxPositionSoFar = newPosition; + } else { + hasNullPositions = true; + } + } + + // Step 3: + // If any color stop still does not have a position, + // then, for each run of adjacent color stops without positions, + // set their positions so that they are evenly spaced between the preceding and + // following color stops with positions. + if (hasNullPositions) { + size_t lastDefinedIndex = 0; + for (size_t i = 1; i < fixedColorStops.size(); i++) { + auto endPosition = fixedColorStops[i].position; + if (endPosition.has_value()) { + size_t unpositionedStops = i - lastDefinedIndex - 1; + if (unpositionedStops > 0) { + auto startPosition = fixedColorStops[lastDefinedIndex].position; + if (startPosition.has_value()) { + auto increment = (endPosition.value() - startPosition.value()) / (unpositionedStops + 1); + for (size_t j = 1; j <= unpositionedStops; j++) { + fixedColorStops[lastDefinedIndex + j] = + ProcessedColorStop{colorStops[lastDefinedIndex + j].color, startPosition.value() + increment * j}; + } + } + } + lastDefinedIndex = i; + } + } + } + return processColorTransitionHints(fixedColorStops); +} +@end + diff --git a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm index c77be2b30104f6..9cafc39854dcc6 100644 --- a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm @@ -11,6 +11,7 @@ #import #include #import +#import "RCTGradientUtils.h" using namespace facebook::react; @@ -40,9 +41,8 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient & CGFloat dx = endPoint.x - startPoint.x; CGFloat dy = endPoint.y - startPoint.y; CGFloat gradientLineLength = sqrt(dx * dx + dy * dy); - const auto processedStops = getFixedColorStops(gradient.colorStops, gradientLineLength); - const auto colorStops = processColorTransitionHints(processedStops); - + const auto colorStops = [RCTGradientUtils getFixedColorStops:gradient.colorStops gradientLineLength:gradientLineLength]; + CGContextRef context = rendererContext.CGContext; NSMutableArray *colors = [NSMutableArray array]; CGFloat locations[colorStops.size()]; @@ -138,194 +138,4 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) } } -// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section) -// Browsers add 9 intermediate color stops when a transition hint is present -// Algorithm is referred from Blink engine -// [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240). -static std::vector processColorTransitionHints(const std::vector &originalStops) -{ - auto colorStops = std::vector(originalStops); - int indexOffset = 0; - - for (size_t i = 1; i < originalStops.size() - 1; ++i) { - // Skip if not a color hint - if (originalStops[i].color) { - continue; - } - - size_t x = i + indexOffset; - if (x < 1) { - continue; - } - - auto offsetLeft = colorStops[x - 1].position.value(); - auto offsetRight = colorStops[x + 1].position.value(); - auto offset = colorStops[x].position.value(); - auto leftDist = offset - offsetLeft; - auto rightDist = offsetRight - offset; - auto totalDist = offsetRight - offsetLeft; - SharedColor leftSharedColor = colorStops[x - 1].color; - SharedColor rightSharedColor = colorStops[x + 1].color; - - if (facebook::react::floatEquality(leftDist, rightDist)) { - colorStops.erase(colorStops.begin() + x); - --indexOffset; - continue; - } - - if (facebook::react::floatEquality(leftDist, .0f)) { - colorStops[x].color = rightSharedColor; - continue; - } - - if (facebook::react::floatEquality(rightDist, .0f)) { - colorStops[x].color = leftSharedColor; - continue; - } - - std::vector newStops; - newStops.reserve(9); - - // Position the new color stops - if (leftDist > rightDist) { - for (int y = 0; y < 7; ++y) { - ProcessedColorStop newStop{SharedColor(), offsetLeft + leftDist * ((7.0f + y) / 13.0f)}; - newStops.push_back(newStop); - } - ProcessedColorStop stop1{SharedColor(), offset + rightDist * (1.0f / 3.0f)}; - ProcessedColorStop stop2{SharedColor(), offset + rightDist * (2.0f / 3.0f)}; - newStops.push_back(stop1); - newStops.push_back(stop2); - } else { - ProcessedColorStop stop1{SharedColor(), offsetLeft + leftDist * (1.0f / 3.0f)}; - ProcessedColorStop stop2{SharedColor(), offsetLeft + leftDist * (2.0f / 3.0f)}; - newStops.push_back(stop1); - newStops.push_back(stop2); - for (int y = 0; y < 7; ++y) { - ProcessedColorStop newStop{SharedColor(), offset + rightDist * (y / 13.0f)}; - newStops.push_back(newStop); - } - } - - // calculate colors for the new color hints. - // The color weighting for the new color stops will be - // pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)). - auto hintRelativeOffset = leftDist / totalDist; - const auto logRatio = log(0.5) / log(hintRelativeOffset); - auto leftColor = RCTUIColorFromSharedColor(leftSharedColor); - auto rightColor = RCTUIColorFromSharedColor(rightSharedColor); - NSArray *inputRange = @[ @0.0, @1.0 ]; - NSArray *outputRange = @[ leftColor, rightColor ]; - - for (auto &newStop : newStops) { - auto pointRelativeOffset = (newStop.position.value() - offsetLeft) / totalDist; - auto weighting = pow(pointRelativeOffset, logRatio); - - if (!std::isfinite(weighting) || std::isnan(weighting)) { - continue; - } - - auto interpolatedColor = RCTInterpolateColorInRange(weighting, inputRange, outputRange); - - auto alpha = (interpolatedColor >> 24) & 0xFF; - auto red = (interpolatedColor >> 16) & 0xFF; - auto green = (interpolatedColor >> 8) & 0xFF; - auto blue = interpolatedColor & 0xFF; - - newStop.color = facebook::react::colorFromRGBA(red, green, blue, alpha); - } - - // Replace the color hint with new color stops - colorStops.erase(colorStops.begin() + x); - colorStops.insert(colorStops.begin() + x, newStops.begin(), newStops.end()); - indexOffset += 8; - } - - return colorStops; -} - -// https://drafts.csswg.org/css-images-4/#color-stop-fixup -static std::vector getFixedColorStops( - const std::vector &colorStops, - CGFloat gradientLineLength) -{ - std::vector fixedColorStops(colorStops.size()); - bool hasNullPositions = false; - auto maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength); - if (!maxPositionSoFar.has_value()) { - maxPositionSoFar = 0.0f; - } - - for (size_t i = 0; i < colorStops.size(); i++) { - const auto &colorStop = colorStops[i]; - auto newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength); - - if (!newPosition.has_value()) { - // Step 1: - // If the first color stop does not have a position, - // set its position to 0%. If the last color stop does not have a position, - // set its position to 100%. - if (i == 0) { - newPosition = 0.0f; - } else if (i == colorStops.size() - 1) { - newPosition = 1.0f; - } - } - - // Step 2: - // If a color stop or transition hint has a position - // that is less than the specified position of any color stop or transition hint - // before it in the list, set its position to be equal to the - // largest specified position of any color stop or transition hint before it. - if (newPosition.has_value()) { - newPosition = std::max(newPosition.value(), maxPositionSoFar.value()); - fixedColorStops[i] = ProcessedColorStop{colorStop.color, newPosition}; - maxPositionSoFar = newPosition; - } else { - hasNullPositions = true; - } - } - - // Step 3: - // If any color stop still does not have a position, - // then, for each run of adjacent color stops without positions, - // set their positions so that they are evenly spaced between the preceding and - // following color stops with positions. - if (hasNullPositions) { - size_t lastDefinedIndex = 0; - for (size_t i = 1; i < fixedColorStops.size(); i++) { - auto endPosition = fixedColorStops[i].position; - if (endPosition.has_value()) { - size_t unpositionedStops = i - lastDefinedIndex - 1; - if (unpositionedStops > 0) { - auto startPosition = fixedColorStops[lastDefinedIndex].position; - if (startPosition.has_value()) { - auto increment = (endPosition.value() - startPosition.value()) / (unpositionedStops + 1); - for (size_t j = 1; j <= unpositionedStops; j++) { - fixedColorStops[lastDefinedIndex + j] = - ProcessedColorStop{colorStops[lastDefinedIndex + j].color, startPosition.value() + increment * j}; - } - } - } - lastDefinedIndex = i; - } - } - } - - return fixedColorStops; -} - -static std::optional resolveColorStopPosition(ValueUnit position, CGFloat gradientLineLength) -{ - if (position.unit == UnitType::Point) { - return position.resolve(0.0f) / gradientLineLength; - } - - if (position.unit == UnitType::Percent) { - return position.resolve(1.0f); - } - - return std::nullopt; -} - @end diff --git a/packages/react-native/React/Fabric/Utils/RCTRadialGradient.h b/packages/react-native/React/Fabric/Utils/RCTRadialGradient.h new file mode 100644 index 00000000000000..769ed75c9dcc91 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTRadialGradient.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTRadialGradient : NSObject + ++ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const facebook::react::RadialGradient &)gradient; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTRadialGradient.mm b/packages/react-native/React/Fabric/Utils/RCTRadialGradient.mm new file mode 100644 index 00000000000000..aee1903a3b7bf1 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTRadialGradient.mm @@ -0,0 +1,213 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTRadialGradient.h" +#import +#import +#include +#import +#import "RCTGradientUtils.h" + +using namespace facebook::react; + +namespace { +using RadiusVector = std::pair; + +static RadiusVector RadiusToSide(CGFloat centerX, CGFloat centerY, CGFloat width, CGFloat height, + bool isCircle, RadialGradientSize::SizeKeyword size) { + CGFloat radiusXFromLeftSide = centerX; + CGFloat radiusYFromTopSide = centerY; + CGFloat radiusXFromRightSide = width - centerX; + CGFloat radiusYFromBottomSide = height - centerY; + CGFloat radiusX; + CGFloat radiusY; + + if (size == RadialGradientSize::SizeKeyword::ClosestSide) { + radiusX = std::min(radiusXFromLeftSide, radiusXFromRightSide); + radiusY = std::min(radiusYFromTopSide, radiusYFromBottomSide); + } else { + radiusX = std::max(radiusXFromLeftSide, radiusXFromRightSide); + radiusY = std::max(radiusYFromTopSide, radiusYFromBottomSide); + } + + if (isCircle) { + CGFloat radius; + if (size == RadialGradientSize::SizeKeyword::ClosestSide) { + radius = std::min(radiusX, radiusY); + } else { + radius = std::max(radiusX, radiusY); + } + return {radius, radius}; + } + + return {radiusX, radiusY}; +} + +static RadiusVector EllipseRadius(CGFloat offsetX, CGFloat offsetY, CGFloat aspectRatio) { + if (aspectRatio == 0 || std::isinf(aspectRatio) || std::isnan(aspectRatio)) { + return {0, 0}; + } + // Ellipse that passes through a point formula: (x-h)^2/a^2 + (y-k)^2/b^2 = 1 + // a = semi major axis length + // b = semi minor axis length = a / aspectRatio + // x - h = offsetX + // y - k = offsetY + CGFloat a = std::sqrt(offsetX * offsetX + offsetY * offsetY * aspectRatio * aspectRatio); + return {a, a / aspectRatio}; +} + +static RadiusVector RadiusToCorner(CGFloat centerX, CGFloat centerY, CGFloat width, CGFloat height, + bool isCircle, RadialGradientSize::SizeKeyword keyword) { + std::array corners = {{{0, 0}, {width, 0}, {width, height}, {0, height}}}; + + size_t cornerIndex = 0; + CGFloat distance = hypot(centerX - corners[cornerIndex].x, centerY - corners[cornerIndex].y); + bool isClosestCorner = keyword == RadialGradientSize::SizeKeyword::ClosestCorner; + + for (size_t i = 1; i < corners.size(); ++i) { + CGFloat newDistance = hypot(centerX - corners[i].x, centerY - corners[i].y); + if (isClosestCorner) { + if (newDistance < distance) { + distance = newDistance; + cornerIndex = i; + } + } else { + if (newDistance > distance) { + distance = newDistance; + cornerIndex = i; + } + } + } + + if (isCircle) { + return {distance, distance}; + } + + // https://www.w3.org/TR/css-images-3/#typedef-radial-size + // Aspect ratio of corner size ellipse is same as the respective side size ellipse + const RadiusVector sideRadius = RadiusToSide(centerX, centerY, width, height, false, isClosestCorner ? RadialGradientSize::SizeKeyword::ClosestSide : RadialGradientSize::SizeKeyword::FarthestSide); + return EllipseRadius(corners[cornerIndex].x - centerX, + corners[cornerIndex].y - centerY, + sideRadius.first / sideRadius.second); +} + +static RadiusVector GetRadialGradientRadius(bool isCircle, const RadialGradientSize& size, + CGFloat centerX, CGFloat centerY, + CGFloat width, CGFloat height) { + if (std::holds_alternative(size.value)) { + const auto& dimensions = std::get(size.value); + CGFloat radiusX = dimensions.x.resolve(width); + CGFloat radiusY = dimensions.y.resolve(height); + if (isCircle) { + CGFloat radius = std::max(radiusX, radiusY); + return {radius, radius}; + } + return {radiusX, radiusY}; + } + + if (std::holds_alternative(size.value)) { + const auto& keyword = std::get(size.value); + if (keyword == RadialGradientSize::SizeKeyword::ClosestSide || keyword == RadialGradientSize::SizeKeyword::FarthestSide) { + return RadiusToSide(centerX, centerY, width, height, isCircle, keyword); + } + + if (keyword == RadialGradientSize::SizeKeyword::ClosestCorner) { + return RadiusToCorner(centerX, centerY, width, height, isCircle, keyword); + } + } + + // defaults to farthest corner + return RadiusToCorner(centerX, centerY, width, height, isCircle, RadialGradientSize::SizeKeyword::FarthestCorner); +} +} // namespace + +@implementation RCTRadialGradient + ++ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const RadialGradient &)gradient +{ + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size]; + UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + + CGPoint centerPoint = CGPointMake(size.width / 2.0, size.height / 2.0); + + if (gradient.position.top) { + centerPoint.y = gradient.position.top->resolve(size.height); + } else if (gradient.position.bottom) { + centerPoint.y = size.height - gradient.position.bottom->resolve(size.height); + } + + if (gradient.position.left) { + centerPoint.x = gradient.position.left->resolve(size.width); + } else if (gradient.position.right) { + centerPoint.x = size.width - gradient.position.right->resolve(size.width); + } + + bool isCircle = (gradient.shape == RadialGradientShape::Circle); + auto [radiusX, radiusY] = GetRadialGradientRadius( + isCircle, + gradient.size, + centerPoint.x, + centerPoint.y, + size.width, + size.height + ); + + CGFloat scale = 1.0; + if (radiusX != radiusY && gradient.shape != RadialGradientShape::Circle) { + scale = radiusX / radiusY; + CGContextSaveGState(context); + // Scale the context to make the circular gradient appear elliptical + CGContextTranslateCTM(context, centerPoint.x, centerPoint.y); + CGContextScaleCTM(context, 1.0, 1.0/scale); + CGContextTranslateCTM(context, -centerPoint.x, -centerPoint.y); + radiusX = std::max(radiusX, radiusY * scale); + } + + const auto colorStops = [RCTGradientUtils getFixedColorStops:gradient.colorStops gradientLineLength:radiusX]; + + NSMutableArray *colors = [NSMutableArray array]; + CGFloat locations[colorStops.size()]; + + for (size_t i = 0; i < colorStops.size(); ++i) { + const auto &colorStop = colorStops[i]; + CGColorRef cgColor = RCTCreateCGColorRefFromSharedColor(colorStop.color); + [colors addObject:(__bridge id)cgColor]; + locations[i] = std::max(std::min(colorStop.position.value(), 1.0), 0.0); + } + + CGGradientRef cgGradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations); + + CGContextDrawRadialGradient( + context, + cgGradient, + centerPoint, + 0, + centerPoint, + radiusX, + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation + ); + + // Restore the context state if we scaled it + if (radiusX != radiusY && gradient.shape != RadialGradientShape::Circle) { + CGContextRestoreGState(context); + } + + for (id color in colors) { + CGColorRelease((__bridge CGColorRef)color); + } + CGGradientRelease(cgGradient); + }]; + + CALayer *gradientLayer = [CALayer layer]; + gradientLayer.contents = (__bridge id)gradientImage.CGImage; + + return gradientLayer; +} + + +@end diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 5bdf3a5b82b050..f62cb78323d9ae 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -1180,6 +1180,44 @@ inline void fromRawValue( } std::string type = (std::string)(typeIt->second); + std::vector colorStops; + auto colorStopsIt = rawBackgroundImageMap.find("colorStops"); + + if (colorStopsIt != rawBackgroundImageMap.end() && + colorStopsIt->second.hasType>()) { + auto rawColorStops = + static_cast>(colorStopsIt->second); + + for (const auto& stop : rawColorStops) { + if (stop.hasType>()) { + auto stopMap = + static_cast>(stop); + auto positionIt = stopMap.find("position"); + auto colorIt = stopMap.find("color"); + + if (positionIt != stopMap.end() && colorIt != stopMap.end()) { + ColorStop colorStop; + if (positionIt->second.hasValue()) { + auto valueUnit = toValueUnit(positionIt->second); + if (!valueUnit) { + result = {}; + return; + } + colorStop.position = valueUnit; + } + if (colorIt->second.hasValue()) { + fromRawValue( + context.contextContainer, + context.surfaceId, + colorIt->second, + colorStop.color); + } + colorStops.push_back(colorStop); + } + } + } + } + if (type == "linearGradient") { LinearGradient linearGradient; @@ -1213,43 +1251,75 @@ inline void fromRawValue( } } - auto colorStopsIt = rawBackgroundImageMap.find("colorStops"); - if (colorStopsIt != rawBackgroundImageMap.end() && - colorStopsIt->second.hasType>()) { - auto rawColorStops = - static_cast>(colorStopsIt->second); - - for (const auto& stop : rawColorStops) { - if (stop.hasType>()) { - auto stopMap = - static_cast>(stop); - auto positionIt = stopMap.find("position"); - auto colorIt = stopMap.find("color"); - - if (positionIt != stopMap.end() && colorIt != stopMap.end()) { - ColorStop colorStop; - if (positionIt->second.hasValue()) { - auto valueUnit = toValueUnit(positionIt->second); - if (!valueUnit) { - result = {}; - return; - } - colorStop.position = valueUnit; - } - if (colorIt->second.hasValue()) { - fromRawValue( - context.contextContainer, - context.surfaceId, - colorIt->second, - colorStop.color); - } - linearGradient.colorStops.push_back(colorStop); - } + if (colorStops.size() > 0) { + linearGradient.colorStops = colorStops; + } + + backgroundImage.push_back(std::move(linearGradient)); + } else if (type == "radialGradient") { + RadialGradient radialGradient; + auto shapeIt = rawBackgroundImageMap.find("shape"); + if (shapeIt != rawBackgroundImageMap.end() && shapeIt->second.hasType()) { + auto shape = (std::string)(shapeIt->second); + radialGradient.shape = shape == "circle" ? RadialGradientShape::Circle : RadialGradientShape::Ellipse; + } + + auto sizeIt = rawBackgroundImageMap.find("size"); + if (sizeIt != rawBackgroundImageMap.end()) { + if (sizeIt->second.hasType()) { + auto sizeStr = (std::string)(sizeIt->second); + if (sizeStr == "closest-side") { + radialGradient.size.value = RadialGradientSize::SizeKeyword::ClosestSide; + } else if (sizeStr == "farthest-side") { + radialGradient.size.value = RadialGradientSize::SizeKeyword::FarthestSide; + } else if (sizeStr == "closest-corner") { + radialGradient.size.value = RadialGradientSize::SizeKeyword::ClosestCorner; + } else if (sizeStr == "farthest-corner") { + radialGradient.size.value = RadialGradientSize::SizeKeyword::FarthestCorner; + } + } else if (sizeIt->second.hasType>()) { + auto sizeMap = static_cast>(sizeIt->second); + auto xIt = sizeMap.find("x"); + auto yIt = sizeMap.find("y"); + if (xIt != sizeMap.end() && yIt != sizeMap.end()) { + RadialGradientSize sizeObj; + sizeObj.value = RadialGradientSize::Dimensions{toValueUnit(xIt->second), toValueUnit(yIt->second)}; + radialGradient.size = sizeObj; + } + } + + auto positionIt = rawBackgroundImageMap.find("position"); + if (positionIt != rawBackgroundImageMap.end() && positionIt->second.hasType>()) { + auto positionMap = static_cast>(positionIt->second); + + auto topIt = positionMap.find("top"); + auto bottomIt = positionMap.find("bottom"); + auto leftIt = positionMap.find("left"); + auto rightIt = positionMap.find("right"); + + if (topIt != positionMap.end()) { + auto topValue = toValueUnit(topIt->second); + radialGradient.position.top = topValue; + } else if (bottomIt != positionMap.end()) { + auto bottomValue = toValueUnit(bottomIt->second); + radialGradient.position.bottom = bottomValue; + } + + if (leftIt != positionMap.end()) { + auto leftValue = toValueUnit(leftIt->second); + radialGradient.position.left = leftValue; + } else if (rightIt != positionMap.end()) { + auto rightValue = toValueUnit(rightIt->second); + radialGradient.position.right = rightValue; } } } - backgroundImage.push_back(std::move(linearGradient)); + if (colorStops.size() > 0) { + radialGradient.colorStops = colorStops; + } + + backgroundImage.push_back(std::move(radialGradient)); } } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h index e6de585bc69f93..9109ddd205590d 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h @@ -9,10 +9,11 @@ #include #include +#include #include namespace facebook::react { -using BackgroundImage = std::variant; +using BackgroundImage = std::variant; }; // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h b/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h new file mode 100644 index 00000000000000..4c71844d05afc1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +struct ColorStop { + bool operator==(const ColorStop& other) const = default; + SharedColor color; + ValueUnit position; +}; + +struct ProcessedColorStop { + bool operator==(const ProcessedColorStop& other) const = default; + SharedColor color; + std::optional position; +}; + +}; // namespace facebook::react \ No newline at end of file diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h index 987afb977ffcf6..122f0a71c97f82 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -33,18 +34,6 @@ struct GradientDirection { } }; -struct ColorStop { - bool operator==(const ColorStop& other) const = default; - SharedColor color; - ValueUnit position; -}; - -struct ProcessedColorStop { - bool operator==(const ProcessedColorStop& other) const = default; - SharedColor color; - std::optional position; -}; - struct LinearGradient { GradientDirection direction; std::vector colorStops; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h b/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h new file mode 100644 index 00000000000000..f9c977f80a9390 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/RadialGradient.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +enum class RadialGradientShape { + Circle, + Ellipse +}; + +struct RadialGradientSize { + enum class SizeKeyword { + ClosestSide, + FarthestSide, + ClosestCorner, + FarthestCorner + }; + + struct Dimensions { + ValueUnit x; + ValueUnit y; + + bool operator==(const Dimensions& other) const { + return x == other.x && y == other.y; + } + bool operator!=(const Dimensions& other) const { + return !(*this == other); + } + }; + + std::variant value; + + bool operator==(const RadialGradientSize& other) const { + if (std::holds_alternative(value) && std::holds_alternative(other.value)) { + return std::get(value) == std::get(other.value); + } else if (std::holds_alternative(value) && std::holds_alternative(other.value)) { + return std::get(value) == std::get(other.value); + } + return false; + } + + bool operator!=(const RadialGradientSize& other) const { + return !(*this == other); + } +}; + +struct RadialGradientPosition { + std::optional top; + std::optional left; + std::optional right; + std::optional bottom; + + bool operator==(const RadialGradientPosition& other) const { + return top == other.top && + left == other.left && + right == other.right && + bottom == other.bottom; + } + + bool operator!=(const RadialGradientPosition& other) const { + return !(*this == other); + } +}; + +struct RadialGradient { + RadialGradientShape shape; + RadialGradientSize size; + RadialGradientPosition position; + std::vector colorStops; + + bool operator==(const RadialGradient& other) const { + return shape == other.shape && + size == other.size && + position == other.position && + colorStops == other.colorStops; + } + bool operator!=(const RadialGradient& other) const { + return !(*this == other); + } +}; + +}; // namespace facebook::react diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 00514ce0201139..c5aac7f1b06805 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -317,9 +317,15 @@ const APIs: Array = ([ module: require('../examples/Filter/FilterExample'), }, { - key: 'LinearGradient', + key: 'LinearGradientExample', + category: 'UI', module: require('../examples/LinearGradient/LinearGradientExample'), }, + { + key: 'RadialGradientExample', + category: 'UI', + module: require('../examples/RadialGradient/RadialGradientExample'), + }, { key: 'MixBlendModeExample', module: require('../examples/MixBlendMode/MixBlendModeExample'), From 64467b8684613912b80418b8a9ae7f8439e078da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Wed, 26 Mar 2025 03:11:15 +0530 Subject: [PATCH 2/3] revert js example --- packages/rn-tester/js/utils/RNTesterList.ios.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index c5aac7f1b06805..00514ce0201139 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -317,15 +317,9 @@ const APIs: Array = ([ module: require('../examples/Filter/FilterExample'), }, { - key: 'LinearGradientExample', - category: 'UI', + key: 'LinearGradient', module: require('../examples/LinearGradient/LinearGradientExample'), }, - { - key: 'RadialGradientExample', - category: 'UI', - module: require('../examples/RadialGradient/RadialGradientExample'), - }, { key: 'MixBlendModeExample', module: require('../examples/MixBlendMode/MixBlendModeExample'), From e72f83040162c7760507c6bc5e933fb21843772e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Fri, 28 Mar 2025 07:31:39 +0530 Subject: [PATCH 3/3] rename type from camelCase to kebab-case --- .../ReactCommon/react/renderer/components/view/conversions.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index f62cb78323d9ae..a55c75c6d83d5a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -1218,7 +1218,7 @@ inline void fromRawValue( } } - if (type == "linearGradient") { + if (type == "linear-gradient") { LinearGradient linearGradient; auto directionIt = rawBackgroundImageMap.find("direction"); @@ -1256,7 +1256,7 @@ inline void fromRawValue( } backgroundImage.push_back(std::move(linearGradient)); - } else if (type == "radialGradient") { + } else if (type == "radial-gradient") { RadialGradient radialGradient; auto shapeIt = rawBackgroundImageMap.find("shape"); if (shapeIt != rawBackgroundImageMap.end() && shapeIt->second.hasType()) {