SwiftUI:使用 CGAffineTransform 和奇偶填充来变换形状

2020-05-08 00:58:32 浏览数 (1)

当您不再满足于简单的形状和路径时,SwiftUI的两个有用功能会合在一起,以极少的工作量创建出漂亮的效果。第一个是CGAffineTransform,它描述了如何旋转,缩放或剪切路径或视图。第二个是奇偶填充(even-odd fills),它使我们可以控制应如何渲染重叠的形状。

为了演示这两种方法,我们将用几个旋转的椭圆形花瓣创建一个花朵形状,每个椭圆形都围绕一个圆放置。这背后的数学方法相对简单,只有一个需要注意点:CGAffineTransform以弧度而非角度来度量角度。如果您上学已经有一段时间了,那么您至少需要知道的是:3.141弧度等于180度,所以3.141弧度乘以2等于360度。3.141并非巧合:实际值是数学常数 π

因此,我们要做的事情:

  • 创建一个新的空路径。
  • 从0到π乘以2(弧度为360度),然后每次计数为π的八分之一,这将为我们提供16个花瓣。
  • 创建一个等于当前数字的旋转变换。
  • 旋转变换的移动量等于绘制空间宽度和高度的一半,因此每个花瓣都以我们的形状为中心。
  • 为花瓣创建一个新路径,该路径等于特定大小的椭圆。
  • 将变换应用到该椭圆,以便将其移到适当位置。
  • 将花瓣的路径添加到我们的主路径中。

一旦您看到代码正在运行,这将更有意义,但是首先我想再添加三个小东西:

  1. 旋转然后移动的东西不会产生与移动然后旋转的结果相同的结果,因为先旋转时,它的移动方向将与未旋转时的不同。
  2. 为了真正帮助您了解发生了什么,我们将使花瓣椭圆使用一些可以从外部传递的属性。
  3. 如果您想一次通过数字计数,则范围为1 ... 5很好,但是如果您想以2s进行计数,或者在我们的情况下以“ pi / 8”为单位,则应使用stride(from:to:by :)代替。

好了,足够多的讨论,现在将此形状添加到您的项目中:

代码语言:swift复制
struct Flower: Shape {
    // 花瓣移离中心多少距离
    var petalOffset: Double = -20

    // 每片花瓣的宽度
    var petalWidth: Double = 100

    func path(in rect: CGRect) -> Path {
        // 容纳所有花瓣的路径
        var path = Path()

        // 从0向上计数到 pi * 2,每次移动 pi / 8
        for number in stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 8) {
            // 根据循环旋转当前的花瓣
            let rotation = CGAffineTransform(rotationAngle: number)

            // 将花瓣移到我们视野的中心
            let position = rotation.concatenating(CGAffineTransform(translationX: rect.width / 2, y: rect.height / 2))

            // 使用我们的属性以及固定的Y和高度为该花瓣创建路径
            let originalPetal = Path(ellipseIn: CGRect(x: CGFloat(petalOffset), y: 0, width: CGFloat(petalWidth), height: rect.width / 2))

            // 将我们的旋转/位置变换应用于花瓣
            let rotatedPetal = originalPetal.applying(position)

            // 将其添加到我们的主路径
            path.addPath(rotatedPetal)
        }

        // 现在将主径 return
        return path
    }
}

我意识到有很多代码,但是希望当您试用时,它会变得更加清晰。将您的ContentView修改为下方代码:

代码语言:swift复制
struct ContentView: View {
    @State private var petalOffset = -20.0
    @State private var petalWidth = 100.0

    var body: some View {
        VStack {
            Flower(petalOffset: petalOffset, petalWidth: petalWidth)
                .stroke(Color.red, lineWidth: 1)

            Text("Offset")
            Slider(value: $petalOffset, in: -40...40)
                .padding([.horizontal, .bottom])

            Text("Width")
            Slider(value: $petalWidth, in: 0...100)
                .padding(.horizontal)
        }
    }
}

现在尝试一下。一旦开始拖动offset和width滑块,您应该就能清楚地看到代码的工作原理——它只是一系列旋转的椭圆,呈圆形排列。

stroke flowerstroke flower

这本身就是有趣的,但是只要稍作改动,我们就可以从有趣升华。如果您查看绘制椭圆的方式,它们经常重叠——有时一个椭圆绘制在另一个椭圆上,有时绘制在其他多个椭圆上。

如果我们使用纯色填充路径,则会得到相当不令人印象深刻的结果。像这样尝试:

代码语言:swift复制
Flower(petalOffset: petalOffset, petalWidth: petalWidth)
    .fill(Color.red)
fill flowerfill flower

但是,作为一种替代方法,我们可以使用奇偶规则填充形状,该规则决定路径的一部分是否应根据其包含的重叠进行着色。它是这样的:

  • 如果路径没有重叠,它将被填充。
  • 如果另一条路径重叠,则重叠的部分将不会被填充。
  • 如果第三个路径与前两个路径重叠,则会被填充。
  • …等等。

仅实际重叠的部分受此规则影响,并且会产生一些非常漂亮的结果。更好的是,Swift UI使其使用起来很简单,因为每当我们在形状上调用fill()时,我们都可以传递一个FillStyle结构体,该结构要求启用奇偶规则。

尝试一下:

代码语言:swift复制
Flower(petalOffset: petalOffset, petalWidth: petalWidth)
    .fill(Color.red, style: FillStyle(eoFill: true))
FillStyle(eoFill: true)FillStyle(eoFill: true)

现在运行程序并开始播放——坦白地说,鉴于我们所做的工作很少,结果非常吸引人!

译自Transforming shapes using CGAffineTransform and even-odd fills

0 人点赞