Как переместить вид / фигуру по пользовательскому пути с помощью swiftUI? - PullRequest
0 голосов
/ 04 марта 2020

Кажется, что нет интуитивно понятного способа перемещения вида / фигуры по пользовательскому пути, особенно по извилистому пути. Я нашел несколько библиотек для UIKit, которые позволяют видам перемещаться по путям Безье (DKChainableAnimationKit, TweenKit, Sica и т. Д. c.), Но мне не очень удобно использовать UIKit, и я продолжал сталкиваться с ошибками.

в настоящее время с помощью swiftUI я вручную делаю это так:

import SwiftUI
struct ContentView: View {
    @State var moveX = true
    @State var moveY = true
    @State var moveX2 = true
    @State var moveY2 = true
    @State var rotate1 = true
    var body: some View {
        ZStack{
            Circle().frame(width:50, height:50)
                .offset(x: moveX ? 0:100, y: moveY ? 0:100)
                .animation(Animation.easeInOut(duration:1).delay(0))
                .rotationEffect(.degrees(rotate1 ? 0:350))
                .offset(x: moveX2 ? 0:-100, y: moveY2 ? 0:-200)
                .animation(Animation.easeInOut(duration:1).delay(1))

                .onAppear(){
                    self.moveX.toggle();
                    self.moveY.toggle();
                    self.moveX2.toggle();
                    self.moveY2.toggle();
                    self.rotate1.toggle();
                    //    self..toggle()
            }
        }
} }

Это несколько выполняет работу, но гибкость сильно ограничена, и задержки при составлении быстро превращаются в беспорядок.

Если кто-нибудь знает, как я могу получить собственный вид / форму для перемещения по следующему пути, это будет очень очень цениться.

  Path { path in
        path.move(to: CGPoint(x: 200, y: 100))
        path.addQuadCurve(to: CGPoint(x: 230, y: 200), control: CGPoint(x: -100, y: 300))
        path.addQuadCurve(to: CGPoint(x: 90, y: 400), control: CGPoint(x: 400, y: 130))
        path.addLine(to: CGPoint(x: 90, y: 600))
    }
    .stroke()

Самое близкое решение, которое мне удалось найти, было на SwiftUILab, но полное руководство, похоже, доступно только для платных подписчиков.

Примерно так:
enter image description here

Ответы [ 2 ]

3 голосов
/ 11 марта 2020

ОК, это не просто, но я хотел бы помочь ...

В следующем фрагменте (приложение macOS) вы можете увидеть основные элементы c, которые вы можете адаптировать к вашим потребностям.

Для простоты я выбираю простую параметрическую кривую c. Если вы хотите использовать более сложную (составную) кривую, вам нужно решить, как отобразить частичный t (параметр) для каждого сегмента в составной t для вся кривая (то же самое должно быть сделано для отображения между частичным расстоянием вдоль пути и составным путем вдоль пути пути).

Почему такое осложнение?

Существует нелинейная зависимость между расстоянием вдоль траектории, требуемым для перемещения воздушного судна (с постоянной скоростью), и параметром кривой t, от которого зависит параметр c определения кривой .

Давайте сначала посмотрим на результат

enter image description here

, а затем посмотрим, как он реализован. Вам нужно изучить этот код и, если необходимо, изучить, как определяются и ведут себя параметры c.

//
//  ContentView.swift
//  tmp086
//
//  Created by Ivo Vacek on 11/03/2020.
//  Copyright © 2020 Ivo Vacek. All rights reserved.
//

import SwiftUI
import Accelerate

protocol ParametricCurve {
    var totalArcLength: CGFloat { get }
    func point(t: CGFloat)->CGPoint
    func derivate(t: CGFloat)->CGVector
    func secondDerivate(t: CGFloat)->CGVector
    func arcLength(t: CGFloat)->CGFloat
    func curvature(t: CGFloat)->CGFloat
}

extension ParametricCurve {
    func arcLength(t: CGFloat)->CGFloat {
        var tmin: CGFloat = .zero
        var tmax: CGFloat = .zero
        if t < .zero {
            tmin = t
        } else {
            tmax = t
        }
        let quadrature = Quadrature(integrator: .qags(maxIntervals: 8), absoluteTolerance: 5.0e-2, relativeTolerance: 1.0e-3)
        let result = quadrature.integrate(over: Double(tmin) ... Double(tmax)) { _t in
            let dp = derivate(t: CGFloat(_t))
            let ds = Double(hypot(dp.dx, dp.dy)) //* x
            return ds
        }
        switch result {
        case .success(let arcLength, _/*, let e*/):
            //print(arcLength, e)
            return t < .zero ? -CGFloat(arcLength) : CGFloat(arcLength)
        case .failure(let error):
            print("integration error:", error.errorDescription)
            return CGFloat.nan
        }
    }
    func curveParameter(arcLength: CGFloat)->CGFloat {
        let maxLength = totalArcLength == .zero ? self.arcLength(t: 1) : totalArcLength
        guard maxLength > 0 else { return 0 }
        var iteration = 0
        var guess: CGFloat = arcLength / maxLength

        let maxIterations = 10
        let maxErr: CGFloat = 0.1

        while (iteration < maxIterations) {
            let err = self.arcLength(t: guess) - arcLength
            if abs(err) < maxErr { break }
            let dp = derivate(t: guess)
            let m = hypot(dp.dx, dp.dy)
            guess -= err / m
            iteration += 1
        }

        return guess
    }
    func curvature(t: CGFloat)->CGFloat {
        /*
                    x'y" - y'x"
        κ(t)  = --------------------
                 (x'² + y'²)^(3/2)
         */
        let dp = derivate(t: t)
        let dp2 = secondDerivate(t: t)
        let dpSize = hypot(dp.dx, dp.dy)
        let denominator = dpSize * dpSize * dpSize
        let nominator = dp.dx * dp2.dy - dp.dy * dp2.dx

        return nominator / denominator
    }
}

struct Bezier3: ParametricCurve {

    let p0: CGPoint
    let p1: CGPoint
    let p2: CGPoint
    let p3: CGPoint

    let A: CGFloat
    let B: CGFloat
    let C: CGFloat
    let D: CGFloat
    let E: CGFloat
    let F: CGFloat
    let G: CGFloat
    let H: CGFloat


    public private(set) var totalArcLength: CGFloat = .zero

    init(from: CGPoint, to: CGPoint, control1: CGPoint, control2: CGPoint) {
        p0 = from
        p1 = control1
        p2 = control2
        p3 = to
        A = to.x - 3 * control2.x + 3 * control1.x - from.x
        B = 3 * control2.x - 6 * control1.x + 3 * from.x
        C = 3 * control1.x - 3 * from.x
        D = from.x
        E = to.y - 3 * control2.y + 3 * control1.y - from.y
        F = 3 * control2.y - 6 * control1.y + 3 * from.y
        G = 3 * control1.y - 3 * from.y
        H = from.y
        // mandatory !!!
        totalArcLength = arcLength(t: 1)
    }

    func point(t: CGFloat)->CGPoint {
        let x = A * t * t * t + B * t * t + C * t + D
        let y = E * t * t * t + F * t * t + G * t + H
        return CGPoint(x: x, y: y)
    }

    func derivate(t: CGFloat)->CGVector {
        let dx = 3 * A * t * t + 2 * B * t + C
        let dy = 3 * E * t * t + 2 * F * t + G
        return CGVector(dx: dx, dy: dy)
    }

    func secondDerivate(t: CGFloat)->CGVector {
        let dx = 6 * A * t + 2 * B
        let dy = 6 * E * t + 2 * F
        return CGVector(dx: dx, dy: dy)
    }

}

class AircraftModel: ObservableObject {
    let track: ParametricCurve
    let path: Path
    var aircraft: some View {
        let t = track.curveParameter(arcLength: alongTrackDistance)
        let p = track.point(t: t)
        let dp = track.derivate(t: t)
        let h = Angle(radians: atan2(Double(dp.dy), Double(dp.dx)))
        return Text("?").font(.largeTitle).rotationEffect(h).position(p)
    }
    @Published var alongTrackDistance = CGFloat.zero
    init(from: CGPoint, to: CGPoint, control1: CGPoint, control2: CGPoint) {
        track = Bezier3(from: from, to: to, control1: control1, control2: control2)
        path = Path({ (path) in
            path.move(to: from)
            path.addCurve(to: to, control1: control1, control2: control2)
        })
    }
}

struct ContentView: View {
    @ObservedObject var aircraft = AircraftModel(from: .init(x: 0, y: 0), to: .init(x: 500, y: 600), control1: .init(x: 600, y: 100), control2: .init(x: -300, y: 400))

    var body: some View {
        VStack {
            ZStack {
                aircraft.path.stroke(style: StrokeStyle( lineWidth: 0.5))
                aircraft.aircraft
            }
            Slider(value: $aircraft.alongTrackDistance, in: (0.0 ... aircraft.track.totalArcLength)) {
                Text("along track distance")
            }.padding()
            Button(action: {
                // fly (to be implemented :-))
            }) {
                Text("Fly!")
            }.padding()
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Если вы беспокоитесь о том, как реализовать «анимированное» движение самолета, анимация SwiftUI не является решением. , Вы должны перемещать самолет программно.

Вы должны импортировать

import Combine

Добавить в модель

@Published var flying = false
var timer: Cancellable? = nil

func fly() {
    flying = true
    timer = Timer
        .publish(every: 0.02, on: RunLoop.main, in: RunLoop.Mode.default)
        .autoconnect()
        .sink(receiveValue: { (_) in
            self.alongTrackDistance += self.track.totalArcLength / 200.0
            if self.alongTrackDistance > self.track.totalArcLength {
                self.timer?.cancel()
                self.flying = false
            }
        })
}

и изменить кнопку

Button(action: {
    self.aircraft.fly()
}) {
    Text("Fly!")
}.disabled(aircraft.flying)
.padding()

Наконец я получил

enter image description here

0 голосов
/ 04 марта 2020

попробуйте это:

НО: будьте осторожны: это не работает в режиме предварительного просмотра, вы должны запустить на симуляторе / устройстве

struct MyShape: Shape {

    func path(in rect: CGRect) -> Path {
        let path =

        Path { path in
            path.move(to: CGPoint(x: 200, y: 100))
            path.addQuadCurve(to: CGPoint(x: 230, y: 200), control: CGPoint(x: -100, y: 300))
            path.addQuadCurve(to: CGPoint(x: 90, y: 400), control: CGPoint(x: 400, y: 130))
            path.addLine(to: CGPoint(x: 90, y: 600))
        }


        return path
    }
}

struct ContentView: View {

    @State var x: CGFloat = 0.0

    var body: some View {
        MyShape()
            .trim(from: 0, to: x)
            .stroke(lineWidth: 10)
            .frame(width: 200, height: 200)
            .onAppear() {
                withAnimation(Animation.easeInOut(duration: 3).delay(0.5)) {
                    self.x = 1
                }
        }
    }
}

enter image description here

...