views:

332

answers:

2

Hi,

I'm still a little fresh to CoreGraphics programming, so please bear with me. I'm trying to write an application, which allows the user to rub stuff off an image with the finger. I have the basic functionality nailed down, but the result is sluggish since the screen is redrawn completely every time a touch is rendered. I did some research and found out that I can refresh only a portion of the screen using UIView's setNeedsDisplayInRect: method.

This does call drawRect: as expected, however everything I draw in the drawRect: following the setNeedsDisplayInRect: is ignored. Instead, the area in the rect parameter is simply filled with white. No matter what I draw inside, all I end up with is a white rectangle.

In essence, this is what I do:

1) when user touches screen, this touch is rendered into a mask 2) when the drawRect: is called, the image is masked with that mask

There must be something simple I'm overlooking, surely?

+1  A: 

Bump

In order to clarifiy things more, I'm pasting some code in here. This is how i capture finger movement:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.lastTouch = [[event touchesForView:self] anyObject];
    [self renderTouch:self.lastTouch];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{  
    self.lastTouch = [[event touchesForView:self] anyObject];
    [self renderTouch:self.lastTouch];
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.lastTouch = nil;

    if ( m_updateRect.size.width > 0 )
    {
        [self setNeedsDisplay];
    }
}

The method renderTouch: renders the fingerpainting into a mask:

- (void) renderTouch:(UITouch *)touch
{
    // ... render in mask here into m_maskContext

    // this works !!
    //  m_updateRect = CGRectUnion( m_updateRect, [self updateRectFromTouch:touch] );

    // this does not work ?!

    if ( m_updateRect.size.width == 0 )
    {
        m_updateRect = [self updateRectFromTouch:touch];
    }
    else 
    {
        m_updateRect = CGRectUnion( m_updateRect, [self updateRectFromTouch:touch] );
    }
}   

While the drawing is in progress, a timer periodically calls this method:

- (void) refreshScreen
{
    if ( m_updateRect.size.width > 0 )
    {
        [self setNeedsDisplayInRect:m_updateRect];

        m_updateRect = CGRectZero;
    }
}

Which only calls setNeedsDisplayInRect: if there are any updates. In drawRect: finally, I check if I'm drawing only a portion of the screen or the whole thing:

- (void)drawRect:(CGRect)rect 
{
    // Drawing code

    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetBlendMode ( context, kCGBlendModeNormal );

    // Turn coordinate system around
    CGContextTranslateCTM( context, 0.0, rect.size.height );

    CGContextScaleCTM( context, 1.0, -1.0 );

    BOOL drawFullScreen = rectIsEqualTo( rect, self.frame );

    // Take masked image and render it

    self.maskImage = CGBitmapContextCreateImage( m_maskContext );

    // Use that to mask the ice image
    CGImageRef maskedImage = CGImageCreateWithMask( self.image, self.maskImage );

    if ( drawFullScreen )
    {
        // Draw the masked image into the context
        CGContextDrawImage( context, self.frame, maskedImage );
    }
    else
    {
        // copy the portion from image to current context
        CGImageRef partialImage = CGImageCreateWithImageInRect( maskedImage, rect );

        CGContextDrawImage( context, rect, partialImage );

        CGImageRelease( partialImage );
    }

    CGImageRelease( maskedImage );
}

Now, the thing that stuns me rigid is this: in the method renderTouch: there are two ways of updating the rectangle in need of updating. This method works:

m_updateRect = CGRectUnion( m_updateRect, [self updateRectFromTouch:touch] );

However, this is not correct. Since m_updateRect starts out as (0,0,0,0), this always begins at (0,0). What should happen is, that only the areas are coalesced for updated, that the user touched, like so:

if ( m_updateRect.size.width == 0 )
{
    m_updateRect = [self updateRectFromTouch:touch];
}
else 
{
    m_updateRect = CGRectUnion( m_updateRect, [self updateRectFromTouch:touch] );
}

But all this does, is draw little white rectangles! Does anyone have an idea what the heck is going on?

Ion Tichy
+1  A: 

I found a solution, however it still escapes me how exactly this works. Here's the code:

This method flips the given rectangle in the same manner, in which the coordinate transformation in the context flips the context coordinate system:

- (CGRect) flippedRect:(CGRect)rect
{
    CGRect flippedRect = rect;

    flippedRect.origin.y = self.bounds.size.height - rect.origin.y - rect.size.height;

    return CGRectIntersection( self.bounds, flippedRect );
}

This calculates the rectangle to be updated from the touch location. Note that the rectangle gets flipped:

- (CGRect) updateRectFromTouch:(UITouch *)touch
{
    CGPoint location = [touch locationInView:self];

    int d = RubbingSize;

    CGRect touchRect = [self flippedRect:CGRectMake( location.x - d, location.y - d, 2*d, 2*d )];

    return CGRectIntersection( self.frame, touchRect );
}

In render touch the 'flipped' update rectangles are coerced:

- (void) renderTouch:(UITouch *)touch
{
    //
    // Code to render into the mask here
    //

    if ( m_updateRect.size.width == 0 )
    {
        m_updateRect = [self updateRectFromTouch:touch];
    }
    else 
    {
        m_updateRect = CGRectUnion( m_updateRect, [self updateRectFromTouch:touch] );
    }
}   

The whole view is refreshed with about 20Hz during the fingerpainting process. The following method is called every 1/20th second and submits the rectangle for rendering:

- (void) refreshScreen
{
    if ( m_updateRect.size.width > 0 )
    {
        [self setNeedsDisplayInRect:[self flippedRect:m_updateRect]];
    }
}

Here's a helper method to compare to rectangles:

BOOL rectIsEqualTo(CGRect a, CGRect b)
{
    return a.origin.x == b.origin.x && a.origin.y == b.origin.y && a.size.width == b.size.width && a.size.height == b.size.height;
}

In the drawRect: method, the update rectangle is used to draw only the portion that needs updating.

- (void)drawRect:(CGRect)rect 
{
    BOOL drawFullScreen = rectIsEqualTo( rect, self.frame );

    // Drawing code

    CGContextRef context = UIGraphicsGetCurrentContext();

    // Turn coordinate system around

    CGContextTranslateCTM( context, 0.0, self.frame.size.height );

    CGContextScaleCTM( context, 1.0, -1.0 );

    if ( drawFullScreen )
    {
        // draw the full thing
        CGContextDrawImage( context, self.frame, self.image );
    }
    else 
    {       
        CGImageRef partialImage = CGImageCreateWithImageInRect( self.image, [self flippedRect:m_updateRect] );

        CGContextDrawImage( context, m_updateRect, partialPhoto );

        CGImageRelease( partialImage );
    }

        ...

    // Reset update box

    m_updateRect = CGRectZero;
}

If someone can explain to me why the flipping works, I'd appreciate it.

Ion Tichy