I am trying to center the ellipse with the two points as the foci. The equation ought to be sufficient, but the ellipse is offset. No solution seems to work. I can rotate the ellipse, but the problem remains.
The ellipse ought to be around the line and points. The rotation as the green point is dragged works.
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()
}
You are drawing the ellipse with its center being at the top left of the Path
view. Add a border to the Path
and comment out the rotationEffect
and position
to see this for yourself.
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)
The blue border is the "logical frame" of the Path
view, so rotationEffect
and position
will operate on that blue rectangle. It correctly transforms the blue border to the desired position. Uncomment the two lines and you get:
If you want to position the ellipse using rotationEffect
and position
in this way, the ellipse should be drawn inside of the "logical frame" of the Path
view. This means the rectangle in which you draw the ellipse should have (0, 0) as its origin
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)
This is equivalent to just using the built-in Ellipse
shape:
Ellipse()
.stroke(Color.red, lineWidth: 2)
.frame(width: semiMajor * 2, height: semiMinor * 2)
.border(.blue)
.rotationEffect(Angle(radians: angle))
.position(x: centerX, y: centerY)
Alternatively, you can keep the position and size of the Path
view constant, and only transform the path's drawing instructions.
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)
Here I used rotation
and offset
, which both transforms the path's drawing instructions, as opposed to the "logical frame" of the Path
view. The "logical frame" of the Path
view will always fill all the available space.
anchor: .topLeading
is needed to rotate the view about (0, 0), instead of the center of the path, which is the default.