Swift如何让矢量图功能更简洁
How to make vector drawing functions more concise in Swift
我制作了一个按钮来匹配我的应用程序其余部分的样式,但是它太笨重了。我尝试了一些东西,但这是我所能得到的最小的东西。
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))
}
}
}
有没有我可以编写的函数来输出角、弧、路径等的值?
您可以通过多种方式简化代码:
- 到处使用
CGFloat
而不是不断地在Int
和Double
之间转换。
- 添加一些
fileprivate
运算符以在 CGPoint
上进行数学计算。如果您在其他文件中需要它们,或者在项目范围内添加它们。
- 分解出一个摆动点的方法。
- 分解出从当前点到另一点绘制波浪线的方法。
您显然使用的是 Path
类型。所以我推断你正在使用 SwiftUI。
SwiftUI 有一个您可能想要使用的 Shape
协议。您通过定义方法 path(in frame: CGRect) -> Path
来遵守 Shape
协议。由于 SwiftUI 为您提供了 Shape
的框架,因此您不必使用 GeometryReader
来获取形状的大小。
此外,由于 SwiftUI 可以随时向 View
(包括 Shape
或 Path
)请求其 body
,而且次数与它的次数一样多想要,使代码具有确定性很重要。这意味着不使用 SystemRandomNumberGenerator
。所以让我们从定义一个确定性的 RandomNumberGenerator
:
开始
struct Xorshift128Plus: RandomNumberGenerator {
var seed: (UInt64, UInt64)
mutating func next() -> UInt64 {
// Based on
// 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
。我们可以硬编码 Xorshift128Plus
或 RandomNumberGenerator
的任何其他具体实现,而不是通用的。但为什么要限制自己?
有了点摆动器,我们可以在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
上通用。请注意,由于我们需要两次提及 start/end 点,因此我们将其摆动存储在 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))
}
}
这是它的样子:
您的代码绘制了顶部形状。此答案中的代码绘制底部形状。我在每条路径后面放了粉红色的背景,以显示路径的框架。请注意,您的代码不会使路径符合其边界。
我制作了一个按钮来匹配我的应用程序其余部分的样式,但是它太笨重了。我尝试了一些东西,但这是我所能得到的最小的东西。
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))
}
}
}
有没有我可以编写的函数来输出角、弧、路径等的值?
您可以通过多种方式简化代码:
- 到处使用
CGFloat
而不是不断地在Int
和Double
之间转换。 - 添加一些
fileprivate
运算符以在CGPoint
上进行数学计算。如果您在其他文件中需要它们,或者在项目范围内添加它们。 - 分解出一个摆动点的方法。
- 分解出从当前点到另一点绘制波浪线的方法。
您显然使用的是 Path
类型。所以我推断你正在使用 SwiftUI。
SwiftUI 有一个您可能想要使用的 Shape
协议。您通过定义方法 path(in frame: CGRect) -> Path
来遵守 Shape
协议。由于 SwiftUI 为您提供了 Shape
的框架,因此您不必使用 GeometryReader
来获取形状的大小。
此外,由于 SwiftUI 可以随时向 View
(包括 Shape
或 Path
)请求其 body
,而且次数与它的次数一样多想要,使代码具有确定性很重要。这意味着不使用 SystemRandomNumberGenerator
。所以让我们从定义一个确定性的 RandomNumberGenerator
:
struct Xorshift128Plus: RandomNumberGenerator {
var seed: (UInt64, UInt64)
mutating func next() -> UInt64 {
// Based on
// 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
。我们可以硬编码 Xorshift128Plus
或 RandomNumberGenerator
的任何其他具体实现,而不是通用的。但为什么要限制自己?
有了点摆动器,我们可以在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
上通用。请注意,由于我们需要两次提及 start/end 点,因此我们将其摆动存储在 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))
}
}
这是它的样子:
您的代码绘制了顶部形状。此答案中的代码绘制底部形状。我在每条路径后面放了粉红色的背景,以显示路径的框架。请注意,您的代码不会使路径符合其边界。