#animation #graph #swiftui #graphing
#Анимация #График #swiftui #построение графиков
Вопрос:
Ищу способы улучшить производительность рисования (надеюсь, без использования металла). Добавление LinearGradient в качестве заливки к фигуре резко снижает производительность рисования, поэтому я это не учел.
Вектор
struct LineGraphVector: VectorArithmetic {
var points: [CGPoint.AnimatableData]
static func (lhs: LineGraphVector, rhs: LineGraphVector) -> LineGraphVector {
return add(lhs: lhs, rhs: rhs, )
}
static func - (lhs: LineGraphVector, rhs: LineGraphVector) -> LineGraphVector {
return add(lhs: lhs, rhs: rhs, -)
}
static func add(lhs: LineGraphVector, rhs: LineGraphVector, _ sign: (CGFloat, CGFloat) -> CGFloat) -> LineGraphVector {
let maxPoints = max(lhs.points.count, rhs.points.count)
let leftIndices = lhs.points.indices
let rightIndices = rhs.points.indices
var newPoints: [CGPoint.AnimatableData] = []
(0 ..< maxPoints).forEach { index in
if leftIndices.contains(index) amp;amp; rightIndices.contains(index) {
// Merge points
let lhsPoint = lhs.points[index]
let rhsPoint = rhs.points[index]
newPoints.append(
.init(
sign(lhsPoint.first, rhsPoint.first),
sign(lhsPoint.second, rhsPoint.second)
)
)
} else if rightIndices.contains(index), let lastLeftPoint = lhs.points.last {
// Right side has more points, collapse to last left point
let rightPoint = rhs.points[index]
newPoints.append(
.init(
sign(lastLeftPoint.first, rightPoint.first),
sign(lastLeftPoint.second, rightPoint.second)
)
)
} else if leftIndices.contains(index), let lastPoint = newPoints.last {
// Left side has more points, collapse to last known point
let leftPoint = lhs.points[index]
newPoints.append(
.init(
sign(lastPoint.first, leftPoint.first),
sign(lastPoint.second, leftPoint.second)
)
)
}
}
return .init(points: newPoints)
}
mutating func scale(by rhs: Double) {
points.indices.forEach { index in
self.points[index].scale(by: rhs)
}
}
var magnitudeSquared: Double {
return 1.0
}
static var zero: LineGraphVector {
return .init(points: [])
}
}
Форма
struct LineGraphShape: Shape {
var points: [CGPoint]
let closePath: Bool
init(points: [CGPoint], closePath: Bool) {
self.points = points
self.closePath = closePath
}
var animatableData: LineGraphVector {
get { .init(points: points.map { CGPoint.AnimatableData($0.x, $0.y) }) }
set { points = newValue.points.map { CGPoint(x: $0.first, y: $0.second) } }
}
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: points.first ?? .zero)
path.addLines(points)
switch (closePath, points.first, points.last) {
case (true, .some(let firstPoint), .some(let lastPoint)):
path.addLine(to: .init(x: lastPoint.x, y: rect.height))
path.addLine(to: .init(x: 0.0, y: rect.height))
path.addLine(to: .init(x: 0.0, y: firstPoint.y))
path.closeSubpath()
default:
break
}
}
}
}
Использование
struct ContentView: View {
static let firstPoints: [CGPoint] = [
.init(x: 0.0, y: 0.0),
.init(x: 20.0, y: 320.0),
.init(x: 40.0, y: 50.0),
.init(x: 60.0, y: 10.0),
.init(x: 90.0, y: 140.0),
.init(x: 200.0, y: 60.0),
.init(x: 420.0, y: 20.0),
]
static let secondPoints: [CGPoint] = [
.init(x: 0.0, y: 0.0),
.init(x: 10.0, y: 200.0),
.init(x: 20.0, y: 50.0),
.init(x: 30.0, y: 70.0),
.init(x: 40.0, y: 90.0),
.init(x: 50.0, y: 150.0),
.init(x: 60.0, y: 120.0),
.init(x: 70.0, y: 20.0),
.init(x: 80.0, y: 30.0),
.init(x: 90.0, y: 20.0),
.init(x: 100.0, y: 0.0),
.init(x: 110.0, y: 200.0),
.init(x: 120.0, y: 50.0),
.init(x: 130.0, y: 70.0),
.init(x: 140.0, y: 90.0),
.init(x: 150.0, y: 150.0),
.init(x: 160.0, y: 120.0),
.init(x: 170.0, y: 20.0),
.init(x: 180.0, y: 30.0),
.init(x: 190.0, y: 20.0),
.init(x: 200.0, y: 0.0),
.init(x: 210.0, y: 200.0),
.init(x: 220.0, y: 50.0),
.init(x: 230.0, y: 70.0),
.init(x: 240.0, y: 90.0),
.init(x: 250.0, y: 150.0),
.init(x: 260.0, y: 120.0),
.init(x: 270.0, y: 20.0),
.init(x: 280.0, y: 30.0),
.init(x: 290.0, y: 20.0),
.init(x: 420.0, y: 20.0),
]
static let thirdPoints: [CGPoint] = [
.init(x: 0.0, y: 0.0),
.init(x: 20.0, y: 30.0),
.init(x: 40.0, y: 20.0),
.init(x: 60.0, y: 320.0),
.init(x: 80.0, y: 200.0),
.init(x: 100.0, y: 300.0),
.init(x: 120.0, y: 320.0),
.init(x: 140.0, y: 400.0),
.init(x: 160.0, y: 400.0),
.init(x: 180.0, y: 320),
.init(x: 200.0, y: 400.0),
.init(x: 420.0, y: 400.0),
]
let pointTypes = [0, 1, 2]
@State private var selectedPointType = 0
let points: [[CGPoint]] = [firstPoints, secondPoints, thirdPoints]
var body: some View {
VStack {
ZStack {
LineGraphShape(points: points[selectedPointType], closePath: true)
.fill(Color.blue.opacity(0.5))
LineGraphShape(points: points[selectedPointType], closePath: false)
.stroke(
Color.blue,
style: .init(
lineWidth: 4.0,
lineCap: .round,
lineJoin: .round,
miterLimit: 10.0
)
)
Text("(points[selectedPointType].count) Points")
.font(.largeTitle)
.animation(.none)
}
.animation(.easeInOut(duration: 2.0))
VStack {
Picker("", selection: $selectedPointType) {
ForEach(pointTypes, id: .self) { pointType in
Text("Graph (pointType)")
}
}
.pickerStyle(SegmentedPickerStyle())
}
.padding(.horizontal, 16.0)
.padding(.bottom, 32.0)
}
}
}
Ответ №1:
Почему вы хотите избежать металла? Включить его поддержку так же просто, как обернуть ваш LineGraphShape
s Group
и изменить его с помощью a drawingGroup()
. Попробуйте:
...
Group {
let gradient = LinearGradient(
gradient: Gradient(colors: [Color.red, Color.blue]),
startPoint: .leading,
endPoint: .trailing
)
LineGraphShape(points: points[selectedPointType], closePath: true)
.fill(gradient)
LineGraphShape(points: points[selectedPointType], closePath: false)
.stroke(
gradient,
style: .init(
lineWidth: 4.0,
lineCap: .round,
lineJoin: .round,
miterLimit: 10.0
)
)
}
.drawingGroup()
...
Приводит к значительному повышению производительности 🙂
Комментарии:
1. Фантастика! Я и не думал, что Metal можно так легко реализовать! Спасибо!