Alexandr Hotko
Alexandr Hotko

Reputation: 83

UIBezierPath stroke with alpha issue / artifact

I need to draw a curve with a translucent color. But if the curve contains many points, then you can get a strange effect. How to get rid of it?

UIBezierPath * bezierPath = [ UIBezierPath bezierPath ];
bezierPath.lineCapStyle = kCGLineCapRound;
bezierPath.lineJoinStyle = kCGLineJoinRound;
bezierPath.lineWidth = 80.0;
[ bezierPath moveToPoint:CGPointMake(50.0, 50.0) ];
for (int i = 0; i < 900; i++)
{
    [ bezierPath addLineToPoint:CGPointMake(50.0 + i, 50.0) ];
}

UIGraphicsImageRendererFormat * format = [ UIGraphicsImageRendererFormat defaultFormat ];
format.scale = 1.0;
format.opaque = NO;
format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
CGSize size = CGSizeMake(1000.0, 100.0);
UIGraphicsImageRenderer * renderer = [ [ UIGraphicsImageRenderer alloc ] initWithSize:size format:format ];
UIImage * image = [ renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext)
{
    [ [ UIColor clearColor ] setFill ];
    [ [ UIColor colorWithWhite:0.0 alpha:0.5 ] setStroke ];
    [ bezierPath stroke ];
} ];

Upvotes: 0

Views: 51

Answers (1)

DonMag
DonMag

Reputation: 77452

Curious...

From experience, UIBezierPath can be rather quirky. In this case, I'm guessing this particular quirk is caused by internal optimizations.

The reason I believe that to be the case is because if we generate a path with 256 points -- 1 moveTo + 255 addLineTo commands -- we don't see the problem:

enter image description here

However, as soon as we use 1 moveTo + 256 addLineTo commands -- for a total of 257 points, we get this:

enter image description here

One option to get around that would be to render a temporary image with clear background and 100% opaque stroke, and then render that image with 50% alpha:

// create image with solid stroke
UIImage * tmpImage = [ renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext)
{
    [ [ UIColor clearColor ] setFill ];
    [ [ UIColor colorWithWhite:0.0 alpha:1.0 ] setStroke ];
    [ bezierPath stroke ];
} ];

// create image2 by rendering tmpImage at 50% alpha
UIImage * image2 = [ renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext)
{
    [tmpImage drawAtPoint:CGPointZero blendMode:kCGBlendModeNormal alpha:0.5];
} ];

Here's a complete, runnable example -- starts with 250 addLineTo commands... each tap anywhere increments that by 1:

#import <UIKit/UIKit.h>

@interface AlphaPathVC : UIViewController

@end

@interface AlphaPathVC ()
{
    NSInteger nPoints;
    UIImageView *imgView1;
    UIImageView *imgView2;
    UILabel *infoLabel;
}
@end

@implementation AlphaPathVC

- (void)viewDidLoad {
    
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.systemYellowColor;
    
    imgView1 = [UIImageView new];
    imgView2 = [UIImageView new];
    
    imgView1.backgroundColor = UIColor.whiteColor;
    imgView2.backgroundColor = UIColor.whiteColor;

    infoLabel = [UILabel new];
    infoLabel.backgroundColor = UIColor.whiteColor;
    infoLabel.numberOfLines = 0;
    infoLabel.text = @"Tap anywhere to increase number of points and re-generate images...";
    
    [self.view addSubview:imgView1];
    [self.view addSubview:imgView2];
    [self.view addSubview:infoLabel];
    
    nPoints = 250;
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    
    UIEdgeInsets i = self.view.safeAreaInsets;
    imgView1.frame = CGRectMake(i.left + 20.0, i.top + 20.0        , 1000.0, 100.0);
    imgView2.frame = CGRectMake(i.left + 20.0, i.top + 20.0 + 120.0, 1000.0, 100.0);
    
    infoLabel.frame = CGRectMake(i.left + 20.0, i.top + 20.0 + 240.0, 200.0, 80.0);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    UIBezierPath * bezierPath = [ UIBezierPath new ];
    bezierPath.lineCapStyle = kCGLineCapRound;
    bezierPath.lineJoinStyle = kCGLineJoinRound;
    bezierPath.lineWidth = 80.0;
    
    [ bezierPath moveToPoint:CGPointMake(50.0, 50.0) ];

    infoLabel.text = [NSString stringWithFormat:@" moveTo plus\n %ld addLineTo", (long)nPoints];
    
    for (int i = 0; i < nPoints; i++)
    {
        [ bezierPath addLineToPoint:CGPointMake(50.0 + i, 50.0) ];
    }
    
    UIGraphicsImageRendererFormat * format = [ UIGraphicsImageRendererFormat defaultFormat ];
    format.scale = 1.0;
    format.opaque = NO;
    format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
    CGSize size = CGSizeMake(1000.0, 100.0);
    UIGraphicsImageRenderer * renderer = [ [ UIGraphicsImageRenderer alloc ] initWithSize:size format:format ];
    
    // create image with alpha stroke
    UIImage * image1 = [ renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext)
    {
        [ [ UIColor clearColor ] setFill ];
        [ [ UIColor colorWithWhite:0.0 alpha:0.5 ] setStroke ];
        [ bezierPath stroke ];
    } ];

    // create image with solid stroke
    UIImage * tmpImage = [ renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext)
    {
        [ [ UIColor clearColor ] setFill ];
        [ [ UIColor colorWithWhite:0.0 alpha:1.0 ] setStroke ];
        [ bezierPath stroke ];
    } ];
    
    // create image2 by rendering tmpImage at 50% alpha
    UIImage * image2 = [ renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext)
    {
        [tmpImage drawAtPoint:CGPointZero blendMode:kCGBlendModeNormal alpha:0.5];
    } ];
    
    imgView1.image = image1;
    imgView2.image = image2;

    ++nPoints;
    
}

@end

Looks like this after 7 taps:

enter image description here

Upvotes: 2

Related Questions