Replicate custom SKAction spiral movement with SKPhysicsBody methods - ios

I recently changed some trigonometric functions to get 4 methods that move an object by creating a spiral path (as described in the code below) :
From right to left to the top
From left to right to down
From right to left to down
From left to right to the top
Everything works fine . In this picture you can see the left to right to the top. (start to the left, go to the right - clockwise - all from bottom to top)
Now I would like to replicate these functions using the physics engine.
In the code below (//MARK: - Physic tests) I started to move the same object horizontally from left to right but I don't know for example how to warp elliptical effectively the same as seen in SKAction methods. Any advice will be appreciated.
Code:
class GameScene: SKScene {
var node: SKShapeNode!
var radius : CGFloat = 30
override func didMoveToView(view: SKView) {
node = SKShapeNode(circleOfRadius: radius)
node.physicsBody = SKPhysicsBody(circleOfRadius: radius)
node.physicsBody!.affectedByGravity = false
node.fillColor = .redColor()
self.node.position = CGPointMake(150, 150)
let myPath = self.rightUpSpiralPathInRect(CGRectMake(node.position.x,node.position.y+100,node.position.x+300,node.position.y+100)).CGPath
self.addChild(node)
//node.runAction(SKAction.followPath(myPath, speed: 350))
//MARK: - Physic tests
let angle: Int = 90 // degrees
let speed: Int = 150
let degrees = Float(angle) * Float(M_PI/180)
let xv:CGFloat = CGFloat(sinf(Float(degrees)) * Float(speed))
let yv:CGFloat = CGFloat(cosf(Float(degrees)) * Float(speed))
let vector : CGVector = CGVectorMake(xv, yv)
node.physicsBody!.velocity = vector
}
//MARK: - Trigonometry methods
private func convertPoint(point: CGPoint, rect: CGRect,reverseY:Bool) -> CGPoint {
var y = rect.origin.y + rect.size.height - point.y * rect.size.height
if reverseY {
y = rect.origin.y + point.y * rect.size.height
}
return CGPoint(
x: rect.origin.x + point.x * rect.size.width,y: y
)
}
private func parametricPathInRect(rect: CGRect, count: Int? = nil, reverseY:Bool = false, function: (CGFloat) -> (CGPoint)) -> UIBezierPath {
let numberOfPoints = count ?? max(Int(rect.size.width), Int(rect.size.height))
let path = UIBezierPath()
let result = function(0)
path.moveToPoint(convertPoint(CGPoint(x: result.x, y: result.y), rect: rect,reverseY:reverseY))
for i in 1 ..< numberOfPoints {
let t = CGFloat(i) / CGFloat(numberOfPoints - 1)
let result = function(t)
path.addLineToPoint(convertPoint(CGPoint(x: result.x, y: result.y), rect: rect, reverseY:reverseY))
}
return path
}
func rightDownSpiralPathInRect(rect: CGRect) -> UIBezierPath {
return parametricPathInRect(rect, count: 1000) { t in
let r = sin(t * CGFloat(M_PI_2))-1.0
return CGPoint(
x: (r * sin(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0,
y: (r * cos(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0
)
}
}
func rightUpSpiralPathInRect(rect: CGRect) -> UIBezierPath {
return parametricPathInRect(rect, count: 1000,reverseY: true) { t in
let r = sin(t * CGFloat(M_PI_2))-1.0
return CGPoint(
x: (r * sin(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0,
y: (r * cos(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0
)
}
}
func leftUpSpiralPathInRect(rect: CGRect) -> UIBezierPath {
return parametricPathInRect(rect, count: 1000) { t in
let r = 1.0 - sin(t * CGFloat(M_PI_2))
return CGPoint(
x: (r * sin(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0,
y: (r * cos(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0
)
}
}
func leftDownSpiralPathInRect(rect: CGRect) -> UIBezierPath {
return parametricPathInRect(rect, count: 1000,reverseY: true) { t in
let r = 1.0 - sin(t * CGFloat(M_PI_2))
return CGPoint(
x: (r * sin(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0,
y: (r * cos(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0
)
}
}
}
P.S. (my code is in swift, but I accept also objective-C)

Well, with the great help of Confused in physics laws, we have obtain a good result using this code as the example below:
import SpriteKit
class GameScene: SKScene {
var bit: SKSpriteNode?
override func sceneDidLoad() {
let field = SKFieldNode.radialGravityField()
field.falloff = -1
field.smoothness = 1
addChild(field)
bit = SKSpriteNode(color: .cyan, size: CGSize(width: 2, height: 2))
let satellite = SKShapeNode(circleOfRadius: 4)
satellite.fillColor = .white
satellite.physicsBody = SKPhysicsBody(circleOfRadius: 4)
satellite.physicsBody?.mass = 1
addChild(satellite)
satellite.position = CGPoint(x: 400, y: 000)
satellite.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 300))
satellite.physicsBody?.isDynamic = true
let dropDot = SKAction.run {
let myBit = self.bit?.copy() as? SKSpriteNode
myBit?.position = satellite.position
self.addChild(myBit!)
}
let dropper = SKAction.sequence([
SKAction.wait(forDuration: 0.033),
dropDot
])
run(SKAction.repeatForever(dropper))
}
}
Output:

Related

CAShapeLayer and UIBezierPath artifacts on touch

I am trying to implement Quick Path effect.
I have the following code:
func path() -> UIBezierPath {
let widthMultiplier = maxWidth / CGFloat(points.count)
let firstPoint = points.last
var path = UIBezierPath()
if let pointStart = firstPoint {
path = UIBezierPath(ovalIn: CGRect(x: pointStart.x - self.circleSize/2.0, y: pointStart.y - self.circleSize/2.0 , width: self.circleSize, height: self.circleSize))
}
var lastPoint: CGPoint!
var lastLeft: CGPoint!
var lastRight: CGPoint!
for (index, point) in points.enumerated() {
if index == 0 {
lastPoint = point
lastLeft = point
lastRight = point
} else {
let angle = lastPoint.angle(to: point)
let width = widthMultiplier * CGFloat(index)
let newLeft = point.offset(byDistance: width, inDirection: angle)
let newRight = point.offset(byDistance: width, inDirection: angle - 180)
path.move(to: lastLeft)
path.addLine(to: newLeft)
path.addLine(to: newRight)
path.addLine(to: lastRight)
path.addLine(to: lastLeft)
path.close()
lastLeft = newLeft
lastRight = newRight
lastPoint = point
if index == points.count - 1 {
path.move(to: lastLeft)
path.addArc(withCenter: point,
radius: width,
startAngle: CGFloat(0).degrees,
endAngle: CGFloat(360).degrees,
clockwise: false)
path.close()
}
}
}
path.fill()
path.stroke()
return path
}
It works, but have some artifacts:
As you can see sharp triangles.
Helper extensions are:
public func angle(to comparisonPoint: CGPoint) -> CGFloat {
let originX = comparisonPoint.x - self.x
let originY = comparisonPoint.y - self.y
let bearingRadians = atan2f(Float(originY), Float(originX))
var bearingDegrees = CGFloat(bearingRadians).degrees
while bearingDegrees < 0 {
bearingDegrees += 360
}
return bearingDegrees
}
public func offset(byDistance distance:CGFloat, inDirection degrees: CGFloat) -> CGPoint {
let radians = (degrees - 90) * .pi / 180
let vertical = sin(radians) * distance
let horizontal = cos(radians) * distance
return self.applying(CGAffineTransform(translationX:horizontal, y:vertical))
}
And for CGFloat:
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180.0 / .pi)
}
}
I've used radians as far as addArc accepts radians as arguments.
What is wrong with my code, I've tried various values, but still have this issue.

Drawing a scale on semi-circle Swift/UIView

Imagine I am having a full semi-circle from 0 to Pi from the unit circle. There is a small number on the left side named min and a big number on the right side called max. There are both interchangeable inside the app depending on some factors.
Does anybody of you have a nice idea on how to draw a scale like I did in the drawing below? I would like to have longer lines for every x mod 10 = 0 and three larger ones in between. The grey circle is just for orientation.
So I started with the following piece of code:
let radius = CGFloat(40)
let dashLong = CGFloat(10)
let dashShort = CGFloat(5)
let middle = CGPoint(x: 50, y: 50)
let leftAngle = CGFloat(Double.pi)
let rightAngle = CGFloat(0)
let min = 45 //random num
let max = 117 //random num
let innerPath = UIBezierPath(arcCenter: middle, radius: radius, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let middlePath = UIBezierPath(arcCenter: middle, radius: radius+dashShort, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let outerPath = UIBezierPath(arcCenter: middle, radius: radius+dashLong, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
So there is a radius and also the length of the two types of dashes in the scale. I chose 45 and 117 as random integers for the extrem values of the scale. My three paths which do not need to be drawn are just an orientation on where the dashes need to be started and ended on. So for 50,60,...110 there start at the innerPath and go to the outer one, I am pretty sure that must be in the same angle for a dash on all circles.
Does anyone has a very smart idea how to continue this to calc the dashes and draw them without getting messed up code?
Here's the math for drawing a tick mark.
Let's do everything as CGFloat to keep the conversions to a minimum:
let radius: CGFloat = 40
let dashLong: CGFloat = 10
let dashShort: CGFloat 5
let middle = CGPoint(x: 50, y: 50)
let leftAngle: CGFloat = .pi
let rightAngle: CGFloat = 0
let min: CGFloat = 45 //random num
let max: CGFloat = 117 //random num
First, compute your angle.
let value: CGFloat = 50
let angle = (max - value)/(max - min) * .pi
Now compute your two points:
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
// use dashLong for a long tick, and dashShort for a short tick
let radius2 = radius + dashLong
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
Then draw a line between p1 and p2.
Note: In iOS, the coordinate system is upside down with +Y being down, which is why the sin calculation is subtracted from middle.y.
Complete Example
enum TickStyle {
case short
case long
}
class ScaleView: UIView {
// ScaleView properties. If any are changed, redraw the view
var radius: CGFloat = 40 { didSet { self.setNeedsDisplay() } }
var dashLong: CGFloat = 10 { didSet { self.setNeedsDisplay() } }
var dashShort: CGFloat = 5 { didSet { self.setNeedsDisplay() } }
var middle = CGPoint(x: 50, y: 50) { didSet { self.setNeedsDisplay() } }
var leftAngle: CGFloat = .pi { didSet { self.setNeedsDisplay() } }
var rightAngle: CGFloat = 0 { didSet { self.setNeedsDisplay() } }
var min: CGFloat = 45 { didSet { self.setNeedsDisplay() } }
var max: CGFloat = 117 { didSet { self.setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
// draw the arc
path.move(to: CGPoint(x: middle.x - radius, y: middle.y))
path.addArc(withCenter: middle, radius: radius, startAngle: leftAngle, endAngle: rightAngle, clockwise: true)
let startTick = ceil(min / 2.5) * 2.5
let endTick = floor(max / 2.5) * 2.5
// add tick marks every 2.5 units
for value in stride(from: startTick, through: endTick, by: 2.5) {
let style: TickStyle = value.truncatingRemainder(dividingBy: 10) == 0 ? .long : .short
addTick(at: value, style: style, to: path)
}
// stroke the path
UIColor.black.setStroke()
path.stroke()
}
// add a tick mark at value with style to path
func addTick(at value: CGFloat, style: TickStyle, to path: UIBezierPath) {
let angle = (max - value)/(max - min) * .pi
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
var radius2 = radius
if style == .short {
radius2 += dashShort
} else if style == .long {
radius2 += dashLong
}
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
path.move(to: p1)
path.addLine(to: p2)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = ScaleView(frame: CGRect(x: 50, y: 50, width: 100, height: 60))
view.backgroundColor = .yellow
self.view.addSubview(view)
}
}
Picture of scale running in app:
My suggestion is Draw this semi circle in CALayer and Draw lines from centre of the semi circle in different CALayer and Mask both of them so that It appears Like this

Get N number of CGPoints around a circle on UIView

I got a center CGPoint and radius Float, I need to get N number of points surrounding the circle, for example in below image how to get the 12 points corresponding red dots.
This is my incomplete function:
func getCirclePoints(centerPoint point: CGPoint, and radius: CGFloat, n: Int) [CGPoint] {
let result: [CGPoint] = stride(from: 0.0, to: 360.0, by: CGFloat(360 / n)).map {
let bearing = $0 * .pi / 180
// NO IDEA WHERE TO MOVE FURTHER
}
return result
}
getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), radius: 120.0, n: 12)
You were almost there!
func getCirclePoints(centerPoint point: CGPoint, radius: CGFloat, n: Int)->[CGPoint] {
let result: [CGPoint] = stride(from: 0.0, to: 360.0, by: Double(360 / n)).map {
let bearing = CGFloat($0) * .pi / 180
let x = point.x + radius * cos(bearing)
let y = point.y + radius * sin(bearing)
return CGPoint(x: x, y: y)
}
return result
}
let points = getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), radius: 120.0, n: 12)
I didn't think and was very clear as an argument name so I've removed this.
Use radians instead of degrees. They are needed inside trigonometric functions
func getCirclePoints(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
return Array(repeating: 0, count: n).enumerated().map { offset, element in
let cgFloatIndex = CGFloat(offset)
let radiansStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
let radians = radiansStep * cgFloatIndex
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
return CGPoint(x: x, y: y)
}
}
func getCirclePoints1(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
var resultPoints: [CGPoint] = []
let radianStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
for radians in stride(from: CGFloat(0.0), to: CGFloat.pi * CGFloat(2.0), by: radianStep) {
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
resultPoints.append(CGPoint(x: x, y: y))
}
return resultPoints
}
func getCirclePoints2(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
let radianStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
return stride(from: CGFloat(0.0), to: CGFloat.pi * CGFloat(2.0), by: radianStep).map { element in
let cgFloatIndex = CGFloat(element)
let radiansStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
let radians = radiansStep * cgFloatIndex
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
return CGPoint(x: x, y: y)
}
}
getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), and: 120.0, n: 12)
For having a draw reference
import UIKit
let numOfItems = 10
class customView : UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
for i in 0...numOfItems
{
let angle = 360/CGFloat(numOfItems) * CGFloat(i) * .pi / 180
let rad = self.bounds.size.width/2 - 10
let x = bounds.midX + cos(angle) * rad
let y = bounds.midY + sin(angle) * rad
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:x,y:y),
radius:10,
startAngle:0,
endAngle:360,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = circlePath.cgPath
layer.addSublayer(shapeLayer)
}
}
}

How to find the endangle for beizerpath for a curve drawn in swift?

I have drawn a beizer path with start & end angle which create a whole circle
circle = UIView(frame: CGRect(x: 100, y: 100, width: 200, height:200))
circle.layoutIfNeeded()
self.view.addSubview(circle)
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83
circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true)
After that i draw a Curve on that circle & does not fill it full to the circle.
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath?.cgPath
progressCircle.strokeColor = UIColor.red.cgColor
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.lineWidth = 4.0
progressCircle.strokeStart = 0
progressCircle.strokeEnd = 0.7
circle.layer.addSublayer(progressCircle)
After that i want to add image at then end of curve so to do that i created another biezer path.But here problem is it should be ended as the curve end.So i am not able to find the end angle for the beizer path based on the endStroke of CASHAPELAYER. Please tell me how can i find the end angle of new beizer path based on the curve end point.
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83
let arcStartAngle: Double = 0.0
let rotationDiff = 360 - abs((0.0 - 270))
let startAngle: CGFloat = -1.57
let endAngle: CGFloat = CGFloat(Double.pi * 0.7)
let bpath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
class ViewController: UIViewController {
var array = [[CGFloat:CGFloat]]()
var midX : CGFloat = CGFloat()
var midY : CGFloat = CGFloat()
let radius : CGFloat = 100.0
override func viewDidLoad() {
super.viewDidLoad()
midX = self.view.frame.midX
midY = self.view.frame.midY
let path = UIBezierPath()
path.addArc(withCenter: CGPoint.init(x: midX, y: midY), radius: radius, startAngle: 0, endAngle:2 * .pi, clockwise: true)
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 5.0
shape.fillColor = UIColor.clear.cgColor
shape.strokeColor = UIColor.lightGray.cgColor
self.view.layer.addSublayer(shape)
var temp090x = [CGFloat:CGFloat]()
var temp90180x = [CGFloat:CGFloat]()
var temp180270x = [CGFloat:CGFloat]()
var temp270360x = [CGFloat:CGFloat]()
for i in 1...360 {
let path3 = UIBezierPath()
path3.move(to: CGPoint.init(x: midX, y: midY))
path3.addLine(to:CGPoint.init(x: midX + radius * sin(CGFloat(i)), y:midY + radius*cos(CGFloat(i))))
//design path in layer
if midX + radius * sin(CGFloat(i)) > midX && midY + radius*cos(CGFloat(i)) < midY {
temp090x[midX + radius * sin(CGFloat(i))] = midY + radius*cos(CGFloat(i))
}else if midX + radius * sin(CGFloat(i)) > midX && midY + radius*cos(CGFloat(i)) > midY {
temp90180x[midX + radius * sin(CGFloat(i))] = midY + radius*cos(CGFloat(i))
}else if midX + radius * sin(CGFloat(i)) < midX && midY + radius*cos(CGFloat(i)) > midY {
temp180270x[midX + radius * sin(CGFloat(i))] = midY + radius*cos(CGFloat(i))
}else if midX + radius * sin(CGFloat(i)) < midX && midY + radius*cos(CGFloat(i)) < midY {
temp270360x[midX + radius * sin(CGFloat(i))] = midY + radius*cos(CGFloat(i))
}
let shapeLayer3 = CAShapeLayer()
shapeLayer3.path = path3.cgPath
shapeLayer3.strokeColor = UIColor.clear.cgColor
shapeLayer3.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer3)
}
let tempSorted090x = BubbleAsceSort(array: temp090x)
let tempSorted90180x = BubbleDescSort(array: temp90180x)
let tempSorted180270x = BubbleDescSort(array: temp180270x)
let tempSorted270360x = BubbleAsceSort(array: temp270360x)
for item in tempSorted090x {
array.append(item)
}
for item in tempSorted90180x {
array.append(item)
}
for item in tempSorted180270x {
array.append(item)
}
for item in tempSorted270360x {
array.append(item)
}
let lbl = UILabel()
lbl.frame = CGRect.init(x: 0, y: 0, width: 30, height: 30)
lbl.backgroundColor = UIColor.black
lbl.layer.cornerRadius = 15
for i in 1...180 {
let xpos = Array(array[i-1].keys)[0]
let ypos = Array(array[i-1].values)[0]
let xpos1 = Array(array[i].keys)[0]
let ypos1 = Array(array[i].values)[0]
let path3 = UIBezierPath()
path3.move(to: CGPoint.init(x: xpos, y: ypos))
path3.addLine(to:CGPoint.init(x: xpos1, y:ypos1))
let shapeLayer3 = CAShapeLayer()
shapeLayer3.path = path3.cgPath
shapeLayer3.strokeColor = UIColor.red.cgColor
shapeLayer3.lineWidth = 5.0
self.view.layer.addSublayer(shapeLayer3)
}
lbl.center = CGPoint.init(x: Array(array[180].keys)[0], y: Array(array[180].values)[0])
self.view.addSubview(lbl)
}
func BubbleDescSort(array : [CGFloat:CGFloat]) -> [[CGFloat:CGFloat]] {
var sortedArray = Array(array.keys)
var sortedvalueArray = Array(array.values)
var sortedAboveIndex = sortedArray.count-1 // Assume all values are not in order
repeat {
var lastSwapIndex = 0
for i in 1...sortedAboveIndex{
if (sortedArray[i] as AnyObject) as! CGFloat > (sortedArray[i - 1] as AnyObject) as! CGFloat {
sortedArray.swapAt(i, i-1)
sortedvalueArray.swapAt(i, i-1)
lastSwapIndex = i
}
}
sortedAboveIndex = lastSwapIndex
} while (sortedAboveIndex != 0)
var index = 0
var arr = [[CGFloat:CGFloat]]()
for item in sortedArray {
var dic = [CGFloat:CGFloat]()
dic[item] = sortedvalueArray[index]
arr.append(dic)
index += 1
}
return arr
}
func BubbleAsceSort(array : [CGFloat:CGFloat]) -> [[CGFloat:CGFloat]] {
var sortedArray = Array(array.keys)
var sortedvalueArray = Array(array.values)
var sortedAboveIndex = sortedArray.count-1 // Assume all values are not in order
repeat {
var lastSwapIndex = 0
for i in 1...sortedAboveIndex{
if (sortedArray[i - 1] as AnyObject) as! CGFloat > (sortedArray[i] as AnyObject) as! CGFloat {
sortedArray.swapAt(i, i-1)
sortedvalueArray.swapAt(i, i-1)
lastSwapIndex = i
}
}
sortedAboveIndex = lastSwapIndex
} while (sortedAboveIndex != 0)
var index = 0
var arr = [[CGFloat:CGFloat]]()
for item in sortedArray {
var dic = [CGFloat:CGFloat]()
dic[item] = sortedvalueArray[index]
arr.append(dic)
index += 1
}
return arr
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
In this code you need to Set Progress Percentage based on the 360 degree. in the upper limit of for loop and Round lbl Center point x and y pos. Here is need only 180 replace to your progress percentage

CAGradientLayer diagonal gradient

I use the following CAGradientLayer:
let layer = CAGradientLayer()
layer.colors = [
UIColor.redColor().CGColor,
UIColor.greenColor().CGColor,
UIColor.blueColor().CGColor
]
layer.startPoint = CGPointMake(0, 1)
layer.endPoint = CGPointMake(1, 0)
layer.locations = [0.0, 0.6, 1.0]
But when I set bounds property for the layer, it just stretches a square gradient. I need a result like in Sketch 3 app image (see above).
How can I achieve this?
Update: Use context.drawLinearGradient() instead of CAGradientLayer in a manner similar to the following. It will draw gradients that are consistent with Sketch/Photoshop.
If you absolutely must use CAGradientLayer, then here is the math you'll need to use...
It took some time to figure out, but from careful observation, I found out that Apple's implementation of gradients in CAGradientLayer is pretty odd:
First it converts the view to a square.
Then it applies the gradient using start/end points.
The middle gradient will indeed form a 90 degree angle in this resolution.
Finally, it squishes the view down to the original size.
This means that the middle gradient will no longer form a 90 degree angle in the new size. This contradicts the behavior of virtually every other paint application: Sketch, Photoshop, etc.
If you want to implement start/end points as it works in Sketch, you'll need to translate the start/end points to account for the fact that Apple is going to squish the view.
Steps to perform (Diagrams)
Code
import UIKit
/// Last updated 4/3/17.
/// See https://stackoverflow.com/a/43176174 for more information.
public enum LinearGradientFixer {
public static func fixPoints(start: CGPoint, end: CGPoint, bounds: CGSize) -> (CGPoint, CGPoint) {
// Naming convention:
// - a: point a
// - ab: line segment from a to b
// - abLine: line that passes through a and b
// - lineAB: line that passes through A and B
// - lineSegmentAB: line segment that passes from A to B
if start.x == end.x || start.y == end.y {
// Apple's implementation of horizontal and vertical gradients works just fine
return (start, end)
}
// 1. Convert to absolute coordinates
let startEnd = LineSegment(start, end)
let ab = startEnd.multiplied(multipliers: (x: bounds.width, y: bounds.height))
let a = ab.p1
let b = ab.p2
// 2. Calculate perpendicular bisector
let cd = ab.perpendicularBisector
// 3. Scale to square coordinates
let multipliers = calculateMultipliers(bounds: bounds)
let lineSegmentCD = cd.multiplied(multipliers: multipliers)
// 4. Create scaled perpendicular bisector
let lineSegmentEF = lineSegmentCD.perpendicularBisector
// 5. Unscale back to rectangle
let ef = lineSegmentEF.divided(divisors: multipliers)
// 6. Extend line
let efLine = ef.line
// 7. Extend two lines from a and b parallel to cd
let aParallelLine = Line(m: cd.slope, p: a)
let bParallelLine = Line(m: cd.slope, p: b)
// 8. Find the intersection of these lines
let g = efLine.intersection(with: aParallelLine)
let h = efLine.intersection(with: bParallelLine)
if let g = g, let h = h {
// 9. Convert to relative coordinates
let gh = LineSegment(g, h)
let result = gh.divided(divisors: (x: bounds.width, y: bounds.height))
return (result.p1, result.p2)
}
return (start, end)
}
private static func unitTest() {
let w = 320.0
let h = 60.0
let bounds = CGSize(width: w, height: h)
let a = CGPoint(x: 138.5, y: 11.5)
let b = CGPoint(x: 151.5, y: 53.5)
let ab = LineSegment(a, b)
let startEnd = ab.divided(divisors: (x: bounds.width, y: bounds.height))
let start = startEnd.p1
let end = startEnd.p2
let points = fixPoints(start: start, end: end, bounds: bounds)
let pointsSegment = LineSegment(points.0, points.1)
let result = pointsSegment.multiplied(multipliers: (x: bounds.width, y: bounds.height))
print(result.p1) // expected: (90.6119039567129, 26.3225059181603)
print(result.p2) // expected: (199.388096043287, 38.6774940818397)
}
}
private func calculateMultipliers(bounds: CGSize) -> (x: CGFloat, y: CGFloat) {
if bounds.height <= bounds.width {
return (x: 1, y: bounds.width/bounds.height)
} else {
return (x: bounds.height/bounds.width, y: 1)
}
}
private struct LineSegment {
let p1: CGPoint
let p2: CGPoint
init(_ p1: CGPoint, _ p2: CGPoint) {
self.p1 = p1
self.p2 = p2
}
init(p1: CGPoint, m: CGFloat, distance: CGFloat) {
self.p1 = p1
let line = Line(m: m, p: p1)
let measuringPoint = line.point(x: p1.x + 1)
let measuringDeltaH = LineSegment(p1, measuringPoint).distance
let deltaX = distance/measuringDeltaH
self.p2 = line.point(x: p1.x + deltaX)
}
var length: CGFloat {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
return sqrt(dx * dx + dy * dy)
}
var distance: CGFloat {
return p1.x <= p2.x ? length : -length
}
var midpoint: CGPoint {
return CGPoint(x: (p1.x + p2.x)/2, y: (p1.y + p2.y)/2)
}
var slope: CGFloat {
return (p2.y-p1.y)/(p2.x-p1.x)
}
var perpendicularSlope: CGFloat {
return -1/slope
}
var line: Line {
return Line(p1, p2)
}
var perpendicularBisector: LineSegment {
let p1 = LineSegment(p1: midpoint, m: perpendicularSlope, distance: -distance/2).p2
let p2 = LineSegment(p1: midpoint, m: perpendicularSlope, distance: distance/2).p2
return LineSegment(p1, p2)
}
func multiplied(multipliers: (x: CGFloat, y: CGFloat)) -> LineSegment {
return LineSegment(
CGPoint(x: p1.x * multipliers.x, y: p1.y * multipliers.y),
CGPoint(x: p2.x * multipliers.x, y: p2.y * multipliers.y))
}
func divided(divisors: (x: CGFloat, y: CGFloat)) -> LineSegment {
return multiplied(multipliers: (x: 1/divisors.x, y: 1/divisors.y))
}
}
private struct Line {
let m: CGFloat
let b: CGFloat
/// y = mx+b
init(m: CGFloat, b: CGFloat) {
self.m = m
self.b = b
}
/// y-y1 = m(x-x1)
init(m: CGFloat, p: CGPoint) {
// y = m(x-x1) + y1
// y = mx-mx1 + y1
// y = mx + (y1 - mx1)
// b = y1 - mx1
self.m = m
self.b = p.y - m*p.x
}
init(_ p1: CGPoint, _ p2: CGPoint) {
self.init(m: LineSegment(p1, p2).slope, p: p1)
}
func y(x: CGFloat) -> CGFloat {
return m*x + b
}
func point(x: CGFloat) -> CGPoint {
return CGPoint(x: x, y: y(x: x))
}
func intersection(with line: Line) -> CGPoint? {
// Line 1: y = mx + b
// Line 2: y = nx + c
// mx+b = nx+c
// mx-nx = c-b
// x(m-n) = c-b
// x = (c-b)/(m-n)
let n = line.m
let c = line.b
if m-n == 0 {
// lines are parallel
return nil
}
let x = (c-b)/(m-n)
return point(x: x)
}
}
Proof it works regardless of rectangle size
I tried this with a view size=320x60, gradient=[red#0,green#0.5,blue#1], startPoint = (0,1), and endPoint = (1,0).
Sketch 3:
Actual generated iOS screenshot using the code above:
Note that the angle of the green line looks 100% accurate. The difference lies in how the red and blue are blended. I can't tell if that's because I'm calculating the start/end points incorrectly, or if it's just a difference in how Apple blends gradients vs. how Sketch blends gradients.
Here's the math to fix the endPoint
let width = bounds.width
let height = bounds.height
let dx = endPoint.x - startPoint.x
let dy = endPoint.y - startPoint.y
if width == 0 || height == 0 || width == height || dx == 0 || dy == 0 {
return
}
let ux = dx * width / height
let uy = dy * height / width
let coef = (dx * ux + dy * uy) / (ux * ux + uy * uy)
endPoint = CGPoint(x: startPoint.x + coef * ux, y: startPoint.y + coef * uy)
Full code of layoutSubviews method is
override func layoutSubviews() {
super.layoutSubviews()
let gradientOffset = self.bounds.height / self.bounds.width / 2
self.gradientLayer.startPoint = CGPointMake(0, 0.5 + gradientOffset)
self.gradientLayer.endPoint = CGPointMake(1, 0.5 - gradientOffset)
self.gradientLayer.frame = self.bounds
}

Resources