Есть ли альтернатива CGPath, которая позволяет вычислять точки на пути для заданного местоположения?

Для алгоритма синхронизации анимации мне нужно указать путь в виде кривой. Вероятно, кривая Безье с контрольными точками на обоих концах.

Проблема в том, что кажется невозможным вычислить точки на CGPath, потому что CGPathRef непрозрачен. Также Apple не предоставляет механизма для вычисления точек на пути.

Существует ли библиотека или служебный класс, который может вычислять точки на кривой или пути Безье для заданного местоположения, например 0,5 для середины пути?

Или позвольте мне перефразировать: если CGPath / CGPathRef делает это невозможным, потому что он непрозрачен, и если вас интересуют только кривые Безье, есть ли способ вычислить точки для местоположений вдоль пути?


person openfrog    schedule 11.12.2013    source источник
comment
Привет, опенфрог. Что значит непрозрачный до CGPath / CGPathRef? Вы не говорите о opacity, я полагаю. У меня была та же проблема, когда я пытался получить точки на CGPath, учитывая, что я знал контрольные точки, начальную и конечную точки.   -  person Unheilig    schedule 26.12.2013


Ответы (3)


Математика пути Безье на самом деле «просто»:

start⋅(1-t)3 + 3⋅c1⋅t(1-t)2 + 3⋅c2 ⋅t2(1-t) + конец⋅t3

Это означает, что если известны начальная, конечная и обе контрольные точки (c1 и c2), то можно вычислить значение для любого t (от 0 к 1).

Если значения представляют собой точки (как на изображении ниже), тогда вы можете выполнить эти вычисления отдельно для x и y.

введите здесь описание изображения

Это моё объяснение путей Безье и код для обновления оранжевого круг при изменении ползунка (в 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)
person David Rönnqvist    schedule 16.12.2013
comment
Это правильно, но стоит отметить, что функция pow() очень медленная по сравнению с ручным умножением. Если вам нужно вычислить много точек, это может иметь серьезные последствия (обычно я не одобряю преждевременную оптимизацию, но функция pow() настолько медленная для возведения в квадрат и куба, что действительно стоит крошечных усилий, необходимых для ее расширения). Я обсуждаю вычисления Безье в ObjC в нескольких статьях: robnapier.net/blog/fast- bezier-intro-701 и robnapier.net/blog/faster-bezier-722< /а>. - person Rob Napier; 16.12.2013
comment
@RobNapier, ссылки в вашем первом комментарии не работают. Можно ли обновить? - person LGP; 13.10.2020
comment
Сейчас: robnapier.net/fast-bezier-intro и robnapier.net/faster-bezier - person Rob Napier; 13.10.2020

Если у вас уже есть контрольные точки на кривой Безье, которые вы хотели бы использовать для временной функции (я полагаю, что это CAAnimation), вам следует использовать следующую функцию, чтобы получить соответствующую временную функцию:

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

Однако, если то, что вы пытаетесь сделать, это вычислить Y-местоположение кривой Безье для данного X-местоположения, вам придется вычислить это самостоятельно. Вот ссылка на то, как это сделать: Кривые Безье

person kamprath    schedule 11.12.2013

Расчет местоположения точки из 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)
person Vlad    schedule 10.06.2018
comment
Это именно то, что я искал, большое спасибо. - person Toby Evetts; 21.10.2019