CoreGraphics: Using finger strokes to erase part of image? - iphone

I'm working on drawing code to erase part of an image. I'm not an expert on CoreGraphics and could use some help.
This routine works fine, however, when moving fast, it loses touches (Not very smooth). Can this routine be modified to make CGContextClearRect smoother? Is there a better, faster way to do this?
-(void)drawRect:(CGRect)rect {
if (!myDrawing) { // touchpoints stored here
myDrawing = [[NSMutableArray alloc] initWithCapacity:0];
}
UIGraphicsBeginImageContext(frontImage.frame.size);
[frontImage.image drawInRect:CGRectMake(0, 0, frontImage.frame.size.width, frontImage.frame.size.height)];
if ([myDrawing count] > 0) {
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 5);
CGContextSetLineCap(UIGraphicsGetCurrentContext(),kCGImageAlphaNone );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1, 0, 0, 10);
for (int i = 0 ; i < [myDrawing count] ; i++) {
NSArray *thisArray = [myDrawing objectAtIndex:i];
if ([thisArray count] > 2) {
float thisX = [[thisArray objectAtIndex:0] floatValue];
float thisY = [[thisArray objectAtIndex:1] floatValue];
CGContextBeginPath(UIGraphicsGetCurrentContext());
for (int j = 2; j < [thisArray count] ; j+=2) {
thisX = [[thisArray objectAtIndex:j] floatValue];
thisY = [[thisArray objectAtIndex:j+1] floatValue];
CGContextClearRect (UIGraphicsGetCurrentContext(), CGRectMake(thisX, thisY, 10, 10));
}
}
}
}
frontImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}

You don't have to use CGContextClearRect to clear strokes.
Instead do CGContextSetBlendMode(context, kCGBlendModeClear)
This call changes the color blending mode in such a way that drawing operations would be clearing bitmap instead of drawing with color.
Then you can just draw lines which connect touch locations so that there are no gaps.
To switch back to normal rendering do CGContextSetBlendMode(context, kCGBlendModeNormal)
Using different blending modes can be very helpful.

You should never do anything but drawing in your drawRect code. That really slows down the process. Instead, think of perhaps splitting off rendering to a separate thread. That will really speed things up.

Related

How to draw to context without losing it?

I am drawing UIImages to/from a grid. I am currently only drawing the changes... and only the changes... but the old images are supposed to stay where they are... however right now they vanish after the next call of drawRect:(CGRect)rect:
- (void)drawRect:(CGRect)rect
{
int cellSize = self.bounds.size.width / WIDTH;
double xOffset = 0;
CGRect cellFrame = CGRectMake(0, 0, cellSize, cellSize);
NSUInteger cellIndex = 0;
cellFrame.origin.x = xOffset;
for (int i = 0; i < WIDTH; i++)
{
cellFrame.origin.y = 0;
for (int j = 0; j < HEIGHT; j++, cellIndex++)
{
if([[self.state.boardChanges objectAtIndex:(i*HEIGHT)+j] intValue]==1){
if (CGRectIntersectsRect(rect, cellFrame)) {
NSNumber *currentCell = [self.state.board objectAtIndex:cellIndex];
if (currentCell.intValue == 1)
{
[image1 drawInRect:cellFrame];
}
else if (currentCell.intValue == 0)
{
[image2 drawInRect:cellFrame];
}
}
}
cellFrame.origin.y += cellSize;
}
cellFrame.origin.x += cellSize;
}
}
I tried mixtures of the following without any results:
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
//CGContextAddRect(context, originalRect);
CGContextClip(context);
[image drawInRect:rect];
CGContextRestoreGState(context);
Cocoa/UIKit may discard the previously-drawn content of views whenever it likes. When your view is asked to draw, it must draw everything within the provided rect. You can't just decide to not draw some stuff. (Well, you can do whatever you like, but you get the results that you're seeing.)
Found another solution-> Just take a screenshot and draw it with drawRect as a background.
-(void)createContent{
CGRect rect = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
UIGraphicsBeginImageContext(rect.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
CGImageRef screenshotRef = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
_backgroundImage = [[UIImage alloc] initWithCGImage:screenshotRef];
CGImageRelease(screenshotRef);
UIGraphicsEndImageContext();
}
- (void)drawRect:(CGRect)rect
{
[_backgroundImage drawInRect:CGRectMake(0.0f, 0.0f, self.bounds.size.width, self.bounds.size.height)];
....
....
....
}
######EDIT
Found a much more sophisticated way to do the above :
http://www.cimgf.com/2009/02/03/record-your-core-animation-animation/
#### EDIT
Another solution would be to use CALayers and only update those parts that change...
####EDIT
OR:
self.layer.contents = (id) [_previousContent CGImage];

Drawing animation

I'm creating a simple app where when the user presses a button, a series of lines will be drawn on the screen and the user will be able to see these lines drawn in real time (almost like an animation).
My code looks something like this (has been simplified):
UIGraphicsBeginImageContext(CGSizeMake(300,300));
CGContextRef context = UIGraphicsGetCurrentContext();
for (int i = 0; i < 100; i++ ) {
CGContextMoveToPoint(context, i, i);
CGContextAddLineToPoint(context, i+20, i+20);
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextStrokePath(context);
}
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
My problem is that:
1) As soon as the user presses the button, the UIThread blocks until the drawing is done.
2) I can't get the lines to be drawn on the screen one at a time - I've tried setting the UIImage directly inside the loop and also tried setting a layer content inside the loop.
How do I get around these problems?
You say "just like an animation". Why not do an actual animation, a la Core Graphics' CABasicAnimation? Do you really need to show it as a series of lines, or is a proper animation ok?
If you want to animate the actual drawing of the line, you could do something like:
#import <QuartzCore/QuartzCore.h>
- (void)drawBezierAnimate:(BOOL)animate
{
UIBezierPath *bezierPath = [self bezierPath];
CAShapeLayer *bezier = [[CAShapeLayer alloc] init];
bezier.path = bezierPath.CGPath;
bezier.strokeColor = [UIColor blueColor].CGColor;
bezier.fillColor = [UIColor clearColor].CGColor;
bezier.lineWidth = 5.0;
bezier.strokeStart = 0.0;
bezier.strokeEnd = 1.0;
[self.view.layer addSublayer:bezier];
if (animate)
{
CABasicAnimation *animateStrokeEnd = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animateStrokeEnd.duration = 10.0;
animateStrokeEnd.fromValue = [NSNumber numberWithFloat:0.0f];
animateStrokeEnd.toValue = [NSNumber numberWithFloat:1.0f];
[bezier addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
}
}
Then all you have to do is create the UIBezierPath for your line, e.g.:
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0.0, 0.0)];
[path addLineToPoint:CGPointMake(200.0, 200.0)];
return path;
}
If you want, you can patch a bunch of lines together into a single path, e.g. here is a roughly sine curve shaped series of lines:
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point = self.view.center;
[path moveToPoint:CGPointMake(0, self.view.frame.size.height / 2.0)];
for (CGFloat f = 0.0; f < M_PI * 2; f += 0.75)
{
point = CGPointMake(f / (M_PI * 2) * self.view.frame.size.width, sinf(f) * 200.0 + self.view.frame.size.height / 2.0);
[path addLineToPoint:point];
}
return path;
}
And these don't block the main thread.
By the way, you'll obviously have to add the CoreGraphics.framework to your target's Build Settings under Link Binary With Libraries.

show image in a CGContextRef

What i am doing, i downloded a code for calender now i want to show images on its tiles(for date).
What i am trying shows in code
- (void)drawTextInContext:(CGContextRef)ctx
{
CGContextSaveGState(ctx);
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
CGFloat numberFontSize = floorf(0.3f * width);
CGContextSetFillColorWithColor(ctx, kDarkCharcoalColor);
CGContextSetTextDrawingMode(ctx, kCGTextClip);
for (NSInteger i = 0; i < [self.text length]; i++) {
NSString *letter = [self.text substringWithRange:NSMakeRange(i, 1)];
CGSize letterSize = [letter sizeWithFont:[UIFont boldSystemFontOfSize:numberFontSize]];
CGContextSaveGState(ctx); // I will need to undo this clip after the letter's gradient has been drawn
[letter drawAtPoint:CGPointMake(4.0f+(letterSize.width*i), 0.0f) withFont:[UIFont boldSystemFontOfSize:numberFontSize]];
if ([self.date isToday]) {
CGContextSetFillColorWithColor(ctx, kWhiteColor);
CGContextFillRect(ctx, self.bounds);
} else {
// CGContextDrawLinearGradient(ctx, TextFillGradient, CGPointMake(0,0), CGPointMake(0, height/3), kCGGradientDrawsAfterEndLocation);
CGDataProviderRef dataProvider = CGDataProviderCreateWithFilename("left-arrow.png");
CGImageRef image = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault);
//UIImage* image = [UIImage imageNamed:#"left-arrow.png"];
//CGImageRef imageRef = image.CGImage;
CGContextDrawImage(ctx, CGRectMake(8.0f+(letterSize.width*i), 0.0f, 5, 5), image);
//im.image=[UIImage imageNamed:#"left-arrow.png"];
}
CGContextRestoreGState(ctx); // get rid of the clip for the current letter
}
CGContextRestoreGState(ctx);
}
In else condition i want to show images on the tile so for that i am converting image objects in the CGImageRef.
Please help me.
I am not sure this would be done in same manner or in other manner please suggest your way to do this.
Thanx a lot.
The file-path of the image seems to problematic. You can retrieve the correct path with the NSBundle-methods. Also you're leaking a lot of memory, because you don't release your images and data-providers. To make a long story short, try this:
[[UIImage imageNamed:#"left-arrow.png"] drawInRect:...]
or even simpler:
[[UIImage imageNamed:#"left-arrow.png"] drawAtPoint:...]

CoreGraphics Multi-Colored Line

I have the following code the only seems to use the last colour for the whole line.....
I want the colour to be changing throughout this. Any ideas?
CGContextSetLineWidth(ctx, 1.0);
for(int idx = 0; idx < routeGrabInstance.points.count; idx++)
{
CLLocation* location = [routeGrabInstance.points objectAtIndex:idx];
CGPoint point = [mapView convertCoordinate:location.coordinate toPointToView:self.mapView];
if(idx == 0)
{
// move to the first point
UIColor *tempColor = [self colorForHex:[[routeGrabInstance.pointHeights objectAtIndex:idx] doubleValue]];
CGContextSetStrokeColorWithColor(ctx,tempColor.CGColor);
CGContextMoveToPoint(ctx, point.x, point.y);
}
else
{
UIColor *tempColor = [self colorForHex:[[routeGrabInstance.pointHeights objectAtIndex:idx] doubleValue]];
CGContextSetStrokeColorWithColor(ctx,tempColor.CGColor);
CGContextAddLineToPoint(ctx, point.x, point.y);
}
}
CGContextStrokePath(ctx);
The CGContextSetStrokeColorWithColor only changes the state of the context, it does not do any drawing. The only drawing done in your code is by the CGContextStrokePath at the end. Since each call to CGContextSetStrokeColorWithColor overrides the value set by the previous call the drawing will use the last color set.
You need to create a new path, set the color and then draw in each loop. Something like this:
for(int idx = 0; idx < routeGrabInstance.points.count; idx++)
{
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, x1, y1);
CGContextAddLineToPoint(ctx, x2, y2);
CGContextSetStrokeColorWithColor(ctx,tempColor.CGColor);
CGContextStrokePath(ctx);
}
CGContextSetStrokeColorWithColor sets the stroke color in the context. That color is used when you stroke the path, it has no effect as you continue building the path.
You need to stroke each line separately (CGContextStrokePath).

Drawing in CATiledLayer with CoreGraphics CGContextDrawImage

I would like to use a CATiledLayer in iPhone OS 3.1.3 and to do so all drawing in -(void)drawLayer:(CALayer *)layer inContext:(CGContext)context has to be done with coregraphics only.
Now I run into the problems of the flipped coordinate system on the iPhone and there are some suggestions how to fix it using transforms:
Image is drawn upside down
CATiledLayer or CALayer not working
My problem is that I cannot get it to work. I started using the PhotoScroller sample code and replacing the drawing method with coregraphics calls only. It looks like this
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
CGContextSaveGState(context);
CGRect rect = CGContextGetClipBoundingBox(context);
CGFloat scale = CGContextGetCTM(context).a;
CGContextConcatCTM(context, CGAffineTransformMakeTranslation(0.f, rect.size.height));
CGContextConcatCTM(context, CGAffineTransformMakeScale(1.f, -1.f));
CATiledLayer *tiledLayer = (CATiledLayer *)layer;
CGSize tileSize = tiledLayer.tileSize;
tileSize.width /= scale;
tileSize.height /= scale;
// calculate the rows and columns of tiles that intersect the rect we have been asked to draw
int firstCol = floorf(CGRectGetMinX(rect) / tileSize.width);
int lastCol = floorf((CGRectGetMaxX(rect)-1) / tileSize.width);
int firstRow = floorf(CGRectGetMinY(rect) / tileSize.height);
int lastRow = floorf((CGRectGetMaxY(rect)-1) / tileSize.height);
for (int row = firstRow; row <= lastRow; row++) {
for (int col = firstCol; col <= lastCol; col++) {
// if (row == 0 ) continue;
UIImage *tile = [self tileForScale:scale row:row col:col];
CGImageRef tileRef = [tile CGImage];
CGRect tileRect = CGRectMake(tileSize.width * col, tileSize.height * row,
tileSize.width, tileSize.height);
// if the tile would stick outside of our bounds, we need to truncate it so as to avoid
// stretching out the partial tiles at the right and bottom edges
tileRect = CGRectIntersection(self.bounds, tileRect);
NSLog(#"row:%d, col:%d, x:%.0f y:%.0f, height:%.0f, width:%.0f", row, col,tileRect.origin.x, tileRect.origin.y, tileRect.size.height, tileRect.size.width);
//[tile drawInRect:tileRect];
CGContextDrawImage(context, tileRect, tileRef);
// just to draw the row and column index in the tile and mark the origin of the tile with a red line
if (self.annotates) {
CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor]CGColor]);
CGContextSetLineWidth(context, 6.0 / scale);
CGContextStrokeRect(context, tileRect);
CGContextSetStrokeColorWithColor(context, [[UIColor redColor]CGColor]);
CGContextMoveToPoint(context, tileRect.origin.x, tileRect.origin.y);
CGContextAddLineToPoint(context, tileRect.origin.x+100.f, tileRect.origin.y+100.f);
CGContextStrokePath(context);
CGContextSetStrokeColorWithColor(context, [[UIColor redColor]CGColor]);
CGContextSetFillColorWithColor(context, [[UIColor whiteColor]CGColor]);
CGContextSelectFont(context, "Courier", 128, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetShouldAntialias(context, true);
char text[30];
int length = sprintf(text,"row:%d col:%d",row,col);
CGContextSaveGState(context);
CGContextShowTextAtPoint(context, tileRect.origin.x+110.f,tileRect.origin.y+100.f, text, length);
CGContextRestoreGState(context);
}
}
}
CGContextRestoreGState(context);
}
As you can see I am using a Scale transform to invert the coordinate system and a translation transform to shift the origin to the lower left corner. The images draw correctly but only the first row of tiles is being drawn. I think there is a problem with the translation operation or the way the coordinates of the tiles are computed.
This is how it looks like:
I am a bit confused with all this transformations.
Bonus question:
How would one handle the retina display pictures in core graphics?
EDIT:
To get it working on the retina display I just took the original method from the sample code to provide the images:
- (UIImage *)tileForScale:(CGFloat)scale row:(int)row col:(int)col
{
// we use "imageWithContentsOfFile:" instead of "imageNamed:" here because we don't want UIImage to cache our tiles
NSString *tileName = [NSString stringWithFormat:#"%#_%d_%d_%d", imageName, (int)(scale * 1000), col, row];
NSString *path = [[NSBundle mainBundle] pathForResource:tileName ofType:#"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
return image;
}
In principle the scale of the display is ignored since Core Graphics is working in pixels not points so when asked to draw more pixels, more CATiledLayers (or sublayers) are used to fill the screen.
Thanks alot
Thomas
Thomas, I started by following the WWDC 2010 ScrollView talk and there is little or no documentation on working within drawLayer:inContext for iOS 3.x. I had the same issues as you do when I moved the demo code from drawRect: across to drawLayer:inContext:.
Some investigation showed me that within drawLayer:inContext: the size and offset of rect returned from CGContextGetClipBoundingBox(context) is exactly what you want to draw in. Where drawRect: gives you the whole bounds.
Knowing this you can remove the row and column iteration, as well as the CGRect intersection for the edge tiles and just draw to the rect once you've translated the context.
Here's what I've ended up with:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CGRect rect = CGContextGetClipBoundingBox(ctx);
CGFloat scale = CGContextGetCTM(ctx).a;
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
CGSize tileSize = tiledLayer.tileSize;
tileSize.width /= scale;
tileSize.height /= scale;
int col = floorf((CGRectGetMaxX(rect)-1) / tileSize.width);
int row = floorf((CGRectGetMaxY(rect)-1) / tileSize.height);
CGImageRef image = [self imageForScale:scale row:row col:col];
if(NULL != image) {
CGContextTranslateCTM(ctx, 0.0, rect.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
rect = CGContextGetClipBoundingBox(ctx);
CGContextDrawImage(ctx, rect, image);
CGImageRelease(image);
}
}
Notice that rect is redefined after the TranslateCTM and ScaleCTM.
And for reference here is my imageForScale:row:col function:
- (CGImageRef) imageForScale:(CGFloat)scale row:(int)row col:(int)col {
CGImageRef image = NULL;
CGDataProviderRef provider = NULL;
NSString *filename = [NSString stringWithFormat:#"img_name_here%0.0f_%d_%d",ceilf(scale * 100),col,row];
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:#"png"];
if(path != nil) {
NSURL *imageURL = [NSURL fileURLWithPath:path];
provider = CGDataProviderCreateWithURL((CFURLRef)imageURL);
image = CGImageCreateWithPNGDataProvider(provider,NULL,FALSE,kCGRenderingIntentDefault);
CFRelease(provider);
}
return image;
}
There's still a bit of work to be done on these two functions in order to support high resolution graphics properly, but it does look nice on everything but an iPhone 4.