Skip to content

Instantly share code, notes, and snippets.

@DrewFitz
Last active February 24, 2023 17:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DrewFitz/9447d8ccbc2b89cac7b5d41ebfbb2a79 to your computer and use it in GitHub Desktop.
Save DrewFitz/9447d8ccbc2b89cac7b5d41ebfbb2a79 to your computer and use it in GitHub Desktop.
Corrected Geometry for SwiftUI UnitPoint Gradient Rotation by _DavidSmith -> https://gist.github.com/UnderscoreDavidSmith/fd77499b3ca791093abeb58407683f8b
//
// ContentView.swift
// GradientComponenet
//
// Created by David Smith on 2/21/23.
// Updaetd by Drew Fitzpatrick on 2/24/23.
//
import SwiftUI
struct ContentView: View {
@State var rotationAngle:CGFloat = 0.0
@State var corrected = false
func unitSquareIntersectionPointCorrected(_ angle: Angle) -> UnitPoint {
// find our unit circle points
let x = cos(-angle.radians)
let y = sin(-angle.radians)
// scale outward such that the longest axis = 1, then renormalize to 0-1
let max = max(abs(x), abs(y))
let newX = (x/max) * 0.5 + 0.5
let newY = (y/max) * 0.5 + 0.5
return UnitPoint(x: newX, y: newY)
}
func unitSquareIntersectionPoint(_ angle:Angle) -> UnitPoint {
if corrected {
return unitSquareIntersectionPointCorrected(angle)
} else {
return unitSquareIntersectionPointPrevious(angle)
}
}
func unitSquareIntersectionPointPrevious(_ 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)
}
.overlay {
// quick and dirty reference line for seeing absolute rotation vs gradient stops
Rectangle()
.fill(.black)
.frame(width: 250, height: 2)
.rotationEffect(Angle.degrees(rotationAngle * -360))
}
.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)
Toggle("Corrected", isOn: $corrected)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@DrewFitz
Copy link
Author

demo-gif

Gif to show the difference between linear interpolation of the points (original) and angle-based interpolation (corrected).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment