How to create 'union' of two intersecting CGPath-s? - ios

I wanted to create a rounded cross by combining two rounded rectangles with CGPathAddRoundedRect. The problem is the area where rects intersect is empty, but I would like it to be filled. (see image of the problem)
I'm also using SpriteKit SKShapeNode to draw on scene. Here is Swift code I'm using:
backgroundColor = UIColor.orangeColor()
let width: CGFloat = 302
let height: CGFloat = 92
let path = CGPathCreateMutable()
CGPathAddRoundedRect(path, nil, CGRectMake((size.width-width)/2, (size.height-height)/2, width, height), height/2, height/2)
CGPathAddRoundedRect(path, nil, CGRectMake((size.width-height)/2, (size.height-width)/2, height, width), height/2, height/2)
CGPathCloseSubpath(path)
let cross = SKShapeNode(path: path)
cross.fillColor = UIColor.whiteColor()
addChild(cross)
Any ideas how to make intersecting area filled? I know I could create two separate shape nodes ... but I just want to learn how to do that with combining CGPaths directly.

Related

Is it possible to draw to individual channels with Core Graphics?

I would like to draw in specific channels using Core Graphics.
Using the code below, each shape is drawn using a single channel color, but the second green filled rectangle will overwrite the previous red ellipse. I would like one ellipse to be only in the red channel and the square to be only in the green channel. I tried using transparency layers but they did not help.
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
let circlePath = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50))
let squarePath = UIBezierPath(rect: CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50))
UIColor.red.setFill()
circlePath.fill()
UIColor.green.setFill()
squarePath.fill()
Is it possible to draw in individual channels? Or will I have to draw in individual bitmaps and combine them at the pixel level?
Rather than using UIBezierPath.fill() (which draws the bezier path as fully opaque), you need to use fill(with:alpha:), which allows you to specify a custom opacity as a CGFloat between 0 (fully transparent) and 1 (fully opaque).
There are multiple blend modes available (see CGBlendMode), which specify how partially transparent surfaces interact with each other. Multiply is a "sensible default", but you can play around with them and see which best matches your design. The blend modes are the same as those in Photoshop and such, so there's plenty of online tutorials, explanations and samples, such as https://photoshoptrainingchannel.com/blending-modes-explained/

How to translate X-axis correctly from VNFaceObservation boundingBox (Vision + ARKit)

I'm using both ARKit & Vision, following along Apple's sample project, "Using Vision in Real Time with ARKit". So I am not setting up my camera as ARKit handles that for me.
Using Vision's VNDetectFaceRectanglesRequest, I'm able to get back a collection of VNFaceObservation objects.
Following various guides online, I'm able to transform the VNFaceObservation's boundingBox to one that I can use on my ViewController's UIView.
The Y-axis is correct when placed on my UIView in ARKit, but the X-axis is completely off & inaccurate.
// face is an instance of VNFaceObservation
let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -view.frame.height)
let translate = CGAffineTransform.identity.scaledBy(x: view.frame.width, y: view.frame.height)
let rect = face.boundingBox.applying(translate).applying(transform)
What is the correct way to display the boundingBox on the screen (in ARKit/UIKit) so that the X & Y axis match up correctly to the detected face rectangle? I can't use self.cameraLayer.layerRectConverted(fromMetadataOutputRect: transformedRect) since I'm not using AVCaptureSession.
Update: Digging into this further, the camera's image is 1920 x 1440. Most of it is not displayed on ARKit's screen space. The iPhone XS screen is 375 x 812 points.
After I get Vision's observation boundingBox, I've transformed it to fit the current view (375 x 812). This isn't working since the actual width seems to be 500 (the left & right sides are out of the screen view). How do I CGAffineTransform the CGRect bounding box (seems like 500x812, a total guess) from 375x812?
The key piece missing here is ARFrame's displayTransform(for:viewportSize:). You can read the documentation for it here.
This function will generate the appropriate transform for a given frame and viewport size (the CGRect of the view you're displaying the image and bounding box in).
func visionTransform(frame: ARFrame, viewport: CGRect) -> CGAffineTransform {
let orientation = UIApplication.shared.statusBarOrientation
let transform = frame.displayTransform(for: orientation,
viewportSize: viewport.size)
let scale = CGAffineTransform(scaleX: viewport.width,
y: viewport.height)
var t = CGAffineTransform()
if orientation.isPortrait {
t = CGAffineTransform(scaleX: -1, y: 1)
t = t.translatedBy(x: -viewport.width, y: 0)
} else if orientation.isLandscape {
t = CGAffineTransform(scaleX: 1, y: -1)
t = t.translatedBy(x: 0, y: -viewport.height)
}
return transform.concatenating(scale).concatenating(t)
}
You can then use this like so:
let transform = visionTransform(frame: yourARFrame, viewport: yourViewport)
let rect = face.boundingBox.applying(transform)

Filling circle animation with UIBezierPath and iOS draw rect

I have this on github: https://github.com/evertoncunha/EPCSpinnerView
It does this animations:
There is a glitch in the animation, when the circle is filling and the view is resizing, and I wan't to fix that.
The glitch happens because the way I fill the circle by setting the layer corners, so it looks like a circle, and I increase the lineWidth with the animation progress, and it looks likes it's filling from outside in. From EPCDrawnIconSpinner.swift file:
let ovalPath = UIBezierPath()
ovalPath.addArc(withCenter: CGPoint(x: ovalRect.midX, y: ovalRect.midY),
radius: ovalRect.width / 2,
startAngle: start * CGFloat.pi/180,
endAngle: end * CGFloat.pi/180, clockwise: true)
layer.cornerRadius = rect.size.height/2
ovalPath.lineWidth = 14 + ((frame.size.width) * progress)
Is there a better way that I can achieve this animation without doing the layer cornerRadius thing? If I could only draw this
I can think of two ways to get away from using cornerRadius:
Use a mask to round the corners, and use a shape layer to fill
You can create a CAShapeLayer of a filled circle, and with a frame set to your view's bounds, then assign it to your layer's mask property. You can animate the size of this mask as you animate the size of your view. Secondly, you will need to create an additional shape later (probably similar to your spinning circle), and do the line width animation on this layer. The mask will ensure that the line does not grow outwards, but only inwards.
Animate the line width and radius of the fill shape
You will need to create a circle shape layer similar to the one described in the first option, but instead of just animating the line width, you'll also need to animate the radius. For example: Let's say your view size is 40x40. Of course, at the start of the fill animation, your shape layer will also need to be 40x40. You will then animate the line width to 20pts, and animate the radius of the circle from 20pts to 10pts. A circle with a radius of 10pts and a line width of 20pts will appear to be a filled circle with a radius of 20pts.
Hope this helps.
I ended up with the following solution:
fileprivate func drawPathCompleted(_ ovalRect: CGRect) {
var size: CGFloat = ovalRect.size.width
var current = size*(1-progress)
var pos: CGFloat = lineWidth/2
while size > current {
let ovalPath = UIBezierPath(ovalIn: CGRect(x: pos, y: pos, width: size, height: size))
ovalPath.lineCapStyle = .round
ovalPath.lineWidth = lineWidth
ovalPath.stroke()
let decr = lineWidth/2
size -= decr
pos += decr/2
}
}
At this commit, line 243:
https://github.com/evertoncunha/EPCSpinnerView/commit/87d968846d92aa97e85ff4c58b6664ad7b03f00b#diff-8dc22814328ed859a00acfcf2909854bR242

SKSpriteNode will not center

I have this inside my GameScene which is called in the didMove()
for i in 1...5 {
// path to create the circle
let path = UIBezierPath(arcCenter: CGPoint(x: center.x, y: center.y), radius: CGFloat(((43 * i) + 140)), startAngle: CGFloat(GLKMathDegreesToRadians(-50)), endAngle: CGFloat(M_PI * 2), clockwise: false)
// the inside edge of the circle used for creating its physics body
let innerPath = UIBezierPath(arcCenter: CGPoint(x: center.x, y: center.y), radius: CGFloat(((43 * i) + 130)), startAngle: CGFloat(GLKMathDegreesToRadians(-50)), endAngle: CGFloat(M_PI * 2), clockwise: false)
// create a shape from the path and customize it
let shape = SKShapeNode(path: path.cgPath)
shape.lineWidth = 20
shape.strokeColor = UIColor(red:0.98, green:0.99, blue:0.99, alpha:1.00)
// create a texture and apply it to the sprite
let trackViewTexture = self.view!.texture(from: shape)
let trackViewSprite = SKSpriteNode(texture: trackViewTexture)
trackViewSprite.physicsBody = SKPhysicsBody(edgeChainFrom: innerPath.cgPath)
self.addChild(trackViewSprite)
}
It uses UIBezierPaths to make a few circles. It converts the path into a SKShapeNode then a SKTexture and then applies it to the final SKSpriteNode.
When I do this, the SKSpriteNode is not where it should be, it is a few to the right:
But when I add the SKShapeNode I created, it is set perfectly fine to where it should be:
Even doing this does not center it!
trackViewSprite.position = CGPoint(x: 0, y: 0)
No matter what I try it just will not center.
Why is this happening? Some sort of bug when converting to a texture?
P.S - This has something to do with this also Keep relative positions of SKSpriteNode from SKShapeNode from CGPath
But there is also no response :(
Edit, When I run this:
let testSprite = SKSpriteNode(color: UIColor.yellow, size: trackViewSprite.size)
self.addChild(testSprite)
It shows it has the same frame also:
After a long discussion, we determined that the problem is due to the frame size not being the expected size of the shape.
To combat this, the OP created an outer path of his original path, and calculated the frame that would surround this. Now this approach may not work for everybody.
If anybody else comes across this issue, they will need to do these things:
1) Check the frame of the SKShapeNode to make sure that it is correct
2) Determine what method is best to calculate the correct desired frame
3) Use this new frame when getting textureFromNode to extract only the desired texture size

Cannot create two CGPathAddArc on the same CGMutablePathRef

I'm trying to understand why i'm seeing only one of mine CGPathAddArc.
Code :
var r: CGRect = self.myView.bounds
var lay: CAShapeLayer = CAShapeLayer()
var path: CGMutablePathRef = CGPathCreateMutable()
CGPathAddArc(path, nil, 30, 30, 30, 0, (360 * CGFloat(M_PI))/180, true )
CGPathAddArc(path, nil, 70, 30, 30, 0, (360 * CGFloat(M_PI))/180, true )
CGPathAddRect(path, nil, r2)
CGPathAddRect(path, nil, r)
lay.path = path
lay.fillRule = kCAFillRuleEvenOdd
self.myView.layer.mask = lay
result :
Any suggestions? Thanks!
If you push down the command key and click on CGPathAddArc function, you will see documentation.
/* Note that using values very near 2π can be problematic. For example,
setting `startAngle' to 0, `endAngle' to 2π, and `clockwise' to true will
draw nothing. (It's easy to see this by considering, instead of 0 and 2π,
the values ε and 2π - ε, where ε is very small.) Due to round-off error,
however, it's possible that passing the value `2 * M_PI' to approximate
2π will numerically equal to 2π + δ, for some small δ; this will cause a
full circle to be drawn.
If you want a full circle to be drawn clockwise, you should set
`startAngle' to 2π, `endAngle' to 0, and `clockwise' to true. This avoids
the instability problems discussed above. */
Setting startAngle to 0, endAngle to 2π, and clockwise to true will
draw nothing. If you want a full circle to be drawn clockwise, you should set
startAngle to 2π, endAngle to 0, and clockwise to true. So that you can see all circles.
What you need to do is debug. How? Well, when in doubt, your first step should be to simplify. In this case, you should start by testing your code outside the context of a mask and a fill rule. When you do, you'll see that the arcs are in fact both present. I ran this reduced version of your code:
let lay = CAShapeLayer()
lay.frame = CGRectMake(20,20,400,400)
let path = CGPathCreateMutable()
CGPathAddArc(path, nil, 30, 30, 30, 0,
(360 * CGFloat(M_PI))/180, true)
CGPathAddArc(path, nil, 70, 30, 30, 0,
(360 * CGFloat(M_PI))/180, true)
lay.path = path
self.view.layer.addSublayer(lay)
And this is what I got:
As you can see, both arcs are present. So your results must be due to some complication beyond the drawing of the arcs.
If we add the fill rule...
lay.fillRule = kCAFillRuleEvenOdd
...we get this:
And if we introduce the mask element...
// self.view.layer.addSublayer(lay)
self.view.layer.mask = lay
...we get this:
Thus, using basic tests, you should be able to convince yourself of what this part of your code does. You can now introduce more and more of your actual code until you start getting undesirable results, and then you'll know what's causing the problem.

Resources