Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS - Circle with angular gradient

How how to draw programically gradient like this in iOS?

For now I have found only code for creating gradient from the center to the edge of the circle (in addition only full circle).

like image 601
Trzy Gracje Avatar asked Jun 25 '26 09:06

Trzy Gracje


1 Answers

If you want a gradient like that, you may have to implement it yourself. For example, this creates a pixel buffer data provider, fills it with alpha/red/green/blue data in that sort of sweeping radial pattern, and creates an image from that:

- (UIImage *)buildAngularGradientInRect:(CGRect)rect
                                 radius:(CGFloat)radius
                             startAngle:(CGFloat)startAngle
                               endAngle:(CGFloat)endAngle
                             startColor:(UIColor *)startColor
                               endColor:(UIColor *)endColor
                              clockwise:(BOOL)clockwise
                                  scale:(CGFloat)scale
{
    if (scale == 0) scale = [[UIScreen mainScreen] scale];

    CGFloat startRed, startGreen, startBlue, startAlpha;
    [startColor getRed:&startRed green:&startGreen blue:&startBlue alpha:&startAlpha];

    CGFloat endRed, endGreen, endBlue, endAlpha;
    [endColor getRed:&endRed green:&endGreen blue:&endBlue alpha:&endAlpha];

    size_t width = rect.size.width * scale;
    size_t height = rect.size.height * scale;
    CGPoint center = CGPointMake(width / 2.0, height / 2.0);
    size_t bufferSize = width * height * 4;
    NSMutableData *data = [NSMutableData dataWithCapacity:bufferSize];
    struct {
        UInt8 alpha;
        UInt8 red;
        UInt8 green;
        UInt8 blue;
    } argb;

    size_t bitsPerComponent = 8;
    size_t bitsPerPixel = 4 * bitsPerComponent;
    size_t bytesPerRow = 4 * width;

    for (NSInteger y = 0; y < height; y++) {
        for (NSInteger x = 0; x < width; x++) {
            CGFloat angle = atan2(y - center.y, x - center.x);
            CGFloat distance = hypot(y - center.y, x - center.x);
            CGFloat value = [self percentAngle:angle betweenStart:startAngle end:endAngle clockwise:clockwise];
            if (distance <= (radius * scale) && value >= 0.0 && value <= 1.0) {
                argb.alpha = (startAlpha + (endAlpha - startAlpha) * value) * 255;
                argb.red   = (startRed   + (endRed   - startRed)   * value) * 255;
                argb.green = (startGreen + (endGreen - startGreen) * value) * 255;
                argb.blue  = (startBlue  + (endBlue  - startBlue)  * value) * 255;
            } else {
                argb.alpha = 0;
                argb.red   = 255;
                argb.green = 255;
                argb.blue  = 255;
            }
            [data appendBytes:&argb length:sizeof(argb)];
        }
    }

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    CGImageRef imageRef = CGImageCreate(width,
                                        height,
                                        bitsPerComponent,
                                        bitsPerPixel,
                                        bytesPerRow,
                                        colorSpace,
                                        (CGBitmapInfo)kCGImageAlphaPremultipliedFirst,
                                        provider,
                                        NULL,
                                        NO,
                                        kCGRenderingIntentDefault);

    CGColorSpaceRelease(colorSpace);
    CGDataProviderRelease(provider);
    UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
    CGImageRelease(imageRef);

    return image;
}

- (CGFloat)percentAngle:(CGFloat)angle betweenStart:(CGFloat)start end:(CGFloat)end clockwise:(BOOL)clockwise
{
    while (start < 0) start += M_PI * 2.0;
    while (angle < 0) angle += M_PI * 2.0;
    while (end < 0)   end += M_PI * 2.0;
    if (clockwise) {
        while (end < start) end += M_PI * 2.0;
        while (angle < start) angle += M_PI * 2.0;
    } else {
        while (start < end) end -= M_PI * 2.0;
        while (start < angle) angle -= M_PI * 2.0;
    }
    CGFloat range = end - start;
    CGFloat value = angle - start;

    return value / range;
}

So, you can either create a simple UIImage:

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
[self.view addSubview:imageView];
UIImage *image = [self buildAngularGradientInRect:imageView.bounds
                                           radius:imageView.bounds.size.width / 2.0
                                       startAngle:M_PI + M_PI_4
                                         endAngle:M_PI + M_PI_2 + M_PI_4
                                       startColor:[UIColor whiteColor]
                                         endColor:[UIColor redColor]
                                        clockwise:YES
                                            scale:0];
imageView.image = image;

Or, as in your example, create an gradient that uses the alpha channel and then apply that as a mask to another view:

UIImage *image = [self buildAngularGradientInRect:view.bounds
                                           radius:view.bounds.size.width / 2.0
                                       startAngle:M_PI + M_PI_4
                                         endAngle:M_PI + M_PI_2 + M_PI_4
                                       startColor:[UIColor clearColor]
                                         endColor:[UIColor whiteColor]
                                        clockwise:YES
                                            scale:0];

CALayer *maskLayer = [CALayer layer];
maskLayer.frame = view.bounds;
maskLayer.contents = (id)image.CGImage;
view.layer.mask = maskLayer;

Hopefully this illustrates the pixel-level control that one can apply to an image or to a mask.


FWIW, here is a Swift 3 rendition:

func buildAngularGradient(in rect: CGRect, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, startColor: UIColor, endColor: UIColor, clockwise: Bool = true, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
    var startRed: CGFloat = 0
    var startGreen: CGFloat = 0
    var startBlue: CGFloat = 0
    var startAlpha: CGFloat = 0
    startColor.getRed(&startRed, green: &startGreen, blue: &startBlue, alpha: &startAlpha)

    var endRed: CGFloat = 0
    var endGreen: CGFloat = 0
    var endBlue: CGFloat = 0
    var endAlpha: CGFloat = 0
    endColor.getRed(&endRed, green: &endGreen, blue: &endBlue, alpha: &endAlpha)

    let width = Int(rect.size.width * scale)
    let height = Int(rect.size.height * scale)
    let center = CGPoint(x: width / 2, y: height / 2)

    let space = CGColorSpaceCreateDeviceRGB()
    let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: space, bitmapInfo: Pixel.bitmapInfo)!

    let buffer = context.data!

    let pixels = buffer.bindMemory(to: Pixel.self, capacity: width * height)
    var pixel: Pixel
    for y in 0 ..< height {
        for x in 0 ..< width {
            let angle = atan2(CGFloat(y) - center.y, CGFloat(x) - center.x)
            let distance = hypot(CGFloat(y) - center.y, CGFloat(x) - center.x)

            let value = percent(angle: angle, between: startAngle, and: endAngle, clockwise: true)

            //    CGFloat value = [self percentAngle:angle betweenStart:startAngle end:endAngle clockwise:clockwise];
            if distance <= (radius * scale) && value >= 0.0 && value <= 1.0 {
                pixel = Pixel(red:   UInt8((startRed   + (endRed   - startRed)   * value) * 255),
                              green: UInt8((startGreen + (endGreen - startGreen) * value) * 255),
                              blue:  UInt8((startBlue  + (endBlue  - startBlue)  * value) * 255),
                              alpha: UInt8((startAlpha + (endAlpha - startAlpha) * value) * 255))
            } else {
                pixel = Pixel(red: 255, green: 255, blue: 255, alpha: 0)
            }
            pixels[y * width + x] = pixel
        }
    }

    let cgImage = context.makeImage()!
    return UIImage(cgImage: cgImage, scale: scale, orientation: .up)
}

private func percent(angle: CGFloat, between start: CGFloat, and end: CGFloat, clockwise: Bool) -> CGFloat {
    var start = start

    while start < 0 { start += .pi * 2 }

    var angle = angle
    while angle < 0 { angle += .pi * 2 }

    var end = end
    while end < 0 { end += .pi * 2 }

    if clockwise {
        while (end < start) { end += .pi * 2 }
        while (angle < start) { angle += .pi * 2 }
    } else {
        while (start < end) { end -= .pi * 2 }
        while (start < angle) { angle -= .pi * 2 }
    }
    let range = end - start
    let value = angle - start

    return value / range;
}

Where

struct Pixel: Equatable {
    private var rgba: UInt32

    var red: UInt8 {
        return UInt8((rgba >> 24) & 255)
    }

    var green: UInt8 {
        return UInt8((rgba >> 16) & 255)
    }

    var blue: UInt8 {
        return UInt8((rgba >> 8) & 255)
    }

    var alpha: UInt8 {
        return UInt8((rgba >> 0) & 255)
    }

    init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
        rgba = (UInt32(red) << 24) | (UInt32(green) << 16) | (UInt32(blue) << 8) | (UInt32(alpha) << 0)
    }

    static let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue

    static func ==(lhs: Pixel, rhs: Pixel) -> Bool {
        return lhs.rgba == rhs.rgba
    }
}
like image 188
Rob Avatar answered Jun 26 '26 23:06

Rob



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!