SpriteKit: Uniform Circular Motion - ios

I'm trying to move a sprite in a circular motion using a reusable function (that I need to use on different objects).
The first result is given by this function:
func circularMotion(angle: CGFloat, radius: CGFloat, center: CGPoint) -> CGPoint{
let x = radius * cos(angle) + center.x
let y = radius * sin(angle) + center.y
return CGPoint(x: x, y: y)
}
And then I update the position of my sprite like this:
override func update(currentTime: NSTimeInterval) {
player.position = circularMotion(ang, radius: 200, center: CGPoint(x: size.width/2, y: size.height/2))
ang += speed
}
Where ang (the angle) and speed (0.05) are two initialized properties.
And this works pretty fine, but the actual problem is that I need to have the velocity of the sprite that rotates, since it has a PhysicBody.
My approach, though, changes only the position of the sprite. So I decided to do something like this:
override func update(currentTime: NSTimeInterval) {
let prevCirc = circularMotion(ang, radius: 200, center: CGPoint(x: size.width/2, y: size.height/2))
let Circ = circularMotion(ang + 0.05, radius: 200, center: CGPoint(x: size.width/2, y: size.height/2))
let v = CGVector(dx: Circ.x - prevCirc.x, dy: Circ.y - prevCirc.y )
player.physicsBody!.velocity = v
}
I just calculated the difference of position, finding the perpendicular Vector respect to the position vector. This still "works", the body follows a circular motion, but it has not the radius and center given (it's a smaller circle).
I know it's because of the length of the vector that it gives this result.
So the real question is, having the radius and the center of the circle I want (and the speed of how the angle changes) how can I calculate the perpendicular velocity so that it gives me a motion with those properties?

Related

UIBezierPath arc to create pie charts with rounded corners and spacing

I wonder how can we create a pie chart with rounded edges and spaces between pie as shown in the photo.
My first approach: I move the pies out of its center point an offset = 10 to make it look like the photo. But It seems like the radius of the biggest pie is smaller than the smaller ones.
Then I make a change on Radius, but the spacing a bit weird
And since the newCenter point is not in the center of superview, It’s cut off on a side.
outerRadius = outerRadius - offset * 2 * (1 - percentage)
(Percentage is the proportion of pie in the chart)
My second approach: I calculate the center point for each pie instead of moving it out of its original center point. Imagine there’s an empty middle as a circle and a new center point for each pie is in that circle.
The issues still occur with large pies.
The new center point for each slide on my tries:
let middleAngle = ((startAngle + endAngle) / 2.0).toRadians()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let newCenter = CGPoint(x: center.x + cos(middleAngle) * offset, y: center.y + sin(middleAngle) * offset)
Issues with radius and center point | Expected result
Here’s my code
https://gist.github.com/phongngo511/dfd416aaad45fc0241cd4526d80d94d6
Hi is this what you're trying to achieve? If so, I think your approach had a couple of issues. Firstly, looking at your code gist, I changed a couple of things from the way you were doing it:
Changed the pie segment sizes (so I could test >180° segments) and colours.
I added a convenience toRadians() function to the CGFloat extension (which is just the opposite of the toRadians() function you'd already added).
I changed the radius variable to be the min (not max as you were doing) of the bounds width / height, so that it fits in view without cropping. This is just personal preference & wouldn't change the overall functioning of the code (you might need it to be bigger & scrollable, for instance, whereas I just wanted to debug this particular problem). I also added padding so that it would still fit the segments when they've been spaced apart.
I went down your original route of solving the problem; draw all segments at the centre of the pie chart, and space them out afterwards, rather than trying to draw each one off centre. You could do either route although keeping them centred while constructing them is simpler and leads to more readable code. The spacing out is achieved by an affine transform at the end of the createPath: function, which spaces them out by a given segment's mid angle. You'd probably want to do it slightly more intelligently than this in real life (it's a bit primitive) as, as per the screenshot, very large segments will appear to be separated further than small segments are from each other (the red segment appears further away from the green and blue than the green and blue are from each other). So you might want to develop an algorithm that not only incorporates a segment's mid angle, but also how big that segment is, in order to determine not only the direction but also the distance to separate it? Or maybe factor in a segment's neighbours' mid angles when determining the direction of separation? Personal taste.
In your layoutSubviews(), you were supplying your createPath() with a different oRadius for each segment. That's why your segments had different radii from each other. I just supplied "radius" for all of them. If you comment out the affine transform in my createPath() function (which spaces them out), you'll see the segments in my version are all the same size radius.
I moved the path.close() into the createPath() function, rather than after calling it. Seems neater.
In terms of drawing a given segment, I've taken a different approach entirely (aside from drawing it centred in the pie chart and then moving it afterwards). I've drawn it with 2 straight lines and an arc for the outer circumference of the pie chart. For the rounded corners, rather than drawing an arc (N.B.: your centre rounded corner for a segment wasn't drawing correctly, causing weird graphical artefacts), I've used quadratic Bézier curves. These take only 1 control point, not 2 control points like a cubic Bézier curve takes. As a result, you can specify the corner of the segment as that control point, and it will give you a rounded corner suitable for the corner of the triangle that you're rounding. Because of this, I only draw the lines / arc up to near each corner, then do a quad Bézier curve to round the corner, then carry on with the rest of the segment.
Let me know if anything needs clarification, hope this helps!
import UIKit
class PieChartView: UIView {
var onTouchPie: ((_ sliceIndex: Int) -> ())?
var shouldHighlightPieOnTouch = false
var shouldShowLabels: Bool = false {
didSet { setNeedsLayout() }
}
var labelTextFont = UIFont.systemFont(ofSize: 12) {
didSet { setNeedsLayout() }
}
var labelTextColor = UIColor.black {
didSet { setNeedsLayout() }
}
var shouldShowTextPercentageFromFieFilledFigures = false {
didSet { setNeedsLayout() }
}
var pieGradientColors: [[UIColor]] = [[.red,.red], [.cyan,.cyan], [.green,.green]] {
didSet { setNeedsLayout() }
}
var pieFilledPercentages:[CGFloat] = [1, 1, 1] {
didSet { setNeedsLayout() }
}
//var segments:[CGFloat] = [40, 30, 30] {
var segments:[CGFloat] = [70, 20, 10] {
didSet { setNeedsLayout() }
}
var offset:CGFloat = 15 {
didSet { setNeedsLayout() }
}
var spaceLineColor: UIColor = .white {
didSet { setNeedsLayout() }
}
private var labels: [UILabel] = []
private var labelSize = CGSize(width: 100, height: 50)
private var shapeLayers = [CAShapeLayer]()
private var gradientLayers = [CAGradientLayer]()
override func layoutSubviews() {
super.layoutSubviews()
labels.forEach({$0.removeFromSuperview()})
labels.removeAll()
shapeLayers.forEach({$0.removeFromSuperlayer()})
shapeLayers.removeAll()
gradientLayers.forEach({$0.removeFromSuperlayer()})
gradientLayers.removeAll()
let valueCount = segments.reduce(CGFloat(0), {$0 + $1})
guard pieFilledPercentages.count >= 3, segments.count >= 3, pieGradientColors.count >= 3 , valueCount > 0 else { return }
let radius = min(bounds.width / 2, bounds.height / 2) * 0.9 //KEN CHANGED
var startAngle: CGFloat = 360
let proportions = segments.map({ ($0 / valueCount * 100).rounded()})
for i in 0..<segments.count {
let endAngle = startAngle - proportions[i] / 100 * 360
let path = createPath(from: startAngle, to: endAngle, oRadius: radius, percentage: proportions[i])
//path.close() //KEN CHANGED
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayers.append(shapeLayer)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = pieGradientColors[i].map({$0.cgColor})
if i == 0 {
gradientLayer.locations = [0.5, 1]
} else {
gradientLayer.locations = [0, 0.5]
}
gradientLayer.mask = shapeLayer
gradientLayer.frame = bounds
if proportions[i] != 0 && pieFilledPercentages[i] != 0 {
layer.addSublayer(gradientLayer)
gradientLayers.append(gradientLayer)
}
let label = labelFromPoint(point: getCenterPointOfArc(startAngle: startAngle, endAngle: endAngle), andText: String(format: "%.f", shouldShowTextPercentageFromFieFilledFigures ? pieFilledPercentages[i] * 100 :segments[i]) + "%")
label.isHidden = !shouldShowLabels
if proportions[i] != 0 {
addSubview(label)
labels.append(label)
}
startAngle = endAngle
}
}
private func labelFromPoint(point: CGPoint, andText text: String) -> UILabel {
let label = UILabel(frame: CGRect(origin: point, size: labelSize))
label.font = labelTextFont
label.textColor = labelTextColor
label.text = text
return label
}
private func getCenterPointOfArc(startAngle: CGFloat, endAngle: CGFloat) -> CGPoint {
let oRadius = max(bounds.width / 2, bounds.height / 2) * 0.8
let center = CGPoint(x: oRadius, y: oRadius)
let centerAngle = ((startAngle + endAngle) / 2.0).toRadians()
let arcCenter = CGPoint(x: center.x + oRadius * cos(centerAngle), y: center.y - oRadius * sin(centerAngle))
return CGPoint(x: (center.x + arcCenter.x) / 2, y: (center.y + arcCenter.y) / 2)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, shouldHighlightPieOnTouch {
shapeLayers.enumerated().forEach { (item) in
if let path = item.element.path, path.contains(touch.location(in: self)) {
item.element.opacity = 1
onTouchPie?(item.offset)
} else {
item.element.opacity = 0.3
}
}
}
super.touchesBegan(touches, with: event)
}
private func highlightLayer(index: Int) {
shapeLayers.enumerated().forEach({$0.element.opacity = $0.offset == index ? 1: 0.3 })
}
private func createPath(from startAngle: CGFloat, to endAngle: CGFloat, oRadius: CGFloat, cornerRadius: CGFloat = 10, percentage: CGFloat) -> UIBezierPath {
let radius: CGFloat = min(bounds.width, bounds.height) / 2.0 - (2.0 * offset)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let midPointAngle = ((startAngle + endAngle) / 2.0).toRadians() //used to spread the segment away from its neighbours after creation
let startAngle = (360.0 - startAngle).toRadians()
let endAngle = (360.0 - endAngle).toRadians()
let circumference: CGFloat = CGFloat(2.0 * (Double.pi * Double(radius)))
let arcLengthPerDegree = circumference / 360.0 //how many pixels long the outer arc is of the pie chart, per 1° of a pie segment
let pieSegmentOuterCornerRadiusInDegrees: CGFloat = 4.0 //for a given segment (and if it's >4° in size), use up 2 of its outer arc's degrees as rounded corners.
let pieSegmentOuterCornerRadius = arcLengthPerDegree * pieSegmentOuterCornerRadiusInDegrees
let path = UIBezierPath()
//move to the centre of the pie chart, offset by the corner radius (so the corner of the segment can be rounded in a bit)
path.move(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)))
//if the size of the pie segment isn't big enough to warrant rounded outer corners along its outer arc, don't round them off
if ((endAngle - startAngle).toDegrees() <= (pieSegmentOuterCornerRadiusInDegrees * 2.0)) {
//add line from centre of pie chart to 1st outer corner of segment
path.addLine(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * radius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * radius)))
//add arc for segment's outer edge on pie chart
path.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
//move down to the centre of the pie chart, leaving room for rounded corner at the end
path.addLine(to: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * cornerRadius)))
//add final rounded corner in middle of pie chart
path.addQuadCurve(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)), controlPoint: center)
} else { //round the corners on the outer arc
//add line from centre of pie chart to circumference of segment, minus the space needed for the rounded corner
path.addLine(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius))))
//add rounded corner onto start of outer arc
let firstRoundedCornerEndOnArc = CGPoint(x: center.x + (cos(startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians() - CGFloat(360).toRadians()) * radius), y: center.y + (sin(startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians() - CGFloat(360).toRadians()) * radius))
path.addQuadCurve(to: firstRoundedCornerEndOnArc, controlPoint: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * radius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * radius)))
//add arc for segment's outer edge on pie chart
path.addArc(withCenter: center, radius: radius, startAngle: startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians(), endAngle: endAngle - pieSegmentOuterCornerRadiusInDegrees.toRadians(), clockwise: true)
//add rounded corner onto end of outer arc
let secondRoundedCornerEndOnLine = CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)))
path.addQuadCurve(to: secondRoundedCornerEndOnLine, controlPoint: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * radius), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * radius)))
//add line back to centre point of pie chart, leaving room for rounded corner at the end
path.addLine(to: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * cornerRadius)))
//add final rounded corner in middle of pie chart
path.addQuadCurve(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)), controlPoint: center)
}
path.close()
//spread the segments out around the pie chart centre
path.apply(CGAffineTransform(translationX: cos(midPointAngle) * offset, y: -sin(midPointAngle) * offset))
return path
}
}
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
func toDegrees() -> CGFloat {
return self / (CGFloat(Double.pi) / 180.0)
}
}

Smooth open UIBezierPath

I'm trying to get smooth edges of Arcs and Curves.
The SKShapeNodes are open UIBezierPaths with a thick line-width, however, jagged edges are noticeable around Arcs and Curves. Changing the flatness doesn't seem to have any effect. Is there a way to smoothen these, or am I going to have to make a closed UIBezierPath with no line-width?
You need to create closed UIBezierPath with no linewidth
also
Make it shouldRasterize = true and set proper scale rasterizationScale = 2 * UIScreen.main.scale
I have a solution to my problem. Instead of using path.addArc(), I now use a function to get as many points as I'd like along an arc and then use path.addLine() to every one of those points. If you want more points for a smoother path, just lower the value of n.
func getCirclePoints(centerPoint: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) -> [CGPoint] {
let n : CGFloat = clockwise ? -1 : 1
let points: [CGPoint] = stride(from: startAngle - n, through: endAngle + n, by: n).map {
let degreesToRadians = CGFloat($0) * .pi / 180
let x = centerPoint.x + radius * cos(degreesToRadians)
let y = centerPoint.y + radius * sin(degreesToRadians)
return CGPoint(x: x, y: y)
}
return points
}

Get all CGPoint from the area of circle

I have center points and radius of circle, I just wanna get all CGPoint that the area of the circle covers. Basically I need to get all pixels through these CGPoint, I have a code for getting pixel from UIImage through CGPoint. I'm getting center points of circle through UITapGestureRecognizer when user tap. And I already know about radius of circle.
Can anyone tell me how I can get all CGPoint or pixels from circle. Thanks
For Example
Origin of below circle is CGPoint(x: 206, y: 105), and diameter of a circle is 50.
Another Example
How I can get these all CGPoints
First, You get minX and minY maybe in radius from center.
Second, check point by point and calculate distance to center.
func distance(from: CGPoint, to: CGPoint) -> CGFloat {
return CGFloat(sqrt((from.x - to.x)*(from.x - to.x) + (from.y - to.y)*(from.y - to.y)))
}
func isInCircle(withCenter center: CGPoint, point: CGPoint, radius: CGFloat) -> Bool {
return distance(from: center, to: point) <= radius
}
func getPoints(withCenter center:CGPoint, radius: CGFloat) -> [CGPoint] {
let minX = Int(center.x - radius)
let minY = Int(center.y - radius)
let maxX = Int(center.x + radius)
let maxY = Int(center.y + radius)
var result = [CGPoint]()
for x in minX...maxX {
for y in minY...maxY {
let point = CGPoint(x: x, y: y)
if isInCircle(withCenter: center, point: point, radius: radius) {
result.append(point)
}
}
}
return result
}
let x = getPoints(withCenter: CGPoint.init(x: 206, y: 150), radius: 50)
print(x)

Moving SpriteNodes outwards in a circular pattern

I have a function that creates 8 bullets coming from the player in a circle firing outwards. I think the problem lies with this line here:
let endPoint = CGPoint(x: distance * cos(angle), y: distance * sin(angle))
This causes the bullets to move faster in the bottom left of the circle than the ones in the top right, when in fact they should all move the same distance at the same speed.
Does anyone know how to implement this?
func fireSpecialWeapon() {
stride(from: 0, to: 2 * CGFloat.pi, by: 2 * CGFloat.pi / 8 ).forEach { angle in
let bullet = SKSpriteNode(imageNamed: "bulletCircle")
bullet.setScale(3)
bullet.zRotation = angle
bullet.position = player.position
bullet.zPosition = 2
//move outwards to the edge of the screen
let distance: CGFloat = 1000
let endPoint = CGPoint(x: distance * cos(angle), y: distance * sin(angle))
let move = SKAction.move(to: endPoint, duration: 2)
self.addChild(bullet)
bullet.run(move)
}
}
if the bullets are moving faster from the bottom left then the top right, that means your anchor points are (0,0) not (0.5,0.5)

How can I draw line intersecting the arc perpendicularly(Core Graphics)?

I want to draw a line on the circle(intercepting the arc of the circle) perpendicularly like in the picture.
I am using this code to draw circle
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: Conversion.degreesToRadians(value: CGFloat(0)), endAngle: Conversion.degreesToRadians(value: CGFloat(360)), clockwise: true)
path.lineWidth = 2
path.lineCapStyle = CGLineCap.square
UIColor.white.setStroke()
path.stroke()
The basic idea is that a circle has a certain radius about a center CGPoint. To figure out a point on the circle, you can calculate the x and y coordinates like so:
func point(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
where the angle is measured in radians, starting at 3 o'clock and going clockwise.
So those perpendicular intersecting strokes are merely line segments between two CGPoint at a given angle, where the "radius" used for the start of the line segment might be, for example, something just less than the radius of the circle. For the ending point of the line, use the same angle, but then use a radius value just greater than the radius of the circle.

Resources