如何以启发式方式制作平行贝塞尔曲线

我只发现这个博客的相关答案http://seant23.wordpress.com/2010/11/12/offset-bezier-curves/ ,但不幸的是我不懂语言,无法理解背后的数学它。 我需要知道如何使贝塞尔曲线与我所拥有的曲线平行。

我有一个Point,Segment和Path类,但我不明白如何将路径分成段。 Point类具有CGPoint位置公共变量,Segment类具有4个属性,Point * control1,* control2,* point2和* point1; Path类包含一个NSMutableArray段和一个Point startPoint。

我是目标c的新手,我真的很感激一些帮助,如果不是我的特定class级建设,至少对于更通用的方法。

我不知道你正在解决的具体问题,但一个可爱(而且非常简单)的解决方案就是渲染贝塞尔曲线的轮廓轮廓,例如:

贝塞尔曲线的轮廓

这很容易使用Core Graphics(在本例中是UIView子类的drawRect )完成:

 - (void)drawRect:(CGRect)rect { CGPathRef path = [self newBezierPath]; CGPathRef outlinePath = CGPathCreateCopyByStrokingPath(path, NULL, 10, kCGLineCapButt, kCGLineJoinBevel, 0); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetLineWidth(context, 3.0); CGContextAddPath(context, outlinePath); CGContextSetStrokeColorWithColor(context, [[UIColor redColor] CGColor]); CGContextDrawPath(context, kCGPathStroke); CGPathRelease(path); CGPathRelease(outlinePath); } - (CGPathRef)newBezierPath { CGPoint point1 = CGPointMake(10.0, 50.0); CGPoint point2 = CGPointMake(self.bounds.size.width - 10.0, point1.y + 150.0); CGPoint controlPoint1 = CGPointMake(point1.x + 400.0, point1.y); CGPoint controlPoint2 = CGPointMake(point2.x - 400.0, point2.y); CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, NULL, point1.x, point1.y); CGPathAddCurveToPoint(path, NULL, controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, point2.x, point2.y); return path; } 

或者在Swift 3中:

 override func draw(_ rect: CGRect) { let path = bezierPath().cgPath let outlinePath = path.copy(strokingWithWidth: 10, lineCap: .butt, lineJoin: .bevel, miterLimit: 0) let context = UIGraphicsGetCurrentContext()! context.setLineWidth(3) context.addPath(outlinePath) context.setStrokeColor(UIColor.red.cgColor) context.strokePath() } private func bezierPath() -> UIBezierPath { let point1 = CGPoint(x: 10.0, y: 50.0) let point2 = CGPoint(x: bounds.size.width - 10.0, y: point1.y + 150.0) let controlPoint1 = CGPoint(x: point1.x + 400.0, y: point1.y) let controlPoint2 = CGPoint(x: point2.x - 400.0, y: point2.y) let path = UIBezierPath() path.move(to: point1) path.addCurve(to: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2) return path } 

如果你真的想绘制一条平行路径,那就更复杂了。 但你可以渲染这样的东西(红色的原始贝塞尔路径,蓝色的“平行”线)。

在此处输入图像描述

我不完全确定你已经确定的算法,但我通过这个算法来实现

  • 我自己计算贝塞尔点(红色路径),将其分解得足够精细,使效果更加平滑;
  • 计算每个点和下一个点之间的角度;
  • 通过取贝塞尔路径上的点,计算与刚刚确定的角度I垂直的新点,计算偏移路径(蓝色路径)的坐标; 和
  • 使用这些偏移点坐标,绘制一系列新的线段以将平行线渲染到贝塞尔曲线。

因此,在Objective-C中,可能看起来像:

 - (void)drawRect:(CGRect)rect { CGPoint point1 = CGPointMake(10.0, 50.0); CGPoint point2 = CGPointMake(self.bounds.size.width - 10.0, point1.y + 150.0); CGPoint controlPoint1 = CGPointMake(point1.x + 400.0, point1.y); CGPoint controlPoint2 = CGPointMake(point2.x - 400.0, point2.y); // draw original bezier path in red [[UIColor redColor] setStroke]; [[self bezierPathFromPoint1:point1 point2:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2] stroke]; // calculate and draw offset bezier curve in blue [[UIColor blueColor] setStroke]; [[self offsetBezierPathBy:10.0 point1:point1 point2:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2] stroke]; } - (UIBezierPath *)bezierPathFromPoint1:(CGPoint)point1 point2:(CGPoint)point2 controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2 { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:point1]; [path addCurveToPoint:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2]; return path; } - (UIBezierPath *)offsetBezierPathBy:(CGFloat)offset point1:(CGPoint)point1 point2:(CGPoint)point2 controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2 { UIBezierPath *path = [UIBezierPath bezierPath]; static NSInteger numberOfPoints = 100; CGPoint previousPoint = [self cubicBezierAtTime:0.0 point1:point1 point2:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2]; CGPoint point; double angle; for (NSInteger i = 1; i <= numberOfPoints; i++) { double t = (double) i / numberOfPoints; point = [self cubicBezierAtTime:t point1:point1 point2:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2]; // calculate the angle to the offset point // this is the angle between the two points, plus 90 degrees (pi / 2.0) angle = atan2(point.y - previousPoint.y, point.x - previousPoint.x) + M_PI_2; if (i == 1) [path moveToPoint:[self offsetPoint:previousPoint by:offset angle:angle]]; previousPoint = point; [path addLineToPoint:[self offsetPoint:previousPoint by:offset angle:angle]]; } return path; } // return point offset by particular distance and particular angle - (CGPoint)offsetPoint:(CGPoint)point by:(CGFloat)offset angle:(double)angle { return CGPointMake(point.x + cos(angle) * offset, point.y + sin(angle) * offset); } // Manually calculate cubic bezier curve // // B(t) = (1-t)^3 * point1 + 3 * (1-t)^2 * t controlPoint1 + 3 * (1-t) * t^2 * pointPoint2 + t^3 * point2 - (CGPoint)cubicBezierAtTime:(double)t point1:(CGPoint)point1 point2:(CGPoint)point2 controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2 { double oneMinusT = 1.0 - t; double oneMinusTSquared = oneMinusT * oneMinusT; double oneMinusTCubed = oneMinusTSquared * oneMinusT; double tSquared = t * t; double tCubed = tSquared * t; CGFloat x = point1.x * oneMinusTCubed + 3.0 * oneMinusTSquared * t * controlPoint1.x + 3.0 * oneMinusT * tSquared * controlPoint2.x + tCubed * point2.x; CGFloat y = point1.y * oneMinusTCubed + 3.0 * oneMinusTSquared * t * controlPoint1.y + 3.0 * oneMinusT * tSquared * controlPoint2.y + tCubed * point2.y; return CGPointMake(x, y); } 

或者,在Swift 3中:

 override func draw(_ rect: CGRect) { let point1 = CGPoint(x: 10.0, y: 50.0) let point2 = CGPoint(x: bounds.size.width - 10.0, y: point1.y + 150.0) let controlPoint1 = CGPoint(x: point1.x + 400.0, y: point1.y) let controlPoint2 = CGPoint(x: point2.x - 400.0, y: point2.y) UIColor.red.setStroke() bezierPath(from: point1, to: point2, withControl: controlPoint1, and: controlPoint2).stroke() UIColor.blue.setStroke() offSetBezierPath(by: 5, from: point1, to: point2, withControl: controlPoint1, and: controlPoint2).stroke() } private func bezierPath(from point1: CGPoint, to point2: CGPoint, withControl controlPoint1: CGPoint, and controlPoint2:CGPoint) -> UIBezierPath { let path = UIBezierPath() path.move(to: point1) path.addCurve(to: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2) return path } private func offSetBezierPath(by offset: CGFloat, from point1: CGPoint, to point2: CGPoint, withControl controlPoint1: CGPoint, and controlPoint2:CGPoint) -> UIBezierPath { let path = UIBezierPath() let numberOfPoints = 100 var previousPoint = cubicBezier(at: 0, point1: point1, point2: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2) for i in 1 ... numberOfPoints { let time = CGFloat(i) / CGFloat(numberOfPoints) let point = cubicBezier(at: time, point1: point1, point2: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2) // calculate the angle to the offset point // this is the angle between the two points, plus 90 degrees (pi / 2.0) let angle = atan2(point.y - previousPoint.y, point.x - previousPoint.x) + .pi / 2; if i == 1 { path.move(to: calculateOffset(of: previousPoint, by: offset, angle: angle)) } previousPoint = point path.addLine(to: calculateOffset(of: previousPoint, by: offset, angle: angle)) } return path } /// Return point offset by particular distance and particular angle /// /// - Parameters: /// - point: Point to offset. /// - offset: How much to offset by. /// - angle: At what angle. /// /// - Returns: New `CGPoint`. private func calculateOffset(of point: CGPoint, by offset: CGFloat, angle: CGFloat) -> CGPoint { return CGPoint(x: point.x + cos(angle) * offset, y: point.y + sin(angle) * offset) } /// Manually calculate cubic bezier curve /// /// B(t) = (1-t)^3 * point1 + 3 * (1-t)^2 * t controlPoint1 + 3 * (1-t) * t^2 * pointPoint2 + t^3 * point2 /// /// - Parameters: /// - time: Time, a value between zero and one. /// - point1: Starting point. /// - point2: Ending point. /// - controlPoint1: First control point. /// - controlPoint2: Second control point. /// /// - Returns: Point on bezier curve. private func cubicBezier(at time: CGFloat, point1: CGPoint, point2: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) -> CGPoint { let oneMinusT = 1.0 - time let oneMinusTSquared = oneMinusT * oneMinusT let oneMinusTCubed = oneMinusTSquared * oneMinusT let tSquared = time * time let tCubed = tSquared * time var x = point1.x * oneMinusTCubed x += 3.0 * oneMinusTSquared * time * controlPoint1.x x += 3.0 * oneMinusT * tSquared * controlPoint2.x x += tCubed * point2.x var y = point1.y * oneMinusTCubed y += 3.0 * oneMinusTSquared * time * controlPoint1.y y += 3.0 * oneMinusT * tSquared * controlPoint2.y y += tCubed * point2.y return CGPoint(x: x, y: y) } 

您可能已经看到了我在Sean博客上的文章链接,如果没有: http : //pomax.github.io/bezierinfo/#offsetting详细介绍了偏移曲线。 它指的是文章中更高层覆盖的一些主题,如在拐点处分割曲线,但是主题消息是:

  1. 你不能为任意曲线B创建一个真正的偏移曲线C.文章解释了为什么,但是tl; dr是一个贝塞尔曲线是一个整数多项式,除了在一个微小的边缘情况下,它们的偏移曲线可以完美地建模数学函数,但它不是整数多项式,因此不方便另一个贝塞尔曲线。
  2. 你可以变平以弯曲成“看起来像一个曲线”的多边形,并且偏移就好了。 这可以非常快速地完成,如果你使用足够的段,看起来会很好。 它只是不会缩放,您必须根据偏移距离确定要使用的段数。
  3. 您可以将曲线切割成可以偏移的小部分,而不会损失其他曲线的精度。 这个速度较慢,但​​允许曲线与其偏移之间的任意距离。

如果您的代码库中没有预先构建的偏移function,那么您将不得不自己实现它,在这种情况下,您将不得不花费一两天的时间从您的开发人员那里开始,并将其用于了解其工作原理(我非常建议您浏览bezier文章。编写偏移算法需要具有可用的function)。