expo-orb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.eslintrc.js +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +129 -0
  4. package/android/build.gradle +60 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/expo/modules/orb/ExpoOrbModule.kt +79 -0
  7. package/android/src/main/java/expo/modules/orb/ExpoOrbView.kt +105 -0
  8. package/android/src/main/java/expo/modules/orb/OrbConfiguration.kt +30 -0
  9. package/android/src/main/java/expo/modules/orb/OrbSharedState.kt +25 -0
  10. package/android/src/main/java/expo/modules/orb/OrbView.kt +368 -0
  11. package/android/src/main/java/expo/modules/orb/ParticlesView.kt +77 -0
  12. package/android/src/main/java/expo/modules/orb/RotatingGlowView.kt +90 -0
  13. package/android/src/main/java/expo/modules/orb/WavyBlobView.kt +90 -0
  14. package/build/ExpoOrb.types.d.ts +17 -0
  15. package/build/ExpoOrb.types.d.ts.map +1 -0
  16. package/build/ExpoOrb.types.js +2 -0
  17. package/build/ExpoOrb.types.js.map +1 -0
  18. package/build/ExpoOrbModule.d.ts +17 -0
  19. package/build/ExpoOrbModule.d.ts.map +1 -0
  20. package/build/ExpoOrbModule.js +11 -0
  21. package/build/ExpoOrbModule.js.map +1 -0
  22. package/build/ExpoOrbModule.web.d.ts +6 -0
  23. package/build/ExpoOrbModule.web.d.ts.map +1 -0
  24. package/build/ExpoOrbModule.web.js +5 -0
  25. package/build/ExpoOrbModule.web.js.map +1 -0
  26. package/build/ExpoOrbView.d.ts +4 -0
  27. package/build/ExpoOrbView.d.ts.map +1 -0
  28. package/build/ExpoOrbView.js +7 -0
  29. package/build/ExpoOrbView.js.map +1 -0
  30. package/build/ExpoOrbView.web.d.ts +4 -0
  31. package/build/ExpoOrbView.web.d.ts.map +1 -0
  32. package/build/ExpoOrbView.web.js +17 -0
  33. package/build/ExpoOrbView.web.js.map +1 -0
  34. package/build/index.d.ts +4 -0
  35. package/build/index.d.ts.map +1 -0
  36. package/build/index.js +4 -0
  37. package/build/index.js.map +1 -0
  38. package/expo-module.config.json +9 -0
  39. package/ios/ExpoOrb.podspec +30 -0
  40. package/ios/ExpoOrbModule.swift +70 -0
  41. package/ios/ExpoOrbView.swift +105 -0
  42. package/ios/Orb/OrbConfiguration.swift +53 -0
  43. package/ios/Orb/OrbView.swift +367 -0
  44. package/ios/Orb/ParticlesView.swift +156 -0
  45. package/ios/Orb/RealisticShadows.swift +42 -0
  46. package/ios/Orb/RotatingGlowView.swift +62 -0
  47. package/ios/Orb/WavyBlobView.swift +83 -0
  48. package/package.json +46 -0
  49. package/src/ExpoOrb.types.ts +17 -0
  50. package/src/ExpoOrbModule.ts +22 -0
  51. package/src/ExpoOrbModule.web.ts +5 -0
  52. package/src/ExpoOrbView.tsx +11 -0
  53. package/src/ExpoOrbView.web.tsx +26 -0
  54. package/src/index.ts +3 -0
  55. package/tsconfig.json +9 -0
@@ -0,0 +1,367 @@
1
+ //
2
+ // OrbView.swift
3
+ // Prototype-Orb
4
+ //
5
+ // Created by Siddhant Mehta on 2024-11-06.
6
+ //
7
+ import Foundation
8
+ import SwiftUI
9
+
10
+ // ARCHITECTURE NOTE: Shared state for activity animation
11
+ // ========================================================
12
+ // This class is the key to smooth animations when bridging React Native ↔ SwiftUI.
13
+ //
14
+ // THE PROBLEM:
15
+ // When JS updates props frequently (e.g., activity level), each update would normally:
16
+ // 1. Cross the JS-to-native bridge
17
+ // 2. Update an @Published property in SwiftUI
18
+ // 3. Trigger SwiftUI view re-renders
19
+ // 4. Interfere with ongoing TimelineView animations → causes flickering/shaking
20
+ //
21
+ // THE SOLUTION:
22
+ // 1. JS sends target values to native (can be frequent, e.g., every 100-800ms)
23
+ // 2. Native writes to this PLAIN (non-reactive) shared state
24
+ // 3. SwiftUI's TimelineView reads from shared state during its animation loop
25
+ // 4. Interpolation happens inside TimelineView - no external state triggers re-renders
26
+ //
27
+ // RULES FOR SMOOTH ANIMATION:
28
+ // - NEVER use @Published for values that change during animation
29
+ // - NEVER pass animated values as View parameters that change frequently
30
+ // - DO use shared state + TimelineView for all animated properties
31
+ // - DO interpolate inside TimelineView body, not in response to prop changes
32
+ //
33
+ public final class OrbSharedState {
34
+ public static let shared = OrbSharedState()
35
+ public var targetActivity: Double = 0 // Written by native bridge, read by animation loop
36
+ var currentActivity: Double = 0 // Interpolated value, updated each frame
37
+ var lastUpdateTime: Date = Date() // For frame-time-based interpolation
38
+
39
+ // Cumulative phase for breathing - allows speed changes without jumps
40
+ var breathingPhase: Double = 0
41
+ var lastBreathingUpdate: Date = Date()
42
+
43
+ // DEBUG: Set to true to test native-only activity generation (no JS involvement)
44
+ public var useNativeDemoMode: Bool = false
45
+ var lastDemoUpdate: Date = Date()
46
+ }
47
+
48
+ public struct OrbView: View {
49
+ private let config: OrbConfiguration
50
+ private let useSharedActivityState: Bool
51
+
52
+ public init(configuration: OrbConfiguration = OrbConfiguration(), targetActivity: Double? = nil, useSharedActivityState: Bool = false) {
53
+ self.config = configuration
54
+ self.useSharedActivityState = useSharedActivityState || (targetActivity != nil)
55
+ // If targetActivity is passed directly, set it to shared state
56
+ if let activity = targetActivity {
57
+ OrbSharedState.shared.targetActivity = activity
58
+ }
59
+ }
60
+
61
+ public var body: some View {
62
+ TimelineView(.animation) { timeline in
63
+ let elapsedTime = timeline.date.timeIntervalSinceReferenceDate
64
+
65
+ GeometryReader { geometry in
66
+ let size = min(geometry.size.width, geometry.size.height)
67
+
68
+ // Interpolate activity in the same animation loop
69
+ let activity = interpolatedActivity(at: timeline.date)
70
+ let effectiveConfig = activityDerivedConfig(activity: activity)
71
+ let scale = breathingScale(at: timeline.date, config: effectiveConfig)
72
+
73
+ ZStack {
74
+ // Base gradient background layer
75
+ if config.showBackground {
76
+ background
77
+ }
78
+
79
+ // Creates depth with rotating glow effects
80
+ baseDepthGlows(size: size, config: effectiveConfig, elapsedTime: elapsedTime)
81
+
82
+ // Adds organic movement with flowing blob shapes
83
+ if config.showWavyBlobs {
84
+ wavyBlob(elapsedTime: elapsedTime)
85
+ wavyBlobTwo(elapsedTime: elapsedTime)
86
+ }
87
+
88
+ // Adds bright, energetic core glow animations
89
+ if config.showGlowEffects {
90
+ coreGlowEffects(size: size, config: effectiveConfig, elapsedTime: elapsedTime)
91
+ }
92
+
93
+ // Overlays floating particle effects for additional dynamism
94
+ if config.showParticles {
95
+ particleView
96
+ .frame(maxWidth: size, maxHeight: size)
97
+ }
98
+ }
99
+ // Orb outline for depth
100
+ .overlay {
101
+ realisticInnerGlows
102
+ }
103
+ // Masking out all the effects so it forms a perfect circle
104
+ .mask {
105
+ Circle()
106
+ }
107
+ .aspectRatio(1, contentMode: .fit)
108
+ // Adding realistic, layered shadows so its brighter near the core, and softer as it grows outwards
109
+ .modifier(
110
+ RealisticShadowModifier(
111
+ colors: config.showShadow ? config.backgroundColors : [.clear],
112
+ radius: size * 0.08
113
+ )
114
+ )
115
+ .scaleEffect(scale)
116
+ }
117
+ }
118
+ }
119
+
120
+ // Interpolate activity toward target within the TimelineView animation loop
121
+ private func interpolatedActivity(at date: Date) -> Double {
122
+ guard useSharedActivityState else {
123
+ return 0
124
+ }
125
+
126
+ let state = OrbSharedState.shared
127
+
128
+ // DEBUG: Native demo mode - generate activity without JS
129
+ if state.useNativeDemoMode {
130
+ // Update target every ~800ms
131
+ if date.timeIntervalSince(state.lastDemoUpdate) > 0.8 {
132
+ state.lastDemoUpdate = date
133
+ let speaking = Double.random(in: 0...1) > 0.4
134
+ if speaking {
135
+ state.targetActivity = 0.4 + Double.random(in: 0...0.6)
136
+ } else {
137
+ state.targetActivity = Double.random(in: 0...0.15)
138
+ }
139
+ }
140
+ }
141
+
142
+ let target = state.targetActivity
143
+ let dt = date.timeIntervalSince(state.lastUpdateTime)
144
+ state.lastUpdateTime = date
145
+
146
+ // Smooth interpolation factor (adjusted for frame time)
147
+ // Higher factor = faster response to activity changes
148
+ let factor = min(1.0, dt * 6.0) // ~6.0 per second convergence rate
149
+ let next = state.currentActivity + (target - state.currentActivity) * factor
150
+ state.currentActivity = next
151
+
152
+ return next
153
+ }
154
+
155
+ // Compute derived config values from interpolated activity
156
+ private func activityDerivedConfig(activity: Double) -> EffectiveConfig {
157
+ if useSharedActivityState {
158
+ // Activity mode: derive values from interpolated activity
159
+ // IMPORTANT: Rotation speed must stay CONSTANT - changing it causes massive jumps
160
+ // because phase = elapsedTime * speed, and elapsedTime is ~788 million seconds
161
+ let speed = 18.0 // Fixed rotation speed
162
+ // Breathing speed CAN vary because we use cumulative phase (see breathingScale)
163
+ // Faster when speaking for punchy effect
164
+ let breathingSpeed = 0.03 + activity * 0.25 // 0.03 idle (almost none) → 0.28 speaking (punchy)
165
+ // Idle: no breathing, Speaking: full breathing
166
+ let breathingIntensity = max(0, (activity - 0.2)) * 1.25 // Kicks in above 0.2 activity
167
+ // Idle: barely visible glow, Speaking: bright
168
+ let coreGlowIntensity = 0.08 + activity * 1.8 // 0.08 idle → 1.88 speaking
169
+ return EffectiveConfig(
170
+ speed: speed,
171
+ breathingIntensity: breathingIntensity,
172
+ breathingSpeed: breathingSpeed,
173
+ coreGlowIntensity: coreGlowIntensity,
174
+ glowColor: config.glowColor
175
+ )
176
+ } else {
177
+ // Static mode: use config values directly
178
+ return EffectiveConfig(
179
+ speed: config.speed,
180
+ breathingIntensity: config.breathingIntensity,
181
+ breathingSpeed: config.breathingSpeed,
182
+ coreGlowIntensity: config.coreGlowIntensity,
183
+ glowColor: config.glowColor
184
+ )
185
+ }
186
+ }
187
+
188
+ private struct EffectiveConfig {
189
+ let speed: Double
190
+ let breathingIntensity: Double
191
+ let breathingSpeed: Double
192
+ let coreGlowIntensity: Double
193
+ let glowColor: Color
194
+ }
195
+
196
+ private var background: some View {
197
+ LinearGradient(colors: config.backgroundColors,
198
+ startPoint: .bottom,
199
+ endPoint: .top)
200
+ }
201
+
202
+ private var orbOutlineColor: LinearGradient {
203
+ LinearGradient(colors: [.white, .clear],
204
+ startPoint: .bottom,
205
+ endPoint: .top)
206
+ }
207
+
208
+ private var particleView: some View {
209
+ // Added multiple particle effects since the blurring amounts are different
210
+ ZStack {
211
+ ParticlesView(
212
+ color: config.particleColor,
213
+ speedRange: 10...20,
214
+ sizeRange: 0.5...1,
215
+ particleCount: 10,
216
+ opacityRange: 0...0.3
217
+ )
218
+ .blur(radius: 1)
219
+
220
+ ParticlesView(
221
+ color: config.particleColor,
222
+ speedRange: 20...30,
223
+ sizeRange: 0.2...1,
224
+ particleCount: 10,
225
+ opacityRange: 0.3...0.8
226
+ )
227
+ }
228
+ .blendMode(.plusLighter)
229
+ }
230
+
231
+ private func wavyBlob(elapsedTime: Double) -> some View {
232
+ GeometryReader { geometry in
233
+ let size = min(geometry.size.width, geometry.size.height)
234
+ // Fixed calm speed for stable blob animation
235
+ let blobSpeed: Double = 20
236
+
237
+ RotatingGlowView(color: .white.opacity(0.75),
238
+ rotationSpeed: blobSpeed * 1.5,
239
+ direction: .clockwise,
240
+ elapsedTime: elapsedTime)
241
+ .mask {
242
+ WavyBlobView(color: .white, loopDuration: 60 / blobSpeed * 1.75)
243
+ .frame(maxWidth: size * 1.875)
244
+ .offset(x: 0, y: size * 0.31)
245
+ }
246
+ .blur(radius: 1)
247
+ .blendMode(.plusLighter)
248
+ }
249
+ }
250
+
251
+ private func wavyBlobTwo(elapsedTime: Double) -> some View {
252
+ GeometryReader { geometry in
253
+ let size = min(geometry.size.width, geometry.size.height)
254
+ // Fixed calm speed for stable blob animation
255
+ let blobSpeed: Double = 20
256
+
257
+ RotatingGlowView(color: .white,
258
+ rotationSpeed: blobSpeed * 0.75,
259
+ direction: .counterClockwise,
260
+ elapsedTime: elapsedTime)
261
+ .mask {
262
+ WavyBlobView(color: .white, loopDuration: 60 / blobSpeed * 2.25)
263
+ .frame(maxWidth: size * 1.25)
264
+ .rotationEffect(.degrees(90))
265
+ .offset(x: 0, y: size * -0.31)
266
+ }
267
+ .opacity(0.5)
268
+ .blur(radius: 1)
269
+ .blendMode(.plusLighter)
270
+ }
271
+ }
272
+
273
+ private func coreGlowEffects(size: CGFloat, config effectiveConfig: EffectiveConfig, elapsedTime: Double) -> some View {
274
+ ZStack {
275
+ // Slower rotation multipliers for more organic, less frantic movement
276
+ RotatingGlowView(color: effectiveConfig.glowColor,
277
+ rotationSpeed: effectiveConfig.speed * 1.2,
278
+ direction: .clockwise,
279
+ elapsedTime: elapsedTime)
280
+ .blur(radius: size * 0.08)
281
+ .opacity(effectiveConfig.coreGlowIntensity)
282
+
283
+ RotatingGlowView(color: effectiveConfig.glowColor,
284
+ rotationSpeed: effectiveConfig.speed * 0.9,
285
+ direction: .clockwise,
286
+ elapsedTime: elapsedTime)
287
+ .blur(radius: size * 0.06)
288
+ .opacity(effectiveConfig.coreGlowIntensity)
289
+ .blendMode(.plusLighter)
290
+ }
291
+ .padding(size * 0.08)
292
+ }
293
+
294
+ // New combined function replacing outerGlow and outerRing
295
+ private func baseDepthGlows(size: CGFloat, config effectiveConfig: EffectiveConfig, elapsedTime: Double) -> some View {
296
+ ZStack {
297
+ // Outer glow (previously outerGlow function)
298
+ RotatingGlowView(color: effectiveConfig.glowColor,
299
+ rotationSpeed: effectiveConfig.speed * 0.75,
300
+ direction: .counterClockwise,
301
+ elapsedTime: elapsedTime)
302
+ .padding(size * 0.03)
303
+ .blur(radius: size * 0.06)
304
+ .rotationEffect(.degrees(180))
305
+ .blendMode(.destinationOver)
306
+
307
+ // Outer ring (previously outerRing function)
308
+ RotatingGlowView(color: effectiveConfig.glowColor.opacity(0.5),
309
+ rotationSpeed: effectiveConfig.speed * 0.25,
310
+ direction: .clockwise,
311
+ elapsedTime: elapsedTime)
312
+ .frame(maxWidth: size * 0.94)
313
+ .rotationEffect(.degrees(180))
314
+ .padding(8)
315
+ .blur(radius: size * 0.032)
316
+ }
317
+ }
318
+
319
+ private var realisticInnerGlows: some View {
320
+ ZStack {
321
+ // Outer stroke with heavy blur
322
+ Circle()
323
+ .stroke(orbOutlineColor, lineWidth: 8)
324
+ .blur(radius: 32)
325
+ .blendMode(.plusLighter)
326
+
327
+ // Inner stroke with light blur
328
+ Circle()
329
+ .stroke(orbOutlineColor, lineWidth: 4)
330
+ .blur(radius: 12)
331
+ .blendMode(.plusLighter)
332
+
333
+ Circle()
334
+ .stroke(orbOutlineColor, lineWidth: 1)
335
+ .blur(radius: 4)
336
+ .blendMode(.plusLighter)
337
+ }
338
+ .padding(1)
339
+ }
340
+
341
+ private func breathingScale(at date: Date, config effectiveConfig: EffectiveConfig) -> CGFloat {
342
+ let intensity = max(0, min(1, effectiveConfig.breathingIntensity))
343
+ if intensity == 0 {
344
+ return 1
345
+ }
346
+
347
+ // Use CUMULATIVE phase - this allows speed to change without jumps
348
+ // Each frame we add (deltaTime * speed) to the phase, rather than (absoluteTime * speed)
349
+ let state = OrbSharedState.shared
350
+ let dt = date.timeIntervalSince(state.lastBreathingUpdate)
351
+ state.lastBreathingUpdate = date
352
+
353
+ let speed = max(0.01, effectiveConfig.breathingSpeed)
354
+ state.breathingPhase += dt * speed * 2 * .pi
355
+
356
+ // Punchy wave shape - sharper peaks to mimic speech rhythm
357
+ let rawWave = sin(state.breathingPhase)
358
+ // Apply power curve: sqrt for positive (sharp attack), squared for negative (slower return)
359
+ let wave = rawWave >= 0
360
+ ? pow(rawWave, 0.6) // Sharp attack (expand fast)
361
+ : -pow(abs(rawWave), 1.4) // Slower return (contract slower)
362
+
363
+ // Amplitude ~0.17 at full intensity = 1/3 total range (0.83 to 1.17)
364
+ let amplitude = CGFloat(intensity) * 0.17
365
+ return 1 + amplitude * CGFloat(wave) // Goes below AND above 1.0
366
+ }
367
+ }
@@ -0,0 +1,156 @@
1
+ //
2
+ // Particles.swift
3
+ // Prototype-Orb
4
+ //
5
+ // Created by Siddhant Mehta on 2024-11-06.
6
+ //
7
+ import SwiftUI
8
+ import SpriteKit
9
+ import UIKit
10
+
11
+ class ParticleScene: SKScene {
12
+ let color: UIColor
13
+ let speedRange: ClosedRange<Double>
14
+ let sizeRange: ClosedRange<CGFloat>
15
+ let particleCount: Int
16
+ let opacityRange: ClosedRange<Double>
17
+
18
+ init(
19
+ size: CGSize,
20
+ color: UIColor,
21
+ speedRange: ClosedRange<Double>,
22
+ sizeRange: ClosedRange<CGFloat>,
23
+ particleCount: Int,
24
+ opacityRange: ClosedRange<Double>
25
+ ) {
26
+ self.color = color
27
+ self.speedRange = speedRange
28
+ self.sizeRange = sizeRange
29
+ self.particleCount = particleCount
30
+ self.opacityRange = opacityRange
31
+ super.init(size: size)
32
+
33
+ backgroundColor = .clear
34
+ setupParticleEmitter()
35
+ }
36
+
37
+ required init?(coder aDecoder: NSCoder) {
38
+ fatalError("init(coder:) has not been implemented")
39
+ }
40
+
41
+ private func setupParticleEmitter() {
42
+ let emitter = SKEmitterNode()
43
+
44
+ // Create a white particle texture
45
+ emitter.particleTexture = createParticleTexture()
46
+
47
+ // Update color properties
48
+ emitter.particleColorSequence = nil
49
+ emitter.particleColor = color
50
+ emitter.particleColorBlendFactor = 1.0
51
+
52
+ // Basic emitter properties
53
+ emitter.particleSpeed = CGFloat(speedRange.lowerBound)
54
+ emitter.particleSpeedRange = CGFloat(speedRange.upperBound - speedRange.lowerBound)
55
+ emitter.particleScale = sizeRange.lowerBound
56
+ emitter.particleScaleRange = sizeRange.upperBound - sizeRange.lowerBound
57
+
58
+ // Alpha and fade properties
59
+ emitter.particleAlpha = 0 // Start invisible
60
+ emitter.particleAlphaSpeed = CGFloat(opacityRange.upperBound) / 0.5 // Fade in over 0.5 seconds
61
+ emitter.particleAlphaRange = CGFloat(opacityRange.upperBound - opacityRange.lowerBound)
62
+
63
+ // Create alpha sequence for fade in/out
64
+ let alphaSequence = SKKeyframeSequence(keyframeValues: [
65
+ 0, // Start invisible
66
+ Double.random(in: opacityRange), // Fade in to max opacity
67
+ Double.random(in: opacityRange), // Stay at max opacity
68
+ Double.random(in: opacityRange) // Fade to min opacity
69
+ ], times: [
70
+ 0, // At start
71
+ 0.2, // Reach max at 20% of lifetime
72
+ 0.8, // Stay at max until 80% of lifetime
73
+ 1.0 // Fade to min by end
74
+ ])
75
+ emitter.particleAlphaSequence = alphaSequence
76
+
77
+ // Create scale sequence for grow/shrink animation
78
+ let scaleSequence = SKKeyframeSequence(keyframeValues: [
79
+ sizeRange.lowerBound * 0.7, // Start at half min size
80
+ sizeRange.upperBound * 0.9, // Grow to max size
81
+ sizeRange.upperBound, // Stay at max
82
+ sizeRange.lowerBound * 0.8 // Shrink back to half min size
83
+ ], times: [
84
+ 0, // At start
85
+ 0.4, // Reach max at 20% of lifetime
86
+ 0.7, // Stay at max until 80% of lifetime
87
+ 1.0 // Shrink by end
88
+ ])
89
+ emitter.particleScaleSequence = scaleSequence
90
+
91
+ emitter.particleBlendMode = .add
92
+
93
+ // Center the emitter and set emission area to full size
94
+ emitter.position = CGPoint(x: size.width/2, y: size.height/2)
95
+ emitter.particlePositionRange = CGVector(dx: size.width, dy: size.height)
96
+
97
+ // Particle birth and lifetime
98
+ emitter.particleBirthRate = CGFloat(particleCount) / 2.0
99
+ emitter.numParticlesToEmit = 0
100
+ emitter.particleLifetime = 2.0
101
+ emitter.particleLifetimeRange = 1.0
102
+
103
+ // Update movement properties
104
+ emitter.emissionAngle = CGFloat.pi / 2 // Point upwards (90 degrees)
105
+ emitter.emissionAngleRange = CGFloat.pi / 6 // Allow 30 degree variation each way
106
+
107
+ // Add some sideways drift
108
+ emitter.xAcceleration = 0 // No horizontal acceleration
109
+ emitter.yAcceleration = 20 // Slight upward acceleration
110
+
111
+ addChild(emitter)
112
+ }
113
+
114
+ private func createParticleTexture() -> SKTexture {
115
+ let size = CGSize(width: 8, height: 8) // Smaller size for better performance
116
+ let renderer = UIGraphicsImageRenderer(size: size)
117
+
118
+ let image = renderer.image { context in
119
+ // Simple filled white circle
120
+ UIColor.white.setFill()
121
+ let circlePath = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size))
122
+ circlePath.fill()
123
+ }
124
+
125
+ return SKTexture(image: image)
126
+ }
127
+ }
128
+
129
+ struct ParticlesView: View {
130
+ let color: Color
131
+ let speedRange: ClosedRange<Double>
132
+ let sizeRange: ClosedRange<CGFloat>
133
+ let particleCount: Int
134
+ let opacityRange: ClosedRange<Double>
135
+
136
+ var scene: SKScene {
137
+ let scene = ParticleScene(
138
+ size: CGSize(width: 300, height: 300), // Use fixed size
139
+ color: UIColor(color),
140
+ speedRange: speedRange,
141
+ sizeRange: sizeRange,
142
+ particleCount: particleCount,
143
+ opacityRange: opacityRange
144
+ )
145
+ scene.scaleMode = .aspectFit
146
+ return scene
147
+ }
148
+
149
+ var body: some View {
150
+ GeometryReader { geometry in
151
+ SpriteView(scene: scene, options: [.allowsTransparency])
152
+ .frame(width: geometry.size.width, height: geometry.size.height)
153
+ .ignoresSafeArea()
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,42 @@
1
+ //
2
+ // RealisticShadows.swift
3
+ // Prototype-Orb
4
+ //
5
+ // Created by Siddhant Mehta on 2024-11-06.
6
+ //
7
+ import SwiftUI
8
+
9
+ struct RealisticShadowModifier: ViewModifier {
10
+ let colors: [Color]
11
+ let radius: CGFloat
12
+
13
+ func body(content: Content) -> some View {
14
+ content
15
+ .background {
16
+ Circle()
17
+ .fill(
18
+ LinearGradient(
19
+ colors: colors,
20
+ startPoint: .bottom,
21
+ endPoint: .top
22
+ )
23
+ )
24
+ .blur(radius: radius * 0.75)
25
+ .opacity(0.5)
26
+ .offset(y: radius * 0.5)
27
+ }
28
+ .background {
29
+ Circle()
30
+ .fill(
31
+ LinearGradient(
32
+ colors: colors,
33
+ startPoint: .bottom,
34
+ endPoint: .top
35
+ )
36
+ )
37
+ .blur(radius: radius * 3)
38
+ .opacity(0.3)
39
+ .offset(y: radius * 0.75)
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,62 @@
1
+ //
2
+ // BackgroundView.swift
3
+ // Prototype-Orb
4
+ //
5
+ // Created by Siddhant Mehta on 2024-11-06.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ enum RotationDirection {
11
+ case clockwise
12
+ case counterClockwise
13
+
14
+ var multiplier: Double {
15
+ switch self {
16
+ case .clockwise: return 1
17
+ case .counterClockwise: return -1
18
+ }
19
+ }
20
+ }
21
+
22
+ struct RotatingGlowView: View {
23
+ private let color: Color
24
+ private let rotationSpeed: Double
25
+ private let direction: RotationDirection
26
+ private let elapsedTime: Double
27
+
28
+ init(color: Color,
29
+ rotationSpeed: Double = 30,
30
+ direction: RotationDirection,
31
+ elapsedTime: Double = 0)
32
+ {
33
+ self.color = color
34
+ self.rotationSpeed = rotationSpeed
35
+ self.direction = direction
36
+ self.elapsedTime = elapsedTime
37
+ }
38
+
39
+ var body: some View {
40
+ GeometryReader { geometry in
41
+ let size = min(geometry.size.width, geometry.size.height)
42
+ let safeSpeed = max(0.01, rotationSpeed)
43
+ let rotation = elapsedTime * safeSpeed * direction.multiplier
44
+
45
+ Circle()
46
+ .fill(color)
47
+ .mask {
48
+ ZStack {
49
+ Circle()
50
+ .frame(width: size, height: size)
51
+ .blur(radius: size * 0.16)
52
+ Circle()
53
+ .frame(width: size * 1.31, height: size * 1.31)
54
+ .offset(y: size * 0.31)
55
+ .blur(radius: size * 0.16)
56
+ .blendMode(.destinationOut)
57
+ }
58
+ }
59
+ .rotationEffect(.degrees(rotation))
60
+ }
61
+ }
62
+ }