计算三次贝塞尔曲线控制点的一种方法

2024-06-12  本文已影响0人  devVector

为什么不用二次曲线

如何用三次曲线解决以上问题

曲线 A-B-C-D-E: A-B 曲线 AB.control1 在AB的连接线上,AB.control2、 BC.control1、点B在一条直线上, 做角ABC 的角平分线 BM, 做BM的垂线 BN, BN可以作为AB.control2、 BC.control1所在直线,至于控制点距离点B的距离可以取AB、BC的1/3(没有固定的数字,可以根据需要调整);以此类推,分别求出中间每段曲线的2个控制点, 最后一段曲线DE的control2则在DE的连接线上。

绿色圆圈为采样点,红色小圆点为control1, 红色方块为control2

Simulator Screen Shot - iPhone 14 Pro Max - 2024-06-13 at 14.31.46.png


struct XPoint {
    var time: TimeInterval
    var point: CGPoint

    init(time: TimeInterval, point: CGPoint) {
        self.time = time
        self.point = point
    }
}

/// 最后的线段长度为0,每3个点A, O, B, 做角AOB的角平分线OM,在经过点O做与OM的垂线,射线方向与 AB方向一致, 求出该射线的单位向量
struct XCurvePoint {
    /// 点 O
    var point: CGPoint
    
    var vector: CGPoint
    
    /// point 到下一个点的直线长度
    var length: CGFloat
    var duration: TimeInterval
    init(point: CGPoint, vector: CGPoint, length: CGFloat, duration: TimeInterval) {
        self.point = point
        self.vector = vector
        self.length = length
        self.duration = duration
    }
}

struct XCurve {
    struct Item {
        var end: CGPoint
        var control1: CGPoint
        var control2: CGPoint
        init(end: CGPoint, control1: CGPoint, control2: CGPoint) {
            self.end = end
            self.control1 = control1
            self.control2 = control2
        }
    }
    var start: CGPoint
    var items: [Item] = []
    
    init(start: CGPoint) {
        self.start = start
    }
    
    mutating func appenCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) {
        self.items.append(Item(end: endPoint, control1: controlPoint1, control2: controlPoint2))
    }
}

public final class CGUtil : NSObject {
    public static func pointToRect(_ point: CGPoint, size: CGFloat) -> CGRect {
        return CGRect(x: point.x - size / 2, y: point.y - size / 2, width: size, height: size)
    }
    
    public static func vectorAdd(start: CGPoint, end: CGPoint) -> (CGPoint, CGFloat) {
        let dx = end.x - start.x
        let dy = end.y - start.y
        let len = sqrt(dx * dx + dy * dy)
        let scale = 1.0 / len
        return (CGPoint(x: dx * scale, y: dy * scale), len)
    }
    
    // 返回start->end 的单位向量,长度
    public static func vectorLength(start: CGPoint, end: CGPoint) -> (CGPoint, CGFloat) {
        let dx = end.x - start.x
        let dy = end.y - start.y
        let len = sqrt(dx * dx + dy * dy)
        let scale = 1.0 / len
        return (CGPoint(x: dx * scale, y: dy * scale), len)
    }
    
    // 根据dx、dy生成单位向量
    public static func vector(dx: CGFloat, dy: CGFloat) -> CGPoint {
        let len = sqrt(dx * dx + dy * dy)
        let scale = 1.0 / len
        return CGPoint(x: dx * scale, y: dy * scale)
    }
    
    /// 返回 角start-point-end的角平分线的垂线(方向与start->end 方向一致) 的单位向量  和 point-end之间的长度
    public static func vector(start: CGPoint, end: CGPoint, point: CGPoint) -> (CGPoint, CGFloat) {
        let a = vectorLength(start: start, end: point)
        let b = vectorLength(start: point, end: end)
        return (vector(dx: b.0.x + a.0.x, dy: b.0.y + a.0.y), b.1)
    }
    
    // point + vector * length
    public static func pointAdd(point: CGPoint, vector: CGPoint, length: CGFloat) -> CGPoint {
        return CGPoint(x: point.x + vector.x * length, y: point.y + vector.y * length)
    }
    
    // point - vector * length
    public static func pointSubtract(point: CGPoint, vector: CGPoint, length: CGFloat) -> CGPoint {
        return CGPoint(x: point.x - vector.x * length, y: point.y - vector.y * length)
    }
}

class XPath {
    let needFill: Bool
    let points: UIBezierPath
    let controlPoints: UIBezierPath
    let path: UIBezierPath
    
    init(curve: XCurve) {
        let points: UIBezierPath = UIBezierPath()
        let controlPoints: UIBezierPath = UIBezierPath()
        let path: UIBezierPath = UIBezierPath()
        path.lineWidth = 2
        points.lineWidth = 2
        controlPoints.lineWidth = 2
        
        if curve.items.isEmpty {
            self.needFill = true
            path.move(to: curve.start)
            points.append(UIBezierPath(ovalIn: CGUtil.pointToRect(curve.start, size: 4)))
            path.append(UIBezierPath(ovalIn: CGUtil.pointToRect(curve.start, size: 2)))
            
        } else {
            self.needFill = false
            path.move(to: curve.start)
            points.append(UIBezierPath(ovalIn: CGUtil.pointToRect(curve.start, size: 4)))
            curve.items.forEach { item in
                path.addCurve(to: item.end, controlPoint1: item.control1, controlPoint2: item.control2)
                controlPoints.append(UIBezierPath(ovalIn: CGUtil.pointToRect(item.control1, size: 3)))
                controlPoints.append(UIBezierPath(rect: CGUtil.pointToRect(item.control2, size: 3)))
                points.append(UIBezierPath(ovalIn: CGUtil.pointToRect(item.end, size: 4)))
            }
        }
        
        self.path = path
        self.controlPoints = controlPoints
        self.points = points
    }
    
    func draw(context: CGContext, rect: CGRect) {
        context.setStrokeColor(UIColor.green.cgColor)
        context.addPath(self.points.cgPath)
        context.strokePath()

        context.setFillColor(UIColor.red.cgColor)
        context.addPath(self.controlPoints.cgPath)
        context.fillPath()
        
        context.setStrokeColor(UIColor.blue.cgColor)
        context.setFillColor(UIColor.blue.cgColor)
        context.addPath(self.path.cgPath)
        context.strokePath()
        
        if self.needFill {
            context.fillPath()
        }
    }
}

class XPathBuilder {
    private var points: [XPoint] = []
    
    func append(_ point: XPoint) {
        self.points.append(point)
    }
    
    func finish() ->XPath? {
        guard !self.points.isEmpty else {
            return nil
        }
        
        guard self.points.count >= 2 else {
            let start = self.points[0].point
            return XPath(curve: XCurve(start: start))
        }
        
        var prev = CGPoint(x: CGFloat.nan, y: CGFloat.nan)
        let lines = self.points.filter { point in
            if point.point == prev {
                return false
            } else {
                prev = point.point
                return true
            }
        }.enumerated().map({ (idx, point) in
            var v: (CGPoint, CGFloat)
            var duration: TimeInterval = 0
            if idx == 0 {
                let next = self.points[idx + 1]
                v = CGUtil.vectorLength(start: point.point, end: next.point)
                duration = next.time - point.time
            } else {
                let prev = self.points[idx - 1]
                if idx == self.points.count - 1 {
                    v = CGUtil.vectorLength(start: prev.point, end: point.point)
                    v.1 = 0
                    duration = 1000
                } else {
                    let next = self.points[idx + 1]
                    v = CGUtil.vector(start: prev.point, end: next.point, point: point.point)
                    duration = next.time - point.time
                }
            }
            return XCurvePoint(point: point.point, vector: v.0, length: v.1, duration: duration)
        })

        var curve = XCurve(start: lines[0].point)
        for i in 1 ..< lines.count {
            let from = lines[i - 1]
            let to = lines[i]
            u
            let control1 = CGUtil.pointAdd(point: from.point, vector: from.vector, length: from.length / 4)
            let control2 = CGUtil.pointSubtract(point: to.point, vector: to.vector, length: from.length / 4)
            curve.appenCurve(to: to.point, controlPoint1: control1, controlPoint2: control2)
        }
        return XPath(curve: curve)
    }
}

class XDrawingView : UIView {
    var paths: [XPath] = []
    var pathBuilder: XPathBuilder? = nil
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        self.pathBuilder = XPathBuilder()
        if let v = event?.touches(for: self)?.first {
            self.handleDraw(v)
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        if let v = event?.touches(for: self)?.first {
            self.handleDraw(v)
        }
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesCancelled(touches, with: event)
        self.finish()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        self.finish()
    }
    
    func finish() {
        if let pathBuilder = self.pathBuilder {
            if let path = pathBuilder.finish() {
                self.paths.append(path)
                self.setNeedsDisplay()
            }
            self.pathBuilder = nil
        }
    }
    
    func makePoint(_ touch: UITouch) -> XPoint {
        return XPoint(time: touch.timestamp, point: touch.preciseLocation(in: self))
    }
    func handleDraw(_ touch: UITouch) {
        let point = self.makePoint(touch)
        if let path = self.pathBuilder {
            path.append(point)
        }
    }
    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        context.setFillColor(UIColor.white.cgColor)
        context.fill([rect])
        self.paths.forEach { path in
            path.draw(context: context, rect: rect)
        }
    }
}

上一篇 下一篇

猜你喜欢

热点阅读