简体   繁体   English

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

[英]CGContext: how do I erase pixels (e.g. kCGBlendModeClear) outside of a bitmap context?

I'm trying to build an eraser tool using Core Graphics, and I'm finding it incredibly difficult to make a performant eraser - it all comes down to: 我正在尝试使用Core Graphics构建一个橡皮擦工具,我发现制作高性能橡皮擦非常困难 - 这一切都归结为:

CGContextSetBlendMode(context, kCGBlendModeClear)

If you google around for how to "erase" with Core Graphics, almost every answer comes back with that snippet. 如果你四处搜索如何使用Core Graphics“擦除”,那么几乎每个答案都会带回来。 The problem is it only (apparently) works in a bitmap context. 问题是它(仅显然)在位图上下文中起作用。 If you're trying to implement interactive erasing, I don't see how kCGBlendModeClear helps you - as far as I can tell, you're more or less locked into erasing on and off-screen UIImage / CGImage and drawing that image in the famously non-performant [UIView drawRect] . 如果您正在尝试实现交互式擦除,我不会看到kCGBlendModeClear如何帮助您 - 据我所知,您或多或少地锁定在屏幕上和屏幕外的UIImage / CGImage并在着名的非执行[UIView drawRect]

Here's the best I've been able to do: 这是我能做的最好的:

-(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];
    }
}

Drawing a normal line ( !eraseModeOn ) is acceptably performant; 绘制法线( !eraseModeOn )是可以接受的; I'm blitting my off-screen drawing buffer ( curImage , which contains all previously drawn strokes) to the current CGContext , and I'm rendering the line (path) being currently drawn. 我正在将我的屏幕外绘图缓冲区( curImage ,其中包含所有先前绘制的笔划)与当前的CGContext ,并且我正在渲染当前绘制的线(路径)。 It's not perfect, but hey, it works, and it's reasonably performant. 它并不完美,但嘿,它有效,并且它的性能相当合理。

However, because kCGBlendModeNormal apparently does not work outside of a bitmap context, I'm forced to: 但是,因为kCGBlendModeNormal显然不能kCGBlendModeNormal图上下文之外工作,所以我不得不:

  1. Create a bitmap context ( UIGraphicsBeginImageContextWithOptions ). 创建位图上下文( UIGraphicsBeginImageContextWithOptions )。
  2. Draw my offscreen buffer ( eraseImage , which is actually derived from curImage when the eraser tool is turned on - so really pretty much the same as curImage for arguments sake). 绘制我的屏幕外缓冲区( eraseImage ,实际上是在打开橡皮擦工具时从curImage派生的 - 所以与参数的curImage实际上非常相似)。
  3. Render the "erase line" (path) currently being drawn to the bitmap context (using kCGBlendModeClear to clear pixels). 渲染当前正在绘制到位图上下文的“擦除线”(路径)(使用kCGBlendModeClear清除像素)。
  4. Extract the entire image into the offscreen buffer ( curImage = UIGraphicsGetImageFromCurrentImageContext(); ) 将整个图像提取到屏幕外缓冲区( curImage = UIGraphicsGetImageFromCurrentImageContext();
  5. And then finally blit the offscreen buffer to the view's CGContext 然后最终将屏幕外缓冲区blit到视图的CGContext

That's horrible, performance-wise. 这很糟糕,表现明智。 Using Instrument's Time tool, it's painfully obvious where the problems with this method are: 使用Instrument的Time工具,很明显这个方法的问题在于:

  • UIGraphicsBeginImageContextWithOptions is expensive UIGraphicsBeginImageContextWithOptions很昂贵
  • Drawing the offscreen buffer twice is expensive 两次绘制屏幕外缓冲区是很昂贵的
  • Extracting the entire image into an offscreen buffer is expensive 将整个图像提取到屏幕外缓冲区是很昂贵的

So naturally, the code performs horribly on a real iPad. 很自然地,代码在真正的iPad上表现得非常糟糕。

I'm not really sure what to do here. 我不确定该怎么做。 I've been trying to figure out how to clear pixels in a non-bitmap context, but as far as I can tell, relying on kCGBlendModeClear is a dead-end. 我一直试图弄清楚如何在非位图上下文中清除像素,但据我所知,依赖于kCGBlendModeClear是一个死胡同。

Any thoughts or suggestions? 有什么想法或建议吗? How do other iOS drawing apps handle erase? 其他iOS绘图应用程序如何处理擦除?


Additional Info 附加信息

I've been playing around with a CGLayer approach, as it does appear that CGContextSetBlendMode(context, kCGBlendModeClear) will work in a CGLayer based on a bit of googling I've done. 我一直在使用CGLayer方法,因为看起来CGContextSetBlendMode(context, kCGBlendModeClear)将基于我已经完成的一些谷歌搜索在CGLayer工作。

However, I'm not super hopeful that this approach will pan out. 但是,我并不希望这种方法能够成功。 Drawing the layer in drawRect (even using setNeedsDisplayInRect ) is hugely non-performant; drawRect绘制图层(甚至使用setNeedsDisplayInRect )是非常不具有效果的; Core Graphics is choking up will rendering each path in the layer in CGContextDrawLayerAtPoint (according to Instruments). 令人窒息的核心图形将渲染CGContextDrawLayerAtPoint中的每个路径(根据Instruments)。 As far as I can tell, using a bitmap context is definitely preferable here in terms of performance - the only problem, of course, being the above question ( kCGBlendModeClear not working after I blit the bitmap context to the main CGContext in drawRect ). 据我所知,在性能方面,使用位图上下文绝对是可取的 - 当然,唯一的问题是上述问题( kCGBlendModeClear在我将位图上下文blit到drawRect的主CGContextdrawRect )。

I've managed to get good results by using the following code: 我通过使用以下代码设法获得了良好的结果:

- (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;
}

The trick was to wrap the following into CGContextBeginTransparencyLayer / CGContextEndTransparencyLayer calls: 诀窍是将以下内容包装到CGContextBeginTransparencyLayer / CGContextEndTransparencyLayer调用中:

  • Blitting the erase background image to the context 将擦除背景图像模糊到上下文
  • Drawing the "erase" path on top of the erase background image, using kCGBlendModeClear 使用kCGBlendModeClear在擦除背景图像上绘制“擦除”路径

Since both the erase background image's pixel data and the erase path are in the same layer, it has the effect of clearing the pixels. 由于擦除背景图像的像素数据和擦除路径都在同一层中,因此它具有清除像素的效果。

2D graphics following painting paradigms. 绘画范式之后的2D图形。 When you are painting, it's hard to remove paint you've already put on the canvas, but super easy to add more paint on top. 当你画画时,很难去除你已经放在画布上的油漆,但是很容易在上面添加更多的油漆。 The blend modes with a bitmap context give you a way to do something hard (scrape paint off the canvas) with few lines of code. 具有位图上下文的混合模式为您提供了一种方法,可以用很少的代码行来做一些难的事情(刮掉画布上的油漆)。 The few lines of code do not make it an easy computing operation (which is why it performs slowly). 几行代码不能使它成为一个简单的计算操作(这就是它执行缓慢的原因)。

The easiest way to fake clearing out pixels without having to do the offscreen bitmap buffering is to paint the background of your view over the image. 伪造清除像素而不必进行屏幕外位图缓冲的最简单方法是在图像上绘制视图的背景。

-(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];
    }
}

The more difficult (but more correct) way is to do the image editing on a background serial queue in response to an editing event. 更困难(但更正确)的方法是在后台串行队列上进行图像编辑以响应编辑事件。 When you get a new action, you do the bitmap rendering in the background to an image buffer. 当您获得新动作时,可以在后台将位图渲染到图像缓冲区。 When the buffered image is ready, you call setNeedsDisplay to allow the view to be redrawn during the next update cycle. 当缓冲的映像准备就绪时,您调用setNeedsDisplay以允许在下一个更新周期中重绘视图。 This is more correct as drawRect: should be displaying the content of your view as quickly as possible, not processing the editing action. 这更正确,因为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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何在一个快速的字符串中写斜杠并一起引用(例如\\“)? - How do I write slash and quote together (e.g. \") inside a swift string? 如何通过深层链接重定向到Apple App Store? (例如查询字符串) - How do I redirect to the Apple App Store with a deep link? (e.g. a query string) 如何创建过去的新NSDate对象,例如比另一个日期提前1秒? - How do I create a new NSDate object that is in the past, e.g. 1 second earlier than another date? ios驱动器SDK | 如何检查每张票,然后将其取消? 例如,如果它的标签= 1,我需要取消它 - ios drive sdk | How do I check each ticket, then cancel it? e.g. if it has tag = 1, I need to cancel it 如何在 CGContext 中擦除自定义形状(不是矩形) - How to erase a custom shape ( not A rectangle ) in CGContext 如何在cgcontext中轻轻擦除绘制的路径? - How to LIGHTLY erase drawn path in cgcontext? 如何检查CGContext是否包含点? - How do I check if CGContext contains point? 如何在Objective-C中设置PDF打印对话框预设(例如DuplexMode)? - How do you set the PDF Print Dialog Presets (e.g. DuplexMode) in objective-c? Swift 5.1不透明结果类型如何与旧版操作系统(例如iOS 12)进行交互 - How do Swift 5.1 Opaque Result Types interact with older OSs (e.g. iOS 12) 我最好如何在UIPageViewController的底部放置一个数字页面指示器? (例如:12之6) - How would I best go about having a numeric page indicator at the bottom of a UIPageViewController? (e.g.: 6 of 12)
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM