Two UIBezierPaths intersection as a UIBezierPath - ios

I have two UIBezierPaths, one that represents the polygon of part of an image and the other is a path to be drawn on top of it.
I need to find the intersection between them so that only the point that are inside this intersection area will be colored.
Is there a method in UIBezierPath that can find intersection points - or a new path - between two paths ?

I'm not aware of a method for getting a new path that's the intersection of two paths, but you can fill or otherwise draw in the intersection by using the clipping property of each path.
In this example, there are two paths, a square and a circle:
let path1 = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100))
let path2 = UIBezierPath(ovalIn: CGRect(x:50, y:50, width: 100, height: 100))
I make a renderer to draw these, but you could be doing this in drawRect or wherever:
let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: 0, y: 0, width: 200, height: 200))
let image = renderer.image {
context in
// You wouldn't actually stroke the paths, this is just to illustrate where they are
UIColor.gray.setStroke()
path1.stroke()
path2.stroke()
// You would just do this part
path1.addClip()
path2.addClip()
UIColor.red.setFill()
context.fill(context.format.bounds)
}
The resulting image looks like this (I've stroked each path for clarity as indicated in the code comments, in actual fact you'd only do the fill part):

I wrote a UIBezierPath library that will let you cut a given closed path into sub shapes based on an intersecting path. It'll do essentially exactly what you're looking for: https://github.com/adamwulf/ClippingBezier
NSArray<UIBezierPath*>* componentShapes = [shapePath uniqueShapesCreatedFromSlicingWithUnclosedPath:scissorPath];
Alternatively, you can also just find the intersection points:
NSArray* intersections = [scissorPath findIntersectionsWithClosedPath:shapePath andBeginsInside:nil];

Related

Can't get Swift UIBezierPath plot to remove and then update

In my app, viewdidLoad() uses UIBezierPath to display green UIBezierPath plotted line.
But later in the program, an event callback uses same UIBezierPath to removeAllPoints and display red line next to green one.
But, all it does is turn the green line to red, at same position.
Ultimately, I want to use UIBezierPath to each second show an ever-changing, multi-line plot --
that is: remove previous multi-line plot and show updated multi-line plot, every second.
CODE THAT SHOWS GREEN LINE...................
var sensor_plot_UIBezierPath = UIBezierPath()
var sensor_plot_CAShapeLayer = CAShapeLayer()
override func viewDidLoad()
{
sensor_plot_UIBezierPath.move(to: CGPoint( x: 100,
y: 100))
sensor_plot_UIBezierPath.addLine( to: CGPoint( x: 200,
y: 200 ) )
init_app_display()
...start background events...
}
func init_app_display()
{
sensor_plot_CAShapeLayer.path = sensor_plot_UIBezierPath.cgPath
sensor_plot_CAShapeLayer.fillColor = UIColor.clear.cgColor // doesn't matter
sensor_plot_CAShapeLayer.strokeColor = UIColor.green.cgColor
sensor_plot_CAShapeLayer.lineWidth = 3.0
view.layer.addSublayer( sensor_plot_CAShapeLayer )
}
CODE THAT IS SUPPOSED TO SHOW RED LINE NEXT TO IT.................
(but actually turns initial green line to red, at same position -- no 2nd line next to first)
...display_plot_update() is called by event from background thread (specifically, BLE event didUpdateValueFor)
func display_plot_update()
{
DispatchQueue.main.async
{
self.sensor_plot_UIBezierPath.removeAllPoints()
self.sensor_plot_UIBezierPath.move(to: CGPoint( x: 110,
y: 100))
self.sensor_plot_UIBezierPath.addLine( to: CGPoint( x: 210,
y: 200 ) )
self.sensor_plot_CAShapeLayer.strokeColor = UIColor.red.cgColor
}
}
You don't show the code that installs your path in your shape layer, or the code that installs your shape layer as a sublayer of your view controller's view. You should show that code so that we can see what you're doing. You should also show the code for your init_app_display() function.
I assume that somewhere you have code that says:
sensor_plot_CAShapeLayer.path = self.sensor_plot_UIBezierPath.cgPath
and code that does something like
view.layer.addSublayer(sensor_plot_CAShapeLayer)
You seem to be under the impression that setting a CAShapeLayer's path to a specific bezier path links the shape layer to that path, and updating the path updates the shape layer. It doesn't work that way. After changing your path, you need to install the new path into your shape layer.
You could change your display_plot_update() function like this:
func display_plot_update()
{
DispatchQueue.main.async
{
// Create a new bezier path.
let newPath = UIBezierPath()
newPath.move(to: CGPoint( x: 110,
y: 100))
newPath.addLine( to: CGPoint( x: 210,
y: 200 ) )
self.sensor_plot_CAShapeLayer.strokeColor = UIColor.red.cgColor
self.sensor_plot_CAShapeLayer.path = newPath.cgPath // The important part
}
}
That would move the line to your new coordinates and make it red.
If you want to add a second line next to the first, then write your `display_plot_update()~ function like this:
func display_plot_update()
{
DispatchQueue.main.async
{
// Add the new line to the existing path (without clearing it)
self.sensor_plot_UIBezierPath.move(to: CGPoint( x: 110,
y: 100))
self.sensor_plot_UIBezierPath.addLine( to: CGPoint( x: 210,
y: 200 ) )
self.sensor_plot_CAShapeLayer.strokeColor = UIColor.red.cgColor
sensor_plot_CAShapeLayer.path = self.sensor_plot_UIBezierPath.cgPath // The important part
}
}
Note that you can't have lines of different colors in a single shape layer. A shape layer can only contain a single path, and it draws the entire path using a single stroke color and a single fill color. (The stroke color and the fill color can be different, but the entire path will use those same colors.)
Rather than trying to remove all the points from a path and reuse the path object, just construct a new path.
Once a path has been drawn, it’s part of the context, you can’t remove or change the graphic by changing the path. If you want to remove something from the drawing you have to erase the context (or part of it) and redraw where you’ve erased.
Usually you erase everything and redraw if.
You also should not be drawing in viewDidLoad, the view might not even be connected to a drawing surface at that point. Drawing should be done by the view, not the view controller.

Plotting waveform in circular path

Trying to plot file waveform in circular form and looking through the underlying EZAudio's EZAudioPlot class docs tells me this should be possible. However I can't quite wrap my head how to construct the 'points' argument in the linked method. Anyone done this and could point to correct way?
UPDATE:
Looking into this I think I've gotten fairly good grasp how EZAudioPlotting works. The actual waveform drawing is done on EZAudioPlotWaveformLayer which is just simple CAShapeLayer subclass. On creation of EZAudioPlot one EZAudioPlotWaveformLayer is created and reference stored on EZAudioPlot class waveformLayer property.
The actual waveform is CGPath on EZAudioPlotWaveformLayer class and to manipulate it it should go something like below.
// Setup audio data for the plot
let file = EZAudioFile(url: url)
guard let data = file?.getWaveformData(withNumberOfPoints: pointCount) else {return}
// Setup the plot
let waveform = EZAudioPlot()
waveform.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
waveform.gain = 10.0
// + any other property tweaking you need
//Get hold of the underlaying layer
let myLayer = waveform.waveformLayer!
// Create the default presentation for the waveforms as CGPath
let path = waveform.createPath(withPoints: waveform.points, pointCount: pointCount, in: EZRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)).takeRetainedValue()
// ??? Here somehow tweak the CGPath eg. into circular shape ???
newPath = plotPointsIntoCircleOrSomeOtherNiftyFunc(path)
// Stick new path to layer
myLayer.path = newPath
Only thing is that currently I haven't figured any math to actually implement anything meaningful at that pseudo code point

How to create a ring with 3D effect using Sprite Kit?

I want to create a ring with a 3D effect using Sprite Kit. (SEE IMAGES)
I tried subclassing a SKNode and adding two nodes as children. (SEE CODE)
One node was a complete SKShapeNode ellipse, and the other was half ellipse using SKCropNode with a higher zPosition.
It looks good, but the SKCropNode increases the app CPU usage from 40% to 99%.
Any ideas on how to reduce the SKCropNode performance cost, or any alternative to create the same ring 3D effect?
class RingNode: SKNode {
let size: CGSize
init(size: CGSize, color: SKColor)
{
self.size = size
self.color = color
super.init()
ringPartsSetup()
}
private func ringPartsSetup() {
// LEFT PART (half ellipse)
let ellipseNodeLeft = getEllipseNode()
let leftMask = SKSpriteNode(texture: nil, color: SKColor.blackColor(), size: CGSize(
width: ellipseNodeLeft.frame.size.width/2,
height: ellipseNodeLeft.frame.size.height))
leftMask.anchorPoint = CGPoint(x: 0, y: 0.5)
leftMask.position = CGPoint(x: -ellipseNodeLeft.frame.size.width/2, y: 0)
let leftNode = SKCropNode()
leftNode.addChild(ellipseNodeLeft)
leftNode.maskNode = leftMask
leftNode.zPosition = 10 // Higher zPosition for 3D effect
leftNode.position = CGPoint(x: -leftNode.frame.size.width/4, y: 0)
addChild(leftNode)
// RIGHT PART (complete ellipse)
let rightNode = getEllipseNode()
rightNode.position = CGPoint(x: 0, y: 0)
rightNode.zPosition = 5
addChild(rightNode)
}
private func getEllipseNode() -> SKShapeNode {
let ellipseNode = SKShapeNode(ellipseOfSize: CGSize(
width: size.width,
height: size.height))
ellipseNode.strokeColor = SKColor.blackColor()
ellipseNode.lineWidth = 5
return ellipseNode
}
}
You've got the right idea with your two-layer approach and the half-slips on top. But instead of using a shape node inside a crop node, why not just use a shape node whose path is a half-ellipse? Create one using either CGPath or UIBezierPath API — use a circular arc with a transform to make it elliptical — then create your SKShapeNode from that path.
You may try converting your SKShapeNode to an SKSpriteNode. You can use SKView textureFromNode: (but we aware of issues with scale that require you to use it only after the node has been added to the view and at least one update cycle has run), or from scratch using an image (created programatically with a CGBitmapContext, of course).

Color filling in Swift

Is there a general method to fill an area in Swift.
I have various shape in my UIView, lines, ellipses, curves … etc.
Those shapes delimit a certain number of areas.
I want to fill one area in a color (uiColorOne) and another area in another color (uiColorTwo).
Is that possible? If YES, how can I do that?
import UIKit
override func drawRect(rect: CGRect)
{
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 200, y: 200))
path.addLineToPoint(CGPoint(x: 200, y: 300))
path.addLineToPoint(CGPoint(x: 300, y: 200))
path.closePath()
UIColor.redColor().set()
path.lineWidth = 1
path.fill()
}
You will need to override drawRect in order to start making some geometric shaped items in your code (ONLY OVERRIDE AND NEVER CALL IT). The code snippet creates an element based on points (careful not pixels) and fills it with the redColor.You will have to use UIBezierPath most of the times you are playing with elements and so I suggest you have a really good look at it from apple documentation here, since it has some awesome methods that will help you out with your every day struggle : UIBezierPath

Subtract Multiple Overlapping Paths from Path

I have three paths. I want two of those paths, path1 and path2, to be subtracted from path3. I do not want the area that overlaps between path1 and path2 to be filled. Here's a diagram I made to explain what I mean:
I already tried this question, but it the accepted answer produces what is found above in "Result With EOClip." I tried CGContextSetBlendMode(ctx, kCGBlendModeClear), but all it did was make the fill black. Any ideas?
Playing a bit with PaintCode (paint-code) I landed with this. Maybe it works for your case?
let context = UIGraphicsGetCurrentContext()
CGContextBeginTransparencyLayer(context, nil)
let path3Path = UIBezierPath(rect: CGRectMake(0, 0, 40, 40))
UIColor.blueColor().setFill()
path3Path.fill()
CGContextSetBlendMode(context, kCGBlendModeDestinationOut)
let path2Path = UIBezierPath(rect: CGRectMake(5, 5, 20, 20))
path2Path.fill()
let path1Path = UIBezierPath(rect: CGRectMake(15, 15, 20, 20))
path1Path.fill()
CGContextEndTransparencyLayer(context)

Resources