代码之家  ›  专栏  ›  技术社区  ›  BJB

SwiftUI中以GeometricReader为中心的椭圆未正确对齐。它应该以两点的中心点为中心

  •  1
  • BJB  · 技术社区  · 1 年前

    我试图以两点为焦点将椭圆居中。方程式应该足够了,但椭圆是偏移的。似乎没有解决方案。我可以旋转椭圆,但问题仍然存在。

    Ellipse offset from points

    椭圆应该围绕直线和点。拖动绿点时的旋转有效。

    import SwiftUI
    import Foundation
    
    struct ContentView: View {
        // Interactive variables for draggable source point
        @State private var s_x: Double = 0
        @State private var s_y: Double = -100
        
        // Fixed focus point (d_x, d_y) set to the center of the screen
        @State private var d_x: Double = 0
        @State private var d_y: Double = 0
        
        // Constants for the sliders (tempo and note value)
        @State private var T: Double = 185
        @State private var selectedNoteValue: String = "32nd Note"
        @State private var M: Double = 20
        
        // Error calculation mode (1 for E1, 2 for E2)
        @State private var selectedErrorCalculation: Int = 1
        
        // Note value mapping to musical durations
        let noteValueMap: [String: Double] = [
            "Half Note": 0.5,
            "Quarter Note": 1,
            "8th Note": 2,
            "Triplet": 3,
            "16th Note": 4,
            "32nd Note": 8,
            "64th Note": 16,
            "128th Note": 32
        ]
        
        // Computed Variables for the distance formula
        var I: Double {
            return (22.5 / 12) / 1150
        }
        
        var E1: Double {
            return 60 / (T * noteValueMap[selectedNoteValue]! * I)
        }
        
        var E2: Double {
            return 13.397244094488 * M / 22.5
        }
        
        // Euclidean Distance Function
        func C(x: Double, y: Double, a: Double, b: Double) -> Double {
            return sqrt(pow(x - a, 2) + pow(y - b, 2))
        }
        
        // Focal Distance (constant sum of distances from any point to the foci)
        func F() -> Double {
            let distanceBetweenFoci = C(x: s_x, y: s_y, a: d_x, b: d_y)
            return distanceBetweenFoci + (selectedErrorCalculation == 1 ? E1 : E2)
        }
        
        // Semi-Major and Semi-Minor Axes for the Ellipse
        func ellipseAxes() -> (semiMajor: Double, semiMinor: Double) {
            let distanceBetweenFoci = C(x: s_x, y: s_y, a: d_x, b: d_y)
            let F_value = F()
            
            // Semi-major axis is the half of the sum of distances (F), semi-minor axis is the difference
            let semiMajor = F_value / 2
            let semiMinor = semiMajor - distanceBetweenFoci / 2
            
            return (semiMajor, semiMinor)
        }
        
        var body: some View {
            GeometryReader { geometry in
                VStack {
                    ZStack {
                        // Draw the line connecting the two foci (source and fixed point)
                        Path { path in
                            path.move(to: CGPoint(x: s_x + geometry.size.width / 2, y: s_y + geometry.size.height / 2))
                            path.addLine(to: CGPoint(x: d_x + geometry.size.width / 2, y: d_y + geometry.size.height / 2))
                        }
                        .stroke(Color.blue, lineWidth: 2)
                        
                        // Compute Ellipse Properties
                        let centerX = (s_x + d_x) / 2 + geometry.size.width / 2
                        let centerY = (s_y + d_y) / 2 + geometry.size.height / 2
                        let focalDistance = C(x: s_x, y: s_y, a: d_x, b: d_y)
                        let semiMajor = F() / 2
                        let semiMinor = sqrt(pow(semiMajor, 2) - pow(focalDistance / 2, 2))
                        
                        // Adjust angle for flipped Y-axis
                        let angle = atan2(s_y - d_y, s_x - d_x)  // Correct the angle calculation for the Y-axis flip
                        
                        // Draw the ellipse
                        Path { path in
                            let rect = CGRect(x: -semiMajor, y: -semiMinor, width: semiMajor * 2, height: semiMinor * 2)
                            path.addEllipse(in: rect)
                        }
                        .stroke(Color.red, lineWidth: 2)
                        .frame(width: semiMajor * 2, height: semiMinor * 2)
                        .rotationEffect(Angle(radians: angle))
                        .position(x: centerX, y: centerY)
                        
                        // Draggable Source Point (focal point 1)
                        Circle()
                            .fill(Color.green)
                            .frame(width: 10, height: 10)
                            .position(x: s_x + geometry.size.width / 2, y: s_y + geometry.size.height / 2)
                            .gesture(
                                DragGesture()
                                    .onChanged { value in
                                        self.s_x = Double(value.location.x - geometry.size.width / 2)
                                        self.s_y = Double(value.location.y - geometry.size.height / 2)
                                    }
                            )
                        
                        // Fixed Point (focal point 2)
                        Circle()
                            .fill(Color.blue)
                            .frame(width: 10, height: 10)
                            .position(x: d_x + geometry.size.width / 2, y: d_y + geometry.size.height / 2)
                    }
                    
                    Spacer()
                    
                    VStack(spacing: 10) {
                        Text("Control Parameters")
                            .font(.headline)
                        
                        HStack {
                            Text("Note Value (N):")
                            Picker("Note Value", selection: $selectedNoteValue) {
                                ForEach(noteValueMap.keys.sorted(), id: \.self) { note in
                                    Text(note).tag(note)
                                }
                            }
                            .pickerStyle(MenuPickerStyle())
                        }
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding([.horizontal])
                        
                        VStack {
                            HStack {
                                Text("Tempo (T): \(Int(T))")
                                Slider(value: $T, in: 60...240, step: 1)
                                    .accentColor(.green)
                            }
                            HStack {
                                Text("MSec Error: \(Int(M))")
                                Slider(value: $M, in: 1...100, step: 1)
                                    .accentColor(.purple)
                            }
                        }
                        .padding([.horizontal])
                        
                        Picker("Select Error Calculation", selection: $selectedErrorCalculation) {
                            Text("E1 (Musically Based)").tag(1)
                            Text("E2 (Error in Milliseconds)").tag(2)
                        }
                        .pickerStyle(SegmentedPickerStyle())
                        .padding([.top, .horizontal])
                        
                        DisclosureGroup("Details") {
                            VStack {
                                Text("I (Seconds per Cell)= \(I, specifier: "%.5f")")
                                Text("E1 (60/TNI)= \(E1, specifier: "%.5f")")
                                Text("E2 (MSec Error/I) = \(E2, specifier: "%.5f")")
                                Text("F (S to D + E)= \(F(), specifier: "%.5f")")
                                Text("Source: (\(Int(s_x)), \(Int(s_y)))")
                                Text("Destination: (\(Int(d_x)), \(Int(d_y)))")
                            }
                            .padding([.top, .horizontal])
                        }
                        .padding([.top, .horizontal])
                    }
                    .frame(maxWidth: .infinity)
                    .padding([.bottom, .horizontal])
                    .background(Color.white)
                }
            }
        }
        
        struct ContentView_Previews: PreviewProvider {
            static var previews: some View {
                ContentView()
            }
        }
    
      
    }
    #Preview {
        ContentView()
    }
    
    1 回复  |  直到 1 年前
        1
  •  1
  •   Sweeper    1 年前

    您正在绘制椭圆,其中心位于 Path 视图。为添加边框 路径 并发表评论 rotationEffect position 亲眼看看。

    Path { path in
        let rect = CGRect(x: -semiMajor, y: -semiMinor, width: semiMajor * 2, height: semiMinor * 2)
        path.addEllipse(in: rect)
    }
    .stroke(Color.red, lineWidth: 2)
    .frame(width: semiMajor * 2, height: semiMinor * 2)
    // add a border
    .border(.blue)
    // .rotationEffect(Angle(radians: angle))
    // .position(x: centerX, y: centerY)
    

    enter image description here

    蓝色边框是“逻辑框架” 路径 视图,所以 旋转效应 位置 将对那个蓝色矩形进行操作。它将蓝色边框正确地转换到所需的位置。取消对这两行的注释,您将得到:

    enter image description here

    如果要使用以下命令定位椭圆 旋转效应 位置 这样,椭圆应该绘制在 路径 视图。这意味着您绘制椭圆的矩形应该以(0,0)为原点

    Path { path in
        let rect = CGRect(x: 0, y: 0, width: semiMajor * 2, height: semiMinor * 2)
        path.addEllipse(in: rect)
    }
    .stroke(Color.red, lineWidth: 2)
    .frame(width: semiMajor * 2, height: semiMinor * 2)
    .border(.blue)
    .rotationEffect(Angle(radians: angle))
    .position(x: centerX, y: centerY)
    

    这相当于只使用内置 Ellipse 形状:

    Ellipse()
        .stroke(Color.red, lineWidth: 2)
        .frame(width: semiMajor * 2, height: semiMinor * 2)
        .border(.blue)
        .rotationEffect(Angle(radians: angle))
        .position(x: centerX, y: centerY)
    

    或者,您可以保留 路径 视图常数,仅转换路径的绘图指令。

    Path { path in
        let rect = CGRect(x: -semiMajor, y: -semiMinor, width: semiMajor * 2, height: semiMinor * 2)
        path.addEllipse(in: rect)
    }
    .rotation(Angle(radians: angle), anchor: .topLeading)
    .offset(x: centerX, y: centerY)
    .stroke(Color.red, lineWidth: 2)
    

    我用过这里 rotation offset ,这两者都转换了路径的绘图指令,而不是路径的“逻辑框架” 路径 视图。“逻辑框架” 路径 视图将始终填充所有可用空间。

    anchor: .topLeading 需要围绕(0,0)旋转视图,而不是默认的路径中心。

    推荐文章