Как анимировать / заполнить UIBezierPath в заданном порядке?

#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 } }