Skip to content

Instantly share code, notes, and snippets.

@mayoff
Last active July 16, 2023 15:07
Show Gist options
  • Select an option

  • Save mayoff/f2e9ff046049de6ec7450cda63608c72 to your computer and use it in GitHub Desktop.

Select an option

Save mayoff/f2e9ff046049de6ec7450cda63608c72 to your computer and use it in GitHub Desktop.

Revisions

  1. mayoff revised this gist Feb 24, 2023. 1 changed file with 9 additions and 7 deletions.
    16 changes: 9 additions & 7 deletions UnitPoint.angle.swift
    Original file line number Diff line number Diff line change
    @@ -1,12 +1,5 @@
    import SwiftUI

    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 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) {
    @@ -22,6 +15,13 @@ extension UnitPoint {
    }
    }

    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 {
    @@ -169,6 +169,8 @@ struct ContentView: View {
    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))
  2. mayoff revised this gist Feb 24, 2023. 1 changed file with 16 additions and 0 deletions.
    16 changes: 16 additions & 0 deletions UnitPoint.angle.swift
    Original file line number Diff line number Diff line change
    @@ -164,6 +164,22 @@ struct ContentView: View {
    )
    )
    }

    Button("Play") {
    Task {
    angle = .zero

    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()
    }
  3. mayoff created this gist Feb 24, 2023.
    191 changes: 191 additions & 0 deletions UnitPoint.angle.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,191 @@
    import SwiftUI

    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 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 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)
    )
    )
    }
    }
    .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()
    }
    }