【问题标题】:How to make vector drawing functions more concise in Swift如何在 Swift 中使矢量绘图功能更简洁
【发布时间】:2020-02-05 22:37:10
【问题描述】:

我制作了一个按钮来匹配我的应用程序其余部分的样式,但是它太笨重了。我尝试了一些东西,但这是我所能得到的一样小。

class ButtonPath {
    private let wiggle: Int = 3
    func points(height: Int) ->
        ((Int,Int),(Int,Int),(Int,Int),
         (Int,Int),(Int,Int),(Int,Int),
         (Int,Int),(Int,Int),(Int,Int),
         (Int,Int),(Int,Int),(Int,Int))
    {
        func rand() -> Int { Int.random(in: -wiggle...wiggle) }
        func r2(x: Int) -> Int { Int.random(in: -x...x) }
        let screen: CGRect = UIScreen.main.bounds
        let widthMx = CGFloat(0.9)
        let origin = (x:15, y:15)
        let width = Int(screen.width * widthMx)

        // Corner points
        let tl   = (x: origin.x + rand(),   y: origin.x + rand()) // tl = Top Left, etc.
        let tr   = (x: origin.x + width + rand(),      y: origin.y + rand())
        let bl   = (x: origin.x + rand(),   y: origin.y + height + rand())
        let br   = (x: origin.x + width + rand(),      y: origin.y + height + rand())

        // Arc controls, we're drawing a rectangle counter-clockwise from the top left
        let a1c1 = (x: origin.x + rand(),   y: Int(Double(origin.y+height+rand()) * 0.3)) // a1c1 = Arc 1 Control 1
        let a1c2 = (x: origin.x + rand(),   y: Int(Double(origin.y+height+rand()) * 0.6))
        let a2c1 = (x: Int(Double(origin.x+width+rand()) * 0.3), y: origin.y + height + rand())
        let a2c2 = (x: Int(Double(origin.x+width+rand()) * 0.6), y: origin.y + height + rand())
        let a3c1 = (x: origin.x + width + rand(),                y: Int(Double(origin.y + height+rand()) * 0.6))
        let a3c2 = (x: origin.x + width + rand(),                y: Int(Double(origin.y + height+rand()) * 0.3))
        let a4c1 = (x: Int(Double(origin.x+width+rand()) * 0.6), y: origin.y + rand())
        let a4c2 = (x: Int(Double(origin.x+width+rand()) * 0.6), y: origin.y + rand())

        return (
            t1: tl, tr: tr, b1: bl, br: br,
            a1c1: a1c1, a1c2: a1c2, a2c1: a2c1,
            a2c2:a2c2, a3c1:a3c1, a3c2:a3c2, a4c1:a4c1, a4c2:a4c2
        )
    }
    func path (height:Int) -> Path {
        let (tl, tr, bl, br, a1c1, a1c2, a2c1, a2c2, a3c1, a3c2, a4c1, a4c2) = points(height: height)
        return Path { path in
            path.move( to: CGPoint(x: tl.0, y: tl.1) )
            path.addCurve( to: CGPoint(x: bl.0, y: bl.1), control1: CGPoint(x: a1c1.0, y: a1c1.1), control2: CGPoint(x: a1c2.0, y: a1c2.1))
            path.addCurve( to: CGPoint(x: br.0, y: br.1), control1: CGPoint(x: a2c1.0, y: a2c1.1), control2: CGPoint(x: a2c2.0, y: a2c2.1))
            path.addCurve( to: CGPoint(x: tr.0, y: tr.1), control1: CGPoint(x: a3c1.0, y: a3c1.1), control2: CGPoint(x: a3c2.0, y: a3c2.1))
            path.addCurve( to: CGPoint(x: tl.0-2, y: tl.1), control1: CGPoint(x: a4c1.0, y: a4c1.1), control2: CGPoint(x: a4c2.0, y: a4c2.1))
        }
    }
}

我可以编写一个函数来输出角、弧、路径等的值吗?

【问题讨论】:

  • 那是相当简洁的代码。有时最好不要将它尽可能缩短,因为那样它又会回到不可读的状态。
  • "Bulky" 很好,只要它的可读性和可维护性。如果你认为你可以在一年内回顾这件事并了解发生了什么,那么这是一个很好的结果。就个人而言,我建议人们使用结构来制作形状,其函数计算属性为您提供形状的点。请参阅此示例:gist.github.com/amomchilov/1ac8e63001bd703cd4e460ba57b4df2f

标签: swift vector-graphics swift5


【解决方案1】:

您可以通过多种方式简化代码:

  • 只需在任何地方使用CGFloat,而不是在IntDouble 之间不断转换。
  • 添加一些fileprivate 运算符用于在CGPoint 上进行数学运算。如果您在其他文件中需要它们,也可以在项目范围内添加它们。
  • 分解出一个摆动点的方法。
  • 分解出一种从当前点到另一点绘制摆动线的方法。

您显然使用的是Path 类型。所以我推断你正在使用 SwiftUI。

SwiftUI 有一个您可能想要使用的Shape 协议。您可以通过定义方法path(in frame: CGRect) -> Path 来遵守Shape 协议。由于 SwiftUI 为您提供了 Shape 的框架,因此您不必使用 GeometryReader 来获取形状的大小。

此外,由于 SwiftUI 可以随时向View(包括ShapePath)询问其body,并且可以根据需要多次询问,因此使代码具有确定性很重要。这意味着不使用SystemRandomNumberGenerator。所以让我们从定义一个确定性的RandomNumberGenerator开始:

struct Xorshift128Plus: RandomNumberGenerator {
    var seed: (UInt64, UInt64)

    mutating func next() -> UInt64 {
        // Based on https://stackoverflow.com/a/40385556/77567
        // The state must be seeded so that it is not everywhere zero.
        if seed == (0, 0) { seed = (0, 1) }
        var x = seed.0
        x ^= x << 23
        x ^= x >> 17
        x ^= seed.1
        x ^= seed.1 >> 26
        seed = (seed.1, x)
        return seed.0 &+ seed.1
    }
}

我们还需要CGPoint 上的这些算术运算符:

fileprivate func *(s: CGFloat, p: CGPoint) -> CGPoint { .init(x: s * p.x, y: s * p.y) }
fileprivate func +(p: CGPoint, q: CGPoint) -> CGPoint { .init(x: p.x + q.x, y: p.y + q.y) }
fileprivate func -(p: CGPoint, q: CGPoint) -> CGPoint { .init(x: p.x - q.x, y: p.y - q.y) }

现在我们准备实现一个摆动点的方法:

extension CGPoint {
    fileprivate func wiggled<RNG: RandomNumberGenerator>(by wiggle: CGFloat, using rng: inout RNG) -> CGPoint {
        return .init(
            x: x + CGFloat.random(in: -wiggle...wiggle, using: &rng),
            y: y + CGFloat.random(in: -wiggle...wiggle, using: &rng))
    }
}

请注意,我们对RandomNumberGenerator 类型是通用的,因为我们采用inout。我们可以硬编码Xorshift128PlusRandomNumberGenerator 的任何其他具体实现,而不是通用的。但为什么要限制自己呢?

有了点摆动器,我们可以在Path 上实现一个addWigglyLine 方法:

extension Path {
    fileprivate mutating func addWigglyLine<RNG: RandomNumberGenerator>(
        to p3: CGPoint, wiggle: CGFloat, rng: inout RNG)
    {
        let p0 = currentPoint ?? .zero
        let v = p3 - p0
        let p1 = (p0 + (1/3) * v).wiggled(by: wiggle, using: &rng)
        let p2 = (p0 + (2/3) * v).wiggled(by: wiggle, using: &rng)
        addCurve(to: p3, control1: p1, control2: p2)
    }
}

最后我们可以实现WigglyRect,一个自定义的Shape。同样,我们需要对RandomNumberGenerator 进行通用化。请注意,由于我们需要提及两次起点/终点,因此我们将其存储在 p0 中,以便我们可以整齐地关闭路径。

struct WigglyRect<RNG: RandomNumberGenerator>: Shape {
    var rng: RNG
    var wiggle: CGFloat = 8

    func path(in frame: CGRect) -> Path {
        var rng = self.rng
        func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
            return CGPoint(x: x, y: y).wiggled(by: wiggle, using: &rng)
        }

        let rect = frame.insetBy(dx: wiggle, dy: wiggle)
        let (x0, x1, y0, y1) = (rect.minX, rect.maxX, rect.minY, rect.maxY)
        let p0 = p(x0, y0)
        var path = Path()
        path.move(to: p0)
        path.addWigglyLine(to: p(x1, y0), wiggle: wiggle, rng: &rng)
        path.addWigglyLine(to: p(x1, y1), wiggle: wiggle, rng: &rng)
        path.addWigglyLine(to: p(x0, y1), wiggle: wiggle, rng: &rng)
        path.addWigglyLine(to: p0, wiggle: wiggle, rng: &rng)
        path.closeSubpath()
        return path
    }
}

我在 macOS 上进行了测试。为了使用您的代码,我将您对 UIScreen.main.bounds 的使用更改为 CGRect(x: 0, y: 0, width: 300, height: 600)。这是预览代码:

struct ContentView: View {
    @State var seed: (UInt64, UInt64) = makeSeed()

    var body: some View {
        VStack(spacing: 20) {
            ButtonPath().path(height: 100)
                .stroke()
                .frame(height: 100)
                .background(Color.pink.opacity(0.2))
            WigglyRect(rng: Xorshift128Plus(seed: seed))
                .stroke()
                .frame(height: 100)
                .background(Color.pink.opacity(0.2))
            Spacer()
            Button("Reseed") {
                self.seed = Self.makeSeed()
            }
        } //
            .padding()
    }

    private static func makeSeed() -> (UInt64, UInt64) {
        (UInt64.random(in: .min ... .max), UInt64.random(in: .min ... .max))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(PreviewLayout.fixed(width: 300, height: 600))
    }
}

它是这样的:

您的代码绘制顶部形状。此答案中的代码绘制底部形状。我在每条路径后面放了粉红色的背景,显示了路径的框架。请注意,您的代码不会使路径符合其边界。

【讨论】:

  • 感谢您抽出宝贵时间撰写如此周到的答案,Rob。这正是我所希望的,甚至更多。极好的!谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-04
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 2013-08-21
  • 1970-01-01
相关资源
最近更新 更多