drawRect Optimization - objective-c

I am working on an application that allows the user to select an image (camera or gallery), then draw on that image with their finger. The area that they draw becomes the transparent portion of a mask. A second image is then drawn below the first image.
I've been working on improving performance, especially on the iPad 3 and I seem to have hit a wall. I am new to iOS and Objective-C.
So my question is: What can I do to improve the drawing performance of my application?
I used this tutorial as a starting point for my code.
First I draw to a cache context:
- (void) drawToCache {
CGRect dirtyPoint1;
CGRect dirtyPoint2;
UIColor *color;
if (point1.x > -1){
hasDrawn = YES;
maskContext = CGLayerGetContext(maskLayer);
blackBackgroundContext = CGLayerGetContext(blackBackgroundLayer);
if (!doUndo){
color = [UIColor whiteColor];
CGContextSetBlendMode(maskContext, kCGBlendModeColor);
}
else{
color = [UIColor clearColor];
CGContextSetBlendMode(maskContext, kCGBlendModeClear);
}
CGContextSetShouldAntialias(maskContext, YES);
CGContextSetRGBFillColor(blackBackgroundContext, 0.0, 0.0, 0.0, 1.0);
CGContextFillRect(blackBackgroundContext, self.bounds);
CGContextSetStrokeColorWithColor(maskContext, [color CGColor]);
CGContextSetLineCap(maskContext, kCGLineCapRound);
CGContextSetLineWidth(maskContext, brushSize);
double x0 = (point0.x > -1) ? point0.x : point1.x; //after 4 touches we should have a back anchor point, if not, use the current anchor point
double y0 = (point0.y > -1) ? point0.y : point1.y; //after 4 touches we should have a back anchor point, if not, use the current anchor point
double x1 = point1.x;
double y1 = point1.y;
double x2 = point2.x;
double y2 = point2.y;
double x3 = point3.x;
double y3 = point3.y;
double xc1 = (x0 + x1) / 2.0;
double yc1 = (y0 + y1) / 2.0;
double xc2 = (x1 + x2) / 2.0;
double yc2 = (y1 + y2) / 2.0;
double xc3 = (x2 + x3) / 2.0;
double yc3 = (y2 + y3) / 2.0;
double len1 = sqrt((x1-x0) * (x1-x0) + (y1-y0) * (y1-y0));
double len2 = sqrt((x2-x1) * (x2-x1) + (y2-y1) * (y2-y1));
double len3 = sqrt((x3-x2) * (x3-x2) + (y3-y2) * (y3-y2));
double k1 = len1 / (len1 + len2);
double k2 = len2 / (len2 + len3);
double xm1 = xc1 + (xc2 - xc1) * k1;
double ym1 = yc1 + (yc2 - yc1) * k1;
double xm2 = xc2 + (xc3 - xc2) * k2;
double ym2 = yc2 + (yc3 - yc2) * k2;
double smooth_value = 0.5;
float ctrl1_x = xm1 + (xc2 - xm1) * smooth_value + x1 - xm1;
float ctrl1_y = ym1 + (yc2 - ym1) * smooth_value + y1 - ym1;
float ctrl2_x = xm2 + (xc2 - xm2) * smooth_value + x2 - xm2;
float ctrl2_y = ym2 + (yc2 - ym2) * smooth_value + y2 - ym2;
CGContextMoveToPoint(maskContext, point1.x, point1.y);
CGContextAddCurveToPoint(maskContext, ctrl1_x, ctrl1_y, ctrl2_x, ctrl2_y, point2.x, point2.y);
CGContextStrokePath(maskContext);
dirtyPoint1 = CGRectMake(point1.x-(brushSize/2), point1.y-(brushSize/2), brushSize, brushSize);
dirtyPoint2 = CGRectMake(point2.x-(brushSize/2), point2.y-(brushSize/2), brushSize, brushSize);
[self setNeedsDisplayInRect:CGRectUnion(dirtyPoint1, dirtyPoint2)];
}
}
And my drawRect:
- (void)drawRect:(CGRect)rect {
CGRect imageRect = CGRectMake(0, 0, 1024, 668);
CGSize size = CGSizeMake(1024, 668);
//Draw the user touches to the context, this creates a black and white image to convert into a mask
UIGraphicsBeginImageContext(size);
CGContextSetShouldAntialias(UIGraphicsGetCurrentContext(), YES);
CGContextDrawLayerInRect(UIGraphicsGetCurrentContext(), imageRect, blackBackgroundLayer);
CGContextDrawLayerInRect(UIGraphicsGetCurrentContext(), imageRect, maskLayer);
CGImageRef maskRef = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
//Make the mask
CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef), CGImageGetHeight(maskRef), CGImageGetBitsPerComponent(maskRef), CGImageGetBitsPerPixel(maskRef), CGImageGetBytesPerRow(maskRef), CGImageGetDataProvider(maskRef), NULL, false);
//Mask the user image
CGImageRef masked = CGImageCreateWithMask([self.original CGImage], mask);
UIGraphicsBeginImageContext(size);
CGContextDrawLayerInRect(UIGraphicsGetCurrentContext(), imageRect, backgroundTextureLayer);
CGImageRef background = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
//Flip the context so everything is right side up
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 0, 668);
CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1.0, -1.0);
//Draw the background on the context
CGContextDrawImage(UIGraphicsGetCurrentContext(), imageRect, background);
//Draws the mask on the context
CGContextDrawImage(UIGraphicsGetCurrentContext(), imageRect, masked);
//Release everything to prevent memory leaks
CGImageRelease(background);
CGImageRelease(maskRef);
CGImageRelease(mask);
CGImageRelease(masked);
}
I spent yesterday researching these questions:
How can I improve CGContextFillRect and CGContextDrawImage performance
CGContextDrawImage very slow on iPhone 4
Drawing in CATiledLayer with CoreGraphics CGContextDrawImage
iPhone: How do I add layers to UIView's root layer to display an image with the content property?
iPhone CGContextDrawImage and UIImageJPEGRepresentation drastically slowing down application
Any help is appreciated!
Thanks,
Kevin

I can give you some tips but you're going to need to experiment to see where the problems are (using Instruments):
do not assume you need to draw the whole view - when you need to dirty some area, use setNeedsDisplayInRect. Then in the drawRect only update the dirty area.
the above means your maskRef is much smaller and easier to construct
instead of '[self.original CGImage]', create a smaller subregion image using CGImageCreateWithImageInRect(), which lets you select a subsection of an image, then use that smaller image in conjunction with the mask.

Related

Rotate image in bounds

I try to rotate an image view in its superview so that this image view while rotating always touches superview's borders not crossing them, with appropriate resizing. How can I implement this? The image view should be able to rotate around 360˚.
Here I use calculations based on triangle formulas, considering initial image view diagonal angle.
Maybe I should take into account new bounding frame of the image view after it gets rotated (its x and y coordinates get negative and its frame size after transform gets bigger too).
No success so far, my image view gets sized down too quickly and too much. So my goal as I understand to get proper scale factor for CGAffineTransformScale. Maybe there are other ways to do the same.
// set initial values
_planImageView.layer.affineTransform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
_degrees = 0;
_initialWidth = _planImageView.frame.size.width;
_initialHeight = _planImageView.frame.size.height;
_initialAngle = MathUtils::radiansToDegrees(atan((_initialWidth / 2) / (_initialHeight / 2)));
// rotation routine
- (void)rotatePlanWithDegrees:(double)degrees
{
double deltaDegrees = degrees - _degrees;
_initialAngle -= deltaDegrees;
double newAngle = _initialAngle;
double newWidth = (_initialWidth / 2) * tan(MathUtils::degreesToRadians(newAngle)) * 2;
double newHeight = newWidth * (_initialHeight / _initialWidth);
NSLog(#"DEG %f DELTA %f A %f W %f H %f", degrees, deltaDegrees, newAngle, newWidth, newHeight);
double currentScale = newWidth / _initialWidth;
_planImageView.layer.affineTransform = CGAffineTransformScale(CGAffineTransformIdentity, currentScale, currentScale);
_planImageView.layer.affineTransform = CGAffineTransformRotate(_planImageView.layer.affineTransform, (CGFloat) MathUtils::degreesToRadians(degrees));
_degrees = degrees;
self->_planImageView.center = _center;
// NSLog(#"%#", NSStringFromCGRect(_planImageView.frame));
}
EDIT
I overwrote routine thanks to the answer and now it works!
- (void)rotatePlanWithDegrees:(double)degrees
{
double newWidth =
_initialWidth * abs(cos(MathUtils::degreesToRadians(degrees))) +
_initialHeight * abs(sin(MathUtils::degreesToRadians(degrees)));
double newHeight =
_initialWidth * abs(sin(MathUtils::degreesToRadians(degrees))) +
_initialHeight * abs(cos(MathUtils::degreesToRadians(degrees)));
CGFloat scale = (CGFloat) MIN(
self.planImageScrollView.frame.size.width / newWidth,
self.planImageScrollView.frame.size.height / newHeight);
CGAffineTransform rotationTransform = CGAffineTransformMakeRotation((CGFloat) MathUtils::degreesToRadians(degrees));
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scale, scale);
_planImageView.layer.affineTransform = CGAffineTransformConcat(rotationTransform, scaleTransform);
self->_planImageView.center = _center;
}
When you rotate a rectangle W x H, the bounding box takes the dimensions W' = W |cos Θ| + H |sin Θ|, H' = W |sin Θ| + H |cos Θ|.
If you need to fit that in a W" x H" rectangle, the scaling factor is the smallest of W"/W' and H"/H'.

Masking a custom shape with UIImageView

I want to programatically crop a shape over my UIImageView. I know about creating a path with QuartzCore, but I don't understand context. Give me an example by subclassing UIImageView.
So how can I make an image go from this:
To this:
I also need the mask to be transparent
The easiest approach is to
create a UIBezierPath for the hexagon;
create a CAShapeLayer from that path; and
add that CAShapeLayer as a mask to the image view's layer.
Thus, it might look like:
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = [[self polygonPathWithRect:self.imageView.bounds lineWidth:0.0 sides:6] CGPath];
mask.strokeColor = [UIColor clearColor].CGColor;
mask.fillColor = [UIColor whiteColor].CGColor;
self.imageView.layer.mask = mask;
where
/** Create UIBezierPath for regular polygon inside a CGRect
*
* #param square The CGRect of the square in which the path should be created.
* #param lineWidth The width of the stroke around the polygon. The polygon will be inset such that the stroke stays within the above square.
* #param sides How many sides to the polygon (e.g. 6=hexagon; 8=octagon, etc.).
*
* #return UIBezierPath of the resulting polygon path.
*/
- (UIBezierPath *)polygonPathWithRect:(CGRect)square
lineWidth:(CGFloat)lineWidth
sides:(NSInteger)sides
{
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat theta = 2.0 * M_PI / sides; // how much to turn at every corner
CGFloat squareWidth = MIN(square.size.width, square.size.height); // width of the square
// calculate the length of the sides of the polygon
CGFloat length = squareWidth - lineWidth;
if (sides % 4 != 0) { // if not dealing with polygon which will be square with all sides ...
length = length * cosf(theta / 2.0); // ... offset it inside a circle inside the square
}
CGFloat sideLength = length * tanf(theta / 2.0);
// start drawing at `point` in lower right corner
CGPoint point = CGPointMake(squareWidth / 2.0 + sideLength / 2.0, squareWidth - (squareWidth - length) / 2.0);
CGFloat angle = M_PI;
[path moveToPoint:point];
// draw the sides and rounded corners of the polygon
for (NSInteger side = 0; side < sides; side++) {
point = CGPointMake(point.x + sideLength * cosf(angle), point.y + sideLength * sinf(angle));
[path addLineToPoint:point];
angle += theta;
}
[path closePath];
return path;
}
I posted another answer that illustrates the idea with rounded corners, too.
If you want to implement the addition of this mask as part of a UIImageView subclass, I'll leave that to you. But hopefully this illustrates the basic idea.

Plotting out Compass Bearing on iPhone screen

Ok so here is my question,
Let's say you have a bearing of 80 degrees. I would like to plot that out on the iPhone screen with a simple 10x10 square. Let's figure the top of the iPhone in portrait mode is North.
Any ideas on how I would accomplish this?
BTW - I don't want to use the following method:
CGAffineTransform where = CGAffineTransformMakeRotation(degreesToRadians(x_rounded));
[self.compassContainer2 setTransform:where];
I would like to plot out on the screen manually via setting the X -Y cords on the iPhone screen.
- (void)drawRect:(CGRect)rect
{
float compass_bearing = 80.0; // 0 = North, 90 = East, etc.
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 5.0, 5.0);
CGPathAddLineToPoint(path, NULL,
5.0 + 5.0 * cos((compass_bearing - 90.0) * M_PI / 180.0),
5.0 + 5.0 * sin((compass_bearing - 90.0) * M_PI / 180.0));
CGContextSetLineWidth(theContext, 2.0);
CGContextSetStrokeColorWithColor(theContext, [UIColor blackColor].CGColor);
CGContextAddPath(theContext, path);
CGContextStrokePath(theContext);
CGPathRelease(path);
}
So it sounds to me like what you want to accomplish should exist within the drawRect method of a custom view, and then this view would be added to your screen by whatever method you desired (i.e. storyboard or programmatically). Here is a possible implementation that you could use to draw a straight line from the center of the view based on some 'angle':
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// Drawing code
CGFloat angle = 0.0 /180.0 * M_PI ;
//Set this to be the length from the center
CGFloat lineDist = 320.0;
CGContextSetLineWidth(context, 5.0);
//Set Color
[[UIColor redColor] setStroke];
//Draw the line
CGContextBeginPath(context);
//Move to center
CGContextMoveToPoint(context, self.frame.size.width/2, self.frame.size.height/2);
//Draw line based on unit circle
//Calculate point based on center starting point, and some movement from there based on the angle.
CGFloat xEndPoint = lineDist * sin(angle) + self.frame.size.width/2;
//Calculate point based on center starting point, and some movement from there based on the angle. (0 is the top of the view, so want to move up when your answer is negative)
CGFloat yEndPoint = -lineDist * cos(angle) + self.frame.size.height/2;
CGContextAddLineToPoint(context, xEndPoint, yEndPoint);
CGContextStrokePath(context);
}

Stroking paths using a pattern color to simulate brush texture

I am making a finger painting app and I am having a hard time giving my brush a nice brush-like feel and texture.
I have this code:
UIColor * brushTexture = [UIColor colorWithPatternImage:[UIImage imageNamed:#"Brush_Black.png"]];
CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), brushTexture.CGColor);
but my output is like this:
The texture of my brush is tiled. How can I make it fit to the stroke Size and stop the tiling of the image?
Here i found a solution!
UIImage *texture = [UIImage imageNamed:"brush.png"]
CGPoint vector = CGPointMake(currentPoint.x - endPoint.x, currentTPoint.y - endPoint.y);
CGFloat distance = hypotf(vector.x, vector.y);
vector.x /= distance;
vector.y /= distance;
for (CGFloat i = 0; i < distance; i += 1.0f) {
CGPoint p = CGPointMake(endPoint.x + i * vector.x, endPoint.y + i * vector.y);
[texture drawAtPoint:p blendMode:blendMode alpha:0.5f];
}

CGContextDrawAngleGradient?

Dipping my feet into some more Core Graphics drawing, I'm attempting to recreate a wicked looking metallic knob, and I've landed on what is probably a show-stopping issue.
There doesn't seem to be any way to draw angle gradients in Core Graphics. I see there's CGContextDrawRadialGradient() and CGContextDrawLinearGradient(), but there's nothing that I see that would allow me to draw an angle gradient. Does anyone know of a workaround, or a bit of framework hidden away somewhere to accomplish this without pre-rendering the knob into an image file?
AngleGradientKnob http://dl.dropbox.com/u/3009808/AngleGradient.png.
This is kind of thrown together, but it's the approach I'd probably take. This is creating an angle gradient by drawing it directly into a bitmap using some simple trig, then clipping it to a circle. I create a grid of memory using a grayscale colorspace, calculate the angle from a given point to the center, and then color that based on a periodic function, running between 0 to 255. You could of course expand this to do RGBA color as well.
Of course you'd cache this and play with the math to get the colors you want. This currently runs all the way from black to white, which doesn't look as good as you'd like.
- (void)drawRect:(CGRect)rect {
CGImageAlphaInfo alphaInfo = kCGImageAlphaNone;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
size_t components = CGColorSpaceGetNumberOfComponents( colorSpace );
size_t width = 100;
size_t height = 100;
size_t bitsPerComponent = 8;
size_t bytesPerComponent = bitsPerComponent / 8;
size_t bytesPerRow = width * bytesPerComponent * components;
size_t dataLength = bytesPerRow * height;
uint8_t data[dataLength];
CGContextRef imageCtx = CGBitmapContextCreate( &data, width, height, bitsPerComponent,
bytesPerRow, colorSpace, alphaInfo );
NSUInteger offset = 0;
for (NSUInteger y = 0; y < height; ++y) {
for (NSUInteger x = 0; x < bytesPerRow; x += components) {
CGFloat opposite = y - height/2.;
CGFloat adjacent = x - width/2.;
if (adjacent == 0) adjacent = 0.001;
CGFloat angle = atan(opposite/adjacent);
data[offset] = abs((cos(angle * 2) * 255));
offset += components * bytesPerComponent;
}
}
CGImageRef image = CGBitmapContextCreateImage(imageCtx);
CGContextRelease(imageCtx);
CGColorSpaceRelease(colorSpace);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect buttonRect = CGRectMake(100, 100, width, width);
CGContextAddEllipseInRect(ctx, buttonRect);
CGContextClip(ctx);
CGContextDrawImage(ctx, buttonRect, image);
CGImageRelease(image);
}
To expand on what's in the comments to the accepted answer, here's the code for generating an angle gradient using Core Image. This should work in iOS 8 or later.
// generate a dummy image of the required size
UIGraphicsBeginImageContextWithOptions(CGSizeMake(256.0, 256.0), NO, [[UIScreen mainScreen] scale]);
CIImage *dummyImage = [CIImage imageWithCGImage:UIGraphicsGetImageFromCurrentImageContext().CGImage];
// define the kernel algorithm
NSString *kernelString = #"kernel vec4 circularGradientKernel(__color startColor, __color endColor, vec2 center, float radius) { \n"
" vec2 point = destCoord() - center;"
" float rsq = point.x * point.x + point.y * point.y;"
" float theta = mod(atan(point.y, point.x), radians(360.0));"
" return (rsq < radius*radius) ? mix(startColor, endColor, 0.5+0.5*cos(4.0*theta)) : vec4(0.0, 0.0, 0.0, 1.0);"
"}";
// initialize a Core Image context and the filter kernel
CIContext *context = [CIContext contextWithOptions:nil];
CIColorKernel *kernel = [CIColorKernel kernelWithString:kernelString];
// argument array, corresponding to the first line of the kernel string
NSArray *args = #[ [CIColor colorWithRed:0.5 green:0.5 blue:0.5],
[CIColor colorWithRed:1.0 green:1.0 blue:1.0],
[CIVector vectorWithCGPoint:CGPointMake(CGRectGetMidX(dummyImage.extent),CGRectGetMidY(dummyImage.extent))],
[NSNumber numberWithFloat:200.0]];
// apply the kernel to our dummy image, and convert the result to a UIImage
CIImage *ciOutputImage = [kernel applyWithExtent:dummyImage.extent arguments:args];
CGImageRef cgOutput = [context createCGImage:ciOutputImage fromRect:ciOutputImage.extent];
UIImage *gradientImage = [UIImage imageWithCGImage:cgOutput];
CGImageRelease(cgOutput);
This generates the following image: