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,368 @@
1
+ package expo.modules.orb
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.aspectRatio
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.offset
9
+ import androidx.compose.foundation.layout.padding
10
+ import androidx.compose.foundation.shape.CircleShape
11
+ import androidx.compose.runtime.Composable
12
+ import androidx.compose.runtime.LaunchedEffect
13
+ import androidx.compose.runtime.getValue
14
+ import androidx.compose.runtime.mutableStateOf
15
+ import androidx.compose.runtime.remember
16
+ import androidx.compose.runtime.setValue
17
+ import androidx.compose.runtime.withFrameNanos
18
+ import androidx.compose.ui.Alignment
19
+ import androidx.compose.ui.Modifier
20
+ import androidx.compose.ui.draw.blur
21
+ import androidx.compose.ui.draw.clip
22
+ import androidx.compose.ui.draw.drawBehind
23
+ import androidx.compose.ui.draw.scale
24
+ import androidx.compose.ui.geometry.Offset
25
+ import androidx.compose.ui.graphics.Brush
26
+ import androidx.compose.ui.graphics.Color
27
+ import androidx.compose.ui.graphics.graphicsLayer
28
+ import androidx.compose.ui.unit.dp
29
+ import kotlin.math.PI
30
+ import kotlin.math.abs
31
+ import kotlin.math.max
32
+ import kotlin.math.min
33
+ import kotlin.math.pow
34
+ import kotlin.math.sin
35
+
36
+ @Composable
37
+ fun OrbView(
38
+ config: OrbConfiguration,
39
+ useSharedActivityState: Boolean = false,
40
+ modifier: Modifier = Modifier
41
+ ) {
42
+ // Frame-based animation state
43
+ var elapsedTime by remember { mutableStateOf(0.0) }
44
+ var frameTime by remember { mutableStateOf(System.nanoTime()) }
45
+
46
+ // Continuous animation loop
47
+ LaunchedEffect(Unit) {
48
+ val startTime = System.nanoTime()
49
+ while (true) {
50
+ withFrameNanos { currentTime ->
51
+ elapsedTime = (currentTime - startTime) / 1_000_000_000.0
52
+ frameTime = currentTime
53
+ }
54
+ }
55
+ }
56
+
57
+ // Interpolate activity
58
+ val activity = if (useSharedActivityState) {
59
+ interpolatedActivity(frameTime)
60
+ } else {
61
+ 0.0
62
+ }
63
+
64
+ // Compute effective config from activity
65
+ val effectiveConfig = activityDerivedConfig(activity, config)
66
+
67
+ // Calculate breathing scale
68
+ val scale = breathingScale(frameTime, effectiveConfig)
69
+
70
+ // Outer container with padding to allow glow/shadow overflow
71
+ Box(
72
+ modifier = modifier
73
+ .graphicsLayer {
74
+ // Disable clipping to allow glow/shadow overflow
75
+ clip = false
76
+ }
77
+ .aspectRatio(1f)
78
+ .scale(scale),
79
+ contentAlignment = Alignment.Center
80
+ ) {
81
+ // Shadow layers (rendered outside the circle)
82
+ if (config.showShadow) {
83
+ RealisticShadow(
84
+ colors = config.backgroundColors,
85
+ modifier = Modifier
86
+ .fillMaxSize()
87
+ .graphicsLayer { clip = false }
88
+ )
89
+ }
90
+
91
+ // Main orb content clipped to circle
92
+ Box(
93
+ modifier = Modifier
94
+ .fillMaxSize()
95
+ .clip(CircleShape)
96
+ ) {
97
+ // Background gradient
98
+ if (config.showBackground) {
99
+ Box(
100
+ modifier = Modifier
101
+ .fillMaxSize()
102
+ .background(
103
+ brush = Brush.verticalGradient(
104
+ colors = config.backgroundColors.reversed()
105
+ )
106
+ )
107
+ )
108
+ }
109
+
110
+ // Base depth glows
111
+ BaseDepthGlows(
112
+ effectiveConfig = effectiveConfig,
113
+ elapsedTime = elapsedTime,
114
+ modifier = Modifier.fillMaxSize()
115
+ )
116
+
117
+ // Wavy blobs
118
+ if (config.showWavyBlobs) {
119
+ WavyBlobLayer(
120
+ elapsedTime = elapsedTime,
121
+ modifier = Modifier.fillMaxSize()
122
+ )
123
+ }
124
+
125
+ // Core glow effects
126
+ if (config.showGlowEffects) {
127
+ CoreGlowEffects(
128
+ effectiveConfig = effectiveConfig,
129
+ elapsedTime = elapsedTime,
130
+ modifier = Modifier
131
+ .fillMaxSize()
132
+ .padding(8.dp)
133
+ )
134
+ }
135
+
136
+ // Particles
137
+ if (config.showParticles) {
138
+ ParticlesView(
139
+ color = config.particleColor,
140
+ particleCount = 10,
141
+ speedRange = 10f..20f,
142
+ sizeRange = 0.5f..1f,
143
+ opacityRange = 0f..0.3f,
144
+ elapsedTime = elapsedTime,
145
+ modifier = Modifier.fillMaxSize()
146
+ )
147
+ ParticlesView(
148
+ color = config.particleColor,
149
+ particleCount = 10,
150
+ speedRange = 20f..30f,
151
+ sizeRange = 0.2f..1f,
152
+ opacityRange = 0.3f..0.8f,
153
+ elapsedTime = elapsedTime,
154
+ modifier = Modifier.fillMaxSize()
155
+ )
156
+ }
157
+
158
+ // Inner glow overlay
159
+ InnerGlowOverlay(
160
+ modifier = Modifier.fillMaxSize()
161
+ )
162
+ }
163
+ }
164
+ }
165
+
166
+ private fun interpolatedActivity(currentTime: Long): Double {
167
+ val state = OrbSharedState
168
+ val target = state.targetActivity
169
+ val dt = (currentTime - state.lastUpdateTime) / 1_000_000_000.0 // Convert to seconds
170
+ state.lastUpdateTime = currentTime
171
+
172
+ // Smooth interpolation (same as iOS)
173
+ val factor = min(1.0, dt * 6.0)
174
+ val next = state.currentActivity + (target - state.currentActivity) * factor
175
+ state.currentActivity = next
176
+
177
+ return next
178
+ }
179
+
180
+ private fun activityDerivedConfig(activity: Double, config: OrbConfiguration): EffectiveConfig {
181
+ // IMPORTANT: Rotation speed must stay CONSTANT
182
+ val speed = 18.0
183
+ // Breathing speed CAN vary because we use cumulative phase
184
+ val breathingSpeed = 0.03 + activity * 0.25
185
+ // Idle: no breathing, Speaking: full breathing
186
+ val breathingIntensity = max(0.0, (activity - 0.2)) * 1.25
187
+ // Idle: barely visible glow, Speaking: bright
188
+ val coreGlowIntensity = 0.08 + activity * 1.8
189
+
190
+ return EffectiveConfig(
191
+ speed = speed,
192
+ breathingIntensity = breathingIntensity,
193
+ breathingSpeed = breathingSpeed,
194
+ coreGlowIntensity = coreGlowIntensity,
195
+ glowColor = config.glowColor
196
+ )
197
+ }
198
+
199
+ private fun breathingScale(currentTime: Long, effectiveConfig: EffectiveConfig): Float {
200
+ val intensity = max(0.0, min(1.0, effectiveConfig.breathingIntensity))
201
+ if (intensity == 0.0) {
202
+ return 1f
203
+ }
204
+
205
+ val state = OrbSharedState
206
+ val dt = (currentTime - state.lastBreathingUpdate) / 1_000_000_000.0
207
+ state.lastBreathingUpdate = currentTime
208
+
209
+ val speed = max(0.01, effectiveConfig.breathingSpeed)
210
+ state.breathingPhase += dt * speed * 2 * PI
211
+
212
+ // Punchy wave shape
213
+ val rawWave = sin(state.breathingPhase)
214
+ val wave = if (rawWave >= 0) {
215
+ rawWave.pow(0.6)
216
+ } else {
217
+ -abs(rawWave).pow(1.4)
218
+ }
219
+
220
+ val amplitude = intensity * 0.17
221
+ return (1.0 + amplitude * wave).toFloat()
222
+ }
223
+
224
+ @Composable
225
+ private fun BaseDepthGlows(
226
+ effectiveConfig: EffectiveConfig,
227
+ elapsedTime: Double,
228
+ modifier: Modifier = Modifier
229
+ ) {
230
+ Box(modifier = modifier) {
231
+ SimpleRotatingGlow(
232
+ color = effectiveConfig.glowColor,
233
+ rotationSpeed = effectiveConfig.speed * 0.75,
234
+ direction = RotationDirection.COUNTER_CLOCKWISE,
235
+ elapsedTime = elapsedTime,
236
+ alpha = 0.5f,
237
+ modifier = Modifier
238
+ .fillMaxSize()
239
+ .padding(4.dp)
240
+ .blur(8.dp)
241
+ )
242
+ SimpleRotatingGlow(
243
+ color = effectiveConfig.glowColor.copy(alpha = 0.5f),
244
+ rotationSpeed = effectiveConfig.speed * 0.25,
245
+ direction = RotationDirection.CLOCKWISE,
246
+ elapsedTime = elapsedTime,
247
+ alpha = 0.3f,
248
+ modifier = Modifier
249
+ .fillMaxSize()
250
+ .padding(8.dp)
251
+ .blur(4.dp)
252
+ )
253
+ }
254
+ }
255
+
256
+ @Composable
257
+ private fun CoreGlowEffects(
258
+ effectiveConfig: EffectiveConfig,
259
+ elapsedTime: Double,
260
+ modifier: Modifier = Modifier
261
+ ) {
262
+ Box(modifier = modifier) {
263
+ SimpleRotatingGlow(
264
+ color = effectiveConfig.glowColor,
265
+ rotationSpeed = effectiveConfig.speed * 1.2,
266
+ direction = RotationDirection.CLOCKWISE,
267
+ elapsedTime = elapsedTime,
268
+ alpha = (effectiveConfig.coreGlowIntensity * 0.8).toFloat(),
269
+ modifier = Modifier
270
+ .fillMaxSize()
271
+ .blur(12.dp)
272
+ )
273
+ SimpleRotatingGlow(
274
+ color = effectiveConfig.glowColor,
275
+ rotationSpeed = effectiveConfig.speed * 0.9,
276
+ direction = RotationDirection.CLOCKWISE,
277
+ elapsedTime = elapsedTime,
278
+ alpha = (effectiveConfig.coreGlowIntensity * 0.6).toFloat(),
279
+ modifier = Modifier
280
+ .fillMaxSize()
281
+ .blur(8.dp)
282
+ )
283
+ }
284
+ }
285
+
286
+ @Composable
287
+ private fun WavyBlobLayer(
288
+ elapsedTime: Double,
289
+ modifier: Modifier = Modifier
290
+ ) {
291
+ val blobSpeed = 20.0
292
+
293
+ Box(modifier = modifier) {
294
+ WavyBlobView(
295
+ color = Color.White.copy(alpha = 0.4f),
296
+ loopDuration = 60.0 / blobSpeed * 1.75,
297
+ elapsedTime = elapsedTime,
298
+ modifier = Modifier
299
+ .fillMaxSize()
300
+ .scale(1.5f)
301
+ .offset(y = 40.dp)
302
+ .blur(2.dp)
303
+ )
304
+ WavyBlobView(
305
+ color = Color.White.copy(alpha = 0.25f),
306
+ loopDuration = 60.0 / blobSpeed * 2.25,
307
+ elapsedTime = elapsedTime,
308
+ modifier = Modifier
309
+ .fillMaxSize()
310
+ .scale(1.2f)
311
+ .offset(y = (-40).dp)
312
+ .graphicsLayer { rotationZ = 90f }
313
+ .blur(2.dp)
314
+ )
315
+ }
316
+ }
317
+
318
+ @Composable
319
+ private fun InnerGlowOverlay(modifier: Modifier = Modifier) {
320
+ Canvas(modifier = modifier) {
321
+ val center = Offset(size.width / 2f, size.height / 2f)
322
+ val radius = min(size.width, size.height) / 2f
323
+
324
+ // Inner glow rings
325
+ drawCircle(
326
+ brush = Brush.verticalGradient(
327
+ colors = listOf(Color.Transparent, Color.White.copy(alpha = 0.15f)),
328
+ ),
329
+ radius = radius,
330
+ center = center
331
+ )
332
+ }
333
+ }
334
+
335
+ @Composable
336
+ private fun RealisticShadow(
337
+ colors: List<Color>,
338
+ modifier: Modifier = Modifier
339
+ ) {
340
+ Box(modifier = modifier) {
341
+ // Soft outer shadow
342
+ Canvas(
343
+ modifier = Modifier
344
+ .fillMaxSize()
345
+ .offset(y = 8.dp)
346
+ .blur(24.dp)
347
+ .graphicsLayer { alpha = 0.3f }
348
+ ) {
349
+ drawCircle(
350
+ brush = Brush.verticalGradient(colors.reversed()),
351
+ radius = size.minDimension / 2f
352
+ )
353
+ }
354
+ // Closer shadow
355
+ Canvas(
356
+ modifier = Modifier
357
+ .fillMaxSize()
358
+ .offset(y = 4.dp)
359
+ .blur(12.dp)
360
+ .graphicsLayer { alpha = 0.5f }
361
+ ) {
362
+ drawCircle(
363
+ brush = Brush.verticalGradient(colors.reversed()),
364
+ radius = size.minDimension / 2f
365
+ )
366
+ }
367
+ }
368
+ }
@@ -0,0 +1,77 @@
1
+ package expo.modules.orb
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.layout.fillMaxSize
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.remember
7
+ import androidx.compose.ui.Modifier
8
+ import androidx.compose.ui.geometry.Offset
9
+ import androidx.compose.ui.graphics.BlendMode
10
+ import androidx.compose.ui.graphics.Color
11
+ import kotlin.math.sin
12
+ import kotlin.random.Random
13
+
14
+ data class Particle(
15
+ val id: Int,
16
+ val startX: Float,
17
+ val startY: Float,
18
+ val speed: Float,
19
+ val size: Float,
20
+ val alpha: Float,
21
+ val phase: Float // Random phase offset for variety
22
+ )
23
+
24
+ @Composable
25
+ fun ParticlesView(
26
+ color: Color,
27
+ particleCount: Int = 10,
28
+ speedRange: ClosedFloatingPointRange<Float> = 10f..20f,
29
+ sizeRange: ClosedFloatingPointRange<Float> = 0.5f..1f,
30
+ opacityRange: ClosedFloatingPointRange<Float> = 0f..0.3f,
31
+ elapsedTime: Double,
32
+ modifier: Modifier = Modifier
33
+ ) {
34
+ // Generate stable particles on first composition
35
+ val particles = remember {
36
+ (0 until particleCount).map { id ->
37
+ Particle(
38
+ id = id,
39
+ startX = Random.nextFloat(),
40
+ startY = Random.nextFloat(),
41
+ speed = Random.nextFloat() * (speedRange.endInclusive - speedRange.start) + speedRange.start,
42
+ size = Random.nextFloat() * (sizeRange.endInclusive - sizeRange.start) + sizeRange.start,
43
+ alpha = Random.nextFloat() * (opacityRange.endInclusive - opacityRange.start) + opacityRange.start,
44
+ phase = Random.nextFloat() * 6.28f // Random phase 0-2π
45
+ )
46
+ }
47
+ }
48
+
49
+ Canvas(modifier = modifier.fillMaxSize()) {
50
+ val width = size.width
51
+ val height = size.height
52
+
53
+ particles.forEach { particle ->
54
+ // Calculate particle position based on time
55
+ val cycleTime = 3.0 + particle.phase // 3-6 second cycle
56
+ val progress = ((elapsedTime + particle.phase) % cycleTime) / cycleTime
57
+
58
+ // Drift upward with slight horizontal wobble
59
+ val x = particle.startX * width + sin(elapsedTime * 0.5 + particle.phase) * 20f
60
+ val y = height * (1f - progress.toFloat()) * particle.startY + height * (1f - progress.toFloat())
61
+
62
+ // Fade in/out based on lifecycle
63
+ val lifecycleAlpha = when {
64
+ progress < 0.2 -> (progress / 0.2).toFloat() // Fade in
65
+ progress > 0.8 -> ((1.0 - progress) / 0.2).toFloat() // Fade out
66
+ else -> 1f
67
+ } * particle.alpha
68
+
69
+ drawCircle(
70
+ color = color.copy(alpha = lifecycleAlpha),
71
+ radius = particle.size * 12f,
72
+ center = Offset(x.toFloat(), y.toFloat()),
73
+ blendMode = BlendMode.Plus
74
+ )
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,90 @@
1
+ package expo.modules.orb
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.layout.fillMaxSize
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.geometry.Offset
8
+ import androidx.compose.ui.graphics.BlendMode
9
+ import androidx.compose.ui.graphics.Color
10
+ import androidx.compose.ui.graphics.drawscope.DrawScope
11
+ import androidx.compose.ui.graphics.drawscope.rotate
12
+ import kotlin.math.min
13
+
14
+ enum class RotationDirection(val multiplier: Double) {
15
+ CLOCKWISE(1.0),
16
+ COUNTER_CLOCKWISE(-1.0)
17
+ }
18
+
19
+ @Composable
20
+ fun RotatingGlowView(
21
+ color: Color,
22
+ rotationSpeed: Double = 30.0,
23
+ direction: RotationDirection,
24
+ elapsedTime: Double,
25
+ modifier: Modifier = Modifier,
26
+ blurRadius: Float = 0f
27
+ ) {
28
+ val safeSpeed = maxOf(0.01, rotationSpeed)
29
+ val rotation = (elapsedTime * safeSpeed * direction.multiplier).toFloat()
30
+
31
+ Canvas(modifier = modifier.fillMaxSize()) {
32
+ val sizePx = min(size.width, size.height)
33
+ val center = Offset(size.width / 2f, size.height / 2f)
34
+
35
+ rotate(rotation, pivot = center) {
36
+ // Main glow circle
37
+ drawCircle(
38
+ color = color,
39
+ radius = sizePx / 2f,
40
+ center = center,
41
+ alpha = 0.8f
42
+ )
43
+
44
+ // Cutout circle offset downward (creates crescent effect)
45
+ drawCircle(
46
+ color = Color.Transparent,
47
+ radius = sizePx * 0.655f, // 1.31 / 2
48
+ center = Offset(center.x, center.y + sizePx * 0.31f),
49
+ blendMode = BlendMode.Clear
50
+ )
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Simplified glow without complex masking - just a soft gradient circle
57
+ */
58
+ @Composable
59
+ fun SimpleRotatingGlow(
60
+ color: Color,
61
+ rotationSpeed: Double = 30.0,
62
+ direction: RotationDirection,
63
+ elapsedTime: Double,
64
+ alpha: Float = 1f,
65
+ modifier: Modifier = Modifier
66
+ ) {
67
+ val safeSpeed = maxOf(0.01, rotationSpeed)
68
+ val rotation = (elapsedTime * safeSpeed * direction.multiplier).toFloat()
69
+ // Clamp alpha to valid range 0-1
70
+ val safeAlpha = alpha.coerceIn(0f, 1f)
71
+
72
+ Canvas(modifier = modifier.fillMaxSize()) {
73
+ val sizePx = min(size.width, size.height)
74
+ val center = Offset(size.width / 2f, size.height / 2f)
75
+
76
+ rotate(rotation, pivot = center) {
77
+ // Draw offset glow to create rotating effect - subtle and smaller
78
+ drawCircle(
79
+ color = color.copy(alpha = (safeAlpha * 0.5f).coerceIn(0f, 1f)),
80
+ radius = sizePx * 0.25f,
81
+ center = Offset(center.x, center.y - sizePx * 0.12f)
82
+ )
83
+ drawCircle(
84
+ color = color.copy(alpha = (safeAlpha * 0.35f).coerceIn(0f, 1f)),
85
+ radius = sizePx * 0.18f,
86
+ center = Offset(center.x + sizePx * 0.08f, center.y + sizePx * 0.08f)
87
+ )
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,90 @@
1
+ package expo.modules.orb
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.layout.fillMaxSize
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.geometry.Offset
8
+ import androidx.compose.ui.graphics.Color
9
+ import androidx.compose.ui.graphics.Path
10
+ import kotlin.math.PI
11
+ import kotlin.math.atan2
12
+ import kotlin.math.cos
13
+ import kotlin.math.min
14
+ import kotlin.math.sin
15
+
16
+ data class BlobPoint(val x: Float, val y: Float)
17
+
18
+ @Composable
19
+ fun WavyBlobView(
20
+ color: Color,
21
+ loopDuration: Double = 1.0,
22
+ elapsedTime: Double,
23
+ modifier: Modifier = Modifier
24
+ ) {
25
+ // Initial blob points (6 points in a circle)
26
+ val basePoints = (0 until 6).map { index ->
27
+ val angle = (index.toDouble() / 6.0) * 2.0 * PI
28
+ BlobPoint(
29
+ x = (0.5 + cos(angle) * 0.9).toFloat(),
30
+ y = (0.5 + sin(angle) * 0.9).toFloat()
31
+ )
32
+ }
33
+
34
+ val angle = ((elapsedTime % loopDuration) / loopDuration) * 2.0 * PI
35
+
36
+ Canvas(modifier = modifier.fillMaxSize()) {
37
+ val sizePx = min(size.width, size.height)
38
+ val center = Offset(size.width / 2f, size.height / 2f)
39
+ val radius = sizePx * 0.45f
40
+
41
+ // Adjust points with sine wave movement
42
+ val adjustedPoints = basePoints.mapIndexed { index, point ->
43
+ val phaseOffset = index * PI / 3.0
44
+ val xOffset = sin(angle + phaseOffset) * 0.15
45
+ val yOffset = cos(angle + phaseOffset) * 0.15
46
+ Offset(
47
+ x = ((point.x - 0.5f + xOffset.toFloat()) * radius + center.x).toFloat(),
48
+ y = ((point.y - 0.5f + yOffset.toFloat()) * radius + center.y).toFloat()
49
+ )
50
+ }
51
+
52
+ val path = Path().apply {
53
+ moveTo(adjustedPoints[0].x, adjustedPoints[0].y)
54
+
55
+ for (i in adjustedPoints.indices) {
56
+ val next = (i + 1) % adjustedPoints.size
57
+
58
+ // Calculate angles for control points
59
+ val currentAngle = atan2(
60
+ adjustedPoints[i].y - center.y,
61
+ adjustedPoints[i].x - center.x
62
+ )
63
+ val nextAngle = atan2(
64
+ adjustedPoints[next].y - center.y,
65
+ adjustedPoints[next].x - center.x
66
+ )
67
+
68
+ val handleLength = radius * 0.33f
69
+
70
+ val control1 = Offset(
71
+ x = adjustedPoints[i].x + cos(currentAngle + PI.toFloat() / 2f) * handleLength,
72
+ y = adjustedPoints[i].y + sin(currentAngle + PI.toFloat() / 2f) * handleLength
73
+ )
74
+ val control2 = Offset(
75
+ x = adjustedPoints[next].x + cos(nextAngle - PI.toFloat() / 2f) * handleLength,
76
+ y = adjustedPoints[next].y + sin(nextAngle - PI.toFloat() / 2f) * handleLength
77
+ )
78
+
79
+ cubicTo(
80
+ control1.x, control1.y,
81
+ control2.x, control2.y,
82
+ adjustedPoints[next].x, adjustedPoints[next].y
83
+ )
84
+ }
85
+ close()
86
+ }
87
+
88
+ drawPath(path = path, color = color)
89
+ }
90
+ }
@@ -0,0 +1,17 @@
1
+ import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
2
+ export type ExpoOrbViewProps = {
3
+ backgroundColors?: ColorValue[];
4
+ glowColor?: ColorValue;
5
+ particleColor?: ColorValue;
6
+ coreGlowIntensity?: number;
7
+ breathingIntensity?: number;
8
+ breathingSpeed?: number;
9
+ showBackground?: boolean;
10
+ showWavyBlobs?: boolean;
11
+ showParticles?: boolean;
12
+ showGlowEffects?: boolean;
13
+ showShadow?: boolean;
14
+ speed?: number;
15
+ style?: StyleProp<ViewStyle>;
16
+ };
17
+ //# sourceMappingURL=ExpoOrb.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoOrb.types.d.ts","sourceRoot":"","sources":["../src/ExpoOrb.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoOrb.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoOrb.types.js","sourceRoot":"","sources":["../src/ExpoOrb.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ColorValue, StyleProp, ViewStyle } from 'react-native';\n\nexport type ExpoOrbViewProps = {\n backgroundColors?: ColorValue[];\n glowColor?: ColorValue;\n particleColor?: ColorValue;\n coreGlowIntensity?: number;\n breathingIntensity?: number;\n breathingSpeed?: number;\n showBackground?: boolean;\n showWavyBlobs?: boolean;\n showParticles?: boolean;\n showGlowEffects?: boolean;\n showShadow?: boolean;\n speed?: number;\n style?: StyleProp<ViewStyle>;\n};\n"]}
@@ -0,0 +1,17 @@
1
+ import { NativeModule } from 'expo';
2
+ declare class ExpoOrbModule extends NativeModule {
3
+ /**
4
+ * Set the activity level (0-1) for the orb animation.
5
+ * Uses a native function instead of a prop to bypass React's reconciliation
6
+ * and prevent view re-renders that cause animation flickering.
7
+ */
8
+ setActivity(activity: number): void;
9
+ }
10
+ declare const module: ExpoOrbModule;
11
+ export default module;
12
+ /**
13
+ * Set the orb activity level directly via native function.
14
+ * This bypasses React's prop system to prevent animation interference.
15
+ */
16
+ export declare function setOrbActivity(activity: number): void;
17
+ //# sourceMappingURL=ExpoOrbModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoOrbModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrbModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,OAAO,aAAc,SAAQ,YAAY;IAC9C;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CACpC;AAED,QAAA,MAAM,MAAM,eAAgD,CAAC;AAE7D,eAAe,MAAM,CAAC;AAEtB;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErD"}
@@ -0,0 +1,11 @@
1
+ import { requireNativeModule } from 'expo';
2
+ const module = requireNativeModule('ExpoOrb');
3
+ export default module;
4
+ /**
5
+ * Set the orb activity level directly via native function.
6
+ * This bypasses React's prop system to prevent animation interference.
7
+ */
8
+ export function setOrbActivity(activity) {
9
+ module.setActivity(activity);
10
+ }
11
+ //# sourceMappingURL=ExpoOrbModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoOrbModule.js","sourceRoot":"","sources":["../src/ExpoOrbModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAWzD,MAAM,MAAM,GAAG,mBAAmB,CAAgB,SAAS,CAAC,CAAC;AAE7D,eAAe,MAAM,CAAC;AAEtB;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\ndeclare class ExpoOrbModule extends NativeModule {\n /**\n * Set the activity level (0-1) for the orb animation.\n * Uses a native function instead of a prop to bypass React's reconciliation\n * and prevent view re-renders that cause animation flickering.\n */\n setActivity(activity: number): void;\n}\n\nconst module = requireNativeModule<ExpoOrbModule>('ExpoOrb');\n\nexport default module;\n\n/**\n * Set the orb activity level directly via native function.\n * This bypasses React's prop system to prevent animation interference.\n */\nexport function setOrbActivity(activity: number): void {\n module.setActivity(activity);\n}\n"]}