Нарисуйте свечение вокруг края нескольких CGPaths

image

Если я создаю CGMutablePathRef путем добавления двух круговых путей, как показано на левом изображении, возможно ли получить окончательный CGPathRef, который представляет только внешнюю границу, как показано на правом изображении?

Спасибо за любую помощь!

Ответ 1

То, о чем вы просите, это объединение путей безье. Apple не выпускает никаких API для вычисления объединения путей. Это на самом деле довольно сложный алгоритм. Вот несколько ссылок:

Если вы объясните, что вы хотите сделать с пулом union, мы можем предложить некоторые альтернативы, которые не требуют фактического вычисления объединения.

Вы можете нарисовать довольно приличное внутреннее свечение, фактически не вычисляя объединение путей. Вместо этого сделайте растровое изображение. Заполните каждый путь в растровом изображении. Вы будете использовать это как маску. Затем создайте инвертированное изображение маски, в котором все, что находится за пределами области объединения, заполнено. Вы нарисуете это, чтобы заставить CoreGraphics нарисовать тень вокруг внутреннего края объединения. Наконец, установите маску в качестве маски CGContext, задайте параметры тени и нарисуйте инвертированное изображение.

Хорошо, это звучит сложно. Но вот что это выглядит (версия Retina справа):

glowglow retina

Это не идеально (слишком светло в углах), но это довольно хорошо.

Итак, вот код. Я перехожу к UIBezierPaths вместо CGPaths, но тривиально конвертировать между ними. Я использую некоторые функции и объекты UIKit. Помните, что вы всегда можете сделать вывод UIKit произвольным CGContext, используя UIGraphicsPushContext и UIGraphicsPopContext.

Во-первых, нам нужно изображение маски. Это должно быть изображение с альфа-каналом, которое равно 1 внутри любого из путей, и 0 вне всех путей. Этот метод возвращает такое изображение:

- (UIImage *)maskWithPaths:(NSArray *)paths bounds:(CGRect)bounds
{
    // Get the scale for good results on Retina screens.
    CGFloat scale = [UIScreen mainScreen].scale;
    CGSize scaledSize = CGSizeMake(bounds.size.width * scale, bounds.size.height * scale);

    // Create the bitmap with just an alpha channel.
    // When created, it has value 0 at every pixel.
    CGContextRef gc = CGBitmapContextCreate(NULL, scaledSize.width, scaledSize.height, 8, scaledSize.width, NULL, kCGImageAlphaOnly);

    // Adjust the current transform matrix for the screen scale.
    CGContextScaleCTM(gc, scale, scale);
    // Adjust the CTM in case the bounds origin isn't zero.
    CGContextTranslateCTM(gc, -bounds.origin.x, -bounds.origin.y);

    // whiteColor has all components 1, including alpha.
    CGContextSetFillColorWithColor(gc, [UIColor whiteColor].CGColor);

    // Fill each path into the mask.
    for (UIBezierPath *path in paths) {
        CGContextBeginPath(gc);
        CGContextAddPath(gc, path.CGPath);
        CGContextFillPath(gc);
    }

    // Turn the bitmap context into a UIImage.
    CGImageRef cgImage = CGBitmapContextCreateImage(gc);
    CGContextRelease(gc);
    UIImage *image = [UIImage imageWithCGImage:cgImage scale:scale orientation:UIImageOrientationDownMirrored];
    CGImageRelease(cgImage);
    return image;
}

Это была тяжелая часть. Теперь нам нужно изображение, которое является нашим цветом свечения повсюду за пределами области маски (путь). Мы можем использовать функции UIKit, чтобы сделать это проще, чем чистый подход CoreGraphics:

- (UIImage *)invertedImageWithMask:(UIImage *)mask color:(UIColor *)color
{
    CGRect rect = { CGPointZero, mask.size };
    UIGraphicsBeginImageContextWithOptions(rect.size, NO, mask.scale); {
        // Fill the entire image with color.
        [color setFill];
        UIRectFill(rect);
        // Now erase the masked part.
        CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, mask.CGImage);
        CGContextClearRect(UIGraphicsGetCurrentContext(), rect);
    }
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

С этими двумя изображениями мы можем нарисовать внутреннее свечение в текущем графическом контексте UIKit для массива путей:

- (void)drawInnerGlowWithPaths:(NSArray *)paths bounds:(CGRect)bounds color:(UIColor *)color offset:(CGSize)offset blur:(CGFloat)blur
{
    UIImage *mask = [self maskWithPaths:paths bounds:bounds];
    UIImage *invertedImage = [self invertedImageWithMask:mask color:color];
    CGContextRef gc = UIGraphicsGetCurrentContext();

    // Save the graphics state so I can restore the clip and
    // shadow attributes after drawing.
    CGContextSaveGState(gc); {
        CGContextClipToMask(gc, bounds, mask.CGImage);
        CGContextSetShadowWithColor(gc, offset, blur, color.CGColor);
        [invertedImage drawInRect:bounds];
    } CGContextRestoreGState(gc);
}

Чтобы протестировать его, я создал изображение с помощью нескольких кругов и поместил его в UIImageView:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBezierPath *path1 = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(20, 20, 60, 60)];
    UIBezierPath *path2 = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(50, 50, 60, 60)];
    NSArray *paths = [NSArray arrayWithObjects:path1, path2, nil];

    UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, 0.0); {
        [self drawInnerGlowWithPaths:paths bounds:self.imageView.bounds color:[UIColor colorWithHue:0 saturation:1 brightness:.8 alpha:.8] offset:CGSizeZero blur:10.0];
    }
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}

Ответ 2

В Swift 3 есть два способа:

sample

код пути:

    let radius          = rect.height * 0.25
    let centerX         = rect.width  * 0.5
    let centerY         = rect.height * 0.5
    let arcCenterOffset = radius - radius * 0.5 * sqrt(3)
    let degree:(_: CGFloat) -> CGFloat = {
        return CGFloat.pi * $0 / 180
    }

    let gourd   = UIBezierPath()
    let circle1 = UIBezierPath(arcCenter: CGPoint(x: centerX - radius + arcCenterOffset, y: centerY), radius: radius, startAngle: degree(-30), endAngle: degree(30), clockwise: false)
    let circle2 = UIBezierPath(arcCenter: CGPoint(x: centerX + radius - arcCenterOffset, y: centerY ), radius: radius, startAngle: degree(150), endAngle: degree(-150), clockwise: false)
    gourd.append(circle1)
    gourd.append(circle2)

    let gourdInverse = UIBezierPath(cgPath: gourd.cgPath)
    let infiniteRect = UIBezierPath(rect: .infinite)
    gourdInverse.append(infiniteRect)

    guard let c = UIGraphicsGetCurrentContext() else {
        fatalError("current context not found.")
    }
  • правило нечетного заполнения:

    c.beginPath()
    c.addPath(gourdInverse.cgPath)
    c.setShadow(offset: CGSize.zero, blur: 10, color: UIColor.red.cgColor)
    c.setFillColor(UIColor(white: 1, alpha: 1).cgColor)
    c.fillPath(using: .evenOdd)
    
  • клип

    c.beginPath()
    c.addPath(gourd.cgPath)
    c.clip()
    
    c.beginPath()
    c.addPath(gourdInverse.cgPath)
    c.setShadow(offset: CGSize.zero, blur: 10, color: UIColor.red.cgColor)
    c.fillPath()