I am drawing a simple path in iOS 5 with Core Graphics:
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint( path, NULL, center.x , topMargin );
CGPathAddLineToPoint(path, NULL, center.x+20, topMargin+50);
CGPathAddLineToPoint(path, NULL, center.x , topMargin+40);
CGPathAddLineToPoint(path, NULL, center.x-20, topMargin+50);
CGPathAddLineToPoint(path, NULL, center.x , topMargin );
Now i want to fill it in Overlay mode like so:
[[UIColor colorWithRed:0 green:0 blue:0 alpha:0.4] setFill];
CGContextAddPath(context, path);
CGContextSetBlendMode (context, kCGBlendModeOverlay);
CGContextFillPath(context);
Which gives me exactly the expected result. But next, i want to create an embossed effect. I thought of using a white and a black drop shadow in order to achieve this effect like so:
[[UIColor colorWithRed:0 green:0 blue:0 alpha:0] setFill];
CGContextAddPath(context, path);
CGContextSetShadowWithColor(context, CGSizeMake(1, 1), 1.0, highlightColor);
CGContextSetBlendMode (context, kCGBlendModeNormal);
CGContextFillPath(context);
[[UIColor colorWithRed:0 green:0 blue:0 alpha:0] setFill];
CGContextAddPath(context, path);
CGContextSetShadowWithColor(context, CGSizeMake(-1, -1), 1.0, shadowColor);
CGContextSetBlendMode (context, kCGBlendModeNormal);
CGContextFillPath(context);
The problem is, the shadows are not drawn when alpha is set to 0.
Now the question: Is there a way to draw only the shadows without the fill color but in full alpha? Can I somehow prevent the inside of my path from being drawn? Or is there perhaps a simpler way of drawing two shadows for one path?
I suggest you set the context's clipping path to the inverse of the shape's path, configure the shadow, and fill the shape normally, with full opacity. The clipping path will mask out the fill color, and only the shadow would remain.
CGContextSaveGState(context);
CGRect boundingRect = CGContextGetClipBoundingBox(context);
CGContextAddRect(context, boundingRect);
CGContextAddPath(context, path);
CGContextEOClip(context);
[[UIColor blackColor] setFill];
CGContextAddPath(context, path);
CGContextSetShadowWithColor(context, CGSizeMake(1, 1), 1.0, highlightColor);
CGContextSetBlendMode (context, kCGBlendModeNormal);
CGContextFillPath(context);
CGContextAddPath(context, path);
CGContextSetShadowWithColor(context, CGSizeMake(-1, -1), 1.0, shadowColor);
CGContextSetBlendMode (context, kCGBlendModeNormal);
CGContextFillPath(context);
CGContextRestoreGState(context);
The trick is using CGContextEOClip and an additional rectangle subpath to set the clipping area to whatever is not covered by the original path. This will work for any path that is not self-intersecting.
The problem is, the shadows are not drawn when alpha is set to 0.
Yes, the shadow relies on the shape's alpha value to render.
Now the question: Is there a way to draw only the shadows without the fill color but in full alpha? Can I somehow prevent the inside of my path from being drawn? Or is there perhaps a simpler way of drawing two shadows for one path?
You can draw the shadow only without rendering the main shape. The idea is:
add extra offset width to the shadow, the offset should be big enough, i.e. the context/canvas width. So that the shadow is separated from the main shape.
Translate the context with a negative same horizontal offset, so that the main shape is moved out of the rendering area, and the shadow is moved back.
For the shadow only layer, you probably want to use beginTransparencyLayer(auxiliaryInfo:) and saveGState() to push the drawing into a separated context.
Here is a comparison:
For code snippet, you can check my answer for another question here: https://stackoverflow.com/a/74844890/3164091
Related
Here is a picture of two lines drawn in a UITableViewCell with the same function, and same width, and color
As you can see the bottom line is a lot thicker than the other line.
The code I am using for drawing:
[CSDrawing drawLineWithColor:[UIColor blackColor] width:1.0 yPosition:1.0 rect:rect];
[CSDrawing drawLineWithColor:[UIColor blackColor] width:1.0 yPosition:CGRectGetMaxY(rect) - 3.0 rect:rect]; // draw a line on top and bottom
+(void)drawLineWithColor:(UIColor *)color width:(CGFloat)width yPosition:(CGFloat)yPosition rect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextMoveToPoint(context, 0.0, yPosition);
CGContextAddLineToPoint(context, CGRectGetMaxX(rect), yPosition);
CGContextSetStrokeColorWithColor(context, color.CGColor);
CGContextSetLineWidth(context, width);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
The problem was with the backgroundView being stretched to fit the content of the cell when the cell was being reused. When the cell was bigger the pixels were stretched. This is solved be setting the contentMode property to UIViewContentModeRedraw
I have this code to color an area between an inside and outside boundary.
CGContextSaveGState(context);
CGContextAddRect(context, self.bounds);
CGContextAddPath(context, inlinePath);
CGContextEOClip(context);
CGContextAddPath(context, outlinePath);
CGContextClip(context);
CGContextSetFillColorWithColor(context, _myColor.CGColor);
CGContextFillRect(context, self.bounds);
CGContextRestoreGState(context);
This is part of a local routine drawChartInContext:.
It works great when I generate an image like this:
UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, 2.0);
[self drawChartInContext:UIGraphicsGetCurrentContext()];
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
But when I call it from drawRect I just get the whole outlinePath area filled with _myColor.
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[self drawChartInContext:context];
}
What could be wrong?
Just figured it out. It was a combination of two oversights:
inlinePath had zero area (a special case that is valid)
myColor had 1.0 alpha (a configuration mistake).
I am using the code below and am trying everything to color the resulting circle that it draws, but cannot for the life of me figure it out! Can anyone help? How do I color the circle in that my method draws?
What I have so far is this:
-(void)drawCircleAtPoint:(CGPoint)p withRadius:(CGFloat)radius inContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
CGContextBeginPath(context);
CGContextAddArc(context, p.x, p.y, radius, 0, 2*M_PI, YES);
CGContextStrokePath(context);
UIGraphicsPopContext();
}
- (void)drawRect:(CGRect)rect
{
CGPoint midPoint;
midPoint.x = self.bounds.origin.x + self.bounds.size.width/2;
midPoint.y = self.bounds.origin.y + self.bounds.size.width/2;
CGFloat size = self.bounds.size.width/2;
if (self.bounds.size.height < self.bounds.size.width) size = self.bounds.size.height/2;
size *= 0.90;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 10.0);
[[UIColor blackColor] setStroke];
CGColorRef color = [[UIColor redColor] CGColor];
CGContextSetFillColorWithColor(context, color);
CGContextFillPath(context);
[self drawCircleAtPoint:midPoint withRadius:size inContext:context];
}
You're trying to fill the current path, but you haven't added anything to it yet. The only point at which you add anything to the current path is in drawCircleAtPoint:withRadius:inContext:, after which you only stroke it.
Move your CGContextFillPath call into that method. You'll want to fill before you stroke, since the stroke will reset the current path. (If you want to fill on top of the inside of the stroke, you'll need to save the gstate before stroking and restore it after, so that the current path is still set when you fill.)
Searching the web for about 4 hours not getting an answer so:
How to draw a shadow on a path which has transparency?
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.8] CGColor]);
// Sample Path
CGContextMoveToPoint(c, 20.0, 10.0);
CGContextAddLineToPoint(c, 100.0, 40.0);
CGContextAddLineToPoint(c, 40.0, 70.0);
CGContextClosePath(c);
CGContextDrawPath(c, kCGPathFillStroke);
}
The first thing I notice, the shadow is only around the stroke. But that isn't the problem so far. The shadow behind the path/rect is still visible, which means: the shadow color is effecting the fill color of my path. The fill color should be white but instead its grey. How to solve this issue?
You will have to clip the context and draw twice.
First you create a reference to your path since you will have to use it a few times and save your graphics context so you can come back to it.
Then you clip the graphics context to a only draw outside of your path. This is done by adding your path to the path that covers the entire view. Once you have clipped you draw your path with the shadow so that it's draw on the outside.
Next you restore the graphics context to how it was before you clipped and draw your path again without the shadow.
It's going to look like this on an orange background (white background wasn't very visible)
The code to do the above drawing is this:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.5] CGColor]);
// Sample Path
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 20.0, 10.0);
CGPathAddLineToPoint(path, NULL, 40.0, 70.0);
CGPathAddLineToPoint(path, NULL, 100.0, 40.0);
CGPathCloseSubpath(path);
// Save the state so we can undo the shadow and clipping later
CGContextSaveGState(c);
{ // Only for readability (so we know what are inside the save/restore scope
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGFloat width = CGRectGetWidth(self.frame);
CGFloat height = CGRectGetHeight(self.frame);
// Create a mask that covers the entire frame
CGContextMoveToPoint(c, 0, 0);
CGContextAddLineToPoint(c, width, 0);
CGContextAddLineToPoint(c, width, height);
CGContextAddLineToPoint(c, 0, height);
CGContextClosePath(c);
// Add the path (which by even-odd rule will remove it)
CGContextAddPath(c, path);
// Clip to that path (drawing will only happen outside our path)
CGContextClip(c);
// Now draw the path in the clipped context
CGContextAddPath(c, path);
CGContextDrawPath(c, kCGPathFillStroke);
}
CGContextRestoreGState(c); // Go back to before the clipping and before the shadow
// Draw the path without the shadow to get the transparent fill
CGContextAddPath(c, path);
CGContextDrawPath(c, kCGPathFillStroke);
}
If you want the entire shadow to be as strong and don't want the transparency of the fill color to make the shadow weaker then you can use a fully opaque color when filling the first time. It's going to get clipped so it won't be visible inside the path anyway. It will only affect the shadow.
Per your request in comments, here's a more in-depth exploration. Consider the following screenshot (StackOverflow shrinks it for me -- it helps to look at it full size.):
What you're seeing here is 5 different drawing approaches (top to bottom) over three different backgrounds (left to right). (I've also dropped the fill alpha from 0.8 to 0.5 to make the effects easier to see.) The three different drawing approaches are (top to bottom):
Just the stroke, not the fill, with a shadow
The way you posted in the code in your original question
The stroke and fill, with no shadow applied
Just the shadow, by itself
The way #DavidRönnqvist proposed in his answer.
The three different backgrounds should be self explanatory.
You said in your original question:
The first thing I notice, the shadow is only around the stroke.
This is why I included the first drawing approach. That's what it really looks like when there is just the stroke, with no fill, and (therefore) only the stroke is being shadowed.
Then, you said:
But that isn't the problem so far. The shadow behind the path/rect is
still visible, which means: the shadow color is effecting the fill
color of my path. The fill color should be white but instead its grey.
Your original code is the next version (#2). What you're seeing there is that the shadow for the stroke is darker than the shadow for the fill. This is because the stroke color's alpha is 1.0 and the fill's alpha is less than 1.0. This might be easier to see in version #4 which is just the shadow -- it's darker around the edge where the stroke is. Version #3 shows the drawing without a shadow. See you you can see the red and the image semi-obsurced in the fill of the shape? So in your original drawing you're seeing the object's own shadow through the object itself.
If that's not making sense, try thinking of a piece of glass that's got a tint to it (if you're into photography, think of a Neutral Density Filter). If you hold that glass between a light source and another surface, and then peek from the side and look just at the lower surface, you know that the semi-transparent glass is going to cast some shadow, but not as dark a shadow as something completely opaque (like a piece of cardboard). This is what you're seeing -- you're looking through the object at it's shadow.
Version #5 is #DavidRönnqvist's approach. The eye-fooling effect I was talking about in my comment is easiest to appreciate (for me, anyway) by looking at the shapes drawn over the image background. What it ends up looking like (in version #5) is that the shape is a bordered, copied, portion of the image that's been overlaid with a semi-transparent white mask of some sort. If you look back at version #3, it's clear, in the absence of the shadow, what's going on: you're looking through the semi-transparent shape at the image beneath. Then if you look at version #4, it's also clear that you have a shadow being cast by an object that's behind your eye/camera. From there, I would argue that that's also clear when looking at version #2 over the image what's going on (even if it's less clear over a solid color). At first glance, my eye/brain doesn't know what it's looking at in version #5 -- there's a moment of "visual dissonance" before I establish the mental model of "copied, masked, portion of the image floating above the original image."
So if that effect (#5) was what you were going for, then David's solution will work great. I just wanted to point out that it's sort of a non-intuitive effect.
Hope this is helpful. I've put the complete sample project I used to generate this screenshot on GitHub.
CGFloat lineWidth = 2.0f;
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextSetLineWidth(c, lineWidth);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathStroke);
CGContextRestoreGState(c);
someRect.origin.x += lineWidth/2;
someRect.origin.y += lineWidth/2;
someRect.size.width -= lineWidth;
someRect.size.height -= lineWidth;
CGContextClearRect(c, someRect);
CGContextSetFillColorWithColor(c, [[[UIColor whiteColor] colorWithAlphaComponent:0.8] CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathFill);
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowColor: [NSColor blackColor]];
[shadow setShadowOffset: NSMakeSize(2.1, -3.1)];
[shadow setShadowBlurRadius: 5];
NSBezierPath* bezierPath = [NSBezierPath bezierPath];
[bezierPath moveToPoint: NSMakePoint(12.5, 6.5)];
[bezierPath curveToPoint: NSMakePoint(52.5, 8.5) controlPoint1: NSMakePoint(40.5, 13.5) controlPoint2: NSMakePoint(52.5, 8.5)];
[bezierPath lineToPoint: NSMakePoint(115.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(150.5, 6.5)];
[bezierPath lineToPoint: NSMakePoint(201.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(222.5, 8.5)];
[NSGraphicsContext saveGraphicsState];
[shadow set];
[[NSColor blackColor] setStroke];
[bezierPath setLineWidth: 1];
[bezierPath stroke];
[NSGraphicsContext restoreGraphicsState];
Can anyone guide me in the correct way to build a colored bubble/circle programmatically?
I can't use images as I need it to be able to be any color depending on user interaction.
My thought was maybe to make a white circle image and then overlay a color on top of it.
However I am not sure if this would work, or how to really go about it.
If someone could point me the right direction I would appreciate it.
There are a couple steps to drawing something in Cocoa.
First you need a path that will be used to define the object that you are going to be drawing. Take a look here Drawing Fundamental Shapes for a guide on creating paths in Cocoa. You will be most interested in sending the "appendBezierPathWithOvalInRect" message to an "NSBezierPath" object, this takes a rectangle that bounds the circle you want to draw.
This code will create a 10x10 circle at coordinates 10,10:
NSRect rect = NSMakeRect(10, 10, 10, 10);
NSBezierPath* circlePath = [NSBezierPath bezierPath];
[circlePath appendBezierPathWithOvalInRect: rect];
Once you have your path you want to set the color for the current drawing context. There are two colors, stroke and fill; stroke is the outline of the path and the fill is the interior color. To set a color you send "set" to an "NSColor" object.
This sets the stroke to black and the fill to red:
[[NSColor blackColor] setStroke];
[[NSColor redColor] setFill];
Now that you have your path and you have your colors set just fill the path and then draw it:
[path stroke];
[path fill];
All of this will need to be done in a graphics context like in drawRect of a view perhaps. All of this together with a graphics context would look like this:
- (void)drawRect:(NSRect)rect
{
// Get the graphics context that we are currently executing under
NSGraphicsContext* gc = [NSGraphicsContext currentContext];
// Save the current graphics context settings
[gc saveGraphicsState];
// Set the color in the current graphics context for future draw operations
[[NSColor blackColor] setStroke];
[[NSColor redColor] setFill];
// Create our circle path
NSRect rect = NSMakeRect(10, 10, 10, 10);
NSBezierPath* circlePath = [NSBezierPath bezierPath];
[circlePath appendBezierPathWithOvalInRect: rect];
// Outline and fill the path
[circlePath stroke];
[circlePath fill];
// Restore the context to what it was before we messed with it
[gc restoreGraphicsState];
}
You may use simple UIView to create perfect circle with only parameter radius:
// Add framework CoreGraphics.framework
#import <QuartzCore/QuartzCore.h>
-(UIView *)circleWithColor:(UIColor *)color radius:(int)radius {
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 2 * radius, 2 * radius)];
circle.backgroundColor = color;
circle.layer.cornerRadius = radius;
circle.layer.masksToBounds = YES;
return circle;
}
Create an NSView subclass that holds an NSColor as an ivar. In the drawRect method, create an NSBezierPath of the appropriate size, using the view's bounds. Then set the color [myColor set] and fill the path [myPath fill]. There's a lot more you can do, such as set transparency, a border, and so on and so on, but I'll leave that to the docs unless you have a specific question.
To use the NSView subclass, just drag a view object onto your nib, and choose the name of your subclass in custom class in IB's inspector. You'll need to also set an outlet to it in your controller, so you can change the color as needed.
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetRGBFillColor(c, 40, 0, 255, 0.1);
CGContextSetRGBStrokeColor(c, 0, 40, 255, 0.5);
// Draw a green solid circle
CGContextSetRGBFillColor(c, 0, 255, 0, 1);
CGContextFillEllipseInRect(c, CGRectMake(100, 100, 25, 25));
Download sketch from apple. http://developer.apple.com/library/mac/#samplecode/Sketch
It can do a lot more, but one of the things is draw circles.