11

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

CGContextSetBlendMode(context, kCGBlendModeClear)

如果您在 Google 上搜索如何使用 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. 然后最后将屏幕外缓冲区blit到视图的CGContext

这太可怕了,就性能而言。使用 Instrument 的 Time 工具,很明显这种方法的问题在哪里:

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

很自然,代码在真正的 iPad 上执行得非常糟糕。

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

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


附加信息

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

但是,我对这种方法能否成功并不抱太大希望。drawRect在(甚至使用)中绘制图层setNeedsDisplayInRect是非常糟糕的;Core Graphics 正在阻塞将渲染层中的每个路径CGContextDrawLayerAtPoint(根据 Instruments)。据我所知,就性能而言,在这里使用位图上下文绝对是更可取的 - 当然,唯一的问题是上述问题(kCGBlendModeClear在我将位图上下文blit到main CGContextin之后不起作用drawRect)。

4

3 回答 3

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调用中:

  • Blitting 擦除背景图像到上下文
  • 在擦除背景图像的顶部绘制“擦除”路径,使用kCGBlendModeClear

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

于 2013-11-11T22:55:12.903 回答
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
于 2013-11-11T17:28:10.787 回答
1

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

于 2016-04-13T07:26:26.420 回答