如何以给定的顺序动画/填充 UIBezierPath?

Posted

技术标签:

【中文标题】如何以给定的顺序动画/填充 UIBezierPath?【英文标题】:How to animate/fill an UIBezierPath in a given order? 【发布时间】:2021-01-09 09:13:39 【问题描述】:

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

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()
            
        
    

【问题讨论】:

您能发布一个指向您的 SwiftUI 最小项目的链接吗?因为即使有人知道如何使用 UIKit,也很难猜测 SwiftUI 对这个问题的限制 其实它只是显示两个形状。我已经添加了主视图的代码。 【参考方案1】:

您可以在 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)
    


【讨论】:

是的,如果您可以添加 north()south()clamp(),那将会很有帮助。另外XCode告诉我CGRect has no member 'center''radius' 我已经发布了辅助函数。 尝试制作单个形状可能不是您的最佳选择,但您可能最终会组合一个形状视图,其中动画在每个单独的形状上都有延迟。 @kirkyoyx 有什么问题吗? 谢谢。我不确定我做错了什么,但是当我使用此代码时月亮没有动画@State var percent: Double = 0.1 var body: some View WaningMoon(percentFullMoon: percent) .onTapGesture percent = 1.0

以上是关于如何以给定的顺序动画/填充 UIBezierPath?的主要内容,如果未能解决你的问题,请参考以下文章

给定路径的 Svg 填充动画

使用 Canvas 为填充圆设置动画

如何为 SVG 多边形设置动画以进行填充?

Android:如何在给定网址的情况下显示大型动画 gif?

如何将canvas轨迹保存为动画

CSS动画速记属性语法顺序