如何使用 Swift 创建波浪路径

3
我希望我的节点能够沿着正弦曲线运动,我尝试使用CGPath实现。如何创建遵循正弦曲线的CGPath?除了手动查找曲线上的点之外,是否还有其他方法可以传递正弦函数?是否可以通过贝塞尔路径来实现,然后将其转换为CGPath?谢谢。 let action = SKAction.followPath(<正弦路径>, asOffset: true, orientToPath: true, duration: 5)

波和路径应该如何定位?只能水平/垂直或在任意两点之间?我认为它只能是横向的。 - Qbyte
从右到左,最好使用@Qbyte - Dieblitzen
1个回答

9

没有内置的方法来从函数构建路径,但是您可以轻松地自己编写一个。在Swift 3中:

/// Build path within rectangle
///
/// Given a `function` that converts values between zero and one to another values between zero and one, this method will create `UIBezierPath` within `rect` using that `function`.
///
/// - parameter rect:      The `CGRect` of points on the screen.
///
/// - parameter count:     How many points should be rendered. Defaults to `rect.size.width`.
///
/// - parameter function:  A closure that will be passed an floating point number between zero and one and should return a return value between zero and one as well.

private func path(in rect: CGRect, count: Int? = nil, function: (CGFloat) -> (CGFloat)) -> UIBezierPath {
    let numberOfPoints = count ?? Int(rect.size.width)

    let path = UIBezierPath()
    path.move(to: convert(point: CGPoint(x: 0, y: function(0)), in: rect))
    for i in 1 ..< numberOfPoints {
        let x = CGFloat(i) / CGFloat(numberOfPoints - 1)
        path.addLine(to: convert(point: CGPoint(x: x, y: function(x)), in: rect))
    }
    return path
}

/// Convert point with x and y values between 0 and 1 within the `CGRect`.
///
/// - parameter point:  A `CGPoint` value with x and y values between 0 and 1.
/// - parameter rect:   The `CGRect` within which that point should be converted.

private func convert(point: CGPoint, in rect: CGRect) -> CGPoint {
    return CGPoint(
        x: rect.origin.x + point.x * rect.size.width,
        y: rect.origin.y + rect.size.height - point.y * rect.size.height
    )
}

那么,让我们传递一个函数,它随着 rectwidth 逐渐进行一个正弦曲线:

func sinePath(in rect: CGRect, count: Int? = nil) -> UIBezierPath {
    // note, since sine returns values between -1 and 1, let's add 1 and divide by two to get it between 0 and 1
    return path(in: rect, count: count) { (sin($0 * .pi * 2.0) + 1.0) / 2.0 }
}

请注意,上述假设您希望从左到右遍历,构建由函数定义的路径。您还可以进行更多的参数化呈现:
/// Build path within rectangle
///
/// Given a `function` that converts values between zero and one to another values between zero and one, this method will create `UIBezierPath` within `rect` using that `function`.
///
/// - parameter rect:      The `CGRect` of points on the screen.
///
/// - parameter count:     How many points should be rendered. Defaults to `rect.size.width` or `rect.size.width`, whichever is larger.
///
/// - parameter function:  A closure that will be passed an floating point number between zero and one and should return a `CGPoint` with `x` and `y` values between 0 and 1.

private func parametricPath(in rect: CGRect, count: Int? = nil, function: (CGFloat) -> (CGPoint)) -> UIBezierPath {
    let numberOfPoints = count ?? max(Int(rect.size.width), Int(rect.size.height))

    let path = UIBezierPath()
    let result = function(0)
    path.move(to: convert(point: CGPoint(x: result.x, y: result.y), in: rect))
    for i in 1 ..< numberOfPoints {
        let t = CGFloat(i) / CGFloat(numberOfPoints - 1)
        let result = function(t)
        path.addLine(to: convert(point: CGPoint(x: result.x, y: result.y), in: rect))
    }
    return path
}

然后您可以使用正弦曲线修改x坐标,只需递增y

func verticalSinePath(in rect: CGRect, count: Int? = nil) -> UIBezierPath {
    // note, since sine returns values between -1 and 1, let's add 1 and divide by two to get it between 0 and 1
    return parametricPath(in: rect, count: count) { CGPoint(
        x: (sin($0 * .pi * 2.0) + 1.0) / 2.0,
        y: $0
    ) }
}

这样做的好处是,您现在可以定义任何类型的路径,例如螺旋形:
func spiralPath(in rect: CGRect, count: Int? = nil) -> UIBezierPath {
    return parametricPath(in: rect, count: count) { t in
        let r = 1.0 - sin(t * .pi / 2.0)
        return CGPoint(
            x: (r * sin(t * 10.0 * .pi * 2.0) + 1.0) / 2.0,
            y: (r * cos(t * 10.0 * .pi * 2.0) + 1.0) / 2.0
        )
    }
}

以下是上述内容的Swift 2版本:
/// Build path within rectangle
///
/// Given a `function` that converts values between zero and one to another values between zero and one, this method will create `UIBezierPath` within `rect` using that `function`.
///
/// - parameter rect:      The `CGRect` of points on the screen.
///
/// - parameter count:     How many points should be rendered. Defaults to `rect.size.width`.
///
/// - parameter function:  A closure that will be passed an floating point number between zero and one and should return a return value between zero and one as well.

private func path(in rect: CGRect, count: Int? = nil, function: (CGFloat) -> (CGFloat)) -> UIBezierPath {
    let numberOfPoints = count ?? Int(rect.size.width)

    let path = UIBezierPath()
    path.moveToPoint(convert(point: CGPoint(x: 0, y: function(0)), rect: rect))
    for i in 1 ..< numberOfPoints {
        let x = CGFloat(i) / CGFloat(numberOfPoints - 1)
        path.addLineToPoint(convert(point: CGPoint(x: x, y: function(x)), rect: rect))
    }
    return path
}

/// Convert point with x and y values between 0 and 1 within the `CGRect`.
///
/// - parameter point:  A `CGPoint` value with x and y values between 0 and 1.
/// - parameter rect:   The `CGRect` within which that point should be converted.

private func convert(point point: CGPoint, rect: CGRect) -> CGPoint {
    return CGPoint(
        x: rect.origin.x + point.x * rect.size.width,
        y: rect.origin.y + rect.size.height - point.y * rect.size.height
    )
}

func sinePath(in rect: CGRect, count: Int? = nil) -> UIBezierPath {
    // note, since sine returns values between -1 and 1, let's add 1 and divide by two to get it between 0 and 1
    return path(in: rect, count: count) { (sin($0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0 }
}

/// Build path within rectangle
///
/// Given a `function` that converts values between zero and one to another values between zero and one, this method will create `UIBezierPath` within `rect` using that `function`.
///
/// - parameter rect:      The `CGRect` of points on the screen.
///
/// - parameter count:     How many points should be rendered. Defaults to `rect.size.width`.
///
/// - parameter function:  A closure that will be passed an floating point number between zero and one and should return a `CGPoint` with `x` and `y` values between 0 and 1.

private func parametricPath(in rect: CGRect, count: Int? = nil, function: (CGFloat) -> (CGPoint)) -> UIBezierPath {
    let numberOfPoints = count ?? max(Int(rect.size.width), Int(rect.size.height))

    let path = UIBezierPath()
    let result = function(0)
    path.moveToPoint(convert(point: CGPoint(x: result.x, y: result.y), rect: rect))
    for i in 1 ..< numberOfPoints {
        let t = CGFloat(i) / CGFloat(numberOfPoints - 1)
        let result = function(t)
        path.addLineToPoint(convert(point: CGPoint(x: result.x, y: result.y), rect: rect))
    }
    return path
}

func verticalSinePath(in rect: CGRect, count: Int? = nil) -> UIBezierPath {
    // note, since sine returns values between -1 and 1, let's add 1 and divide by two to get it between 0 and 1
    return parametricPath(in: rect, count: count) { CGPoint(
        x: (sin($0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0,
        y: $0
    ) }
}

func spiralPath(in rect: CGRect, count: Int? = nil) -> UIBezierPath {
    return parametricPath(in: rect, count: count) { t in
        let r = 1.0 - sin(t * CGFloat(M_PI_2))
        return CGPoint(
            x: (r * sin(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0,
            y: (r * cos(t * 10.0 * CGFloat(M_PI * 2.0)) + 1.0) / 2.0
        )
    }
}

非常感谢!这看起来像是天才的作品!不过目前它是从屏幕中心开始的。我应该怎么做才能改变它的起始位置?还有,我需要编辑哪些代码才能改变曲线的振幅和频率?再次感谢!@Rob - Dieblitzen
您还可以通过更改矩形的高度来调整振幅。 - 0x141E
@0x141E 是的,有很多种方法可以解决这个问题。 - Rob
嗨,太棒了,谢谢!只有一个最后的问题......如何反转节点的路径,使其从右到左飞行?谢谢!@Rob - Dieblitzen
你可以编写一个与 pathInRect 相反的变体,或者更简单的方法是使用参数化版本,其中 x1.0-ty 部分是您现有的函数,同样使用 1.0-t 替换之前只有 t 的部分。 - Rob
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接