How to draw a multiple point line using UIBezierPath with gradient - objective-c

I need to draw a lot of lines.
I'm using UIBezierPath for drawing lines and small 5x10 pattern image for gradient.
Here's the part of code below:
l1 = [UIBezierPath bezierPath];
[l1 moveToPoint:CGPointMake(76, 373)];
[l1 addLineToPoint:CGPointMake(940, 373)];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
CGRect pathRect = self.view.frame;
path = l1;
pathLayer.frame = self.view.bounds;
pathLayer.bounds = pathRect;
pathLayer.geometryFlipped = NO;
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"line1Pattern.png"]];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 8.0f;
pathLayer.opacity = 1;
[pathLayer setShadowOffset:CGSizeMake(0, 5)];
[pathLayer setShadowOpacity:0.5];
pathLayer.lineCap = kCALineCapRound;
pathLayer.lineJoin = kCALineJoinRound;
[pathArray addObject:pathLayer];
self.pathLayer = pathLayer;
If the line is straight horizontal (like blue and red) - everything is ok and the result is great.
But if I draw tilted line, or straight and then tiled - the result is not ok.
Here's the link to image: left side - what I need, right side - what mu result: example
So, as I understand - I need to rotate the gradient to match the line. But I cant imagine how to realize that.
Also some lines could have zig-zag form like on this graph: example
Could anyone help me with this problem? Or suggest another way of drawing lines.
Thanks!

You can draw only horizontal lines and rotate them with transform. But if you'll need a curve, then everything will be much more complicated.
Also you can create a gradient and draw it instead of tiled image.

Related

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.

Antialiasing on CAShapeLayers

I have a custom view for a loading indicator which is comprised of a number of CAShapeLayers which I perform various animations on to represent various states. In the init method I create a number of shape layers and add them as sublayers to the view's layer. The issue I'm having is the shapes that I'm drawing have very rough edges and don't seem to be antialiased. I've tried a number of things from similar answers on here but I can't seem to antialias the shapes. I've never had this problem before then drawing directly in drawRect.
Is there anything I'm missing or is there a better way to accomplish what I'm trying to do?
Update: Here's a comparison of how the shapes are being drawn:
On the left I'm drawing in layoutSubviews using this code:
CGFloat lineWidth = 2.5f;
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGPoint center = CGPointMake(CGRectGetWidth(self.frame)/2, CGRectGetHeight(self.frame)/2);
CGFloat startAngle = - ((float)M_PI / 2);
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *trackPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
self.trackLayer.path = trackPath.CGPath;
self.trackLayer.lineWidth = lineWidth;
On the right I'm calling a separate method from awakeFromNib:
-(void)awakeFromNib
{
....
self.trackLayer = [self trackShape]
[self.layer addSublayer:self.trackLayer];
}
-(CAShapeLayer*)trackShape
{
float start_angle = 2*M_PI*-M_PI_2;
float end_angle = 2*M_PI*1-M_PI_2;
float minSize = MIN(self.frame.size.width, self.frame.size.height);
float radius = (minSize-_strokeWidth)/2 -4;
CGPoint center = CGPointMake(self.frame.size.width/2,self.frame.size.height/2);
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:start_angle
endAngle:end_angle
clockwise:YES].CGPath;
circle.strokeColor = self.trackColor.CGColor;
circle.fillColor = nil;
circle.lineWidth = (_strokeWidth == -1.0) ? minSize * _strokeWidthRatio
: _strokeWidth;
circle.rasterizationScale = [[UIScreen mainScreen] scale];
return circle;
}
The picture you provided is exactly what happened to me. I got the chunky circle. In my case, the code was inside the drawRect method. The problem was that I was adding a CAShapeLayer, as a subLayer of my view, every time drawRect was called.
So to anyone who stumbles upon something similar, keep track of the CAShapeLayer you are adding or clear everything before drawing again in the drawRect.

Draw a common outline for the intersecting UIViews (ObjectiveC)?

I have a UIView. Inside this UIView, I have two intersecting subviews. I want to draw a common outline between these two sub-UIViews. How do I get about this?
To make it clear:
[mainView addSubview:subView1];
[mainView addSubview:subView2];
I need to draw an outline for both these intersecting UIViews.
You can combine shapes of both rectangles and create new sublayer from the final shape. This layer will act as your outline.
See my code, its fully commented:
// Create shape of merged rectangles
UIBezierPath *outlinePath = [UIBezierPath bezierPathWithRect:subView1.frame];
[outlinePath appendPath:[UIBezierPath bezierPathWithRect:subView2.frame]];
// Configure the outline
UIColor *outlineColor = [UIColor redColor];
CGFloat outlineWidth = 1.0f * [[UIScreen mainScreen] scale];
// Create shape layer representing the outline shape
CAShapeLayer *outlineLayer = [CAShapeLayer layer];
outlineLayer.frame = mainView.bounds;
outlineLayer.fillColor = outlineColor.CGColor;
outlineLayer.strokeColor = outlineColor.CGColor;
outlineLayer.lineWidth = outlineWidth;
// Set the path to the layer
outlineLayer.path = outlinePath.CGPath;
// Add sublayer at index 0 - below everything already in the view
// so it looks like the border
// "outlineLayer" is in fact the shape of the combined rectangles
// outset by layer border
// Moving it at the bottom of layer hierarchy imitates the border
[mainView.layer insertSublayer:outlineLayer atIndex:0];
Output looks like this:
Edit: I misunderstood the question at first. Below is my original answer which outlines rects intersection.
You can just create another view which will be representing the intersection and add it above the two subviews. It's frame would be intersection of the subView1 and subView2 frames. Just give it clear background color and border using its layer property
Your code could look like this:
// Calculate intersection of 2 views
CGRect intersectionRect = CGRectIntersection(subView1.frame, subView2.frame);
// Create intersection view
UIView *intersectionView = [[UIView alloc] initWithFrame:intersectionRect];
[mainView addSubview:intersectionView];
// Configure intersection view (no background color, only border)
intersectionView.backgroundColor = [UIColor clearColor];
intersectionView.layer.borderColor = [UIColor redColor].CGColor;
intersectionView.layer.borderWidth = 1.0f;

Drawing a CGPath UIBezierCurve

Hi all I am looking into ways of how I can draw a shape like the one in the illustration above.I have been looking and reading but getting slightly confused of how curves are drawn using UIBezierPath. I found really nice code which uses CAShapeLayer with animation to draw lines.
The code so far I have is :
#synthesize animationLayer = _animationLayer;
#synthesize pathLayer = _pathLayer;
#synthesize penLayer = _penLayer;
- (void) setupDrawingLayer
{
if (self.pathLayer != nil) {
[self.penLayer removeFromSuperlayer];
[self.pathLayer removeFromSuperlayer];
self.pathLayer = nil;
self.penLayer = nil;
}
CGPoint upperCurve = CGPointMake(101, 100);
CGPoint lowerCurve = CGPointMake(224,200);
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineCapStyle = kCGLineCapRound;
path.miterLimit = -10.0f;
path.lineWidth = 10.0f;
[path moveToPoint:lowerCurve];
[path addQuadCurveToPoint:upperCurve controlPoint:lowerCurve];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.animationLayer.bounds;
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [[UIColor blackColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 10.0f;
pathLayer.lineJoin = kCALineJoinBevel;
[self.animationLayer addSublayer:pathLayer];
self.pathLayer = pathLayer;
}
-(void) startAnimation
{
[self.pathLayer removeAllAnimations];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 10.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.animationLayer = [CALayer layer];
self.animationLayer.frame = CGRectMake(20.0f, 64.0f,
CGRectGetWidth(self.view.layer.bounds) - 40.0f,
CGRectGetHeight(self.view.layer.bounds) - 84.0f);
[self.view.layer addSublayer:self.animationLayer];
[self setupDrawingLayer];
[self startAnimation];
}
The way I solve this kind of problem is to draw the shape in a drawing program such as Illustrator. This shows clearly where the bezier curve points need to go in order to get the curve I'm after.
A UIBezierPath generally starts with moveToPoint to set the starting point of the curve. Then it's followed by any number of curve segments using these methods:
– addLineToPoint:
– addArcWithCenter:radius:startAngle:endAngle:clockwise:
– addCurveToPoint:controlPoint1:controlPoint2:
– addQuadCurveToPoint:controlPoint:
You didn't state specifically what you're having trouble with, so I'm going to make a leap and assume it is addCurveToPoint:controlPoint1:controlPoint2 that you are struggling with.
The segment added by this call draws a segment starting at the most recently added or moved to point in the curve to the first argument. How it undulates is determined by the control points.
The simplest way to understand how it undulates is to imagine lines connecting the first point (established in the previous method call) to the first control point (let's call this control point line segment 1), and another line connecting the first argument (ending point of the segment being added) to the second control point (let's call this control point line segment 2).
The Bezier curve at the starting point is tangent to control point line segment 1. It is tangent to control point line segment 2 at the end of the curve.
So if you want to draw a curve with multiple Beziers such that they form a smooth line, you need to make sure that the slope of control point line segment 2 of one curve is the same as the slope of control point line segment 1 of the next curve that joins to it.
The distances from the starting point to the first control point, and the ending point to the second control point determine the curvature of the curve.
A friend of mine imagines it this way. Imagine a space ship traveling from point A to point B. The space ship starts out with a heading determined by the slope of control point line segment 1 and a speed determined by its length. The heading is gradually changed to the slope of control point line segment 2. Meanwhile, the speed is gradually changed to the length of of control point line segment 2. By the time the space ship arrives at point B, it is traveling tangent to that segment.

Draw image into area between two concentric circles in Xcode / objective-c

I have an image (its actually circular) and wish to only draw the part of that image that corresponds to the area between two concentric circles, the outer one is fixed, the inner one can adjust in size.
The outer concentric circle will always correspond to the outside edge of the image - but a "hole" needs to be left in the drawn image - corresponding to the size of the smaller concentric circle.
I'm trying to do this in objective-c for iOS - to draw a variable-sized "viewfinder" control.
Any help much appreciated.
Sounds like the standard case to use CGContextEOClip. Haven't checked this code, but something like:
CGContextRef ctxt = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, outerCircle);
CGPathAddEllipseInRect(path, NULL, innerCircle);
CGContextEOClip(ctxt);
//Draw your stuff
The easiest and most performant solution would probably be to use a UIImageView and apply a CAShapeLayer as the mask of its layer.
Example:
UIImage *image = ...;
CGFloat ringWidth = 20.0;
CGRect outerCircleRect = CGRectMake(0, 0, image.size.width, image.size.height);
CGRect innerCircleRect = CGRectInset(outerCircleRect, ringWidth, ringWidth);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:outerCircleRect];
[path appendPath:[UIBezierPath bezierPathWithOvalInRect:innerCircleRect]];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.fillRule = kCAFillRuleEvenOdd;
shapeLayer.path = [path CGPath];
UIImageView *imageView = [[[UIImageView alloc] initWithImage:image] autorelease];
imageView.layer.mask = shapeLayer;
[self.view addSubview:imageView];
You'll have to include the QuartzCore framework for this to work.
When you change the radius of the inner circle, you can simply assign a new path to the shape layer. This can even be animated.