Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ BOOL RCTIsAttributedStringEffectivelySame(
NSDictionary<NSAttributedStringKey, id> *insensitiveAttributes,
const facebook::react::TextAttributes &baseTextAttributes);

/*
* Normalized [0, 1] start/end points (top-left origin, y increasing downward) describing the
* gradient line for `angleDegrees`, following the same convention as `CAGradientLayer`: 0deg runs
* left->right, 90deg runs top->bottom, and increasing angles rotate from there. Callers map these
* onto their target rect (e.g. the layer frame, or glyph bounds in a flipped context). Sharing this
* keeps text gradient direction identical across mirror and clamp modes and across the two files
* that draw them.
*/
static inline void RCTNormalizedGradientPointsForAngle(CGFloat angleDegrees, CGPoint *outStart, CGPoint *outEnd)
{
CGFloat radians = angleDegrees * M_PI / 180.0;
CGFloat halfCos = 0.5 * cos(radians);
CGFloat halfSin = 0.5 * sin(radians);
*outStart = CGPointMake(0.5 - halfCos, 0.5 - halfSin);
*outEnd = CGPointMake(0.5 + halfCos, 0.5 + halfSin);
}

static inline NSData *RCTWrapEventEmitter(const facebook::react::SharedEventEmitter &eventEmitter)
{
auto eventEmitterPtr = new std::weak_ptr<const facebook::react::EventEmitter>(eventEmitter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,34 +151,49 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex

if ([cgColors count] > 0) {
BOOL isClampMode = textAttributes.gradientMode.value_or("") == "clamp";
if (!isClampMode) {
// Mirror mode (default) duplicates the first color at the end.
[cgColors addObject:cgColors[0]];
}
CGFloat angle = !isnan(textAttributes.gradientAngle) ? textAttributes.gradientAngle : 0.0;

CAGradientLayer *gradient = [CAGradientLayer layer];
CGFloat patternWidth =
(!isnan(textAttributes.gradientWidth) && textAttributes.gradientWidth > 0) ? textAttributes.gradientWidth : 100;
CGFloat lineHeight = !isnan(textAttributes.lineHeight)
? textAttributes.lineHeight
: (!isnan(textAttributes.fontSize) ? textAttributes.fontSize : 14);
CGFloat height = lineHeight * RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes);
gradient.frame = CGRectMake(0, 0, patternWidth, height);
gradient.colors = cgColors;

CGFloat angle = !isnan(textAttributes.gradientAngle) ? textAttributes.gradientAngle : 0.0;
CGFloat radians = angle * M_PI / 180.0;

CGFloat startX = 0.5 - 0.5 * cos(radians);
CGFloat startY = 0.5 - 0.5 * sin(radians);
CGFloat endX = 0.5 + 0.5 * cos(radians);
CGFloat endY = 0.5 + 0.5 * sin(radians);
CGFloat gradientHeight = lineHeight * RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes);

CGPoint startPoint;
CGPoint endPoint;
RCTNormalizedGradientPointsForAngle(angle, &startPoint, &endPoint);

CGFloat imageHeight = gradientHeight;
if (isClampMode) {
// Clamp mode (Shader.TileMode.CLAMP on Android): the gradient must not repeat when the text
// is taller than the gradient - e.g. descenders below the last stop should hold the edge
// color, not wrap back to the opposite end. `colorWithPatternImage` tiles, so instead of
// appending a color we render the pattern taller than any single line of text and let
// CAGradientLayer clamp the padded remainder to its edge colors. Text height is bounded by
// the font, so padding the height covers the vertical case (the one that wraps); a
// horizontal gradient's width is unbounded and can still tile.
UIFont *font = RCTEffectiveFontFromTextAttributes(textAttributes);
CGFloat glyphHeight = font != nil ? font.lineHeight : gradientHeight;
imageHeight = MAX(gradientHeight, glyphHeight) * 2.0;

// Keep the gradient spanning its intended `gradientHeight` at the top of the padded image so
// everything below it clamps to the edge color.
CGFloat scaleY = imageHeight > 0 ? gradientHeight / imageHeight : 1.0;
startPoint.y *= scaleY;
endPoint.y *= scaleY;
} else {
// Mirror mode (default) duplicates the first color at the end so the tiled pattern mirrors.
[cgColors addObject:cgColors[0]];
}

gradient.startPoint = CGPointMake(startX, startY);
gradient.endPoint = CGPointMake(endX, endY);
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(0, 0, patternWidth, imageHeight);
gradient.colors = cgColors;
gradient.startPoint = startPoint;
gradient.endPoint = endPoint;

UIGraphicsImageRenderer *renderer =
[[UIGraphicsImageRenderer alloc] initWithSize:gradient.frame.size];
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:gradient.frame.size];
UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
[gradient renderInContext:rendererContext.CGContext];
}];
Expand Down