#swift #animation #swiftui
#swift #Анимация #swiftui
Вопрос:
Из файла у меня есть куча 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()
}
}
}
Комментарии:
1. Не могли бы вы опубликовать ссылку на свой минимальный проект SwiftUI? Потому что, даже если кто-то знает, как это сделать с помощью UIKit, трудно угадать ограничения, которые SwiftUI накладывает на эту проблему
2. На самом деле это просто отображение двух фигур. Я добавил код для основного представления.
Ответ №1:
Вы можете определить animatableData
свойство в своей форме, чтобы SwiftUI мог интерполировать между состояниями (например, заполненным и незаполненным). Где вы начинаете рисовать каждый штрих, будет иметь значение для направления. Вы также можете использовать .trim в пути, чтобы усечь его, если хотите, и связать это значение с animatableData .
Для целого символа / кандзи вам может потребоваться создать мета-фигуру или представление нескольких вложенных фигур с определенными позициями, но в конечном итоге это займет у вас меньше работы, потому что вы можете создать библиотеку штрихов, которые легко комбинировать, нет?
В приведенном ниже примере я перемещаю луну из полной в полумесяц, изменяя percentFullMoon
свойство из других представлений. Это свойство используется функцией рисования пути для настройки некоторых дуг. SwiftUI считывает свойство animatableData, чтобы выяснить, как нарисовать столько кадров, сколько он выберет для интерполяции во время анимации.
Это довольно просто, даже если этот API имеет неясное имя.
Имеет ли это смысл? Примечание: приведенный ниже код использует некоторые вспомогательные функции, такие как clamp, north / south и center / radius, но не имеет отношения к концепции.
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(amp;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(amp;path, rect, angle: crescent)
return path
case 90.nextUp...:
let gibbous = Angle(degrees: .clamp(0, 90.nextDown, 180 - cycle))
addInteriorArc(amp;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)
}
}
Комментарии:
1. Да, это было бы полезно, если бы вы могли добавить
north()
,south()
иclamp()
. Кроме того, XCode сообщает мне, чтоCGRect has no member 'center'
и'radius'
2. Я опубликовал вспомогательные функции.
3. Попытка создать одну фигуру, вероятно, не ваш лучший выбор, скорее всего, вы, вероятно, в конечном итоге создадите представление фигур, где анимация задерживается на каждой отдельной фигуре.
4. @kirkyoyx Есть вопросы?
5. Спасибо. Я не уверен, что я делаю неправильно, но луна не анимируется, когда я использую этот код
@State var percent: Double = 0.1 var body: some View { WaningMoon(percentFullMoon: percent) .onTapGesture { percent = 1.0 } }