在分析来自@roman-filippov 的代码时,我对代码进行了一些简化。这是一个完整的 Swift Playground 版本,下面是一个 ObjC 版本。
我发现如果 x 增量不规则,那么当点在 x 轴上靠得很近时,原始算法会创建一些不幸的逆行线。简单地将控制点限制为不超过以下 x 值似乎可以解决问题,尽管我对此没有数学依据,只是实验。有两个部分标记为 //** added` 实现了此更改。
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;
}