Calculation of stroke property 'Dash' in dashed circle - Swift - swift

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

Related

Why does CGFloat casted from Float not exhibit CGFloat behavior?

I have this simple example where I try to draw a circle. This code below does not give me a circle.
import UIKit
class PlayingCardView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext(){
context.addArc(center: CGPoint(x: bounds.midX,
y: bounds.midY), radius: 100.0,
startAngle: 0,
endAngle: CGFloat(2.0*Float.pi),
clockwise: true)
context.setLineWidth(5.0)
UIColor.red.setStroke()
context.strokePath()
print(2.0*CGFloat.pi)
print(CGFloat(2.0*Float.pi))
}
}
}
This is what I get with the above code:
with output:
6.283185307179586
6.283185005187988
from the print statements which correspond to 2.0*CGFloat.pi and CGFloat(2.0*Float.pi) respectively.
Updating the code to this (I only change the endAngle in context.addArc to be 2.0*CGFloat.pi instead of CGFloat(2.0*Float.pi)
import UIKit
class PlayingCardView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext(){
context.addArc(center: CGPoint(x: bounds.midX,
y: bounds.midY), radius: 100.0,
startAngle: 0,
endAngle: 2.0*CGFloat.pi,
clockwise: true)
context.setLineWidth(5.0)
UIColor.red.setStroke()
context.strokePath()
print(2.0*CGFloat.pi)
print(CGFloat(2.0*Float.pi))
}
}
}
I get this drawing (The circle is there)
There is obviously a difference between the casted CGFloat from Float and CGFloat. Does anybody know what it is and why this behavior is useful in Swift?
On a 64-bit platform, CGFloat is (essentially) Double, a 64-bit floating point number, whereas Float is a 32-bit floating point number.
So 2.0*Float.pi is “2π with 32-bit precision”, and converting that to the 64-bit quantity CGFloat preserves the value, without increasing the precision.
That is why 2.0*CGFloat.pi != CGFloat(2.0*Float.pi). The former is “2π with 64-bit precision”, and is what you should pass to the drawing functions.
In your particular case, CGFloat(2.0*Float.pi) is a tiny bit smaller than 2.0*CGFloat.pi, so that only an invisibly short arc is drawn (from radians 0.0 to approximately -0.00000003).
For a full circle you can alternatively use
let radius: CGFloat = 100.0
context.addEllipse(in: CGRect(x: bounds.midX - radius, y: bounds.midY - radius,
width: 2.0 * radius, height: 2.0 * radius))
and avoid all rounding problems.

Swift draw and RESIZE circle with CAShapeLayer() and UIBezierPath

I want to draw a circle with animation, where the animation starts from 12 o'clock, and goes 360 degrees. Everything works fine, but I can not resize the circle.
If I use UIBezierPath - I can define the starting point ("startAngle: -CGFloat.pi / 2").
let shapeLayer = CAShapeLayer()
let center = progressView.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
If I use UIBezierPath(ovalIn:...) I can resize.. But I want to use both, or how can I keep the start angle, and the size too?
shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 8, y: 78, width: 70, height: 70)).cgPath
For the first version of your path code, the radius parameter determines the size of the circle.
In the second version, it draws an oval (which may or may not be a circle) into a rectangular box.
Both let you control the size, just with different parameters.
If you want to vary the size of the arc drawn with init(arcCenter:radius:startAngle:endAngle:clockwise:) then vary the radius parameter.
A circle drawn with
let circularPath = UIBezierPath(arcCenter: center,
radius: 100,
startAngle: -CGFloat.pi / 2,
endAngle: 2 * CGFloat.pi,
clockwise: true)
Will be twice as big as a circle drawn with
let circularPath = UIBezierPath(arcCenter: center,
radius: 50,
startAngle: -CGFloat.pi / 2,
endAngle: 2 * CGFloat.pi,
clockwise: true)
(And btw, if the start angle is -π/2, shouldn't the end angle be 3π/2, so the arc is 360° (or 2π) rather than 450°?)

Filter on CALayer except for a shape which is an union of (non necessarily distinct) rectangles

I want to apply a CIFilter to a CALayer except for an area which is an union of rectangles.
I post my not working code, it does exactly the opposite, i.e. the filter is applied only in the rectangles and not outside!
func refreshTheFrameWithEffect() {
self.layer!.masksToBounds = true
self.layerUsesCoreImageFilters = true
self.layer!.needsDisplayOnBoundsChange = true
let filter = CIFilter(name: "CICircleSplashDistortion")
filter!.setDefaults()
self.layer!.backgroundFilters = [filter!]
var excludedRects: [CGRect] = [] //INITIALISE THEM HOW YOU PREFER
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
self.layer!.fillMode = kCAFillRuleEvenOdd
var maskPath = NSBezierPath()
for rect in excludedRects {
maskPath.append(NSBezierPath(rect: rect))
}
maskLayer.path = maskPath.CGPath
self.layer!.mask = maskLayer
self.layer!.needsDisplay()
}
and then the following code from the Internet since NSBezierPath does not have the attribute CGPath, unlike UIBezierPath.
public var CGPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveToBezierPathElement: path.move(to: CGPoint(x: points[0].x, y: points[0].y) )
case .lineToBezierPathElement: path.addLine(to: CGPoint(x: points[0].x, y: points[0].y) )
case .curveToBezierPathElement: path.addCurve( to: CGPoint(x: points[2].x, y: points[2].y),
control1: CGPoint(x: points[0].x, y: points[0].y),
control2: CGPoint(x: points[1].x, y: points[1].y) )
case .closePathBezierPathElement: path.closeSubpath()
}
}
return path
}
AFAIK, you can't mask a layer to the inverse of a path.
A couple of observations:
If you were just trying to knock out the paths inside the whole view, you could do that with the trick of creating a path that consists of the whole CGRect plus the various interior paths and leverage the even/odd winding/fill rule, but that won't work if your interior paths overlap with each other.
You can mask images to the inverse of a path (by creating a separate "image mask"), but that won't work for dynamic CALayer masking. It's used for masking a NSImage. So, if you were OK using a snapshot for the filtered part of the view, that's an option.
See code snippet below for example of using image masks.
Another approach is to apply your filter to the whole view, snapshot it, put that snapshot underneath the view in question and then mask the top level view to your interior paths. In effect, mask the un-filtered view to your interior paths, revealing a filtered snapshot of your view below it.
Yet approach would be to create a path representing the outline of the union of all of your interior paths. If the paths are simple (e.g. non-rotated rectangles) this is pretty easy. If the paths are complex (some rotated, some non-rectangular paths, etc.), this gets hairy. But the trivial scenario isn't too bad. Anyway, if you do that, then you can fall back to that even-odd trick.
I'm not wholly satisfied with any of these approaches, but I don't see any other way to accomplish what you're looking for. Hopefully someone will suggest some better ways to tackle this.
To expand on option 2 (using an image mask created by drawing a few paths, possibly overlapping), in Swift 3 you can do something like:
private func maskImageWithPaths(image: NSImage, size: CGSize) -> NSImage {
// define parameters for two overlapping paths
let center1 = CGPoint(x: size.width * 0.40, y: size.height / 2)
let radius1 = size.width * 0.20
let center2 = CGPoint(x: size.width * 0.60 , y: size.height / 2)
let radius2 = size.width * 0.20
// create these two overlapping paths
let path = CGMutablePath()
path.move(to: center1)
path.addArc(center: center1, radius: radius1, startAngle: -.pi / 2, endAngle: .pi * 3 / 2, clockwise: false)
path.move(to: center2)
path.addArc(center: center2, radius: radius2, startAngle: -.pi / 2, endAngle: .pi * 3 / 2, clockwise: false)
// create image from these paths
let imageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSDeviceRGBColorSpace, bitmapFormat: .alphaFirst, bytesPerRow: 4 * Int(size.width), bitsPerPixel: 32)!
let context = NSGraphicsContext(bitmapImageRep: imageRep)!
context.cgContext.addPath(path)
context.cgContext.setFillColor(NSColor.blue.cgColor)
context.cgContext.fillPath()
let maskImage = context.cgContext.makeImage()!
let mask = CGImage(maskWidth: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: 4 * Int(size.width), provider: maskImage.dataProvider!, decode: nil, shouldInterpolate: true)!
let finalImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil)!.masking(mask)!
return NSImage(cgImage: finalImage, size: size)
}
That yields:
This masks out the drawn paths.

setLineDash symmetrically in Swift 2/3

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.

Drawing a partial circle

I'm writing a program that will take a number between 0 and 1, and then spits out a circle (or arc I guess) that is completed by that much.
So for example, if 0.5 was inputted, the program would output a semicircle
if 0.1, the program would output a tiny little arc that would ultimately be 10% of the whole circle.
I can get this to work by making the angle starting point 0, and the angle ending point 2*M_PI*decimalInput
However, I need to have the starting point at the top of the circle, so the starting point is 3*M_PI_2 and the ending point would be 7*M_PI_2
I'm just having trouble drawing a circle partially complete with these new starting/ending points. And I'll admit, my math is not the best so any advice/input is appreciated
Here is what I have so far
var decimalInput = 0.75 //this number can be any number between 0 and 1
let start = CGFloat(3*M_PI_2)
let end = CGFloat(7*M_PI_2*decimalInput)
let circlePath = UIBezierPath(arcCenter: circleCenter, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
circlePath.stroke()
I just cannot seem to get it right despite what I try. I reckon the end angle is culprit, unless I'm going about this the wrong way
The arc length is 2 * M_PI * decimalInput. You need to add the arc length to the starting angle, like this:
let circleCenter = CGPointMake(100, 100)
let circleRadius = CGFloat(80)
var decimalInput = 0.75
let start = CGFloat(3 * M_PI_2)
let end = start + CGFloat(2 * M_PI * decimalInput)
let circlePath = UIBezierPath(arcCenter: circleCenter, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
XCPCaptureValue("path", circlePath)
Result:
Note that the path will be flipped vertically when used to draw in a UIView.
You can use this extension to draw a partial circle
extension UIBezierPath {
func addCircle(center: CGPoint, radius: CGFloat, startAngle: Double, circlePercentage: Double) {
let start = deg2rad(startAngle)
let end = start + CGFloat(2 * Double.pi * circlePercentage)
addArc(withCenter: center,
radius: radius,
startAngle: start,
endAngle: end,
clockwise: true)
}
private func deg2rad(_ number: Double) -> CGFloat {
return CGFloat(number * Double.pi / 180)
}
}
Example usage (you can copy paste it in a playground to see the result)
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.backgroundColor = UIColor.green
let layer = CAShapeLayer()
layer.strokeColor = UIColor.red.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 8
let path = UIBezierPath()
path.addCircle(center: CGPoint(x: 50, y: 50), radius: 50, startAngle: 270, circlePercentage: 0.87)
layer.path = path.cgPath
view.layer.addSublayer(layer)
view.setNeedsLayout()