Skip to content

Instantly share code, notes, and snippets.

@mayoff
Last active July 16, 2023 15:07
Show Gist options
  • Save mayoff/f2e9ff046049de6ec7450cda63608c72 to your computer and use it in GitHub Desktop.
Save mayoff/f2e9ff046049de6ec7450cda63608c72 to your computer and use it in GitHub Desktop.
point on unit square at a given angle
import SwiftUI
extension UnitPoint {
/// - returns: The point on the perimeter of the unit square that is at angle `angle` relative to the center of the unit square.
init(_ angle: Angle) {
// Inspired by https://math.stackexchange.com/a/4041510/399217
// Also see https://www.desmos.com/calculator/k13553cbgk
let s = sin(angle.radians)
let c = cos(angle.radians)
self.init(
x: (c / s).clamped(to: -1...1) * copysign(1, s) * 0.5 + 0.5,
y: (s / c).clamped(to: -1...1) * copysign(1, c) * 0.5 + 0.5
)
}
}
extension Comparable {
/// - returns: The nearest value to `self` that is in `range`.
func clamped(to range: ClosedRange<Self>) -> Self {
return max(range.lowerBound, min(self, range.upperBound))
}
}
extension Bool {
/// - returns: The opposite of `self`. Note that I have a setter, so I can be used in a `Binding`.
var not: Self {
get { !self }
set { self = !newValue }
}
}
struct ContentView: View {
@State var angle: Angle = .zero
@State var useRob = false
func unitSquareIntersectionPoint(_ angle: Angle) -> UnitPoint {
var normalizedDegree = angle.degrees
while normalizedDegree > 360.0 {
normalizedDegree -= 360.0
}
while normalizedDegree < 0.0 {
normalizedDegree += 360.0
}
if normalizedDegree < 45.0 || normalizedDegree >= 315 {
//Right Edge, x = 1.0
var degreeToConsider = normalizedDegree
if degreeToConsider < 45.0 {
degreeToConsider = normalizedDegree + 360.0
//angle now between 315 & 405
}
let degreeProportion = (degreeToConsider - 315.0) / 90.0
return UnitPoint(x: 1.0, y: 1.0 - degreeProportion)
} else if normalizedDegree < 135.0 {
//Top Edge, y = 0.0
let degreeProportion = (normalizedDegree - 45.0) / 90.0
return UnitPoint(x: 1.0 - degreeProportion, y: 0.0)
} else if normalizedDegree < 225.0 {
//left Edge, x = 0.0
let degreeProportion = (normalizedDegree - 135) / 90.0
return UnitPoint(x: 0.0, y: degreeProportion)
} else if normalizedDegree < 315.0 {
//Bottom Edge, y = 1.0
let degreeProportion = (normalizedDegree - 225) / 90.0
return UnitPoint(x: degreeProportion, y: 1.0)
}
return .zero
}
var body: some View {
VStack {
let _start = unitSquareIntersectionPoint(angle)
let _end = unitSquareIntersectionPoint(angle + .radians(.pi))
let rstart = UnitPoint(-angle)
let rend = UnitPoint(-angle + .radians(.pi))
let start = useRob ? rstart : _start
let end = useRob ? rend : _end
ZStack {
LinearGradient(colors: [Color.red, Color.blue], startPoint: start, endPoint: end)
.border(.black)
Circle()
.fill(.red)
.frame(width:15, height:15)
.overlay(Circle().stroke(.black))
.position(x:start.x * 200, y:start.y * 200)
Rectangle()
.fill(.blue)
.frame(width:15, height:15)
.border(.black)
.position(x:end.x * 200, y:end.y * 200)
Canvas { gc, size in
gc.translateBy(x: size.width * 0.5, y: size.height * 0.5)
let t: CGFloat = 0.5 * size.width / 2.squareRoot()
gc.stroke(
Path(ellipseIn: CGRect(
x: -t,
y: -t,
width: 2 * t,
height: 2 * t
)),
with: .color(.black)
)
let path = Path {
$0.move(to: .zero)
$0.addLine(to: .init(
x: t * cos(angle.radians),
y: -t * sin(angle.radians)
))
}
gc.stroke(path, with: .color(.black))
}
.padding(-100)
}
.frame(width:200, height:200)
Spacer()
.frame(height: 60)
Slider(value: $angle.degrees, in: 0.0 ... 360.0)
.padding(.horizontal, 50)
Text("\(Int(angle.degrees))°")
Grid(alignment: .trailing) {
GridRow {
Text("")
HStack(spacing: 0) {
Circle().fill(.red).frame(width:10, height:10)
Text(".x")
}
HStack(spacing: 0) {
Circle().fill(.red).frame(width:10, height:10)
Text(".y")
}
HStack(spacing: 0) {
Rectangle().fill(.blue).frame(width:10, height:10)
Text(".x")
}
HStack(spacing: 0) {
Rectangle().fill(.blue).frame(width:10, height:10)
Text(".y")
}
}
gridRow("_David", $useRob.not, _start, _end)
gridRow("Rob", $useRob, rstart, rend)
gridRow(
"diff", nil,
.init(
x: abs(_start.x - rstart.x),
y: abs(_start.y - rstart.y)
),
.init(
x: abs(_end.x - rend.x),
y: abs(_end.y - rend.y)
)
)
}
Button("Play") {
Task {
angle = .zero
try await Task.sleep(for: .seconds(1))
for d in stride(from: 0.0, through: 90.0, by: 1/10) {
try await Task.sleep(for: .seconds(1/60))
angle = .degrees(Double(d))
}
for d in stride(from: 90.0, through: 0.0, by: -1/10) {
try await Task.sleep(for: .seconds(1/60))
angle = .degrees(Double(d))
}
}
}
}
.padding()
}
private func gridRow(_ label: String, _ binding: Binding<Bool>?, _ start: UnitPoint, _ end: UnitPoint) -> some View {
return GridRow {
if let binding {
Toggle(isOn: binding) { Text(label) }
.toggleStyle(.button)
} else {
Text(label)
}
Text(start.x, format: .number.rounded(increment: 0.01))
Text(start.y, format: .number.rounded(increment: 0.01))
Text(end.x, format: .number.rounded(increment: 0.01))
Text(end.y, format: .number.rounded(increment: 0.01))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment