Created
February 22, 2023 11:59
-
-
Save UnderscoreDavidSmith/fd77499b3ca791093abeb58407683f8b to your computer and use it in GitHub Desktop.
SwiftUI UnitPoint for Angle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// GradientComponenet | |
// | |
// Created by David Smith on 2/21/23. | |
// | |
import SwiftUI | |
struct ContentView: View { | |
@State var rotationAngle:CGFloat = 0.0 | |
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 startPoint:UnitPoint { | |
return unitSquareIntersectionPoint(Angle(degrees: 360.0 * rotationAngle)) | |
} | |
var endPoint:UnitPoint { | |
return unitSquareIntersectionPoint(Angle(degrees: 360.0 * rotationAngle + 180.0)) | |
} | |
var body: some View { | |
VStack { | |
let start = startPoint | |
let end = endPoint | |
ZStack { | |
LinearGradient(colors: [Color.red, Color.blue], startPoint: startPoint, endPoint: endPoint) | |
.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) | |
} | |
.frame(width:200, height:200) | |
Text("\(Int(rotationAngle * 360.0))°") | |
HStack { | |
Circle().fill(.red).frame(width:10, height:10) | |
Text("Start: \(String(format:"%0.1f", start.x)),\(String(format:"%0.1f", start.y))") | |
} | |
HStack { | |
Rectangle().fill(.blue).frame(width:10, height:10) | |
Text("End: \(String(format:"%0.1f", end.x)),\(String(format:"%0.1f", end.y))") | |
} | |
GeometryReader { proxy in | |
ZStack { | |
Color(white:0.9) | |
RoundedRectangle(cornerRadius: 8) | |
.fill(Color.green) | |
.frame(width:32, height:32) | |
.overlay( | |
HStack(spacing:3) { | |
RoundedRectangle(cornerRadius: 4) | |
.fill(.white) | |
.frame(width: 2, height:24) | |
RoundedRectangle(cornerRadius: 4) | |
.fill(.white) | |
.frame(width: 2, height:24) | |
RoundedRectangle(cornerRadius: 4) | |
.fill(.white) | |
.frame(width: 2, height:24) | |
} | |
) | |
.position(x:22 + ((proxy.size.width - 44) * rotationAngle), y:proxy.size.height * 0.5) | |
.highPriorityGesture(DragGesture().onChanged { dragValue in | |
var dragLocation = dragValue.location.x | |
dragLocation = max(22, dragLocation) | |
dragLocation = min(proxy.size.width - 22, dragLocation) | |
rotationAngle = (dragLocation - 22) / (proxy.size.width - 44) | |
}) | |
} | |
} | |
.frame(height:44) | |
Text("Rotation") | |
.frame(minWidth:0, maxWidth:.infinity) | |
} | |
.padding() | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
Thanks for the great article! I updated the geometry calculation to be an angle-based interpolation vs the linear interpolation you used! The linear solution is very close so I included a reference line to show the input angle.
https://gist.github.com/DrewFitz/9447d8ccbc2b89cac7b5d41ebfbb2a79
This:
var normalizedDegree = angle.degrees
while normalizedDegree > 360.0 {
normalizedDegree -= 360.0
}
while normalizedDegree < 0.0 {
normalizedDegree += 360.0
}
can be simplified to this:
var normalizedDegree = angle.degrees % 360;
normalizedDegree = normalizedDegree < 0 ? normalizedDegree + 360 : normalizedDegree;
That first while
loop is the definition of the %
operator. Using %
with a negative value on the left results in a negative number greater than -360 so adding 360 brings in into the range 0-359.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick SwiftUI file for calculating arbitrary LinearGradient angles.