是否有替代CGPath允许计算给定位置的路径上的点?

对于动画计时算法,我需要提供一条路径作为曲线。 可能是两端带控制点的贝塞尔曲线。

问题是似乎无法计算CGPath上的点,因为CGPathRef是不透明的。 Apple也没有提供计算路径上的点的机制。

是否有一个库或实用程序类可以计算贝塞尔曲线或路径上的点,对于给定位置(如路径中间的0.5)?

或者让我重新说一下:如果CGPath / CGPathRef使这不可能,因为它是不透明的,如果你只关心bezier曲线,有没有办法计算沿路径位置的点?

Bézier路径背后的数学实际上是“正义”:

start⋅(1-t) 3 +3⋅c1⋅t(1-t) 2 +3⋅c2⋅t2(1-t)+end⋅t3

这意味着如果您知道开始,结束和两个控制点(c 1和c 2 ),那么您可以计算任何t的值(从0到1)。

它的值是点(如下图所示),然后您可以分别为x和y进行这些计算。

在此处输入图像描述

这是我对Bézier路径的解释以及更新橙色圆圈的代码,因为滑块更改(在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) 

如果您已经将bezier曲线的控制点用于计时function(我认为是CAAnimation ),那么您应该使用以下函数来获得适当的计时function:

 [CAMediaTimingFunction functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y] 

但是,如果您要做的是计算给定X位置的贝塞尔曲线的Y位置,则必须自己计算。 以下是如何执行此操作的参考: Bezier曲线

从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) { 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)