2

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

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

我是目标 c 的新手,如果不是针对我的特定班级建设,至少对于更通用的方法,我将非常感谢一些帮助。

4

2 回答 2

9

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

贝塞尔曲线的轮廓

这很容易使用 Core Graphics 完成(在这种情况下,是drawRect一个UIView子类):

- (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
}

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

在此处输入图像描述

我不完全确定您确定的算法,但我通过

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

因此,在 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)
}
于 2013-08-02T12:55:33.050 回答
3

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

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

如果您的代码库中没有预建的偏移功能,那么您将不得不自己实现它,在这种情况下,您将不得不花一两天时间从您的开发中抽出时间来了解它是如何工作的(为此,我非常推荐阅读 bezier 文章。编写偏移算法需要具有它所依赖的可用函数)。

于 2013-08-02T17:44:39.863 回答