Swift Combine and reverse UIBezierPaths - swift

The problem I am facing is reversing subpaths. Take this example:
let circlePaths: [UIBezierPath] = ...
let rectanglePath: UIBezierPath = ... // a rectangle
let totalPath: UIBezierPath = .init()
for path in circlePaths {
totalPath.append(path)
}
rectanglePath.append(totalPath)
It should look like this:
Now ideally I want to cut out all the circles using
bezierPath.append(totalPath.reversing())
However the effect is not as expected. I expect the two circles to make up a path and this one is reversed, however in reality both circle paths are reversed, which causes the intersection to be part of the path (reversing() twice has no effect). I'd like to combine the circle paths into one with the intersection not being present but as part of the path. I want the smaller circle to "extend" the larger circle as a path.
Any idea how I would do it?
Edit 1: Here is an image how the resulting path should look like.

If you need to actually create a single path as the combination / union of your multiple paths, you may want to look at one of the libraries that are out there.
However, if you only need that visual output, this might be a usable approach.
Create 3 paths -- outer rect, large circle, small circle.
Stroke and Fill the outer rect path
Stroke both circle paths
Fill both circle paths
An example UIView class:
class FakeUnionUIBezierPaths : UIView {
override func draw(_ rect: CGRect) {
// yellow line width
let lWidth: CGFloat = 8
// paths are stroked on the center, so
// use one-half the line width to adjust ediges
let hWidth: CGFloat = lWidth * 0.5
// border / outer rect, inset by one-half line width
let dRect: CGRect = rect.insetBy(dx: hWidth, dy: hWidth)
// first circle
// full-height
// aligned to left edge
var c1Rect: CGRect = dRect
c1Rect.size.width = c1Rect.height
// second circle
// half-height
// aligned to right edge
var c2Rect: CGRect = dRect
c2Rect.size.height *= 0.5
c2Rect.size.width = c2Rect.height
c2Rect.origin.x = dRect.width - c2Rect.width + hWidth
c2Rect.origin.y = dRect.height * 0.25
let pRect: UIBezierPath = UIBezierPath(rect: dRect)
let p1: UIBezierPath = UIBezierPath(ovalIn: c1Rect)
let p2: UIBezierPath = UIBezierPath(ovalIn: c2Rect)
UIColor.yellow.setStroke()
UIColor.blue.setFill()
// same line-width for all three paths
[pRect, p1, p2].forEach { p in
p.lineWidth = lWidth
}
// stroke and fill the border / outer rect
pRect.stroke()
pRect.fill()
// stroke both circle paths
p1.stroke()
p2.stroke()
// fill both circle paths
p1.fill()
p2.fill()
}
}
Output:

Related

Swift. UIBezierPath shape detection

I working with UIBezierPath and shape detection. For painting im using "UIPanGestureRecognizer".
Example of code:
My shape definder
var gesture = UIPanGestureRecognizer()
.
.
.
view.addGestureRecognizer(gesture.onChange { \[weak self\] gesture in
let point = gesture.location(in: self.view)
let shapeL = CAShapeLayer()
shapeL.strokeColor = UIColor.black.cgColor
shapeL.lineWidth = 2
shapeL.fillColor = UIColor.clear.cgColor
switch gesture.state {
case .began:
//some code
currentBezierPath = UIBezierPath()
break
case .changed:
//some code
shapeLayer.path = self.currentBezierPath.cgPath
break
case .ended:
//define what user was painted(circle, rectangle, etc)
shapeDefinder(path: currentBezierPath)
break
default:
break
})
shapeDefinder
func shapeDefinder(path: UIBezierPath) {
if(path.hasFourRightAngles()){
// square
}
}
extension hasFourRightAngles
extension UIBezierPath {
func hasFourRightAngles() -> Bool {
guard self.currentPoint != .zero else {
// empty path cannot have angles
return false
}
let bounds = self.bounds
let points = [
bounds.origin,
CGPoint(x: bounds.minX, y: bounds.minY),
CGPoint(x: bounds.maxX, y: bounds.maxY),
CGPoint(x: bounds.minX, y: bounds.maxY)
]
let angleTolerance = 5.0 // in degrees
var rightAngleCount = 0
for i in 0...3 {
let p1 = points[i]
let p2 = points[(i+1)%4]
let p3 = points[(i+2)%4]
let angle = p2.angle(between: p1, and: p3)
if abs(angle - 90) <= angleTolerance {
rightAngleCount += 1
}
}
return rightAngleCount >= 4
}
}
and
extension CGPoint {
func angle(between p1: CGPoint, and p2: CGPoint) -\> CGFloat {
let dx1 = self.x - p1.x
let dy1 = self.y - p1.y
let dx2 = p2.x - self.x
let dy2 = p2.y - self.y
let dotProduct = dx1*dx2 + dy1*dy2
let crossProduct = dx1*dy2 - dx2*dy1
return atan2(crossProduct, dotProduct) \* 180 / .pi
}
}
but my method hasFourRightAngles() doesnt work, it always has true.
Cant understand how i can detect square(the user must draw exactly a square, if the user draws a circle, then the check should not pass.)
Maybe someone know about some library which works with UIBezierPath for detect shapes?
The bounds of a path are always a rectangle, no matter the shape, so you should expect this function to always return true. From the docs:
The value in this property represents the smallest rectangle that completely encloses all points in the path, including any control points for Bézier and quadratic curves.
If you want to consider the components of the path itself, you'd need to iterate over its components using it's CGPath. See applyWithBlock for how to get the elements. That said, this probably won't work very well, since you likely don't care precisely how the shape was drawn. If you go down this road, you'll probably want to do some work to simplify the curve first, and perhaps put the stokes in a useful order.
If the drawing pattern itself is the important thing (i.e. the user's gesture is what matters), then I would probably keep track of whether this could be a rectangle at each point of the drawing. Either it needs to be roughly colinear to the previous line, or roughly normal. And then the final point must be close to the original point.
The better approach is possibly to consider the final image of the shape, regardless of how it was drawn, and then classify it. For various algorithms to do that, see How to identify different objects in an image?
Your code to get the bounds of your path will not let you tell if the lines inside the path make right angles. As Rob says in his answer, the bounding box of a path will always be a rectangle, so your current test will always return true.
The bounding box of a circle will be a square, as will the box of any shape who's horizontal and vertical maxima and minima are equal.
It is possible to interrogate the internal elements of the underlying CGPath and look for a series of lines that make a square. I suggest searching for code that parses the elements of a CGPath.
Note that if you are checking freehand drawing, you will likely need some "slop" in your calculations to allow for shapes that are close to, but not exactly squares, or you will likely never find a perfect square.
Also, what if the path contains a square plus other shape elements? You will need to decide how to handle situations like that.

NSBezierPath stroke not scaled correctly

I have what should be a simple subclass of an NSView that draws circular nodes at specified locations.
To render the nodes in my view, I translate the graphics context's origin to the center of the view's frame and scale it such that it spans from -1.25 to 1.25 in the limiting dimension (the node coordinates are all in the range -1...1). I then create for each node an NSBezierPath using the ovalIn: constructor. Finally, I fill the path with yellow and stroke it with black.
But... While the yellow fill looks ok, the black outline is not being scaled correctly!
What am I missing?
Here's the code:
override func draw(_ dirtyRect: NSRect)
{
let nodeRadius = CGFloat(0.05)
let unscaledSpan = CGFloat(2.5)
super.draw(dirtyRect)
NSColor.white.set()
self.frame.fill()
guard let graph = graph else { return }
let scale = min(bounds.width/unscaledSpan, bounds.height/unscaledSpan)
NSGraphicsContext.current?.saveGraphicsState()
defer { NSGraphicsContext.current?.restoreGraphicsState() }
let xform = NSAffineTransform()
xform.translateX( by: 0.5*bounds.width, yBy: 0.5*bounds.height)
xform.scale(by: scale)
xform.concat()
for v in graph.vertices
{
let r = NSRect(x: v.x-nodeRadius, y: v.y-nodeRadius, width: 2.0*nodeRadius, height: 2.0*nodeRadius)
let p = NSBezierPath(ovalIn: r)
NSColor.yellow.set()
p.fill()
NSColor.black.set()
p.stroke()
}
}
This is what I'm seeing (shown with two different window sizes)
Clearly, the translation is working fine for both fill and stroke.
But, the scaling is off for stroke.
Thanks for any/all hints/suggestions.
Doh... I wasn't considering the effect of scaling on the line width.
Made the following edit and all is well:
...
let p = NSBezierPath(ovalIn: r)
p.lineWidth = CGFloat(0.01)
NSColor.yellow.set()
p.fill()
NSColor.black.set()
p.stroke()
...

iOS Fill half of UIBezierPath with other color without CAGradientLayer

I have a question about drawing half of a UIBezierPath. How do I fill left part (left from thumb) with green color and right part (right from thumb) with white color without using CAGradientLayer?
Code I used to create Bezier Path - https://gist.github.com/robertmryan/67484c74297cede3926a3aed2fceedb9
Screenshot of what I want to achieve:
One approach is to add a mask layer to your curved-path shape layer.
When the "thumb" position changes, change the width of the mask to reveal only the "left-side" of the shape layer.
Create a shape layer to use as the mask:
let maskLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.black.cgColor
return layer
}()
In viewDidLoad() set that layer as the mask for the curved-shape layer:
pathLayer.mask = maskLayer
Whenever the "thumb" position is set, update the width of the mask:
func updateMask(at point: CGPoint) -> Void {
var f = view.bounds
f.size.width = point.x
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.frame = f
CATransaction.commit()
}
I posted a modified version of your gist at: https://gist.github.com/DonMag/a2154e70a3c67193a7b19bee41c8fe95
It really has only a few changes... look for comments beginning with // DonMag -
Here is the result (with an imageView behind it to show the transparency):
Edit
After comments, the goal is to have the "right-side" of the track path be white instead of transparent.
Using the same approach, we can add a white shape layer on top of the original shape layer, and mask it to show only the right-hand-side.
Here is an updated gist - https://gist.github.com/DonMag/397dfbe4779e817531ef7a663365b2e7 - showing this result:

Change drawing order of multiple CAShapeLayers

I have my program drawing several different CGMutablePaths, each of which belongs to its own CAShapeLayer. It currently looks like this
Notice how the lines of the rectangle overlap the circles. Here is how I want it to look
Essentially, I want the ellipses and their fill to be drawn on top of the lines of the rectangle.
In my code, I have an array of CAShapeLayers, the first of which is the rectangle. In awakeFromNib(), I loop through all the shape layers and update information like the stroke width and fill, then I manually change the fill color of the rectangle to be different. In the draw function, I create all my paths, rectangle first, then ellipses. Finally, I add those paths to their relative shape layer, ellipses after the rectangle.
I have tried swapping the order in each case, but no luck. I honestly can't think of anything else that might be effecting draw order. The strangest part about it is that, in the first image, you'll see that the bottom left ellipse has indeed been drawn above the rectangle, but nowhere in my code is it set apart.
var shapeLayers: [String: CAShapeLayer] = [
"rectangle": CAShapeLayer(),
"ellipse1": CAShapeLayer(),
"ellipse2": CAShapeLayer()...and so on
]
override func awakeFromNib() {
wantsLayer = true
for (_, shapeLayer) in shapeLayers {
shapeLayer.lineWidth = borderWidth
shapeLayer.strokeColor = ColorScheme.blue.cgColor
shapeLayer.fillColor = ColorScheme.white.cgColor
shapeLayer.path = nil
shapeLayer.lineCap = kCALineCapSquare
layer?.addSublayer(shapeLayer)
}
shapeLayers["rectangle"]?.fillColor = NSColor.clear.cgColor
}
override func draw(_ dirtyRect: NSRect) {
let rectangle = CGMutablePath().addRect(selectionBounds)
let ellipse1 = CGMutablePath().addEllipse(in: CGRect(origin: somePoint, size: someSize))
let ellipse2 = CGMutablePath().addEllipse(in: CGRect(origin: somePoint, size: someSize))
...and so on
shapeLayers["rectangle"]?.path = rectangle
shapeLayers["ellipse1"]?.path = ellipse1
shapeLayers["ellipse2"]?.path = ellipse2...and so on
}
I'm clearly missing something basic in my understanding of shapeLayers and paths. Your help is greatly appreciated - TIA
You can use the zPosition to force the layers to draw in a specific order. Just assign them arbitrary values as long as the numbers ascend from back layer to front layer.

Is there a way to spawn nodes uniformly?

I have a game where circles are moving up and down lines like so:
http://i.stack.imgur.com/5zKS4.png
, and Im using a .plist file to generate them, but its a little monotonous.Is there another way to generate several columns of circles uniformly without using a .plist file? Will post code if necessary.
You could write a function like this:
func createCirclesOnLine(line : CGFloat){
//Set this to however low you want the circles to start.
var currentY : CGFloat = -20.0
//Set this to however high you want the circles to go
let maxY = self.size.height + 100
//Set this to the spacing you want between the circles
let spacing : CGFloat = 50
while currentY < maxY {
let circle = SKShapeNode(circleOfRadius: 20)
circle.position = CGPoint(x: line, y: currentY)
self.addChild(circle)
currentY += spacing
}
}
And then pass it the line (think longitude) of wherever you want the circles to be drawn:
let line = self.size.width / 2
createCirclesOnLine(line)
Is that what you're looking for?
EDIT: To answer your question about how to use this multiple times:
If you want multiple lines of circles on the screen, you can simply decide what lines (again, think longitude) you want to place the circles on. For instance, if I wanted three equally spaced lines of circles on the screen, I would do the following:
let line = self.size.width / 4
createCirclesOnLine(line)
createCirclesOnLine(line * 2)
createCirclesOnLine(line * 3)
If you try this and don't see all of the lines, you might need to add this
scene.size = skView.bounds.size
to the viewDidLoad method in your ViewController.swift right under
skView.ignoresSiblingOrder = true
since you are running the app in portrait mode.