/* * 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 { CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.type = kCAGradientLayerRadial; 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); const auto gradientLineLength = std::max(radiusX, radiusY); const auto colorStops = [RCTGradientUtils getFixedColorStops:gradient.colorStops gradientLineLength:gradientLineLength]; gradientLayer.startPoint = CGPointMake(centerPoint.x / size.width, centerPoint.y / size.height); // endpoint.x is horizontal length and endpoint.y is vertical length gradientLayer.endPoint = CGPointMake( gradientLayer.startPoint.x + radiusX / size.width, gradientLayer.startPoint.y + radiusY / size.height); NSMutableArray *colors = [NSMutableArray array]; NSMutableArray *locations = [NSMutableArray array]; [RCTGradientUtils getColors:colors andLocations:locations fromColorStops:colorStops]; gradientLayer.colors = colors; gradientLayer.locations = locations; return gradientLayer; } @end