首页 > 解决方案 > 如何以给定的顺序动画/填充 UIBezierPath?

问题描述

从一个文件中,我有一堆我转换为 UIBezierPath 的 SVG 路径。为了使我的示例简单,我手动创建了路径:

struct myShape: Shape {
    func path(in rect: CGRect) -> Path {
        let p = UIBezierPath()

        p.move(to: CGPoint(x: 147, y: 32))
        p.addQuadCurve(to: CGPoint(x: 203, y: 102), controlPoint: CGPoint(x: 181, y: 74))
        p.addQuadCurve(to: CGPoint(x: 271, y: 189), controlPoint: CGPoint(x: 242, y: 166))
        p.addQuadCurve(to: CGPoint(x: 274, y: 217), controlPoint: CGPoint(x: 287, y: 204))
        p.addQuadCurve(to: CGPoint(x: 229, y: 235), controlPoint: CGPoint(x: 258, y: 229))
        p.addQuadCurve(to: CGPoint(x: 193, y: 235), controlPoint: CGPoint(x: 204, y: 241))
        p.addQuadCurve(to: CGPoint(x: 190, y: 219), controlPoint: CGPoint(x: 183, y: 231))
        p.addQuadCurve(to: CGPoint(x: 143, y: 71), controlPoint: CGPoint(x: 199, y: 195))
        p.addQuadCurve(to: CGPoint(x: 125, y: 33), controlPoint: CGPoint(x: 134, y: 55))
        p.addCurve(to: CGPoint(x: 147, y: 32), controlPoint1: CGPoint(x: 113, y: 5), controlPoint2: CGPoint(x: 128, y: 9))
        p.close()

        return Path(p.cgPath)
    }
}

结果如下图:

在此处输入图像描述

对于这个数字,我有一个单独的路径/形状,代表数字的“中位数”和这个数字应该被填充的顺序。路径只是行的串联。

 struct myMedian: Shape {
    func path(in rect: CGRect) -> Path {
        let p = UIBezierPath()

        p.move(to: CGPoint(x: 196, y: 226))
        p.addLine(to: CGPoint(x: 209, y: 220))
        p.addLine(to: CGPoint(x: 226, y: 195))
        p.addLine(to: CGPoint(x: 170, y: 86))
        p.addLine(to: CGPoint(x: 142, y: 43))
        p.addLine(to: CGPoint(x: 131, y: 39))

        return Path(p.cgPath)
    }
}

为了可视化线条的顺序,我添加了红色箭头:

在此处输入图像描述

现在我需要按照与“中位笔画”相同的顺序填写“大图”。我知道如何一步填充整个图形,但不是拆分,尤其是我不知道如何管理动画的方向。

最终结果应如下所示:

在此处输入图像描述

由于我使用的是 SwiftUI,它应该与它兼容。

主要观点是:

struct DrawCharacter: View {
    var body: some View {
        ZStack(alignment: .topLeading){
            myShape()
            myMedian()
            }
        }
    }

标签: swiftanimationswiftui

解决方案


您可以在 Shape 中定义一个animatableData属性,以使 SwiftUI 能够在状态之间进行插值(例如,填充和未填充)。从哪里开始绘制每个笔画对于方向很重要。如果您愿意,也可以在路径上使用 .trim 来截断它,并将该值绑定到 animatableData。

对于整个字符/汉字,您可能需要组合具有定义位置的多个子形状的元形状或视图,但从长远来看,这实际上对您来说工作量较小,因为您可以创建一个易于重新组合的笔画库, 不?

percentFullMoon在下面的示例中,我通过从其他视图更改属性来将月亮从满月移动到新月。路径绘制函数使用该属性来设置一些弧。SwiftUI 读取 animatableData 属性来确定如何绘制它选择在动画时插入多少帧。

这很简单,即使这个 API 的名字很隐晦。

那有意义吗?注意:下面的代码使用了一些辅助函数,如钳位、北/南和中心/半径,但与概念无关。

import SwiftUI

/// Percent full moon is from 0 to 1
struct WaningMoon: Shape {
    /// From 0 to 1
    var percentFullMoon: Double

    var animatableData: Double {
        get { percentFullMoon }
        set { self.percentFullMoon = newValue }
    }

    func path(in rect: CGRect) -> Path {
        var path = Path()
        addExteriorArc(&path, rect)

        let cycle = percentFullMoon * 180
        switch cycle {
            case 90:
                return path

            case ..<90:
                let crescent = Angle(degrees: 90 + .clamp(0.nextUp, 90, 90 - cycle))
                addInteriorArc(&path, rect, angle: crescent)
                return path

            case 90.nextUp...:
                let gibbous = Angle(degrees: .clamp(0, 90.nextDown, 180 - cycle))
                addInteriorArc(&path, rect, angle: gibbous)
                return path

            default: return path

        }
    }

    private func addInteriorArc(_ path: inout Path, _ rect: CGRect, angle: Angle) {
        let xOffset = rect.radius * angle.tan()
        let offsetCenter = CGPoint(x: rect.midX - xOffset, y: rect.midY)

        return path.addArc(
            center: offsetCenter,
            radius: rect.radius / angle.cos(),
            startAngle: .south() - angle,
            endAngle: .north() + angle,
            clockwise: angle.degrees < 90) // False == Crescent, True == Gibbous
    }

    private func addExteriorArc(_ path: inout Path, _ rect: CGRect) {
        path.addArc(center: rect.center,
                    radius: rect.radius,
                    startAngle: .north(),
                    endAngle: .south(),
                    clockwise: true)
    }
}

帮手


extension Comparable {
    static func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N {
        Swift.max(min, Swift.min(variable, max))
    }

    func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N {
        Swift.max(min, Swift.min(variable, max))
    }
}

extension Angle {
    func sin() -> CGFloat {
        CoreGraphics.sin(CGFloat(self.radians))
    }

    func cos() -> CGFloat {
        CoreGraphics.cos(CGFloat(self.radians))
    }

    func tan() -> CGFloat {
        CoreGraphics.tan(CGFloat(self.radians))
    }

    static func north() -> Angle {
        Angle(degrees: -90)
    }

    static func south() -> Angle {
        Angle(degrees: 90)
    }
}



extension CGRect {
    var center: CGPoint {
        CGPoint(x: midX, y: midY)
    }

    var radius: CGFloat {
        min(width, height) / 2
    }

    var diameter: CGFloat {
        min(width, height)
    }

    var N: CGPoint { CGPoint(x: midX, y: minY) }
    var E: CGPoint { CGPoint(x: minX, y: midY) }
    var W: CGPoint { CGPoint(x: maxX, y: midY) }
    var S: CGPoint { CGPoint(x: midX, y: maxY) }

    var NE: CGPoint { CGPoint(x: maxX, y: minY) }
    var NW: CGPoint { CGPoint(x: minX, y: minY) }
    var SE: CGPoint { CGPoint(x: maxX, y: maxY) }
    var SW: CGPoint { CGPoint(x: minX, y: maxY) }


    func insetN(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: midX,                               y: minY + height / denominator)
    }

    func insetE(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: minX - width / denominator,         y: midY)
    }

    func insetW(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: maxX + width / denominator,         y: midY)
    }

    func insetS(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: midX,                               y: maxY - height / denominator)
    }

    func insetNE(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: maxX + width / denominator,         y: minY + height / denominator)
    }

    func insetNW(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: minX - width / denominator,         y: minY + height / denominator)
    }

    func insetSE(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: maxX + width / denominator,         y: maxY - height / denominator)
    }

    func insetSW(_ denominator: CGFloat) -> CGPoint {
        CGPoint(x: minX - width / denominator,         y: maxY - height / denominator)
    }
}


推荐阅读