Bezier Path Not Drawing on context - ios

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

Related

How can I animate the corner radius of a CALayer

I'm trying to create an animation where a CALayer rectangle changes from being flush with the borders of a view to having either the left, right, or both corners rounded and the width of the rectangle is also changed.
The problem I'm having is that the animation looks distorted. If I disable the corner radius change in the animation, the distortion doesn't happen. I suspect this is because when the original path and the final path in the animation have a different number of rounded corners, the paths have a different number of control points, so the path animation behavior is undefined as per the docs.
I'm thinking that I should try and find a way to get the number of control points in the rounded rect to be equal to the number in the non-rounded rect but I'm not sure how I would do this since I haven't found a way to count the number of control points in a CGPath/UIBezierPath.
Here's the code I'm using right now, where I'm animating the path, but I'm open to changing the implementation entirely to 'cheat' by having two rectangles or something like that.
func setSelection(to color: UIColor, connectedLeft: Bool, connectedRight: Bool, animated: Bool) {
self.connectedLeft = connectedLeft
self.connectedRight = connectedRight
self.color = color
if animated {
let pathAnimation = CABasicAnimation(keyPath: "path")
let colorAnimation = CABasicAnimation(keyPath: "fillColor")
self.configure(animation: pathAnimation)
self.configure(animation: colorAnimation)
pathAnimation.toValue = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
colorAnimation.toValue = color.cgColor
let group = CAAnimationGroup()
group.animations = [pathAnimation, colorAnimation]
self.configure(animation: group)
group.delegate = self
self.rectLayer.add(group, forKey: Constants.selectionChangeAnimationKey)
} else {
self.rectLayer.fillColor = color.cgColor
self.rectLayer.path = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
}
}
private func rectPath(connectedLeft: Bool, connectedRight: Bool) -> UIBezierPath {
let spacing: CGFloat = 5
var origin = self.bounds.origin
var size = self.bounds.size
var corners = UIRectCorner()
if !connectedLeft {
origin.x += spacing
size.width -= spacing
corners.formUnion([.topLeft, .bottomLeft])
}
if !connectedRight {
size.width -= spacing
corners.formUnion([.topRight, .bottomRight])
}
let path = UIBezierPath(roundedRect: .init(origin: origin, size: size), byRoundingCorners: corners, cornerRadii: .init(width: 8, height: 8))
print(path.cgPath)
return path
}
You are correct as to why your animation doesn't work correctly.
The Core Graphics/Core Animation methods that draw arcs compose the arcs using variable numbers of cubic bezier curves depending on the angle being drawn.
I believe that a 90 degree corner is rendered as a single cubic bezier curve.
Based on a recent test I did, it seems all you need to do is have the same number of endpoints as a Bezier curve in order to get the sharp-corner-to-rounded-corner animation to draw correctly. For a rounded corner, I'm pretty sure it draws a line segment up to the rounded corner, then the arc as one Bezier curve, then the next line segment. Thus you SHOULD be able to create a rectangle by drawing each sharp corner point twice. I haven't tried it, but I think it would work.
(Based on my understanding of how arcs of circles are drawn using bezier curve, an arc of a circle is composed of a cubic bezier curve for each quarter-circle or part of a quarter circle that is drawn. So for a 90 degree bend, you just need 2 control points for a sharp corner to replace a rounded corner. If the angle is >90 degrees but less than 180, you'd want 3 control points at the corner, and if it's >180 but less than 270, you'd want 4 control points.)
Edit:
The above approach of adding each corner-point twice for a rounded rectangle SHOULD work for the rounded rectangle case (where every corner is exactly 90 degrees) but it doesn't work for irregular polygons where the angle that needs to be rounded varies. I couldn't work out how to handle that case. I already created a demo project that generated irregular polygons with any mixture of sharp and curved corners, so I adapted it to handle animation.
The project now includes a function that generates CGPaths with a settable mixture of rounded and sharp corners that will animate the transition between sharp corners and rounded corners. The function you want to look at is called roundedRectPath(rect:byRoundingCorners:cornerRadius:)
Here is the function header and in-line documentation
/**
This function works like the UIBezierPath initializer `init(roundedRect:byRoundingCorners:cornerRadii:)` It returns a CGPath a rounded rectangle with a mixture of rounded and sharp corners, as specified by the options in `corners`.
Unlike the UIBezierPath `init(roundedRect:byRoundingCorners:cornerRadii:` intitializer, The CGPath that is returned by this function will animate smoothly from rounded to non-rounded corners.
- Parameter rect: The starting rectangle who's corners you wish to round
- Parameter corners: The corners to round
- Parameter cornerRadius: The corner radius to use
- Returns: A CGPath containing for the rounded rectangle.
*/
public func roundedRectPath(rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadius: CGFloat) -> CGPath
You can download the project from Github and try it out at this link.
Here is a GIF of the demo app in operation:

Overlaying image onto CGRect swift

I'm using the following sample app that Apple provides to do some object detection.
https://developer.apple.com/documentation/vision/tracking_multiple_objects_or_rectangles_in_video
I'm trying to paste an image of a face on top of the green rectangle in the video. (Video Download Link: https://drive.google.com/file/d/1aw5L-6uBMTxeuq378Y98dZcTh6N_Y2Pf/view?usp=sharing)
So far, I'm able to detect the green rectangle from the video very consistently, but whenever I try to overlay an image, the frame just does not appear in the view.
Here's what I've tried so far:
In TrackingImageView.swift, I've added an instance variable called faceImage and I've tried adding it to the screen by adding the following code to the bottom of the draw function.
UIGraphicsBeginImageContextWithOptions(self.imageAreaRect.size, false, 0.0)
// self.faceImage.draw(in: CGRect(origin: CGPoint.init(x: rect.minX, y: rect.minY), size: rect.size))
self.faceImage.draw(in: CGRect(x: previous.x, y: previous.y, width: polyRect.boundingBox.width, height: polyRect.boundingBox.height))
// self.faceImage.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.image = newImage
Then in TrackingViewController, in the function called func displayFrame(_ frame: CVPixelBuffer?, withAffineTransform transform: CGAffineTransform, rects: [TrackedPolyRect]?), I've added the following lines.
self.trackingView.faceImage = UIImage(named: "dwight1")
self.trackingView.displayImage(rect: self.trackingView.polyRects[0].boundingBox)
UPDATE, Here's another approach I tried:
This is what it says in the documentation: Use the observation’s boundingBox to determine its location, so you can update your app or UI with the tracked object’s new location. Also use it to seed the next round of tracking.
So in the function func performTracking(type: TrackedObjectType) in VisionTrackerProcessor, I added this:
delegate?.updateImage(observation.boundingBox)
And in TrackingViewController I added this:
func updateImage(_ rect: CGRect) {
print(rect)
self.faceImage.frame = rect
}
And faceImage is this:
#IBOutlet weak var faceImage: UIImageView!
When I print out the CGPoints of the rectangle where I want to place the image, I get the following output:
(0.45066666666666666, 0.5595238095238095, 0.09599999999999997, 0.16666666666666663)
(0.4521519184112549, 0.5643428802490235, 0.09600000381469731, 0.16666666666666663)
(0.4546553611755371, 0.5875609927707248, 0.09555779099464418, 0.16589893764919705)
(0.4543778896331787, 0.5984047359890408, 0.09505770206451414, 0.1650307231479221)
(0.454343843460083, 0.6052030351426866, 0.09476101398468023, 0.16451564364963112)
(0.45296874046325686, 0.6065650092230903, 0.09457258582115169, 0.16418851216634112)
(0.4510493755340576, 0.6057157728407118, 0.09507998228073117, 0.1650694105360243)
(0.4481017589569092, 0.5987161000569662, 0.09499880075454714, 0.16492846806844075)
(0.44568862915039065, 0.5735456678602431, 0.09511266946792607, 0.16512615415785048)
(0.4434205532073975, 0.5485235426161025, 0.09506692290306096, 0.16504673428005645)
(0.4413131237030029, 0.5238201141357421, 0.09566491246223452, 0.1660849147372776)
(0.4388014316558838, 0.5072469923231336, 0.09601176977157588, 0.1666870964898003)
(0.4374812602996826, 0.4967741224500868, 0.09586981534957884, 0.16644064585367835)
(0.43827009201049805, 0.48819330003526473, 0.09551617503166199, 0.1658266809251574)
(0.44115781784057617, 0.4852377573649089, 0.09499365091323853, 0.1649195247226291)
(0.4417849540710449, 0.4845396253797743, 0.0949023962020874, 0.1647610982259115)
(0.4476351737976074, 0.49016346401638455, 0.09391363859176638, 0.16304450564914275)
(0.4497058391571045, 0.49209620157877604, 0.09434010386466984, 0.16378489600287544)
(0.4514862060546875, 0.49223976135253905, 0.09459822773933413, 0.16423302756415475)
(0.454580020904541, 0.4904879252115885, 0.0949873864650726, 0.16490865283542205)
(0.4566154479980469, 0.48613760206434464, 0.09480695724487309, 0.16459540261162653)
(0.45992450714111327, 0.47563196818033854, 0.09525291323661805, 0.1653696378072103)
(0.464534330368042, 0.46896955702039933, 0.09566755294799806, 0.1660895029703776)
(0.4682444095611572, 0.4513437059190538, 0.09700422883033755, 0.16841011047363275)
(0.4709425926208496, 0.438845952351888, 0.09843692183494568, 0.17089743084377712)
(0.47597203254699705, 0.4264893849690755, 0.10058027505874634, 0.17461851967705622)
(0.48175721168518065, 0.42467672559950087, 0.10141149759292606, 0.1760616196526421)
(0.483599328994751, 0.44046991136338975, 0.10279589891433716, 0.17846510145399308)
(0.4847916603088379, 0.44517923990885416, 0.10338790416717525, 0.17949288686116532)
(0.4889643669128418, 0.45437651740180124, 0.09983686804771424, 0.17332788043551978)
(0.49118928909301757, 0.4580091264512804, 0.09644789695739747, 0.16744425031873916)
(0.4905869483947754, 0.45951224433051213, 0.09397981166839603, 0.16315938101874455)
(0.4874621868133545, 0.45792486402723526, 0.09055853486061094, 0.15721967485215932)
(0.48279714584350586, 0.4531046549479167, 0.08872739672660823, 0.1540406121148004)
(0.4783169269561768, 0.4456812964545356, 0.0860174298286438, 0.1493358188205295)
(0.4728221893310547, 0.44693773057725694, 0.084199583530426, 0.14617982440524635)
(0.471103572845459, 0.4579927232530382, 0.08219499588012691, 0.14269964430067272)
(0.4676462173461914, 0.47325596279568144, 0.08054903745651243, 0.1398420651753744)
(0.463164234161377, 0.4803483327229818, 0.07916470766067507, 0.13743872112698025)
(0.4597337245941162, 0.4865601857503255, 0.07723031044006345, 0.1340803888108995)
(0.4575923442840576, 0.4861404842800564, 0.07577759623527525, 0.13155832290649416)
(0.456453275680542, 0.48211678398980035, 0.0741972386837006, 0.12881464428371853)
(0.45630569458007814, 0.47852266099717883, 0.0741972386837006, 0.12881464428371853)
(0.45930023193359376, 0.4749870724148221, 0.0741972386837006, 0.12881464428371847)
(0.4619853973388672, 0.460075675116645, 0.0741972386837006, 0.12881464428371853)
(0.4647641658782959, 0.44653006659613714, 0.0741972386837006, 0.12881464428371858)
(0.46242194175720214, 0.43739403618706596, 0.07220322489738468, 0.1253528171115451)
(0.4625579357147217, 0.41982913547092016, 0.07062785029411311, 0.12261778513590493)
(0.46608676910400393, 0.4134985182020399, 0.06866733431816097, 0.11921412150065108)
(0.46996197700500486, 0.41352043151855467, 0.0672459602355957, 0.11674645741780598)
(0.4733128547668457, 0.42267172071668835, 0.06592562794685364, 0.11445420583089194)
(0.4805797576904297, 0.4420909881591797, 0.06590123176574703, 0.11441185209486215)
(0.48854408264160154, 0.46238810221354165, 0.06529000997543333, 0.11335069868299696)
(0.4921866416931152, 0.47235264248318143, 0.06412824392318728, 0.11133375167846682)
(0.4948731899261475, 0.481452645195855, 0.06294543147087095, 0.10928025775485567)
(0.49323139190673826, 0.48434698316786023, 0.06219365000724797, 0.10797508027818464)
(0.4935962200164795, 0.47917471991644967, 0.061773008108139016, 0.10724479887220595)
(0.49112601280212403, 0.4626174502902561, 0.06177300810813907, 0.107244798872206)
(0.48893303871154786, 0.4498925950792101, 0.06069326996803287, 0.10537025663587785)
(0.4902684688568115, 0.45128373040093317, 0.06060827970504756, 0.10522270202636719)
(0.4870577812194824, 0.45470954047309026, 0.06060827970504756, 0.10522270202636724)
(0.45066666666666666, 0.5595238095238095, 0.09599999999999997, 0.16666666666666663)
(0.45066666666666666, 0.5595238095238095, 0.09599999999999997, 0.16666666666666663)
Any help with overlaying the image on top of my detected object would be amazing. Thanks!
Are you realising that the coordinates you get from the Vision framework are normalised ones(between 0 and 1)?. You will have to transform those to fit the size of your view.
In addition, as far as I remember, Vision coordinates start from the bottom left corner (contrary to UIKit, starting from the top- left), so you might have to flip them vertically as well(not 100% sure here).
Edit:
I see you have available videoReader.affineTransform, you can give it a try modifying your CGRects using that transform.

How to transform UIBezierPath/CGPath within a reference frame?

I need to scale a path, but instead of using the path's bounds, the transform should be performed using a larger, enclosing, arbitrary rect. (i.e. a "canvas".)
My problem is how to do that without drawing the canvas.
For example, in the following image:
the blue square indicates the native rect of the path (a rocket in this case).
the purple rect indicates the bounds of the "canvas" or parent coordinate system.
the purple rect is for positioning only; not to be drawn. The whitespace is similarly ignored.
I need to scale the canvas, and keep the transformed rocket path (for clipping operations, caching etc.). i.e. I need to lose the purple rect after transformation.
Ideally, I'd use something like the following fiction:
var canvasPath = UIBezierPath(rect: canvasRect)
canvasPath.append(rocketPath, withName: "rocket")
canvasPath.doSomeTransformOrOther()
let transformedRocketPath = canvasPath.path(forName: "rocket")
Sadly, the magical append(:withName:) and path(forName:) methods do not exist, even if UIBezierPath or CGPath treated subpaths like this. Alas!
Another approach could be to temporarily add points to the path, one for each canvas corner. Then, transform the path. Then, walk the path with CGPath's (hideous) apply(info:function:) method, rebuild the path without the canvas points, then draw the result. (Yeah, pass the vodka.)
Can anyone offer a more sane solution? Thanks for any helpi
(For those interested: this is related to a problem of how to center irregularly-shaped paths. The purple rect represents the bounds of the minimum enclosing circle. Drawing the rocket with the MEC's coordinate system will make the rocket appear more visually centered, compared to using boundingBox.)
More info: Fitting UIBezierPath/CGPath to circle
Here's my final approach to the problem: use a "canvas" rect that will be scaled along with the path. Then use the canvas rect when translating the (scaled) path. This keeps the path relative to the canvas
An example of doing this is getting a path from an SVG, and then scaling, positioning the path relative not to the path's bounds, but relative to the native dimensions (the 'viewbox') of the SVG. This in turn allows relative positioning of the path within the SVG canvas.
extension UIBezierPath
{
func scaled(usingCanvasSize canvasSize: CGSize,
scale: CGFloat) -> CGRect
{
let canvasRect = CGRect(origin: .zero, size: canvasSize)
let canvasPath = UIBezierPath(rect: canvasRect)
let scaleTransform = CGAffineTransform(scaleX: scale, y: scale)
canvasPath.apply(scaleTransform)
self.apply(scaleTransform)
return canvas.bounds
}
}
// Usage:
let nativeSVGSize: CGSize = ... // 'viewBox' size of SVG
let path = UIBezierPath(...) // From SVG, wherever.
let scale: CGFloat = 0.5
// Scale the path, get a "canvas" rect.
let canvasRect = path.scaled(usingCanvasSize: nativeSVGSize,
scale: scale)
// Use the (scaled) canvas for info about the path from now on.
// e.g. move the path using the center of the canvasRect.
let pathCenter = canvasRect.center
let destCenter = ...
let movement = CGPoint(x: destCenter.x - pathCenter.x,
y: destCenter.y - pathCenter.y)
path.apply(CGAffineTransform(translationX: movement.x,
y: movement.y))

Drawing lines on top of CGImage

I am currently trying to develop a drawing app. The problem that I've come across is optimising the app's performance.
I have subclassed an UIView, in which I am detecting user's inputs.
At first I tried to draw all the lines with CoreGraphics in draw(_ rect: CGRect) method but when the number of lines was 10 000+, the lag was very noticeable. So then I thought of creating a CGImage that I would make after every draw(...) cycle and then when the next draw cycle came, I would put just the new lines on top of that CGImage, therefore not drawing ALL previous lines again and saving precious time.
I am registering new lines in touchesBegan() and touchesMoved() methods and then I am calling self.needsDisplay().
This is the code that I am currently using:
var mainImage: CGImage?
var newLines: [Line]
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
//context?.translateBy(x: 0, y: bounds.height)
//context?.scaleBy(x: 1, y: -1)
if let image = mainImage {
context?.draw(image, in: bounds)
}
for line in newLines {
context?.setStrokeColor(UIColor.black.cgColor)
context?.setLineWidth(1)
context?.move(to: line.previousLocation)
context?.addLine(to: line.location)
context?.strokePath()
}
newLines = []
mainImage = context?.makeImage()
}
Now the problem with it is that the lines I draw are divided in two parts and are mirrored vertically. (Image split vertically). I have drawn here two continuous lines - one straight line and one curved line. The weird thing is that if I flipped half of the UIView, the lines would be aligned continuously with no space between them.
If I uncommented the two lines from my previously mentioned code (context?.translateBy(x: 0, y: bounds.height) and context?.scaleBy(x: 1, y: -1)), the image would look like this.
The only wrong thing now is that I was drawing on the lower side of the screen and getting results on the upper one.
How do I correct it? What is the proper solution for my problem?
Thank you very much!
When you comment out those two lines, then you are NOT making any changes to the graphics context. If you un-comment them, then you ARE changing the context at each call and not saving it. So I think you're essentially flipping it each time. Not sure if it's that but you should definitely be pushing/saving your context before drawing (and obviously restoring/popping it).
Check out this post: using UIImage drawInRect still renders it upside down.

Draw UIBezierPath outside of UIView::drawRect()?

I'm trying to draw a set of small circles around the edge of a view to mirror physical lights (pixels) I'm controlling in my app. These circles will change color frequently. So I've created a PixelSimulator object to draw each circle into a custom UIView object.
Here's the relevant code
class PixelSimulator {
let size: CGSize;
let color: UIColor;
let pixelPath: UIBezierPath;
init (point: CGPoint, size: CGSize, pixel: Pixel) {
self.point = point;
self.size = size;
self.pixel = pixel;
pixelRect = CGRect(origin: point, size: size);
pixelPath = UIBezierPath(ovalInRect: pixelRect);
color = pixel.color;
}
func render () {
UIGraphicsBeginImageContext(size);
color.setFill();
pixelPath.fill();
UIGraphicsEndImageContext();
}
}
I've tried a different approach using CGContextAddEllipseInRect(context, pixelRect) to no avail. I've also tried declaring the pixelPath inside of the render() method, also to no avail.
What do I need to change to get my bezierPath drawn onto the screen at any time, inside of drawRect() or out of it?
I'd remove the image context and just directly draw. I'd call render only from inside draw rect.
When the colour changes I'd update the appropriate simulators and post a notification to let the 'system' know that it happened. In this case a notification is appropriate because you don't want to know what is interested about the change event and multiple things in the system may be interested.
When the notification is received by your view controller it simply calls setNeedsDisplay on the view which will result in the simulators being rendered on the next draw cycle.

Resources