expo-orb 0.1.0 → 0.2.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 (39) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/expo/modules/breathing/BreathingConfiguration.kt +20 -0
  3. package/android/src/main/java/expo/modules/breathing/BreathingExerciseView.kt +247 -0
  4. package/android/src/main/java/expo/modules/breathing/BreathingSharedState.kt +104 -0
  5. package/android/src/main/java/expo/modules/breathing/BreathingTextCue.kt +41 -0
  6. package/android/src/main/java/expo/modules/breathing/ExpoBreathingExerciseModule.kt +156 -0
  7. package/android/src/main/java/expo/modules/breathing/ExpoBreathingExerciseView.kt +123 -0
  8. package/android/src/main/java/expo/modules/breathing/MorphingBlobView.kt +128 -0
  9. package/android/src/main/java/expo/modules/breathing/ProgressRingView.kt +50 -0
  10. package/build/ExpoBreathingExercise.types.d.ts +48 -0
  11. package/build/ExpoBreathingExercise.types.d.ts.map +1 -0
  12. package/build/ExpoBreathingExercise.types.js +2 -0
  13. package/build/ExpoBreathingExercise.types.js.map +1 -0
  14. package/build/ExpoBreathingExerciseModule.d.ts +16 -0
  15. package/build/ExpoBreathingExerciseModule.d.ts.map +1 -0
  16. package/build/ExpoBreathingExerciseModule.js +52 -0
  17. package/build/ExpoBreathingExerciseModule.js.map +1 -0
  18. package/build/ExpoBreathingExerciseView.d.ts +4 -0
  19. package/build/ExpoBreathingExerciseView.d.ts.map +1 -0
  20. package/build/ExpoBreathingExerciseView.js +7 -0
  21. package/build/ExpoBreathingExerciseView.js.map +1 -0
  22. package/build/index.d.ts +3 -0
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +3 -0
  25. package/build/index.js.map +1 -1
  26. package/expo-module.config.json +2 -2
  27. package/ios/Breathing/BreathingConfiguration.swift +57 -0
  28. package/ios/Breathing/BreathingExerciseView.swift +451 -0
  29. package/ios/Breathing/BreathingSharedState.swift +84 -0
  30. package/ios/Breathing/BreathingTextCue.swift +14 -0
  31. package/ios/Breathing/MorphingBlobView.swift +242 -0
  32. package/ios/Breathing/ProgressRingView.swift +27 -0
  33. package/ios/ExpoBreathingExerciseModule.swift +182 -0
  34. package/ios/ExpoBreathingExerciseView.swift +124 -0
  35. package/package.json +8 -5
  36. package/src/ExpoBreathingExercise.types.ts +50 -0
  37. package/src/ExpoBreathingExerciseModule.ts +67 -0
  38. package/src/ExpoBreathingExerciseView.tsx +11 -0
  39. package/src/index.ts +11 -0
@@ -0,0 +1,242 @@
1
+ import SwiftUI
2
+
3
+ struct MorphingBlobView: View {
4
+ let baseRadius: CGFloat
5
+ let pointCount: Int
6
+ let offsets: [Double]
7
+ let colors: [Color]
8
+ let innerColor: Color
9
+ let showInnerBlob: Bool
10
+ var elapsedTime: Double = 0
11
+
12
+ var body: some View {
13
+ GeometryReader { geometry in
14
+ let size = min(geometry.size.width, geometry.size.height)
15
+ let center = CGPoint(x: size / 2, y: size / 2)
16
+ let effectiveRadius = baseRadius * size / 2
17
+
18
+ ZStack {
19
+ // Main blob with linear gradient fill (like OrbView)
20
+ MorphingBlobShape(
21
+ center: center,
22
+ baseRadius: effectiveRadius,
23
+ pointCount: pointCount,
24
+ offsets: offsets
25
+ )
26
+ .fill(
27
+ LinearGradient(
28
+ colors: colors,
29
+ startPoint: .bottom,
30
+ endPoint: .top
31
+ )
32
+ )
33
+
34
+ // Inner glow overlay (like OrbView's realisticInnerGlows)
35
+ MorphingBlobShape(
36
+ center: center,
37
+ baseRadius: effectiveRadius,
38
+ pointCount: pointCount,
39
+ offsets: offsets
40
+ )
41
+ .stroke(
42
+ LinearGradient(
43
+ colors: [.white.opacity(0.6), .clear],
44
+ startPoint: .bottom,
45
+ endPoint: .top
46
+ ),
47
+ lineWidth: 3
48
+ )
49
+ .blur(radius: 4)
50
+ .blendMode(.plusLighter)
51
+
52
+ // Inner animated blob like OrbView's WavyBlobView
53
+ if showInnerBlob {
54
+ InnerWavyBlob(
55
+ center: center,
56
+ baseRadius: effectiveRadius * 0.55,
57
+ color: innerColor,
58
+ elapsedTime: elapsedTime
59
+ )
60
+ .blur(radius: 10)
61
+ .blendMode(.plusLighter)
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // Inner blob with time-based animation like WavyBlobView
69
+ struct InnerWavyBlob: View {
70
+ let center: CGPoint
71
+ let baseRadius: CGFloat
72
+ let color: Color
73
+ let elapsedTime: Double
74
+
75
+ private let pointCount = 6
76
+ private let loopDuration: Double = 4.0
77
+
78
+ var body: some View {
79
+ Canvas { context, size in
80
+ let angle = (elapsedTime.truncatingRemainder(dividingBy: loopDuration) / loopDuration) * 2 * .pi
81
+
82
+ var path = Path()
83
+
84
+ // Generate points with time-based smooth movement
85
+ var points: [CGPoint] = []
86
+ for i in 0..<pointCount {
87
+ let baseAngle = (Double(i) / Double(pointCount)) * 2 * .pi
88
+ let phaseOffset = Double(i) * .pi / 3
89
+
90
+ // Smooth oscillating offsets
91
+ let radiusOffset = sin(angle * 2 + phaseOffset) * 0.15 + cos(angle * 1.5 + phaseOffset * 0.7) * 0.1
92
+ let angleOffset = cos(angle + phaseOffset) * 0.1
93
+
94
+ let r = baseRadius * (1.0 + radiusOffset)
95
+ let a = baseAngle + angleOffset
96
+
97
+ points.append(CGPoint(
98
+ x: center.x + cos(a) * r,
99
+ y: center.y + sin(a) * r
100
+ ))
101
+ }
102
+
103
+ // Start path
104
+ path.move(to: points[0])
105
+
106
+ // Create smooth curves between points
107
+ let handleLength = baseRadius * 0.5
108
+
109
+ for i in 0..<pointCount {
110
+ let current = points[i]
111
+ let next = points[(i + 1) % pointCount]
112
+
113
+ let currentAngle = atan2(current.y - center.y, current.x - center.x)
114
+ let nextAngle = atan2(next.y - center.y, next.x - center.x)
115
+
116
+ let control1 = CGPoint(
117
+ x: current.x + cos(currentAngle + .pi / 2) * handleLength,
118
+ y: current.y + sin(currentAngle + .pi / 2) * handleLength
119
+ )
120
+
121
+ let control2 = CGPoint(
122
+ x: next.x + cos(nextAngle - .pi / 2) * handleLength,
123
+ y: next.y + sin(nextAngle - .pi / 2) * handleLength
124
+ )
125
+
126
+ path.addCurve(to: next, control1: control1, control2: control2)
127
+ }
128
+
129
+ path.closeSubpath()
130
+ context.fill(path, with: .color(color))
131
+ }
132
+ }
133
+ }
134
+
135
+ struct MorphingBlobShape: Shape {
136
+ var center: CGPoint
137
+ var baseRadius: CGFloat
138
+ var pointCount: Int
139
+ var offsets: [Double]
140
+
141
+ var animatableData: AnimatableVector {
142
+ get { AnimatableVector(values: offsets) }
143
+ set { offsets = newValue.values }
144
+ }
145
+
146
+ func path(in rect: CGRect) -> Path {
147
+ var path = Path()
148
+
149
+ guard pointCount >= 3 else { return path }
150
+
151
+ // Generate points around the circle with offsets
152
+ var points: [CGPoint] = []
153
+ for i in 0..<pointCount {
154
+ let angle = (Double(i) / Double(pointCount)) * 2 * .pi - .pi / 2
155
+ let offset = i < offsets.count ? offsets[i] : 0
156
+ let radius = baseRadius * (1.0 + offset)
157
+
158
+ points.append(CGPoint(
159
+ x: center.x + CGFloat(cos(angle)) * radius,
160
+ y: center.y + CGFloat(sin(angle)) * radius
161
+ ))
162
+ }
163
+
164
+ // Tangent coefficient for smooth cubic bezier curves
165
+ let tangentCoeff = CGFloat((4.0 / 3.0) * tan(.pi / (2.0 * Double(pointCount))))
166
+
167
+ // Start at first point
168
+ path.move(to: points[0])
169
+
170
+ // Draw cubic bezier curves between each pair of points
171
+ for i in 0..<pointCount {
172
+ let current = points[i]
173
+ let next = points[(i + 1) % pointCount]
174
+
175
+ let currentAngle = (Double(i) / Double(pointCount)) * 2 * .pi - .pi / 2
176
+ let nextAngle = (Double((i + 1) % pointCount) / Double(pointCount)) * 2 * .pi - .pi / 2
177
+
178
+ let currentOffset = i < offsets.count ? offsets[i] : 0
179
+ let currentRadius = baseRadius * CGFloat(1.0 + currentOffset)
180
+ let currentTangentLength = currentRadius * tangentCoeff
181
+
182
+ let nextOffset = (i + 1) % pointCount < offsets.count ? offsets[(i + 1) % pointCount] : 0
183
+ let nextRadius = baseRadius * CGFloat(1.0 + nextOffset)
184
+ let nextTangentLength = nextRadius * tangentCoeff
185
+
186
+ let control1 = CGPoint(
187
+ x: current.x + CGFloat(cos(currentAngle + .pi / 2)) * currentTangentLength,
188
+ y: current.y + CGFloat(sin(currentAngle + .pi / 2)) * currentTangentLength
189
+ )
190
+
191
+ let control2 = CGPoint(
192
+ x: next.x + CGFloat(cos(nextAngle - .pi / 2)) * nextTangentLength,
193
+ y: next.y + CGFloat(sin(nextAngle - .pi / 2)) * nextTangentLength
194
+ )
195
+
196
+ path.addCurve(to: next, control1: control1, control2: control2)
197
+ }
198
+
199
+ path.closeSubpath()
200
+ return path
201
+ }
202
+ }
203
+
204
+ struct AnimatableVector: VectorArithmetic {
205
+ var values: [Double]
206
+
207
+ static var zero: AnimatableVector {
208
+ AnimatableVector(values: [])
209
+ }
210
+
211
+ static func + (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
212
+ let maxCount = max(lhs.values.count, rhs.values.count)
213
+ var result = [Double](repeating: 0, count: maxCount)
214
+ for i in 0..<maxCount {
215
+ let l = i < lhs.values.count ? lhs.values[i] : 0
216
+ let r = i < rhs.values.count ? rhs.values[i] : 0
217
+ result[i] = l + r
218
+ }
219
+ return AnimatableVector(values: result)
220
+ }
221
+
222
+ static func - (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
223
+ let maxCount = max(lhs.values.count, rhs.values.count)
224
+ var result = [Double](repeating: 0, count: maxCount)
225
+ for i in 0..<maxCount {
226
+ let l = i < lhs.values.count ? lhs.values[i] : 0
227
+ let r = i < rhs.values.count ? rhs.values[i] : 0
228
+ result[i] = l - r
229
+ }
230
+ return AnimatableVector(values: result)
231
+ }
232
+
233
+ mutating func scale(by rhs: Double) {
234
+ for i in 0..<values.count {
235
+ values[i] *= rhs
236
+ }
237
+ }
238
+
239
+ var magnitudeSquared: Double {
240
+ values.reduce(0) { $0 + $1 * $1 }
241
+ }
242
+ }
@@ -0,0 +1,27 @@
1
+ import SwiftUI
2
+
3
+ struct ProgressRingView: View {
4
+ let progress: Double
5
+ let color: Color
6
+ let lineWidth: CGFloat
7
+ let size: CGFloat
8
+
9
+ var body: some View {
10
+ ZStack {
11
+ // Background ring
12
+ Circle()
13
+ .stroke(color.opacity(0.2), lineWidth: lineWidth)
14
+ .frame(width: size, height: size)
15
+
16
+ // Progress arc
17
+ Circle()
18
+ .trim(from: 0, to: CGFloat(min(max(progress, 0), 1)))
19
+ .stroke(
20
+ color,
21
+ style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
22
+ )
23
+ .frame(width: size, height: size)
24
+ .rotationEffect(.degrees(-90))
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,182 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ public class ExpoBreathingExerciseModule: Module {
5
+ public func definition() -> ModuleDefinition {
6
+ Name("ExpoBreathingExercise")
7
+
8
+ Events("onPhaseChange", "onExerciseComplete")
9
+
10
+ Function("startBreathingExercise") { (pattern: [String: Any]) in
11
+ DispatchQueue.main.async {
12
+ self.startExercise(pattern: pattern)
13
+ }
14
+ }
15
+
16
+ Function("stopBreathingExercise") {
17
+ DispatchQueue.main.async {
18
+ BreathingSharedState.shared.reset()
19
+ }
20
+ }
21
+
22
+ Function("pauseBreathingExercise") {
23
+ DispatchQueue.main.async {
24
+ let state = BreathingSharedState.shared
25
+ if state.state == .running {
26
+ state.state = .paused
27
+ state.pauseTime = Date()
28
+ }
29
+ }
30
+ }
31
+
32
+ Function("resumeBreathingExercise") {
33
+ DispatchQueue.main.async {
34
+ let state = BreathingSharedState.shared
35
+ if state.state == .paused, let pauseTime = state.pauseTime {
36
+ let pauseDuration = Date().timeIntervalSince(pauseTime)
37
+ state.phaseStartTime = state.phaseStartTime.addingTimeInterval(pauseDuration)
38
+ state.exerciseStartTime = state.exerciseStartTime.addingTimeInterval(pauseDuration)
39
+ state.state = .running
40
+ state.pauseTime = nil
41
+ }
42
+ }
43
+ }
44
+
45
+ View(ExpoBreathingExerciseView.self) {
46
+ Events("onPhaseChange", "onExerciseComplete")
47
+
48
+ Prop("blobColors") { (view: ExpoBreathingExerciseView, colors: [UIColor]) in
49
+ view.updateProps { $0.blobColors = colors }
50
+ }
51
+
52
+ Prop("innerBlobColor") { (view: ExpoBreathingExerciseView, color: UIColor) in
53
+ view.updateProps { $0.innerBlobColor = color }
54
+ }
55
+
56
+ Prop("glowColor") { (view: ExpoBreathingExerciseView, color: UIColor) in
57
+ view.updateProps { $0.glowColor = color }
58
+ }
59
+
60
+ Prop("particleColor") { (view: ExpoBreathingExerciseView, color: UIColor) in
61
+ view.updateProps { $0.particleColor = color }
62
+ }
63
+
64
+ Prop("progressRingColor") { (view: ExpoBreathingExerciseView, color: UIColor) in
65
+ view.updateProps { $0.progressRingColor = color }
66
+ }
67
+
68
+ Prop("textColor") { (view: ExpoBreathingExerciseView, color: UIColor) in
69
+ view.updateProps { $0.textColor = color }
70
+ }
71
+
72
+ Prop("showProgressRing") { (view: ExpoBreathingExerciseView, value: Bool) in
73
+ view.updateProps { $0.showProgressRing = value }
74
+ }
75
+
76
+ Prop("showTextCue") { (view: ExpoBreathingExerciseView, value: Bool) in
77
+ view.updateProps { $0.showTextCue = value }
78
+ }
79
+
80
+ Prop("showInnerBlob") { (view: ExpoBreathingExerciseView, value: Bool) in
81
+ view.updateProps { $0.showInnerBlob = value }
82
+ }
83
+
84
+ Prop("showShadow") { (view: ExpoBreathingExerciseView, value: Bool) in
85
+ view.updateProps { $0.showShadow = value }
86
+ }
87
+
88
+ Prop("showParticles") { (view: ExpoBreathingExerciseView, value: Bool) in
89
+ view.updateProps { $0.showParticles = value }
90
+ }
91
+
92
+ Prop("showWavyBlobs") { (view: ExpoBreathingExerciseView, value: Bool) in
93
+ view.updateProps { $0.showWavyBlobs = value }
94
+ }
95
+
96
+ Prop("showGlowEffects") { (view: ExpoBreathingExerciseView, value: Bool) in
97
+ view.updateProps { $0.showGlowEffects = value }
98
+ }
99
+
100
+ Prop("pointCount") { (view: ExpoBreathingExerciseView, value: Int) in
101
+ view.updateProps { $0.pointCount = value }
102
+ }
103
+
104
+ Prop("wobbleIntensity") { (view: ExpoBreathingExerciseView, value: Double) in
105
+ view.updateProps { $0.wobbleIntensity = value }
106
+ }
107
+ }
108
+ }
109
+
110
+ private func startExercise(pattern: [String: Any]) {
111
+ let state = BreathingSharedState.shared
112
+ state.reset()
113
+
114
+ // Parse phases
115
+ guard let phasesArray = pattern["phases"] as? [[String: Any]] else {
116
+ return
117
+ }
118
+
119
+ var phases: [BreathPhaseConfig] = []
120
+ for phaseDict in phasesArray {
121
+ guard let phaseString = phaseDict["phase"] as? String,
122
+ let duration = phaseDict["duration"] as? Double,
123
+ let targetScale = phaseDict["targetScale"] as? Double,
124
+ let label = phaseDict["label"] as? String else {
125
+ continue
126
+ }
127
+
128
+ let phase: BreathPhase
129
+ switch phaseString {
130
+ case "inhale": phase = .inhale
131
+ case "holdIn": phase = .holdIn
132
+ case "exhale": phase = .exhale
133
+ case "holdOut": phase = .holdOut
134
+ default: continue
135
+ }
136
+
137
+ phases.append(BreathPhaseConfig(
138
+ phase: phase,
139
+ duration: duration / 1000.0, // Convert ms to seconds
140
+ targetScale: targetScale,
141
+ label: label
142
+ ))
143
+ }
144
+
145
+ guard !phases.isEmpty else { return }
146
+
147
+ // Parse cycles
148
+ if let cycles = pattern["cycles"] as? Int {
149
+ state.totalCycles = cycles
150
+ } else {
151
+ state.totalCycles = nil
152
+ }
153
+
154
+ // Setup state
155
+ state.phases = phases
156
+ state.currentPhaseIndex = 0
157
+ state.currentCycle = 0
158
+ state.phaseStartTime = Date()
159
+ state.exerciseStartTime = Date()
160
+
161
+ // Set initial phase values
162
+ let firstPhase = phases[0]
163
+ state.currentPhase = firstPhase.phase
164
+ state.currentLabel = firstPhase.label
165
+ state.startScale = 1.0 // Start scale for first phase
166
+ state.targetScale = firstPhase.targetScale
167
+ state.currentScale = 1.0 // Start at base scale
168
+ state.phaseProgress = 0
169
+
170
+ // Set initial wobble intensity
171
+ switch firstPhase.phase {
172
+ case .inhale, .exhale:
173
+ state.wobbleIntensity = 1.0
174
+ case .holdIn, .holdOut:
175
+ state.wobbleIntensity = 0.3
176
+ case .idle:
177
+ state.wobbleIntensity = 0.5
178
+ }
179
+
180
+ state.state = .running
181
+ }
182
+ }
@@ -0,0 +1,124 @@
1
+ import ExpoModulesCore
2
+ import SwiftUI
3
+
4
+ private final class BreathingConfigurationModel: ObservableObject {
5
+ @Published var configuration: BreathingConfiguration
6
+
7
+ init(configuration: BreathingConfiguration) {
8
+ self.configuration = configuration
9
+ }
10
+ }
11
+
12
+ private struct BreathingContainerView: View {
13
+ @ObservedObject var model: BreathingConfigurationModel
14
+
15
+ var body: some View {
16
+ BreathingExerciseView(configuration: model.configuration)
17
+ }
18
+ }
19
+
20
+ struct BreathingProps {
21
+ private static let defaultBlobColors: [UIColor] = [
22
+ UIColor(red: 0.4, green: 0.7, blue: 0.9, alpha: 1.0),
23
+ UIColor(red: 0.3, green: 0.5, blue: 0.8, alpha: 1.0),
24
+ UIColor(red: 0.5, green: 0.3, blue: 0.7, alpha: 1.0)
25
+ ]
26
+
27
+ var blobColors: [UIColor] = BreathingProps.defaultBlobColors
28
+ var innerBlobColor: UIColor = UIColor.white.withAlphaComponent(0.3)
29
+ var glowColor: UIColor = .white
30
+ var particleColor: UIColor = .white
31
+ var progressRingColor: UIColor = UIColor.white.withAlphaComponent(0.5)
32
+ var textColor: UIColor = .white
33
+ var showProgressRing: Bool = true
34
+ var showTextCue: Bool = true
35
+ var showInnerBlob: Bool = true
36
+ var showShadow: Bool = true
37
+ var showParticles: Bool = true
38
+ var showWavyBlobs: Bool = true
39
+ var showGlowEffects: Bool = true
40
+ var pointCount: Int = 8
41
+ var wobbleIntensity: Double = 1.0
42
+
43
+ func makeConfiguration() -> BreathingConfiguration {
44
+ let resolvedBlobColors = blobColors.count >= 2
45
+ ? blobColors
46
+ : BreathingProps.defaultBlobColors
47
+
48
+ return BreathingConfiguration(
49
+ blobColors: resolvedBlobColors.map { Color(uiColor: $0) },
50
+ innerBlobColor: Color(uiColor: innerBlobColor),
51
+ glowColor: Color(uiColor: glowColor),
52
+ particleColor: Color(uiColor: particleColor),
53
+ progressRingColor: Color(uiColor: progressRingColor),
54
+ textColor: Color(uiColor: textColor),
55
+ showProgressRing: showProgressRing,
56
+ showTextCue: showTextCue,
57
+ showInnerBlob: showInnerBlob,
58
+ showShadow: showShadow,
59
+ showParticles: showParticles,
60
+ showWavyBlobs: showWavyBlobs,
61
+ showGlowEffects: showGlowEffects,
62
+ pointCount: max(6, pointCount),
63
+ wobbleIntensity: max(0, min(1, wobbleIntensity))
64
+ )
65
+ }
66
+ }
67
+
68
+ class ExpoBreathingExerciseView: ExpoView {
69
+ private var props = BreathingProps()
70
+ private let model: BreathingConfigurationModel
71
+ private let hostingController: UIHostingController<BreathingContainerView>
72
+
73
+ private let onPhaseChange = EventDispatcher()
74
+ private let onExerciseComplete = EventDispatcher()
75
+
76
+ required init(appContext: AppContext? = nil) {
77
+ let initialConfig = BreathingConfiguration()
78
+ let model = BreathingConfigurationModel(configuration: initialConfig)
79
+ self.model = model
80
+ self.hostingController = UIHostingController(rootView: BreathingContainerView(model: model))
81
+
82
+ super.init(appContext: appContext)
83
+
84
+ clipsToBounds = false
85
+ hostingController.view.backgroundColor = .clear
86
+ hostingController.view.clipsToBounds = false
87
+ hostingController.view.isOpaque = false
88
+ hostingController.view.translatesAutoresizingMaskIntoConstraints = false
89
+ addSubview(hostingController.view)
90
+
91
+ NSLayoutConstraint.activate([
92
+ hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
93
+ hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
94
+ hostingController.view.topAnchor.constraint(equalTo: topAnchor),
95
+ hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
96
+ ])
97
+
98
+ // Setup callbacks
99
+ setupCallbacks()
100
+ }
101
+
102
+ private func setupCallbacks() {
103
+ BreathingSharedState.shared.onPhaseChange = { [weak self] phase, label, phaseIndex, cycle in
104
+ self?.onPhaseChange([
105
+ "phase": phase.rawValue,
106
+ "label": label,
107
+ "phaseIndex": phaseIndex,
108
+ "cycle": cycle
109
+ ])
110
+ }
111
+
112
+ BreathingSharedState.shared.onExerciseComplete = { [weak self] totalCycles, totalDuration in
113
+ self?.onExerciseComplete([
114
+ "totalCycles": totalCycles,
115
+ "totalDuration": totalDuration
116
+ ])
117
+ }
118
+ }
119
+
120
+ func updateProps(_ update: (inout BreathingProps) -> Void) {
121
+ update(&props)
122
+ model.configuration = props.makeConfiguration()
123
+ }
124
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-orb",
3
- "version": "0.1.0",
4
- "description": "Animated orb component for React Native (iOS, Android, Web)",
3
+ "version": "0.2.0",
4
+ "description": "Animated orb and breathing exercise components for React Native (iOS, Android)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "scripts": {
@@ -21,16 +21,19 @@
21
21
  "expo-orb",
22
22
  "orb",
23
23
  "animation",
24
+ "breathing",
25
+ "breathing-exercise",
26
+ "meditation",
24
27
  "ios",
25
28
  "android"
26
29
  ],
27
- "repository": "https://github.com/enso-works/expo-orb",
30
+ "repository": "https://github.com/enso-works/expo-orb-animation",
28
31
  "bugs": {
29
- "url": "https://github.com/enso-works/expo-orb/issues"
32
+ "url": "https://github.com/enso-works/expo-orb-animation/issues"
30
33
  },
31
34
  "author": "Ensar Bavrk <ensar.bavrk@gmail.com> (https://github.com/enso-works)",
32
35
  "license": "MIT",
33
- "homepage": "https://github.com/enso-works/expo-orb#readme",
36
+ "homepage": "https://github.com/enso-works/expo-orb-animation#readme",
34
37
  "dependencies": {},
35
38
  "devDependencies": {
36
39
  "@types/react": "~19.1.0",
@@ -0,0 +1,50 @@
1
+ import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export type BreathPhase = 'inhale' | 'holdIn' | 'exhale' | 'holdOut';
4
+
5
+ export interface BreathPhaseConfig {
6
+ phase: BreathPhase;
7
+ duration: number; // milliseconds
8
+ targetScale: number; // e.g., 1.0 to 1.3
9
+ label: string; // "Breathe In"
10
+ }
11
+
12
+ export interface BreathingPattern {
13
+ phases: BreathPhaseConfig[];
14
+ cycles?: number; // undefined = infinite
15
+ }
16
+
17
+ export type BreathingPreset = 'relaxing' | 'box' | 'energizing' | 'calming';
18
+
19
+ export interface PhaseChangeEvent {
20
+ phase: BreathPhase;
21
+ label: string;
22
+ phaseIndex: number;
23
+ cycle: number;
24
+ }
25
+
26
+ export interface ExerciseCompleteEvent {
27
+ totalCycles: number;
28
+ totalDuration: number;
29
+ }
30
+
31
+ export interface ExpoBreathingExerciseViewProps {
32
+ blobColors?: ColorValue[];
33
+ innerBlobColor?: ColorValue;
34
+ glowColor?: ColorValue;
35
+ particleColor?: ColorValue;
36
+ progressRingColor?: ColorValue;
37
+ textColor?: ColorValue;
38
+ showProgressRing?: boolean;
39
+ showTextCue?: boolean;
40
+ showInnerBlob?: boolean;
41
+ showShadow?: boolean;
42
+ showParticles?: boolean;
43
+ showWavyBlobs?: boolean;
44
+ showGlowEffects?: boolean;
45
+ pointCount?: number; // Morphing points (default: 8)
46
+ wobbleIntensity?: number; // 0-1
47
+ onPhaseChange?: (event: { nativeEvent: PhaseChangeEvent }) => void;
48
+ onExerciseComplete?: (event: { nativeEvent: ExerciseCompleteEvent }) => void;
49
+ style?: StyleProp<ViewStyle>;
50
+ }