I am implementing line graph as shown in the attached video and image with dynamic values added in the array at every 1 second, mainly I want the graph to be drawn with the animation as shown in the video attached.
Till date i have tried using the https://github.com/Boris-Em/BEMSimpleLineGraph but i am not getting required result.
OUTPUT REQUIRED VIDEO LINK this reference video is from my android app.
CURRENT OUTPUT VIDEO LINK this is the output i am getting using BEMSimpleLineGraph
NOTE:- As we can see in the current output video there is no smoothness and animation as reference video, so need to achieve same
Need to get smoothness and curves exactly as shown HERE.!!
Also i have tried building the custom file for graph taking references from links during my research time, below is the code that i have tried
class GraphCustom: UIView {
// var data: [CGFloat] = [2, 6, 12, 4, 5, 7, 5, 6, 6, 3] {
// didSet {
// setNeedsDisplay()
// }
// }
#objc var dynamicData : [CGFloat] = [0]{
didSet {
setNeedsDisplay()
}
}
let shapeLayer = CAShapeLayer()
var fromValue = CGPoint()
var toValue = CGPoint()
func coordYFor(index: Int) -> CGFloat {
return bounds.height - bounds.height * dynamicData[index] / (dynamicData.max() ?? 0)
}
#objc override func draw(_ rect: CGRect) {
let path = quadCurvedPath()
// UIColor.black.setStroke()
// path.lineWidth = 1
// path.stroke()
// let animation = CABasicAnimation(keyPath: "strokeEnd")
// let timing = CAMediaTimingFunction()
// animation.duration = 10.0
// animation.fromValue = 0
// animation.toValue = 1
// self.layer.add(animation, forKey: "strokeAnim")
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 2.0
shapeLayer.lineCap = kCALineCapRound
self.layer.addSublayer(shapeLayer)
}
#objc func drawLine(value:CGFloat){
dynamicData.append(value)
if (dynamicData.count > 3){
dynamicData.remove(at: 0)
}
print("dynamicData >>>>>>>",dynamicData)
// let path = quadCurvedPath()
//
// let animation = CABasicAnimation(keyPath: "strokeEnd")
//
//// animation.fromValue = 0.0
//// animation.byValue = 1.0
// animation.duration = 2.0
//
// animation.fillMode = kCAFillModeForwards
// animation.isRemovedOnCompletion = false
//
// shapeLayer.add(animation, forKey: "drawLineAnimation")
// UIColor.black.setStroke()
// path.lineWidth = 1
// path.stroke()
}
func quadCurvedPath() -> UIBezierPath {
let path = UIBezierPath()
let step = bounds.width / CGFloat(dynamicData.count - 1)
print("Indccccc>>>>",dynamicData.count)
var p1 = CGPoint(x: 0, y: coordYFor(index:dynamicData.count - 1 ))
path.move(to: p1)
// drawPoint(point: p1, color: UIColor.red, radius: 3)
if (dynamicData.count == 2) {
path.addLine(to: CGPoint(x: step, y: coordYFor(index: 1)))
return path
}
var oldControlP: CGPoint?
for i in 0..<dynamicData.count {
let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
let mid = midPoint(p1: p1, p2: p2)
path.addQuadCurve(to: mid, controlPoint: controlPoint(p1: mid, p2: p1))
path.addQuadCurve(to: p2, controlPoint: controlPoint(p1: mid, p2: p2))
p1 = p2
}
/* for i in 1..<dynamicData.count {
print(">>>>>>>>>>",i)
let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
// drawPoint(point: p2, color: UIColor.red, radius: 3)
var p3: CGPoint?
if i < dynamicData.count - 1 {
p3 = CGPoint(x: step * CGFloat(i + 1), y: coordYFor(index: i + 1))
}
let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)
// print(" >>>>>>>>>>>>>>>>",p2,oldControlP ?? p1,newControlP ?? p2)
path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)
fromValue = oldControlP ?? p1
toValue = newControlP ?? p2
p1 = p2
oldControlP = antipodalFor(point: newControlP, center: p2)
}*/
return path;
}
func controlPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
var controlPoint = midPoint(p1: p1, p2: p2)
let diffY = abs(p2.y - controlPoint.y)
if p1.y < p2.y {
controlPoint.y += diffY
} else if p1.y > p2.y {
controlPoint.y -= diffY
}
return controlPoint
}
func midPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
}
/// located on the opposite side from the center point
func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
guard let p1 = point, let center = center else {
return nil
}
let newX = 2 * center.x - p1.x
let diffY = abs(p1.y - center.y)
let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)
return CGPoint(x: newX, y: newY)
}
/// halfway of two points
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
}
/// Find controlPoint2 for addCurve
/// - Parameters:
/// - p1: first point of curve
/// - p2: second point of curve whose control point we are looking for
/// - next: predicted next point which will use antipodal control point for finded
func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
guard let p3 = p3 else {
return nil
}
let leftMidPoint = midPointForPoints(p1: p1, p2: p2)
let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)
if p1.y.between(a: p2.y, b: controlPoint.y) {
controlPoint.y = p1.y
} else if p2.y.between(a: p1.y, b: controlPoint.y) {
controlPoint.y = p2.y
}
let imaginContol = antipodalFor(point: controlPoint, center: p2)!
if p2.y.between(a: p3.y, b: imaginContol.y) {
controlPoint.y = p2.y
}
if p3.y.between(a: p2.y, b: imaginContol.y) {
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
}
// make lines easier
controlPoint.x += (p2.x - p1.x) * 0.1
return controlPoint
}
func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
color.setFill()
ovalPath.fill()
}
}
extension CGFloat {
func between(a: CGFloat, b: CGFloat) -> Bool {
return self >= Swift.min(a, b) && self <= Swift.max(a, b)
}
}
I have been trying to get required output since long time and have tried almost every link available on stackoverflow.
Any help would be great..!!
Thank You.
Related
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.
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)
}
}
}
I've been trying to figure this out for too long. With the help of this blog I managed to draw the diagram itself, but it can't show me any data, because it seems like my idea of creating a context array is not possible and I can have only one context per view, is that right? So how can I change the color of each marker individually? I've seen the solution using SpriteKit, but I don't know anything at all about SpriteKit.
func degree2Radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
override func draw(_ rect: CGRect) {
color.set()
pathForCircleCenteredAtPoint(midPoint: circleCenter, withRadius: circleRadius).stroke()
color = UIColor.white
color.set()
pathForCircleCenteredAtPoint(midPoint: CGPoint(x: bounds.midX, y: bounds.midY), withRadius: circleRadius).fill()
color = UIColor(red: 0.93, green: 0.93, blue: 0.94, alpha: 1)
color.set()
let ctx = UIGraphicsGetCurrentContext()
for i in 0...100 {
secondMarkers(ctx: ctx!, x: circleCenter.x, y: circleCenter.y, radius: circleRadius - 4, sides: 100, color: color)
}
diagramArray[0].strokePath()
}
func degree2radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
func circleCircumferencePoints(sides:Int,x:CGFloat,y:CGFloat,radius:CGFloat,adjustment:CGFloat=0)->[CGPoint] {
let angle = degree2radian(a: 360/CGFloat(sides))
let cx = x // x origin
let cy = y // y origin
let r = radius // radius of circle
var i = sides
var points = [CGPoint]()
while points.count <= sides {
let xpo = cx - r * cos(angle * CGFloat(i)+degree2radian(a: adjustment))
let ypo = cy - r * sin(angle * CGFloat(i)+degree2radian(a: adjustment))
points.append(CGPoint(x: xpo, y: ypo))
i -= 1;
}
return points
}
func secondMarkers(ctx:CGContext, x:CGFloat, y:CGFloat, radius:CGFloat, sides:Int, color:UIColor) {
// retrieve points
let points = circleCircumferencePoints(sides: sides,x: x,y: y,radius: radius)
// create path
// determine length of marker as a fraction of the total radius
var divider:CGFloat = 1/16
//for p in points {
let path = CGMutablePath()
divider = 1/10
let xn = points[counter].x + divider * (x-points[counter].x)
let yn = points[counter].y + divider * (y-points[counter].y)
// build path
path.move(to: CGPoint(x: points[counter].x, y: points[counter].y))
//path, nil, p.x, p.y)
path.addLine(to: CGPoint(x: xn, y: yn))
//CGPathAddLineToPoint(path, nil, xn, yn)
path.closeSubpath()
// add path to context
ctx.addPath(path)
ctx.setStrokeColor(color.cgColor)
ctx.setLineWidth(2.0)
//ctx.strokePath()
diagramArray.append(ctx)
counter += 1
//}
// set path color
}
So basically I'm trying to append context for each marker to an array, but when I draw one element of this array, it draws the whole diagram. This is what I need to achieve.
You shouldn't need to create more than one CGContext - you should just be reusing the same one to draw all graphics. Also, your method to calculate the secondMarkers seems unnecessarily complex. I believe this does what you want:
private func drawTicks(context: CGContext, tickCount: Int, center: CGPoint, startRadius: CGFloat, endRadius: CGFloat, ticksToColor: Int) {
for i in 0 ... tickCount {
let color: UIColor = i < ticksToColor ? .blue : .lightGray
context.setStrokeColor(color.cgColor)
let angle = .pi - degree2Radian(a: (CGFloat(360.0) / CGFloat(tickCount)) * CGFloat(i))
let path = CGMutablePath()
path.move(to: circleCircumferencePoint(center: center, angle: angle, radius: startRadius))
path.addLine(to: circleCircumferencePoint(center: center, angle: angle, radius: endRadius))
context.addPath(path)
context.strokePath()
}
}
private func circleCircumferencePoint(center: CGPoint, angle: CGFloat, radius: CGFloat) -> CGPoint {
return CGPoint(x: radius * sin(angle) + center.x, y: radius * cos(angle) + center.y)
}
I'm trying to create a curly bracket in Swift, from two points. The idea works fine, with a straight line, because it's currently not dynamic in anyway. My issue lies in finding the dynamic control points and center depending on the location of p1 and p2 points.
This is my current code:
override func viewDidLoad() {
super.viewDidLoad()
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let c1 = CGPointMake(150, 80)
let c2 = CGPointMake(250, 80)
var midPoint = midPointForPoints(p1, p2: p2)
var midP1 = midPoint
midP1.x -= 10
var midP2 = midPoint
midP2.x += 10
midPoint.y -= 20
path.moveToPoint(p1)
path.addQuadCurveToPoint(midP1, controlPoint: c1)
path.addLineToPoint(midPoint)
path.addLineToPoint(midP2)
path.addQuadCurveToPoint(p2, controlPoint: c2)
let shape = CAShapeLayer()
shape.lineWidth = 5
shape.strokeColor = UIColor.redColor().CGColor
shape.fillColor = UIColor.clearColor().CGColor
shape.path = path.CGPath
self.view.layer.addSublayer(shape)
}
func midPointForPoints(p1: CGPoint, p2: CGPoint)->CGPoint{
let deltaX = (p1.x + p2.x)/2
let deltaY = (p1.y + p2.y)/2
let midPoint = CGPointMake(deltaX, deltaY)
return midPoint
}
This doesen't take the degrees of the points into account, so if I were to create the two points as:
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 300)
It would not find the proper control points and midpoint.
Hope someone can help me in the right direction. The idea is of course in the end to just know the two points (p1, p2) and dynamically create every other points, I just typed in values for the moment, to make it easier for myself. I've added images of the issue to better show you.
First create a path for a brace that starts at (0, 0) and ends at (1, 0). Then apply an affine transformation that moves, scales, and rotates the path to span your designed endpoints. It needs to transform (0, 0) to your start point and (1, 0) to your end point. Creating the transformation efficiently requires some trigonometry, but I've done the homework for you:
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
Result:
Here's the entire Swift playground I used to make the demo:
import UIKit
import PlaygroundSupport
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
class ShapeView: UIView {
override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }
lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()
}
class ViewController: UIViewController {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 200))
view.backgroundColor = .white
for (i, handle) in handles.enumerated() {
handle.autoresizingMask = [ .flexibleTopMargin, .flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin ]
let frame = CGRect(x: view.bounds.width * 0.1 + CGFloat(i) * view.bounds.width * 0.8 - 22, y: view.bounds.height / 2 - 22, width: 44, height: 44)
handle.frame = frame
handle.shapeLayer.path = CGPath(ellipseIn: handle.bounds, transform: nil)
handle.shapeLayer.lineWidth = 2
handle.shapeLayer.lineDashPattern = [2, 6]
handle.shapeLayer.lineCap = kCALineCapRound
handle.shapeLayer.strokeColor = UIColor.blue.cgColor
handle.shapeLayer.fillColor = nil
view.addSubview(handle)
let panner = UIPanGestureRecognizer(target: self, action: #selector(pannerDidFire(panner:)))
handle.addGestureRecognizer(panner)
}
brace.shapeLayer.lineWidth = 2
brace.shapeLayer.lineCap = kCALineCapRound
brace.shapeLayer.strokeColor = UIColor.black.cgColor
brace.shapeLayer.fillColor = nil
view.addSubview(brace)
setBracePath()
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setBracePath()
}
private let handles: [ShapeView] = [
ShapeView(),
ShapeView()
]
private let brace = ShapeView()
private func setBracePath() {
brace.shapeLayer.path = UIBezierPath.brace(from: handles[0].center, to: handles[1].center).cgPath
}
#objc private func pannerDidFire(panner: UIPanGestureRecognizer) {
let view = panner.view!
let offset = panner.translation(in: view)
panner.setTranslation(.zero, in: view)
var center = view.center
center.x += offset.x
center.y += offset.y
view.center = center
setBracePath()
}
}
let vc = ViewController()
PlaygroundPage.current.liveView = vc.view
The key to the problem is when the figure is rotated your base vectors will rotate. When your figure is axis-aligned your base vectors are u (1, 0) and v (0, 1).
So when you are performing midPoint.y -= 20 you can see it as the same as midPoint.x -= v.x * 20; midPoint.y -= v.y * 20 where v is (0, 1). The results are the same, check for yourself.
This implementation will do what your code does, only axis independent.
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let o = p1.plus(p2).divide(2.0) // origo
let u = p2.minus(o) // base vector 1
let v = u.turn90() // base vector 2
let c1 = o.minus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(150, 80)
let c2 = o.plus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(250, 80)
var midPoint = o.minus(v.times(0.2))
var midP1 = o.minus(u.times(0.2))
var midP2 = o.plus(u.times(0.2))
Note: I set the factors to match the initial values in your implementation.
Also added this CGPoint extension for convenience. Hope it helps.
extension CGPoint {
public func plus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x + p.x, y: self.y + p.y)
}
public func minus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x - p.x, y: self.y - p.y)
}
public func times(f: CGFloat) -> (CGPoint)
{
return CGPoint(x: self.x * f, y: self.y * f)
}
public func divide(f: CGFloat) -> (CGPoint)
{
return self.times(1.0/f)
}
public func turn90() -> (CGPoint)
{
return CGPoint(x: -self.y, y: x)
}
}
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
}