Skip to content

Instantly share code, notes, and snippets.

@LidorFadida
Created January 17, 2025 10:28
Show Gist options
  • Select an option

  • Save LidorFadida/5df5e337a956b00ed6999f8c1f1e7512 to your computer and use it in GitHub Desktop.

Select an option

Save LidorFadida/5df5e337a956b00ed6999f8c1f1e7512 to your computer and use it in GitHub Desktop.
//
// 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