diff --git a/Sources/OrbView/OrbView.swift b/Sources/OrbView/OrbView.swift index 1530b56..1de57e1 100644 --- a/Sources/OrbView/OrbView.swift +++ b/Sources/OrbView/OrbView.swift @@ -7,6 +7,10 @@ import SwiftUI public struct OrbView: View { + @Environment(\.scenePhase) var scenePhase + @State private var isAnimating: Bool = true + @State private var resumedFromBG = false + private let config: OrbConfiguration public init(configuration: OrbConfiguration = OrbConfiguration()) { @@ -60,6 +64,24 @@ public struct OrbView: View { ) ) } + .onChange(of: scenePhase) { oldPhase, newPhase in + if oldPhase == .background { resumedFromBG = true } + + switch newPhase { + case .active: + Task { + try await Task.sleep(nanoseconds: resumedFromBG ? 300_000_000 : 100_000_000) + if resumedFromBG { resumedFromBG = false } + isAnimating = true + } + case .background: + isAnimating = false + case .inactive: + isAnimating = false + @unknown default: + break + } + } } private var background: some View { @@ -78,6 +100,7 @@ public struct OrbView: View { // Added multiple particle effects since the blurring amounts are different ZStack { ParticlesView( + isAnimating: $isAnimating, color: config.particleColor, speedRange: 10...20, sizeRange: 0.5...1, @@ -87,6 +110,7 @@ public struct OrbView: View { .blur(radius: 1) ParticlesView( + isAnimating: $isAnimating, color: config.particleColor, speedRange: 20...30, sizeRange: 0.2...1, @@ -101,16 +125,23 @@ public struct OrbView: View { GeometryReader { geometry in let size = min(geometry.size.width, geometry.size.height) - RotatingGlowView(color: .white.opacity(0.75), - rotationSpeed: config.speed * 1.5, - direction: .clockwise) - .mask { - WavyBlobView(color: .white, loopDuration: 60 / config.speed * 1.75) - .frame(maxWidth: size * 1.875) - .offset(x: 0, y: size * 0.31) - } - .blur(radius: 1) - .blendMode(.plusLighter) + RotatingGlowView( + isAnimating: $isAnimating, + color: .white.opacity(0.75), + rotationSpeed: config.speed * 1.5, + direction: .clockwise + ) + .mask { + WavyBlobView( + isAnimating: $isAnimating, + color: .white, + loopDuration: 60 / config.speed * 1.75 + ) + .frame(maxWidth: size * 1.875) + .offset(x: 0, y: size * 0.31) + } + .blur(radius: 1) + .blendMode(.plusLighter) } } @@ -118,35 +149,48 @@ public struct OrbView: View { GeometryReader { geometry in let size = min(geometry.size.width, geometry.size.height) - RotatingGlowView(color: .white, - rotationSpeed: config.speed * 0.75, - direction: .counterClockwise) - .mask { - WavyBlobView(color: .white, loopDuration: 60 / config.speed * 2.25) - .frame(maxWidth: size * 1.25) - .rotationEffect(.degrees(90)) - .offset(x: 0, y: size * -0.31) - } - .opacity(0.5) - .blur(radius: 1) - .blendMode(.plusLighter) + RotatingGlowView( + isAnimating: $isAnimating, + color: .white, + rotationSpeed: config.speed * 0.75, + direction: .counterClockwise + ) + .mask { + WavyBlobView( + isAnimating: $isAnimating, + color: .white, + loopDuration: 60 / config.speed * 2.25 + ) + .frame(maxWidth: size * 1.25) + .rotationEffect(.degrees(90)) + .offset(x: 0, y: size * -0.31) + } + .opacity(0.5) + .blur(radius: 1) + .blendMode(.plusLighter) } } private func coreGlowEffects(size: CGFloat) -> some View { ZStack { - RotatingGlowView(color: config.glowColor, - rotationSpeed: config.speed * 3, - direction: .clockwise) - .blur(radius: size * 0.08) - .opacity(config.coreGlowIntensity) - - RotatingGlowView(color: config.glowColor, - rotationSpeed: config.speed * 2.3, - direction: .clockwise) - .blur(radius: size * 0.06) - .opacity(config.coreGlowIntensity) - .blendMode(.plusLighter) + RotatingGlowView( + isAnimating: $isAnimating, + color: config.glowColor, + rotationSpeed: config.speed * 3, + direction: .clockwise + ) + .blur(radius: size * 0.08) + .opacity(config.coreGlowIntensity) + + RotatingGlowView( + isAnimating: $isAnimating, + color: config.glowColor, + rotationSpeed: config.speed * 2.3, + direction: .clockwise + ) + .blur(radius: size * 0.06) + .opacity(config.coreGlowIntensity) + .blendMode(.plusLighter) } .padding(size * 0.08) } @@ -155,22 +199,28 @@ public struct OrbView: View { private func baseDepthGlows(size: CGFloat) -> some View { ZStack { // Outer glow (previously outerGlow function) - RotatingGlowView(color: config.glowColor, - rotationSpeed: config.speed * 0.75, - direction: .counterClockwise) - .padding(size * 0.03) - .blur(radius: size * 0.06) - .rotationEffect(.degrees(180)) - .blendMode(.destinationOver) - + RotatingGlowView( + isAnimating: $isAnimating, + color: config.glowColor, + rotationSpeed: config.speed * 0.75, + direction: .counterClockwise + ) + .padding(size * 0.03) + .blur(radius: size * 0.06) + .rotationEffect(.degrees(180)) + .blendMode(.destinationOver) + // Outer ring (previously outerRing function) - RotatingGlowView(color: config.glowColor.opacity(0.5), - rotationSpeed: config.speed * 0.25, - direction: .clockwise) - .frame(maxWidth: size * 0.94) - .rotationEffect(.degrees(180)) - .padding(8) - .blur(radius: size * 0.032) + RotatingGlowView( + isAnimating: $isAnimating, + color: config.glowColor.opacity(0.5), + rotationSpeed: config.speed * 0.25, + direction: .clockwise + ) + .frame(maxWidth: size * 0.94) + .rotationEffect(.degrees(180)) + .padding(8) + .blur(radius: size * 0.032) } } diff --git a/Sources/OrbView/Subviews/ParticlesView.swift b/Sources/OrbView/Subviews/ParticlesView.swift index aa44e54..ff214d6 100644 --- a/Sources/OrbView/Subviews/ParticlesView.swift +++ b/Sources/OrbView/Subviews/ParticlesView.swift @@ -126,6 +126,8 @@ class ParticleScene: SKScene { } struct ParticlesView: View { + @Binding var isAnimating: Bool + let color: Color let speedRange: ClosedRange let sizeRange: ClosedRange @@ -150,12 +152,16 @@ struct ParticlesView: View { SpriteView(scene: scene, options: [.allowsTransparency]) .frame(width: geometry.size.width, height: geometry.size.height) .ignoresSafeArea() + .onChange(of: isAnimating) { newValue in + scene.isPaused = !newValue + } } } } #Preview { ParticlesView( + isAnimating: .constant(true), color: .green, speedRange: 30...60, sizeRange: 0.2...1, diff --git a/Sources/OrbView/Subviews/RotatingGlowView.swift b/Sources/OrbView/Subviews/RotatingGlowView.swift index 19665e2..ed2a283 100644 --- a/Sources/OrbView/Subviews/RotatingGlowView.swift +++ b/Sources/OrbView/Subviews/RotatingGlowView.swift @@ -20,16 +20,21 @@ enum RotationDirection { } struct RotatingGlowView: View { - @State private var rotation: Double = 0 + @Binding var isAnimating: Bool + @State private var animationStartDate: Date? + @State private var accumulatedRotation: Double = 0 private let color: Color private let rotationSpeed: Double private let direction: RotationDirection - init(color: Color, - rotationSpeed: Double = 30, - direction: RotationDirection) - { + init( + isAnimating: Binding, + color: Color, + rotationSpeed: Double = 30, + direction: RotationDirection + ) { + self._isAnimating = isAnimating self.color = color self.rotationSpeed = rotationSpeed self.direction = direction @@ -38,34 +43,64 @@ struct RotatingGlowView: View { var body: some View { GeometryReader { geometry in let size = min(geometry.size.width, geometry.size.height) + TimelineView(.animation) { timeline in + let date = timeline.date - Circle() - .fill(color) - .mask { - ZStack { - Circle() - .frame(width: size, height: size) - .blur(radius: size * 0.16) - Circle() - .frame(width: size * 1.31, height: size * 1.31) - .offset(y: size * 0.31) - .blur(radius: size * 0.16) - .blendMode(.destinationOut) + // Encapsulate the computation in a closure + let rotationAngle: Double = { + if let startDate = animationStartDate { + let elapsedTime = date.timeIntervalSince(startDate) + return accumulatedRotation + direction.multiplier * rotationSpeed * elapsedTime + } else { + return accumulatedRotation } + }() + + let rotationAngleAdjusted = rotationAngle.truncatingRemainder(dividingBy: 360) + + Circle() + .fill(color) + .mask { + ZStack { + Circle() + .frame(width: size, height: size) + .blur(radius: size * 0.16) + Circle() + .frame(width: size * 1.31, height: size * 1.31) + .offset(y: size * 0.31) + .blur(radius: size * 0.16) + .blendMode(.destinationOut) + } + } + .rotationEffect(.degrees(rotationAngleAdjusted)) + } + .onAppear { + if isAnimating { + animationStartDate = Date() } - .rotationEffect(.degrees(rotation)) - .onAppear { - withAnimation(.linear(duration: 360 / rotationSpeed).repeatForever(autoreverses: false)) { - rotation = 360 * direction.multiplier + } + .onChange(of: isAnimating) { newValue in + if newValue { + animationStartDate = Date() + } else { + if let startDate = animationStartDate { + let elapsedTime = Date().timeIntervalSince(startDate) + accumulatedRotation += direction.multiplier * rotationSpeed * elapsedTime + accumulatedRotation = accumulatedRotation.truncatingRemainder(dividingBy: 360) + animationStartDate = nil } } + } } } } #Preview { - RotatingGlowView(color: .purple, - rotationSpeed: 30, - direction: .counterClockwise) - .frame(width: 128, height: 128) + RotatingGlowView( + isAnimating: .constant(true), + color: .purple, + rotationSpeed: 30, + direction: .counterClockwise + ) + .frame(width: 128, height: 128) } diff --git a/Sources/OrbView/Subviews/WavyBlobView.swift b/Sources/OrbView/Subviews/WavyBlobView.swift index 680e3df..c51d51c 100644 --- a/Sources/OrbView/Subviews/WavyBlobView.swift +++ b/Sources/OrbView/Subviews/WavyBlobView.swift @@ -1,7 +1,11 @@ import SwiftUI struct WavyBlobView: View { - @State private var points: [CGPoint] = (0 ..< 6).map { index in + @Binding var isAnimating: Bool + @State private var animationStartDate: Date? + @State private var accumulatedTime: Double = 0 + + @State private var points: [CGPoint] = (0..<6).map { index in let angle = (Double(index) / 6) * 2 * .pi return CGPoint( x: 0.5 + cos(angle) * 0.9, @@ -12,17 +16,28 @@ struct WavyBlobView: View { private let color: Color private let loopDuration: Double - init(color: Color, loopDuration: Double = 1) { + init(isAnimating: Binding, color: Color, loopDuration: Double = 1) { + self._isAnimating = isAnimating self.color = color self.loopDuration = loopDuration } var body: some View { TimelineView(.animation) { timeline in - Canvas { context, size in - let timeNow = timeline.date.timeIntervalSinceReferenceDate - let angle = (timeNow.remainder(dividingBy: loopDuration) / loopDuration) * 2 * .pi + let date = timeline.date + // Compute the angle based on whether the animation is active + let angle: Double = { + if let startDate = animationStartDate { + let elapsedTime = date.timeIntervalSince(startDate) + return ((accumulatedTime + elapsedTime) / loopDuration).truncatingRemainder(dividingBy: 1) + * 2 * .pi + } else { + return (accumulatedTime / loopDuration).truncatingRemainder(dividingBy: 1) * 2 * .pi + } + }() + + Canvas { context, size in var path = Path() let center = CGPoint(x: size.width / 2, y: size.height / 2) let radius = min(size.width, size.height) * 0.45 @@ -78,12 +93,29 @@ struct WavyBlobView: View { context.fill(path, with: .color(color)) } } - .animation(.spring(), value: points) + .onAppear { + if isAnimating { + animationStartDate = Date() + } + } + .onChange(of: isAnimating) { newValue in + if newValue { + // Resume animation + animationStartDate = Date() + } else { + // Pause animation and accumulate elapsed time + if let startDate = animationStartDate { + let elapsedTime = Date().timeIntervalSince(startDate) + accumulatedTime += elapsedTime + animationStartDate = nil + } + } + } } } #Preview { - WavyBlobView(color: .purple) + WavyBlobView(isAnimating: .constant(true) ,color: .purple) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black) }