Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Last active October 14, 2023 06:15
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 JadenGeller/249fa8e50514a63442593e8212ca9ea1 to your computer and use it in GitHub Desktop.
Save JadenGeller/249fa8e50514a63442593e8212ca9ea1 to your computer and use it in GitHub Desktop.
import SwiftUI
import simd
struct Rotation3DEffect: GeometryEffect {
var angle: Angle
var axis: (x: CGFloat, y: CGFloat, z: CGFloat)
var anchor: UnitPoint = .center
var anchorZ: CGFloat = 0
var focalLength: CGFloat
var unitTransform3D: CATransform3D {
var rotate = CATransform3DIdentity
rotate.m34 = -1 / focalLength
rotate = CATransform3DRotate(rotate, CGFloat(angle.radians), axis.x, axis.y, axis.z)
let translateToAnchor = CATransform3DMakeTranslation(-anchor.x, -anchor.y, -anchorZ)
let translateFromAnchor = CATransform3DMakeTranslation(anchor.x, anchor.y, anchorZ)
return CATransform3DConcat(translateToAnchor, CATransform3DConcat(rotate, translateFromAnchor))
}
func effectValue(size: CGSize) -> ProjectionTransform {
.init(CATransform3DConcat(
CATransform3DMakeScale(1 / size.width, 1 / size.height, 1),
CATransform3DConcat(unitTransform3D, CATransform3DMakeScale(size.width, size.height, 1))
))
}
struct MeasurementProxy {
var transform: simd_double4x4
func transform(_ anchor: UnitPoint) -> UnitPoint {
let result = transform * SIMD4(anchor.x, anchor.y, 0, 1)
return .init(x: result.x / result.w, y: result.y / result.w)
}
func length(of segment: (UnitPoint, UnitPoint)) -> CGFloat {
let segment = (transform(segment.0), transform(segment.1))
return sqrt(pow(segment.1.x - segment.0.x, 2) + pow(segment.1.y - segment.0.y, 2))
}
func bounds(for size: CGSize) -> CGRect {
var minPoint = CGPoint(x: Double.infinity, y: Double.infinity)
var maxPoint = CGPoint(x: -Double.infinity, y: -Double.infinity)
for anchor in [.topLeading, .topTrailing, .bottomLeading, .bottomTrailing] as [UnitPoint] {
let point = transform(anchor)
minPoint.x = min(minPoint.x, point.x)
minPoint.y = min(minPoint.y, point.y)
maxPoint.x = max(maxPoint.x, point.x)
maxPoint.y = max(maxPoint.y, point.y)
}
return .init(
x: minPoint.x * size.width,
y: minPoint.y * size.height,
width: (maxPoint.x - minPoint.x) * size.width,
height: (maxPoint.y - minPoint.y) * size.height
)
}
}
var measurements: MeasurementProxy {
return .init(transform: unitTransform3D.matrix)
}
}
import SwiftUI
struct BoundsLayout: Layout {
var bounds: (ProposedViewSize, LayoutSubview) -> CGRect
func makeCache(subviews: Subviews) -> CGRect {
.zero
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout CGRect
) -> CGSize {
precondition(subviews.count == 1)
cache = bounds(proposal, subviews[0])
return cache.size
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout CGRect
) {
precondition(subviews.count == 1)
subviews[0].place(at: .init(x: bounds.origin.x - cache.origin.x, y: bounds.origin.y - cache.origin.y), anchor: .topLeading, proposal: proposal)
}
func spacing(
subviews: Self.Subviews,
cache: inout Self.Cache
) -> ViewSpacing {
.zero
}
}
extension View {
func layoutBounds(_ bounds: @escaping (ProposedViewSize, LayoutSubview) -> CGRect) -> some View {
BoundsLayout(bounds: bounds) {
self
}
}
}
import SwiftUI
struct ContentView: View {
@State var size: CGSize = .init(width: 300, height: 300)
@State var alpha: CGFloat = 0.5
@State var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 1, 0)
@State var anchor: UnitPoint = .leading
@State var anchorZ: CGFloat = 0
@State var focalLength: CGFloat = 1
@State var angle: Angle = .degrees(30)
var rotation: Rotation3DEffect {
.init(angle: angle, axis: axis, anchor: anchor, anchorZ: anchorZ, focalLength: focalLength)
}
var body: some View {
Slider(value: $size.width, in: 50...300) {
Text("Size (width): \(size.width)")
}
Slider(value: $size.height, in: 50...300) {
Text("Size (height): \(size.height)")
}
Slider(value: $alpha, in: 0...1) {
Text("Alpha: \(alpha)")
}
Slider(value: $angle.degrees, in: -90...90) {
Text("Angle (degrees): \(angle.degrees)")
}
Slider(value: $anchor.x, in: 0...1) {
Text("Anchor (x): \(anchor.x)")
}
Slider(value: $anchor.y, in: 0...1) {
Text("Anchor (y): \(anchor.y)")
}
Slider(value: $anchorZ, in: -0...1) {
Text("Anchor (z): \(anchorZ)")
}
Slider(value: $axis.x, in: 0...1) {
Text("Axis (x): \(axis.x)")
}
Slider(value: $axis.y, in: 0...1) {
Text("Axis (y): \(axis.y)")
}
Slider(value: $axis.z, in: 0...1) {
Text("Axis (z): \(axis.z)")
}
Slider(value: $focalLength, in: 0...2) {
Text("Focal Length: \(focalLength)")
}
Color.yellow.opacity(alpha)
.frame(width: size.width, height: size.height)
.modifier(rotation)
.layoutBounds { proposal, view in
rotation.measurements.bounds(for: view.sizeThatFits(proposal))
}
.background(.black)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment