2012-12-05 102 views
12

我正在绘制我的应用程序中的图形。我的问题是,我想绘制连接顶点作为曲线的线。目前我正在用UIBezierPath的函数addLineToPoint:来绘制它们。我想绘制他们作为曲线。我很清楚UIBezierPath具有以下两项功能来支持此功能。用UIBezierPath绘制图形曲线

三次曲线:addCurveToPoint:controlPoint1:controlPoint2: 二次曲线:addQuadCurveToPoint:controlPoint:

但问题是,我没有控制点。我所拥有的是两个终点。我也没有找到确定控制点的方法/公式。有人能帮我一下吗?我会感激,如果有人可以提出一些替代方案...

回答

8

所以我找到了一个基于@ Fogmeister的答案。

UIBezierPath *path = [UIBezierPath bezierPath]; 
    [path setLineWidth:3.0]; 
    [path setLineCapStyle:kCGLineCapRound]; 
    [path setLineJoinStyle:kCGLineJoinRound]; 

    // actualPoints are my points array stored as NSValue 

    NSValue *value = [actualPoints objectAtIndex:0]; 
    CGPoint p1 = [value CGPointValue]; 
    [path moveToPoint:p1]; 

    for (int k=1; k<[actualPoints count];k++) { 

     NSValue *value = [actualPoints objectAtIndex:k]; 
     CGPoint p2 = [value CGPointValue]; 

     CGPoint centerPoint = CGPointMake((p1.x+p2.x)/2, (p1.y+p2.y)/2); 

     // See if your curve is decreasing or increasing 
     // You can optimize it further by finding point on normal of line passing through midpoint 

     if (p1.y<p2.y) { 
      centerPoint = CGPointMake(centerPoint.x, centerPoint.y+(abs(p2.y-centerPoint.y))); 
     }else if(p1.y>p2.y){ 
      centerPoint = CGPointMake(centerPoint.x, centerPoint.y-(abs(p2.y-centerPoint.y))); 
     } 

     [path addQuadCurveToPoint:p2 controlPoint:centerPoint]; 
     p1 = p2; 
    } 

    [path stroke]; 
+1

+1对于Fogmeister –

12

如果你只有两个点,那么你不能真正推断出他们的曲线。

如果你有两个以上的点(即曲线上的几个点),那么你可以大致绘制一条曲线来跟随它们。

如果你做以下...

好吧,说你有5分。 p1,p2,p3,p4和p5。 您需要计算出每对点之间的中点。 m1 = p1和p2的中点(依此类推)...

所以你有m1,m2,m3和m4。

现在你可以使用中间点,作为曲线的部分和点为四曲线的控制点的终点......

所以......

移到点M1。 将p2的四边形曲线添加到点m2作为控制点。

将p3作为控制点添加四点曲线到点m3。

将四边形曲线添加到以m4作为控制点的m4点。

等等...

这将让大家除了曲线的两端(不记得如何让他们在此刻,抱歉)。

+0

但是BRO大约增加和减少的效果的内容。对于poits的实施例Y-cordinates为了是Y1 = 10,Y2 = 15,Y = 8。现在曲线Y1-> Y2应在曲线y2-> y3 sh上方应该在线以下。这意味着我们不能把中间点作为控制点,我们需要将中间点Y上/下翻转。 –

+0

你是对的,这只是对曲线的估计。点越接近,曲线越准确。此外,曲线的梯度变化越尖锐,越远离实际的点。这是获得相当准确估计的最快,最简单的方法。要获得更高的准确性,需要更多难度更大的数学。在代码中推断曲线比通过眼睛难得多:D – Fogmeister

+0

你好@Fogmeister,我正在做同样的事情,你有上面的建议,但我没有得到非常光滑的曲线。这里是链接http://stackoverflow.com/questions/20881721/calculate-controlpoints-while-drawing-in-ios。有什么可以做得更好。我请求你请帮助我。等待您的回复 – Ranjit

3

一个好的解决方案是通过你的点创建一个直线UIBezierPath,然后用样条线来曲线。看看这个其他答案,它给出了执行Catmull-Rom样条曲线的UIBezier路径的类别。 Drawing Smooth Curves - Methods Needed

5

我在这方面做了一些工作,并从Catmull-Rom样条中获得了相当不错的结果。这是一个方法,它接受一个CGPoint数组(存储为NSValues)并将该行添加到给定的视图。

- (void)addBezierPathBetweenPoints:(NSArray *)points 
         toView:(UIView *)view 
        withColor:(UIColor *)color 
       andStrokeWidth:(NSUInteger)strokeWidth 
{ 
    UIBezierPath *path = [UIBezierPath bezierPath]; 

    float granularity = 100; 

    [path moveToPoint:[[points firstObject] CGPointValue]]; 

    for (int index = 1; index < points.count - 2 ; index++) { 

     CGPoint point0 = [[points objectAtIndex:index - 1] CGPointValue]; 
     CGPoint point1 = [[points objectAtIndex:index] CGPointValue]; 
     CGPoint point2 = [[points objectAtIndex:index + 1] CGPointValue]; 
     CGPoint point3 = [[points objectAtIndex:index + 2] CGPointValue]; 

     for (int i = 1; i < granularity ; i++) { 
      float t = (float) i * (1.0f/(float) granularity); 
      float tt = t * t; 
      float ttt = tt * t; 

      CGPoint pi; 
      pi.x = 0.5 * (2*point1.x+(point2.x-point0.x)*t + (2*point0.x-5*point1.x+4*point2.x-point3.x)*tt + (3*point1.x-point0.x-3*point2.x+point3.x)*ttt); 
      pi.y = 0.5 * (2*point1.y+(point2.y-point0.y)*t + (2*point0.y-5*point1.y+4*point2.y-point3.y)*tt + (3*point1.y-point0.y-3*point2.y+point3.y)*ttt); 

      if (pi.y > view.frame.size.height) { 
       pi.y = view.frame.size.height; 
      } 
      else if (pi.y < 0){ 
       pi.y = 0; 
      } 

      if (pi.x > point0.x) { 
       [path addLineToPoint:pi]; 
      } 
     } 

     [path addLineToPoint:point2]; 
    } 

    [path addLineToPoint:[[points objectAtIndex:[points count] - 1] CGPointValue]]; 

    CAShapeLayer *shapeView = [[CAShapeLayer alloc] init]; 

    shapeView.path = [path CGPath]; 

    shapeView.strokeColor = color.CGColor; 
    shapeView.fillColor = [UIColor clearColor].CGColor; 
    shapeView.lineWidth = strokeWidth; 
    [shapeView setLineCap:kCALineCapRound]; 

    [view.layer addSublayer:shapeView]; 
} 

我在这里使用它https://github.com/johnyorke/JYGraphViewController

20

展开对阿比德·侯赛因的12月5日'12答案。

我Implemeted一个代码,和它的工作,但结果看上去像是: enter image description here

有了一点调整,我能得到我想要的东西: enter image description here

我所做的就是addQuadCurveToPoint到以中点作为控制点。以下是基于Abid样本的代码;希望它能帮助别人。

+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points 
{ 
    UIBezierPath *path = [UIBezierPath bezierPath]; 

    NSValue *value = points[0]; 
    CGPoint p1 = [value CGPointValue]; 
    [path moveToPoint:p1]; 

    if (points.count == 2) { 
     value = points[1]; 
     CGPoint p2 = [value CGPointValue]; 
     [path addLineToPoint:p2]; 
     return path; 
    } 

    for (NSUInteger i = 1; i < points.count; i++) { 
     value = points[i]; 
     CGPoint p2 = [value CGPointValue]; 

     CGPoint midPoint = midPointForPoints(p1, p2); 
     [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)]; 
     [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)]; 

     p1 = p2; 
    } 
    return path; 
} 

static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) { 
    return CGPointMake((p1.x + p2.x)/2, (p1.y + p2.y)/2); 
} 

static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) { 
    CGPoint controlPoint = midPointForPoints(p1, p2); 
    CGFloat 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; 
} 
+1

你的结果看起来不错。那时我使用https://github.com/amogh-talpallikar/smooth-curve-algorithm绘制平滑的正弦曲线。尽管如此,为了你们的努力,他们还是很努力。 –

+0

如果连续减少两个点,这会是什么样子?即而不是1,5,3,10你有3,1,5,10?它会显示正常增长的顺利吗? – Fogmeister

+0

@Fogmeister它不会。我已经尝试过这种算法,虽然它可以非常好地交替上下笔划,但是如果数值变为6,10,12,18,就会导致像楼梯一样的阶梯。尽管如此,还是非常感谢! –

10

使用@ user1244109的答案,我在斯威夫特3. chart with my implementation

var data: [CGFloat] = [0, 0, 0, 0, 0, 0] { 
    didSet { 
     setNeedsDisplay() 
    } 
} 

func coordXFor(index: Int) -> CGFloat { 
    return bounds.height - bounds.height * data[index]/(data.max() ?? 0) 
} 

override func draw(_ rect: CGRect) { 

    let path = quadCurvedPath() 

    UIColor.black.setStroke() 
    path.lineWidth = 1 
    path.stroke() 
} 

func quadCurvedPath() -> UIBezierPath { 
    let path = UIBezierPath() 
    let step = bounds.width/CGFloat(data.count - 1) 

    var p1 = CGPoint(x: 0, y: coordXFor(index: 0)) 
    path.move(to: p1) 

    drawPoint(point: p1, color: UIColor.red, radius: 3) 

    if (data.count == 2) { 
     path.addLine(to: CGPoint(x: step, y: coordXFor(index: 1))) 
     return path 
    } 

    var oldControlP: CGPoint? 

    for i in 1..<data.count { 
     let p2 = CGPoint(x: step * CGFloat(i), y: coordXFor(index: i)) 
     drawPoint(point: p2, color: UIColor.red, radius: 3) 
     var p3: CGPoint? 
     if i == data.count - 1 { 
      p3 = nil 
     } else { 
      p3 = CGPoint(x: step * CGFloat(i + 1), y: coordXFor(index: i + 1)) 
     } 

     let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3) 

     path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2) 

     p1 = p2 
     oldControlP = imaginFor(point1: newControlP, center: p2) 
    } 
    return path; 
} 

func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? { 
    guard let p1 = point1, 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) 
} 

func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { 
    return CGPoint(x: (p1.x + p2.x)/2, y: (p1.y + p2.y)/2); 
} 

func controlPointForPoints(p1: CGPoint, p2: CGPoint, 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: imaginFor(point1: rightMidPoint, center: p2)!) 


    // this part needs for optimization 

    if p1.y < p2.y { 
     if controlPoint.y < p1.y { 
      controlPoint.y = p1.y 
     } 
     if controlPoint.y > p2.y { 
      controlPoint.y = p2.y 
     } 
    } else { 
     if controlPoint.y > p1.y { 
      controlPoint.y = p1.y 
     } 
     if controlPoint.y < p2.y { 
      controlPoint.y = p2.y 
     } 
    } 

    let imaginContol = imaginFor(point1: controlPoint, center: p2)! 
    if p2.y < p3.y { 
     if imaginContol.y < p2.y { 
      controlPoint.y = p2.y 
     } 
     if imaginContol.y > p3.y { 
      let diffY = abs(p2.y - p3.y) 
      controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1) 
     } 
    } else { 
     if imaginContol.y > p2.y { 
      controlPoint.y = p2.y 
     } 
     if imaginContol.y < p3.y { 
      let diffY = abs(p2.y - p3.y) 
      controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -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() 
} 
+0

非常好!我回到ObjC,并有几个问题。从概念上讲,imaginFor做什么?其次,为什么采取绝对值然后有条件地乘以-1? (例如,在imaginFor中,只是'让newY = center.y * 2 - p1.y'就像newX一样) – mackworth

+1

真棒..节省ME很多时间..不错的工作让它继续下去..谢谢....... 。 – gamal

1

实现一个更好的算法虽然从@罗曼·菲利波夫分析代码,我简化了一些代码。这里有一个完整的Swift操场版本,以及下面的一个ObjC版本。

我发现如果x增量不规则,那么当x轴上的点靠近在一起时,原始算法会创建一些不幸的退行。简单地将控制点约束为不超过以下x值似乎解决了这个问题,尽管我没有数学上的理由,只是试验。有两个部分标记为// **添加`执行此更改。

import UIKit 
import PlaygroundSupport 

infix operator ° 
func °(x: CGFloat, y: CGFloat) -> CGPoint { 
    return CGPoint(x: x, y: y) 
} 

extension UIBezierPath { 
    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() 
    } 

    func drawWithLine (point: CGPoint, color: UIColor) { 
     let startP = self.currentPoint 
     self.addLine(to: point) 
     drawPoint(point: point, color: color, radius: 3) 
     self.move(to: startP) 
    } 

} 

class TestView : UIView { 
    var step: CGFloat = 1.0; 
    var yMaximum: CGFloat = 1.0 
    var xMaximum: CGFloat = 1.0 
    var data: [CGPoint] = [] { 
     didSet { 
      xMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.x) }) 
      yMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.y) }) 
      setNeedsDisplay() 
     } 
    } 

    func scale(point: CGPoint) -> CGPoint { 
     return CGPoint(x: bounds.width * point.x/xMaximum , 
         y: (bounds.height - bounds.height * point.y/yMaximum)) 
    } 

    override func draw(_ rect: CGRect) { 
     if data.count <= 1 { 
      return 
     } 
     let path = cubicCurvedPath() 

     UIColor.black.setStroke() 
     path.lineWidth = 1 
     path.stroke() 
    } 

    func cubicCurvedPath() -> UIBezierPath { 
     let path = UIBezierPath() 

     var p1 = scale(point: data[0]) 
     path.drawPoint(point: p1, color: UIColor.red, radius: 3) 
     path.move(to: p1) 
     var oldControlP = p1 

     for i in 0..<data.count { 
      let p2 = scale(point:data[i]) 
      path.drawPoint(point: p2, color: UIColor.red, radius: 3) 
      var p3: CGPoint? = nil 
      if i < data.count - 1 { 
       p3 = scale(point:data [i+1]) 
      } 

      let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3) 
      //uncomment the following four lines to graph control points 
      //if let controlP = newControlP { 
      // path.drawWithLine(point:controlP, color: UIColor.blue) 
      //} 
      //path.drawWithLine(point:oldControlP, color: UIColor.gray) 

      path.addCurve(to: p2, controlPoint1: oldControlP , controlPoint2: newControlP ?? p2) 
      oldControlP = imaginFor(point1: newControlP, center: p2) ?? p1 
      //***added to algorithm 
      if let p3 = p3 { 
       if oldControlP.x > p3.x { oldControlP.x = p3.x } 
      } 
      //*** 
      p1 = p2 
     } 
     return path; 
    } 

    func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? { 
     //returns "mirror image" of point: the point that is symmetrical through center. 
     //aka opposite of midpoint; returns the point whose midpoint with point1 is center) 
     guard let p1 = point1, let center = center else { 
      return nil 
     } 
     let newX = center.x + center.x - p1.x 
     let newY = center.y + center.y - p1.y 

     return CGPoint(x: newX, y: newY) 
    } 

    func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { 
     return CGPoint(x: (p1.x + p2.x)/2, y: (p1.y + p2.y)/2); 
    } 

    func clamp(num: CGFloat, bounds1: CGFloat, bounds2: CGFloat) -> CGFloat { 
     //ensure num is between bounds. 
     if (bounds1 < bounds2) { 
      return min(max(bounds1,num),bounds2); 
     } else { 
      return min(max(bounds2,num),bounds1); 
     } 
    } 

    func controlPointForPoints(p1: CGPoint, p2: CGPoint, p3: CGPoint?) -> CGPoint? { 
     guard let p3 = p3 else { 
      return nil 
     } 

     let leftMidPoint = midPointForPoints(p1: p1, p2: p2) 
     let rightMidPoint = midPointForPoints(p1: p2, p2: p3) 
     let imaginPoint = imaginFor(point1: rightMidPoint, center: p2) 

     var controlPoint = midPointForPoints(p1: leftMidPoint, p2: imaginPoint!) 

     controlPoint.y = clamp(num: controlPoint.y, bounds1: p1.y, bounds2: p2.y) 

     let flippedP3 = p2.y + (p2.y-p3.y) 

     controlPoint.y = clamp(num: controlPoint.y, bounds1: p2.y, bounds2: flippedP3); 
     //***added: 
     controlPoint.x = clamp (num:controlPoint.x, bounds1: p1.x, bounds2: p2.x) 
     //*** 
     // print ("p1: \(p1), p2: \(p2), p3: \(p3), LM:\(leftMidPoint), RM:\(rightMidPoint), IP:\(imaginPoint), fP3:\(flippedP3), CP:\(controlPoint)") 
     return controlPoint 
    } 

} 

let u = TestView(frame: CGRect(x: 0, y: 0, width: 700, height: 600)); 
u.backgroundColor = UIColor.white 

PlaygroundPage.current.liveView = u 
u.data = [0.5 ° 1, 1 ° 3, 2 ° 5, 4 ° 9, 8 ° 15, 9.4 ° 8, 9.5 ° 10, 12 ° 4, 13 ° 10, 15 ° 3, 16 ° 1] 

而且在ObjC相同的代码(更多或更少。这不包括绘图函数本身,以及它允许​​点阵列包括丢失的数据

+ (UIBezierPath *)pathWithPoints:(NSArray <NSValue *> *)points open:(BOOL) open { 
    //based on Roman Filippov code: http://stackoverflow.com/a/40203583/580850 
    //open means allow gaps in path. 
    UIBezierPath *path = [UIBezierPath bezierPath]; 
    CGPoint p1 = [points[0] CGPointValue]; 
    [path moveToPoint:p1]; 
    CGPoint oldControlPoint = p1; 
    for (NSUInteger pointIndex = 1; pointIndex< points.count; pointIndex++) { 
     CGPoint p2 = [points[pointIndex] CGPointValue]; //note: mark missing data with CGFloatMax 

     if (p1.y >= CGFloatMax || p2.y >= CGFloatMax) { 
      if (open) { 
       [path moveToPoint:p2]; 
      } else { 
       [path addLineToPoint:p2]; 
      } 
      oldControlPoint = p2; 
     } else { 
      CGPoint p3 = CGPointZero; 
      if (pointIndex +1 < points.count) p3 = [points[pointIndex+1] CGPointValue] ; 
      if (p3.y >= CGFloatMax) p3 = CGPointZero; 
      CGPoint newControlPoint = controlPointForPoints2(p1, p2, p3); 
      if (!CGPointEqualToPoint(newControlPoint, CGPointZero)) { 
       [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: newControlPoint]; 
       oldControlPoint = imaginForPoints(newControlPoint, p2); 
       //**added to algorithm 
       if (! CGPointEqualToPoint(p3,CGPointZero)) { 
        if (oldControlPoint.x > p3.x) { 
         oldControlPoint.x = p3.x; 
        } 
       //*** 
      } else { 
       [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: p2]; 
       oldControlPoint = p2; 
      } 
     } 
     p1 = p2; 
    } 
    return path; 
} 

static CGPoint imaginForPoints(CGPoint point, CGPoint center) { 
    //returns "mirror image" of point: the point that is symmetrical through center. 
    if (CGPointEqualToPoint(point, CGPointZero) || CGPointEqualToPoint(center, CGPointZero)) { 
     return CGPointZero; 
    } 
    CGFloat newX = center.x + (center.x-point.x); 
    CGFloat newY = center.y + (center.y-point.y); 
    if (isinf(newY)) { 
     newY = BEMNullGraphValue; 
    } 
    return CGPointMake(newX,newY); 
} 

static CGFloat clamp(CGFloat num, CGFloat bounds1, CGFloat bounds2) { 
    //ensure num is between bounds. 
    if (bounds1 < bounds2) { 
     return MIN(MAX(bounds1,num),bounds2); 
    } else { 
     return MIN(MAX(bounds2,num),bounds1); 
    } 
} 

static CGPoint controlPointForPoints2(CGPoint p1, CGPoint p2, CGPoint p3) { 
    if (CGPointEqualToPoint(p3, CGPointZero)) return CGPointZero; 
    CGPoint leftMidPoint = midPointForPoints(p1, p2); 
    CGPoint rightMidPoint = midPointForPoints(p2, p3); 
    CGPoint imaginPoint = imaginForPoints(rightMidPoint, p2); 
    CGPoint controlPoint = midPointForPoints(leftMidPoint, imaginPoint); 

    controlPoint.y = clamp(controlPoint.y, p1.y, p2.y); 

    CGFloat flippedP3 = p2.y + (p2.y-p3.y); 

    controlPoint.y = clamp(controlPoint.y, p2.y, flippedP3); 
    //**added to algorithm 
    controlPoint.x = clamp(controlPoint.x, p1.x, p2.x); 
    //** 
    return controlPoint; 
} 
2

这里的@ user1244109“的回答转换为夫特3

private func quadCurvedPath(with points:[CGPoint]) -> UIBezierPath { 
    let path = UIBezierPath() 
    var p1 = points[0] 

    path.move(to: p1) 

    if points.count == 2 { 
    path.addLine(to: points[1]) 
    return path 
    } 

    for i in 0..<points.count { 
    let mid = midPoint(for: (p1, points[i])) 
    path.addQuadCurve(to: mid, 
        controlPoint: controlPoint(for: (mid, p1))) 
    path.addQuadCurve(to: points[i], 
        controlPoint: controlPoint(for: (mid, points[i]))) 
    p1 = points[i] 
    } 
    return path 
} 


private func midPoint(for points: (CGPoint, CGPoint)) -> CGPoint { 
    return CGPoint(x: (points.0.x + points.1.x)/2 , y: (points.0.y + points.1.y)/2) 
} 


private func controlPoint(for points: (CGPoint, CGPoint)) -> CGPoint { 
    var controlPoint = midPoint(for: points) 
    let diffY = abs(points.1.y - controlPoint.y) 

    if points.0.y < points.1.y { 
    controlPoint.y += diffY 
    } else if points.0.y > points.1.y { 
    controlPoint.y -= diffY 
    } 
    return controlPoint 
}