Avoiding Stretching Stroke when Using CGContextScaleCTM - iphone

I have a shape that I'm drawing in drawRect which is stored in a CGMutablePathRef (shapeMutablePath). Each time drawRect is called, the shape is stretched to fit the screen with a stroke border around it. I am wondering, how is it possible to draw the stroke border without also stretching it? ie stretching the shapeMutablePath, then drawing the stroke border around it so it's the same width every time it's drawn? I've tried changing the order of the scale and the add and draw path to no avail.
- (void) drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetRGBFillColor(context, 1.0000, 1.0000, 1.0000, 1.0000);
CGContextSetRGBStrokeColor(context,0.0000,0.0000,0.0000,1.0000);
CGContextSetLineWidth(context, DialogueTextViewLineWidth);
CGContextScaleCTM (context, self.frame.size.width / self.shapeMutablePathWidth, self.frame.size.height / self.shapeMutablePathHeight);
CGContextAddPath(context, self.shapeMutablePath);
CGContextDrawPath(context, kCGPathFillStroke);
CGContextRestoreGState(context);
}

Instead of scaling the CTM and using the original path:
CGContextScaleCTM (context, self.frame.size.width / self.shapeMutablePathWidth, self.frame.size.height / self.shapeMutablePathHeight);
CGContextAddPath(context, self.shapeMutablePath);
... create a transformed path and use that instead:
CGAffineTransform trn = CGAffineTransformMakeScale(self.bounds.size.width / self.shapeMutablePathWidth, self.bounds.size.height / self.shapeMutablePathHeight);
CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(self.shapeMutablePath, &trn);
CGContextAddPath(context, transformedPath);
CGPathRelease(transformedPath);
This will fill and stroke the same (scaled) area but the transform will not affect the stroke width.
B.t.w. You would typically use the bounds, not the frame's size to calculate the scale.

Related

iphone stroke path used for clipping UIImage

I'm very new to CoreGraphics (although I've been doing iphone programming for a while)
Question, I have this snippet that scales proportionally and clips an UIImage to a circle:
-(UIImage *)counterpartImageForSchedule:(Schedule *)counterpartSchedule inSize:(CGSize)size
{
// This function returns a newImage, based on image, that has been:
// - scaled to fit in (CGRect) rect
// - and cropped within a circle of radius: rectWidth/2
UIImage *image=[UIImage imageNamed:#"fakeuser1.png"];
// when kendy sends hash, check that this image is not on the cache, otherwise download, clip & stylized
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size.width, size.height), NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
//Get the width and heights
CGFloat imageWidth = image.size.width;
CGFloat imageHeight = image.size.height;
CGFloat rectWidth = size.width;
CGFloat rectHeight = size.height;
//Calculate the scale factor
CGFloat scaleFactorX = rectWidth/imageWidth;
CGFloat scaleFactorY = rectHeight/imageHeight;
if (scaleFactorX>scaleFactorY) {
scaleFactorY=scaleFactorX;
} else
{
scaleFactorX=scaleFactorY;
}
//Calculate the centre of the circle
CGFloat imageCentreX = rectWidth/2;
CGFloat imageCentreY = rectHeight/2;
// Create and CLIP to a CIRCULAR Path
// (This could be replaced with any closed path if you want a different shaped clip)
CGFloat radius = rectWidth/2;
CGContextBeginPath (context);
CGContextAddArc (context, imageCentreX, imageCentreY, radius, 0, 2*M_PI, 0);
CGContextClosePath (context);
CGContextClip (context);
//Set the SCALE factor for the graphics context
//All future draw calls will be scaled by this factor
CGContextScaleCTM (context, scaleFactorX, scaleFactorY);
// the stroke
// Draw the IMAGE
CGRect myRect = CGRectMake(0, 0, imageWidth, imageHeight);
[image drawInRect:myRect];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Question is, how can I stroke the path I'm using to clip the image (say, 4 pixels with black or white color)? do i need to draw a new one?
any help is much appreciated!
A path can be an object like any other - in this case, a CGPath (CGPathRef). Before using the path for anything, encapsulate it as a CGPath (in this case, probably a CGMutablePathRef, or perhaps you can call CGContextCopyPath before using the path to clip to). Now you can reuse that path.
Here's an example from one of my apps where I form a path, then stroke it, and then clip to the same path (c is the graphics context):
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, CGRectGetMaxX(r) - radius, ins);
CGPathAddArc(path, nil,
radius+ins, radius+ins, radius, -M_PI/2.0, M_PI/2.0, true);
CGPathAddArc(path, nil,
CGRectGetMaxX(r) - radius, radius+ins, radius, M_PI/2.0, -M_PI/2.0, true);
CGPathCloseSubpath(path);
CGContextAddPath(c, path);
CGContextSetLineWidth(c, 2);
CGContextStrokePath(c);
CGContextAddPath(c, path);
CGContextClip(c);
CGPathRelease(path);
Another possibility is to use UIBezierPath - a full-fledged Objective-C object, instead of CGContext functions. It encapsulates a CGPath and you can reuse that path - clip to it, then stroke it. Or the other way round.

How to draw a line with a custom style using Core Graphics?

I'm currently drawing a line using Core Graphics. It's really bare bones and simple.
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
CGFloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
CGContextSetStrokeColor(c, red);
CGContextBeginPath(c);
CGContextMoveToPoint(c, 5.0f, 5.0f);
CGContextAddLineToPoint(c, 300.0f, 600.0f);
CGContextSetLineWidth(c, 25);
CGContextSetLineCap(c, kCGLineCapRound);
CGContextStrokePath(c);
}
This works well. Let's say that we wanted to draw a custom style line. Say we wanted to imitate the style of a crayon for example. And that the designer handed your crayon style images: http://imgur.com/a/N40ig
To do accomplish this effect I think I need to do something like this:
Create a special colored versions of crayonImage1-crayonImage4
Every time you add a line to line you use one of the crayonImages
You alternate the crayonImages every time you draw a point.
Step 1 makes sense. I can use the following method:
- (UIImage *)image:(UIImage *)img withColor:(UIColor *)color {
// begin a new image context, to draw our colored image onto
UIGraphicsBeginImageContext(img.size);
// get a reference to that context we created
CGContextRef context = UIGraphicsGetCurrentContext();
// set the fill color
[color setFill];
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
CGContextTranslateCTM(context, 0, img.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// set the blend mode to color burn, and the original image
CGContextSetBlendMode(context, kCGBlendModeColorBurn);
CGRect rect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, rect, img.CGImage);
// set a mask that matches the shape of the image, then draw (color burn) a colored rectangle
CGContextClipToMask(context, rect, img.CGImage);
CGContextAddRect(context, rect);
CGContextDrawPath(context,kCGPathFill);
// generate a new UIImage from the graphics context we drew onto
UIImage *coloredImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//return the color-burned image
return coloredImg;
}
I'm unsure how I can complete steps 2 and 3. Is there an API in CoreGraphics for setting an image as the point of line? If so what is it and how can I use it?
Thanks in advance,
-David
Start with the following example: http://www.ifans.com/forums/showthread.php?t=132024
But for brushes, don't draw a line. Simply draw the brush image using CGContextDrawImage.
Basically, you simply draw an image for every touch.

Core Graphics - drawing a line at an angle

I've been searching struggling with this for a while now and need some help. I just want to draw 25 lines from the centre of my display (160,200) at 14.4 degrees separation. I have been using this line of code in a for loop where x is the 14.4 degree multiplier -
UIImage*backgroundImage = [UIImage imageNamed:#"Primate Background Only.png"];
[backgroundImage drawInRect:CGRectMake(0, 0, 320, 480)];
// Draw Outer Circle
rect = CGRectMake(0.0, 35.0, 320.0, 320.0);
CGContextRef contextRef = UIGraphicsGetCurrentContext(); // Get the contextRef
CGContextSetLineWidth (contextRef, 0.5); // Set the border width
CGContextSetRGBFillColor (contextRef, (219.0f/255.0f), (219.0f/255.0f), (219.0f/255.0f), 0.05f); // Set the circle fill color to GREEN
CGContextSetRGBStrokeColor (contextRef, 0.0, 0.0, 0.0, 0.2); // Set the circle border color to BLACK
CGContextFillEllipseInRect (contextRef, rect); // Fill the circle with the fill color
CGContextStrokeEllipseInRect (contextRef, rect); // Draw the circle border
// Draw Inner Circle
rect = CGRectMake(25.0, 60.0, 270.0, 270.0); // Get the contextRef
CGContextSetLineWidth (contextRef, 0.5); // Set the border width
CGContextSetRGBFillColor (contextRef, (219.0f/255.0f), (219.0f/255.0f), (219.0f/255.0f), 0.25f);
CGContextSetRGBStrokeColor (contextRef, 0.0, 0.0, 0.0, 0.2); // Set the circle border color to BLACK
CGContextFillEllipseInRect (contextRef, rect); // Fill the circle with the fill color
CGContextStrokeEllipseInRect (contextRef, rect);
// Draw Segments
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0.0, 0.0);
for (x=1; x<26; x++) {
CGContextSetLineWidth (context, 0.5);
CGContextSetRGBStrokeColor (context, 0.0, 0.0, 0.0, 0.25);
CGContextMoveToPoint (context, 160, 200);
CGContextAddLineToPoint (context, (160.0 * (cos((x*14.4)*(M_PI/180)))), (160.0 * (sin((x*14.4)*(M_PI/180))))); // The angle is in degrees
CGContextAddLineToPoint (context, 200, 65);
CGContextAddLineToPoint (context, 160, 200);
CGContextStrokePath(context); // Why is this not showing both line color and infill?
CGContextSetFillColorWithColor (context, [UIColor whiteColor].CGColor);
CGContextFillPath (context);
CGContextRef context = UIGraphicsGetCurrentContext();
}
(I intended to post an image here but it won't permit me!)
Can someone please correct my trig functions. This is meant to draw 25 lines clockwise from 12:00 o'clock to 24:00. Instead it draws backwards only 90 degrees then returns, all lines are way out in length also.
Very grateful
Above is a bigger sample of the code used to draw tow outer circles and the interior segments as requested. I'll try and upload the image next.
Your main problem is this line of code:
CGContextAddLineToPoint(context, (160.0 * (cos((x*14.4)*(M_PI/180)))),
(160.0 * (sin((x*14.4)*(M_PI/180)))));
it should be:
CGContextAddLineToPoint(context, 160 + (160.0 * (cos((x*14.4)*(M_PI/180)))),
200 + (160.0 * (sin((x*14.4)*(M_PI/180)))));
As you had originally written it the lines were be drawn relative to 0,0 but you want to draw relative to your center point (160, 200).
The next 2 lines are also a little suspect:
CGContextAddLineToPoint (context, 200, 65);
CGContextAddLineToPoint (context, 160, 200);
It is not clear to me what you were trying to do here, but this would end up drawing a line from the end of each arm to a fixed point (200,65) and from there back to you center point, which is almost certainly not what you intended! Try commenting out these lines, confirm that the "arms" are being drawn correctly and take it from there...
Hope this is clear
If you make these corrections your screen will look something like this:

CGContextDrawPath Stroke disappears if Fill is added

trying to draw circle divided to dynamically provided number of sectors. In the end my circle should look like pie.
I've created class Circle which contains number of Pies inside. Circle has method - (void)draw which calles [pie draw]; in for (Pie *pie in self.pies) loop.
Implementation of pie looks like this:
- (void)draw {
CGContextRef context = UIGraphicsGetCurrentContext();
float startAngleRads = degreesToRadians(_startAngle);
float endAngleRads = degreesToRadians(_endAngle);
CGContextBeginPath(context);
CGContextSetLineWidth(context, 1.0f);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGColorRef strokeColor = [_strokeColor CGColor];
CGContextSetStrokeColorWithColor(context, strokeColor);
CGColorRef fillColor = [_bgColor CGColor];
CGContextSetFillColorWithColor(context, fillColor);
CGContextMoveToPoint(context, _x, _y);
CGContextAddArc(context, _x, _y, _r, startAngleRads, endAngleRads, YES);
CGContextDrawPath(context, kCGPathStroke);
CGContextFlush(context);
}
This draws nice circle with sectors but without any filling of course. When I change last lines to CGContextDrawPath(context, kCGPathStrokeFill); my stroke lines disappears and only last line (last sector) stays at the screen, other parts of circle are filled with bgcolor and no stroke is visible anymore.
The question is: how can I draw completly separated "sectors" (every sector should look like triangle with Arc and Stroke and Fill). I need this because I want to toggle visibility of that sectors but at the moment I cant even figure out how to draw them correctly.
If this is important, all drawing happens on the layer with self.opaque = NO;.
Ok, found my error: had to set NO in CGContextAddArc call.

Photo button like in contacts and Facebook app

How can I create a "person photo" button like the one in contact info or in the Facebook app? (grey and rounded border with a small radius and an image that is cropped inside it)
Edit:
It must obviously work for all photos, not just one that I prerender in Photoshop.
I guess I could do it manually using masks etc., but Facebook app does it exactly like the contacts app, so I suspect there is some way to do it in the SDK.
Here you (I :P) go:
+ (UIImage *)imageWithBorderForImage:(UIImage *)image
{
CGFloat radius = 5;
CGSize size = image.size;
radius = MIN(radius, .5 * MIN(size.width, size.height));
CGRect interiorRect = CGRectInset(CGRectMake(0, 0, size.width, size.height), radius, radius);
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGMutablePathRef borderPath = CGPathCreateMutable();
CGPathAddArc(borderPath, NULL, CGRectGetMinX(interiorRect), CGRectGetMinY(interiorRect), radius, 1.0*M_PI, 1.5*M_PI, NO);
CGPathAddArc(borderPath, NULL, CGRectGetMaxX(interiorRect), CGRectGetMinY(interiorRect), radius, 1.5*M_PI, 0.0*M_PI, NO);
CGPathAddArc(borderPath, NULL, CGRectGetMaxX(interiorRect), CGRectGetMaxY(interiorRect), radius, 0.0*M_PI, 0.5*M_PI, NO);
CGPathAddArc(borderPath, NULL, CGRectGetMinX(interiorRect), CGRectGetMaxY(interiorRect), radius, 0.5*M_PI, 1.0*M_PI, NO);
CGContextBeginPath(context);
CGContextAddPath(context, borderPath);
CGContextClosePath(context);
CGContextClip(context);
[image drawAtPoint:CGPointMake(0,0)];
CGContextBeginPath(context);
CGContextAddPath(context, borderPath);
CGContextClosePath(context);
CGContextSetRGBStrokeColor(context, 0.5, 0.5, 0.5, 1.0);
CGContextSetLineWidth(context, 1.0);
CGContextStrokePath(context);
CGPathRelease(borderPath);
UIImage *borderedImage = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(context);
UIGraphicsEndImageContext();
return borderedImage;
}
Based largely on this question.
One problem is that the border is actually 2px wide (although 1px falls outside of the clip area) because of anti-aliasing. Ideally the border would have ~0.5 alpha, but since the antialiasing gives each of the 2 pixels some alpha, I set it to 1 and it comes out just about right. If I disable antialiasing, then the corners aren't rounded all the same :/
Create it as an image (like "person.png"), then load it up like this:
UIImage *personImage = [UIImage imageNamed:#"person.png"];
[[myButton imageView] setImage:personImage];
//where myButton is a UIButton of type UIButtonTypeCustom