Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 99 additions & 49 deletions Sources/OrbView/OrbView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -87,6 +110,7 @@ public struct OrbView: View {
.blur(radius: 1)

ParticlesView(
isAnimating: $isAnimating,
color: config.particleColor,
speedRange: 20...30,
sizeRange: 0.2...1,
Expand All @@ -101,52 +125,72 @@ 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)
}
}

private var wavyBlobTwo: some 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)
}
Expand All @@ -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)
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/OrbView/Subviews/ParticlesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ class ParticleScene: SKScene {
}

struct ParticlesView: View {
@Binding var isAnimating: Bool

let color: Color
let speedRange: ClosedRange<Double>
let sizeRange: ClosedRange<CGFloat>
Expand All @@ -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,
Expand Down
85 changes: 60 additions & 25 deletions Sources/OrbView/Subviews/RotatingGlowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool>,
color: Color,
rotationSpeed: Double = 30,
direction: RotationDirection
) {
self._isAnimating = isAnimating
self.color = color
self.rotationSpeed = rotationSpeed
self.direction = direction
Expand All @@ -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)
}
Loading