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
package/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/native', 'universe/web'],
4
+ ignorePatterns: ['build'],
5
+ };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ensar Bavrk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # expo-orb
2
+
3
+ An animated orb component for React Native with Expo. Renders a glowing, animated sphere with particles, wavy blobs, and customizable visual effects.
4
+
5
+ Inspired by and iOS implementation from [metasidd/Orb](https://github.com/metasidd/Orb)
6
+
7
+ ## Demo
8
+
9
+ | iOS | Android |
10
+ |:---:|:-------:|
11
+ | ![iOS Demo](https://raw.githubusercontent.com/enso-works/expo-orb/main/docs/demo-ios.gif) | ![Android Demo](https://raw.githubusercontent.com/enso-works/expo-orb/main/docs/demo-android.gif) |
12
+
13
+ ## Features
14
+
15
+ - Smooth 60fps animations
16
+ - Activity-based animation states (idle, speaking, etc.)
17
+ - Breathing animation effect
18
+ - Customizable colors, particles, and glow effects
19
+ - Native performance on iOS (SwiftUI/SpriteKit) and Android (Jetpack Compose)
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install expo-orb
25
+ ```
26
+
27
+ For bare React Native projects, run `npx pod-install` after installation.
28
+
29
+ ## Usage
30
+
31
+ ```tsx
32
+ import { ExpoOrbView, setOrbActivity } from 'expo-orb';
33
+
34
+ function MyComponent() {
35
+ return (
36
+ <ExpoOrbView
37
+ style={{ width: 200, height: 200 }}
38
+ backgroundColors={['#7c3aed', '#3b82f6', '#ec4899']}
39
+ glowColor="#ffffff"
40
+ particleColor="#ffffff"
41
+ showBackground={true}
42
+ showWavyBlobs={true}
43
+ showParticles={true}
44
+ showGlowEffects={true}
45
+ showShadow={true}
46
+ />
47
+ );
48
+ }
49
+
50
+ // Control animation intensity (0-1)
51
+ setOrbActivity(0.8); // Active/speaking state
52
+ setOrbActivity(0.1); // Idle state
53
+ ```
54
+
55
+ ## Props
56
+
57
+ | Prop | Type | Default | Description |
58
+ |------|------|---------|-------------|
59
+ | `backgroundColors` | `ColorValue[]` | `['green', 'blue', 'pink']` | Gradient colors for the orb background (min 2 colors) |
60
+ | `glowColor` | `ColorValue` | `'white'` | Color of the glow effects |
61
+ | `particleColor` | `ColorValue` | `'white'` | Color of floating particles |
62
+ | `coreGlowIntensity` | `number` | `1.0` | Intensity of the core glow effect (0+) |
63
+ | `breathingIntensity` | `number` | `0` | Intensity of breathing animation (0-1) |
64
+ | `breathingSpeed` | `number` | `0.25` | Speed of breathing animation |
65
+ | `showBackground` | `boolean` | `true` | Show/hide background gradient |
66
+ | `showWavyBlobs` | `boolean` | `true` | Show/hide animated wavy blob overlays |
67
+ | `showParticles` | `boolean` | `true` | Show/hide floating particles |
68
+ | `showGlowEffects` | `boolean` | `true` | Show/hide rotating glow effects |
69
+ | `showShadow` | `boolean` | `true` | Show/hide drop shadow |
70
+ | `speed` | `number` | `60` | Animation speed multiplier |
71
+ | `style` | `StyleProp<ViewStyle>` | - | Container style (set width/height here) |
72
+
73
+ ## Methods
74
+
75
+ ### `setOrbActivity(activity: number)`
76
+
77
+ Controls the orb's animation intensity. Pass a value between 0 and 1:
78
+
79
+ - `0` - Minimal activity (idle)
80
+ - `0.1-0.3` - Low activity
81
+ - `0.5-0.7` - Medium activity
82
+ - `0.8-1.0` - High activity (speaking/active)
83
+
84
+ This function bypasses React's prop system to prevent animation flickering during rapid updates.
85
+
86
+ ```tsx
87
+ import { setOrbActivity } from 'expo-orb';
88
+
89
+ // Transition to speaking state
90
+ setOrbActivity(0.85);
91
+
92
+ // Return to idle
93
+ setOrbActivity(0.08);
94
+ ```
95
+
96
+ ## Example
97
+
98
+ See the `/example` directory for a complete working example with activity simulation.
99
+
100
+ ```tsx
101
+ import * as React from 'react';
102
+ import { View } from 'react-native';
103
+ import { ExpoOrbView, setOrbActivity } from 'expo-orb';
104
+
105
+ export default function App() {
106
+ React.useEffect(() => {
107
+ // Simulate activity changes
108
+ const interval = setInterval(() => {
109
+ const activity = Math.random() > 0.5 ? 0.85 : 0.08;
110
+ setOrbActivity(activity);
111
+ }, 2000);
112
+
113
+ return () => clearInterval(interval);
114
+ }, []);
115
+
116
+ return (
117
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
118
+ <ExpoOrbView
119
+ style={{ width: 220, height: 220 }}
120
+ backgroundColors={['#7c3aed', '#3b82f6', '#ec4899']}
121
+ />
122
+ </View>
123
+ );
124
+ }
125
+ ```
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,60 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ }
6
+
7
+ plugins {
8
+ id 'com.android.library'
9
+ id 'kotlin-android'
10
+ id 'org.jetbrains.kotlin.plugin.compose' version '2.1.20'
11
+ }
12
+
13
+ group = 'expo.modules.iosorb'
14
+ version = '0.1.0'
15
+
16
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
17
+ apply from: expoModulesCorePlugin
18
+ applyKotlinExpoModulesCorePlugin()
19
+ useCoreDependencies()
20
+ useExpoPublishing()
21
+
22
+ android {
23
+ namespace "expo.modules.iosorb"
24
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
25
+
26
+ defaultConfig {
27
+ minSdkVersion safeExtGet("minSdkVersion", 24)
28
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
29
+ versionCode 1
30
+ versionName "0.1.0"
31
+ }
32
+
33
+ lintOptions {
34
+ abortOnError false
35
+ }
36
+
37
+ buildFeatures {
38
+ compose true
39
+ }
40
+
41
+ kotlinOptions {
42
+ jvmTarget = '17'
43
+ }
44
+
45
+ compileOptions {
46
+ sourceCompatibility JavaVersion.VERSION_17
47
+ targetCompatibility JavaVersion.VERSION_17
48
+ }
49
+ }
50
+
51
+ dependencies {
52
+ def composeBom = platform('androidx.compose:compose-bom:2024.02.00')
53
+ implementation composeBom
54
+
55
+ implementation 'androidx.compose.ui:ui'
56
+ implementation 'androidx.compose.ui:ui-graphics'
57
+ implementation 'androidx.compose.foundation:foundation'
58
+ implementation 'androidx.compose.runtime:runtime'
59
+ implementation 'androidx.activity:activity-compose:1.8.2'
60
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,79 @@
1
+ package expo.modules.orb
2
+
3
+ import android.graphics.Color as AndroidColor
4
+ import expo.modules.kotlin.modules.Module
5
+ import expo.modules.kotlin.modules.ModuleDefinition
6
+
7
+ class ExpoOrbModule : Module() {
8
+ override fun definition() = ModuleDefinition {
9
+ Name("ExpoOrb")
10
+
11
+ // Activity function - bypasses React's prop reconciliation
12
+ Function("setActivity") { activity: Double ->
13
+ val clampedActivity = activity.coerceIn(0.0, 1.0)
14
+ OrbSharedState.targetActivity = clampedActivity
15
+ }
16
+
17
+ View(ExpoOrbView::class) {
18
+ // Colors come as strings (hex) or integers from React Native
19
+ Prop("backgroundColors") { view: ExpoOrbView, colors: List<Any> ->
20
+ val parsedColors = colors.map { parseColor(it) }
21
+ view.setBackgroundColors(parsedColors)
22
+ }
23
+
24
+ Prop("glowColor") { view: ExpoOrbView, color: Any ->
25
+ view.setGlowColor(parseColor(color))
26
+ }
27
+
28
+ Prop("particleColor") { view: ExpoOrbView, color: Any ->
29
+ view.setParticleColor(parseColor(color))
30
+ }
31
+
32
+ Prop("coreGlowIntensity") { view: ExpoOrbView, value: Double ->
33
+ view.setCoreGlowIntensity(value)
34
+ }
35
+
36
+ Prop("breathingIntensity") { view: ExpoOrbView, value: Double ->
37
+ view.setBreathingIntensity(value)
38
+ }
39
+
40
+ Prop("breathingSpeed") { view: ExpoOrbView, value: Double ->
41
+ view.setBreathingSpeed(value)
42
+ }
43
+
44
+ Prop("showBackground") { view: ExpoOrbView, value: Boolean ->
45
+ view.setShowBackground(value)
46
+ }
47
+
48
+ Prop("showWavyBlobs") { view: ExpoOrbView, value: Boolean ->
49
+ view.setShowWavyBlobs(value)
50
+ }
51
+
52
+ Prop("showParticles") { view: ExpoOrbView, value: Boolean ->
53
+ view.setShowParticles(value)
54
+ }
55
+
56
+ Prop("showGlowEffects") { view: ExpoOrbView, value: Boolean ->
57
+ view.setShowGlowEffects(value)
58
+ }
59
+
60
+ Prop("showShadow") { view: ExpoOrbView, value: Boolean ->
61
+ view.setShowShadow(value)
62
+ }
63
+
64
+ Prop("speed") { view: ExpoOrbView, value: Double ->
65
+ view.setSpeed(value)
66
+ }
67
+ }
68
+ }
69
+
70
+ private fun parseColor(value: Any): Int {
71
+ return when (value) {
72
+ is String -> AndroidColor.parseColor(value)
73
+ is Int -> value
74
+ is Double -> value.toInt()
75
+ is Long -> value.toInt()
76
+ else -> AndroidColor.WHITE
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,105 @@
1
+ package expo.modules.orb
2
+
3
+ import android.content.Context
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.fillMaxSize
6
+ import androidx.compose.foundation.layout.padding
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.getValue
9
+ import androidx.compose.runtime.mutableStateOf
10
+ import androidx.compose.runtime.setValue
11
+ import androidx.compose.ui.Alignment
12
+ import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.graphics.Color
14
+ import androidx.compose.ui.platform.ComposeView
15
+ import androidx.compose.ui.unit.dp
16
+ import expo.modules.kotlin.AppContext
17
+ import expo.modules.kotlin.views.ExpoView
18
+
19
+ class ExpoOrbView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
20
+
21
+ // Mutable state for configuration
22
+ private var config by mutableStateOf(OrbConfiguration())
23
+
24
+ private val composeView = ComposeView(context).apply {
25
+ setContent {
26
+ OrbViewContent()
27
+ }
28
+ }
29
+
30
+ init {
31
+ // Allow glow/shadow effects to render outside view bounds
32
+ clipChildren = false
33
+ clipToPadding = false
34
+ composeView.clipChildren = false
35
+ composeView.clipToPadding = false
36
+ addView(composeView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
37
+ }
38
+
39
+ @Composable
40
+ private fun OrbViewContent() {
41
+ // Add padding so glow/shadow can overflow outside the orb circle
42
+ Box(
43
+ modifier = Modifier
44
+ .fillMaxSize()
45
+ .padding(24.dp), // Space for glow overflow
46
+ contentAlignment = Alignment.Center
47
+ ) {
48
+ OrbView(
49
+ config = config,
50
+ useSharedActivityState = true,
51
+ modifier = Modifier.fillMaxSize()
52
+ )
53
+ }
54
+ }
55
+
56
+ fun setBackgroundColors(colors: List<Int>) {
57
+ config = config.copy(
58
+ backgroundColors = colors.map { Color(it) }
59
+ )
60
+ }
61
+
62
+ fun setGlowColor(color: Int) {
63
+ config = config.copy(glowColor = Color(color))
64
+ }
65
+
66
+ fun setParticleColor(color: Int) {
67
+ config = config.copy(particleColor = Color(color))
68
+ }
69
+
70
+ fun setCoreGlowIntensity(value: Double) {
71
+ config = config.copy(coreGlowIntensity = value)
72
+ }
73
+
74
+ fun setBreathingIntensity(value: Double) {
75
+ config = config.copy(breathingIntensity = value)
76
+ }
77
+
78
+ fun setBreathingSpeed(value: Double) {
79
+ config = config.copy(breathingSpeed = value)
80
+ }
81
+
82
+ fun setShowBackground(value: Boolean) {
83
+ config = config.copy(showBackground = value)
84
+ }
85
+
86
+ fun setShowWavyBlobs(value: Boolean) {
87
+ config = config.copy(showWavyBlobs = value)
88
+ }
89
+
90
+ fun setShowParticles(value: Boolean) {
91
+ config = config.copy(showParticles = value)
92
+ }
93
+
94
+ fun setShowGlowEffects(value: Boolean) {
95
+ config = config.copy(showGlowEffects = value)
96
+ }
97
+
98
+ fun setShowShadow(value: Boolean) {
99
+ config = config.copy(showShadow = value)
100
+ }
101
+
102
+ fun setSpeed(value: Double) {
103
+ config = config.copy(speed = value)
104
+ }
105
+ }
@@ -0,0 +1,30 @@
1
+ package expo.modules.orb
2
+
3
+ import androidx.compose.ui.graphics.Color
4
+
5
+ data class OrbConfiguration(
6
+ val backgroundColors: List<Color> = listOf(Color.Green, Color.Blue, Color.Magenta),
7
+ val glowColor: Color = Color.White,
8
+ val particleColor: Color = Color.White,
9
+ val coreGlowIntensity: Double = 1.0,
10
+ val showBackground: Boolean = true,
11
+ val showWavyBlobs: Boolean = true,
12
+ val showParticles: Boolean = true,
13
+ val showGlowEffects: Boolean = true,
14
+ val showShadow: Boolean = true,
15
+ val speed: Double = 60.0,
16
+ val breathingIntensity: Double = 0.0,
17
+ val breathingSpeed: Double = 0.25
18
+ )
19
+
20
+ /**
21
+ * Effective configuration computed from activity level.
22
+ * All speeds must stay CONSTANT to avoid animation jumps.
23
+ */
24
+ data class EffectiveConfig(
25
+ val speed: Double,
26
+ val breathingIntensity: Double,
27
+ val breathingSpeed: Double,
28
+ val coreGlowIntensity: Double,
29
+ val glowColor: Color
30
+ )
@@ -0,0 +1,25 @@
1
+ package expo.modules.orb
2
+
3
+ /**
4
+ * Shared state for activity animation - mirrors iOS OrbSharedState.
5
+ *
6
+ * This singleton allows smooth animations when bridging React Native ↔ Compose.
7
+ * JS sends target values, native interpolates during animation loop.
8
+ */
9
+ object OrbSharedState {
10
+ @Volatile
11
+ var targetActivity: Double = 0.0 // Written by native bridge, read by animation loop
12
+
13
+ @Volatile
14
+ var currentActivity: Double = 0.0 // Interpolated value, updated each frame
15
+
16
+ @Volatile
17
+ var lastUpdateTime: Long = System.nanoTime() // For frame-time-based interpolation
18
+
19
+ // Cumulative phase for breathing - allows speed changes without jumps
20
+ @Volatile
21
+ var breathingPhase: Double = 0.0
22
+
23
+ @Volatile
24
+ var lastBreathingUpdate: Long = System.nanoTime()
25
+ }