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

时间:2013-11-09 14:04:03

标签: ios objective-c core-graphics cgcontext

我正在尝试使用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非常相似。 / LI>
  3. 渲染当前正在绘制到位图上下文的“擦除线”(路径)(使用kCGBlendModeClear清除像素)。
  4. 将整个图像提取到屏幕外缓冲区(curImage = UIGraphicsGetImageFromCurrentImageContext();
  5. 然后最终将屏幕外缓冲区blit到视图的CGContext
  6. 这很糟糕,表现明智。使用Instrument的Time工具,很明显这个方法的问题在哪里:

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

    很自然地,代码在真正的iPad上表现得非常糟糕。

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

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


    其他信息

    我一直在使用CGLayer方法,因为基于我已经完成的一些谷歌搜索,CGContextSetBlendMode(context, kCGBlendModeClear)似乎可以在CGLayer中工作。

    然而,我并不十分希望这种方法能够成功。在drawRect中绘制图层(即使使用setNeedsDisplayInRect)也非常不具备效果;令人窒息的Core Graphics将在CGContextDrawLayerAtPoint中呈现图层中的每个路径(根据Instruments)。据我所知,在性能方面,使用位图上下文绝对是可取的 - 当然,唯一的问题是上面的问题(kCGBlendModeClear在我将位图上下文blit到main {CGContext之后无效1}}在drawRect)。

3 个答案:

答案 0 :(得分:8)

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

- (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
  • 在擦除背景图像上绘制“擦除”路径

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

答案 1 :(得分:2)

绘画范式之后的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

答案 2 :(得分:1)

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