195 lines
6.5 KiB
Plaintext
195 lines
6.5 KiB
Plaintext
/*
|
|
* 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 <React/RCTAnimationUtils.h>
|
|
#import <React/RCTConversions.h>
|
|
#include <react/renderer/graphics/ValueUnit.h>
|
|
#import <react/utils/FloatComparison.h>
|
|
#import "RCTGradientUtils.h"
|
|
|
|
using namespace facebook::react;
|
|
|
|
namespace {
|
|
using RadiusVector = std::pair<CGFloat, CGFloat>;
|
|
|
|
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<CGPoint, 4> 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<RadialGradientSize::Dimensions>(size.value)) {
|
|
const auto &dimensions = std::get<RadialGradientSize::Dimensions>(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<RadialGradientSize::SizeKeyword>(size.value)) {
|
|
const auto &keyword = std::get<RadialGradientSize::SizeKeyword>(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<id> *colors = [NSMutableArray array];
|
|
NSMutableArray<NSNumber *> *locations = [NSMutableArray array];
|
|
|
|
[RCTGradientUtils getColors:colors andLocations:locations fromColorStops:colorStops];
|
|
|
|
gradientLayer.colors = colors;
|
|
gradientLayer.locations = locations;
|
|
|
|
return gradientLayer;
|
|
}
|
|
|
|
@end
|