Created
January 17, 2025 10:28
-
-
Save LidorFadida/5df5e337a956b00ed6999f8c1f1e7512 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| // | |
| // OrbitView.swift | |
| // PathTranslationEffectExample | |
| // | |
| // Created by Lidor Fadida on 17/01/2025. | |
| // | |
| import SwiftUI | |
| struct OrbitView: View { | |
| @State private var earthProgress: CGFloat = 0.0 | |
| @State private var earthRotation: CGFloat = 0.0 | |
| @State private var moonProgress: CGFloat = 0.0 | |
| @State private var sunScale: CGFloat = 1.0 | |
| var body: some View { | |
| GeometryReader { proxy in | |
| let size = proxy.size | |
| sun( | |
| position: size.center, | |
| size: 70.0 | |
| ) | |
| .shadow( | |
| color: .yellow, | |
| radius: sunScale * 10.0 | |
| ) | |
| earth( | |
| size: 50.0 | |
| ) | |
| .pathAnimation( | |
| progress: earthProgress, | |
| path: Path.circle( | |
| center: size.center, | |
| size: size.applying(.init(scaleX: 0.6, y: 0.6)) | |
| ) | |
| ) | |
| } | |
| .onAppear(perform: animation) | |
| } | |
| private func sun(position: CGPoint, size: CGFloat) -> some View { | |
| Text("π") | |
| .font(.system(size: size)) | |
| .position(position) | |
| .scaleEffect( | |
| CGSize( | |
| width: sunScale, | |
| height: sunScale | |
| ) | |
| ) | |
| } | |
| private func earth(size: CGFloat) -> some View { | |
| Text("π") | |
| .font(.system(size: size)) | |
| .rotationEffect(.degrees((earthRotation * 360.0))) | |
| .overlay(earthOverlay(moonSize: size - 10.0)) | |
| } | |
| private func earthOverlay(moonSize: CGFloat) -> some View { | |
| GeometryReader { proxy in | |
| let size = proxy.size | |
| let scaleFactor: CGFloat = 2.0 | |
| Text("π") | |
| .font(.system(size: moonSize)) | |
| .pathAnimation( | |
| progress: moonProgress, | |
| path: Path.circle( | |
| center: size.center, | |
| size: size | |
| .applying( | |
| CGAffineTransform(scaleX: scaleFactor, y: scaleFactor) | |
| ) | |
| ) | |
| ) | |
| } | |
| } | |
| private func animation() { | |
| let earthDuration: CGFloat = 30 | |
| withAnimation(.linear(duration: earthDuration).repeatForever(autoreverses: false)) { | |
| earthProgress = 1.0 | |
| } | |
| withAnimation(.linear(duration: earthDuration / 8).repeatForever(autoreverses: false)) { | |
| earthRotation = 1.0 | |
| } | |
| withAnimation(.linear(duration: (earthDuration / 6)).repeatForever(autoreverses: false)) { | |
| moonProgress = 1.0 | |
| } | |
| withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { | |
| sunScale = 1.1 | |
| } | |
| } | |
| } | |
| //MARK: - Path+Extensions | |
| extension Path { | |
| static func circle(center: CGPoint, size: CGSize) -> Self { | |
| Path { path in | |
| path.addArc( | |
| center: center, | |
| radius: size.width / 2.0, | |
| startAngle: .zero, | |
| endAngle: .degrees(360.0), | |
| clockwise: false | |
| ) | |
| } | |
| } | |
| } | |
| //MARK: - CGSize+Extensions | |
| extension CGSize { | |
| var center: CGPoint { | |
| return CGPoint(x: width / 2.0, y: height / 2.0) | |
| } | |
| } | |
| //MARK: - View+PathAnimation | |
| extension View { | |
| func pathAnimation(progress: CGFloat, path: Path) -> some View { | |
| self.modifier( | |
| PathTranslationEffect( | |
| progress: progress, | |
| path: path | |
| ) | |
| ) | |
| } | |
| } | |
| //MARK: - PathTranslationEffect | |
| struct PathTranslationEffect: GeometryEffect { | |
| var progress: CGFloat | |
| let path: Path | |
| var animatableData: CGFloat { | |
| get { progress } | |
| set { progress = newValue } | |
| } | |
| func effectValue(size: CGSize) -> ProjectionTransform { | |
| let clampedProgress = max(0.0, min(progress, 1.0)) | |
| let trimmedPath = path.trimmedPath(from: 0.0, to: clampedProgress) | |
| guard let point = trimmedPath.currentPoint else { | |
| return ProjectionTransform(.identity) | |
| } | |
| let transform = CGAffineTransform( | |
| translationX: point.x - (size.width / 2.0), | |
| y: point.y - (size.height / 2.0) | |
| ) | |
| return ProjectionTransform(transform) | |
| } | |
| } | |
| #Preview("Orbit Scene") { | |
| OrbitView() | |
| .background(Color.black) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment