UIBezierPath Quadratic Curve is a Straight Line - swift

I'm trying to create a curved arrow for displaying in ab ARKit scene, however, the curvature of the arrow staff is just rendering as a straight line on both sides.
func createTurnArrow(_ direction: Direction) -> SCNShape {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.2, y: 0)) // A
path.addLine(to: CGPoint(x: 0, y: 0.2)) // B
path.addLine(to: CGPoint(x: 0, y: 0.1)) // C
path.addQuadCurve(to: CGPoint(x: -0.3, y: -0.3), controlPoint: CGPoint(x: -0.3, y: 0.1)) // Curve 1
path.addLine(to: CGPoint(x: -0.1, y: -0.3)) // D
path.addQuadCurve(to: CGPoint(x: 0, y: -0.1), controlPoint: CGPoint(x: -0.1, y: -0.1)) // Curve 2
path.addLine(to: CGPoint(x: 0, y: -0.2)) // E
path.close()
return direction == .left ?
SCNShape(path: path.reversing(), extrusionDepth: self.defaultDepth) :
SCNShape(path: path, extrusionDepth: self.defaultDepth)
}
My intuition tells me that create a node with this function:
SCNNode(geometry: createTurnArrow(.right))
should produce a shape like this:
but instead renders this without any curves to the tail of the arrow:
I've tried a bunch of other math to get the current control points for the quadratic curves but nothing is worry. Any ideas?
EDIT:
Where is the schematic with plotted points and my assumption of how this should be rendered with the curves.

Read the SCNShape path documentation. It says this:
The path’s flatness (see flatness in NSBezierPath) determines the level of detail SceneKit uses in building a three-dimensional shape from the path—a larger flatness value results in fewer polygons to render, increasing performance.
(Since you're on iOS, substitute UIBezierPath for NSBezierPath.)
What is the default flatness of a UIBezierPath? Here's what the documentation says:
The flatness value measures the largest permissible distance (measured in pixels) between a point on the true curve and a point on the rendered curve. Smaller values result in smoother curves but require more computation time. Larger values result in more jagged curves but are rendered much faster. The default flatness value is 0.6.
Now compare the default flatness (0.6) to the overall size of your shape (0.5 × 0.5). Notice that the flatness is bigger than the size of your shape! So each of your curves is getting flattened to a single straight line.
Change the flatness of your path to something more appropriate for your shape, or change the scale of your shape to something more appropriate for the default flatness.
let path = UIBezierPath()
path.flatness = 0.05 // <----------------------- insert this statement
path.move(to: CGPoint(x: 0.2, y: 0)) // A
path.addLine(to: CGPoint(x: 0, y: 0.2)) // B
// etc.

Related

ARKit Drawing 3D paper plane with rounded corner

I'm trying to draw 3D paper plane with rounded corner in ARKit, but I can't do that.
I did that with bazier path:
// create bezeir path
let path = UIBezierPath()
// A bezier path
path.move(to: CGPoint(x: 0, y: 0.025))
path.addLine(to: CGPoint(x: 0.02, y: -0.005))
path.addLine(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: -0.02, y: -0.005))
path.close()
// create arrow shape
let arrowShape = SCNShape(path: path, extrusionDepth: 0.001)
arrowShape.chamferRadius = 50
// create new node
arrownode = SCNNode(geometry: arrowShape);
// set arrow color
arrownode!.geometry?.firstMaterial?.diffuse.contents = UIColor.yellow
The result :
I need like this exactly but with rounded corner.
An instance property chamferRadius creates chamfer for extruded zDepth, not for XY path.
let arrowShape = SCNShape(path: path, extrusionDepth: 0.005)
arrowShape.chamferRadius = 20
If you want to create a rounded corners use the following method for path:
path.addQuadCurve(to: CGPoint, controlPoint: CGPoint)
or
path.addCurve(to: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)

How to draw a UIBezierPath with an ARC to the right similar to the image shown?

How do I draw this UIBezierPath to make it look identical to the green strip on the left of the image below? The rounded arc to the right.
path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 20, y: 0))
path.addLine(to: CGPoint(x: 20, y: 80))
//Add Half Circle Arc To Right
path.addArc(withCenter: CGPoint(x: 0, y: 160), radius: bounds.width, startAngle: 0, endAngle: 90, clockwise: true)
path.addLine(to: CGPoint(x: 20, y: 215))
path.addLine(to: CGPoint(x: 20, y: bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
path.close()
What you need is a routine that given the height of the desired arc and how far this “bubble” should stick out, and will determine the desired arc angle, radius, and center offset.
To determine the center of the circle giving three points on that circle is to identify the chord between two points and then identify the line that bisects that line segment. That results in a line that goes through the center of the circle. Then repeat that for a different two points on the circle. The intersection of those two lines will be the center of the circle.
So, I used a little basic algebra to calculate the slope (m), the y-intercept (b), and the x-intercept (xIntercept) of the line that bisects the line segment between the start of the arc and its half point. We can take that line, and see where it intercepts the x-axis and determine the center of the circle.
From that, a little trigonometry gives us the angle and the radius of the arc that intersects these three points (the top of the arc, the middle of the arc, and the bottom of the arc).
You get something like:
/// Calculate parameters necessary for arc.
/// - Parameter height: The height of top half of the arc.
/// - Parameter distance: How far out the arc should project.
func angleRadiusAndOffset(height: CGFloat, distance: CGFloat) -> (CGFloat, CGFloat, CGFloat) {
let m = distance / height
let b = height / 2 - distance * distance / (2 * height)
let xIntercept = -b / m
let angle = atan2(height, -xIntercept)
let radius = height / sin(angle)
return (angle, radius, xIntercept)
}
And you can then use that to create your path:
var point: CGPoint = CGPoint(x: bounds.minX, y: bounds.minY)
let path = UIBezierPath()
path.move(to: point)
point.x += edgeWidth
path.addLine(to: point)
point.y += bubbleStartY
path.addLine(to: point)
let (angle, radius, offset) = angleRadiusAndOffset(height: bubbleHeight / 2, distance: bubbleWidth)
let center = CGPoint(x: point.x + offset, y:point.y + bubbleHeight / 2)
path.addArc(withCenter: center, radius: radius, startAngle: -angle, endAngle: angle, clockwise: true)
point.y = bounds.maxY
path.addLine(to: point)
point.x = bounds.minX
path.addLine(to: point)
path.close()
And that yields:
That’s using these values:
var edgeWidth: CGFloat = 10
var bubbleWidth: CGFloat = 30
var bubbleHeight: CGFloat = 100
var bubbleStartY: CGFloat = 80
But you can obviously adjust these values as needed.

Create UIView with rounded specified borders

I want to create a UIView that has rounded borders which I have specified. For example, I want to create a UIView that has 3 borders: left, top and right, and the topright and topleft borders should be rounded.
This let's me create resizable border views (without rounded corners):
open class ResizableViewBorder: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsDisplay()
}
open override func draw(_ rect: CGRect) {
let edges = ... get some UIRectEdges here
let lineWidth = 1
if edges.contains(.top) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: 0 + lineWidth / 2),
CGPoint(x: self.bounds.width, y: 0 + lineWidth / 2)
])
}
if edges.contains(.bottom) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: self.bounds.height - lineWidth / 2),
CGPoint(x: self.bounds.width, y: self.bounds.height - lineWidth / 2)
])
}
if (edges.contains(.left) || edges.contains(.all) || edges.contains(.right)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: 0 + lineWidth / 2, y: 0),
CGPoint(x: 0 + lineWidth / 2, y: self.bounds.height)
])
}
if (edges.contains(.right) || edges.contains(.all) || edges.contains(.left)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: self.bounds.width - lineWidth / 2, y: 0),
CGPoint(x: self.bounds.width - lineWidth / 2, y: self.bounds.height)
])
}
}
private func addBezierPath(paths: [CGPoint]) {
let lineWidth = 1
let borderColor = UIColor.black
let path = UIBezierPath()
path.lineWidth = lineWidth
borderColor.setStroke()
UIColor.blue.setFill()
var didAddedFirstLine = false
for singlePath in paths {
if !didAddedFirstLine {
didAddedFirstLine = true
path.move(to: singlePath)
} else {
path.addLine(to: singlePath)
}
}
path.stroke()
}
}
However, I can not find a nice robust way to add a corner radius to a specified corner. I have a hacky way to do it with a curve:
let path = UIBezierPath()
path.lineWidth = 2
path.move(to: CGPoint(x: fakeCornerRadius, y: 0))
path.addLine(to: CGPoint(x: frame.width - fakeCornerRadius, y: 0))
path.addQuadCurve(to: CGPoint(x: frame.width, y: fakeCornerRadius), controlPoint: CGPoint(x: frame.width, y: 0))
path.addLine(to: CGPoint(x: frame.width, y: frame.height - fakeCornerRadius))
path.stroke()
Which gives me this:
Why is that line of the quad curve so fat? I prefer using UIBezierPaths over CALayers because CALayers have a huge performance impact...
What's wrong with your curve-drawing code is not that the curve is fat but that the straight lines are thin. They are thin because they are smack dab on the edge of the view. So your line width is 2 points, but one of those points is outside the view. And points are not pixels, so what pixels are there left to fill in? Only the ones inside the view. So the straight lines have an apparent visible line width of 1, and only the curve has a visible line width 2.
Another problem is that you probably are looking at this app running in the simulator on your computer. But there is a mismatch between the pixels of the simulator and the pixels of your computer monitor. That causes numerous drawing artifacts. The way to examine the drawing accurately down to the pixel level is to use the simulator application's screen shot image facility and look at the resulting image file, full-size, in Preview or similar. Or run on a device and take the screen shot image there.
To demonstrate this, I modified your code to operate in an inset version of the original rect (which, by the way, should be your view's bounds, not its frame):
let fakeCornerRadius : CGFloat = 20
let rect2 = self.bounds.insetBy(dx: 2, dy: 2)
let path = UIBezierPath()
path.lineWidth = 2
path.move(
to: CGPoint(x: rect2.minX + fakeCornerRadius, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX - fakeCornerRadius, y: rect2.minY))
path.addQuadCurve(
to: CGPoint(x: rect2.maxX, y: rect2.minY + fakeCornerRadius),
controlPoint: CGPoint(x: rect2.maxX, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX, y: rect2.maxY - fakeCornerRadius))
path.stroke()
Taking a screen shot from within the Simulator application, I got this:
As you can see, this lacks the artifacts of your screen shot.

How can I make an SKPhysicsBody for this image using CGPath?

I am trying to make an SKPhysics body for this SKSpriteNode using a CGPath polygon.
The problem is that when I check for a collision between this node and the player node, the didBeginContact method is executed even though they did not touch each other. I believe their is something wrong with the coordinates but I cannot see the polygon lines, which makes it difficult for me too see the accuracy of the lines.
Here is the code that I am using:
let triangle = SKSpriteNode(imageNamed: "Triangle_ZigZag")
let trianglePath = CGMutablePath()
trianglePath.addLines(between: [CGPoint(x: triangle.size.width,
y: triangle.size.height),
CGPoint(x: triangle.size.width,
y: - triangle.size.height),
CGPoint(x: -triangle.size.width,
y: triangle.size.height / 2)])
trianglePath.closeSubpath()
triangle.physicsBody = SKPhysicsBody(polygonFrom: trianglePath)
Can someone please help me figure out what am I doing wrong ?
Thank You
FYI physics lines are green, so a green sprite probably isn't the best choice you can't see the lines very well.
your sprite has a centre anchorPoint or an anchorPoint of (0, 0) by default. Therefore your physics points need to take that into account. top right corner would be half the width from centre and half the height from centre etc. You have full width from centre and full height from centre, that is the issue.
trianglePath.addLines(between: [CGPoint(x: triangle.size.width / 2, y: triangle.size.height / 2), CGPoint(x: triangle.size.width / 2, y: -triangle.size.height / 2), CGPoint(x: -triangle.size.width / 2, y: 0)])

How to move SKSpriteNode in a curve without rotating the sprite in Swift

I'm trying to move a sprite with a curve.
I got this code:
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.addQuadCurve(to: CGPoint(x: ball.position.x+200, y: ball.position.y+50), controlPoint: CGPoint(x: ball.position.x+100, y: ball.position.y+200))
ball.run(SKAction.follow(path.cgPath, speed: 1.0))
I have few questions:
1 - why my sprite is rotating while moving, and if I can control this rotation?
2 - any idea why the ball is moving only small part of the way and very slow and not a smooth moving (10-20 seconds)?
Does anyone have any idea how this code works?
All the answers I found were related to older Swift version that had different method.
At last I've found the solution :)
func beizerSprite()
{
// create a bezier path that defines our curve
let path = UIBezierPath()
path.move(to: CGPoint(x: 16,y: 239))
path.addCurve(to:CGPoint(x: 301, y: 239),
controlPoint1: CGPoint(x: 136, y: 373),
controlPoint2: CGPoint(x: 178, y: 110))
// use the beizer path in an action
_playButton.run(SKAction.follow(path.cgPath,
asOffset: false,
orientToPath: true,
speed: 50.0))
}
This move the SKSprite in a curve along the screen.