CGContext:如何在位图上下文之外擦除像素(例如 kCGBlendModeClear)



我正在尝试使用Core Graphics构建橡皮擦工具,但我发现制作高性能橡皮擦非常困难 - 这一切都归结为:

CGContextSetBlendMode(context, kCGBlendModeClear)

如果你用谷歌搜索如何使用Core Graphics"擦除",几乎每个答案都会返回该片段。问题是它(显然)仅适用于位图上下文。如果您正在尝试实现交互式擦除,我看不出kCGBlendModeClear如何帮助您 - 据我所知,您或多或少地被锁定在屏幕内外的擦除UIImage/CGImage并在著名的非性能[UIView drawRect]中绘制该图像。

这是我能做的最好的事情:

-(void)drawRect:(CGRect)rect
{
    if (drawingStroke) {
        if (eraseModeOn) {
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
            CGContextRef context = UIGraphicsGetCurrentContext();
            [eraseImage drawAtPoint:CGPointZero];
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, lineWidth);
            CGContextSetBlendMode(context, kCGBlendModeClear);
            CGContextSetLineWidth(context, ERASE_WIDTH);
            CGContextStrokePath(context);
            curImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            [curImage drawAtPoint:CGPointZero];
        } else {
            [curImage drawAtPoint:CGPointZero];
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, lineWidth);
            CGContextSetBlendMode(context, kCGBlendModeNormal);
            CGContextSetStrokeColorWithColor(context, lineColor.CGColor);
            CGContextStrokePath(context);
        }
    } else {
        [curImage drawAtPoint:CGPointZero];
    }
}

绘制一条普通线(!eraseModeOn)是可以接受的;我正在将屏幕外绘图缓冲区(curImage,其中包含所有以前绘制的笔触)到当前CGContext,并且我正在渲染当前正在绘制的线条(路径)。它并不完美,但嘿,它有效,而且性能合理。

但是,由于kCGBlendModeNormal显然无法在位图上下文之外工作,因此我被迫:

  1. 创建位图上下文 ( UIGraphicsBeginImageContextWithOptions )。
  2. 绘制我的屏幕外缓冲区(eraseImage,它实际上是从打开橡皮擦工具时的curImage派生的 - 所以为了参数起见,它真的与curImage几乎相同)。
  3. 渲染当前正在绘制到位图上下文的"擦除线"(路径)(使用 kCGBlendModeClear 清除像素)。
  4. 将整个图像提取到屏幕外缓冲区 ( curImage = UIGraphicsGetImageFromCurrentImageContext();
  5. 然后最后将屏幕外缓冲区传送到视图的CGContext

这太可怕了,性能方面。使用仪器的时间工具,这种方法的问题在哪里非常明显:

  • UIGraphicsBeginImageContextWithOptions很贵
  • 绘制两次屏幕外缓冲区的成本很高
  • 将整个图像提取到屏幕外缓冲区中是昂贵的

因此,自然而然地,代码在真正的iPad上表现得很糟糕。

我真的不确定在这里做什么。我一直在试图弄清楚如何在非位图上下文中清除像素,但据我所知,依赖kCGBlendModeClear是一个死胡同。

有什么想法或建议吗?其他 iOS 绘图应用程序如何处理擦除?


附加信息

我一直在尝试一种CGLayer的方法,因为根据我所做的一些谷歌搜索,CGContextSetBlendMode(context, kCGBlendModeClear)似乎确实可以在CGLayer中工作。

但是,我并不十分希望这种方法会成功。在drawRect中绘制图层(即使使用 setNeedsDisplayInRect )是非常不高性能的;Core Graphics 正在阻塞,将以 CGContextDrawLayerAtPoint 为单位渲染图层中的每个路径(根据 Instruments)。据我所知,就性能而言,使用位图上下文在这里绝对更可取 - 当然,唯一的问题是上述问题(kCGBlendModeClear将位图上下文传送到drawRect中的主CGContext后不起作用)。

我使用以下代码设法获得了良好的结果:

- (void)drawRect:(CGRect)rect
{
    if (drawingStroke) {
        if (eraseModeOn) {
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextBeginTransparencyLayer(context, NULL);
            [eraseImage drawAtPoint:CGPointZero];
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, ERASE_WIDTH);
            CGContextSetBlendMode(context, kCGBlendModeClear);
            CGContextSetStrokeColorWithColor(context, [[UIColor clearColor] CGColor]);
            CGContextStrokePath(context);
            CGContextEndTransparencyLayer(context);
        } else {
            [curImage drawAtPoint:CGPointZero];
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, self.lineWidth);
            CGContextSetBlendMode(context, kCGBlendModeNormal);
            CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
            CGContextStrokePath(context);
        }
    } else {
        [curImage drawAtPoint:CGPointZero];
    }
    self.empty = NO;
}

诀窍是将以下内容包装成CGContextBeginTransparencyLayer/CGContextEndTransparencyLayer调用:

  • 将擦除背景图像切换到上下文
  • 使用kCGBlendModeClear在擦除背景图像的顶部绘制"擦除"路径

由于擦除背景图像的像素数据和擦除路径都在同一层中,因此具有清除像素的效果。

遵循绘画范式的2D图形。当你在绘画时,很难去除你已经放在画布上的油漆,但在上面添加更多的油漆非常容易。具有位图上下文的混合模式为您提供了一种使用几行代码即可执行困难操作(从画布上刮除油漆)的方法。几行代码并不能使它成为一个简单的计算操作(这就是为什么它执行缓慢的原因)。

无需进行屏幕外位图缓冲即可假清除像素的最简单方法是在图像上绘制视图的背景。

-(void)drawRect:(CGRect)rect
{
    if (drawingStroke) {
        CGColor lineCgColor = lineColor.CGColor;
        if (eraseModeOn) {
            //Use concrete background color to display erasing. You could use the backgroundColor property of the view, or define a color here
            lineCgColor = [[self backgroundColor] CGColor];
        } 
        [curImage drawAtPoint:CGPointZero];
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextAddPath(context, currentPath);
        CGContextSetLineCap(context, kCGLineCapRound);
        CGContextSetLineWidth(context, lineWidth);
        CGContextSetBlendMode(context, kCGBlendModeNormal);
        CGContextSetStrokeColorWithColor(context, lineCgColor);
        CGContextStrokePath(context);
    } else {
        [curImage drawAtPoint:CGPointZero];
    }
}

困难(但更正确)的方法是在后台串行队列上进行图像编辑以响应编辑事件。获取新操作时,在后台将位图呈现到图像缓冲区。缓冲图像准备就绪后,调用setNeedsDisplay以允许在下一个更新周期中重绘视图。这更正确,因为drawRect:应该尽快显示视图的内容,而不是处理编辑操作。

@interface ImageEditor : UIView
@property (nonatomic, strong) UIImage * imageBuffer;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@end
@implementation ImageEditor
- (dispatch_queue_t) serialQueue
{
    if (_serialQueue == nil)
    {
        _serialQueue = dispatch_queue_create("com.example.com.imagebuffer", DISPATCH_QUEUE_SERIAL);
    }
    return _serialQueue;
}
- (void)editingAction
{
    dispatch_async(self.serialQueue, ^{
        CGSize bufferSize = [self.imageBuffer size];
        UIGraphicsBeginImageContext(bufferSize);
        CGContext context = UIGraphicsGetCurrentContext();
        CGContextDrawImage(context, CGRectMake(0, 0, bufferSize.width, bufferSize.height), [self.imageBuffer CGImage]);
        //Do editing action, draw a clear line, solid line, etc
        self.imageBuffer = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            [self setNeedsDisplay];
        });
    });
}
-(void)drawRect:(CGRect)rect
{
    [self.imageBuffer drawAtPoint:CGPointZero];
}
@end

键是CGContextBeginTransparencyLayer并使用clearColor并设置CGContextSetBlendMode(context, kCGBlendModeClear);

最新更新