CoreGraphics - Draw stroke of arc - ios

I have a UIBezierPath which is an arc. I only want to draw the stroke to the graphics context (that is, the stroke of the arc, NOT including the line connecting the end and start points of the arc)
Once I’ve done that, I can then add a linear gradient to the context, effectively drawing a gradient on the stroke of an arc.
Here is my drawRect:
let context = UIGraphicsGetCurrentContext()
CGContextSaveGState(context)
CGContextAddPath(context, _progressPathForProgress(_progressToDrawForProgress(progress)).CGPath)
CGContextStrokePath(context)
CGContextClip(context)
let colours = [self.startColour.CGColor, self.endColour.CGColor]
let colourSpace = CGColorSpaceCreateDeviceRGB()
let colourLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradientCreateWithColors(colourSpace, colours, colourLocations)
var startPoint = CGPoint.zeroPoint
var endPoint = CGPoint(x: 0, y: bounds.height)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, CGGradientDrawingOptions.allZeros)
CGContextRestoreGState(context)
But all this does is add a gradient to the entirety of my UIView. Where have I gone wrong?

The path needs to be closed in order to get the clipping to work.
If you don't want to see the closing line you can set the stroke color to UIColor.clearColor() and then close the path.
CGContextSaveGState(context)
CGContextAddPath(context, _progressPathForProgress(_progressToDrawForProgress(progress)).CGPath)
CGContextStrokePath(context)
CGContextSetStrokeColorWithColor(context, UIColor.clearColor().CGColor)
// close the path, you will need to add some more points here, otherwise
// the end point is simply connected to the start point
CGPathCloseSubpath(...)
CGContextClip(context)
// draw the gradient
I'll explain what I mean by adding more points to close the path. The following images are a screenshot from one of my apps, it is an altitude profile with a gradient below the graph.
Closing the path directly would result in this, meaning the gradient would not be drawn down to the x-axis:
To close the path correctly I add points from the last coordinate (x,y) down to the x axis (x,0), then to (0,0) and finally close the path with the first point, like so:
I don't want to see the closing lines, so I use UIColor.clearColor() here.
Hope you get the idea.

Related

Unwanted outlined appearing for a UIBezierPath that is filled and used as a clip for a see thru gradient

I have a path that is filled and also used for a clip to provide a gradient. The rendering of the gradient is just a tiny bit smaller or differently anti-aliased compared with the rendering of the fill. This creates an outline effect:
Is there any way to remove this outline?
Note that:
Using two separately drawn identical paths does not solve the problem
Using no fill, but two gradients instead does not solve the problem. Even with just a gradient over another gradient, you get the outline.
It is possible to get the gradient I want without the outline by using just a solid gradient with no fill, but that makes animating the effects of the gradient more difficult later.
Here is the code:
let path4Path = UIBezierPath()
//ordinary drawing stuff here
path4Path.close()
fillColor.setFill() //set to black fill
path4Path.fill()
context.saveGState()
context.setAlpha(0.9)
path4Path.addClip()
context.drawLinearGradient(allToldGradient, start: CGPoint(x: 52.72, y: 114.48), end: CGPoint(x: 30.22, y: 91.99), options: [])
context.restoreGState()
I think you have to turn off antialiasing like this:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetShouldAntialias(context, NO);
CGContextSetAllowsAntialiasing(context, NO);
CGContextSetInterpolationQuality(context, kCGInterpolationNone);

Bezier Path Not Drawing on context

I'm trying give my user fine selection of a point they touch on a UIImage.
I have a magnifying square in the top left corner that shows where they're touching at 2x zoom. It works well.
I'm trying to add a "crosshair" in the center of the magnifying area to make selection clearer.
With the code below no line is visible.
//Bulk of the magifying code
public override func drawRect(rect: CGRect) {
let context: CGContextRef = UIGraphicsGetCurrentContext()!
CGContextScaleCTM(context, 2, 2)
CGContextTranslateCTM(context, -self.touchPoint.x, -self.touchPoint.y)
drawLine(context)
self.viewToMagnify.layer.renderInContext(context)
}
//Code for drawing horizontal line of crosshair
private func drawLine(ctx: CGContext) {
let lineHeight: CGFloat = 3.0
let lineWidth: CGFloat = min(bounds.width, bounds.height) * 0.3
let horizontalPath = UIBezierPath()
horizontalPath.lineWidth = lineHeight
let hStart = CGPoint(x:bounds.width/2 - lineWidth/2, y:bounds.height/2)
let hEnd = CGPoint(x:bounds.width/2 + lineWidth/2,y:bounds.height/2)
horizontalPath.moveToPoint(hStart)
horizontalPath.addLineToPoint(hEnd)
UIColor.whiteColor().setStroke()
horizontalPath.stroke()
}
It's possible that the line is being drawn but too small or not where I expect it to be.
I've tried other ways of drawing the line like using CGContextAddPath
I think the issue might be related to the renderInContextView not taking my drawing into account, or I'm not adding the path to the context correctly?
The magnification code is based on GJNeilson's work, all I've done is pin the centre point of the magnifying glass to the top left and remove the mask.
I think you're drawing the line then drawing the image over it. Try calling drawLine last.
Also, the scale and translate are still active when you draw the line which may be positioning it offscreen. You might have to reset it using CGContextSaveGState and CGContextRestoreGState

iOS the quality of drawing lines using CGContext is much worse than SKShapeNode in SpriteKit

I'm making a game where the user can draw lines with finger. There are tons of methods on websites. I tried two methods, one using CGContext in UIView (UIKit), and the other using CGPath and SKShapeNode in SpriteKit. But the latter shows much better quality. The first one using CGContext has ugly rough edges.
Please see following screen shots. I also attached part of code for both methods here, (in the touchesMove function).
Note: var ref = CGPathCreateMutable()
CGContext in UIView
CGPathAddLineToPoint(ref, nil, currentPoint.x, currentPoint.y)
UIGraphicsBeginImageContext(self.frame.size)
let context = UIGraphicsGetCurrentContext()
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height))
CGContextAddPath(context, ref)
// 3
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, brushWidth)
CGContextSetRGBStrokeColor(context, red, green, blue, 1.0)
CGContextSetBlendMode(context, CGBlendMode.Normal)
// 4
CGContextStrokePath(context)
// 5
UIGraphicsEndImageContext()
SKShapeNode in SpriteKit
Note: var lineDrawing = SKShapeNode()
CGPathAddLineToPoint(ref, nil, location.x, location.y)
lineDrawing.path = ref
lineDrawing.lineWidth = 3
lineDrawing.strokeColor = UIColor.blackColor()
lineDrawing.alpha = 1.0
self.addChild(lineDrawing)
How can I draw lines in UIView with the same quality of SKShapeNode?
One obvious problem is that you're using an outdated function that doesn't handle Retina screens:
UIGraphicsBeginImageContext(self.frame.size)
You should be using this instead:
UIGraphicsBeginImageContextWithOptions(self.frame.size, false, 0)
The WithOptions version, with 0 as the final argument, creates an image context at Retina resolution if the device has a Retina screen. (The 0 argument means “use the device screen's scale factor”.)
There may be other issues, because you didn't show the code that creates the points in the path for each test case.

iOS - CAShapeLayer.strokeEnd does not seem to work

I am currently trying to draw a segment of an arc.
I managed to draw the segment, but now I want to control the length of the segment via the strokeEnd property but it does not work.
Here is my code:
let arc:CGMutablePathRef = CGPathCreateMutable()
CGPathAddArc(arc, nil, arcCenter.x, arcCenter.y, arcDiameter/2, arcStartAngle, arcStopAngle, false)
let arcLineWidth = 5.0
let strokedArc:CGpathRef = CGPathCreateCopyByStrokingPath(arc, nil, arcLineWidth, kcGLineCapButt, kCGJoinMiter, 10)
arcLayer.path = strokedArc
arcLayer.fillColor = arcColor
arcView.layer.addsublayer (arcLayer)
...
self.addSubview(arcView)
I then try to modify the length of the segment:
arcLayer.strokeEnd = 0.5
But it does not do anything.
Can you please tell me what I am doing wrong?
Thanks,
MG
You are creating a new path by stroking the original path and then you are assigning the new path to the shape layer and filling it.
From the code that you have supplied in your question, you are only configuring the shape layer to fill the shape, not to stroke it. Thus there is no stroke to be affected by the strokeEnd property.
To have the strokeEnd work on the original arc path. Assign the original arc path to the shape layer and configure the stroke color, line width, etc. on the shape layer. If you don't want to fill the arc you will also have to set the fill color to a clear color (I seem to remember that it works if you set it to nil in Swift).

Quartz2D: How to convert a clipping rect to an inverted mask at runtime?

Given:
a CGContextRef (ctx) with frame {0,0,100,100}
and a rect (r), with frame {25,25,50,50}
It's easy to clip the context to that rect:
CGContextClipToRect(ctx, r);
to mask out the red area below (red == mask):
But I want to invert this clipping rect to convert it into a clipping mask. The desired outcome is to mask the red portion below (red == mask):
I want to do this programmatically at runtime.
I do not want to manually prepare a bitmap image to ship statically with my app.
Given ctx and r, how can this be done at runtime most easily/straightforwardly?
Read about fill rules in the “Filling a Path” section of the Quartz 2D Programming Guide.
In your case, the easiest thing to do is use the even-odd fill rule. Create a path consisting of your small rectangle, and a much larger rectangle:
CGContextBeginPath(ctx);
CGContextAddRect(ctx, CGRectMake(25,25,50,50));
CGContextAddRect(ctx, CGRectInfinite);
Then, intersect this path into the clipping path using the even-odd fill rule:
CGContextEOClip(ctx);
You could clip the context with CGContextClipToRects() by passing rects that make up the red frame you've wanted.
Can you just do all your painting as normal, and then do:
CGContextClearRect(ctx, r);
after everything has been done?
Here is a helpful extension for implementing rob's answer
extension UIBezierPath {
func addClipInverse() {
let paths = UIBezierPath()
paths.append(self)
paths.append(.init(rect: .infinite))
paths.usesEvenOddFillRule = true
paths.addClip()
}
}

Resources