Unable to get rid of spike on bezier path - objective-c

I'm drawing some simple bezier paths, but I'm finding it impossible to remove the spikes created when the angle between line segments is small:
(Note: The circle is from a separate drawing operation, but I'm trying to make sure the line does not spike past the circle...).
I've tried all kinds of variations of lineCapStyle and lineJoinStyle but nothing seems to work.
In addition to what is shown below I have tried using a Miter Join with 'setMiterLimit'.
Here's my line drawing code snip:
CAShapeLayer *myShapeLayer=[CAShapeLayer layer];
UIBezierPath *myPath=[UIBezierPath bezierPath];
[myPath moveToPoint:tmpPoint];
[myPath addLineToPoint:tmpPoint];
[myPath setLineCapStyle:kCGLineCapRound];
[myPath setLineJoinStyle:kCGLineJoinRound];
myShapeLayer.path=[myPath CGPath];
myShapeLayer.strokeColor = [[UIColor yellowColor] CGColor];
myShapeLayer.fillColor = [[UIColor clearColor] CGColor];
myShapeLayer.lineWidth = 3.0;
Just in case - here's the Miter code I've used, varying the value rom 0.0 to 100.0 - all with no effect:
[myPath setLineCapStyle:kCGLineCapRound];
[myPath setLineJoinStyle:kCGLineJoinMiter];
[myPath setMiterLimit:1.0];

You should be setting lineJoin on the shape layer instead of the path:
myShapeLayer.lineJoin = kCALineJoinRound;
The confusion comes from the fact that UIBezierPath has the ability to draw the path (by calling fill and stroke on the path). The configuration of line joins and line caps on the path is only affecting this drawing.
However, since you are drawing the path using a CAShapeLayer, the configurations of both line joins and line caps should be done on the shape layer.

Related

CAShapeLayer keeps closing path, I want the path open

I'm using CA to draw line segments from an array of points. For some reason, although I did not close the NSBezierPath, CAShapeLayer results in a closed shape. The following are my codes. Do anyone else have this problem?
// mappedPoints is an array of CGPoints
NSBezierPath *path = [NSBezierPath bezierPath];
[path appendBezierPathWithPoints:mappedPoints count:numPoints];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [color CGColor];
shapeLayer.lineWidth = lineWidth;
shapeLayer.fillColor = nil;
[self.layer addSublayer:shapeLayer];
I'm allowing myself to guess here that the [path CGPath] call is what's closing that path for you, as that's exactly what happened to me.
To give a bit more context - Apple's Creating a CGPathRef From an NSBezierPath Object sample code, which creates an NSBezierPath category method like CGPath above, has this in it:
// Be sure the path is closed or Quartz may not do valid hit detection.
if (!didClosePath)
CGPathCloseSubpath(path);
The reasoning behind that (from the same source):
Quartz requires paths to be closed in order to do hit detection on the
path’s fill area
If you're not interested in fill-area hit detection, it's safe to skip over that path-closing part and you'd get the expected behavior.

UIBezierPath of a circle and rectangle intersection for a mask

I am trying to create a mask region of the intersection of a circle and a rectangle.
I am starting with this code that seems to create an XOR of the circle and rectangle for the mask region but I want just a plain old AND:
- (void)addMaskToHoleViewAtX:(CGFloat) x AtY:(CGFloat) y Radius:(CGFloat) kRadius {
CGRect bounds = holeView.bounds;
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.fillColor = [UIColor blackColor].CGColor;
CGRect const rect = CGRectMake(CGRectGetMidX(bounds) - kRadius/2,
CGRectGetMidY(bounds) - kRadius,
kRadius,
2 * kRadius);
UIBezierPath *pathrect = [UIBezierPath bezierPathWithRect:rect];
CGRect const circ = CGRectMake(CGRectGetMidX(bounds) - kRadius,
CGRectGetMidY(bounds) - kRadius,
2 * kRadius,
2 * kRadius);
UIBezierPath *pathcirc= [UIBezierPath bezierPathWithOvalInRect:circ];
UIBezierPath *allPaths= [[UIBezierPath alloc] init];
[allPaths appendPath:pathrect];
[allPaths appendPath:pathcirc];
[allPaths appendPath:[UIBezierPath bezierPathWithRect:bounds]];
maskLayer.path = allPaths.CGPath;
maskLayer.fillRule = kCAFillRuleEvenOdd;
holeView.layer.mask = maskLayer;
holeView.center = CGPointMake(x, y);
}
Could someone help me with the syntax to do the AND, I think I might need to use addClip but it is not obvious to me how to do that with the above code?
MY SOLUTION: It appears to me that if I were able to figure how to use addClip to solve this problem in one manner, I would not actually end up with the closed NSBezierPath of the intersection. I prefer not to do it that way as having the intersection NSBezierPath is also needed to easily determine if a point is inside the path. SOOOO, I just created the NSBezierPath of the intersection through calculations and used my derived path to append to the masklayer bounds path. It sure would be nice to have a way of actually obtaining the intersection NSBezierPath without calculations but I just had to move on. Thanks for trying.
Thanks,
Carmen
EDIT : Here is the routine I am calling to put the 'intersection' mask over my _map View. Change _map to your view if you want to try this:
- (void)addHoleSubview {
holeView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10000, 10000)];
holeView.backgroundColor = [UIColor colorWithRed:255 green:0 blue:0 alpha:0.2];
holeView.autoresizingMask = 0;
[_map addSubview:holeView];
[self addMaskToHoleViewAtX:100 AtY:100 Radius:50];
}
If you aim for the intersection of two paths you should not create a compound path. Appending a path to another path will
result in a union of both paths if using the non-zero winding number rule and both paths have the same direction - or in other words it will have the effect of an AND
result in a shape that contains only the parts of the areas surrounded by the two paths that do not overlap if using the even-odd rule - or in other words it will have the effect of an XOR.
Instead I suggest you first add the first path pathrect to your graphics context and clip
[pathrect addClip];
and then you add the second path pathcirc to your context and clip again:
[pathcirc addClip];
You can now use any filling rule within that context and it will fill the intersection (the AND) of the two paths.

Core graphics - Seams show on diagonally joined sides

I'm having a problem in Core Graphics where two shapes that are supposed to fit together are showing seams between them when their sides are diagonal. This is causing problems. I can 'fix' this by overlapping the shapes. This works for simple stuff, but when the shape is more complex things start to fall apart.
Here is a simple example:
[[UIColor blackColor] setFill];
[path fill]; //fills the entire background black
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 moveToPoint:CGPointMake(0.0f, 0.0f)];
[path1 addLineToPoint:CGPointMake(100, 0.0f)];
[path1 addLineToPoint:CGPointMake(50, rect.size.height)];
[path1 addLineToPoint:CGPointMake(0, rect.size.height)];
[path1 closePath];
UIBezierPath *path2 = [UIBezierPath bezierPath];
[path2 moveToPoint:CGPointMake(100, 0)];
[path2 addLineToPoint:CGPointMake(rect.size.width, 0)];
[path2 addLineToPoint:CGPointMake(rect.size.width, rect.size.height)];
[path2 addLineToPoint:CGPointMake(50, rect.size.height)];
[path2 closePath];
[[UIColor lightGrayColor] setFill];
[path1 fill];
[path2 fill];
This produces a seam that looks something like this:
This is exceptionally problematic because this is occurring in a CGPathApply call back function I'm using to process shapes (I divide the shapes up, do some processing and put them back together again). Because of this, it's very difficult to determine how those shapes should be shifted in such a way as to remove the seams (by overlapping) without distorting the shape. Especially since some of these shapes are rather complex (not simple rects).
I did discover that turning off antialiasing (CGContextSetShouldAntialias(context, NO);) does remove the seems but things just get ugly if I do that, so it's not really an option.
Any ideas?

getting points of Bezier Curve after moving that particular object

I have a curve which is drawn by using Bezier curve. Now i am moving this particular object and placing it in another location. How can i get the new points for the curve of that object.
- (void)drawRect:(CGRect)rect
{
[myPath moveToPoint:CGPointMake(100, 100)];
[myPath addLineToPoint:CGPointMake(100, 400)];
[myPath addLineToPoint:CGPointMake(400, 400)];
[myPath addLineToPoint:CGPointMake(400, 100)];
[myPath addLineToPoint:CGPointMake(100, 100)];
[myPath closePath];
[[UIColor redColor] setStroke];
[myPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
[self setNeedsDisplay];
}
This is my code to draw the rectangle. Now by using touches i am moving the rectangle, after moving how can i get the new points of the rectangle.In this particular case i am using a rectangle but there may be a polygon of indefinite points, in that case how can i get those points.
For a simple linear transformation you only need to apply dx and dy to each control point of your curve (aka add your change in x and y to all the points you defined for the curve).

CoreGraphics Quartz drawing shadow on transparent alpha path

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];