简体   繁体   English

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

[英]how to make a parallel bezier curve heuristically

I have only found this blog with a relevant answer http://seant23.wordpress.com/2010/11/12/offset-bezier-curves/ ,but unfortunately i don't know the language and can't understand the maths behind it. 我只在这个博客中找到了相关答案http://seant23.wordpress.com/2010/11/12/offset-bezier-curves/ ,但是很遗憾,我不懂该语言,也无法理解其背后的数学原理它。 What i need is to know how to make a bezier curve parallel to the one that i have. 我需要的是知道如何使贝塞尔曲线平行于我的贝塞尔曲线。

I have a Point, Segment and Path class, but i don't understand how to divide the path into segments. 我有一个Point,Segment和Path类,但是我不明白如何将路径分成多个段。 The Point class has the CGPoint location public variable, the Segment class has as properties 4 points, Point *control1, *control2, *point2 and*point1; Point类具有CGPoint位置公共变量,Segment类具有作为属性的4个点,即Point * control1,* control2,* point2和* point1; the Path class contains an NSMutableArray of segments and a Point startPoint. Path类包含一个NSMutableArray段和一个Point startPoint。

I am new to objective c and i would really appreciate some help, if not for my specific class construction, at least for a more general method. 我对目标c还是陌生的,如果不是针对我的特定类的构造,至少对于更通用的方法,我将不胜感激。

I don't know about the specific problem you're solving, but one cute (and very easy) solution is to just render the outline outline of a bezier curve, eg: 我不知道您要解决的特定问题,但是一种可爱(且非常简单)的解决方案是仅绘制贝塞尔曲线的轮廓,例如:

贝塞尔曲线的轮廓

This is easily done using Core Graphics (in this case, a drawRect of a UIView subclass): 使用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;
}

Or in Swift 3: 或在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
}

If you really want to draw a parallel path, that's more complicated. 如果您真的想画一条平行的路径,那就更复杂了。 But you can render something like this (original bezier path in red, a "parallel" line in blue). 但是,您可以渲染类似的内容(红色的原始贝塞尔曲线路径,蓝色的“平行”线)。

在此处输入图片说明

I'm not entirely sure about the algorithm you've identified, but I rendered this by 我不确定您确定的算法,但是我通过

  • calculating the bezier points (the red path) myself, breaking it up finely enough to make the effect smooth; 自己计算贝塞尔曲线点(红色路径),将其细化到足以使效果平滑的程度;
  • calculate the angle between each point and the next; 计算每个点和下一个点之间的角度;
  • calculate the coordinates of the offset path (the blue one) by taking the points on the bezier path, and calculating new points that are perpendicular to the angle I just determined; 通过获取贝塞尔曲线路径上的点,并计算与刚确定的角度I垂直的新点,来计算偏移路径(蓝色坐标)的坐标; and
  • using those offset point coordinates, draw a new series of line segments to render the parallel line to the bezier. 使用这些偏移点坐标,绘制一系列新的线段,以将平行线渲染到贝塞尔曲线。

Thus, in Objective-C, that might look like: 因此,在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);
}

Or, in Swift 3: 或者,在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)
}

You might have seen the link to my article on Sean's blog, if not: http://pomax.github.io/bezierinfo/#offsetting covers offset curves in detail. 您可能会在Sean的博客上看到我的文章的链接,如果没有: http : //pomax.github.io/bezierinfo/#offsetting详细介绍了偏移曲线。 It refers to some subjects covered higher up in the article, like splitting curves at inflection points, but the takehome message is: 它指的是本文上半部分涉及的一些主题,例如在拐点处分割曲线,但要注意的是:

  1. you cannot create a true offset curve C for an arbitrary curve B. The article explains why, but the tl;dr is that a bezier curve is an integer polynomial, and except in a minute edge case, their offset curves can be perfectly modelled with a mathematical function, but it is not an integer polynomial, and as such isn't conveniently another Bezier curve. 您不能为任意曲线B创建真正的偏移曲线C。本文解释了原因,但tl; dr是贝塞尔曲线是整数多项式,并且除了在微小边缘情况下,它们的偏移曲线可以用数学函数,但是它不是整数多项式,因此也不是另一个贝塞尔曲线。
  2. you can flatten to curve to a "looks like a curve" polygon, and offset that just fine. 您可以展平以弯曲成“看起来像曲线”多边形,然后将其偏移就可以了。 This can be done really fast, and if you use enough segments, will look just fine. 这可以非常快地完成,如果您使用足够的段,看起来就很好。 It just won't scale, and you have to determine how many segments to use based on the offsetting distance. 它只是无法缩放,您必须根据偏移距离确定要使用多少段。
  3. you can cut up the curve into small sections that can be offset without loss of precision by other curves. 您可以将曲线切成小段,这些段可以偏移而不会因其他曲线而损失精度。 This is slower, but allows arbitrary distance between the curve and its offset. 这比较慢,但允许曲线与其偏移之间有任意距离。

If there is no prebuilt offsetting function in your codebase, then you're going to have to implement it yourself, in which case you're going to have to take a day or two out of your dev and dedicate it to understanding how this works (for which I'd pretty much recommend running through the bezier article. writing the offsetting algorithm requires having the functions it depends on available). 如果您的代码库中没有预构建的偏移功能,那么您将必须自己实现它,在这种情况下,您将需要花一两天的时间来开发它,并专门研究它的工作原理(为此,我非常建议您遍历bezier文章。编写偏移算法需要具有依赖于可用功能的功能)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM