有没有替代CGPath的方法可以计算给定位置上路径上的点?

3
为了动画时序算法,我需要提供一条曲线路径。可能是一个有两端控制点的贝塞尔曲线。
问题在于似乎不可能计算出CGPath上的点,因为CGPathRef是不透明的。此外,苹果没有提供任何计算路径上点的机制。
是否有一个库或实用程序类可以计算给定位置(如路径中间的0.5)的贝塞尔曲线或路径上的点?
或者让我重新表述一下:如果CGPath / CGPathRef使这个过程变得不可能,因为它是不透明的,并且如果你只关心贝塞尔曲线,是否有一种方法来计算沿着路径的位置的点?

你好,openfrog。在 CGPath / CGPathRef 中,“opaque”是什么意思?我想你不是在谈论“opacity”。我也遇到了同样的问题,尝试从 CGPath 中获取点,因为我知道控制点、起始点和结束点。 - Unheilig
3个回答

6
Bézier路径背后的数学实际上是“只需”:起点⋅(1-t)³ + 3⋅c₁⋅t(1-t)² + 3⋅c₂⋅t²(1-t) + 终点⋅t³。这意味着如果你知道起点、终点和两个控制点(c₁和c₂),那么你可以计算出任何t值(从0到1)的值。如果值是点(如下图所示),则可以分别对x和y进行这些计算。

enter image description here

这是一个关于贝塞尔路径的解释(点击此处),而根据滑块变化更新橙色圆形的代码(使用Javascript)只需这么简单(虽然将其翻译成Objective-C或C并不难,但我有些懒惰):
var sx = 190; var sy = 80; // start
var ex = 420; var ey = 250; // end

var c1x = -30; var c1y = 350; // control point 1
var c2x = 450; var c2y = -20; // control point 2

var t = (x-minSliderX)/(maxSliderX-minSliderX); // t from 0 to 1

var px = sx*Math.pow(1-t, 3) + 3*c1x*t*Math.pow(1-t, 2) + 3*c2x*Math.pow(t,2)*(1-t) + ex*Math.pow(t, 3);
var py = sy*Math.pow(1-t, 3) + 3*c1y*t*Math.pow(1-t, 2) + 3*c2y*Math.pow(t,2)*(1-t) + ey*Math.pow(t, 3);
// new point is at (px, py)

2
这是正确的,但值得注意的是,与手动乘法相比,pow()函数非常慢。如果您需要计算大量点,则可能会产生严重影响(我通常不鼓励过早优化,但是pow()对于平方和立方太慢了,因此扩展它所需的微小麻烦确实值得)。我在几篇文章中讨论了ObjC中的Bézier计算:http://robnapier.net/blog/fast-bezier-intro-701和http://robnapier.net/blog/faster-bezier-722。 - Rob Napier
1
还有一个小提示:沿着贝塞尔曲线移动不是线性的。虽然0.5会在中间,但0.25不会在它的四分之一处。如果您需要平滑地沿着曲线移动,则必须计算曲线长度,这并不容易。有关此类函数的示例,请参见https://github.com/iosptl/ios6ptl/blob/master/ch26/CurvyText/CurvyText/CurvyTextView.m中的`offsetAtDistance:fromPoint:offset:`。 - Rob Napier
@RobNapier,您在第一条评论中的链接已经失效了。是否可以更新一下? - LGP
现在:https://robnapier.net/fast-bezier-intro 和 https://robnapier.net/faster-bezier - Rob Napier

2

从CGPath计算点位置(Swift 4)。

extension Math {

   // Inspired by ObjC version of this code: https://github.com/ImJCabus/UIBezierPath-Length/blob/master/UIBezierPath%2BLength.m
   public class BezierPath {

      public let cgPath: CGPath
      public let approximationIterations: Int

      private (set) lazy var subpaths = processSubpaths(iterations: approximationIterations)
      public private (set) lazy var length = subpaths.reduce(CGFloat(0)) { $0 + $1.length }

      public init(cgPath: CGPath, approximationIterations: Int = 100) {
         self.cgPath = cgPath
         self.approximationIterations = approximationIterations
      }
   }
}

extension Math.BezierPath {

   public func point(atPercentOfLength: CGFloat) -> CGPoint {

      var percent = atPercentOfLength
      if percent < 0 {
         percent = 0
      } else if percent > 1 {
         percent = 1
      }

      let pointLocationInPath = length * percent
      var currentLength: CGFloat = 0
      var subpathContainingPoint = Subpath(type: .moveToPoint)
      for element in subpaths {
         if currentLength + element.length >= pointLocationInPath {
            subpathContainingPoint = element
            break
         } else {
            currentLength += element.length
         }
      }

      let lengthInSubpath = pointLocationInPath - currentLength
      if subpathContainingPoint.length == 0 {
         return subpathContainingPoint.endPoint
      } else {
         let t = lengthInSubpath / subpathContainingPoint.length
         return point(atPercent: t, of: subpathContainingPoint)
      }
   }

}

extension Math.BezierPath {

   struct Subpath {

      var startPoint: CGPoint = .zero
      var controlPoint1: CGPoint = .zero
      var controlPoint2: CGPoint = .zero
      var endPoint: CGPoint = .zero
      var length: CGFloat = 0

      let type: CGPathElementType

      init(type: CGPathElementType) {
         self.type = type
      }
   }

   private typealias SubpathEnumerator = @convention(block) (CGPathElement) -> Void

   private func enumerateSubpaths(body: @escaping SubpathEnumerator) {
      func applier(info: UnsafeMutableRawPointer?, element: UnsafePointer<CGPathElement>) {
         if let info = info {
            let callback = unsafeBitCast(info, to: SubpathEnumerator.self)
            callback(element.pointee)
         }
      }
      let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
      cgPath.apply(info: unsafeBody, function: applier)
   }

   func processSubpaths(iterations: Int) -> [Subpath] {

      var subpathArray: [Subpath] = []
      var currentPoint = CGPoint.zero
      var moveToPointSubpath: Subpath?
      enumerateSubpaths { element in
         let elType = element.type
         let points = element.points
         var subLength: CGFloat = 0
         var endPoint = CGPoint.zero
         var subpath = Subpath(type: elType)
         subpath.startPoint = currentPoint

         switch elType {
         case .moveToPoint:
            endPoint = points[0]
         case .addLineToPoint:
            endPoint = points[0]
            subLength = type(of: self).linearLineLength(from: currentPoint, to: endPoint)
         case .addQuadCurveToPoint:
            endPoint = points[1]
            let controlPoint = points[0]
            subLength = type(of: self).quadCurveLength(from: currentPoint, to: endPoint, controlPoint: controlPoint,
                                                       iterations: iterations)
            subpath.controlPoint1 = controlPoint
         case .addCurveToPoint:
            endPoint = points[2]
            let controlPoint1 = points[0]
            let controlPoint2 = points[1]
            subLength = type(of: self).cubicCurveLength(from: currentPoint, to: endPoint, controlPoint1: controlPoint1,
                                                        controlPoint2: controlPoint2, iterations: iterations)
            subpath.controlPoint1 = controlPoint1
            subpath.controlPoint2 = controlPoint2
         case .closeSubpath:
            break
         }
         subpath.length = subLength
         subpath.endPoint = endPoint
         if elType != .moveToPoint {
            subpathArray.append(subpath)
         } else {
            moveToPointSubpath = subpath
         }
         currentPoint = endPoint
      }

      if subpathArray.isEmpty, let subpath = moveToPointSubpath {
         subpathArray.append(subpath)
      }
      return subpathArray
   }

   private func point(atPercent t: CGFloat, of subpath: Subpath) -> CGPoint {
      var p = CGPoint.zero
      switch subpath.type {
      case .addLineToPoint:
         p = type(of: self).linearBezierPoint(t: t, start: subpath.startPoint, end: subpath.endPoint)
      case .addQuadCurveToPoint:
         p = type(of: self).quadBezierPoint(t: t, start: subpath.startPoint, c1: subpath.controlPoint1, end: subpath.endPoint)
      case .addCurveToPoint:
         p = type(of: self).cubicBezierPoint(t: t, start: subpath.startPoint, c1: subpath.controlPoint1, c2: subpath.controlPoint2,
                              end: subpath.endPoint)
      default:
         break
      }
      return p
   }

}

extension Math.BezierPath {

   @inline(__always)
   public static func linearLineLength(from: CGPoint, to: CGPoint) -> CGFloat {
      return sqrt(pow(to.x - from.x, 2) + pow(to.y - from.y, 2))
   }

   public static func quadCurveLength(from: CGPoint, to: CGPoint, controlPoint: CGPoint, iterations: Int) -> CGFloat {
      var length: CGFloat = 0
      let divisor = 1.0 / CGFloat(iterations)

      for idx in 0 ..< iterations {
         let t = CGFloat(idx) * divisor
         let tt = t + divisor
         let p = quadBezierPoint(t: t, start: from, c1: controlPoint, end: to)
         let pp = quadBezierPoint(t: tt, start: from, c1: controlPoint, end: to)
         length += linearLineLength(from: p, to: pp)
      }
      return length
   }

   public static func cubicCurveLength(from: CGPoint, to: CGPoint, controlPoint1: CGPoint,
                                       controlPoint2: CGPoint, iterations: Int) -> CGFloat {
      let iterations = 100
      var length: CGFloat = 0
      let divisor = 1.0 / CGFloat(iterations)

      for idx in 0 ..< iterations {
         let t = CGFloat(idx) * divisor
         let tt = t + divisor
         let p = cubicBezierPoint(t: t, start: from, c1: controlPoint1, c2: controlPoint2, end: to)
         let pp = cubicBezierPoint(t: tt, start: from, c1: controlPoint1, c2: controlPoint2, end: to)
         length += linearLineLength(from: p, to: pp)
      }
      return length
   }

   @inline(__always)
   public static func linearBezierPoint(t: CGFloat, start: CGPoint, end: CGPoint) -> CGPoint{
      let dx = end.x - start.x
      let dy = end.y - start.y
      let px = start.x + (t * dx)
      let py = start.y + (t * dy)
      return CGPoint(x: px, y: py)
   }

   @inline(__always)
   public static func quadBezierPoint(t: CGFloat, start: CGPoint, c1: CGPoint, end: CGPoint) -> CGPoint {
      let x = QuadBezier(t: t, start: start.x, c1: c1.x, end: end.x)
      let y = QuadBezier(t: t, start: start.y, c1: c1.y, end: end.y)
      return CGPoint(x: x, y: y)
   }

   @inline(__always)
   public static func cubicBezierPoint(t: CGFloat, start: CGPoint, c1: CGPoint, c2: CGPoint, end: CGPoint) -> CGPoint {
      let x = CubicBezier(t: t, start: start.x, c1: c1.x, c2: c2.x, end: end.x)
      let y = CubicBezier(t: t, start: start.y, c1: c1.y, c2: c2.y, end: end.y)
      return CGPoint(x: x, y: y)
   }

   /*
    *  http://ericasadun.com/2013/03/25/calculating-bezier-points/
    */
   @inline(__always)
   public static func CubicBezier(t: CGFloat, start: CGFloat, c1: CGFloat, c2: CGFloat, end: CGFloat) -> CGFloat {
      let t_ = (1.0 - t)
      let tt_ = t_ * t_
      let ttt_ = t_ * t_ * t_
      let tt = t * t
      let ttt = t * t * t

      return start * ttt_
         + 3.0 *  c1 * tt_ * t
         + 3.0 *  c2 * t_ * tt
         + end * ttt
   }

   /*
    *  http://ericasadun.com/2013/03/25/calculating-bezier-points/
    */
   @inline(__always)
   public static func QuadBezier(t: CGFloat, start: CGFloat, c1: CGFloat, end: CGFloat) -> CGFloat {
      let t_ = (1.0 - t)
      let tt_ = t_ * t_
      let tt = t * t

      return start * tt_
         + 2.0 *  c1 * t_ * t
         + end * tt
   }
}

使用方法:

let path = CGMutablePath()
path.move(to: CGPoint(x: 10, y: 10))
path.addQuadCurve(to: CGPoint(x: 100, y: 100), control: CGPoint(x: 50, y: 50))
let pathCalc = Math.BezierPath(cgPath: path)
let pointAtTheMiddleOfThePath = pathCalc.point(atPercentOfLength: 0.5)

这正是我一直在寻找的,非常感谢。 - Toby Evetts

1
如果您已经有了贝塞尔曲线的控制点,并且想将其用于时间函数(我猜是 CAAnimation ),那么应该使用以下函数获取适当的时间函数:
[CAMediaTimingFunction functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y]

然而,如果你想要计算给定X位置的贝塞尔曲线的Y位置,你需要自己计算。这里提供了一个参考:贝塞尔曲线


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