Moving SpriteNodes outwards in a circular pattern - ios

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)

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)
}
}

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.

SpriteKit: Uniform Circular Motion

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?

SpriteKit : Draw line using angle and Length

I want to do some drawing based on angles and lengths instead of coordinance in SpriteKit (Swift)
I have the following function that draws from 2 points but I want to create one that draws from 1 point to another place based on angle and length of line
func drawLine( start: CGPoint, end: CGPoint)
{
CGPathMoveToPoint(ref, nil, start.x * 100, start.y * 100)
let line = SKShapeNode()
CGPathAddLineToPoint(ref, nil, end.x * 100, end.y * 100)
line.path = ref
line.lineWidth = 4
line.fillColor = UIColor.redColor()
line.strokeColor = UIColor.redColor()
self.addChild(line)
}
sin and cos are your friends here and this answer isn't specific to Swift.
Given angle and radius, if you define your angle in radians rather than degrees, the end point of your line should be:
let endPoint = CGPoint(x: start.x + sin(angle) * radius,
y: start.y + cos(angle) * radius)
Simon

Finding position of pentagon inside circle

I'm trying to draw small circles over corners of pentagon inside a circle.
I know the following information.
The radius of circle is 50.
Centre of circle is x = 100, y = 100
How can I get position of pentagon corners in x and y so that I can draw a circle there.
Thanks.
I hope you are familiar with polar coordinates. I wrote simple algorithm to calculate corners:
let center = CGPoint(x: 100, y: 100)
let numberOfCorners: Int = 5
let radius = 50.0
var angle:Double = 0.0
let startAngle = M_PI_2
var points: [CGPoint] = []
angle = startAngle
while angle - startAngle < 2*M_PI {
let x = radius * cos(angle) + Double(center.x) // transform polar coordinates to XY
let y = radius * sin(angle) + Double(center.y)
let point = CGPoint(x: x, y: y)
points.append(point)
let angleIncrease = Double(2.0*M_PI)/Double(numberOfCorners)
angle += angleIncrease
}
print(points)
It should works with other polygons. You only have to specify numberOfCorners and startAngle (in radians). Correct StartAngle for iOS coordinates is I think: 3/2 PI.
But for default XY plane to achieve polygon like on your picture, pi/4 it's ok.

Resources