Am am writing some code to draw circular charts, and while reading some example code, became confused about how angles are measured in Swift.
In the first post I read, Swift seems to use radians:
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2, floatPi * 3 / 2 + floatPi * 2 * percent, 0)
In the second post, Swift seems to define angles as a range from 0 to 1:
let circlePath = UIBezierPath(ovalInRect: CGRect(x: 200, y: 200, width: 150, height: 150))
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = (360 * 0.125) / 360
for var i = 0; i < 8; i++ {
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
// start angle is number of segments * the segment angle
circleLayer.strokeStart = segmentAngle * CGFloat(i)
// end angle is the start plus one segment, minus a little to make a gap
// you'll have to play with this value to get it to look right at the size you need
let gapSize: CGFloat = 0.008
circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle - gapSize
circleLayer.lineWidth = 10
circleLayer.strokeColor = UIColor(red:0, green:0.004, blue:0.549, alpha:1).CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
// add the segment to the segments array and to the view
segments.insert(circleLayer, atIndex: i)
view.layer.addSublayer(segments[i])
}
So, how does Swift measure angles?
Thanks
Swift has absolutely nothing to do with the angle unit, it's just a Float/Double to it. It's the functions of the Core Graphics framework that specify what angle unit they're expecting. Just read the docs of the functions in question.
From the CGContextAddArc docs:
startAngle: The angle to the starting point of the arc, measured in radians from the positive x-axis.
endAngle: The angle to the end point of the arc, measured in radians from the positive x-axis.
Also the strokeStart and strokeEnd properties of the CAShapeLayer say what part of the given path should be drawn, it's not an angle. Just read the docs next time.
Related
I'm making a SpriteKit game. I have six cannons that I've made rotate back and fourth. I want to shoot cannonballs from each cannon that sync with the rotation of each cannon. I want a duration of one second between each cannonball.
So basically: A cannon that is in constant rotation is shooting cannonballs in the same direction as the rotation.
For the cannons i'm using an extension:
extension CGFloat {
func degreesToRadians() -> CGFloat {
return self * CGFloat.pi / 180
}
}
I'm gonna put the code for just one cannon, since I should be able to figure out how to adjust one of the cannonball movements to the others. Here is one:
func createCannons() {
let cannonLeftBottom = SKSpriteNode(imageNamed: "Cannon")
cannonLeftBottom.anchorPoint = CGPoint(x: 0.5, y: 0.5)
cannonLeftBottom.position = CGPoint(x: -325, y: -420)
cannonLeftBottom.zPosition = 4
cannonLeftBottom.setScale(0.4)
cannonLeftBottom.zRotation = CGFloat(65).degreesToRadians()
let rotateLB = SKAction.rotate(byAngle:
CGFloat(-65).degreesToRadians(), duration: 2)
let rotateBackLB = SKAction.rotate(byAngle:
CGFloat(65).degreesToRadians(), duration: 2)
let repeatRotationLB =
SKAction.repeatForever(SKAction.sequence([rotateLB,rotateBackLB]))
cannonLeftBottom.run(repeatRotationLB)
self.addChild(cannonLeftBottom)
}
Here is my function for the cannonball:
func createBalls() {
let cannonBallLB = SKSpriteNode(imageNamed: "Ball")
cannonBallLB.name = "CannonBall"
cannonBallLB.position = CGPoint(x: -325, y: -420)
cannonBallLB.physicsBody = SKPhysicsBody(circleOfRadius:
cannonBallLB.size.height / 2)
cannonBallLB.physicsBody?.affectedByGravity = false
cannonBallLB.zPosition = 3
cannonBallLB.setScale(0.1)
self.addChild(cannonBallLB)
}
THX!
You need to convert from Polar Coordinates to Rectangular Coordinates.
You do this by using sin and cos
E.G.
let speed = 100 //This would mean move 100 points per second
let force = CGVector(dx:cos(cannon.zRotation) * speed,dy:sin(cannon.zRotation) * speed)
cannonBall.applyForce(force)
Note: Now unless they changed this, force used to be in units of points, if they fixed it to units of meters, then you need to divide your speed by 150, since 150 points = 1 meter in Spritekit
I'm developing an internet speed test app. Something I will do to practice and learn more about a future project.
This is my Swift Code:
import UIKit
#IBDesignable
class Arc: UIView {
#IBInspectable var dashWidth: CGFloat = 12.0
#IBInspectable var smallDashWidth: CGFloat = 5.0
override func draw(_ rect: CGRect) {
// Position and Radius of Arc
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
// Calculate Angles
let π = CGFloat(M_PI)
let startAngle = 3 * π / 4
let endAngle = π / 4
let radius = max(bounds.width / 1.15, bounds.height / 1.15) / 2 - dashWidth / 2
// Start Context
let context = UIGraphicsGetCurrentContext()
// MARK: Base
let arc = UIBezierPath(arcCenter: center, radius: radius,startAngle: startAngle,endAngle: endAngle,clockwise: true)
arc.addArc(withCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
arc.lineJoinStyle = .bevel
arc.lineCapStyle = .round
arc.close()
UIColor.yellow().setStroke()
arc.lineWidth = smallDashWidth
//context!.saveGState()
context!.setLineDash(phase: 0, lengths: [0, 0], count: 2)
arc.stroke()
context!.saveGState()
// MARK: dash Arc
let dashArc = UIBezierPath()
dashArc.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
// Round Line
dashArc.lineJoinStyle = .round;
// Set Stroke and Width of Dash
UIColor.white().setStroke()
dashArc.lineWidth = dashWidth
// Save Context and Set Line Dash
context!.saveGState()
context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)
// Draw Line
dashArc.stroke()
// Restore Context
context!.restoreGState()
}
}
The result is this:
What I need to do:
I need to automate this line of code:
context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)
The lengths are [2, 54] which numbers are added without calculation, only to get the final equation number taken to obtain this dynamically.
1: need to add dashes 12 (which may later be changed, being assigned as a variable) across the arc. The arc begins and ends at a variable angle (possibility to change later).
2: The value of dashArc.lineWidth = dashWidth can also be changed, and is an important item to calculate the space between the 12 dashes.
3: Since all the variables presented values can be variable, which is the best way to do this calculation.
4: The first and the last dash should be at the same angle that the respective startAngle and endAngle.
What I need:
I need a calculation that looks and spreads as symmetrically as possible the dashes during the arc.
I thought of a similar calculation to this:
var numberOfDashes = 12
var perimeterArc = ?
var widthDash = 2
spacing = (perimeterArc - (widthDash * numberOfDashes)) / numberOfDashes
context!.setLineDash(phase: 0, lengths: [widthDash, spacing], count: 2)
But I do not know how to calculate the perimeterArc.
Can someone help me? I could not think of anything to create a logical calculation for this in Swift 2/3.
I appreciate any tip.
Instead of trying to compute spaces directly by trying to use a dash pattern, first of all, think in terms of angles.
I lifted some code that was originally created as an example in my book PostScript By Example (page 281). I transliterated the postscript code to Swift (version 2, as version 3 can't seem to do anything useful).
I also eschewed your use of UIBezierPath mixed in with an access to the Graphics Context, since I have the feeling that there are some strange interactions between the two. UIBezierPath is intended to manage the Graphics Context under the covers.
Here is the code:
class ComputeDashes : UIView
{
let insetAmount: CGFloat = 40.0
let numberOfDashes: Int = 16
let startAngle: CGFloat = CGFloat(1.0 * M_PI / 4.0)
let endAngle: CGFloat = CGFloat(3.0 * M_PI / 4.0)
let subtendedAngle: CGFloat = CGFloat(2.0 * M_PI) - CGFloat(2.0 * M_PI / 4.0)
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
override func drawRect(rect: CGRect)
{
let insets: UIEdgeInsets = UIEdgeInsetsMake(insetAmount, insetAmount, insetAmount, insetAmount)
let newbounds: CGRect = UIEdgeInsetsInsetRect(self.bounds, insets)
let centre: CGPoint = CGPointMake(CGRectGetMidX(newbounds), CGRectGetMidY(newbounds))
let radius: CGFloat = CGRectGetWidth(newbounds) / 2.0
let context: CGContext = UIGraphicsGetCurrentContext()!
CGContextAddArc(context, centre.x, centre.y, radius, startAngle, endAngle, 1)
CGContextSetLineWidth(context, 10.0)
UIColor.magentaColor().set()
CGContextStrokePath(context)
// MARK: paint dashes
let innerRadius: CGFloat = radius * 0.75
CGContextSaveGState(context)
CGContextTranslateCTM(context, centre.x, centre.y)
let angle = subtendedAngle / CGFloat(numberOfDashes)
CGContextRotateCTM(context, endAngle)
for rot in 0...numberOfDashes {
let innerPoint: CGPoint = CGPointMake(innerRadius, 0.0)
CGContextMoveToPoint(context, innerPoint.x, innerPoint.y)
let outerPoint: CGPoint = CGPointMake(radius, 0.0)
CGContextAddLineToPoint(context, outerPoint.x, outerPoint.y)
CGContextRotateCTM(context, angle)
}
CGContextSetLineWidth(context, 2.0)
UIColor.blackColor().set()
CGContextStrokePath(context)
CGContextRestoreGState(context)
}
}
I believe that this approach is much more flexible, and avoids the tricky computations you would need to do to account for line widths in the 'on' phase of the dash pattern.
I hope this helps. Let me know what you think.
I'm developing an app. I haven't completed my apprenticeship of Swift 2.2/3.0 and I'm in search of some answers.
First:
I am using PaintCode to generate the start of Swift code. I made a drawing with the following properties:
Code Swift:
func drawSpeedView(strokeGap strokeGap: CGFloat = 30, strokeDash: CGFloat = 4, strokePhase: CGFloat = 0, strokeWidth: CGFloat = 31, strokeStartAngle: CGFloat = 225, strokeEndAngle: CGFloat = 315) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// DashedLarge Drawing
let dashedLargeRect = CGRect(x: 14.5, y: 17.5, width: 291, height: 291)
let dashedLargePath = UIBezierPath()
dashedLargePath.addArcWithCenter(CGPoint(x: dashedLargeRect.midX, y: dashedLargeRect.midY), radius: dashedLargeRect.width / 2, startAngle: -strokeStartAngle * CGFloat(M_PI)/180, endAngle: -strokeEndAngle * CGFloat(M_PI)/180, clockwise: true)
UIColor.blackColor().setStroke()
dashedLargePath.lineWidth = strokeWidth
CGContextSaveGState(context)
CGContextSetLineDash(context, strokePhase, [strokeDash, strokeGap], 2)
dashedLargePath.stroke()
CGContextRestoreGState(context)
}
My problem:
This code is optimized for iPhone 5.
For the proportion of dotted numbers, I need to create a calculation for the value of strokeDash for iPhone 6/6s and 6+/6s+
What I need:
I have no knowledge base to mount a circumference calculation yet.
I need a calculation for replace the line:
CGContextSetLineDash(context, strokePhase, [strokeDash, strokeGap], 2)
to replace with a decimal number able to put 12/6/15 dashed around the circle, no matter the perimeter of circle.
Does anyone have a solution to my problem?
Where do I start?
iPhone 5 Issue
As you can see from the line (from your code snippet):
let dashedLargeRect = CGRect(x: 14.5, y: 17.5, width: 291, height: 291)
the drawing space is defined of width 291 points and height 291 points.
Hardcoding these numbers is a bad idea because, as you've noticed, it means that the code would work well only on certain devices and not in others.
To fix this issue you can just grab these resolutions dynamically by declaring the following functions:
func windowHeight() -> CGFloat {
return UIScreen.mainScreen().applicationFrame.size.height
}
func windowWidth() -> CGFloat {
return UIScreen.mainScreen().applicationFrame.size.width
}
then you can create dashedLargeRect for example:
let dashedLargeRect = CGRect(x: 14.5, y: 17.5, width: windowWidth()-20, height: windowHeight()-20)
This way you make your code device-agnostic that is always good practice. (please take note that I haven't touched the CGRect "origin", a.k.a. the x,y parameters, consider that your homework :) ).
Second Issue
If I understand correctly, you want to know how to draw that dashed arc.
The secret lays on the line
dashedLargePath.addArcWithCenter(CGPoint(x: dashedLargeRect.midX, y: dashedLargeRect.midY), radius: dashedLargeRect.width / 2, startAngle: -strokeStartAngle * CGFloat(M_PI)/180, endAngle: -strokeEndAngle * CGFloat(M_PI)/180, clockwise: true)
You can see the addArcWithCenter documentation here, let me simplify it for you:
let arcCenter = CGPoint(x: dashedLargeRect.midX, y: dashedLargeRect.midY)
let radius = dashedLargeRect.width / 2
let startAngle = -strokeStartAngle * CGFloat(M_PI)/180
let endAngle = -strokeEndAngle * CGFloat(M_PI)/180
dashedLargePath.addArcWithCenter(arcCenter,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
now it should be much clearer what this code does, in case that you would like to draw another line over this one (for example to display the car speed, wind speed, air pressure, etc) all you have to do is call a similar function with a smaller/bigger radius and, probably, a different line style (a.k.a. change the CGContextSetLineDash, dashedLargePath.lineWidth, and UIColor.blackColor().setStroke() lines).
I have the following code:
func createScene(){
count += 1
let sphereGeom = SCNSphere(radius: 1.5)
sphereGeom.firstMaterial?.diffuse.contents = UIColor.redColor()
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
let radius = 3.0
var radians = Double(0)
var yPosition = Float(5.4)
while count <= 20 {
if radians >= 2{
radians -= 2
}
let sphereNode = SCNNode(geometry: sphereGeom)
let angle = Double(radians * M_PI)
let xPosition = Float(radius * cos(angle))
let zPosition = Float(radius * sin(angle))
sphereNode.position = SCNVector3(xPosition, yPosition, zPosition)
let cgX = CGFloat(xPosition)
let cgY = CGFloat(yPosition)
path.addQuadCurveToPoint(CGPoint(x: cgX, y: cgY), controlPoint: CGPoint(x: (cgX / 2), y: (cgY / 2)))
path.addLineToPoint(CGPoint(x: (cgX - (cgX * 0.01)), y: cgY))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: (cgX / 2), y: ((cgY / 2) - (cgY * 0.01))))
let shape = SCNShape(path: path, extrusionDepth: 3.0)
shape.firstMaterial?.diffuse.contents = UIColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.eulerAngles.y = Float(-M_PI_4)
self.rootNode.addChildNode(shapeNode)
count += 1
radians += 0.5556
yPosition -= 1.35
self.rootNode.addChildNode(sphereNode)
}
I want to add a Bezier path connecting each sphere to the next one, creating a spiral going down the helix. For some reason, when I add this code, the shape doesn't even appear. But when I use larger x and y values, I see the path fine, but it is no way oriented to the size of the spheres. I don't understand why it disappears when I try to make it smaller.
Your SCNShape doesn't ever get extruded. Per Apple doc,
An extrusion depth of zero creates a flat, one-sided shape.
With larger X/Y values your flat shape happens to become visible. You can't build a 3D helix with SCNShape, though: the start and end planes of the extrusion are parallel.
You'll have to use custom geometry, or approximate your helix with a series of elongated SCNBox nodes. And I bet someone out there knows how to do this with a shader.
I'm creating a simple player app. There is a circle, that shows a progress of playing a song.
What is the best way to draw this circle in Swift and make a mask? I assume I can draw a 2 circles putting the width stroke to the thickness I want and without filling it. And the white one has to be masked according to some parameter. I don't have an idea, how to mask it in a proper way.
I came up with this solution recently:
class CircularProgressView: UIView {
private let floatPi = CGFloat(M_PI)
private var progressColor = UIColor.greenColor()
private var progressBackgroundColor = UIColor.grayColor()
#IBInspectable var percent: CGFloat = 0.11 {
didSet {
setNeedsDisplay()
}
}
#IBInspectable var lineWidth: CGFloat = 18
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let origo = CGPointMake(frame.size.width / 2, frame.size.height / 2)
let radius: CGFloat = frame.size.height / 2 - lineWidth / 2
CGContextSetLineWidth(context, lineWidth)
CGContextMoveToPoint(context, frame.width / 2, lineWidth / 2)
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2, floatPi * 3 / 2 + floatPi * 2 * percent, 0)
progressColor.setStroke()
let lastPoint = CGContextGetPathCurrentPoint(context)
CGContextStrokePath(context)
CGContextMoveToPoint(context, lastPoint.x, lastPoint.y)
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2 + floatPi * 2 * percent, floatPi * 3 / 2, 0)
progressBackgroundColor.setStroke()
CGContextStrokePath(context)
}
}
You just have to set a correct frame to it (via code or interface builder), and set the percent property.
This solution is not using mask or two circles, just two arcs, the first start at 12 o clock and goes to 2 * Pi * progress percent, and the other arc is drawn from the end of the previous arc to 12 o clock.
Important: the percent property has to be between 0 and 1!