NSString drawInRect: and drop shadows on iOS - ios

I'm using [NSString drawInRect:] to draw text to a texture and everything works fine until I add drop shadows. They appear correctly, but are often clipped by the rect I'm drawing to.
The issue is that [NSString sizeWithFont:] doesn't know about the drop shadows since they are applied via CGContextSetShadowWithColor(...).
Here is the code I'm using (fluff removed):
CGSize dim = [theString sizeWithFont:uifont];
...
CGContextSetRGBFillColor(context, 1.0f, 1.0f, 1.0f, 1.0f);
CGContextTranslateCTM(context, 0.0f, dim.height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGContextSetShadowWithColor(context, CGSizeMake(dropOffset.x, dropOffset.y), dropBlur, dropColorRef);
...
[theString drawInRect:dim withFont:uifont lineBreakMode:UILineBreakModeWordWrap alignment:align];
I've tried expanding dim to take the drop shadow and blur into account and that mostly works, but sometimes the expanded rect causes the line to be wrapped completely different due to the extra space that was added.
Is there a better way to be finding the size of the texture/rect needed to draw to (or to draw the string) than I'm using?

You just need to keep track of two different rects.
The rect that will contain the text, which you pass to -[NSString drawInRect:]. Call this stringBounds.
The expanded/offset rect that contains the shadow. Call this shadowBounds.
Make your texture the size of shadowBounds.
When you draw the text, you'll need to translate by shadowBounds.origin - stringBounds.origin. (Or possibly the reverse -- it depends on exactly what you do, in which order. You'll know it when you get it.)
Then do [theString drawInRect:stringBounds ...].

Related

Line width in other angle

I draw line like this:
- (void)drawRect:(CGRect)rect {
_lineColor = [UIColor blackColor];
[_lineColor setStroke];
[_lineColor setFill];
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextClearRect(c, rect);
[[UIColor clearColor] setFill];
CGContextAddRect(c, rect);
CGContextSetLineWidth(c, 1);
CGContextDrawPath(c, kCGPathFill);
CGContextBeginPath(c);
CGContextMoveToPoint(c, 0, 0);
CGContextAddLineToPoint(c, x1, y1);
CGContextStrokePath(c);
}
but, my line is have different width which angle is 90 or 45 degrees. How I can draw line with same width
I whipped up something that may make the effect here more visible. Here's the code:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
// Get us a gray background
CGContextSetFillColorWithColor(c, [[UIColor grayColor] CGColor]);
CGContextFillRect(c, CGRectInfinite);
CGContextSetStrokeColorWithColor(c, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(c, 1.0);
CGContextBeginPath(c);
// Draw some lines at various angles
for (CGFloat i = -10.; i <= 10.; i += 1.0)
{
CGContextMoveToPoint(c, CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGContextAddLineToPoint(c, CGRectGetMidX(self.bounds) + i * 10., CGRectGetMidY(self.bounds) + 100);
}
CGContextStrokePath(c);
}
Here's the output from that code on a retina device: (This should display at 100% in the standard StackOverflow format, but you can look at it full size to be sure)
Now here's a piece of that blown up:
What you're seeing here is anti-aliasing at work. For starters the vertical line is 2.0 pixels across, all the time, with no anti-aliasing (assuming you draw it on a pixel boundary). Now think about a 45deg line drawn using the same pixel grid, and employ the Pythagorean theorem. Here's another diagram:
At it's narrowest (i.e. in the dimension perpendicular to the line itself), a 45 deg line will appear 1.414px wide, and at it's widest opaque section (not counting the mostly transparent pixel that's bridging the space in the jaggy gaps) it's going to appear 2.828px across. When blown up, you can see how the work that's being done to anti-alias these lines is effecting the optical appearance of the lines.
Someone is probably going to come along and suggest that you turn off anti-aliasing, but for reference, that makes the optical effect even worse (because then the rasterizer is going to make every pixel with coverage completely opaque):
In short, this is expected behavior, and if you need to adjust how you draw each line to achieve some desired optical appearance that the default anti-aliasing code doesn't provide, then you just have to do that work. There's not a "make all my lines optically similar" setting in CoreGraphics -- they've done the best they can for the general case, and if you need something more specific that's up to you. FWIW, many many applications simply use the default, and people seem pretty satisfied with the results, so you might ask yourself if this is really the place in your app where you want to put in a bunch of extra work.
It occurred to me: I'm not sure you'll like the effect, but one possible approach for achieving optical similarity might be to draw your vertical lines not on pixel boundaries. This will cause them to appear anti-aliased too, which (depending on how you look at it) may make them appear more consistent with the angled, anti-aliased lines. Here's what that looks like:
What you lose doing this is a certain "crispness" to the lines. Drawing off pixel-boundaries can most easily be achieved by translating the context by 0.25pt in the X direction (for retina -- for non-retina, 0.5pt will have a similar effect) like this:
CGContextTranslateCTM(c, 0.25, 0);
Hope this helps.

How to smooth edges of drawn image?

I'm using this code to colorize some images of a UIButton subclass:
UIImage *img = [self imageForState:controlState];
// begin a new image context, to draw our colored image onto
UIGraphicsBeginImageContextWithOptions(img.size, NO, 0.0f);
// get a reference to that context we created
CGContextRef context = UIGraphicsGetCurrentContext();
// set the fill color
[self.buttonColor setFill];
CGContextSetAllowsAntialiasing(context, true);
CGContextSetShouldAntialias(context, true);
// 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 multiply, and the original image
CGContextSetBlendMode(context, kCGBlendModeScreen);
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 the colored image
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 colored image
[self setImage:coloredImg forState:controlState];
But the images come out with rough edges. I've tried using screen, lighten, and plusLighter blend modes, because some of the images have white parts that I want to stay white. The only part I want colorized is the black areas. I've attached the original button images, and after they've been colorized. I can't get the edges to look good. When I had them as white images that were colorized using multiply blend mode, it looked much better. But I want to use black so I can use one method for colorizing images with and without white in them. I tried with anti-aliasing, that didn't help either. It looks like it just isn't anti-aliasing it. I haven't worked with Core Graphics enough to know what's up with it.
EDIT
Here's what the original PNGs look like:
and here's what it should look like:
and here's what it does look like:
The size if different, but you can see the bad quality around the edges.
Maybe your original icons (PNGs?) are just "too sharp"? Could you show us? You just draw the image at its original size without resizing, so the problem could be right from the start.
I'm not sure what is what you are trying to accomplish here. Are you trying to round the edges of the images? If so, you are better of by changing the round corner property of the UIButton's layer. Since UIButton is a subclass of UIView, you can get its layer property and change the edge color and round its corner.

IOS Quartz 2D drawRect and Resizing

I'm using an app called Quarkee to convert a logo from SVG to Quartz 2d code, which works a treat. Only problem is I can't seem to figure out how resize the result. If I set the frame of the UIView, the result from drawRect stays huge in the frame. How do I get it be the size of the frame I'm setting?
An example of the out is below.
Can someone help?
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorSpaceRef colorspace_1 = CGColorSpaceCreateDeviceRGB();
CGFloat components_1[] = {0.9961,0.9961,0.9961, 1.0000};
CGColorRef color_1 = CGColorCreate(colorspace_1, components_1);
CGContextSetFillColorWithColor(context,color_1);
CGContextMoveToPoint(context, 0.4960,0.1090);
CGContextAddLineToPoint(context,662.9260,0.1090);
CGContextAddLineToPoint(context,662.9260,227.8780);
CGContextAddLineToPoint(context,0.4960,227.8780);
CGContextAddLineToPoint(context,0.4960,0.1090);
CGContextClosePath(context);
CGContextFillPath(context);
The solution here was to use view.transform = CGAffineTransformMakeScale(0.5f, 0.5f); to resize the view
Take a look at these values:
CGContextMoveToPoint(context, 0.4960,0.1090);
CGContextAddLineToPoint(context,662.9260,0.1090);
CGContextAddLineToPoint(context,662.9260,227.8780);
CGContextAddLineToPoint(context,0.4960,227.8780);
CGContextAddLineToPoint(context,0.4960,0.1090);
when the drawRect method is called you can use self.bounds to take the current rect of your view and adjust those values according to it. You say you have generated this code from a application - those apps usually hardcode the size of the graphics you have drawn, when generate code, so you must see what is the relation between these values with the actual size of the image you have drawn and make them dynamic according to the self.bounds size...

Is there a way to add text using Paths Drawing

I have a map custom view that inherit from MKOverlayPathView. I need this custom view to display circle, line and text.
I already managed to draw circle and line using path drawing CGPathAddArc and CGPathAddLineToPoint functions.
However, I still need to add text.
I tried to add text using
[text drawAtPoint:centerPoint withFont:font];
but I got invalid context error.
any idea?
With MKOverlayPathView, I think the easiest way to add text is to override drawMapRect:zoomScale:inContext: and put the path and text drawing there (and do nothing in or don't implement createPath).
But if you're going to use drawMapRect anyway, you might want to just switch to subclassing a plain MKOverlayView instead of MKOverlayPathView.
With an MKOverlayView, override the drawMapRect:zoomScale:inContext: method and draw the circle using CGContextAddArc (or CGContextAddEllipseInRect or CGPathAddArc).
You can draw the text using drawAtPoint in this method which will have the required context.
For example:
-(void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
//calculate CG values from circle coordinate and radius...
CLLocationCoordinate2D center = circle_overlay_center_coordinate_here;
CGPoint centerPoint =
[self pointForMapPoint:MKMapPointForCoordinate(center)];
CGFloat radius = MKMapPointsPerMeterAtLatitude(center.latitude) *
circle_overlay_radius_here;
CGFloat roadWidth = MKRoadWidthAtZoomScale(zoomScale);
//draw the circle...
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetFillColorWithColor(context, [[UIColor blueColor] colorWithAlphaComponent:0.2].CGColor);
CGContextSetLineWidth(context, roadWidth);
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, 2 * M_PI, true);
CGContextDrawPath(context, kCGPathFillStroke);
//draw the text...
NSString *text = #"Hello";
UIGraphicsPushContext(context);
[[UIColor redColor] set];
[text drawAtPoint:centerPoint
withFont:[UIFont systemFontOfSize:(5.0 * roadWidth)]];
UIGraphicsPopContext();
}
In relation to a comment in another answer...
When the center coordinate or radius (or whatever) of the associated MKOverlay changes, you can make the MKOverlayView "move" by calling setNeedsDisplayInMapRect: on it (instead of removing and adding the overlay again). (When using a MKOverlayPathView, you can call invalidatePath instead.)
When calling setNeedsDisplayInMapRect:, you can pass the boundingMapRect of the overlay for the map rect parameter.
In the LocationReminders sample app from WWDC 2010, the overlay view uses KVO to observe changes to the associated MKOverlay and makes itself move whenever it detects a change to the circle's properties but you could monitor the changes in other ways and call setNeedsDisplayInMapRect: explicitly from outside the overlay view.
(In a comment on another answer I did mention using MKOverlayPathView and that is how the LocationReminders app implements a moving circle overlay view. But I should have mentioned how you can also use MKOverlayView to draw a circle. Sorry about that.)
Pushing the context with UIGraphicsPushContext generated a problem for me. Remind that the method drawMapRect:zoomScale:inContext: is called from different threads in the same time so I had to synchronize the piece of code starting where the UIGraphicsPushContext is called down to UIGraphicsPopContext call.
Also when calculating the font size like in [UIFont systemFontOfSize:(5.0 * roadWidth)] one should take into consideration the [UIScreen mainScreen].scale, which for iPad, iPad2, iPhone3 is 1 and for iPhone4 - 5 and iPad3 is 2. Otherwise the text size will be different from iPad2 to iPad3.
So for me it ended like this: [UIFont boldSystemFontOfSize:(6.0f * [UIScreen mainScreen].scale * roadWidth)]

why CGContextSaveGState is not required even after several modification to the current context?

I am really strugggling with Quartz2D for more then 10 days please help me understand few concepts I will be really grateful, please look at this code and screenshot url.
This code draw image with border and write text to it and the image become whole new image with border and text.
//part 1
CGSize cgs = CGSizeMake(250.0, 400.0);
UIGraphicsBeginImageContext(cgs);
CGRect rectangle = CGRectMake(0,0,cgs.width,cgs.height);
CGRect imageRect = CGRectInset(rectangle, 5.4, 5.4);
imageRect.size.height -= 100;
UIImage *myImage = [UIImage imageNamed:#"BMW.jpg"];
[myImage drawInRect:imageRect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 10.0);
CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
CGContextStrokeRect(context, rectangle);
//
//part 2
1. CGRect contextRect = rectangle;
2. CGContextTranslateCTM(context, 0, contextRect.size.height);
3. CGContextScaleCTM(context, 1, -1);
4. float w, h;
5. w = contextRect.size.width;
6. h = contextRect.size.height;
7. CGContextSelectFont (context, "Helvetica-Bold", 25,
kCGEncodingMacRoman);
8. CGContextSetCharacterSpacing (context, 5);
9. CGContextSetRGBFillColor(context, 0.0, 1.0, 1.0, 1.0);
10. CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
11. CGContextShowTextAtPoint(context, 45, 50, "Quartz 2D", 9);
//
//part 3
UIImage *testImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[testImg drawAtPoint:CGPointMake(35, 10)];
//
http://i40.tinypic.com/140aptv.png
part 1 and part 3 of the code is very clear to me
problem is regarding part 2
on line 2 and 3 coordinates are transformed so the text do not display
upside down, but uiimage already take care of this internally, why it
didn't transformed to upside down? why it is still displaying in
correct position after transform is applied for text using same
context? I am asking this because when uiimage coordinates are already
modified then this coordinate transform will not make uiimage again
upside down?
on line 9 and 10 fillcolor and strokecolor methods are called and
fillcolor changes the text color, but strokecolor not doing any thing
to text why? And why without CGContextSaveGState it modified the
color of text not the border color?
regarding these both points I mentioned above the common confusion is
why its working perfectly why this code didn't need
CGContextSaveGState and CGContextRestoreGState. How it is possible
that context is modified and it didn't effect the perviously drawing
item like blue border in this case and coordinates transformation for
text.
Please correct me if I am lacking in any way to make you understand my points.
Thanks in advance,
Regards.
Quartz 2D uses the "painter's model." That means, you draw one thing, and it's done. Then you draw another thing, and it goes on top of what you drew before. Then you draw another thing and that goes on top, etc. If I pick up a stamp, dip it in paint and press it to paper, then turn it over and do it again to another part of the paper, the first stamped image doesn't flip over just because I flipped the stamp.
Every time you see "stroke" or "draw," you're modifying the final image. Later changes to the context don't effect that.

Resources