diff --git a/.gitignore b/.gitignore index 71af6b2..c68dca5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml .DS_Store /build /captures @@ -14,6 +8,9 @@ .cxx local.properties +# Android Studio / IntelliJ +.idea/ + .kotlin/ app/src/main/res/a diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 9028944..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -IngrediCheck \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml deleted file mode 100644 index 4a53bee..0000000 --- a/.idea/AndroidProjectSystem.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml deleted file mode 100644 index cc804ff..0000000 --- a/.idea/appInsightsSettings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b86273d..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index 62599a1..0000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml deleted file mode 100644 index 91f9558..0000000 --- a/.idea/deviceManager.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index d124cf2..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 7061a0d..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6..0000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index b2c751a..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 16660f1..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml deleted file mode 100644 index 539e3b8..0000000 --- a/.idea/studiobot.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dce7420..c589ff7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,14 +48,14 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) - implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("androidx.datastore:datastore-preferences:1.2.0") implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) - implementation("androidx.compose.material:material-icons-extended") - implementation("androidx.browser:browser:1.8.0") + implementation(libs.androidx.compose.material.icons.extended) + implementation("androidx.browser:browser:1.9.0") implementation("androidx.startup:startup-runtime:1.1.1") implementation("com.google.android.gms:play-services-auth:20.7.0") implementation("io.ktor:ktor-client-okhttp:3.1.1") @@ -63,6 +63,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.1") + implementation("com.airbnb.android:lottie-compose:6.7.1") implementation("io.github.jan-tennert.supabase:supabase-kt:3.2.4") implementation("io.github.jan-tennert.supabase:auth-kt:3.2.4") implementation("io.github.jan-tennert.supabase:postgrest-kt:3.2.4") diff --git a/app/src/main/assets/dynamicJsonData.json b/app/src/main/assets/dynamicJsonData.json new file mode 100644 index 0000000..556f99a --- /dev/null +++ b/app/src/main/assets/dynamicJsonData.json @@ -0,0 +1,845 @@ +{ + "steps": [ + { + "id": "allergies", + "type": "type-1", + "header": { + "iconUrl": "allergies", + "name": "Allergies", + "individual": { + "question": "Got any allergies we should keep in mind?", + "description": "Choose all that apply so we can give you smarter food tips." + }, + "family": { + "question": "Does anyone in your IngrediFam have allergies we should know ?", + "description": "Select all that apply to keep meals worry-free." + }, + "singleMember": { + "question": "Does {name} have any allergies we should keep in mind?", + "description": "Choose all that apply so we can give smarter food tips." + } + }, + "content": { + "options": [ + { + "name": "Peanuts", + "icon": "πŸ₯œ" + }, + { + "name": "Tree nuts", + "icon": "🌰" + }, + { + "name": "Dairy", + "icon": "πŸ₯›" + }, + { + "name": "Eggs", + "icon": "πŸ₯š" + }, + { + "name": "Soy", + "icon": "🌱" + }, + { + "name": "Wheat", + "icon": "🌾" + }, + { + "name": "Fish", + "icon": "🐟" + }, + { + "name": "Shellfish", + "icon": "🦐" + }, + { + "name": "Sesame", + "icon": "βšͺ" + }, + { + "name": "Celery", + "icon": "πŸ₯¬" + }, + { + "name": "Lupin", + "icon": "🫘" + }, + { + "name": "Sulphites", + "icon": "πŸ§‚" + }, + { + "name": "Mustard", + "icon": "🟑" + }, + { + "name": "Molluscs", + "icon": "🐚" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + } + }, + { + "id": "intolerances", + "type": "type-1", + "header": { + "iconUrl": "mingcute_alert-line", + "name": "Intolerances", + "individual": { + "question": "Any sensitivities that make eating tricky?", + "description": "We'll make sure your food suggestions avoid these." + }, + "family": { + "question": "Any sensitivities or intolerances in your IngrediFam?", + "description": "We'll avoid foods that cause discomfort." + }, + "singleMember": { + "question": "Does {name} have any food sensitivities?", + "description": "We'll make sure food suggestions avoid these." + } + }, + "content": { + "options": [ + { + "name": "Lactose", + "icon": "πŸ₯›" + }, + { + "name": "Fructose", + "icon": "πŸ“" + }, + { + "name": "Histamine", + "icon": "🍷" + }, + { + "name": "Gluten / wheat", + "icon": "🌾" + }, + { + "name": "Fodmap", + "icon": "πŸ§„" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + } + }, + { + "id": "healthConditions", + "type": "type-1", + "header": { + "iconUrl": "lucide_stethoscope", + "name": "Health Conditions", + "individual": { + "question": "Do you follow any special diets or have health conditions?", + "description": "This helps us recommend meals that work for you." + }, + "family": { + "question": "Any doctor diets or health conditions in your IngrediFam?", + "description": "This helps us tailor recommendations better." + }, + "singleMember": { + "question": "Does {name} follow any special diets or have any health conditions?", + "description": "This helps us recommend meals that work better." + } + }, + "content": { + "options": [ + { + "name": "Diabetes", + "icon": "🍭" + }, + { + "name": "Hypertension", + "icon": "πŸ’Š" + }, + { + "name": "Kidney Disease", + "icon": "🩺" + }, + { + "name": "Heart Health", + "icon": "πŸ«€" + }, + { + "name": "PKU (phenylalanine-sensitive)", + "icon": "🧬" + }, + { + "name": "Anti-inflammatory medical diet", + "icon": "πŸ₯—" + }, + { + "name": "Celiac disease", + "icon": "πŸ₯–" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + } + }, + { + "id": "lifeStage", + "type": "type-1", + "header": { + "iconUrl": "lucide_baby", + "name": "Life Stage", + "individual": { + "question": "Do you have special needs we should keep in mind?", + "description": "Select all that apply, this helps us tailor tips for you." + }, + "family": { + "question": "Does anyone in your IngrediFam have special life stage needs?", + "description": "Select all that apply so tips match every life stage." + }, + "singleMember": { + "question": "Any specific needs for {name}?", + "description": "Select all that apply so tips match their life stage." + } + }, + "content": { + "options": [ + { + "name": "Kids Baby-friendly foods", + "icon": "πŸ‘Ά" + }, + { + "name": "Toddler picky-eating adaptations", + "icon": "πŸ™„" + }, + { + "name": "Pregnancy Prenatal nutrition", + "icon": "🀰" + }, + { + "name": "Breastfeeding diets", + "icon": "🍼" + }, + { + "name": "Senior-friendly", + "icon": "πŸ‘΄" + }, + { + "name": "None of these apply", + "icon": "βœ…" + } + ] + } + }, + { + "id": "region", + "type": "type-3", + "header": { + "iconUrl": "nrk_globe", + "name": "Region", + "individual": { + "question": "Where are you from? This helps us customize your experience!", + "description": "Pick your region(s) or cultural practices." + }, + "family": { + "question": "Where does your IngrediFam draw its food traditions from?", + "description": "Select your region or cultural roots." + }, + "singleMember": { + "question": "Where is {name} from? It helps us personalize the experience.", + "description": "Pick region(s) or cultural practices." + } + }, + "content": { + "regions": [ + { + "name": "India & South Asia", + "subRegions": [ + { + "name": "Ayurveda", + "icon": "🌿" + }, + { + "name": "Hindu food traditions", + "icon": "πŸ•‰" + }, + { + "name": "Jain diet", + "icon": "πŸ§˜β€β™‚οΈ" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + }, + { + "name": "Africa", + "subRegions": [ + { + "name": "Rastafarian Ital diet", + "icon": "πŸ₯—" + }, + { + "name": "Ethiopian Orthodox fasting", + "icon": "πŸ₯–" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + }, + { + "name": "Middle East & Mediterranean", + "subRegions": [ + { + "name": "Halal (Islamic dietary laws)", + "icon": "β˜ͺ️" + }, + { + "name": "Kosher (Jewish dietary laws)", + "icon": "✑️" + }, + { + "name": "Greek / Mediterranean diets", + "icon": "πŸ«’" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + }, + { + "name": "East Asia", + "subRegions": [ + { + "name": "Traditional Chinese Medicine (TCM)", + "icon": "🧧" + }, + { + "name": "Buddhist food rules", + "icon": "🧘" + }, + { + "name": "Japanese Macrobiotic diet", + "icon": "πŸ™" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + }, + { + "name": "Western / Native traditions", + "subRegions": [ + { + "name": "Native American traditions", + "icon": "πŸͺΆ" + }, + { + "name": "Christian traditions", + "icon": "✝️" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + }, + { + "name": "Seventh-day Adventist", + "subRegions": [ + { + "name": "Seventh-day Adventist", + "icon": "✝️" + } + ] + }, + { + "name": "Other", + "subRegions": [ + { + "name": "Other", + "icon": "✏️" + } + ] + } + ] + } + }, + { + "id": "avoid", + "type": "type-2", + "header": { + "iconUrl": "charm_circle-cross", + "name": "Avoid", + "individual": { + "question": "Anything you avoid in your diet?" + }, + "family": { + "question": "Anything your IngrediFam avoids?" + }, + "singleMember": { + "question": "Anything to avoid in {name}'s diet?" + } + }, + "content": { + "subSteps": [ + { + "id": "avoid_oils_fats", + "title": "Oils & Fats", + "description": "In fats or oils, what do you avoid?", + "color": "#FFF6B3", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Hydrogenated oils / Trans fats", + "icon": "🧈" + }, + { + "name": "Canola / Seed oils", + "icon": "🌾" + }, + { + "name": "Palm oil", + "icon": "🌴" + }, + { + "name": "Corn / High-fructose corn syrup", + "icon": "🌽" + } + ] + }, + { + "id": "avoid_animal_based", + "title": "Animal-Based", + "description": "Any animal products you don't consume?", + "color": "#DCC7F6", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Pork", + "icon": "πŸ–" + }, + { + "name": "Beef", + "icon": "πŸ„" + }, + { + "name": "Honey", + "icon": "🍯" + }, + { + "name": "Gelatin / Rennet", + "icon": "πŸ§‚" + }, + { + "name": "Shellfish", + "icon": "🦐" + }, + { + "name": "Insects", + "icon": "🐜" + }, + { + "name": "Seafood (fish)", + "icon": "🐟" + }, + { + "name": "Lard / Animal fat", + "icon": "πŸ–" + } + ] + }, + { + "id": "avoid_stimulants_substances", + "title": "Stimulants & Substances", + "description": "Do you avoid these?", + "color": "#BFF0D4", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Alcohol", + "icon": "🍷" + }, + { + "name": "Caffeine", + "icon": "β˜•" + } + ] + }, + { + "id": "avoid_additives_sweeteners", + "title": "Additives & Sweeteners", + "description": "Do you stay away from processed ingredients?", + "color": "#FFD9B5", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "MSG", + "icon": "βš—οΈ" + }, + { + "name": "Artificial sweeteners", + "icon": "🍬" + }, + { + "name": "Preservatives", + "icon": "πŸ§‚" + }, + { + "name": "Refined sugar", + "icon": "🍚" + }, + { + "name": "Corn syrup / HFCS", + "icon": "🌽" + }, + { + "name": "Stevia / Monk fruit", + "icon": "🍈" + } + ] + }, + { + "id": "avoid_plant_based_restrictions", + "title": "Plant-Based Restrictions", + "description": "Any plant foods you avoid?", + "color": "#F9C6D0", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Nightshades (paprika, peppers, etc.)", + "icon": "πŸ…" + }, + { + "name": "Garlic / Onion", + "icon": "πŸ§„" + } + ] + } + ] + } + }, + { + "id": "lifeStyle", + "type": "type-2", + "header": { + "iconUrl": "hugeicons_plant-01", + "name": "Life Style", + "individual": { + "question": "What's your way of eating?" + }, + "family": { + "question": "What's your IngrediFam's food lifestyle?" + }, + "singleMember": { + "question": "How does {name} prefer to eat?" + } + }, + "content": { + "subSteps": [ + { + "id": "lifestyle_plant_balance", + "title": "Plant & Balance", + "description": "Do you follow a plant-forward or flexible eating style?", + "color": "#FFF6B3", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Vegetarian", + "icon": "πŸ₯¦" + }, + { + "name": "Vegan", + "icon": "🌱" + }, + { + "name": "Flexitarian", + "icon": "πŸ”„" + }, + { + "name": "Reducetarian", + "icon": "βž–" + }, + { + "name": "Pescatarian", + "icon": "🐟" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + }, + { + "id": "lifestyle_quality_source", + "title": "Quality & Source", + "description": "Do you care about where your food comes from and how it's grown?", + "color": "#DCC7F6", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Organic Only", + "icon": "🌱" + }, + { + "name": "Non-GMO", + "icon": "🧬" + }, + { + "name": "Locally Sourced", + "icon": "πŸ“" + }, + { + "name": "Seasonal Eater", + "icon": "πŸ•°οΈ" + } + ] + }, + { + "id": "lifestyle_sustainable_living", + "title": "Sustainable Living", + "description": "Are you mindful of waste, packaging, and ingredient transparency?", + "color": "#D7EEB2", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Zero-Waste / Minimal Packing", + "icon": "🌍" + }, + { + "name": "Clean Label", + "icon": "βœ…" + } + ] + } + ] + } + }, + { + "id": "nutrition", + "type": "type-2", + "header": { + "iconUrl": "fluent-emoji-high-contrast_fork-and-knife-with-plate", + "name": "Nutrition", + "individual": { + "question": "What's your nutrition focus right now?" + }, + "family": { + "question": "What's your IngrediFam's nutrition focus?" + }, + "singleMember": { + "question": "What's {name}'s nutrition focus right now?" + } + }, + "content": { + "subSteps": [ + { + "id": "nutrition_macronutrient_goals", + "title": "Macronutrient Goals", + "description": "Do you want to balance your proteins, carbs, and fats or focus on one?", + "color": "#F9C6D0", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "High Protein", + "icon": "πŸ—" + }, + { + "name": "Low Carb", + "icon": "πŸ₯’" + }, + { + "name": "Low Fat", + "icon": "πŸ₯‘" + }, + { + "name": "Balanced Macros", + "icon": "βš–οΈ" + } + ] + }, + { + "id": "nutrition_sugar_fiber", + "title": "Sugar & Fiber", + "description": "Do you prefer low sugar or high-fiber foods for better digestion and energy?", + "color": "#A7D8F0", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Low Sugar", + "icon": "πŸ“" + }, + { + "name": "Sugar-Free", + "icon": "🍭" + }, + { + "name": "High Fiber", + "icon": "🌾" + } + ] + }, + { + "id": "nutrition_diet_frameworks_patterns", + "title": "Diet Frameworks & Patterns", + "description": "Do you follow a structured eating plan or experiment with fasting?", + "color": "#FFD9B5", + "bgImageUrl": "leaf-recycle", + "options": [ + { + "name": "Keto", + "icon": "πŸ₯‘" + }, + { + "name": "DASH", + "icon": "πŸ’§" + }, + { + "name": "Paleo", + "icon": "πŸ₯©" + }, + { + "name": "Mediterranean", + "icon": "πŸ«’" + }, + { + "name": "Whole30", + "icon": "πŸ₯—" + }, + { + "name": "Fasting", + "icon": "πŸ•‘" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + } + ] + } + }, + { + "id": "ethical", + "type": "type-1", + "header": { + "iconUrl": "streamline_recycle-1-solid", + "name": "Ethical", + "individual": { + "question": "What ethical or environmental values are important to you?", + "description": "Select the causes that matter most when it comes to the food you eat." + }, + "family": { + "question": "What ethical or environmental values matter to your IngrediFam?", + "description": "Select causes that shape your food choices." + }, + "singleMember": { + "question": "What ethical or environmental priorities does {name} have?", + "description": "Select causes that shape food choices." + } + }, + "content": { + "options": [ + { + "name": "Animal welfare focused", + "icon": "πŸ„" + }, + { + "name": "Fair trade", + "icon": "🀝" + }, + { + "name": "Sustainable fishing / no overfished species", + "icon": "🐟" + }, + { + "name": "Low carbon footprint foods", + "icon": "♻️" + }, + { + "name": "Water footprint concerns", + "icon": "πŸ’§" + }, + { + "name": "Palm-oil free", + "icon": "🌴" + }, + { + "name": "Plastic-free packaging", + "icon": "🚫" + }, + { + "name": "Other", + "icon": "✏️" + } + ] + } + }, + { + "id": "taste", + "type": "type-1", + "header": { + "iconUrl": "iconoir_chocolate", + "name": "Taste", + "individual": { + "question": "What are your taste and texture preferences?", + "description": "Choose what you love or avoid when it comes to flavors and textures." + }, + "family": { + "question": "What tastes and textures does your family prefer?", + "description": "Customize tastes so every plate feels just right." + }, + "singleMember": { + "question": "What tastes and textures does {name} prefer?", + "description": "Customize tastes so every plate feels just right." + } + }, + "content": { + "options": [ + { + "name": "Spicy lover", + "icon": "🌢️" + }, + { + "name": "Avoid Spicy", + "icon": "🚫" + }, + { + "name": "Sweet tooth", + "icon": "🍰" + }, + { + "name": "Avoid slimy textures", + "icon": "πŸ₯’" + }, + { + "name": "Avoid bitter foods", + "icon": "🍡" + }, + { + "name": "Other", + "icon": "✏️" + }, + { + "name": "Crunchy / Soft preferences", + "icon": "πŸͺ" + }, + { + "name": "Low-sweet preference", + "icon": "🍯" + } + ] + } + } + ] +} diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index f31436a..a052d35 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/lc/fungee/Ingredicheck/auth/AppleLoginWebViewActivity.kt b/app/src/main/java/lc/fungee/Ingredicheck/auth/AppleLoginWebViewActivity.kt index 1d11c71..afce38c 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/auth/AppleLoginWebViewActivity.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/auth/AppleLoginWebViewActivity.kt @@ -17,6 +17,8 @@ import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout import androidx.activity.ComponentActivity +import com.russhwolf.settings.BuildConfig + class AppleLoginWebViewActivity : ComponentActivity() { @@ -71,8 +73,10 @@ class AppleLoginWebViewActivity : ComponentActivity() { .toString() } - Log.d("AppleWebView", "Starting Apple WebView auth. redirectUri=$redirectUri") - Log.d("AppleWebView", "Auth URL: ${sanitizeUrl(authUrl)}") + if (BuildConfig.DEBUG) { + Log.d("AppleWebView", "Starting Apple WebView auth. redirectUri=$redirectUri") + Log.d("AppleWebView", "Auth URL: ${sanitizeUrl(authUrl)}") + } webView.webChromeClient = object : WebChromeClient() { override fun onCreateWindow( @@ -98,7 +102,9 @@ class AppleLoginWebViewActivity : ComponentActivity() { val uri = Uri.parse(url) val resultIntent = Intent() - Log.d("AppleWebView", "Redirect detected: ${sanitizeUrl(url)}") + if (BuildConfig.DEBUG) { + Log.d("AppleWebView", "Redirect detected: ${sanitizeUrl(url)}") + } val error = uri.getQueryParameter("error") val errorDescription = uri.getQueryParameter("error_description") @@ -146,7 +152,9 @@ class AppleLoginWebViewActivity : ComponentActivity() { return true } if (url != null) { - Log.d("AppleWebView", "Navigating: ${sanitizeUrl(url)}") + if (BuildConfig.DEBUG) { + Log.d("AppleWebView", "Navigating: ${sanitizeUrl(url)}") + } } return false } @@ -158,21 +166,27 @@ class AppleLoginWebViewActivity : ComponentActivity() { return true } - Log.d("AppleWebView", "Navigating: ${sanitizeUrl(url)}") + if (BuildConfig.DEBUG) { + Log.d("AppleWebView", "Navigating: ${sanitizeUrl(url)}") + } return false } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) if (url != null) { - Log.d("AppleWebView", "Page started: ${sanitizeUrl(url)}") + if (BuildConfig.DEBUG) { + Log.d("AppleWebView", "Page started: ${sanitizeUrl(url)}") + } } } override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) if (url != null) { - Log.d("AppleWebView", "Page finished: ${sanitizeUrl(url)}") + if (BuildConfig.DEBUG) { + Log.d("AppleWebView", "Page finished: ${sanitizeUrl(url)}") + } } } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/auth/AuthViewModel.kt b/app/src/main/java/lc/fungee/Ingredicheck/auth/AuthViewModel.kt index dcba791..e829982 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/auth/AuthViewModel.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/auth/AuthViewModel.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.russhwolf.settings.BuildConfig import io.github.jan.supabase.auth.user.UserSession import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -18,6 +19,10 @@ import lc.fungee.Ingredicheck.memoji.MemojiRepository import lc.fungee.Ingredicheck.memoji.MemojiRequestMapper import lc.fungee.Ingredicheck.family.FamilyMemberDto import lc.fungee.Ingredicheck.dietary.DietaryPreferenceRepository +import lc.fungee.Ingredicheck.foodnotes.FoodNotesRepository +import lc.fungee.Ingredicheck.onboarding.data.EVERYONE_MEMBER_ID +import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData + enum class AuthProvider { Google, @@ -45,6 +50,7 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) { private val memojiRepository = MemojiRepository() private val familyRepository = FamilyRepository() private val dietaryPreferenceRepository = DietaryPreferenceRepository() + private val foodNotesRepository = FoodNotesRepository() private val _state = MutableStateFlow(AuthState.Idle) val state: StateFlow = _state @@ -61,7 +67,9 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) { private fun pushDebug(message: String) { val line = "${System.currentTimeMillis()} | $message" _debugLog.value = (_debugLog.value + line).takeLast(60) - Log.d("AuthDebug", message) + if (BuildConfig.DEBUG) { + Log.d("AuthDebug", message) + } } /** @@ -71,7 +79,9 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) { */ fun syncDietaryPreferencesFromOnboarding(preferenceText: String) { if (preferenceText.isBlank()) { - Log.d("AuthDebug", "DietaryPreference: skip sync (empty text)") + if (BuildConfig.DEBUG) { + Log.d("AuthDebug", "DietaryPreference: skip sync (empty text)") + } return } viewModelScope.launch { @@ -93,7 +103,9 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) { when (validationResult) { is lc.fungee.Ingredicheck.dietary.PreferenceValidationResult.Success -> { pushDebug("DietaryPreference: sync success id=${validationResult.preference.id}") - Log.d("AuthDebug", "DietaryPreference: sync success id=${validationResult.preference.id}") + if (BuildConfig.DEBUG) { + Log.d("AuthDebug", "DietaryPreference: sync success id=${validationResult.preference.id}") + } } is lc.fungee.Ingredicheck.dietary.PreferenceValidationResult.Failure -> { pushDebug("DietaryPreference: sync failure ${validationResult.explanation}") @@ -109,6 +121,71 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) { } } + /** + * Sync onboarding chip selections to food-notes API (per-member and Everyone), matching iOS. + * Called when user taps "All Set!" or completes the last preference step. + * - EVERYONE_MEMBER_ID ("ALL") -> PUT family/food-notes + * - Each member id -> PUT family/members/{id}/food-notes + */ + fun syncFoodNotesFromOnboarding(selectedAllergiesByMember: Map>) { + val keys = selectedAllergiesByMember.keys.toList() + if (BuildConfig.DEBUG) { + Log.d("FoodNotesAPI", "FoodNotes API implementation: sync started, keys=${keys}, size=${selectedAllergiesByMember.size}") + } + if (selectedAllergiesByMember.isEmpty()) { + if (BuildConfig.DEBUG) { + Log.d("FoodNotesAPI", "FoodNotes API: skip sync (empty selections)") + } + return + } + viewModelScope.launch { + val accessToken = repository.accessTokenOrNull() + if (accessToken.isNullOrBlank()) { + Log.w("FoodNotesAPI", "FoodNotes API: skip sync (no access token)") + return@launch + } + var successCount = 0 + var failCount = 0 + for ((memberKey, chipIds) in selectedAllergiesByMember) { + if (chipIds.isEmpty()) continue + val content = OnboardingChipData.buildFoodNotesContentFromChipIds(chipIds) + if (content.isEmpty()) continue + val isEveryone = memberKey == EVERYONE_MEMBER_ID || memberKey.isBlank() + val result = if (isEveryone) { + foodNotesRepository.updateFamilyFoodNotes( + accessToken = accessToken, + content = content, + version = 0 + ) + } else { + foodNotesRepository.updateMemberFoodNotes( + accessToken = accessToken, + memberId = memberKey.lowercase(), + content = content, + version = 0 + ) + } + result.fold( + onSuccess = { + successCount++ + pushDebug("FoodNotes: sync success ${if (isEveryone) "Everyone" else memberKey}") + if (BuildConfig.DEBUG) { + Log.d("FoodNotesAPI", "FoodNotes API implementation: sync success ${if (isEveryone) "Everyone" else "member=$memberKey"} β€” working") + } + }, + onFailure = { e -> + failCount++ + pushDebug("FoodNotes: sync error ${if (isEveryone) "Everyone" else memberKey} ${e.message}") + Log.e("FoodNotesAPI", "FoodNotes API: sync error ${if (isEveryone) "Everyone" else memberKey}", e) + } + ) + } + if (BuildConfig.DEBUG) { + Log.d("FoodNotesAPI", "FoodNotes API implementation: sync completed β€” success=$successCount fail=$failCount (check logs above for details)") + } + } + } + fun addFamilyMember(member: FamilyMemberDto, onResult: (Result) -> Unit) { viewModelScope.launch { val accessToken = repository.accessTokenOrNull() diff --git a/app/src/main/java/lc/fungee/Ingredicheck/dietary/DIETARY_PREFERENCE.md b/app/src/main/java/lc/fungee/Ingredicheck/dietary/DIETARY_PREFERENCE.md index f1665e1..db8dd98 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/dietary/DIETARY_PREFERENCE.md +++ b/app/src/main/java/lc/fungee/Ingredicheck/dietary/DIETARY_PREFERENCE.md @@ -28,4 +28,28 @@ - `DietaryPreference`: GET/POST/PUT/DELETE and status/body length - `AuthDebug`: `DietaryPreference: syncing...`, `sync success id=...`, `sync failure...`, or `sync error...` -Filter logcat by `DietaryPreference`, `OnboardingAllergies`, or `AuthDebug` to confirm it’s working. +Filter logcat by `DietaryPreference`, `OnboardingAllergies`, or `AuthDebug` to confirm it's working. + +--- + +## Food-notes API (per-member / Everyone) – matches iOS + +Onboarding chip selections are also synced to the **food-notes** API so that each family member (and "Everyone") has their own note on the backend, matching iOS behavior. + +1. **Endpoints** + - `PUT family/food-notes` (body: `{ "content": {...}, "version": 0 }`) β†’ "Everyone" / family note + - `PUT family/members/{memberId}/food-notes` β†’ one member's note + - `GET family/food-notes/all` β†’ load all (family + members) for versions/cache + +2. **When we sync** + - Same as above: when the user taps "All Set!" or completes the last preference step. + - For each key in `selectedAllergiesByMember`: **ALL** β†’ `updateFamilyFoodNotes`; member UUID β†’ `updateMemberFoodNotes(memberId.lowercase(), ...)`. + +3. **Where it runs** + - **OnboardingHost**: Calls `authViewModel.syncFoodNotesFromOnboarding(...)` in both exit paths (with dietary preference sync). + - **AuthViewModel**: `syncFoodNotesFromOnboarding` builds content via `OnboardingChipData.buildFoodNotesContentFromChipIds(chipIds)` and calls `FoodNotesRepository` for each member/Everyone. + - **FoodNotesRepository**: PUT requests; on 409 retries once with `currentNote.version` or `version = 0`. + +4. **Logs** + - `FoodNotes`: updateFamilyFoodNotes, updateMemberFoodNotes, status and retries. + - `AuthDebug`: `FoodNotes: sync success Everyone` / `FoodNotes: sync success `. diff --git a/app/src/main/java/lc/fungee/Ingredicheck/dietary/DietaryPreferenceRepository.kt b/app/src/main/java/lc/fungee/Ingredicheck/dietary/DietaryPreferenceRepository.kt index 8ea5355..6b7d770 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/dietary/DietaryPreferenceRepository.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/dietary/DietaryPreferenceRepository.kt @@ -1,6 +1,7 @@ package lc.fungee.Ingredicheck.dietary import android.util.Log +import com.russhwolf.settings.BuildConfig import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout @@ -23,6 +24,7 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import lc.fungee.Ingredicheck.AppConfig + private const val TAG = "DietaryPreference" /** @@ -65,21 +67,27 @@ class DietaryPreferenceRepository { /** GET list of dietary preferences. */ suspend fun getDietaryPreferences(accessToken: String): Result> { val url = baseUrl("preferencelists/default") - Log.d(TAG, "getDietaryPreferences: GET $url") + if (BuildConfig.DEBUG) { + Log.d(TAG, "getDietaryPreferences: GET $url") + } return runCatching { val response: HttpResponse = client.get(url) { authHeaders(accessToken).forEach { (k, v) -> header(k, v) } accept(ContentType.Application.Json) } val body = response.bodyAsText() - Log.d(TAG, "getDietaryPreferences: status=${response.status.value} bodyLength=${body.length}") + if (BuildConfig.DEBUG) { + Log.d(TAG, "getDietaryPreferences: status=${response.status.value} bodyLength=${body.length}") + } if (!response.status.isSuccess()) { Log.e(TAG, "getDietaryPreferences: failed ${response.status.value} $body") throw IllegalStateException("GET failed: ${response.status.value}") } json.decodeFromString>(body) }.onSuccess { list -> - Log.d(TAG, "getDietaryPreferences: success count=${list.size}") + if (BuildConfig.DEBUG) { + Log.d(TAG, "getDietaryPreferences: success count=${list.size}") + } }.onFailure { e -> Log.e(TAG, "getDietaryPreferences: error", e) } @@ -98,7 +106,9 @@ class DietaryPreferenceRepository { val path = if (id != null) "preferencelists/default/$id" else "preferencelists/default" val method = if (id != null) "PUT" else "POST" val url = baseUrl(path) - Log.d(TAG, "addOrEditDietaryPreference: $method $url clientActivityId=$clientActivityId preferenceLength=${preferenceText.length} id=$id") + if (BuildConfig.DEBUG) { + Log.d(TAG, "addOrEditDietaryPreference: $method $url clientActivityId=$clientActivityId preferenceLength=${preferenceText.length} id=$id") + } return runCatching { val formBody = MultiPartFormDataContent(formData { append("clientActivityId", clientActivityId) @@ -119,7 +129,9 @@ class DietaryPreferenceRepository { } val res = response as HttpResponse val body = res.bodyAsText() - Log.d(TAG, "addOrEditDietaryPreference: status=${res.status.value} bodyLength=${body.length}") + if (BuildConfig.DEBUG) { + Log.d(TAG, "addOrEditDietaryPreference: status=${res.status.value} bodyLength=${body.length}") + } if (!res.status.isSuccess() && res.status.value !in 200..299 && res.status.value != 422) { Log.e(TAG, "addOrEditDietaryPreference: bad status ${res.status.value} $body") throw IllegalStateException("Request failed: ${res.status.value}") @@ -128,7 +140,9 @@ class DietaryPreferenceRepository { }.onSuccess { result -> when (result) { is PreferenceValidationResult.Success -> - Log.d(TAG, "addOrEditDietaryPreference: success id=${result.preference.id}") + if (BuildConfig.DEBUG) { + Log.d(TAG, "addOrEditDietaryPreference: success id=${result.preference.id}") + } is PreferenceValidationResult.Failure -> Log.w(TAG, "addOrEditDietaryPreference: failure explanation=${result.explanation}") } @@ -144,7 +158,9 @@ class DietaryPreferenceRepository { id: Int ): Result { val url = baseUrl("preferencelists/default/$id") - Log.d(TAG, "deleteDietaryPreference: DELETE $url id=$id") + if (BuildConfig.DEBUG) { + Log.d(TAG, "deleteDietaryPreference: DELETE $url id=$id") + } return runCatching { val formBody = MultiPartFormDataContent(formData { append("clientActivityId", clientActivityId) @@ -153,7 +169,9 @@ class DietaryPreferenceRepository { authHeaders(accessToken).forEach { (k, v) -> header(k, v) } setBody(formBody) } - Log.d(TAG, "deleteDietaryPreference: status=${response.status.value}") + if (BuildConfig.DEBUG) { + Log.d(TAG, "deleteDietaryPreference: status=${response.status.value}") + } if (response.status.value != 204 && !response.status.isSuccess()) { throw IllegalStateException("DELETE failed: ${response.status.value}") } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/foodnotes/FoodNotesDto.kt b/app/src/main/java/lc/fungee/Ingredicheck/foodnotes/FoodNotesDto.kt new file mode 100644 index 0000000..6c4069a --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/foodnotes/FoodNotesDto.kt @@ -0,0 +1,26 @@ +package lc.fungee.Ingredicheck.foodnotes + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Response from GET family/food-notes/all. + * Matches iOS FoodNotesAllResponse. + */ +@Serializable +data class FoodNotesAllResponse( + val familyNote: FoodNotesResponse? = null, + val memberNotes: Map = emptyMap() +) + +/** + * Single food note (family or member): content + version for optimistic locking. + * Matches iOS FoodNotesResponse. + */ +@Serializable +data class FoodNotesResponse( + val content: JsonObject, + val version: Int, + @SerialName("updatedAt") val updatedAt: String = "" +) diff --git a/app/src/main/java/lc/fungee/Ingredicheck/foodnotes/FoodNotesRepository.kt b/app/src/main/java/lc/fungee/Ingredicheck/foodnotes/FoodNotesRepository.kt new file mode 100644 index 0000000..8446e00 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/foodnotes/FoodNotesRepository.kt @@ -0,0 +1,262 @@ +package lc.fungee.Ingredicheck.foodnotes + +import android.util.Log +import com.russhwolf.settings.BuildConfig +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.put +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.isSuccess +import java.util.concurrent.TimeUnit +import kotlinx.serialization.Serializable + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import lc.fungee.Ingredicheck.AppConfig + + +private const val TAG = "FoodNotes" +private const val MAX_VERSION_RETRY = 2 + +/** + * Food-notes API: per-member and "Everyone" chip selections (allergies, intolerances, etc.). + * Matches iOS: PUT family/food-notes for Everyone, PUT family/members/{id}/food-notes per member. + */ +class FoodNotesRepository { + + private val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + } + + private val client = HttpClient(OkHttp) { + install(HttpTimeout) { + requestTimeoutMillis = 30_000 + connectTimeoutMillis = 15_000 + socketTimeoutMillis = 30_000 + } + engine { + config { + connectTimeout(15, TimeUnit.SECONDS) + readTimeout(30, TimeUnit.SECONDS) + writeTimeout(30, TimeUnit.SECONDS) + } + } + } + + private fun baseUrl(path: String): String { + val base = AppConfig.supabaseFunctionsURLBase + return if (base.endsWith("/")) base + path else "$base$path" + } + + private fun authHeaders(accessToken: String): Map = mapOf( + "apikey" to AppConfig.supabaseKey, + "Authorization" to "Bearer $accessToken", + "Content-Type" to "application/json" + ) + + @Serializable + data class FoodNotesItem(val name: String, val iconName: String = "") + + @Serializable + data class FoodNotesUpdateRequest( + val content: Map>, + val version: Int + ) + + /** + * Convert onboarding content (stepId -> list of { name, iconName }) to API request format. + */ + private fun toRequestContent(content: Map>>): Map> = + content.mapValues { (_, list) -> + list.map { m -> FoodNotesItem(name = m["name"] ?: "", iconName = m["iconName"] ?: "") } + } + + /** + * GET family/food-notes/all – load family note + all member notes (for versions/cache). + */ + suspend fun fetchFoodNotesAll(accessToken: String): Result { + val url = baseUrl("family/food-notes/all") + if (BuildConfig.DEBUG) { + Log.d(TAG, "fetchFoodNotesAll: GET $url") + } + return runCatching { + val response: HttpResponse = client.get(url) { + authHeaders(accessToken).forEach { (k, v) -> header(k, v) } + accept(ContentType.Application.Json) + } + val body = response.bodyAsText() + when { + response.status.value == 404 -> { + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API: fetchFoodNotesAll 404, no notes yet") + } + return@runCatching null + } + body.isBlank() || body.trim() == "null" -> { + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API: fetchFoodNotesAll null/empty body") + } + return@runCatching null + } + !response.status.isSuccess() -> { + Log.e(TAG, "FoodNotes API: fetchFoodNotesAll failed ${response.status.value} $body") + throw IllegalStateException("GET failed: ${response.status.value}") + } + else -> { + val parsed = json.decodeFromString(body) + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API implementation: fetchFoodNotesAll success β€” data loaded") + } + parsed + } + } + }.onFailure { e -> Log.e(TAG, "FoodNotes API: fetchFoodNotesAll error", e) } + } + + /** + * PUT family/food-notes – update "Everyone" / family note. + * On 409 (version_mismatch), retries once with currentNote.version if present, else version=0. + * @param content Map from step id to list of { "name", "iconName" } (e.g. from OnboardingChipData.buildFoodNotesContentFromChipIds). + */ + suspend fun updateFamilyFoodNotes( + accessToken: String, + content: Map>>, + version: Int, + retryCount: Int = 0 + ): Result { + val requestContent = toRequestContent(content) + val url = baseUrl("family/food-notes") + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API: updateFamilyFoodNotes PUT $url version=$version") + } + return runCatching { + val body = json.encodeToString(FoodNotesUpdateRequest(requestContent, version)) + val response: HttpResponse = client.put(url) { + authHeaders(accessToken).forEach { (k, v) -> header(k, v) } + accept(ContentType.Application.Json) + setBody(body) + } + val responseBody = response.bodyAsText() + if (response.status.isSuccess()) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API implementation: updateFamilyFoodNotes success (Everyone) β€” working") + } + } + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API: updateFamilyFoodNotes status=${response.status.value}") + } + when { + response.status.isSuccess() -> parseFoodNotesResponse(responseBody) + response.status.value == 409 -> { + if (retryCount >= MAX_VERSION_RETRY) { + Log.w(TAG, "updateFamilyFoodNotes: 409 conflict after $MAX_VERSION_RETRY retries, giving up") + throw IllegalStateException("version conflict (family) after $MAX_VERSION_RETRY retries") + } + val currentVersion = parseVersionFrom409Response(responseBody) + val nextVersion = currentVersion ?: 0 + if (BuildConfig.DEBUG) { + Log.d( + TAG, + "updateFamilyFoodNotes: 409 retry=${retryCount + 1} with version=$nextVersion (currentVersion=$currentVersion)" + ) + } + updateFamilyFoodNotes( + accessToken = accessToken, + content = content, + version = nextVersion, + retryCount = retryCount + 1 + ).getOrThrow() + } + else -> throw IllegalStateException("PUT failed: ${response.status.value} $responseBody") + } + }.onFailure { e -> Log.e(TAG, "updateFamilyFoodNotes: error", e) } + } + + /** + * PUT family/members/{memberId}/food-notes – update one member's note. + * @param content Map from step id to list of { "name", "iconName" } (e.g. from OnboardingChipData.buildFoodNotesContentFromChipIds). + */ + suspend fun updateMemberFoodNotes( + accessToken: String, + memberId: String, + content: Map>>, + version: Int, + retryCount: Int = 0 + ): Result { + val requestContent = toRequestContent(content) + val url = baseUrl("family/members/$memberId/food-notes") + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API: updateMemberFoodNotes PUT $url version=$version") + } + return runCatching { + val body = json.encodeToString(FoodNotesUpdateRequest(requestContent, version)) + val response: HttpResponse = client.put(url) { + authHeaders(accessToken).forEach { (k, v) -> header(k, v) } + accept(ContentType.Application.Json) + setBody(body) + } + val responseBody = response.bodyAsText() + if (response.status.isSuccess()) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API implementation: updateMemberFoodNotes success (memberId=$memberId) β€” working") + } + } + if (BuildConfig.DEBUG) { + Log.d(TAG, "FoodNotes API: updateMemberFoodNotes status=${response.status.value}") + } + when { + response.status.isSuccess() -> parseFoodNotesResponse(responseBody) + response.status.value == 409 -> { + if (retryCount >= MAX_VERSION_RETRY) { + Log.w(TAG, "updateMemberFoodNotes: 409 conflict after $MAX_VERSION_RETRY retries (memberId=$memberId), giving up") + throw IllegalStateException("version conflict (memberId=$memberId) after $MAX_VERSION_RETRY retries") + } + val currentVersion = parseVersionFrom409Response(responseBody) + val nextVersion = currentVersion ?: 0 + if (BuildConfig.DEBUG) { + Log.d( + TAG, + "updateMemberFoodNotes: 409 retry=${retryCount + 1} with version=$nextVersion (currentVersion=$currentVersion)" + ) + } + updateMemberFoodNotes( + accessToken = accessToken, + memberId = memberId, + content = content, + version = nextVersion, + retryCount = retryCount + 1 + ).getOrThrow() + } + else -> throw IllegalStateException("PUT failed: ${response.status.value} $responseBody") + } + }.onFailure { e -> Log.e(TAG, "updateMemberFoodNotes: error", e) } + } + + private fun parseFoodNotesResponse(body: String): FoodNotesResponse { + val obj = json.parseToJsonElement(body).jsonObject + val version = obj["version"]?.jsonPrimitive?.content?.toIntOrNull() ?: 0 + val updatedAt = obj["updatedAt"]?.jsonPrimitive?.content ?: "" + val content = obj["content"]?.jsonObject ?: buildJsonObject { } + return FoodNotesResponse(content = content, version = version, updatedAt = updatedAt) + } + + private fun parseVersionFrom409Response(body: String): Int? { + return try { + val obj = json.parseToJsonElement(body).jsonObject + val currentNote = obj["currentNote"]?.jsonObject ?: return null + currentNote["version"]?.jsonPrimitive?.content?.toIntOrNull() + } catch (_: Exception) { + null + } + } +} diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/DynamicStepsDto.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/DynamicStepsDto.kt new file mode 100644 index 0000000..120305c --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/DynamicStepsDto.kt @@ -0,0 +1,62 @@ +package lc.fungee.Ingredicheck.onboarding.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Root payload from dynamicJsonData.json (same structure as iOS). */ +@Serializable +data class DynamicStepsPayload( + val steps: List +) + +@Serializable +data class DynamicStep( + val id: String, + val type: String, + val header: DynamicStepHeader, + val content: DynamicStepContent +) + +@Serializable +data class DynamicStepHeader( + val iconUrl: String, + val name: String, + val individual: DynamicHeaderVariant, + val family: DynamicHeaderVariant, + val singleMember: DynamicHeaderVariant? = null +) + +@Serializable +data class DynamicHeaderVariant( + val question: String, + val description: String? = null +) + +@Serializable +data class DynamicStepContent( + val options: List? = null, + val subSteps: List? = null, + val regions: List? = null +) + +@Serializable +data class DynamicOption( + val name: String, + val icon: String +) + +@Serializable +data class DynamicSubStep( + val id: String, + val title: String, + val description: String, + val color: String, + @SerialName("bgImageUrl") val bgImageUrl: String, + val options: List? = null +) + +@Serializable +data class DynamicRegion( + val name: String, + val subRegions: List +) diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/DynamicStepsLoader.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/DynamicStepsLoader.kt new file mode 100644 index 0000000..b04abe0 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/DynamicStepsLoader.kt @@ -0,0 +1,68 @@ +package lc.fungee.Ingredicheck.onboarding.data + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.io.IOException + +private const val ASSET_FILE = "dynamicJsonData.json" +private const val TAG = "DynamicJsonData" + +private val json = Json { ignoreUnknownKeys = true } + +/** + * Loads and caches onboarding steps from assets/dynamicJsonData.json (same as iOS). + * Call [ensureLoaded] when you have a context (e.g. from OnboardingHost); then + * [OnboardingChipData] will use dynamic steps when [getSteps] is non-null. + */ +object DynamicStepsLoader { + + @Volatile + private var cachedSteps: List? = null + + /** Returns cached steps if already loaded; null if not loaded or load failed. */ + fun getSteps(): List? = cachedSteps + + /** Loads from assets if not yet loaded. Safe to call multiple times. */ + suspend fun ensureLoaded(context: Context): List? { + // Fast path: already cached in memory. + cachedSteps?.let { + Log.d(TAG, "JSON data: using cached steps (count=${it.size}), already loaded β€” UI will use same data after restart") + return it + } + + // Do disk I/O + JSON parsing on a background thread so we don't block the UI. + val steps = withContext(Dispatchers.IO) { + loadFromAssets(context) + } + + if (steps != null) { + cachedSteps = steps + Log.d(TAG, "JSON data: loaded successfully from assets. steps count=${steps.size}, stepIds=${steps.map { it.id }}") + } else { + Log.w(TAG, "JSON data: load failed β€” no steps available; UI will have empty onboarding steps") + } + return steps + } + + private fun loadFromAssets(context: Context): List? { + Log.d(TAG, "JSON data: loading from assets/$ASSET_FILE...") + return try { + context.assets.open(ASSET_FILE).use { input -> + val text = input.bufferedReader().use { it.readText() } + val payload = json.decodeFromString(text) + val steps = payload.steps + Log.d(TAG, "JSON data: parse OK. steps count=${steps.size} β€” all cases (including after restart) will use this data") + steps + } + } catch (e: IOException) { + Log.e(TAG, "JSON data: load failed (IOException) ${e.message}", e) + null + } catch (e: kotlinx.serialization.SerializationException) { + Log.e(TAG, "JSON data: load failed (parse error) ${e.message}", e) + null + } + } +} diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/OnboardingData.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/OnboardingData.kt index 36df614..5ae6188 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/OnboardingData.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/data/OnboardingData.kt @@ -5,6 +5,16 @@ import lc.fungee.Ingredicheck.R /** Member id used for "Everyone" in add-family and just-me flows. */ const val EVERYONE_MEMBER_ID = "ALL" +/** + * Flow type for onboarding questions - determines which question text to show. + */ +enum class OnboardingFlowType { + /** "Just Me" / Individual flow */ + INDIVIDUAL, + /** "Add Family" flow */ + FAMILY +} + /** * Static configuration for the multi‑step fine‑tune flow (allergy/sensitivity/health/life‑stage chips) * and shared avatar lists. Keeping this data out of the UI layer keeps @@ -46,368 +56,144 @@ data class AvoidCardDefinition( object OnboardingChipData { + private fun slug(name: String): String = + name.lowercase().replace(Regex("[^a-z0-9]"), "_").replace(Regex("_+"), "_").trim('_') + + private fun dynamicStepToChips(step: DynamicStep): List { + return when (step.type) { + "type-1" -> step.content.options?.map { o -> + ChipDefinition(slug(o.name), o.name, o.icon + " ") + } ?: emptyList() + "type-2" -> step.content.subSteps?.flatMap { sub -> + (sub.options ?: emptyList()).map { o -> + ChipDefinition("${sub.id}_${slug(o.name)}", o.name, o.icon + " ") + } + } ?: emptyList() + "type-3" -> step.content.regions?.flatMap { r -> + r.subRegions.map { o -> + ChipDefinition("${slug(r.name)}_${slug(o.name)}", o.name, o.icon + " ") + } + } ?: emptyList() + else -> emptyList() + } + } + + private fun dynamicSteps(): List? = DynamicStepsLoader.getSteps() + /** * Returns the (question, subtitle) pair for the given fine‑tune step. + * @param step Step index (0-9) + * @param flowType Flow type (INDIVIDUAL for "Just Me", FAMILY for "Add Family") */ - fun questionForStep(step: Int): Pair { - return when (step.coerceIn(0, 9)) { - 0 -> "Does anyone in your IngrediFam have allergies we should know?" to - "Select all that apply to keep meals worry-free." - 1 -> "Any sensitivities or intolerances in your IngrediFam?" to - "We’ll avoid foods that cause discomfort." - 2 -> "Any doctor diets or health conditions in your IngrediFam?" to - "This helps us tailor recommendations better." - 3 -> "Does anyone in your IngrediFam have special life stage needs?" to - "Select all that apply so tips match every life stage." - // 4 – Region / cultural practices - 4 -> "Where are you from? This helps us customize your experience!" to - "Pick your region(s) or cultural practices." - 5 -> "Anything your IngrediFam avoids?" to - "We’ll steer clear of those ingredients and products." - // 6 – LifeStyle (plant/balance, quality/source, sustainable living) - 6 -> "What’s your lifestyle when it comes to food?" to - "Tell us about your eating style, sourcing, and habits." - // 7 – Nutrition (macros, sugar/fiber, diet frameworks) - 7 -> "How do you like to approach nutrition?" to - "Set your macronutrient goals, sugar & fiber preferences, and diet patterns." - else -> "Does anyone in your IngrediFam have allergies we should know?" to - "Select all that apply to keep meals worry-free." + fun questionForStep(step: Int, flowType: OnboardingFlowType = OnboardingFlowType.FAMILY): Pair { + val steps = dynamicSteps() + if (steps != null && step in steps.indices) { + val h = steps[step].header + val v = if (flowType == OnboardingFlowType.INDIVIDUAL) h.individual else h.family + return v.question to (v.description ?: "") } + return "" to "" } /** * Returns the chip set (id, label, emoji prefix) for the given fine‑tune step. */ fun chipsForStep(step: Int): List { - return when (step.coerceIn(0, 9)) { - // 0 – Allergies - 0 -> listOf( - ChipDefinition("peanuts", "Peanuts", "πŸ₯œ "), - ChipDefinition("tree_nuts", "Tree nuts", "🌰 "), - ChipDefinition("dairy", "Dairy", "πŸ₯› "), - ChipDefinition("eggs", "Eggs", "πŸ₯š "), - ChipDefinition("soy", "Soy", "🌱 "), - ChipDefinition("wheat", "Wheat", "🌾 "), - ChipDefinition("fish", "Fish", "🐟 "), - ChipDefinition("shellfish", "Shellfish", "🍀 "), - ChipDefinition("sesame", "Sesame", "✨ "), - ChipDefinition("celery", "Celery", "πŸ₯¬ "), - ChipDefinition("lupin", "Lupin", "🫘 "), - ChipDefinition("sulphites", "Sulphites", "πŸ§‚ "), - ChipDefinition("mustard", "Mustard", "🟑 "), - ChipDefinition("molluscs", "Molluscs", "🐚 "), - ChipDefinition("other", "Other", "✏️ ") - ) - - // 1 – Sensitivities / intolerances - 1 -> listOf( - ChipDefinition("lactose", "Lactose", "πŸ₯› "), - ChipDefinition("fructose", "Fructose", "πŸ“ "), - ChipDefinition("histamine", "Histamine", "🍷 "), - ChipDefinition("gluten_wheat", "Gluten / wheat", "🌾 "), - ChipDefinition("fodmap", "FODMAP", "πŸ§„ "), - ChipDefinition("other_sens", "Other", "✏️ ") - ) - - // 2 – Health conditions / doctor diets - 2 -> listOf( - ChipDefinition("diabetes", "Diabetes", "🍭 "), - ChipDefinition("hypertension", "Hypertension", "πŸ’Š "), - ChipDefinition("kidney_disease", "Kidney disease", "🩺 "), - ChipDefinition("heart_health", "Heart health", "\uD83E\uDEC0 "), - ChipDefinition("pku", "PKU (phenylalanine-sensitive)", "🧬 "), - ChipDefinition("anti_inflammatory", "Anti-inflammatory / medical diet", "πŸ₯— "), - ChipDefinition("celiac_disease", "Celiac disease", "πŸ₯– "), - ChipDefinition("other_health", "Other", "✏️ ") - ) - - // 3 – Life stage needs - 3 -> listOf( - ChipDefinition("kids_baby_friendly", "Kids / Baby-friendly foods", "πŸ‘Ά "), - ChipDefinition("toddler_picky", "Toddler picky-eating adaptations", "πŸ™„ "), - ChipDefinition("pregnancy_prenatal", "Pregnancy / Prenatal nutrition", "🀰 "), - ChipDefinition("breastfeeding", "Breastfeeding diets", "🍼 "), - ChipDefinition("senior_friendly", "Senior-friendly", "πŸ‘΄ "), - ChipDefinition("none_lifestage", "None of these apply", "βœ… ") - ) - - // 4 – Region / cultural practices (subRegions as chips for capsule display) - 4 -> regions.flatMap { it.subRegions } + val steps = dynamicSteps() + if (steps != null && step in steps.indices) { + return dynamicStepToChips(steps[step]) + } + return emptyList() + } - // 5 – Avoid (stacked card options as chips for capsule display) - 5 -> avoidCards.flatMap { it.options }.map { o -> - ChipDefinition(o.id, o.label, o.iconPrefix) + // Avoid stacked cards (type-2) from dynamic JSON "avoid" step. + val avoidCards: List + get() { + dynamicSteps()?.find { it.id == "avoid" }?.content?.subSteps?.let { subSteps -> + return subSteps.map { sub -> + AvoidCardDefinition( + id = sub.id, + title = sub.title, + description = sub.description, + colorHex = sub.color, + options = (sub.options ?: emptyList()).map { o -> + AvoidOptionDefinition("${sub.id}_${slug(o.name)}", o.name, o.icon + " ") + } + ) + } } + return emptyList() + } - // 6 – LifeStyle (stacked card options for capsule display) - 6 -> lifestyleCards.flatMap { it.options }.map { o -> - ChipDefinition(o.id, o.label, o.iconPrefix) + /** LifeStyle stacked cards from dynamic JSON "lifeStyle" step. */ + val lifestyleCards: List + get() { + dynamicSteps()?.find { it.id == "lifeStyle" }?.content?.subSteps?.let { subSteps -> + return subSteps.map { sub -> + AvoidCardDefinition( + id = sub.id, + title = sub.title, + description = sub.description, + colorHex = sub.color, + options = (sub.options ?: emptyList()).map { o -> + AvoidOptionDefinition("${sub.id}_${slug(o.name)}", o.name, o.icon + " ") + } + ) + } } + return emptyList() + } - // 7 – Nutrition (stacked card options for capsule display) - 7 -> nutritionCards.flatMap { it.options }.map { o -> - ChipDefinition(o.id, o.label, o.iconPrefix) + /** Nutrition stacked cards from dynamic JSON "nutrition" step. */ + val nutritionCards: List + get() { + dynamicSteps()?.find { it.id == "nutrition" }?.content?.subSteps?.let { subSteps -> + return subSteps.map { sub -> + AvoidCardDefinition( + id = sub.id, + title = sub.title, + description = sub.description, + colorHex = sub.color, + options = (sub.options ?: emptyList()).map { o -> + AvoidOptionDefinition("${sub.id}_${slug(o.name)}", o.name, o.icon + " ") + } + ) + } } - - // 8 – Ethical preferences - 8 -> listOf( - ChipDefinition("ethical_animal_welfare", "Animal welfare focused", "πŸ„ "), - ChipDefinition("ethical_fair_trade", "Fair trade", "🀝 "), - ChipDefinition( - "ethical_sustainable_fishing", - "Sustainable fishing / no overfished species", - "🐟 " - ), - ChipDefinition("ethical_low_carbon", "Low carbon footprint foods", "♻️ "), - ChipDefinition("ethical_water_footprint", "Water footprint concerns", "πŸ’§ "), - ChipDefinition("ethical_palm_oil_free", "Palm-oil free", "🌴 "), - ChipDefinition("ethical_plastic_free_packaging", "Plastic-free packaging", "🚫 "), - ChipDefinition("ethical_other", "Other", "✏️ ") - ) - - // 9 – Taste preferences - 9 -> listOf( - ChipDefinition("taste_spicy_lover", "Spicy lover", "🌢️ "), - ChipDefinition("taste_avoid_spicy", "Avoid Spicy", "🚫 "), - ChipDefinition("taste_sweet_tooth", "Sweet tooth", "🍰 "), - ChipDefinition("taste_avoid_slimy", "Avoid slimy textures", "πŸ₯’ "), - ChipDefinition("taste_avoid_bitter", "Avoid bitter foods", "🍡 "), - ChipDefinition("taste_other", "Other", "✏️ "), - ChipDefinition("taste_crunchy_soft", "Crunchy / Soft preferences", "πŸͺ "), - ChipDefinition("taste_low_sweet", "Low-sweet preference", "🍯 ") - ) - - else -> chipsForStep(0) + return emptyList() } - } - // Avoid stacked cards (type-2) used for the Avoid step. - val avoidCards: List = listOf( - AvoidCardDefinition( - id = "avoid_oils_fats", - title = "Oils & Fats", - description = "In fats or oils, what do you avoid?", - colorHex = "#FFF6B3", - options = listOf( - AvoidOptionDefinition("avoid_oils_trans_fats", "Hydrogenated oils / Trans fats", "🧈 "), - AvoidOptionDefinition("avoid_oils_seed", "Canola / Seed oils", "🌾 "), - AvoidOptionDefinition("avoid_oils_palm", "Palm oil", "🌴 "), - AvoidOptionDefinition("avoid_oils_corn_hfcs", "Corn / High-fructose corn syrup", "🌽 ") - ) - ), - AvoidCardDefinition( - id = "avoid_animal_based", - title = "Animal-Based", - description = "Any animal products you don't consume?", - colorHex = "#DCC7F6", - options = listOf( - AvoidOptionDefinition("avoid_animal_pork", "Pork", "πŸ– "), - AvoidOptionDefinition("avoid_animal_beef", "Beef", "πŸ„ "), - AvoidOptionDefinition("avoid_animal_honey", "Honey", "🍯 "), - AvoidOptionDefinition("avoid_animal_gelatin", "Gelatin / Rennet", "πŸ§‚ "), - AvoidOptionDefinition("avoid_animal_shellfish", "Shellfish", "🦐 "), - AvoidOptionDefinition("avoid_animal_insects", "Insects", "🐜 "), - AvoidOptionDefinition("avoid_animal_seafood", "Seafood (fish)", "🐟 "), - AvoidOptionDefinition("avoid_animal_lard", "Lard / Animal fat", "πŸ– ") - ) - ), - AvoidCardDefinition( - id = "avoid_stimulants_substances", - title = "Stimulants & Substances", - description = "Do you avoid these?", - colorHex = "#BFF0D4", - options = listOf( - AvoidOptionDefinition("avoid_stim_alcohol", "Alcohol", "🍷 "), - AvoidOptionDefinition("avoid_stim_caffeine", "Caffeine", "β˜• ") - ) - ), - AvoidCardDefinition( - id = "avoid_additives_sweeteners", - title = "Additives & Sweeteners", - description = "Do you stay away from processed ingredients?", - colorHex = "#FFD9B5", - options = listOf( - AvoidOptionDefinition("avoid_add_msg", "MSG", "βš—οΈ "), - AvoidOptionDefinition("avoid_add_artificial_sweeteners", "Artificial sweeteners", "🍬 "), - AvoidOptionDefinition("avoid_add_preservatives", "Preservatives", "πŸ§‚ "), - AvoidOptionDefinition("avoid_add_refined_sugar", "Refined sugar", "🍚 "), - AvoidOptionDefinition("avoid_add_corn_syrup", "Corn syrup / HFCS", "🌽 "), - AvoidOptionDefinition("avoid_add_stevia_monk", "Stevia / Monk fruit", "🍈 ") - ) - ), - AvoidCardDefinition( - id = "avoid_plant_based_restrictions", - title = "Plant-Based Restrictions", - description = "Any plant foods you avoid?", - colorHex = "#F9C6D0", - options = listOf( - AvoidOptionDefinition("avoid_plant_nightshades", "Nightshades (paprika, peppers, etc.)", "πŸ… "), - AvoidOptionDefinition("avoid_plant_garlic_onion", "Garlic / Onion", "πŸ§„ ") - ) - ) - ) - - /** LifeStyle stacked cards (3 cards): Plant & Balance, Quality & Source, Sustainable Living. */ - val lifestyleCards: List = listOf( - AvoidCardDefinition( - id = "lifestyle_plant_balance", - title = "Plant & Balance", - description = "Do you follow a plant-forward or flexible eating style?", - colorHex = "#FFF6B3", - options = listOf( - AvoidOptionDefinition("lifestyle_plant_vegetarian", "Vegetarian", "πŸ₯¦ "), - AvoidOptionDefinition("lifestyle_plant_vegan", "Vegan", "🌱 "), - AvoidOptionDefinition("lifestyle_plant_flexitarian", "Flexitarian", "πŸ”„ "), - AvoidOptionDefinition("lifestyle_plant_reducetarian", "Reducetarian", "βž– "), - AvoidOptionDefinition("lifestyle_plant_pescatarian", "Pescatarian", "🐟 "), - AvoidOptionDefinition("lifestyle_plant_other", "Other", "✏️ ") - ) - ), - AvoidCardDefinition( - id = "lifestyle_quality_source", - title = "Quality & Source", - description = "Do you care about where your food comes from and how it's grown?", - colorHex = "#DCC7F6", - options = listOf( - AvoidOptionDefinition("lifestyle_quality_organic", "Organic Only", "🌱 "), - AvoidOptionDefinition("lifestyle_quality_nongmo", "Non-GMO", "🧬 "), - AvoidOptionDefinition("lifestyle_quality_local", "Locally Sourced", "πŸ“ "), - AvoidOptionDefinition("lifestyle_quality_seasonal", "Seasonal Eater", "πŸ•°οΈ ") - ) - ), - AvoidCardDefinition( - id = "lifestyle_sustainable_living", - title = "Sustainable Living", - description = "Are you mindful of waste, packaging, and ingredient transparency?", - colorHex = "#D7EEB2", - options = listOf( - AvoidOptionDefinition("lifestyle_sustainable_zerowaste", "Zero-Waste / Minimal Packing", "🌍 "), - AvoidOptionDefinition("lifestyle_sustainable_clean_label", "Clean Label", "βœ… ") - ) - ) - ) - - /** Nutrition stacked cards (3 cards): Macronutrient Goals, Sugar & Fiber, Diet Frameworks & Patterns. */ - val nutritionCards: List = listOf( - AvoidCardDefinition( - id = "nutrition_macronutrient_goals", - title = "Macronutrient Goals", - description = "Do you want to balance your proteins, carbs, and fats or focus on one?", - colorHex = "#F9C6D0", - options = listOf( - AvoidOptionDefinition("nutrition_macro_high_protein", "High Protein", "πŸ— "), - AvoidOptionDefinition("nutrition_macro_low_carb", "Low Carb", "πŸ₯’ "), - AvoidOptionDefinition("nutrition_macro_low_fat", "Low Fat", "πŸ₯‘ "), - AvoidOptionDefinition("nutrition_macro_balanced", "Balanced Macros", "βš–οΈ ") - ) - ), - AvoidCardDefinition( - id = "nutrition_sugar_fiber", - title = "Sugar & Fiber", - description = "Do you prefer low sugar or high-fiber foods for better digestion and energy?", - colorHex = "#A7D8F0", - options = listOf( - AvoidOptionDefinition("nutrition_sugar_low", "Low Sugar", "πŸ“ "), - AvoidOptionDefinition("nutrition_sugar_free", "Sugar-Free", "🍭 "), - AvoidOptionDefinition("nutrition_fiber_high", "High Fiber", "🌾 ") - ) - ), - AvoidCardDefinition( - id = "nutrition_diet_frameworks_patterns", - title = "Diet Frameworks & Patterns", - description = "Do you follow a structured eating plan or experiment with fasting?", - colorHex = "#FFD9B5", - options = listOf( - AvoidOptionDefinition("nutrition_diet_keto", "Keto", "πŸ₯‘ "), - AvoidOptionDefinition("nutrition_diet_dash", "DASH", "πŸ’§ "), - AvoidOptionDefinition("nutrition_diet_paleo", "Paleo", "πŸ₯© "), - AvoidOptionDefinition("nutrition_diet_mediterranean", "Mediterranean", "πŸ«’ "), - AvoidOptionDefinition("nutrition_diet_whole30", "Whole30", "πŸ₯— "), - AvoidOptionDefinition("nutrition_diet_fasting", "Fasting", "πŸ•‘ "), - AvoidOptionDefinition("nutrition_diet_other", "Other", "✏️ ") - ) - ) - ) - - /** - * Static definition of cultural / regional food traditions used on the - * "Where does your IngrediFam draw its food traditions from?" step. - * - * Mirrors the iOS `regions` JSON structure (DynamicRegionsQuestionView), - * but reuses `ChipDefinition` for sub‑regions so selections behave like - * normal chips on Android. - */ - val regions: List = listOf( - RegionDefinition( - name = "India & South Asia", - subRegions = listOf( - ChipDefinition("region_india_ayurveda", "Ayurveda", "🌿 "), - ChipDefinition("region_india_hindu_traditions", "Hindu food traditions", "πŸ•‰ "), - ChipDefinition("region_india_jain_diet", "Jain diet", "πŸ§˜β€β™‚οΈ "), - ChipDefinition("region_india_other", "Other", "✏️ ") - ) - ), - RegionDefinition( - name = "Africa", - subRegions = listOf( - ChipDefinition("region_africa_rastafarian_ital", "Rastafarian Ital diet", "πŸ₯— "), - ChipDefinition("region_africa_ethiopian_orthodox", "Ethiopian Orthodox fasting", "πŸ₯– "), - ChipDefinition("region_africa_other", "Other", "✏️ ") - ) - ), - RegionDefinition( - name = "Middle East & Mediterranean", - subRegions = listOf( - ChipDefinition("region_middleeast_halal", "Halal (Islamic dietary laws)", "β˜ͺ️ "), - ChipDefinition("region_middleeast_kosher", "Kosher (Jewish dietary laws)", "✑️ "), - ChipDefinition("region_middleeast_mediterranean", "Greek / Mediterranean diets", "πŸ«’ "), - ChipDefinition("region_middleeast_other", "Other", "✏️ ") - ) - ), - RegionDefinition( - name = "East Asia", - subRegions = listOf( - ChipDefinition("region_eastasia_tcm", "Traditional Chinese Medicine (TCM)", "🧧 "), - ChipDefinition("region_eastasia_buddhist_rules", "Buddhist food rules", "🧘 "), - ChipDefinition("region_eastasia_macrobiotic", "Japanese Macrobiotic diet", "πŸ™ "), - ChipDefinition("region_eastasia_other", "Other", "✏️ ") - ) - ), - RegionDefinition( - name = "Western / Native traditions", - subRegions = listOf( - ChipDefinition("region_western_native_american", "Native American traditions", "πŸͺΆ "), - ChipDefinition("region_western_christian", "Christian traditions", "✝️ "), - ChipDefinition("region_western_other", "Other", "✏️ ") - ) - ), - RegionDefinition( - name = "Seventh-day Adventist", - subRegions = listOf( - ChipDefinition("region_sda_seventh_day_adventist", "Seventh-day Adventist", "✝️ ") - ) - ), - RegionDefinition( - name = "Other", - subRegions = listOf( - ChipDefinition("region_other_other", "Other", "✏️ ") - ) - ) - ) + /** Cultural / regional food traditions from dynamic JSON "region" step. */ + val regions: List + get() { + dynamicSteps()?.find { it.id == "region" }?.content?.regions?.let { regs -> + return regs.map { r -> + RegionDefinition( + name = r.name, + subRegions = r.subRegions.map { o -> + ChipDefinition("${slug(r.name)}_${slug(o.name)}", o.name, o.icon + " ") + } + ) + } + } + return emptyList() + } /** * Resolves a chip id to its definition (label + emoji) from any step. * Used to display selected chips in the CapsuleSkeletonBox. */ fun chipForId(id: String): ChipDefinition? { - for (step in 0..9) { + val steps = dynamicSteps() + val maxStep = (steps?.size ?: 0) - 1 + for (step in 0..maxStep) { chipsForStep(step).find { it.id == id }?.let { return it } } return null } /** - * Shared avatar lists used by multiple onboarding screens. + * Base avatar items mapping avatar IDs to drawable resource IDs. */ val baseAvatarItems: List> = listOf( "baby_boy" to R.drawable.family_member_baby, @@ -426,14 +212,66 @@ object OnboardingChipData { "tomato_avtar" to R.drawable.avtar_tomato ) + /** + * Avatar items for editing (same as baseAvatarItems). + */ val editAvatarItems: List> = baseAvatarItems - /** Resolve avatar id to drawable resource id; null if not found. Shared by Host and screens. */ + /** + * Resolve avatar id to drawable resource id; null if not found. + * Shared by Host and screens. + */ fun avatarResOrNull(avatarId: String): Int? = baseAvatarItems.firstOrNull { (id, _) -> id == avatarId }?.second /** Resolve chip id to display label for dietary preference sync; returns id if not found. */ - fun labelForChipId(chipId: String): String = - (0..9).flatMap { chipsForStep(it) }.firstOrNull { it.id == chipId }?.label ?: chipId + fun labelForChipId(chipId: String): String { + val steps = dynamicSteps() + val maxStep = (steps?.size ?: 0) - 1 + return (0..maxStep).flatMap { chipsForStep(it) }.firstOrNull { it.id == chipId }?.label ?: chipId + } + + /** Step IDs in order from dynamic JSON (food-notes API). */ + val foodNotesStepIds: List + get() = dynamicSteps()?.map { it.id } ?: emptyList() + + /** Map step id to drawable for CapsuleStepperRow / section headers. */ + fun iconResForStepId(stepId: String): Int = when (stepId) { + "allergies" -> R.drawable.ic_step_allergies + "intolerances" -> R.drawable.ic_step_intolerances + "healthConditions" -> R.drawable.ic_step_health_conditions + "lifeStage" -> R.drawable.ic_step_life_style + "region" -> R.drawable.ic_step_region + "avoid" -> R.drawable.ic_step_avoid_cross + "lifeStyle" -> R.drawable.ic_step_diet_preferences + "nutrition" -> R.drawable.ic_step_meals + "ethical" -> R.drawable.ic_step_ethical + "taste" -> R.drawable.iconoir_chocolate + else -> R.drawable.ic_step_allergies + } + + /** + * Build food-notes API content from a set of selected chip IDs (for one member or Everyone). + * Matches iOS buildContentFromPreferences: step id -> list of { "name", "iconName" }. + */ + fun buildFoodNotesContentFromChipIds(chipIds: Set): Map>> { + if (chipIds.isEmpty()) return emptyMap() + val content = mutableMapOf>>() + val stepIds = foodNotesStepIds + for (stepIndex in stepIds.indices) { + val stepId = stepIds.getOrNull(stepIndex) ?: continue + val chips = chipsForStep(stepIndex) + val selected = chips.filter { it.id in chipIds }.map { chip -> + mapOf( + "name" to chip.label, + "iconName" to (chip.iconPrefix.trim().ifEmpty { "" }) + ) + } + if (selected.isNotEmpty()) { + content[stepId] = selected.toMutableList() + } + } + return content + } } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingPersistence.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingPersistence.kt index 8b30e6a..472a3bf 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingPersistence.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingPersistence.kt @@ -1,6 +1,7 @@ package lc.fungee.Ingredicheck.onboarding.model import android.content.Context +import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey @@ -29,6 +30,9 @@ class OnboardingPersistence( private val KEY_ADD_FAMILY_GENERATED_AVATAR_URL = stringPreferencesKey("onboarding_add_family_generated_avatar_url") private val KEY_MEMOJI_GENERATION_COMPLETED = booleanPreferencesKey("onboarding_memoji_generation_completed") private val KEY_FAMILY_OVERVIEW_MEMBERS = stringPreferencesKey("onboarding_family_overview_members") + private val KEY_SELECTED_ALLERGIES_BY_MEMBER = stringPreferencesKey("onboarding_selected_allergies_by_member") + private val KEY_SELECTED_ALLERGY_MEMBER_ID = stringPreferencesKey("onboarding_selected_allergy_member_id") + private val KEY_ALLERGY_STEP_INDEX = stringPreferencesKey("onboarding_allergy_step_index") } data class SavedState( @@ -125,4 +129,51 @@ class OnboardingPersistence( prefs[KEY_FAMILY_OVERVIEW_MEMBERS] = Json.encodeToString(familyOverviewMembers) } } + + /** + * Save allergy selections state (selected chips/cards per member and current step index). + */ + suspend fun setAllergySelectionsState( + selectedAllergiesByMember: Map>, + selectedAllergyMemberId: String, + allergyStepIndex: Int + ) { + Log.d( + "OnboardingAllergies", + "[PERSIST] setAllergySelectionsState selections=$selectedAllergiesByMember " + + "selectedMember=$selectedAllergyMemberId stepIndex=$allergyStepIndex" + ) + context.onboardingDataStore.edit { prefs -> + // Convert Map> to JSON + val selectionsJson = Json.encodeToString( + selectedAllergiesByMember.mapValues { it.value.toList() } + ) + prefs[KEY_SELECTED_ALLERGIES_BY_MEMBER] = selectionsJson + prefs[KEY_SELECTED_ALLERGY_MEMBER_ID] = selectedAllergyMemberId + prefs[KEY_ALLERGY_STEP_INDEX] = allergyStepIndex.toString() + } + } + + /** + * Get allergy selections state (selected chips/cards per member and current step index). + */ + suspend fun getAllergySelectionsState(): Triple>, String, Int> { + val prefs = context.onboardingDataStore.data.first() + val selectionsJson = prefs[KEY_SELECTED_ALLERGIES_BY_MEMBER].orEmpty() + val selectedAllergiesByMember = if (selectionsJson.isNotBlank()) { + runCatching { + Json.decodeFromString>>(selectionsJson) + .mapValues { it.value.toSet() } + }.getOrElse { emptyMap() } + } else emptyMap() + val selectedAllergyMemberId = prefs[KEY_SELECTED_ALLERGY_MEMBER_ID].orEmpty() + val allergyStepIndex = prefs[KEY_ALLERGY_STEP_INDEX]?.toIntOrNull() ?: 0 + Log.d( + "OnboardingAllergies", + "[RESTORE] getAllergySelectionsState selections=$selectedAllergiesByMember " + + "selectedMember=$selectedAllergyMemberId stepIndex=$allergyStepIndex " + + "jsonLength=${selectionsJson.length}" + ) + return Triple(selectedAllergiesByMember, selectedAllergyMemberId, allergyStepIndex) + } } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingViewModel.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingViewModel.kt index d9058e0..25bb5e1 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingViewModel.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/model/OnboardingViewModel.kt @@ -263,6 +263,12 @@ class OnboardingViewModel( memojiGenerationCompleted = false, familyOverviewMembers = emptyList() ) + // Clear allergy selections state + persistence.setAllergySelectionsState( + selectedAllergiesByMember = emptyMap(), + selectedAllergyMemberId = "", + allergyStepIndex = 0 + ) } } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/AllergyScreens.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/AllergyScreens.kt index bd5832e..1244a03 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/AllergyScreens.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/AllergyScreens.kt @@ -65,12 +65,16 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.Devices import coil.compose.AsyncImage import lc.fungee.Ingredicheck.R import lc.fungee.Ingredicheck.onboarding.data.EVERYONE_MEMBER_ID import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData +import lc.fungee.Ingredicheck.onboarding.data.OnboardingFlowType import lc.fungee.Ingredicheck.onboarding.data.RegionDefinition import lc.fungee.Ingredicheck.onboarding.data.AvoidOptionDefinition +import lc.fungee.Ingredicheck.onboarding.data.AvoidCardDefinition import lc.fungee.Ingredicheck.onboarding.data.avatarBackgroundColorForId import lc.fungee.Ingredicheck.onboarding.data.memberAvatarBackgroundColor import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModel @@ -79,6 +83,7 @@ import lc.fungee.Ingredicheck.ui.components.buttons.primaryButtonEffect import lc.fungee.Ingredicheck.ui.components.buttons.primaryChipEffect import lc.fungee.Ingredicheck.ui.components.buttons.PrimaryButton import lc.fungee.Ingredicheck.ui.components.buttons.SecondaryButton +import lc.fungee.Ingredicheck.ui.components.NonDraggableBottomSheet import lc.fungee.Ingredicheck.ui.theme.Greyscale10 import lc.fungee.Ingredicheck.ui.theme.Greyscale40 import lc.fungee.Ingredicheck.ui.theme.Greyscale70 @@ -90,8 +95,11 @@ import lc.fungee.Ingredicheck.ui.theme.Greyscale30 import lc.fungee.Ingredicheck.ui.theme.Manrope import lc.fungee.Ingredicheck.ui.theme.Nunito import lc.fungee.Ingredicheck.ui.theme.NunitoSemiBold +import lc.fungee.Ingredicheck.ui.theme.NunitoBold import lc.fungee.Ingredicheck.ui.theme.Primary700 import lc.fungee.Ingredicheck.onboarding.ui.OnboardingAnimations +import lc.fungee.Ingredicheck.ui.chatbot.ChatBotIntroScreen +import lc.fungee.Ingredicheck.ui.chatbot.ChatBotConversationScreen @Composable internal fun AddAllergiesSheet( @@ -103,12 +111,22 @@ internal fun AddAllergiesSheet( onNext: () -> Unit, onSkipPreferences: () -> Unit = {}, showFineTuneDecision: Boolean = false, + showSummaryScreen: Boolean = false, + hasOtherSelection: Boolean = false, + showChatBotIntro: Boolean = false, + showChatConversation: Boolean = false, + onChatBotLetsGo: () -> Unit = {}, + onChatSkip: () -> Unit = {}, questionStepIndex: Int = 0 ) { val everyoneId = EVERYONE_MEMBER_ID val fallbackMembers = remember(members) { if (members.isNotEmpty()) members else emptyList() } + // Determine flow type: INDIVIDUAL if no members (just me flow), FAMILY if members exist + val flowType = remember(members) { + if (members.isEmpty()) OnboardingFlowType.INDIVIDUAL else OnboardingFlowType.FAMILY + } val resolvedSelectedId = remember(selectedMemberId, fallbackMembers) { when { selectedMemberId == everyoneId -> everyoneId @@ -118,6 +136,30 @@ internal fun AddAllergiesSheet( } } + // Summary screen with floating robot (shown after last step completes) + if (showSummaryScreen) { + SummaryScreenWithFloatingRobot() + return + } + + // After the summary completes, show the IngrediBot chat intro UI. + if (showChatBotIntro) { + ChatBotIntroScreen( + onMaybeLater = onChatSkip, + onYesLetsGo = onChatBotLetsGo, + hasOtherSelection = hasOtherSelection + ) + return + } + + // After tapping "Yes, let’s go", show the static chat conversation UI. + if (showChatConversation) { + ChatBotConversationScreen( + onSkip = onChatSkip + ) + return + } + // Special fine‑tune decision screen between Life Style and Nutrition. if (showFineTuneDecision) { Column( @@ -180,15 +222,20 @@ internal fun AddAllergiesSheet( return } + val stepIds = OnboardingChipData.foodNotesStepIds + AnimatedContent( - targetState = questionStepIndex.coerceIn(0, 9), + targetState = questionStepIndex.coerceIn(0, (stepIds.size - 1).coerceAtLeast(0)), label = "allergyQuestion", transitionSpec = { fadeIn(animationSpec = tween(durationMillis = 250)) togetherWith fadeOut(animationSpec = tween(durationMillis = 250)) } ) { idx -> - val (question, subtitle) = OnboardingChipData.questionForStep(idx) + val (question, subtitle) = OnboardingChipData.questionForStep(idx, flowType) + // Hide subtitle for dynamic steps with ids "avoid", "lifeStyle", and "nutrition" + val stepId = stepIds.getOrNull(idx) + val shouldShowSubtitle = stepId !in setOf("avoid", "lifeStyle", "nutrition") Column { Text( text = buildAnnotatedString { @@ -201,16 +248,20 @@ internal fun AddAllergiesSheet( modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp) ) Spacer(modifier = Modifier.height(6.dp)) - Text( - text = subtitle, - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = Greyscale120, - textAlign = TextAlign.Start, - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp) - ) - Spacer(modifier = Modifier.height(10.dp)) + if (shouldShowSubtitle) { + Text( + text = subtitle, + fontFamily = Manrope, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Greyscale120, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp) + ) + Spacer(modifier = Modifier.height(10.dp)) + } else { + Spacer(modifier = Modifier.height(10.dp)) + } } } @@ -441,358 +492,27 @@ internal fun AddAllergiesSheet( } else if (questionStepIndex == 5) { // Avoid step: show stacked cards with forward arrow button always visible val avoidCards = OnboardingChipData.avoidCards - val totalCards = avoidCards.size - - Box( - modifier = Modifier - .fillMaxWidth() - .height(280.dp) - - ) { - StackedCardsComponent( - modifier = Modifier, - cardCount = avoidCards.size, - cardContent = { index, isTop, positionInStack -> - val card = avoidCards[index] - - val bgColor = try { - Color(android.graphics.Color.parseColor(card.colorHex)) - } catch (_: IllegalArgumentException) { - Color(0xFFFFF6B3) - } - - // Smooth fade-in of card content when this card becomes top (0 -> 1 opacity) - val contentAlpha by animateFloatAsState( - targetValue = if (isTop) 1f else 0f, - animationSpec = tween( - durationMillis = 750, - easing = FastOutSlowInEasing - ), - label = "cardContentAlpha" - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(270.dp) - .shadow( - elevation = 8.dp, - shape = RoundedCornerShape(24.dp), - spotColor = Color.Black.copy(alpha = 0.15f) - ) - .clip(RoundedCornerShape(24.dp)) - .background(bgColor) - .padding(horizontal = 12.dp, vertical = 16.dp) - - ) { - if (isTop) { - Column( - modifier = Modifier - .fillMaxSize() - .alpha(contentAlpha), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = card.title, - fontFamily = Nunito, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = Greyscale150 - ) - Text( - text = "${positionInStack}/$totalCards", - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = Greyscale140 - ) - } - - Text( - text = card.description, - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = Greyscale140 - ) - - Spacer(modifier = Modifier.height(6.dp)) - - SimpleFlowRow( - horizontalSpacing = 8.dp, - verticalSpacing = 8.dp - ) { - card.options.forEach { opt -> - val isSelected = selectedAllergies.contains(opt.id) - AvoidOptionChip( - option = opt, - isSelected = isSelected, - onClick = { onToggleAllergy(opt.id) } - ) - } - } - } - } - - if (isTop) { - // Adjust leaf icon position (positive X = right, positive Y = down) - val leafIconOffsetX = 5.dp - val leafIconOffsetY = 34.dp - Image( - painter = painterResource(id = R.drawable.leaf_arrow_circlepath), - contentDescription = null, - modifier = Modifier - .align(Alignment.BottomEnd) - .offset(x = leafIconOffsetX, y = leafIconOffsetY) - .height(100.dp) - .alpha(contentAlpha), - contentScale = ContentScale.Fit - ) - } - } - } - ) - - } + PreferenceCardsStackedSection( + cards = avoidCards, + selectedAllergies = selectedAllergies, + onToggleAllergy = onToggleAllergy + ) } else if (questionStepIndex == 6) { // LifeStyle step: same stacked cards as Avoid, 3 cards (Plant & Balance, Quality & Source, Sustainable Living) val lifestyleCards = OnboardingChipData.lifestyleCards - val totalCards = lifestyleCards.size - - Box( - modifier = Modifier - .fillMaxWidth() - .height(280.dp) - ) { - StackedCardsComponent( - modifier = Modifier, - cardCount = lifestyleCards.size, - cardContent = { index, isTop, positionInStack -> - val card = lifestyleCards[index] - - val bgColor = try { - Color(android.graphics.Color.parseColor(card.colorHex)) - } catch (_: IllegalArgumentException) { - Color(0xFFFFF6B3) - } - - val contentAlpha by animateFloatAsState( - targetValue = if (isTop) 1f else 0f, - animationSpec = tween( - durationMillis = 750, - easing = FastOutSlowInEasing - ), - label = "cardContentAlpha" - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(270.dp) - .shadow( - elevation = 8.dp, - shape = RoundedCornerShape(24.dp), - spotColor = Color.Black.copy(alpha = 0.15f) - ) - .clip(RoundedCornerShape(24.dp)) - .background(bgColor) - .padding(horizontal = 12.dp, vertical = 16.dp) - ) { - if (isTop) { - Column( - modifier = Modifier - .fillMaxSize() - .alpha(contentAlpha), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = card.title, - fontFamily = Nunito, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = Greyscale150 - ) - Text( - text = "${positionInStack}/$totalCards", - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = Greyscale140 - ) - } - - Text( - text = card.description, - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = Greyscale140 - ) - - Spacer(modifier = Modifier.height(6.dp)) - - SimpleFlowRow( - horizontalSpacing = 8.dp, - verticalSpacing = 8.dp - ) { - card.options.forEach { opt -> - val isSelected = selectedAllergies.contains(opt.id) - AvoidOptionChip( - option = opt, - isSelected = isSelected, - onClick = { onToggleAllergy(opt.id) } - ) - } - } - } - } - - if (isTop) { - val leafIconOffsetX = 5.dp - val leafIconOffsetY = 34.dp - Image( - painter = painterResource(id = R.drawable.leaf_arrow_circlepath), - contentDescription = null, - modifier = Modifier - .align(Alignment.BottomEnd) - .offset(x = leafIconOffsetX, y = leafIconOffsetY) - .height(100.dp) - .alpha(contentAlpha), - contentScale = ContentScale.Fit - ) - } - } - } - ) - - } + PreferenceCardsStackedSection( + cards = lifestyleCards, + selectedAllergies = selectedAllergies, + onToggleAllergy = onToggleAllergy + ) } else if (questionStepIndex == 7) { // Nutrition step: 3 cards (Macronutrient Goals, Sugar & Fiber, Diet Frameworks & Patterns) val nutritionCards = OnboardingChipData.nutritionCards - val totalCards = nutritionCards.size - - Box( - modifier = Modifier - .fillMaxWidth() - .height(280.dp) - ) { - StackedCardsComponent( - modifier = Modifier, - cardCount = nutritionCards.size, - cardContent = { index, isTop, positionInStack -> - val card = nutritionCards[index] - - val bgColor = try { - Color(android.graphics.Color.parseColor(card.colorHex)) - } catch (_: IllegalArgumentException) { - Color(0xFFFFF6B3) - } - - val contentAlpha by animateFloatAsState( - targetValue = if (isTop) 1f else 0f, - animationSpec = tween( - durationMillis = 750, - easing = FastOutSlowInEasing - ), - label = "cardContentAlpha" - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(270.dp) - .shadow( - elevation = 8.dp, - shape = RoundedCornerShape(24.dp), - spotColor = Color.Black.copy(alpha = 0.15f) - ) - .clip(RoundedCornerShape(24.dp)) - .background(bgColor) - .padding(horizontal = 12.dp, vertical = 16.dp) - ) { - if (isTop) { - Column( - modifier = Modifier - .fillMaxSize() - .alpha(contentAlpha), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = card.title, - fontFamily = Nunito, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = Greyscale150 - ) - Text( - text = "${positionInStack}/$totalCards", - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = Greyscale140 - ) - } - - Text( - text = card.description, - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = Greyscale140 - ) - - Spacer(modifier = Modifier.height(6.dp)) - - SimpleFlowRow( - horizontalSpacing = 8.dp, - verticalSpacing = 8.dp - ) { - card.options.forEach { opt -> - val isSelected = selectedAllergies.contains(opt.id) - AvoidOptionChip( - option = opt, - isSelected = isSelected, - onClick = { onToggleAllergy(opt.id) } - ) - } - } - } - } - - if (isTop) { - val leafIconOffsetX = 5.dp - val leafIconOffsetY = 34.dp - Image( - painter = painterResource(id = R.drawable.leaf_arrow_circlepath), - contentDescription = null, - modifier = Modifier - .align(Alignment.BottomEnd) - .offset(x = leafIconOffsetX, y = leafIconOffsetY) - .height(100.dp) - .alpha(contentAlpha), - contentScale = ContentScale.Fit - ) - } - } - } - ) - - } + PreferenceCardsStackedSection( + cards = nutritionCards, + selectedAllergies = selectedAllergies, + onToggleAllergy = onToggleAllergy + ) } else { val allergies = remember(questionStepIndex) { OnboardingChipData.chipsForStep(questionStepIndex) @@ -941,13 +661,135 @@ fun AvoidOptionChip( fontFamily = Manrope, fontWeight = FontWeight.Medium, fontSize = 16.sp, - color = if (isSelected) Greyscale150 else Color(0xFF303030), + color = if (isSelected) Color.White else Greyscale150, maxLines = 1 ) } } } +@Composable +private fun PreferenceCardsStackedSection( + cards: List, + selectedAllergies: Set, + onToggleAllergy: (String) -> Unit, + modifier: Modifier = Modifier +) { + val totalCards = cards.size + + Box( + modifier = modifier + .fillMaxWidth() + .height(280.dp) + ) { + StackedCardsComponent( + modifier = Modifier, + cardCount = cards.size, + cardContent = { index, isTop, positionInStack -> + val card = cards[index] + + val bgColor = try { + Color(android.graphics.Color.parseColor(card.colorHex)) + } catch (_: IllegalArgumentException) { + Color(0xFFFFF6B3) + } + + val contentAlpha by animateFloatAsState( + targetValue = if (isTop) 1f else 0f, + animationSpec = tween( + durationMillis = 750, + easing = FastOutSlowInEasing + ), + label = "cardContentAlpha" + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(270.dp) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(24.dp), + spotColor = Color.Black.copy(alpha = 0.15f) + ) + .clip(RoundedCornerShape(24.dp)) + .background(bgColor) + .padding(horizontal = 12.dp, vertical = 16.dp) + ) { + if (isTop) { + Column( + modifier = Modifier + .fillMaxSize() + .alpha(contentAlpha), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = card.title, + fontFamily = Nunito, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Greyscale150 + ) + Text( + text = "${positionInStack}/$totalCards", + fontFamily = Manrope, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Greyscale140 + ) + } + + Text( + text = card.description, + fontFamily = Manrope, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = Greyscale140 + ) + + Spacer(modifier = Modifier.height(6.dp)) + + SimpleFlowRow( + horizontalSpacing = 8.dp, + verticalSpacing = 8.dp + ) { + card.options.forEach { opt -> + val isSelected = selectedAllergies.contains(opt.id) + AvoidOptionChip( + option = opt, + isSelected = isSelected, + onClick = { onToggleAllergy(opt.id) } + ) + } + } + } + } + + if (isTop) { + val leafIconOffsetX = 5.dp + val leafIconOffsetY = 34.dp + Image( + painter = painterResource(id = R.drawable.leaf_arrow_circlepath), + contentDescription = null, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = leafIconOffsetX, y = leafIconOffsetY) + .height(100.dp) + .alpha(contentAlpha), + contentScale = ContentScale.Fit + ) + } + } + } + ) + } +} + @Composable private fun RegionSelectionSection( selectedAllergies: Set, @@ -1128,3 +970,61 @@ fun SimpleFlowRow( } } } + +/** + * Summary screen shown after completing the fine-tune flow. + * Displays a floating robot image (ingredi_robo2) with the text "Working on your personalized summary…" + */ +@Composable +private fun SummaryScreenWithFloatingRobot() { + // Use common floating robot animation + val (botX, botY) = OnboardingAnimations.rememberFloatingRobotOffsets(label = "summaryRobot") + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(vertical = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Floating robot image - same size as ingredi_robo1 (147.dp) + Box( + modifier = Modifier + .offset(x = botX.dp, y = botY.dp) + .size(147.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ingredi_robo2), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Text: "Working on your personalized summary…" - Nunito Bold 20sp + Text( + text = "Working on your\npersonalized summary…", + fontFamily = Nunito, + fontWeight = FontWeight.Bold, + maxLines = 2, + fontSize = 20.sp, + color = Greyscale150, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Preview(showBackground = true, showSystemUi = true, device = Devices.PIXEL_8_PRO) +@Composable +private fun SummaryScreenWithFloatingRobotPreview() { + NonDraggableBottomSheet( + onDismissRequest = { }, + horizontalPaddingEnabled = true + ) { + SummaryScreenWithFloatingRobot() + } +} diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAnimations.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAnimations.kt index 27e2af2..03dba82 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAnimations.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAnimations.kt @@ -1,12 +1,19 @@ -package lc.fungee.Ingredicheck.onboarding.ui + package lc.fungee.Ingredicheck.onboarding.ui +import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.unit.Dp /** - * Shared animation specs for onboarding UI (e.g. avatar selection size/border). + * Shared animation specs for onboarding UI (e.g. avatar selection size/border, floating robot). */ object OnboardingAnimations { /** Tween used for avatar size and border when selection changes (180ms, FastOutSlowInEasing). */ @@ -14,4 +21,45 @@ object OnboardingAnimations { durationMillis = 180, easing = FastOutSlowInEasing ) + + /** + * Smooth easing curve that mimics SwiftUI's .easeInOut (cubic bezier: 0.42, 0.0, 0.58, 1.0). + * This provides smoother, more natural motion for floating animations. + */ + private val EaseInOutEasing = CubicBezierEasing(0.42f, 0.0f, 0.58f, 1.0f) + + /** + * Floating robot animation offsets (X, Y) used for both ingredi_robo1 and ingredi_robo2. + * Matches iOS IngrediBotWithText.swift animation timings with smoother easing: + * - bot float X: easeInOut 3.0s repeatForever autoreverse (0f to 8f) + * - bot float Y: easeInOut 2.5s repeatForever autoreverse with ~0.5s delay (0f to -6f) + * + * Uses a custom cubic bezier easing curve for smoother, more natural floating motion. + * + * @param label Optional label for the infinite transition (useful for debugging) + * @return Pair of Float values: (botX offset in dp, botY offset in dp) + */ + @Composable + fun rememberFloatingRobotOffsets(label: String = "floatingRobot"): Pair { + val infinite = rememberInfiniteTransition(label = label) + val botX by infinite.animateFloat( + initialValue = 0f, + targetValue = 8f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 3000, easing = EaseInOutEasing), + repeatMode = RepeatMode.Reverse + ), + label = "botX" + ) + val botY by infinite.animateFloat( + initialValue = 0f, + targetValue = -6f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 2500, delayMillis = 500, easing = EaseInOutEasing), + repeatMode = RepeatMode.Reverse + ), + label = "botY" + ) + return Pair(botX, botY) + } } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAuthSheet.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAuthSheet.kt new file mode 100644 index 0000000..da52873 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingAuthSheet.kt @@ -0,0 +1,118 @@ +package lc.fungee.Ingredicheck.onboarding.ui + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.runtime.Composable +import com.russhwolf.settings.BuildConfig +import lc.fungee.Ingredicheck.auth.AuthViewModel +import lc.fungee.Ingredicheck.family.CreateFamilyRequest +import lc.fungee.Ingredicheck.onboarding.model.OnboardingStep +import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModel + +/** + * Renders the sheet content for all sign-in / auth steps in one place. + * Keeps [OnboardingHost] thin by moving the auth step branch and callbacks here. + */ +@Composable +internal fun OnboardingAuthSheetContent( + step: OnboardingStep, + vm: OnboardingViewModel, + authViewModel: AuthViewModel, + context: Context, + onBack: () -> Unit, + onGoogleClick: () -> Unit, + onAppleClick: () -> Unit, + isJustMeLoading: Boolean, + isAuthLoading: Boolean, + buildBiteBuddyFamilyRequest: () -> CreateFamilyRequest, + setCreatingBiteBuddyFamily: (Boolean) -> Unit, + onNavigateToAddFamilyWelcome: () -> Unit, + onNavigateToFallingCapsules: () -> Unit +) { + when (step) { + OnboardingStep.SIGN_IN_INITIAL -> { + SignInInitialSheet( + onExistingUserContinue = { vm.navigateTo(OnboardingStep.SIGN_IN_SOCIAL_LOGIN) }, + onStartNew = { vm.navigateTo(OnboardingStep.SIGN_IN_INVITE_CODE) } + ) + } + + OnboardingStep.SIGN_IN_SOCIAL_LOGIN -> { + SignInSocialLoginSheet( + onBackClick = onBack, + onGoogleClick = onGoogleClick, + onAppleClick = onAppleClick + ) + } + + OnboardingStep.SIGN_IN_INVITE_CODE -> { + SignInInviteCodeSheet( + onBackClick = onBack, + onEnterInviteCode = { vm.navigateTo(OnboardingStep.SIGN_IN_ENTER_INVITE_CODE) }, + onNoContinue = { vm.navigateTo(OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR) } + ) + } + + OnboardingStep.SIGN_IN_ENTER_INVITE_CODE -> { + SignInEnterInviteCodeSheet( + inviteCode = vm.inviteCode, + isError = vm.inviteCodeError, + onInviteCodeChange = { vm.inviteCode = it }, + onBackClick = onBack, + onVerifyContinue = { + if (vm.inviteCode.isBlank()) { + vm.inviteCodeError = true + } else { + vm.inviteCodeError = false + vm.navigateTo(OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR) + } + } + ) + } + + OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR -> { + SignInWhoIsThisForSheet( + onBackClick = onBack, + isJustMeLoading = isJustMeLoading, + isAddFamilyLoading = false, + isAuthLoading = isAuthLoading, + onJustMe = { + if (BuildConfig.DEBUG) { + Log.d("OnboardingHost", "Just Me: Creating Bite Buddy family then navigating to FALLING_CAPSULES") + } + authViewModel.debugLogCurrentSession("Just Me clicked") + setCreatingBiteBuddyFamily(true) + val req = buildBiteBuddyFamilyRequest() + authViewModel.createFamily(req) { result -> + setCreatingBiteBuddyFamily(false) + result.fold( + onSuccess = { + if (BuildConfig.DEBUG) { + Log.d("OnboardingHost", "Just Me: Bite Buddy family created, navigating to FALLING_CAPSULES") + } + onNavigateToFallingCapsules() + }, + onFailure = { e -> + Log.e("OnboardingHost", "Just Me: createFamily (Bite Buddy) failed", e) + Toast.makeText( + context, + e.localizedMessage ?: "Failed to create profile", + Toast.LENGTH_SHORT + ).show() + } + ) + } + }, + onAddFamily = { + authViewModel.debugLogCurrentSession("Add Family clicked") + onNavigateToAddFamilyWelcome() + } + ) + } + + else -> { + // Not an auth step; caller should not use this composable for other steps. + } + } +} diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingFamilySheet.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingFamilySheet.kt new file mode 100644 index 0000000..096ecc3 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingFamilySheet.kt @@ -0,0 +1,206 @@ +package lc.fungee.Ingredicheck.onboarding.ui + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import com.russhwolf.settings.BuildConfig +import lc.fungee.Ingredicheck.auth.AuthViewModel +import lc.fungee.Ingredicheck.family.CreateFamilyRequest +import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModel +import lc.fungee.Ingredicheck.ui.components.NonDraggableBottomSheet + + +/** + * UI overlay and bottom sheet for inviting a family member. + * Invite flow logic is handled by [runInviteFlow]; the host wires callbacks here. + */ +@Composable +internal fun InviteMemberOverlay( + member: OnboardingViewModel.FamilyOverviewMember, + onDismiss: () -> Unit, + onMaybeLater: () -> Unit, + onInvite: () -> Unit, + isLoading: Boolean +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + NonDraggableBottomSheet( + onDismissRequest = onDismiss, + horizontalPaddingEnabled = true + ) { + InviteConfirmationSheet( + memberName = member.name, + onMayBeLater = onMaybeLater, + onInvite = onInvite, + isLoading = isLoading + ) + } + } +} + +/** + * Dimmed overlay shown behind the invite sheet. Host should compose this when [memberToInvite != null]. + */ +@Composable +internal fun InviteMemberOverlayScrim(onDismiss: () -> Unit) { + Box( + modifier = Modifier + .fillMaxSize() + .alpha(0.5f) + .background(Color.Black) + .clickable(onClick = onDismiss) + ) +} + +/** + * Runs the full invite flow: ensure family exists, create if needed, invite member, share code. + * Handles duplicate memberId retry. Calls [setInviting] and [onDismiss] as appropriate. + */ +internal fun runInviteFlow( + context: Context, + authViewModel: AuthViewModel, + vm: OnboardingViewModel, + member: OnboardingViewModel.FamilyOverviewMember, + currentFamily: Any?, + getMembers: () -> List, + buildCreateFamilyRequestFromMembers: (List) -> CreateFamilyRequest?, + shareInviteCode: (Context, String) -> Unit, + setInviting: (Boolean) -> Unit, + onDismiss: () -> Unit +) { + setInviting(true) + if (BuildConfig.DEBUG) { + Log.d("OnboardingHost", "Invite confirmed for memberId=${member.id}, name=${member.name}") + } + + if (currentFamily != null) { + authViewModel.inviteFamilyMember(member.id) { result -> + val code = result.getOrNull() + if (code != null) { + vm.setInvitePending(member.id, true) + onDismiss() + shareInviteCode(context, code) + } else { + Toast.makeText(context, "Failed to create invite", Toast.LENGTH_SHORT).show() + } + setInviting(false) + } + return + } + + val req = buildCreateFamilyRequestFromMembers(getMembers()) + if (req == null) { + authViewModel.inviteFamilyMember(member.id) { result -> + val code = result.getOrNull() + if (code != null) { + vm.setInvitePending(member.id, true) + onDismiss() + shareInviteCode(context, code) + } else { + Toast.makeText(context, "Failed to create invite", Toast.LENGTH_SHORT).show() + } + setInviting(false) + } + return + } + + authViewModel.createFamily(req) { createResult -> + createResult.fold( + onSuccess = { + authViewModel.inviteFamilyMember(member.id) { result -> + val code = result.getOrNull() + if (code != null) { + vm.setInvitePending(member.id, true) + onDismiss() + shareInviteCode(context, code) + } else { + Toast.makeText(context, "Failed to create invite", Toast.LENGTH_SHORT).show() + } + setInviting(false) + } + }, + onFailure = { + val msg = it.localizedMessage.orEmpty() + val isDuplicateMemberId = + msg.contains("members_pkey", ignoreCase = true) || + msg.contains("duplicate key", ignoreCase = true) + + if (isDuplicateMemberId) { + Log.e( + "OnboardingHost", + "createFamily failed due to duplicate memberId; regenerating ids + retry", + it + ) + val idMap = vm.regenerateFamilyOverviewMemberIds() + val regeneratedMemberId = idMap[member.id] ?: member.id + val retryReq = buildCreateFamilyRequestFromMembers(getMembers()) + + if (retryReq == null) { + setInviting(false) + Toast.makeText(context, "Failed to retry createFamily", Toast.LENGTH_SHORT).show() + return@createFamily + } + + authViewModel.createFamily(retryReq) { retryResult -> + retryResult.fold( + onSuccess = { + authViewModel.inviteFamilyMember(regeneratedMemberId) { result -> + val code = result.getOrNull() + if (code != null) { + vm.setInvitePending(regeneratedMemberId, true) + onDismiss() + shareInviteCode(context, code) + } else { + Toast.makeText( + context, + "Failed to create invite", + Toast.LENGTH_SHORT + ).show() + } + setInviting(false) + } + }, + onFailure = { retryErr -> + Log.e( + "OnboardingHost", + "createFamily retry failed; not inviting memberId=$regeneratedMemberId", + retryErr + ) + setInviting(false) + Toast.makeText( + context, + retryErr.localizedMessage ?: "Failed to create family", + Toast.LENGTH_SHORT + ).show() + } + ) + } + return@createFamily + } + + setInviting(false) + Log.e( + "OnboardingHost", + "createFamily failed; not inviting memberId=${member.id}", + it + ) + Toast.makeText( + context, + it.localizedMessage ?: "Failed to create family", + Toast.LENGTH_SHORT + ).show() + } + ) + } +} diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHeaderScaffold.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHeaderScaffold.kt new file mode 100644 index 0000000..0f85217 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHeaderScaffold.kt @@ -0,0 +1,350 @@ +package lc.fungee.Ingredicheck.onboarding.ui + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import lc.fungee.Ingredicheck.R +import lc.fungee.Ingredicheck.onboarding.data.EVERYONE_MEMBER_ID +import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData +import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModel +import lc.fungee.Ingredicheck.onboarding.ui.components.AnimatedProgressLine +import lc.fungee.Ingredicheck.onboarding.ui.components.CapsuleStep +import lc.fungee.Ingredicheck.onboarding.ui.components.CapsuleStepperRow +import lc.fungee.Ingredicheck.onboarding.ui.components.PreferenceCapsuleCard +import lc.fungee.Ingredicheck.onboarding.ui.components.familyPlaceholderColor +import lc.fungee.Ingredicheck.ui.theme.Greyscale10 +import lc.fungee.Ingredicheck.ui.theme.Greyscale140 +import lc.fungee.Ingredicheck.ui.theme.Greyscale150 +import lc.fungee.Ingredicheck.ui.theme.Manrope +import lc.fungee.Ingredicheck.ui.theme.Primary800 +import lc.fungee.Ingredicheck.ui.theme.Secondary200 +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage +import coil.compose.SubcomposeAsyncImageContent +import kotlinx.coroutines.delay + +@Composable +internal fun CapsuleEveryoneAvatarSmall() { + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + androidx.compose.foundation.Image( + painter = painterResource(id = R.drawable.everyone_seleted_home_icon), + contentDescription = null, + modifier = Modifier.size(18.dp), + contentScale = ContentScale.Fit + ) + } +} + +@Composable +internal fun CapsuleMemberAvatarSmall(member: OnboardingViewModel.FamilyOverviewMember) { + val avatarRes = OnboardingChipData.avatarResOrNull(member.avatarId) + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + when { + member.generatedAvatarUrl.trim().isNotBlank() -> { + SubcomposeAsyncImage( + model = member.generatedAvatarUrl.trim(), + contentDescription = null, + modifier = Modifier + .size(22.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } + + avatarRes != null -> { + androidx.compose.foundation.Image( + painter = painterResource(id = avatarRes), + contentDescription = null, + modifier = Modifier + .size(22.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } + + else -> { + Box( + modifier = Modifier + .size(22.dp) + .clip(CircleShape) + .background(Secondary200) + ) + } + } + } +} + +@Composable +internal fun CapsuleChipMemberAvatars( + memberIds: Set, + members: List +) { + if (memberIds.isEmpty()) return + + val everyoneId = EVERYONE_MEMBER_ID + val hasEveryone = memberIds.contains(everyoneId) + val concreteMemberIds = memberIds.filter { it != everyoneId }.toSet() + val concreteMembers = members.filter { concreteMemberIds.contains(it.id) } + + Row( + horizontalArrangement = Arrangement.spacedBy(-8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + concreteMembers.forEach { m -> + CapsuleMemberAvatarSmall(member = m) + } + if (hasEveryone) { + CapsuleEveryoneAvatarSmall() + } + } +} + +@Composable +internal fun OnboardingAllergyBackground( + dynamicStepsLoaded: Boolean, + allergySteps: List, + showPreferenceSummary: Boolean, + allergyStepIndex: Int, + onAllergyStepIndexChange: (Int) -> Unit, + selectedAllergies: List, + selectedAllergyMemberId: String, + showSummaryScreen: Boolean, + showFineTuneDecision: Boolean, + onShowFineTuneDecisionChange: (Boolean) -> Unit, + selectedAllergiesByMember: Map>, + familyOverviewMembers: List +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF2F2F7)) + ) { + if (dynamicStepsLoaded && allergySteps.isNotEmpty() && !showPreferenceSummary) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Spacer(modifier = Modifier.height(40.dp)) + + val rawProgress = + if (allergySteps.size <= 1) 1f + else allergyStepIndex.toFloat() / (allergySteps.size - 1).coerceAtLeast(1) + val animatedProgress by animateFloatAsState( + targetValue = rawProgress.coerceIn(0f, 1f), + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + label = "allergyProgress" + ) + + val maxSelectedStepIndex = (0..allergySteps.lastIndex).lastOrNull { stepIndex -> + val stepChipIds = OnboardingChipData + .chipsForStep(stepIndex) + .map { it.id } + .toSet() + selectedAllergies.any { it in stepChipIds } + } ?: 0 + val maxReachedAllergyStepIndex = maxOf(allergyStepIndex, maxSelectedStepIndex) + + AnimatedProgressLine( + progress = animatedProgress, + modifier = Modifier.padding(horizontal = 20.dp) + ) + + if (!showSummaryScreen) { + CapsuleStepperRow( + steps = allergySteps, + activeIndex = allergyStepIndex, + maxReachedIndex = maxReachedAllergyStepIndex, + onStepClick = { clickedIndex -> + val clamped = clickedIndex.coerceIn(0, allergySteps.lastIndex) + if (clamped <= maxReachedAllergyStepIndex) { + onAllergyStepIndexChange(clamped) + if (showFineTuneDecision) { + onShowFineTuneDecisionChange(false) + } + } + } + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + val hasAnySelections = selectedAllergies.isNotEmpty() + val maxStepIndex = allergySteps.lastIndex + + val cardSteps: List = if (hasAnySelections) { + (0..maxStepIndex).filter { stepIndex -> + val stepChipIds = OnboardingChipData + .chipsForStep(stepIndex) + .map { it.id } + .toSet() + selectedAllergies.any { it in stepChipIds } + } + } else { + val upper = minOf(maxStepIndex, 3) + (0..upper).toList() + } + + val cardsListState = rememberLazyListState() + + val activeMemberId = selectedAllergyMemberId + val everyoneIdCaps = EVERYONE_MEMBER_ID + val activeMember = familyOverviewMembers + .firstOrNull { it.id == activeMemberId } + + val trailingAvatarContent: (@Composable () -> Unit)? = + when { + activeMemberId == everyoneIdCaps -> { + { CapsuleEveryoneAvatarSmall() } + } + + activeMember != null -> { + { CapsuleMemberAvatarSmall(activeMember) } + } + + else -> null + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false), + state = cardsListState, + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 20.dp), + userScrollEnabled = hasAnySelections + ) { + items(cardSteps) { stepIndex -> + val stepChipIds = OnboardingChipData + .chipsForStep(stepIndex) + .map { it.id } + .toSet() + val selectedChipsForStep = selectedAllergies + .filter { it in stepChipIds } + .toSet() + + PreferenceCapsuleCard( + modifier = Modifier.fillMaxWidth(), + selectedChipIds = if (hasAnySelections) { + selectedChipsForStep + } else { + emptySet() + }, + sectionTitle = allergySteps + .getOrNull(stepIndex) + ?.title ?: "Step ${stepIndex + 1}", + sectionIconRes = allergySteps + .getOrNull(stepIndex) + ?.iconRes ?: R.drawable.ic_step_allergies, + trailingAvatarsForChip = { chipId -> + val memberIds = selectedAllergiesByMember + .mapNotNull { (memberKey, chips) -> + if (chips.contains(chipId)) memberKey else null + } + .toSet() + if (memberIds.isEmpty()) { + null + } else { + { + CapsuleChipMemberAvatars( + memberIds = memberIds, + members = familyOverviewMembers + ) + } + } + } + ) + } + + if (hasAnySelections) { + item { + Spacer(modifier = Modifier.height(140.dp)) + } + } + } + + LaunchedEffect(allergyStepIndex, cardSteps, hasAnySelections) { + if (!hasAnySelections || cardSteps.isEmpty()) return@LaunchedEffect + + val layoutInfo = cardsListState.layoutInfo + if (layoutInfo.totalItemsCount <= layoutInfo.visibleItemsInfo.size) { + return@LaunchedEffect + } + + val healthConditionsIndex = allergySteps.indexOfFirst { it.id == "healthConditions" } + .takeIf { it >= 0 } ?: 2 + + val hasLaterCards = cardSteps.any { it >= healthConditionsIndex } + if (allergyStepIndex < healthConditionsIndex && !hasLaterCards) { + return@LaunchedEffect + } + + val clampedStep = allergyStepIndex.coerceIn(0, allergySteps.lastIndex) + + val currentStepChipIds = OnboardingChipData + .chipsForStep(clampedStep) + .map { it.id } + .toSet() + val stepHasSelection = selectedAllergies.any { it in currentStepChipIds } + if (!stepHasSelection) return@LaunchedEffect + + val targetIndex = cardSteps.indexOf(clampedStep) + if (targetIndex >= 0) { + cardsListState.animateScrollToItem(targetIndex) + } + } + } + } else { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHost.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHost.kt index d1b808e..29ba3d2 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHost.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingHost.kt @@ -55,6 +55,7 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import kotlinx.coroutines.delay import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -62,8 +63,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSavedStateRegistryOwner -import kotlin.math.absoluteValue import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.layout.ContentScale @@ -81,9 +80,10 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.ui.Alignment import androidx.compose.ui.layout.Layout +import com.russhwolf.settings.BuildConfig + import lc.fungee.Ingredicheck.auth.AppleLoginWebViewActivity import lc.fungee.Ingredicheck.ui.theme.Nunito -import lc.fungee.Ingredicheck.ui.components.NonDraggableBottomSheet import lc.fungee.Ingredicheck.ui.theme.Greyscale10 import lc.fungee.Ingredicheck.ui.theme.Greyscale30 import lc.fungee.Ingredicheck.ui.theme.Greyscale40 @@ -100,6 +100,7 @@ import lc.fungee.Ingredicheck.memoji.GetStatedScreen import lc.fungee.Ingredicheck.onboarding.model.OnboardingPersistence import lc.fungee.Ingredicheck.onboarding.data.EVERYONE_MEMBER_ID import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData +import lc.fungee.Ingredicheck.onboarding.data.DynamicStepsLoader import lc.fungee.Ingredicheck.onboarding.data.avatarBackgroundColorForId import lc.fungee.Ingredicheck.onboarding.model.OnboardingStep import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModel @@ -107,13 +108,21 @@ import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModelFactory import lc.fungee.Ingredicheck.onboarding.ui.components.AnimatedProgressLine import lc.fungee.Ingredicheck.onboarding.ui.components.CapsuleStep import lc.fungee.Ingredicheck.onboarding.ui.components.CapsuleStepperRow +import lc.fungee.Ingredicheck.onboarding.ui.components.FallingCapsulesScreen +import lc.fungee.Ingredicheck.onboarding.ui.components.PreferenceCapsuleCard +import lc.fungee.Ingredicheck.onboarding.ui.components.FamilyOverviewBackground +import lc.fungee.Ingredicheck.onboarding.ui.components.FlowRowChips +import lc.fungee.Ingredicheck.onboarding.ui.components.SelectedChipPill +import lc.fungee.Ingredicheck.onboarding.ui.PreferenceSummaryScreen import lc.fungee.Ingredicheck.ui.theme.Greyscale100 import lc.fungee.Ingredicheck.ui.theme.Greyscale110 import lc.fungee.Ingredicheck.ui.theme.Greyscale150 import lc.fungee.Ingredicheck.ui.theme.Greyscale60 import lc.fungee.Ingredicheck.ui.theme.Manrope import lc.fungee.Ingredicheck.ui.theme.Secondary200 +import lc.fungee.Ingredicheck.onboarding.ui.components.familyPlaceholderColor import lc.fungee.Ingredicheck.ui.theme.Primary800 + import kotlin.random.Random private fun Context.findActivity(): Activity? = when (this) { @@ -131,6 +140,24 @@ private fun shareInviteCode(context: Context, code: String) { context.startActivity(Intent.createChooser(shareIntent, "Invite")) } +/** Builds the default "Bite Buddy" family request for the Just Me flow (matches iOS createBiteBuddyFamily). */ +private fun buildBiteBuddyFamilyRequest(): CreateFamilyRequest { + val selfId = java.util.UUID.randomUUID().toString() + val selfMember = FamilyMemberDto( + id = selfId, + name = "Bite Buddy", + color = "#FFFFBA", + joined = true, + invitePending = null, + imageFileHash = "memoji_3" + ) + return CreateFamilyRequest( + name = "Bite Buddy", + selfMember = selfMember, + otherMembers = null + ) +} + private fun buildCreateFamilyRequestFromMembers( members: List ): CreateFamilyRequest? { @@ -157,634 +184,12 @@ private fun buildCreateFamilyRequestFromMembers( ) } -private fun familyPlaceholderColor(seed: String): Color { - val palette = listOf( - Color(0xFF9AD0FF), - Color(0xFFFFB3C1), - Color(0xFFB9F6CA), - Color(0xFFFFE29A), - Color(0xFFD7B9FF), - Color(0xFFFFC59A) - ) - val idx = (seed.hashCode().absoluteValue % palette.size) - return palette[idx] -} - -private val SelectedPillBackground = Secondary200 -private val PillShape = RoundedCornerShape(30.dp) - /** Build a single preference string from onboarding chip selections for backend sync (same as iOS). */ private fun buildDietaryPreferenceText(selectedAllergiesByMember: Map>): String { val allChipIds = selectedAllergiesByMember.values.flatMap { it.toList() }.toSet() return allChipIds.map { OnboardingChipData.labelForChipId(it) }.joinToString(", ") } -@Composable -private fun SelectedChipPill( - emoji: String, - label: String, - trailingAvatars: (@Composable () -> Unit)? = null -) { - Row( - modifier = Modifier - .clip(PillShape) - .background(SelectedPillBackground) - .padding(horizontal = 12.dp, vertical = 6.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = emoji.trim(), - fontSize = 16.sp, - color = Greyscale150 - ) - Text( - text = label, - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = Greyscale150, - maxLines = 1 - ) - if (trailingAvatars != null) { - Spacer(modifier = Modifier.width(6.dp)) - trailingAvatars() - } - } -} - -@Composable -private fun CapsuleEveryoneAvatarSmall() { - Box( - modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .background(Color.White) - .border( - width = 1.dp, - color = Greyscale40, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(id = R.drawable.everyone_seleted_home_icon), - contentDescription = null, - modifier = Modifier.size(18.dp), - contentScale = ContentScale.Fit - ) - } -} - -@Composable -private fun CapsuleMemberAvatarSmall(member: OnboardingViewModel.FamilyOverviewMember) { - val avatarRes = OnboardingChipData.avatarResOrNull(member.avatarId) - Box( - modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .background(Color.White) - .border( - width = 1.dp, - color = Greyscale40, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - when { - member.generatedAvatarUrl.trim().isNotBlank() -> { - SubcomposeAsyncImage( - model = member.generatedAvatarUrl.trim(), - contentDescription = null, - modifier = Modifier - .size(22.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop - ) - } - - avatarRes != null -> { - Image( - painter = painterResource(id = avatarRes), - contentDescription = null, - modifier = Modifier - .size(22.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop - ) - } - - else -> { - Box( - modifier = Modifier - .size(22.dp) - .clip(CircleShape) - .background(Greyscale40) - ) - } - } - } -} - -@Composable -private fun CapsuleChipMemberAvatars( - memberIds: Set, - members: List -) { - if (memberIds.isEmpty()) return - - val everyoneId = EVERYONE_MEMBER_ID - val hasEveryone = memberIds.contains(everyoneId) - val concreteMemberIds = memberIds.filter { it != everyoneId }.toSet() - val concreteMembers = members.filter { concreteMemberIds.contains(it.id) } - - Row( - horizontalArrangement = Arrangement.spacedBy(-8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - concreteMembers.forEach { m -> - CapsuleMemberAvatarSmall(member = m) - } - if (hasEveryone) { - CapsuleEveryoneAvatarSmall() - } - } -} - -@Composable -private fun FlowRowChips( - modifier: Modifier = Modifier, - horizontalSpacing: Dp = 8.dp, - verticalSpacing: Dp = 8.dp, - content: @Composable () -> Unit -) { - Layout(content = content, modifier = modifier) { measurables, constraints -> - if (measurables.isEmpty()) { - return@Layout layout(0, 0) {} - } - val density = this - val spacingX = with(density) { horizontalSpacing.roundToPx() } - val spacingY = with(density) { verticalSpacing.roundToPx() } - val placeables = measurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) } - val maxWidth = constraints.maxWidth - var x = 0 - var y = 0 - var rowHeight = 0 - val positions = placeables.map { p -> - if (x > 0 && x + p.width > maxWidth) { - x = 0 - y += rowHeight + spacingY - rowHeight = 0 - } - val pos = x to y - x += p.width + spacingX - rowHeight = maxOf(rowHeight, p.height) - pos - } - val totalHeight = (y + rowHeight).coerceIn(constraints.minHeight, constraints.maxHeight) - layout(maxWidth, totalHeight) { - placeables.forEachIndexed { i, p -> - val (px, py) = positions[i] - p.placeRelative(px, py) - } - } - } -} - -@Composable -private fun CapsuleSkeletonBox( - modifier: Modifier = Modifier, - selectedChipIds: Set = emptySet(), - sectionTitle: String = "Allergies", - @DrawableRes sectionIconRes: Int = R.drawable.ic_step_allergies, - trailingAvatarsForChip: ((String) -> (@Composable () -> Unit)?)? = null -) { - val showSelectedChips = selectedChipIds.isNotEmpty() - val resolvedChips = remember(selectedChipIds) { - selectedChipIds.mapNotNull { id -> OnboardingChipData.chipForId(id) } - } - val hasOtherSelection = remember(selectedChipIds) { - // Any chip id containing "other" (e.g. "other", "other_sens", "region_*_other", etc.) - selectedChipIds.any { it.contains("other", ignoreCase = true) } - } - - BoxWithConstraints( - modifier = modifier - .fillMaxWidth() - .then( - if (showSelectedChips) Modifier.heightIn(min = 130.dp) - else Modifier.height(130.dp) - ) - .clip(RoundedCornerShape(20.dp)) - .border((0.25).dp, Greyscale60, RoundedCornerShape(20.dp)) - .background(Greyscale10) - .padding(12.dp) - ) { - if (showSelectedChips && resolvedChips.isNotEmpty()) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start - ) { - Row(modifier = Modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - painter = painterResource(sectionIconRes), - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = Greyscale110 - ) - Text( - text = sectionTitle, - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - color = Greyscale110 - ) - } - Spacer(modifier = Modifier.height(8.dp)) - FlowRowChips( - modifier = Modifier.fillMaxWidth(), - horizontalSpacing = 8.dp, - verticalSpacing = 8.dp - ) { - resolvedChips.forEach { def -> - val trailing = trailingAvatarsForChip?.invoke(def.id) - SelectedChipPill( - emoji = def.iconPrefix, - label = def.label, - trailingAvatars = trailing - ) - } - } - if (hasOtherSelection) { - Spacer(modifier = Modifier.height(6.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - Icon( - painter = painterResource(R.drawable.emoji_warning), - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = Color.Unspecified - ) - Text( - text = "Something else too, don't worry we'll ask later!", - fontFamily = Manrope, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = Greyscale100 - ) - } - } - } - } else { - val maxWidth = maxWidth - val spacing = 8.dp - val minFirst = 90.dp - val minSecond = 110.dp - - fun randomRowWidths(): Pair { - val maxExtra = (maxWidth - spacing - minFirst - minSecond).coerceAtLeast(0.dp) - if (maxExtra == 0.dp) { - val second = (maxWidth - spacing - minFirst).coerceAtLeast(minSecond) - return minFirst to second - } - val extraFraction = Random.nextFloat() - val first = minFirst + maxExtra * extraFraction - val second = maxWidth - spacing - first - return first to second - } - - val (row1First, row1Second) = remember(maxWidth) { randomRowWidths() } - val (row2First, row2Second) = remember(maxWidth) { randomRowWidths() } - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.Start - ) { - Box( - modifier = Modifier - .width(165.dp) - .height(13.dp) - .clip(RoundedCornerShape(4.dp)) - .background(Greyscale30) - ) - Spacer(modifier = Modifier.height(12.dp)) - CapsuleRow(firstWidth = row1First, secondWidth = row1Second) - Spacer(modifier = Modifier.height(8.dp)) - CapsuleRow(firstWidth = row2First, secondWidth = row2Second) - } - } - } -} - -@Composable -private fun CapsuleRow( - firstWidth: Dp, - secondWidth: Dp -) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .width(firstWidth) - .height(36.dp) - .clip(RoundedCornerShape(30.dp)) - .background(Greyscale30) - ) - Box( - modifier = Modifier - .width(secondWidth) - .height(36.dp) - .clip(RoundedCornerShape(30.dp)) - .background(Greyscale30) - ) - } -} - -@Composable -private fun FamilyOverviewBackground( - members: List, - modifier: Modifier = Modifier, - bottomSheetHeight: Dp = 0.dp, - onLeaveFamily: (() -> Unit)? = null, - onInvite: ((OnboardingViewModel.FamilyOverviewMember) -> Unit)? = null, - onEditMember: ((OnboardingViewModel.FamilyOverviewMember) -> Unit)? = null -) { - Column( - modifier = modifier - .fillMaxSize() - .background(Color.White) - .padding(top = 64.dp, start = 20.dp, end = 20.dp) - ) { - - Text( - text = "Your Family Overview", - style = TextStyle( - fontFamily = Nunito, - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - color = Greyscale150 - ), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(18.dp)) - - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(bottom = bottomSheetHeight + 24.dp) - ) { - members.forEachIndexed { index, member -> - if (index > 0) Spacer(modifier = Modifier.height(14.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(28.dp)) - .border(1.dp, Greyscale110.copy(alpha = 0.28f), RoundedCornerShape(28.dp)) - .background(Color.White) - .padding(horizontal = 18.dp, vertical = 16.dp) - ) { - Box( - - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val hasGenerated = member.generatedAvatarUrl.trim().isNotBlank() - val circleBackground = if (hasGenerated) { - avatarBackgroundColorForId(member.backgroundColorId) - } else { - Color.White - } - - Box( - modifier = Modifier.size(54.dp), - contentAlignment = Alignment.BottomEnd - ) { - // Avatar circle - Box( - modifier = Modifier - .matchParentSize() - .clip(CircleShape) - .border(2.dp, Primary800.copy(alpha = 0.18f), CircleShape) - .background(circleBackground), - contentAlignment = Alignment.Center - ) { - val trimmedUrl = member.generatedAvatarUrl.trim() - val res = lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData.avatarResOrNull(member.avatarId.trim()) - when { - trimmedUrl.isNotBlank() -> { - SubcomposeAsyncImage( - model = trimmedUrl, - contentDescription = null, - modifier = Modifier - .size(50.dp) - .clip(CircleShape) - ) { - when (painter.state) { - is coil.compose.AsyncImagePainter.State.Loading -> { - Box( - modifier = Modifier - .fillMaxSize() - .background(circleBackground), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = Primary800 - ) - } - } - else -> { - SubcomposeAsyncImageContent() - } - } - } - } - res != null -> { - Image( - painter = androidx.compose.ui.res.painterResource(id = res), - contentDescription = null, - modifier = Modifier - .size(50.dp) - .clip(CircleShape) - ) - } - else -> { - val bg = familyPlaceholderColor(member.name) - Box( - modifier = Modifier - .size(50.dp) - .clip(CircleShape) - .background(bg), - contentAlignment = Alignment.Center - ) { - val initial = member.name.trim().firstOrNull()?.uppercase() ?: "?" - Text( - text = initial, - style = TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - color = Color.White - ) - ) - } - } - } - } - - // Edit (pen) icon overlay - if (onEditMember != null) { - Box( - modifier = Modifier - .offset(x = 2.dp, y = 2.dp) - .size(20.dp) - .clip(CircleShape) - .background(color = Greyscale40) - .clickable { onEditMember(member) }, - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(id = R.drawable.pen_line_icon), - contentDescription = null, - modifier = Modifier.size(10.dp) - ) - } - } - } - - Spacer(modifier = Modifier.width(14.dp)) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = member.name, - style = TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - color = Greyscale150 - ) - ) - - if (member.invitePending) { - Spacer(modifier = Modifier.height(4.dp)) - Box( - modifier = Modifier - .clip(RoundedCornerShape(12.50.dp)) - .background(Color(0xFFFFF9ED)) // soft yellow pending pill - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.exclamation_circle), - contentDescription = null, - tint = Color(0xFFFAB222), - modifier = Modifier.size(12.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = "Pending", - style = TextStyle( - fontFamily = Nunito, - fontWeight = FontWeight.SemiBold, - fontSize = 10.sp, - color = Color(0xFFFAB222) - ) - ) - } - } - } else { - Text( - text = if (member.joined) "(You)" else "Not joined yet !", - style = TextStyle( - fontFamily = Nunito, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = Greyscale110 - ) - ) - } - } - - val isFirst = index == 0 - val isPending = member.invitePending - val actionText = if (isFirst) { - "Leave Family" - } else if (isPending) { - "Re-invite" - } else { - "Invite" - } - val leaveFamilyColor = Color(0xFFFF3F31) - val inviteColor = Color(0xFF75990E) - val actionColor = if (isFirst) leaveFamilyColor else inviteColor - val borderColor = if (isFirst) leaveFamilyColor else Greyscale40 - - Box( - modifier = Modifier - .height(36.dp) - .clip(RoundedCornerShape(18.dp)) - .border(1.dp, borderColor, RoundedCornerShape(18.dp) ,) - .clickable { - if (isFirst) { - onLeaveFamily?.invoke() - } else { - onInvite?.invoke(member) - } - } - .padding(horizontal = 16.dp), - contentAlignment = Alignment.Center - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - if (!isFirst) { - Icon( - painter = painterResource(id = R.drawable.share_icon), - contentDescription = null, - tint = actionColor, - modifier = Modifier - .size(16.dp) - .padding(end = 3.dp) - ) - } - - Text( - text = actionText, - style = TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - color = actionColor - ) - ) - } - } - } - } - } - } - } - } -} - @SuppressLint("SuspiciousIndentation") @Composable fun OnboardingHost( @@ -793,7 +198,7 @@ fun OnboardingHost( ) { val context = LocalContext.current val focusManager = LocalFocusManager.current - val savedStateOwner = LocalSavedStateRegistryOwner.current + val savedStateOwner = androidx.savedstate.compose.LocalSavedStateRegistryOwner.current val persistence = remember(context) { OnboardingPersistence(context.applicationContext) } val factory = remember(savedStateOwner, persistence) { OnboardingViewModelFactory(owner = savedStateOwner, persistence = persistence) @@ -802,18 +207,25 @@ fun OnboardingHost( val vm: OnboardingViewModel = viewModel(factory = factory) val step = vm.currentStep var isCreatingFamily by remember { mutableStateOf(false) } + var isCreatingBiteBuddyFamily by remember { mutableStateOf(false) } var isInviting by remember { mutableStateOf(false) } val isRestored = vm.isRestored val authState by authViewModel.state.collectAsState() - val memojiState by authViewModel.memojiState.collectAsState() + val emojiState by authViewModel.memojiState.collectAsState() val isAuthLoading = authState is AuthState.Loading val currentFamily by authViewModel.currentFamily.collectAsState() var sheetHeight by remember { mutableStateOf(0.dp) } var memberToInvite by remember { mutableStateOf(null) } + // While onboarding state is still restoring from persistence, show a simple white screen + // instead of flashing the default GET_STARTED UI. if (!isRestored) { - Box(modifier = Modifier.fillMaxSize()) + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) return } @@ -823,6 +235,7 @@ fun OnboardingHost( } if ( step == OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR || + step == OnboardingStep.FALLING_CAPSULES || step == OnboardingStep.ADD_FAMILY_WELCOME || step == OnboardingStep.ADD_FAMILY_NAME || step == OnboardingStep.ADD_FAMILY_AVATAR_PICKER || @@ -833,13 +246,17 @@ fun OnboardingHost( authViewModel.debugLogCurrentSession("Entered $step (before ensureAnonymousSession)") authViewModel.ensureAnonymousSession() } + + // Note: signInAsGuest() should NOT be called here because it sets authState to Success, + // which causes MainActivity to immediately exit onboarding. Guest sign-in should happen + // only when onboarding is actually completed (e.g., when user exits after completing allergies). // Restore memoji generation UI state after process death: // - If we are on the generating screen and have a saved image URL with // memojiGenerationCompleted == true, restore Success state. if (step == OnboardingStep.ADD_FAMILY_AVATAR_GENERATING) { - val currentMemoji = authViewModel.memojiState.value - if (currentMemoji is MemojiGenState.Idle && vm.memojiGenerationCompleted) { + val currentEmoji = authViewModel.memojiState.value + if (currentEmoji is MemojiGenState.Idle && vm.memojiGenerationCompleted) { val url = vm.addFamilyGeneratedAvatarUrl.trim() if (url.isNotBlank()) { authViewModel.restoreMemojiSuccess(url) @@ -878,37 +295,132 @@ fun OnboardingHost( } // On first launch show "Everyone" as selected (ALL); user can switch to a member later. + // Local state for allergy selections (restored asynchronously from DataStore). val selectedAllergyMemberIdState = remember(vm.familyOverviewMembers.size) { mutableStateOf(EVERYONE_MEMBER_ID) } - val selectedAllergies = remember { mutableStateListOf() } + val selectedAllergies = remember { + mutableStateListOf() + } // memberKey ("ALL" or member.id) -> set of chipIds selected for that member - val selectedAllergiesByMember = remember { mutableStateMapOf>() } + val selectedAllergiesByMember = remember { + mutableStateMapOf>() + } // Bump on every chip toggle so the sheet reliably recomposes (workaround for SnapshotStateMap). var allergySelectionRevision by remember { mutableStateOf(0) } - // Explicitly track selections for the active member to force sheet recomposition - var activeMemberSelections by remember { mutableStateOf>(emptySet()) } + // Allergy step index (restored asynchronously below) + var allergyStepIndex by remember { + mutableStateOf(0) + } + // Active member's chip selections + var activeMemberSelections by remember { + mutableStateOf>(emptySet()) + } + + // Restore allergy selections state from persistence (per‑member chip ids + active member + step index) + // without blocking the main thread. + LaunchedEffect(Unit) { + try { + val (restoredSelections, restoredMemberId, restoredStepIndex) = + persistence.getAllergySelectionsState() + + if (restoredSelections.isNotEmpty()) { + // Update member id + val activeKey = restoredMemberId.ifBlank { EVERYONE_MEMBER_ID } + selectedAllergyMemberIdState.value = activeKey + + // Rebuild per-member map + selectedAllergiesByMember.clear() + restoredSelections.forEach { (memberKey, chipIds) -> + selectedAllergiesByMember[memberKey] = chipIds.toMutableSet() + } + + // Rebuild flat union and active member selections + val union = restoredSelections.values.flatten().toSet() + selectedAllergies.clear() + selectedAllergies.addAll(union) + activeMemberSelections = restoredSelections[activeKey] ?: emptySet() + + // Restore step index + allergyStepIndex = restoredStepIndex + + if (BuildConfig.DEBUG) { + Log.d( + "OnboardingAllergies", + "[RESTORE_APPLY] restoredSelections=$restoredSelections " + + "restoredMemberId=$restoredMemberId restoredStepIndex=$restoredStepIndex " + + "union=$union activeMemberSelections=$activeMemberSelections" + ) + } + } else { + if (BuildConfig.DEBUG) { + Log.d( + "OnboardingAllergies", + "[RESTORE_APPLY] no restoredSelections found; keeping defaults" + ) + } + } + } catch (e: Exception) { + Log.w("OnboardingAllergies", "[RESTORE] getAllergySelectionsState failed", e) + } + } // Progress tracking within the fine‑tune flow (allergies, intolerances, etc.) // These same steps drive both the CapsuleStepperRow and the AnimatedProgressLine. - val allergySteps = remember { - listOf( - CapsuleStep("allergies", "Allergies", R.drawable.ic_step_allergies), - CapsuleStep("intolerances", "Intolerances", R.drawable.ic_step_intolerances), - CapsuleStep("health_conditions", "Health Conditions", R.drawable.ic_step_health_conditions), - CapsuleStep("life_stage", "Life Stage", R.drawable.ic_step_life_style), - CapsuleStep("region", "Region", R.drawable.ic_step_region), - CapsuleStep("avoid", "Avoid", R.drawable.ic_step_avoid_cross), - CapsuleStep("life_style", "Life Style", R.drawable.ic_step_diet_preferences), - CapsuleStep("nutrition", "Nutrition", R.drawable.ic_step_meals), - CapsuleStep("ethical", "Ethical", R.drawable.ic_step_ethical), - CapsuleStep("taste", "Taste", R.drawable.iconoir_chocolate) - ) + // Load dynamic JSON from assets (same as iOS) so step order/copy can be driven from dynamicJsonData.json. + var dynamicStepsLoaded by remember { mutableStateOf(false) } + LaunchedEffect(context) { + DynamicStepsLoader.ensureLoaded(context) + dynamicStepsLoaded = true + } + val allergySteps = remember(dynamicStepsLoaded) { + val steps = DynamicStepsLoader.getSteps()?.map { s -> + CapsuleStep(s.id, s.header.name, OnboardingChipData.iconResForStepId(s.id)) + } ?: emptyList() + if (BuildConfig.DEBUG) { + Log.d("DynamicJsonData", "JSON data: UI using ${steps.size} steps from dynamicJsonData.json (same after restart)") + } + steps } - var allergyStepIndex by remember { mutableStateOf(0) } + // Persist allergy selections whenever they change + LaunchedEffect(selectedAllergiesByMember, selectedAllergyMemberIdState.value, allergyStepIndex) { + if (isRestored && step == OnboardingStep.ADD_FAMILY_ALLERGIES) { + val snapshot = selectedAllergiesByMember.mapValues { it.value.toSet() } + if (BuildConfig.DEBUG) { + Log.d( + "OnboardingAllergies", + "[PERSIST_EFFECT] step=$step isRestored=$isRestored " + + "selectedMember=${selectedAllergyMemberIdState.value} " + + "stepIndex=$allergyStepIndex selections=$snapshot" + ) + } + persistence.setAllergySelectionsState( + selectedAllergiesByMember = snapshot, + selectedAllergyMemberId = selectedAllergyMemberIdState.value, + allergyStepIndex = allergyStepIndex + ) + } + } // When true, show the fine‑tune decision screen between Life Style and Nutrition var showFineTuneDecision by remember { mutableStateOf(false) } + // When true, show the summary screen with floating robot after completing fine-tune flow + var showSummaryScreen by remember { mutableStateOf(false) } + // When true, show the IngrediBot chat intro screen after the summary completes. + var showChatBotIntro by remember { mutableStateOf(false) } + // When true, show the AI chat conversation sheet after the intro. + var showChatConversation by remember { mutableStateOf(false) } + // When true, show the AI summary screen with PreferenceCapsuleCard list. + var showPreferenceSummary by remember { mutableStateOf(false) } + + // After showing the summary screen for a short time, transition smoothly to the chat intro. + LaunchedEffect(showSummaryScreen) { + if (showSummaryScreen) { + delay(3000) // Show summary screen for 3 seconds + showSummaryScreen = false + showChatBotIntro = true + } + } if (step == OnboardingStep.GET_STARTED) { GetStatedScreen( @@ -935,9 +447,9 @@ fun OnboardingHost( } // Keep addFamilyGeneratedAvatarUrl in sync with memoji Success so it can be restored. - LaunchedEffect(memojiState) { - if (memojiState is MemojiGenState.Success) { - vm.addFamilyGeneratedAvatarUrl = (memojiState as MemojiGenState.Success).imageUrl + LaunchedEffect(emojiState) { + if (emojiState is MemojiGenState.Success) { + vm.addFamilyGeneratedAvatarUrl = (emojiState as MemojiGenState.Success).imageUrl vm.memojiGenerationCompleted = true } } @@ -1028,164 +540,25 @@ fun OnboardingHost( } 5 -> { FallingCapsulesScreen( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().background(Color.White), bottomInset = sheetHeight ) } 6 -> { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFFF2F2F7)) - ) { - Column( - modifier = Modifier.fillMaxSize() - ) { - Spacer(modifier = Modifier.height(40.dp)) - - // Animate progress based on current allergyStepIndex. - // NOTE: The fine‑tune decision screen between Life Style and Nutrition - // does NOT advance allergyStepIndex, so progress will not increase - // while that screen is shown. - val rawProgress = - if (allergySteps.size <= 1) 1f - else allergyStepIndex.toFloat() / (allergySteps.size - 1).coerceAtLeast(1) - val animatedProgress by animateFloatAsState( - targetValue = rawProgress.coerceIn(0f, 1f), - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), - label = "allergyProgress" - ) - - AnimatedProgressLine( - progress = animatedProgress, - modifier = Modifier.padding(horizontal = 20.dp) - ) -// Spacer(modifier = Modifier.height(10.dp)) - - CapsuleStepperRow( - steps = allergySteps, - activeIndex = allergyStepIndex, - onStepClick = { clickedIndex -> - // Only allow jumping to steps the user has already visited. - val clamped = clickedIndex.coerceIn(0, allergySteps.lastIndex) - if (clamped <= allergyStepIndex) { - allergyStepIndex = clamped - showFineTuneDecision = false - } - } - ) - - - Spacer(modifier = Modifier.height(10.dp)) - - // Show a scrollable list of CapsuleSkeletonBox cards. - // Steps 0–9: Allergies, Intolerances, Health, Life Stage, Region, Avoid, LifeStyle, Nutrition, Ethical, Taste. - val hasAnySelections = selectedAllergies.isNotEmpty() - val maxStepIndex = allergySteps.lastIndex - - // Decide which step indices should render cards. - val cardSteps: List = if (hasAnySelections) { - // Only steps that have at least one selected chip (including Region, Avoid, LifeStyle, Nutrition). - (0..maxStepIndex).filter { stepIndex -> - val stepChipIds = OnboardingChipData - .chipsForStep(stepIndex) - .map { it.id } - .toSet() - selectedAllergies.any { it in stepChipIds } - } - } else { - // No selections yet – show all steps as empty placeholders. - (0..maxStepIndex).toList() - } - - val cardsListState = rememberLazyListState() - - // Small avatar(s) to show who this capsule applies to (Everyone or a member) - val activeMemberId = selectedAllergyMemberIdState.value - val everyoneIdCaps = EVERYONE_MEMBER_ID - val activeMember = vm.familyOverviewMembers - .firstOrNull { it.id == activeMemberId } - - val trailingAvatarContent: (@Composable () -> Unit)? = - when { - activeMemberId == everyoneIdCaps -> { - { CapsuleEveryoneAvatarSmall() } - } - - activeMember != null -> { - { CapsuleMemberAvatarSmall(activeMember) } - } - - else -> null - } - - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f, fill = false), - state = cardsListState, - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 20.dp) - ) { - items(cardSteps) { stepIndex -> - val stepChipIds = OnboardingChipData - .chipsForStep(stepIndex) - .map { it.id } - .toSet() - val selectedChipsForStep = selectedAllergies - .filter { it in stepChipIds } - .toSet() - - CapsuleSkeletonBox( - modifier = Modifier.fillMaxWidth(), - selectedChipIds = if (hasAnySelections) { - // When anything is selected, only show chips for this step. - selectedChipsForStep - } else { - // Initial empty state – show skeletons. - emptySet() - }, - sectionTitle = allergySteps - .getOrNull(stepIndex) - ?.title ?: "Step ${stepIndex + 1}", - sectionIconRes = allergySteps - .getOrNull(stepIndex) - ?.iconRes ?: R.drawable.ic_step_allergies, - trailingAvatarsForChip = { chipId -> - // Derive which members have this chip selected - val memberIds = selectedAllergiesByMember - .mapNotNull { (memberKey, chips) -> - if (chips.contains(chipId)) memberKey else null - } - .toSet() - if (memberIds.isEmpty()) { - null - } else { - { - CapsuleChipMemberAvatars( - memberIds = memberIds, - members = vm.familyOverviewMembers.toList() - ) - } - } - } - ) - } - } - - // When user switches steps, auto-scroll so that step's card is visible. - LaunchedEffect(allergyStepIndex, cardSteps) { - if (cardSteps.isEmpty()) return@LaunchedEffect - - // Map the active step index to its position in the list. - val targetStep = allergyStepIndex.coerceIn(0, 3) - val targetIndex = cardSteps.indexOf(targetStep) - if (targetIndex >= 0) { - cardsListState.animateScrollToItem(targetIndex) - } - } - } - } + OnboardingAllergyBackground( + dynamicStepsLoaded = dynamicStepsLoaded, + allergySteps = allergySteps, + showPreferenceSummary = showPreferenceSummary, + allergyStepIndex = allergyStepIndex, + onAllergyStepIndexChange = { allergyStepIndex = it }, + selectedAllergies = selectedAllergies, + selectedAllergyMemberId = selectedAllergyMemberIdState.value, + showSummaryScreen = showSummaryScreen, + showFineTuneDecision = showFineTuneDecision, + onShowFineTuneDecisionChange = { showFineTuneDecision = it }, + selectedAllergiesByMember = selectedAllergiesByMember.mapValues { it.value.toSet() }, + familyOverviewMembers = vm.familyOverviewMembers.toList() + ) } } } @@ -1211,6 +584,17 @@ fun OnboardingHost( ) } OnboardingStep.ADD_FAMILY_ALLERGIES -> { + if (!dynamicStepsLoaded || allergySteps.isEmpty()) { + // Don't show sheet content until steps are loaded so chips and question are visible (e.g. after restart). + Box( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { // Compute per-member selections for the bottom sheet: // the sheet should reflect ONLY what the currently selected member // (or Everyone) has chosen, not the union across all members. @@ -1241,6 +625,8 @@ fun OnboardingHost( // selections so that chip toggles do not recreate the sheet (preserves // stacked card order when user selects chips on 2nd/3rd card). key(activeMemberKey) { + val hasOtherSelection = + selectedAllergies.any { it.contains("other", ignoreCase = true) } AddAllergiesSheet( members = vm.familyOverviewMembers.toList(), selectedMemberId = selectedAllergyMemberIdState.value, @@ -1320,42 +706,56 @@ fun OnboardingHost( if (allergyStepIndex < allergySteps.lastIndex) { allergyStepIndex++ } else { - // Sync dietary preferences to backend (same as iOS) before exiting + // Sync dietary preferences to backend (same as iOS) before showing summary val preferenceText = buildDietaryPreferenceText(selectedAllergiesByMember) Log.d("OnboardingAllergies", "[DietaryPreference] onNext complete: syncing textLength=${preferenceText.length}") authViewModel.syncDietaryPreferencesFromOnboarding(preferenceText) - onExitOnboarding() + authViewModel.syncFoodNotesFromOnboarding(selectedAllergiesByMember.mapValues { it.value.toSet() }) + showSummaryScreen = true } } }, onSkipPreferences = { // User tapped "All Set!" on the fine‑tune decision screen: - // sync preferences to backend (same as iOS) then close and exit. + // sync preferences to backend (same as iOS) then show summary screen. val preferenceText = buildDietaryPreferenceText(selectedAllergiesByMember) Log.d("OnboardingAllergies", "[DietaryPreference] onSkipPreferences: syncing textLength=${preferenceText.length}") authViewModel.syncDietaryPreferencesFromOnboarding(preferenceText) + authViewModel.syncFoodNotesFromOnboarding(selectedAllergiesByMember.mapValues { it.value.toSet() }) showFineTuneDecision = false - onExitOnboarding() + showSummaryScreen = true }, showFineTuneDecision = showFineTuneDecision, + showSummaryScreen = showSummaryScreen, + hasOtherSelection = hasOtherSelection, + showChatBotIntro = showChatBotIntro, + showChatConversation = showChatConversation, + onChatBotLetsGo = { + showChatBotIntro = false + showChatConversation = true + }, + onChatSkip = { + // Close chat and show AI summary screen. + showChatConversation = false + showChatBotIntro = false + showPreferenceSummary = true + }, questionStepIndex = allergyStepIndex ) } + } } - OnboardingStep.SIGN_IN_INITIAL -> { - SignInInitialSheet( - onExistingUserContinue = { - vm.navigateTo(OnboardingStep.SIGN_IN_SOCIAL_LOGIN) - }, - onStartNew = { - vm.navigateTo(OnboardingStep.SIGN_IN_INVITE_CODE) - } - ) - } - - OnboardingStep.SIGN_IN_SOCIAL_LOGIN -> { - SignInSocialLoginSheet( - onBackClick = handleBack, + OnboardingStep.SIGN_IN_INITIAL, + OnboardingStep.SIGN_IN_SOCIAL_LOGIN, + OnboardingStep.SIGN_IN_INVITE_CODE, + OnboardingStep.SIGN_IN_ENTER_INVITE_CODE, + OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR -> { + OnboardingAuthSheetContent( + step = s, + vm = vm, + authViewModel = authViewModel, + context = context, + onBack = handleBack, onGoogleClick = { if (activity != null) { val client = GoogleAuthDataSource.getClient(activity) @@ -1374,7 +774,6 @@ fun OnboardingHost( .appendQueryParameter("redirect_to", redirectUri) .appendQueryParameter("flow_type", "implicit") .build() - val intent = Intent( activity, AppleLoginWebViewActivity::class.java @@ -1384,50 +783,13 @@ fun OnboardingHost( } appleLauncher.launch(intent) } - } - ) - } - - OnboardingStep.SIGN_IN_INVITE_CODE -> { - SignInInviteCodeSheet( - onBackClick = handleBack, - onEnterInviteCode = { - vm.navigateTo(OnboardingStep.SIGN_IN_ENTER_INVITE_CODE) }, - onNoContinue = { - vm.navigateTo(OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR) - } - ) - } - - OnboardingStep.SIGN_IN_ENTER_INVITE_CODE -> { - SignInEnterInviteCodeSheet( - inviteCode = vm.inviteCode, - isError = vm.inviteCodeError, - onInviteCodeChange = { vm.inviteCode = it }, - onBackClick = handleBack, - onVerifyContinue = { - if (vm.inviteCode == "ABCXYZ") { - vm.inviteCodeError = true - } else { - vm.navigateTo(OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR) - } - } - ) - } - - OnboardingStep.SIGN_IN_WHO_IS_THIS_FOR -> { - SignInWhoIsThisForSheet( - onBackClick = handleBack, - isLoading = isAuthLoading, - onJustMe = { - authViewModel.debugLogCurrentSession("Just Me clicked") - authViewModel.signInAsGuest() - }, - onAddFamily = { - authViewModel.debugLogCurrentSession("Add Family clicked") - vm.navigateTo(OnboardingStep.ADD_FAMILY_WELCOME) - } + isJustMeLoading = isCreatingBiteBuddyFamily, + isAuthLoading = isAuthLoading, + buildBiteBuddyFamilyRequest = { buildBiteBuddyFamilyRequest() }, + setCreatingBiteBuddyFamily = { isCreatingBiteBuddyFamily = it }, + onNavigateToAddFamilyWelcome = { vm.navigateTo(OnboardingStep.ADD_FAMILY_WELCOME) }, + onNavigateToFallingCapsules = { vm.navigateTo(OnboardingStep.FALLING_CAPSULES) } ) } @@ -1725,7 +1087,7 @@ fun OnboardingHost( OnboardingStep.ADD_FAMILY_AVATAR_GENERATING -> { AddFamilyAvatarGeneratingSheet( - state = memojiState, + state = emojiState, selections = vm.addFamilyAvatarSelections, onBackClick = handleBack, onRetry = { authViewModel.generateAddFamilyMemoji(vm.addFamilyAvatarSelections) }, @@ -1797,185 +1159,56 @@ fun OnboardingHost( } ) + if (showPreferenceSummary) { + PreferenceSummaryScreen( + isFamilyFlow = vm.familyOverviewMembers.isNotEmpty(), + selectedChipIds = selectedAllergies.toSet(), + onEditSection = { stepId -> + showPreferenceSummary = false + val idx = allergySteps.indexOfFirst { it.id == stepId } + if (idx >= 0) { + allergyStepIndex = idx + } + }, + onContinue = { + showPreferenceSummary = false + onExitOnboarding() + } + ) + } - // Overlay with opacity when invite confirmation is shown if (memberToInvite != null) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(0.5f) - .background(Color.Black) - .clickable { memberToInvite = null } - ) + InviteMemberOverlayScrim(onDismiss = { memberToInvite = null }) } - // Invite confirmation bottom sheet memberToInvite?.let { member -> - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter - ) { - NonDraggableBottomSheet( - onDismissRequest = { memberToInvite = null }, - horizontalPaddingEnabled = true - ) { - InviteConfirmationSheet( - memberName = member.name, - onMayBeLater = { - if (!isInviting) { - vm.setInvitePending(member.id, true) - memberToInvite = null - } - }, - onInvite = { - if (isInviting) return@InviteConfirmationSheet - isInviting = true - Log.d( - "OnboardingHost", - "Invite confirmed for memberId=${member.id}, name=${member.name}" - ) - // Ensure family exists on backend before inviting. - if (currentFamily != null) { - authViewModel.inviteFamilyMember(member.id) { result -> - val code = result.getOrNull() - if (code != null) { - vm.setInvitePending(member.id, true) - memberToInvite = null - shareInviteCode(context, code) - } else { - Toast.makeText( - context, - "Failed to create invite", - Toast.LENGTH_SHORT - ).show() - } - isInviting = false - } - } else { - val req = buildCreateFamilyRequestFromMembers(vm.familyOverviewMembers.toList()) - if (req == null) { - authViewModel.inviteFamilyMember(member.id) { result -> - val code = result.getOrNull() - if (code != null) { - vm.setInvitePending(member.id, true) - memberToInvite = null - shareInviteCode(context, code) - } else { - Toast.makeText( - context, - "Failed to create invite", - Toast.LENGTH_SHORT - ).show() - } - isInviting = false - } - } else { - authViewModel.createFamily(req) { createResult -> - createResult.fold( - onSuccess = { - authViewModel.inviteFamilyMember(member.id) { result -> - val code = result.getOrNull() - if (code != null) { - vm.setInvitePending(member.id, true) - memberToInvite = null - shareInviteCode(context, code) - } else { - Toast.makeText( - context, - "Failed to create invite", - Toast.LENGTH_SHORT - ).show() - } - isInviting = false - } - }, - onFailure = { - val msg = it.localizedMessage.orEmpty() - val isDuplicateMemberId = - msg.contains("members_pkey", ignoreCase = true) || - msg.contains("duplicate key", ignoreCase = true) - - if (isDuplicateMemberId) { - Log.e( - "OnboardingHost", - "createFamily failed due to duplicate memberId; regenerating ids + retry", - it - ) - - val idMap = vm.regenerateFamilyOverviewMemberIds() - val regeneratedMemberId = idMap[member.id] ?: member.id - val retryReq = buildCreateFamilyRequestFromMembers(vm.familyOverviewMembers.toList()) - - if (retryReq == null) { - isInviting = false - Toast.makeText( - context, - "Failed to retry createFamily", - Toast.LENGTH_SHORT - ).show() - return@createFamily - } - - authViewModel.createFamily(retryReq) { retryResult -> - retryResult.fold( - onSuccess = { - authViewModel.inviteFamilyMember(regeneratedMemberId) { result -> - val code = result.getOrNull() - if (code != null) { - vm.setInvitePending(regeneratedMemberId, true) - memberToInvite = null - shareInviteCode(context, code) - } else { - Toast.makeText( - context, - "Failed to create invite", - Toast.LENGTH_SHORT - ).show() - } - isInviting = false - } - }, - onFailure = { retryErr -> - Log.e( - "OnboardingHost", - "createFamily retry failed; not inviting memberId=$regeneratedMemberId", - retryErr - ) - isInviting = false - Toast.makeText( - context, - retryErr.localizedMessage - ?: "Failed to create family", - Toast.LENGTH_SHORT - ).show() - } - ) - } - - return@createFamily - } - - isInviting = false - Log.e( - "OnboardingHost", - "createFamily failed; not inviting memberId=${member.id}", - it - ) - Toast.makeText( - context, - it.localizedMessage ?: "Failed to create family", - Toast.LENGTH_SHORT - ).show() - } - ) - } - } - } - }, - isLoading = isInviting - ) - } - } + InviteMemberOverlay( + member = member, + onDismiss = { memberToInvite = null }, + onMaybeLater = { + if (!isInviting) { + vm.setInvitePending(member.id, true) + memberToInvite = null + } + }, + onInvite = { + if (!isInviting) { + runInviteFlow( + context = context, + authViewModel = authViewModel, + vm = vm, + member = member, + currentFamily = currentFamily, + getMembers = { vm.familyOverviewMembers.toList() }, + buildCreateFamilyRequestFromMembers = { buildCreateFamilyRequestFromMembers(it) }, + shareInviteCode = { c, code -> shareInviteCode(c, code) }, + setInviting = { isInviting = it }, + onDismiss = { memberToInvite = null } + ) + } + }, + isLoading = isInviting + ) } } } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingStepScreens.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingStepScreens.kt index 3b16c31..6bf6b9f 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingStepScreens.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/OnboardingStepScreens.kt @@ -33,9 +33,14 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -47,6 +52,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -64,6 +70,7 @@ import lc.fungee.Ingredicheck.R import lc.fungee.Ingredicheck.auth.MemojiGenState import lc.fungee.Ingredicheck.ui.components.buttons.PrimaryButton import lc.fungee.Ingredicheck.ui.components.buttons.SecondaryButton +import lc.fungee.Ingredicheck.onboarding.data.DynamicStepsLoader import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData import lc.fungee.Ingredicheck.onboarding.data.avatarBackgroundColorForId import lc.fungee.Ingredicheck.onboarding.ui.components.AnimatedProgressLine @@ -88,6 +95,7 @@ import lc.fungee.Ingredicheck.ui.theme.Primary800 import lc.fungee.Ingredicheck.ui.theme.sheetTitleTextStyle import lc.fungee.Ingredicheck.ui.theme.sheetSubtitleTextStyle import lc.fungee.Ingredicheck.memoji.avatarOptionsForCategory +import kotlinx.coroutines.delay @Composable internal fun AddFamilyAvatarPickerSheet( @@ -234,25 +242,8 @@ internal fun AddFamilyAvatarGeneratingSheet( label = "shimmerProgress" ) - val botX by infinite.animateFloat( - initialValue = 0f, - targetValue = 8f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 3000, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "botX" - ) - - val botY by infinite.animateFloat( - initialValue = 0f, - targetValue = -6f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 2500, delayMillis = 500, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "botY" - ) + // Use common floating robot animation + val (botX, botY) = OnboardingAnimations.rememberFloatingRobotOffsets(label = "avatarGenerating") Column( modifier = Modifier @@ -359,51 +350,25 @@ internal fun AddFamilyAvatarGeneratingSheet( } is MemojiGenState.Success -> { - Spacer(modifier = Modifier.height(10.dp)) - // Determine the background color from the user's selected color category (index 5) val selectedColorId = selections[5] val avatarBackgroundColor = remember(selectedColorId) { avatarBackgroundColorForId(selectedColorId) } - Box( - modifier = Modifier - .fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - SubcomposeAsyncImage( - model = state.imageUrl, - contentDescription = null, - modifier = Modifier - .size(170.dp) - .clip(CircleShape) - .background(avatarBackgroundColor), - contentScale = ContentScale.Crop - ) { - when (painter.state) { - is coil.compose.AsyncImagePainter.State.Loading -> { - Box( - modifier = Modifier - .fillMaxSize() - .background(avatarBackgroundColor), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(160.dp), - strokeWidth = 3.dp, - color = Primary800 - ) - } - } - else -> { - SubcomposeAsyncImageContent() - } - } - } - } + // Confetti overlay: match iOS behavior (start after 0.4s, stop after ~5.4s), + // without changing the layout or height of the sheet. + var showConfetti by remember { mutableStateOf(false) } + val confettiComposition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.confetti) + ) - Spacer(modifier = Modifier.height(16.dp)) + LaunchedEffect(Unit) { + delay(400L) + showConfetti = true + delay(5000L) + showConfetti = false + } val selectedMembers = remember(selections) { (0..5).mapNotNull { index -> @@ -412,76 +377,133 @@ internal fun AddFamilyAvatarGeneratingSheet( } } - Column( + Box( modifier = Modifier .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + contentAlignment = Alignment.TopCenter ) { - Text( - text = "Selected", - style = sheetSubtitleTextStyle(), - color = Greyscale110, - modifier = Modifier.padding(horizontal = 20.dp) - ) - - Spacer(modifier = Modifier.height(10.dp)) - - Row( + Column( modifier = Modifier - .padding(horizontal = 20.dp) - .clip(RoundedCornerShape(12.dp)) - .background(Greyscale30) - .padding(horizontal = 10.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - selectedMembers.forEach { member -> - Box( + Box( + modifier = Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + SubcomposeAsyncImage( + model = state.imageUrl, + contentDescription = null, modifier = Modifier - .size(30.dp) + .size(170.dp) + .clip(CircleShape) + .background(avatarBackgroundColor), + contentScale = ContentScale.Crop ) { - Image( - painter = painterResource(id = member.iconRes), - contentDescription = null, - modifier = Modifier - .fillMaxSize() - .padding(2.dp), - contentScale = ContentScale.Fit - ) + when (painter.state) { + is coil.compose.AsyncImagePainter.State.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(avatarBackgroundColor), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(160.dp), + strokeWidth = 3.dp, + color = Primary800 + ) + } + } + else -> { + SubcomposeAsyncImageContent() + } + } } } - } - Spacer(modifier = Modifier.height(18.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Meet your new avatar,\nlooking good!", - style = sheetTitleTextStyle(), - color = Greyscale150, - textAlign = TextAlign.Center - ) + Text( + text = "Selected", + style = sheetSubtitleTextStyle(), + color = Greyscale110, + modifier = Modifier.padding(horizontal = 20.dp) + ) - Spacer(modifier = Modifier.height(18.dp)) + Spacer(modifier = Modifier.height(10.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalAlignment = Alignment.CenterVertically - ) { - SecondaryButton( - title = "Regenerate", - textColor = Primary800, - icon = R.drawable.two_star_image, - takeFullWidth = true, - modifier = Modifier.weight(1f).fillMaxWidth(), - onClick = onRegenerate + Row( + modifier = Modifier + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Greyscale30) + .padding(horizontal = 10.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + selectedMembers.forEach { member -> + Box( + modifier = Modifier + .size(30.dp) + ) { + Image( + painter = painterResource(id = member.iconRes), + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .padding(2.dp), + contentScale = ContentScale.Fit + ) + } + } + } + + Spacer(modifier = Modifier.height(18.dp)) + + Text( + text = "Meet your new avatar,\nlooking good!", + style = sheetTitleTextStyle(), + color = Greyscale150, + textAlign = TextAlign.Center ) - PrimaryButton( - title = "Assign", - takeFullWidth = true, - modifier = Modifier.weight(1f).fillMaxWidth(), - onClick = { onAssign(state.imageUrl) } + Spacer(modifier = Modifier.height(18.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SecondaryButton( + title = "Regenerate", + textColor = Primary800, + icon = R.drawable.two_star_image, + takeFullWidth = true, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + onClick = onRegenerate + ) + + PrimaryButton( + title = "Assign", + takeFullWidth = true, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + onClick = { onAssign(state.imageUrl) } + ) + } + } + + if (showConfetti && confettiComposition != null) { + LottieAnimation( + composition = confettiComposition, + iterations = LottieConstants.IterateForever, + modifier = Modifier + .matchParentSize() ) } } @@ -1267,6 +1289,17 @@ private fun AddFamilyAllergiesFullPreview_Pixel8Pro() { val selectedMemberIdState = remember { mutableStateOf(members.first().id) } val selectedAllergiesState = remember { mutableStateListOf() } + val context = LocalContext.current + var dynamicLoaded by remember { mutableStateOf(false) } + LaunchedEffect(context) { + DynamicStepsLoader.ensureLoaded(context) + dynamicLoaded = true + } + val steps = remember(dynamicLoaded) { + DynamicStepsLoader.getSteps()?.map { s -> + CapsuleStep(s.id, s.header.name, OnboardingChipData.iconResForStepId(s.id)) + } ?: emptyList() + } OnboardingShell( onDismissRequest = { }, @@ -1281,26 +1314,7 @@ private fun AddFamilyAllergiesFullPreview_Pixel8Pro() { AnimatedProgressLine(progress = 0.1f) Spacer(modifier = Modifier.height(10.dp)) CapsuleStepperRow( - steps = listOf( - CapsuleStep("allergies", "Allergies", R.drawable.ic_step_allergies), - CapsuleStep("intolerances", "Intolerances", R.drawable.ic_step_intolerances), - CapsuleStep( - "health_conditions", - "Health Conditions", - R.drawable.ic_step_health_conditions - ), - CapsuleStep("life_stage", "Life Stage", R.drawable.ic_step_life_style), - CapsuleStep("region", "Region", R.drawable.ic_step_region), - CapsuleStep("avoid", "Avoid", R.drawable.ic_step_avoid_cross), - CapsuleStep( - "life_style", - "Life Style", - R.drawable.ic_step_diet_preferences - ), - CapsuleStep("nutrition", "Nutrition", R.drawable.ic_step_meals), - CapsuleStep("ethical", "Ethical", R.drawable.ic_step_ethical), - CapsuleStep("taste", "Taste", R.drawable.iconoir_chocolate) - ), + steps = steps, activeIndex = 0 ) } @@ -1320,6 +1334,14 @@ private fun AddFamilyAllergiesFullPreview_Pixel8Pro() { } }, onNext = { }, + onSkipPreferences = { }, + showFineTuneDecision = false, + showSummaryScreen = false, + hasOtherSelection = selectedAllergiesState.any { it.contains("other", ignoreCase = true) }, + showChatBotIntro = false, + showChatConversation = false, + onChatBotLetsGo = {}, + onChatSkip = {}, questionStepIndex = 0 ) } diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/PreferenceSummaryScreen.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/PreferenceSummaryScreen.kt new file mode 100644 index 0000000..7d77d23 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/PreferenceSummaryScreen.kt @@ -0,0 +1,138 @@ +package lc.fungee.Ingredicheck.onboarding.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData +import lc.fungee.Ingredicheck.onboarding.ui.components.PreferenceCapsuleCard +import lc.fungee.Ingredicheck.ui.components.NonDraggableBottomSheet +import lc.fungee.Ingredicheck.ui.components.buttons.PrimaryButton +import lc.fungee.Ingredicheck.ui.theme.Greyscale10 +import lc.fungee.Ingredicheck.ui.theme.Greyscale120 +import lc.fungee.Ingredicheck.ui.theme.Greyscale150 +import lc.fungee.Ingredicheck.ui.theme.Manrope +import lc.fungee.Ingredicheck.ui.theme.Nunito + +@Composable +fun PreferenceSummaryScreen( + modifier: Modifier = Modifier.Companion, + isFamilyFlow: Boolean, + selectedChipIds: Set, + onEditSection: (String) -> Unit = {}, + onContinue: () -> Unit = {} +) { + val steps = OnboardingChipData.foodNotesStepIds + + Box( + modifier = modifier + .fillMaxSize() + .background(Greyscale10), + contentAlignment = Alignment.Companion.TopCenter + ) { + Column( + modifier = Modifier.Companion + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + Spacer(modifier = Modifier.Companion.height(30.dp)) + Text( + text = if (isFamilyFlow) "Your IngrediFam Food Notes" else "Your Food Notes", + fontFamily = Nunito, + fontWeight = FontWeight.Companion.Bold, + fontSize = 20.sp, + color = Greyscale150, + textAlign = TextAlign.Companion.Center, + modifier = Modifier.Companion + .fillMaxWidth() + ) + Spacer(modifier = Modifier.Companion.height(16.dp)) + + LazyColumn( + modifier = Modifier.Companion + + .weight(1f) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(steps) { stepId -> + val stepIndex = steps.indexOf(stepId) + if (stepIndex >= 0) { + val stepChips = OnboardingChipData.chipsForStep(stepIndex) + val chipIdsForStep = stepChips.map { it.id }.toSet() + val selectedForStep = + selectedChipIds.filter { it in chipIdsForStep }.toSet() + if (selectedForStep.isNotEmpty()) { + PreferenceCapsuleCard( + selectedChipIds = selectedForStep, + sectionTitle = stepId.replaceFirstChar { it.uppercase() }, + sectionIconRes = OnboardingChipData.iconResForStepId(stepId), + showEditAction = true, + onEditClick = { onEditSection(stepId) } + ) + } + } + } + + + } + } + + NonDraggableBottomSheet( + onDismissRequest = {}, + horizontalPaddingEnabled = true + ) { + Column( + modifier = Modifier.Companion + .fillMaxWidth(), + horizontalAlignment = Alignment.Companion.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = if (isFamilyFlow) "All set to join your family!" else "Preferences added successfully!", + fontFamily = Nunito, + fontWeight = FontWeight.Companion.Bold, + fontSize = 20.sp, + color = Greyscale150, + textAlign = TextAlign.Companion.Center, + modifier = Modifier.Companion.fillMaxWidth() + ) + Text( + text = if (isFamilyFlow) { + "Your family’s food preferences are already added.You can review them anytime, or edit a specific preference section by tapping Edit." + } else { + "Your food preferences are saved. You can review them anytime, or edit a specific preference section by tapping Edit." + }, + fontFamily = Manrope, + fontWeight = FontWeight.Companion.Normal, + fontSize = 14.sp, + color = Greyscale120, + textAlign = TextAlign.Companion.Center, + modifier = Modifier.Companion.fillMaxWidth() + ) + Spacer(modifier = Modifier.Companion.height(4.dp)) + PrimaryButton( + title = "Continue", +// modifier = Modifier.fillMaxWidth(), + takeFullWidth = true, + onClick = onContinue + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/SignInScreens.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/SignInScreens.kt index 62a7864..e52c277 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/SignInScreens.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/SignInScreens.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardCapitalization + import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.TextStyle @@ -66,8 +66,6 @@ import lc.fungee.Ingredicheck.ui.theme.Greyscale110 import lc.fungee.Ingredicheck.ui.theme.Greyscale150 import lc.fungee.Ingredicheck.ui.theme.Nunito import lc.fungee.Ingredicheck.ui.theme.buttonIconSize -import lc.fungee.Ingredicheck.ui.theme.sheetSubtitleTextStyle -import lc.fungee.Ingredicheck.ui.theme.sheetTitleTextStyle import lc.fungee.Ingredicheck.ui.theme.subtitleTextStyle import lc.fungee.Ingredicheck.ui.theme.titleTextStyle @@ -501,7 +499,9 @@ private fun InviteCodeBox( @Composable internal fun SignInWhoIsThisForSheet( onBackClick: () -> Unit, - isLoading: Boolean, + isJustMeLoading: Boolean = false, + isAddFamilyLoading: Boolean = false, + isAuthLoading: Boolean = false, onJustMe: () -> Unit, onAddFamily: () -> Unit ) { @@ -525,8 +525,8 @@ internal fun SignInWhoIsThisForSheet( modifier = Modifier.weight(1f), takeFullWidth = true, width = 0.dp, - isLoading = isLoading, - isDisabled = isLoading, + isLoading = isJustMeLoading, + isDisabled = isAuthLoading || isJustMeLoading, textColor = Greyscale150, borderColor = Greyscale40, textStyle = TextStyle( @@ -544,8 +544,8 @@ internal fun SignInWhoIsThisForSheet( modifier = Modifier.weight(1f), takeFullWidth = true, width = 0.dp, - isLoading = isLoading, - isDisabled = isLoading, + isLoading = isAddFamilyLoading, + isDisabled = isAuthLoading || isAddFamilyLoading, textColor = Greyscale150, borderColor = Greyscale40, textStyle = TextStyle( diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/CapsuleStepperRow.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/CapsuleStepperRow.kt index cc1cc3b..f0d70e4 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/CapsuleStepperRow.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/CapsuleStepperRow.kt @@ -56,6 +56,7 @@ data class CapsuleStep( fun CapsuleStepperRow( steps: List, activeIndex: Int, + maxReachedIndex: Int = activeIndex, modifier: Modifier = Modifier.Companion, onStepClick: ((Int) -> Unit)? = null, inactiveColor: Color = Color(0xFFF6FCED), @@ -69,19 +70,14 @@ fun CapsuleStepperRow( if (steps.isEmpty()) return val clampedActive = activeIndex.coerceIn(0, steps.lastIndex) - - var maxReachedIndex by remember { mutableIntStateOf(clampedActive) } - if (clampedActive > maxReachedIndex) { - maxReachedIndex = clampedActive - } + val clampedMaxReached = maxReachedIndex.coerceIn(clampedActive, steps.lastIndex) // Adjust maxReachedIndex for progress calculation: exclude the excluded step - val effectiveMaxReachedIndex = if (progressExcludedIndex != null && maxReachedIndex > progressExcludedIndex) { - maxReachedIndex - 1 + val effectiveMaxReachedIndex = if (progressExcludedIndex != null && clampedMaxReached > progressExcludedIndex) { + clampedMaxReached - 1 } else { - maxReachedIndex + clampedMaxReached } - // Layout model (fixed widths so we can compute the progress fill length deterministically) val collapsedHeight = 44.dp val collapsedWidth = 64.dp @@ -94,10 +90,10 @@ fun CapsuleStepperRow( // Calculate fill width to the max reached index. // If the active item is before the max reached index, we must add the expansion delta // because the active item (which is wider) pushes the subsequent items (up to max) further to the right. - val baseFill = (collapsedWidth + itemSpacing) * maxReachedIndex + val baseFill = (collapsedWidth + itemSpacing) * clampedMaxReached // Use the *measured* active capsule width instead of the old fixed expandedWidth, // so the filled line stops exactly before the first *unvisited* capsule. - val expansionDelta = if (clampedActive < maxReachedIndex) (activeItemWidth - collapsedWidth) else 0.dp + val expansionDelta = if (clampedActive < clampedMaxReached) (activeItemWidth - collapsedWidth) else 0.dp val fillToStartOfActive = baseFill + expansionDelta val fillToStartOfActiveState by animateDpAsState( @@ -301,4 +297,4 @@ private fun CapsuleStepperRowPreview() { } -} \ No newline at end of file +} diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/ChipPillComponents.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/ChipPillComponents.kt new file mode 100644 index 0000000..50597ff --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/ChipPillComponents.kt @@ -0,0 +1,102 @@ +package lc.fungee.Ingredicheck.onboarding.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.material3.Text +import androidx.compose.ui.graphics.Color +import lc.fungee.Ingredicheck.ui.theme.Greyscale30 +import lc.fungee.Ingredicheck.ui.theme.Greyscale150 +import lc.fungee.Ingredicheck.ui.theme.Manrope +import lc.fungee.Ingredicheck.ui.theme.Secondary200 + +@Composable +fun FlowRowChips( + modifier: Modifier = Modifier, + horizontalSpacing: Dp = 8.dp, + verticalSpacing: Dp = 8.dp, + content: @Composable () -> Unit +) { + Layout(content = content, modifier = modifier) { measurables, constraints -> + if (measurables.isEmpty()) { + return@Layout layout(0, 0) {} + } + val density = this + val spacingX = with(density) { horizontalSpacing.roundToPx() } + val spacingY = with(density) { verticalSpacing.roundToPx() } + val placeable = measurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) } + val maxWidth = constraints.maxWidth + var x = 0 + var y = 0 + var rowHeight = 0 + val positions = placeable.map { p -> + if (x > 0 && x + p.width > maxWidth) { + x = 0 + y += rowHeight + spacingY + rowHeight = 0 + } + val pos = x to y + x += p.width + spacingX + rowHeight = maxOf(rowHeight, p.height) + pos + } + val totalHeight = (y + rowHeight).coerceIn(constraints.minHeight, constraints.maxHeight) + layout(maxWidth, totalHeight) { + placeable.forEachIndexed { i, p -> + val (px, py) = positions[i] + p.placeRelative(px, py) + } + } + } +} + +private val SelectedPillBackground = Secondary200 +private val PillShape = RoundedCornerShape(30.dp) + +@Composable +fun SelectedChipPill( + emoji: String, + label: String, + trailingAvatars: (@Composable () -> Unit)? = null +) { + Row( + modifier = Modifier + .clip(PillShape) + .background(SelectedPillBackground) + .padding(horizontal = 12.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = emoji.trim(), + fontSize = 16.sp, + color = Greyscale150 + ) + Text( + text = label, + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = Greyscale150, + maxLines = 1 + ) + if (trailingAvatars != null) { + Spacer(modifier = Modifier.width(6.dp)) + trailingAvatars() + } + } +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/FallingCapsulesScreen.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FallingCapsulesScreen.kt similarity index 54% rename from app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/FallingCapsulesScreen.kt rename to app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FallingCapsulesScreen.kt index 3e3d341..99c4af3 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/FallingCapsulesScreen.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FallingCapsulesScreen.kt @@ -1,4 +1,4 @@ - package lc.fungee.Ingredicheck.onboarding.ui +package lc.fungee.Ingredicheck.onboarding.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -37,7 +37,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min import kotlin.random.Random /** @@ -46,16 +48,18 @@ import kotlin.random.Random */ @Composable fun FallingCapsulesScreen( - modifier: Modifier = Modifier, - seed: Int = remember { Random.nextInt() }, + modifier: Modifier = Modifier.Companion, + seed: Int = remember { Random.Default.nextInt() }, spawnIntervalMs: Long = 280L, // Slightly faster spawn maxCapsules: Int = 22, // More capsules to fill the bottom - gravity: Float = 3200f, + gravity: Float = 2800f, bottomInset: Dp = 0.dp ) { val bodies = remember { mutableStateListOf() } // Ensure we use the provided seed, but it's generated once per composition - val rng = remember(seed) { Random(seed) } + val rng = remember(seed) { Random(seed) } + // One capsule per spec, shuffled so the order feels organic and non‑repeating. + val specs = remember(seed) { defaultCapsuleSpecs().shuffled(rng) } BoxWithConstraints(modifier = modifier.fillMaxSize()) { val density = LocalDensity.current @@ -72,20 +76,27 @@ fun FallingCapsulesScreen( containerH = heightPx var nextId by remember { mutableIntStateOf(0) } + var nextSpecIndex by remember { mutableIntStateOf(0) } // Spawn Loop LaunchedEffect(containerW, containerH, seed) { if (containerW <= 0f || containerH <= 0f) return@LaunchedEffect bodies.clear() nextId = 0 + nextSpecIndex = 0 - while (isActive && bodies.size < maxCapsules) { + // Only spawn as many capsules as we have unique specs. + val maxUnique = min(maxCapsules, specs.size) + + while (isActive && bodies.size < maxUnique && nextSpecIndex < specs.size) { // Anti-Stacking Rhythm: Add varied delay per chip delay(spawnIntervalMs + rng.nextLong(0, 150)) - + + val spec = specs[nextSpecIndex++] bodies.add( createCapsule( id = nextId++, + spec = spec, rng = rng, containerW = containerW, density = density @@ -108,26 +119,19 @@ fun FallingCapsulesScreen( val dt = ((now - lastNanos).coerceAtMost(32_000_000L)) / 1_000_000_000f lastNanos = now - // Sub-stepping for stability - val steps = 4 + // Sub-stepping for stability (more steps = smoother, less stuck/vibrate) + val steps = 6 val subDt = dt / steps - + repeat(steps) { - // 1. Apply Gravity & Movement + // 1. Apply gravity and integrate (no random jitter – like iOS SpriteKit) for (i in bodies.indices) { val b = bodies[i] - - val effectiveGravity = if (b.isSettled) 0f else gravity - - // Flight Jitter: Subtle horizontal force to prevent towers - val jitter = if (!b.isSettled) (rng.nextFloat() - 0.5f) * 120f else 0f - + val effectiveGravity = if (b.isSettled) 0f else gravity / b.mass val nextVy = b.vy + effectiveGravity * subDt - val nextVx = (b.vx + jitter * subDt) * 0.95f // Air friction - + val nextVx = b.vx * 0.98f // light air friction only val nextX = b.x + nextVx * subDt val nextY = b.y + nextVy * subDt - bodies[i] = b.copy( x = nextX, y = nextY, @@ -136,21 +140,19 @@ fun FallingCapsulesScreen( ) } - // 2. Resolve Boundary & Stacking - resolveCollisions(bodies, containerW, containerH, rng) - - // 3. Post-Collision: Check support stability - // Slide-Heavy: 55% support needed to stay settled + resolveCollisions(bodies, containerW, containerH) + + // 3. Un-settle only if no support and body is clearly above floor (wider tolerance) for (i in bodies.indices) { val b = bodies[i] - if (b.isSettled && b.y + b.h < containerH - 1f) { + if (b.isSettled && b.y + b.h < containerH - 2f) { var hasSupport = false for (j in bodies.indices) { if (i == j) continue val other = bodies[j] - val overlapX = min(b.x + b.w, other.x + other.w) - max(b.x, other.x) - // Requirement: at least 55% overlap to stay settled - if (abs(other.y - (b.y + b.h)) < 5f && overlapX > b.w * 0.55f) { + val overlapX = + min(b.x + b.w, other.x + other.w) - max(b.x, other.x) + if (abs(other.y - (b.y + b.h)) < 10f && overlapX > b.w * 0.5f) { hasSupport = true break } @@ -166,11 +168,11 @@ fun FallingCapsulesScreen( } // Render - Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.Companion.fillMaxSize()) { for (b in bodies) { CapsuleChip( body = b, - modifier = Modifier + modifier = Modifier.Companion .graphicsLayer { translationX = b.x translationY = b.y @@ -188,6 +190,7 @@ private data class CapsuleBody( val gradient: List, val w: Float, val h: Float, + val mass: Float, val x: Float, val y: Float, val vx: Float, @@ -199,29 +202,46 @@ private data class CapsuleBody( private fun createCapsule( id: Int, + spec: CapsuleSpec, rng: Random, containerW: Float, density: Density ): CapsuleBody { - val specs = defaultCapsuleSpecs() - val spec = specs[rng.nextInt(specs.size)] + // Vary mass slightly per capsule so some fall / settle a bit differently. + val mass = 0.8f + rng.nextFloat() * 0.7f // 0.8 .. 1.5 + + // Approximate content-based width: emoji (18sp) + text (14sp) + padding. + val parts = spec.label.split(" ", limit = 2) + val emojiPart = parts.getOrNull(0)?.trim().orEmpty() + val textPart = parts.getOrNull(1)?.trim().let { value -> + if (value.isNullOrBlank()) spec.label.trim() else value + } + + val emojiFontPx = with(density) { 18.sp.toPx() } + val titleFontPx = with(density) { 14.sp.toPx() } + val emojiWidthPx = if (emojiPart.isNotEmpty()) emojiFontPx else 0f + val avgCharWidthPx = titleFontPx * 0.6f + val titleWidthPx = textPart.length * avgCharWidthPx + + val horizontalPaddingPx = with(density) { 16.dp.toPx() } + val spacingPx = with(density) { 6.dp.toPx() } - val widthDp = rng.nextInt(spec.minWidthDp.value.toInt(), spec.maxWidthDp.value.toInt() + 1).dp - val heightDp = spec.heightDp + val contentWidthPx = + horizontalPaddingPx + + emojiWidthPx + + (if (emojiWidthPx > 0f && textPart.isNotEmpty()) spacingPx else 0f) + + titleWidthPx + + horizontalPaddingPx - val wPx = with(density) { widthDp.toPx() } - val hPx = with(density) { heightDp.toPx() } + val hPx = with(density) { spec.heightDp.toPx() } + val wPx = contentWidthPx - // Extreme Spread Spawning: 120% coverage (-10% to 110%) - val startX = (rng.nextFloat() * 1.2f - 0.1f) * containerW + // Spawn in center band (like iOS: avoid edges to reduce wall hits and sliding) + val startX = (0.05f + rng.nextFloat() * 0.9f) * containerW val y = -hPx * 4f - - // Guide Velocity: Guide chips from extreme edges back toward the center area - val centerX = containerW / 2f - val centerAttract = (centerX - startX) * 0.4f - val vx = (rng.nextFloat() - 0.5f) * 450f + centerAttract - val vy = 100f + rng.nextFloat() * 100f - val rot = (rng.nextFloat() - 0.5f) * 20f + val vx = (rng.nextFloat() - 0.5f) * 120f + val vy = 80f + rng.nextFloat() * 80f + val rot = (rng.nextFloat() - 0.5f) * 12f return CapsuleBody( id = id, @@ -229,6 +249,7 @@ private fun createCapsule( gradient = spec.gradient, w = wPx, h = hPx, + mass = mass, x = startX, y = y, vx = vx, @@ -240,71 +261,64 @@ private fun createCapsule( private fun resolveCollisions( bodies: MutableList, containerW: Float, - containerH: Float, - rng: Random + containerH: Float ) { - val restitution = 0.05f - val settleThreshold = 160f + val restitution = 0.2f + val settleThreshold = 60f + val separationEpsilon = 0.5f - for (i in bodies.indices) { + bodies.indices.forEach { i -> val b = bodies[i] - - // Floor + if (b.y + b.h > containerH) { if (!b.hasBounced && abs(b.vy) > settleThreshold) { bodies[i] = b.copy(y = containerH - b.h, vy = -b.vy * restitution, hasBounced = true) } else { - bodies[i] = b.copy(y = containerH - b.h, vy = 0f, isSettled = true) + bodies[i] = b.copy(y = containerH - b.h, vy = 0f, vx = 0f, isSettled = true) } } - // Walls - if (b.x < 0) { - bodies[i] = bodies[i].copy(x = 0f, vx = abs(b.vx) * 0.15f) - } else if (b.x + b.w > containerW) { - bodies[i] = bodies[i].copy(x = containerW - b.w, vx = -abs(b.vx) * 0.15f) + if (bodies[i].x < 0) { + bodies[i] = bodies[i].copy(x = 0f, vx = 0f) + } else if (bodies[i].x + bodies[i].w > containerW) { + bodies[i] = bodies[i].copy(x = containerW - bodies[i].w, vx = 0f) } } - // Inter-capsule stacking & sliding - repeat(2) { + repeat(3) { for (i in 0 until bodies.size - 1) { for (j in i + 1 until bodies.size) { val a = bodies[i] val b = bodies[j] - val overlapX = min(a.x + a.w, b.x + b.w) - max(a.x, b.x) val overlapY = min(a.y + a.h, b.y + b.h) - max(a.y, b.y) if (overlapX > 0 && overlapY > 0) { - // Slide-Heavy Threshold: 55% horizontal overlap - val isVertical = (overlapY < overlapX) && (overlapX > min(a.w, b.w) * 0.55f) - + val isVertical = overlapY < overlapX && overlapX > min(a.w, b.w) * 0.5f + val sepY = overlapY + separationEpsilon + val pushX = (overlapX / 2f) + separationEpsilon * 0.5f + if (isVertical) { if (a.y < b.y) { if (!a.hasBounced && abs(a.vy) > settleThreshold) { - bodies[i] = a.copy(y = a.y - overlapY, vy = -a.vy * restitution, hasBounced = true) + bodies[i] = a.copy(y = a.y - sepY, vy = -a.vy * restitution, hasBounced = true) } else { - bodies[i] = a.copy(y = a.y - overlapY, vy = 0f, isSettled = true) + bodies[i] = a.copy(y = a.y - sepY, vy = 0f, vx = 0f, isSettled = true) } } else { if (!b.hasBounced && abs(b.vy) > settleThreshold) { - bodies[j] = b.copy(y = b.y - overlapY, vy = -b.vy * restitution, hasBounced = true) + bodies[j] = b.copy(y = b.y - sepY, vy = -b.vy * restitution, hasBounced = true) } else { - bodies[j] = b.copy(y = b.y - overlapY, vy = 0f, isSettled = true) + bodies[j] = b.copy(y = b.y - sepY, vy = 0f, vx = 0f, isSettled = true) } } } else { - // Aggressive Slide Force - val nudge = if (overlapX < min(a.w, b.w) * 0.3f) 2.2f else 1.2f - val push = (overlapX / 2f) * nudge - if (a.x < b.x) { - bodies[i] = a.copy(x = a.x - push, vx = -abs(a.vx) * 0.4f, isSettled = false) - bodies[j] = b.copy(x = b.x + push, vx = abs(b.vx) * 0.4f, isSettled = false) + bodies[i] = a.copy(x = a.x - pushX, vx = a.vx * 0.1f, isSettled = false) + bodies[j] = b.copy(x = b.x + pushX, vx = b.vx * 0.1f, isSettled = false) } else { - bodies[i] = a.copy(x = a.x + push, vx = abs(a.vx) * 0.4f, isSettled = false) - bodies[j] = b.copy(x = b.x - push, vx = -abs(b.vx) * 0.4f, isSettled = false) + bodies[i] = a.copy(x = a.x + pushX, vx = a.vx * 0.1f, isSettled = false) + bodies[j] = b.copy(x = b.x - pushX, vx = b.vx * 0.1f, isSettled = false) } } } @@ -323,56 +337,71 @@ private data class CapsuleSpec( private fun defaultCapsuleSpecs(): List { return listOf( - CapsuleSpec("Mediterranean", listOf(Color(0xFFF6A54F), Color(0xFFF07A2D)), 150.dp, 190.dp, 38.dp), - CapsuleSpec("Dairy Free", listOf(Color(0xFF8D6BFF), Color(0xFF6A4BFF)), 120.dp, 155.dp, 38.dp), - CapsuleSpec("Organic Only", listOf(Color(0xFFFF6D77), Color(0xFFFF3D4E)), 130.dp, 170.dp, 38.dp), - CapsuleSpec("Low Fat", listOf(Color(0xFF7FE0FF), Color(0xFF4BC7F8)), 110.dp, 140.dp, 38.dp), - CapsuleSpec("High Protein", listOf(Color(0xFF4FA0FF), Color(0xFF297BFF)), 140.dp, 175.dp, 38.dp), - CapsuleSpec("Paleo", listOf(Color(0xFFFF8A65), Color(0xFFFF7043)), 95.dp, 125.dp, 38.dp), - CapsuleSpec("Low Sugar", listOf(Color(0xFFFFB74D), Color(0xFFFF9800)), 115.dp, 150.dp, 38.dp), - CapsuleSpec("Vegetarian", listOf(Color(0xFF9CCC65), Color(0xFF7CB342)), 120.dp, 155.dp, 38.dp), - CapsuleSpec("Gluten", listOf(Color(0xFFFFA726), Color(0xFFFF8F00)), 95.dp, 125.dp, 38.dp), - CapsuleSpec("Celery", listOf(Color(0xFF66BB6A), Color(0xFF43A047)), 95.dp, 125.dp, 38.dp), - CapsuleSpec("Molluscs", listOf(Color(0xFFFF8A80), Color(0xFFFF5252)), 110.dp, 145.dp, 38.dp), - CapsuleSpec("Heart Health", listOf(Color(0xFFFF8DA1), Color(0xFFFF5D7E)), 135.dp, 175.dp, 38.dp) + // Order and labels (including emoji) match iOS ChipCategory list. + CapsuleSpec("πŸ«’ Mediterranean", listOf(Color(0xFFF6A54F), Color(0xFFF07A2D)), 150.dp, 190.dp, 38.dp), + CapsuleSpec("πŸ₯› Dairy Free", listOf(Color(0xFF8D6BFF), Color(0xFF6A4BFF)), 120.dp, 155.dp, 38.dp), + CapsuleSpec("πŸƒ Organic Only", listOf(Color(0xFFFF6D77), Color(0xFFFF3D4E)), 130.dp, 170.dp, 38.dp), + CapsuleSpec("πŸ₯© Paleo", listOf(Color(0xFFFF8A65), Color(0xFFFF7043)), 95.dp, 125.dp, 38.dp), + CapsuleSpec("πŸ“ Low Sugar", listOf(Color(0xFFFFB74D), Color(0xFFFF9800)), 115.dp, 150.dp, 38.dp), + CapsuleSpec("πŸ₯¦ Vegetarian", listOf(Color(0xFF9CCC65), Color(0xFF7CB342)), 120.dp, 155.dp, 38.dp), + CapsuleSpec("πŸ«€ Heart Health", listOf(Color(0xFFFF8DA1), Color(0xFFFF5D7E)), 135.dp, 175.dp, 38.dp), + CapsuleSpec("🐚 Molluscs", listOf(Color(0xFFFF8A80), Color(0xFFFF5252)), 110.dp, 145.dp, 38.dp), + CapsuleSpec("πŸ— High Protein", listOf(Color(0xFF4FA0FF), Color(0xFF297BFF)), 140.dp, 175.dp, 38.dp), + CapsuleSpec("πŸ₯¬ Celery", listOf(Color(0xFF66BB6A), Color(0xFF43A047)), 95.dp, 125.dp, 38.dp), + CapsuleSpec("πŸ₯‘ Low Fat", listOf(Color(0xFF7FE0FF), Color(0xFF4BC7F8)), 110.dp, 140.dp, 38.dp), + CapsuleSpec("🌾 Gluten", listOf(Color(0xFFFFA726), Color(0xFFFF8F00)), 95.dp, 125.dp, 38.dp) ) } @Composable private fun CapsuleChip( body: CapsuleBody, - modifier: Modifier = Modifier + modifier: Modifier = Modifier.Companion ) { val shape = RoundedCornerShape(percent = 50) - val brush = remember(body.gradient) { Brush.horizontalGradient(body.gradient) } + val brush = remember(body.gradient) { Brush.Companion.horizontalGradient(body.gradient) } Surface( modifier = modifier - .size(with(LocalDensity.current) { body.w.toDp() }, with(LocalDensity.current) { body.h.toDp() }), + // Let width be driven purely by content; only fix the height. + .height(with(LocalDensity.current) { body.h.toDp() }), shape = shape, - color = Color.Transparent, + color = Color.Companion.Transparent, shadowElevation = 8.dp ) { Row( - modifier = Modifier + modifier = Modifier.Companion .clip(shape) .background(brush) - .padding(horizontal = 14.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.Companion.CenterVertically ) { - Box( - modifier = Modifier - .size(8.dp) - .background(Color(0x33000000), RoundedCornerShape(percent = 50)) - ) - Spacer(modifier = Modifier.size(10.dp)) - Text( - text = body.label, - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.bodyMedium - ) + val parts = body.label.split(" ", limit = 2) + if (parts.size == 2) { + Text( + text = parts[0].trim(), + color = Color.Companion.White, + fontSize = 18.sp, + fontWeight = FontWeight.Companion.Medium, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.Companion.width(4.dp)) + Text( + text = parts[1].trim(), + color = Color.Companion.White, + fontSize = 14.sp, + fontWeight = FontWeight.Companion.Medium, + style = MaterialTheme.typography.bodyMedium + ) + } else { + Text( + text = body.label, + color = Color.Companion.White, + fontSize = 14.sp, + fontWeight = FontWeight.Companion.Medium, + style = MaterialTheme.typography.bodyMedium + ) + } } } } @@ -381,11 +410,11 @@ private fun CapsuleChip( @Composable private fun FallingCapsulesScreenPreview() { FallingCapsulesScreen( - modifier = Modifier + modifier = Modifier.Companion .fillMaxSize() .background(Color(0xFFF6F6F6)), seed = 42, spawnIntervalMs = 300L, maxCapsules = 16 ) -} +} \ No newline at end of file diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FamilyOverviewBackground.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FamilyOverviewBackground.kt new file mode 100644 index 0000000..8a85a27 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FamilyOverviewBackground.kt @@ -0,0 +1,321 @@ +package lc.fungee.Ingredicheck.onboarding.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.SubcomposeAsyncImage +import coil.compose.SubcomposeAsyncImageContent +import lc.fungee.Ingredicheck.R +import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData +import lc.fungee.Ingredicheck.onboarding.data.avatarBackgroundColorForId +import lc.fungee.Ingredicheck.onboarding.model.OnboardingViewModel +import lc.fungee.Ingredicheck.onboarding.ui.components.familyPlaceholderColor +import lc.fungee.Ingredicheck.ui.theme.Greyscale40 +import lc.fungee.Ingredicheck.ui.theme.Greyscale100 +import lc.fungee.Ingredicheck.ui.theme.Greyscale110 +import lc.fungee.Ingredicheck.ui.theme.Greyscale150 +import lc.fungee.Ingredicheck.ui.theme.Manrope +import lc.fungee.Ingredicheck.ui.theme.Nunito +import lc.fungee.Ingredicheck.ui.theme.Primary800 + +@Composable +fun FamilyOverviewBackground( + members: List, + modifier: Modifier = Modifier, + bottomSheetHeight: Dp = 0.dp, + onLeaveFamily: (() -> Unit)? = null, + onInvite: ((OnboardingViewModel.FamilyOverviewMember) -> Unit)? = null, + onEditMember: ((OnboardingViewModel.FamilyOverviewMember) -> Unit)? = null +) { + Column( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .padding(top = 64.dp, start = 20.dp, end = 20.dp) + ) { + + Text( + text = "Your Family Overview", + style = TextStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + color = Greyscale150 + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(bottom = bottomSheetHeight + 24.dp) + ) { + members.forEachIndexed { index, member -> + if (index > 0) Spacer(modifier = Modifier.height(14.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(28.dp)) + .border(1.dp, Greyscale110.copy(alpha = 0.28f), RoundedCornerShape(28.dp)) + .background(Color.White) + .padding(horizontal = 18.dp, vertical = 16.dp) + ) { + Box( + + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + val hasGenerated = member.generatedAvatarUrl.trim().isNotBlank() + val circleBackground = if (hasGenerated) { + avatarBackgroundColorForId(member.backgroundColorId) + } else { + Color.White + } + + Box( + modifier = Modifier.size(54.dp), + contentAlignment = Alignment.BottomEnd + ) { + // Avatar circle + Box( + modifier = Modifier + .matchParentSize() + .clip(CircleShape) + .border(2.dp, Primary800.copy(alpha = 0.18f), CircleShape) + .background(circleBackground), + contentAlignment = Alignment.Center + ) { + val trimmedUrl = member.generatedAvatarUrl.trim() + val res = OnboardingChipData.avatarResOrNull(member.avatarId.trim()) + when { + trimmedUrl.isNotBlank() -> { + SubcomposeAsyncImage( + model = trimmedUrl, + contentDescription = null, + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + ) { + when (painter.state) { + is coil.compose.AsyncImagePainter.State.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(circleBackground), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = Primary800 + ) + } + } + else -> { + SubcomposeAsyncImageContent() + } + } + } + } + res != null -> { + Image( + painter = painterResource(id = res), + contentDescription = null, + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + ) + } + else -> { + val bg = familyPlaceholderColor(member.name) + Box( + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + .background(bg), + contentAlignment = Alignment.Center + ) { + val initial = member.name.trim().firstOrNull()?.uppercase() ?: "?" + Text( + text = initial, + style = TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + color = Color.White + ) + ) + } + } + } + } + + // Edit (pen) icon overlay + if (onEditMember != null) { + Box( + modifier = Modifier + .offset(x = 2.dp, y = 2.dp) + .size(20.dp) + .clip(CircleShape) + .background(color = Greyscale40) + .clickable { onEditMember(member) }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.pen_line_icon), + tint = Greyscale150, + contentDescription = null, + modifier = Modifier.size(12.dp) + ) + } + } + } + + Spacer(modifier = Modifier.width(14.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = member.name, + style = TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + color = Greyscale150 + ) + ) + + if (member.invitePending) { + Spacer(modifier = Modifier.height(4.dp)) + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.50.dp)) + .background(Color(0xFFFFF9ED)) // soft yellow pending pill + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.exclamation_circle), + contentDescription = null, + tint = Color(0xFFFAB222), + modifier = Modifier.size(12.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Pending", + style = TextStyle( + fontFamily = Nunito, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp, + color = Color(0xFFFAB222) + ) + ) + } + } + } else { + Text( + text = if (member.joined) "(You)" else "Not joined yet !", + style = TextStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = Greyscale110 + ) + ) + } + } + + val isFirst = index == 0 + val isPending = member.invitePending + val actionText = if (isFirst) { + "Leave Family" + } else if (isPending) { + "Re-invite" + } else { + "Invite" + } + val leaveFamilyColor = Color(0xFFFF3F31) + val inviteColor = Color(0xFF75990E) + val actionColor = if (isFirst) leaveFamilyColor else inviteColor + val borderColor = if (isFirst) leaveFamilyColor else Greyscale40 + + Box( + modifier = Modifier + .height(36.dp) + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, borderColor, RoundedCornerShape(18.dp)) + .clickable { + if (isFirst) { + onLeaveFamily?.invoke() + } else { + onInvite?.invoke(member) + } + } + .padding(horizontal = 16.dp), + contentAlignment = Alignment.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (!isFirst) { + Icon( + painter = painterResource(id = R.drawable.share_icon), + contentDescription = null, + tint = actionColor, + modifier = Modifier + .size(16.dp) + .padding(end = 3.dp) + ) + } + + Text( + text = actionText, + style = TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + color = actionColor + ) + ) + } + } + } + } + } + } + } + } +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FamilyPlaceholderUtils.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FamilyPlaceholderUtils.kt new file mode 100644 index 0000000..2f2c748 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/FamilyPlaceholderUtils.kt @@ -0,0 +1,18 @@ +package lc.fungee.Ingredicheck.onboarding.ui.components + +import androidx.compose.ui.graphics.Color +import kotlin.math.absoluteValue + +fun familyPlaceholderColor(seed: String): Color { + val palette = listOf( + Color(0xFF9AD0FF), + Color(0xFFFFB3C1), + Color(0xFFB9F6CA), + Color(0xFFFFE29A), + Color(0xFFD7B9FF), + Color(0xFFFFC59A) + ) + val idx = (seed.hashCode().absoluteValue % palette.size) + return palette[idx] +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/PreferenceCapsuleCard.kt b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/PreferenceCapsuleCard.kt new file mode 100644 index 0000000..401f0f2 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/onboarding/ui/components/PreferenceCapsuleCard.kt @@ -0,0 +1,213 @@ +package lc.fungee.Ingredicheck.onboarding.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import lc.fungee.Ingredicheck.R +import lc.fungee.Ingredicheck.onboarding.data.OnboardingChipData +import lc.fungee.Ingredicheck.ui.theme.Greyscale10 +import lc.fungee.Ingredicheck.ui.theme.Greyscale30 +import lc.fungee.Ingredicheck.ui.theme.Greyscale60 +import lc.fungee.Ingredicheck.ui.theme.Greyscale100 +import lc.fungee.Ingredicheck.ui.theme.Greyscale110 +import lc.fungee.Ingredicheck.ui.theme.Greyscale150 +import lc.fungee.Ingredicheck.ui.theme.Manrope + +@Composable +fun PreferenceCapsuleCard( + modifier: Modifier = Modifier, + selectedChipIds: Set = emptySet(), + sectionTitle: String = "Allergies", + @DrawableRes sectionIconRes: Int = R.drawable.ic_step_allergies, + trailingAvatarsForChip: ((String) -> (@Composable () -> Unit)?)? = null, + showEditAction: Boolean = false, + onEditClick: (() -> Unit)? = null +) { + val showSelectedChips = selectedChipIds.isNotEmpty() + val resolvedChips = remember(selectedChipIds) { + selectedChipIds.mapNotNull { id -> OnboardingChipData.chipForId(id) } + } + val hasOtherSelection = remember(selectedChipIds) { + selectedChipIds.any { it.contains("other", ignoreCase = true) } + } + + BoxWithConstraints( + modifier = modifier + .fillMaxWidth() + .then( + if (showSelectedChips) Modifier.heightIn(min = 130.dp) + else Modifier.height(130.dp) + ) + .clip(RoundedCornerShape(20.dp)) + .border((0.25).dp, Greyscale60, RoundedCornerShape(20.dp)) + .background(Greyscale10) + .padding(12.dp) + ) { + if (showSelectedChips && resolvedChips.isNotEmpty()) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(sectionIconRes), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Greyscale110 + ) + Text( + text = sectionTitle, + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + color = Greyscale110 + ) + Spacer(modifier = Modifier.weight(1f)) + if (showEditAction) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .clickable(enabled = onEditClick != null) { + onEditClick?.invoke() + } + ) { + Text( + text = "Edit", + fontFamily = Manrope, + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + color = Greyscale110 + ) + Icon( + painter = painterResource(id = R.drawable.pen_line_icon), + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = Greyscale110 + ) + } + } + } + Spacer(modifier = Modifier.height(8.dp)) + FlowRowChips( + modifier = Modifier.fillMaxWidth(), + horizontalSpacing = 8.dp, + verticalSpacing = 8.dp + ) { + resolvedChips.forEach { def -> + val trailing = trailingAvatarsForChip?.invoke(def.id) + SelectedChipPill( + emoji = def.iconPrefix, + label = def.label, + trailingAvatars = trailing + ) + } + } + if (hasOtherSelection) { + Spacer(modifier = Modifier.height(6.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + painter = painterResource(R.drawable.emoji_warning), + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = androidx.compose.ui.graphics.Color.Unspecified + ) + Text( + text = "Something else too, don't worry we'll ask later!", + fontFamily = Manrope, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = Greyscale100 + ) + } + } + } + } else { + val maxWidth = maxWidth + val spacing = 8.dp + val minFirst = 90.dp + val minSecond = 110.dp + + fun randomRowWidths(): Pair { + val maxExtra = (maxWidth - spacing - minFirst - minSecond).coerceAtLeast(0.dp) + if (maxExtra == 0.dp) { + val second = (maxWidth - spacing - minFirst).coerceAtLeast(minSecond) + return minFirst to second + } + val extraFraction = kotlin.random.Random.nextFloat() + val first = minFirst + maxExtra * extraFraction + val second = maxWidth - spacing - first + return first to second + } + + val (row1First, row1Second) = remember(maxWidth) { randomRowWidths() } + val (row2First, row2Second) = remember(maxWidth) { randomRowWidths() } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.Start + ) { + Box( + modifier = Modifier + .width(165.dp) + .height(13.dp) + .clip(RoundedCornerShape(4.dp)) + .background(Greyscale30) + ) + Spacer(modifier = Modifier.height(12.dp)) + PreferenceCapsuleSkeletonRow(firstWidth = row1First, secondWidth = row1Second) + Spacer(modifier = Modifier.height(8.dp)) + PreferenceCapsuleSkeletonRow(firstWidth = row2First, secondWidth = row2Second) + } + } + } +} + +@Composable +private fun PreferenceCapsuleSkeletonRow( + firstWidth: Dp, + secondWidth: Dp +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .width(firstWidth) + .height(36.dp) + .clip(RoundedCornerShape(30.dp)) + .background(Greyscale30) + ) + Box( + modifier = Modifier + .width(secondWidth) + .height(36.dp) + .clip(RoundedCornerShape(30.dp)) + .background(Greyscale30) + ) + } +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/ui/chatbot/ChatBotConversationScreen.kt b/app/src/main/java/lc/fungee/Ingredicheck/ui/chatbot/ChatBotConversationScreen.kt new file mode 100644 index 0000000..b19a170 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/ui/chatbot/ChatBotConversationScreen.kt @@ -0,0 +1,330 @@ +package lc.fungee.Ingredicheck.ui.chatbot + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.tooling.preview.Preview + +import lc.fungee.Ingredicheck.R +import lc.fungee.Ingredicheck.ui.components.NonDraggableBottomSheet +import lc.fungee.Ingredicheck.ui.components.buttons.primaryButtonEffect +import lc.fungee.Ingredicheck.ui.theme.Greyscale100 +import lc.fungee.Ingredicheck.ui.theme.Greyscale110 +import lc.fungee.Ingredicheck.ui.theme.Greyscale140 +import lc.fungee.Ingredicheck.ui.theme.Greyscale40 +import lc.fungee.Ingredicheck.ui.theme.Greyscale60 +import lc.fungee.Ingredicheck.ui.theme.Manrope +import lc.fungee.Ingredicheck.ui.theme.Nunito + +private data class ChatMessage(val isUser: Boolean, val text: String) + +/** + * AI chat conversation sheet. Tapping the input opens the keyboard; input stays above keyboard. + * Sent messages appear on the user (right) side. + */ +@Composable +fun ChatBotConversationScreen( + modifier: Modifier = Modifier, + onSkip: () -> Unit = {} +) { + val focusRequester = remember { FocusRequester() } + var inputText by remember { mutableStateOf("") } + val messages = remember { + mutableStateListOf( + ChatMessage(false, "Hello!!"), + ChatMessage(false, "What else did you want to avoid?"), + ChatMessage(true, "I usually avoid processed snacks."), + ChatMessage(false, "That's wonderful!\nThanks for sharing. Anything else about\nyour food habits."), + ChatMessage(true, "Yeah, I follow a mix of ayurvedic and\nseasonal eating.") + ) + } + val scrollState = rememberScrollState() + + LaunchedEffect(messages.size) { + scrollState.animateScrollTo(scrollState.maxValue) + } + + Box( + modifier = modifier + .fillMaxWidth() + .fillMaxHeight(0.7f) + .padding(horizontal = 20.dp), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Text( + text = "Skip", + fontFamily = Manrope, + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = Greyscale110, + textAlign = TextAlign.Center, + modifier = Modifier.clickable { onSkip() } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.weight(1f) + ) { + Icon( + painter = painterResource(id = R.drawable.hugeicons_ai_magic), + tint = Color.Unspecified, + contentDescription = null, + modifier = Modifier.size(30.dp) + ) + Text( + text = "Asking with AI suggestions", + fontFamily = Nunito, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + color = Greyscale110, + textAlign = TextAlign.Center + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Scrollable message list + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + val firstBotIndex = messages.indexOfFirst { !it.isUser } + messages.forEachIndexed { index, msg -> + if (msg.isUser) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + UserBubble(text = msg.text) + } + } else { + val isFirstWithAvatar = (index == firstBotIndex) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (isFirstWithAvatar) { + Image( + painter = painterResource(id = R.drawable.ingredi_robo2), + contentDescription = null, + modifier = Modifier + .size(28.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + ChatBubble(text = msg.text, isFirstWithAvatar = true) + } else { + Spacer(modifier = Modifier.size(28.dp)) + ChatBubble(text = msg.text, isFirstWithAvatar = false) + } + } + } + } + } + + // Input row: stays above keyboard via imePadding(); tap opens keyboard + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .imePadding() + , + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + .height(52.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color.White) + .border( + width = 0.5.dp, + color = Greyscale60, + shape = RoundedCornerShape(16.dp) + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { focusRequester.requestFocus() } + .padding(horizontal = 20.dp, vertical = 14.dp), + contentAlignment = Alignment.CenterStart + ) { + BasicTextField( + value = inputText, + onValueChange = { inputText = it }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + textStyle = TextStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Greyscale140 + ), + cursorBrush = SolidColor(Greyscale140), + singleLine = false, + maxLines = 4, + decorationBox = { inner -> + if (inputText.isEmpty()) { + Text( + text = "\"Type your answer...\"", + fontFamily = Manrope, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Greyscale100 + ) + } + inner() + } + ) + } + + Box( + modifier = Modifier + .size(52.dp) + .clip(CircleShape) + .primaryButtonEffect( + isDisabled = false, + shape = RoundedCornerShape(percent = 50), + disabledBackgroundColor = Greyscale40 + ) + .clickable { + val trimmed = inputText.trim() + if (trimmed.isNotEmpty()) { + messages.add(ChatMessage(isUser = true, text = trimmed)) + inputText = "" + } + }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.msg_send_arrow), + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(22.dp) + ) + } + } + } + } +} + +@Composable +private fun ChatBubble( + text: String, + isFirstWithAvatar: Boolean = false +) { + val shape = if (isFirstWithAvatar) { + RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomEnd = 16.dp, bottomStart = 16.dp) + } else { + RoundedCornerShape(16.dp) + } + Box( + modifier = Modifier + .clip(shape) + .background(Color(0xFF91B640)) + .padding(horizontal = 16.dp, vertical = 10.dp) + ) { + Text( + text = text, + fontFamily = Nunito, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = Color.White + ) + } +} + +@Composable +private fun UserBubble(text: String) { + Box( + modifier = Modifier + .clip(RoundedCornerShape( topStart = 16.dp , bottomStart = 16.dp , topEnd = 16.dp , bottomEnd = 4.dp)) + .background(Greyscale40) + .padding(horizontal = 16.dp, vertical = 10.dp) + ) { + Text( + text = text, + fontFamily = Nunito, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = Greyscale140 + ) + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun ChatBotConversationScreenPreview() { + NonDraggableBottomSheet( + onDismissRequest = {}, + horizontalPaddingEnabled = true + ) { + ChatBotConversationScreen() + } +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/ui/chatbot/ChatBotIntroScreen.kt b/app/src/main/java/lc/fungee/Ingredicheck/ui/chatbot/ChatBotIntroScreen.kt new file mode 100644 index 0000000..58ac829 --- /dev/null +++ b/app/src/main/java/lc/fungee/Ingredicheck/ui/chatbot/ChatBotIntroScreen.kt @@ -0,0 +1,222 @@ +package lc.fungee.Ingredicheck.ui.chatbot + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.tooling.preview.Preview +import lc.fungee.Ingredicheck.R +import lc.fungee.Ingredicheck.ui.components.NonDraggableBottomSheet +import lc.fungee.Ingredicheck.ui.components.buttons.PrimaryButton +import lc.fungee.Ingredicheck.ui.components.buttons.SecondaryButton +import lc.fungee.Ingredicheck.ui.theme.Greyscale100 +import lc.fungee.Ingredicheck.ui.theme.Greyscale110 +import lc.fungee.Ingredicheck.ui.theme.Greyscale140 +import lc.fungee.Ingredicheck.ui.theme.Greyscale150 +import lc.fungee.Ingredicheck.ui.theme.Manrope +import lc.fungee.Ingredicheck.ui.theme.Nunito + +/** + * Intro screen for IngrediBot chat, matching the iOS "Hey! I'm IngrediBot" design. + * This only covers the UI; chat behaviour is wired separately. + */ +@Composable +fun ChatBotIntroScreen( + modifier: Modifier = Modifier, + onMaybeLater: () -> Unit = {}, + onYesLetsGo: () -> Unit = {}, + hasOtherSelection: Boolean = false +) { + Box( + modifier = modifier + .fillMaxWidth() + + // Make the sheet take roughly 90% of the height when used inside NonDraggableBottomSheet. + .fillMaxHeight(0.8f) + .padding(horizontal = 20.dp), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + + ) { + // Floating IngrediBot image at the top‑center, rotated by 10 degrees. + Image( + painter = painterResource(id = R.drawable.ingredi_robo2), + contentDescription = null, + modifier = Modifier + .size(width = 190.dp, height = 180.dp) + .rotate(10f), + contentScale = ContentScale.Fit + ) + + // "Hey! πŸ‘‹ I’m IngrediBot," subtitle + val introText = buildAnnotatedString { + withStyle( + SpanStyle( + fontFamily = Nunito, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + color = Greyscale100 + ) + ) { + append("Hey! πŸ‘‹ I’m ") + } + withStyle( + SpanStyle( + fontFamily = Nunito, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + color = androidx.compose.ui.graphics.Color(0xFF91B640) + ) + ) { + append("IngrediBot,") + } + } + + Text( + text = introText, + textAlign = TextAlign.Center + ) + + // Title: "How about making food choices easier together?" + Text( + text = "How about making food choices easier together?", + fontFamily = Nunito, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = Greyscale150, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) // πŸ‘ˆ takes remaining space + Text( + text = "Shall we get started?", + fontFamily = Nunito, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + color = Greyscale110, + textAlign = TextAlign.Center, + ) + + val introsubText = if (hasOtherSelection) { + buildAnnotatedString { + withStyle( + SpanStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + color = Greyscale110 + ) + ) { + append("I noticed you selected ") + } + withStyle( + SpanStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + color = Greyscale140 + ) + ) { + append("β€œOther”") + } + withStyle( + SpanStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + color = Greyscale110 + ) + ) { + append(" earlier, that’s great! Could you tell me a bit more about it?") + } + } + } else { + buildAnnotatedString { + withStyle( + SpanStyle( + fontFamily = Nunito, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Greyscale110 + ) + ) { + append("Tell me a bit about what kind of food experience you’d love here.") + } + } + } + Text( + text = introsubText, + textAlign = TextAlign.Center + ) + Row(modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp)) + { + SecondaryButton( + "Maybe later", + textColor = Color(color = 0xFF75990E), + modifier = Modifier.weight(1f), + onClick = onMaybeLater + ) + PrimaryButton( + "Yes, let’s go", + modifier = Modifier.weight(1f), + onClick = onYesLetsGo + ) + + } + + Text(text = "No problem! You can come back anytime β€” I’ll be here when you’re ready.", + fontWeight = FontWeight.Normal, + fontSize = 12.sp , + fontFamily = Manrope , + color = Color(0xFFB6B6B6) , + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + + } + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun ChatBotIntroScreenPreview() { + NonDraggableBottomSheet( + onDismissRequest = {}, + horizontalPaddingEnabled = true + ) { + ChatBotIntroScreen() + } +} + diff --git a/app/src/main/java/lc/fungee/Ingredicheck/ui/theme/Color.kt b/app/src/main/java/lc/fungee/Ingredicheck/ui/theme/Color.kt index e53eb99..094abf4 100644 --- a/app/src/main/java/lc/fungee/Ingredicheck/ui/theme/Color.kt +++ b/app/src/main/java/lc/fungee/Ingredicheck/ui/theme/Color.kt @@ -71,7 +71,7 @@ val GrayScale150 = Color(0xFF303030) val Greyscale40 = GrayScale40 val Greyscale50 = GrayScale50 -val Greyscale60 = GrayScale60 + val Greyscale60 = GrayScale60 val Greyscale70 = GrayScale70 val Greyscale80 = GrayScale80 val Greyscale90 = GrayScale90 diff --git a/app/src/main/res/drawable/hugeicons_ai_magic.xml b/app/src/main/res/drawable/hugeicons_ai_magic.xml new file mode 100644 index 0000000..c8a30fb --- /dev/null +++ b/app/src/main/res/drawable/hugeicons_ai_magic.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 5347bce..5e0c8ad 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -3,34 +3,37 @@ android:height="108dp" android:viewportWidth="512" android:viewportHeight="512"> - + + android:fillColor="#200A0A"/> - + + diff --git a/app/src/main/res/drawable/ingredi_robo2.png b/app/src/main/res/drawable/ingredi_robo2.png new file mode 100644 index 0000000..164e52f Binary files /dev/null and b/app/src/main/res/drawable/ingredi_robo2.png differ diff --git a/app/src/main/res/drawable/msg_send_arrow.xml b/app/src/main/res/drawable/msg_send_arrow.xml new file mode 100644 index 0000000..72fd54e --- /dev/null +++ b/app/src/main/res/drawable/msg_send_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 1cbc38c..2c7c15d 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 70733e5..79b2616 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 53a332b..f49df89 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index e8b0ef9..5e65540 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 66f9b74..5e51f0d 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 547d495..073287e 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index fba3a73..e3b2e0f 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index f405973..28afebc 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 8643c8a..f7da6bb 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 774cd93..eb27f23 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/raw/confetti.json b/app/src/main/res/raw/confetti.json new file mode 100644 index 0000000..b1fe2f0 --- /dev/null +++ b/app/src/main/res/raw/confetti.json @@ -0,0 +1,18278 @@ +{ + "v": "5.5.7", + "meta": { + "g": "LottieFiles AE 0.1.20", + "a": "", + "k": "", + "d": "", + "tc": "" + }, + "fr": 25, + "ip": 0, + "op": 126, + "w": 1920, + "h": 1080, + "nm": "ConfettiAnimation", + "ddd": 1, + "assets": [ + { + "id": "comp_0", + "layers": [ + { + "ddd": 1, + "ind": 1, + "ty": 4, + "nm": "Shape Layer 16", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 29, + "s": [ + 1664, + -23, + 0 + ], + "to": [ + 92, + 254.667, + 0 + ], + "ti": [ + 168, + -414.667, + 0 + ] + }, + { + "t": 104, + "s": [ + 1792, + 1185, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.270588235294, + 0.909803981407, + 0.639215686275, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 29, + "op": 105, + "st": 29, + "bm": 0 + }, + { + "ddd": 1, + "ind": 2, + "ty": 4, + "nm": "Shape Layer 15", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 12, + "s": [ + 1235, + -59, + 0 + ], + "to": [ + 96, + 526.667, + 0 + ], + "ti": [ + -104, + -486.667, + 0 + ] + }, + { + "t": 87, + "s": [ + 1039, + 1237, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.239215701234, + 0.419607873056, + 0.709803921569, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 12, + "op": 88, + "st": 12, + "bm": 0 + }, + { + "ddd": 1, + "ind": 3, + "ty": 4, + "nm": "Shape Layer 14", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 18, + "s": [ + 528, + -59, + 0 + ], + "to": [ + 288, + 686.667, + 0 + ], + "ti": [ + -180, + -406.667, + 0 + ] + }, + { + "t": 93, + "s": [ + 708, + 1377, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.270588235294, + 0.909803981407, + 0.639215686275, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 18, + "op": 94, + "st": 18, + "bm": 0 + }, + { + "ddd": 1, + "ind": 4, + "ty": 4, + "nm": "Shape Layer 13", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 15, + "s": [ + 292, + -147, + 0 + ], + "to": [ + -90, + 314.667, + 0 + ], + "ti": [ + 194, + -532.667, + 0 + ] + }, + { + "t": 90, + "s": [ + 292, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -248, + -26 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 15, + "op": 91, + "st": 15, + "bm": 0 + }, + { + "ddd": 1, + "ind": 5, + "ty": 4, + "nm": "Shape Layer 12", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 28, + "s": [ + 1128, + -59, + 0 + ], + "to": [ + 180, + 310.667, + 0 + ], + "ti": [ + -112, + -490.667, + 0 + ] + }, + { + "t": 103, + "s": [ + 1088, + 1137, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.290196078431, + 0.952941236309, + 0.968627510819, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 104, + "st": 28, + "bm": 0 + }, + { + "ddd": 1, + "ind": 6, + "ty": 4, + "nm": "Shape Layer 11", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 19, + "s": [ + 1455, + -59, + 0 + ], + "to": [ + -184, + 406.667, + 0 + ], + "ti": [ + 112, + -438.667, + 0 + ] + }, + { + "t": 94, + "s": [ + 1459, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 19, + "op": 95, + "st": 19, + "bm": 0 + }, + { + "ddd": 1, + "ind": 7, + "ty": 4, + "nm": "Shape Layer 10", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 24, + "s": [ + 728, + -59, + 0 + ], + "to": [ + 388, + 434.667, + 0 + ], + "ti": [ + -584, + -762.667, + 0 + ] + }, + { + "t": 99, + "s": [ + 732, + 1413, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 24, + "op": 100, + "st": 24, + "bm": 0 + }, + { + "ddd": 1, + "ind": 8, + "ty": 4, + "nm": "Shape Layer 9", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 19, + "s": [ + 232, + -147, + 0 + ], + "to": [ + -200, + 554.667, + 0 + ], + "ti": [ + 560, + -378.667, + 0 + ] + }, + { + "t": 94, + "s": [ + 232, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -248, + -26 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.156862745098, + 0.215686289469, + 0.470588265213, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 19, + "op": 95, + "st": 19, + "bm": 0 + }, + { + "ddd": 1, + "ind": 9, + "ty": 4, + "nm": "Shape Layer 8", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 21, + "s": [ + 1664, + -23, + 0 + ], + "to": [ + 108, + 234.667, + 0 + ], + "ti": [ + -336, + -306.667, + 0 + ] + }, + { + "t": 96, + "s": [ + 1772, + 1177, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.490196108351, + 0.231372563979, + 0.752941236309, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 21, + "op": 97, + "st": 21, + "bm": 0 + }, + { + "ddd": 1, + "ind": 10, + "ty": 4, + "nm": "Shape Layer 7", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 0, + "s": [ + 1235, + -59, + 0 + ], + "to": [ + 96, + 526.667, + 0 + ], + "ti": [ + -104, + -486.667, + 0 + ] + }, + { + "t": 75, + "s": [ + 1039, + 1237, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.239215701234, + 0.419607873056, + 0.709803921569, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 76, + "st": 0, + "bm": 0 + }, + { + "ddd": 1, + "ind": 11, + "ty": 4, + "nm": "Shape Layer 6", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 6, + "s": [ + 528, + -59, + 0 + ], + "to": [ + -208, + 742.667, + 0 + ], + "ti": [ + 216, + -566.667, + 0 + ] + }, + { + "t": 81, + "s": [ + 532, + 1413, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.270588235294, + 0.909803981407, + 0.639215686275, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 6, + "op": 82, + "st": 6, + "bm": 0 + }, + { + "ddd": 1, + "ind": 12, + "ty": 4, + "nm": "Shape Layer 5", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 2, + "s": [ + 92, + -147, + 0 + ], + "to": [ + -90, + 314.667, + 0 + ], + "ti": [ + 194, + -532.667, + 0 + ] + }, + { + "t": 77, + "s": [ + 92, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -248, + -26 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 2, + "op": 78, + "st": 2, + "bm": 0 + }, + { + "ddd": 1, + "ind": 13, + "ty": 4, + "nm": "Shape Layer 4", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 16, + "s": [ + 1128, + -59, + 0 + ], + "to": [ + -208, + 358.667, + 0 + ], + "ti": [ + -112, + -490.667, + 0 + ] + }, + { + "t": 91, + "s": [ + 1088, + 1137, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.490196108351, + 0.231372563979, + 0.752941236309, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 16, + "op": 92, + "st": 16, + "bm": 0 + }, + { + "ddd": 1, + "ind": 14, + "ty": 4, + "nm": "Shape Layer 3", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 7, + "s": [ + 1455, + -59, + 0 + ], + "to": [ + -184, + 406.667, + 0 + ], + "ti": [ + 112, + -438.667, + 0 + ] + }, + { + "t": 82, + "s": [ + 1459, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 7, + "op": 83, + "st": 7, + "bm": 0 + }, + { + "ddd": 1, + "ind": 15, + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 13, + "s": [ + 728, + -59, + 0 + ], + "to": [ + 388, + 434.667, + 0 + ], + "ti": [ + -584, + -762.667, + 0 + ] + }, + { + "t": 88, + "s": [ + 732, + 1413, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -247.885, + -20.203 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 13, + "op": 89, + "st": 13, + "bm": 0 + }, + { + "ddd": 1, + "ind": 16, + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 500);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 300);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 100);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 9, + "s": [ + 232, + -147, + 0 + ], + "to": [ + 368, + 422.667, + 0 + ], + "ti": [ + 20, + -326.667, + 0 + ] + }, + { + "t": 84, + "s": [ + 232, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + -248, + -39, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -248, + -52 + ], + [ + -248, + -26 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.156862745098, + 0.215686289469, + 0.470588265213, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 10, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 9, + "op": 85, + "st": 9, + "bm": 0 + } + ] + }, + { + "id": "comp_1", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "blueCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 32, + "s": [ + 1453, + -59, + 0 + ], + "to": [ + -116, + 296, + 0 + ], + "ti": [ + 168, + -388, + 0 + ] + }, + { + "t": 93, + "s": [ + 1413, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 45.849, + 45.849, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 32, + "op": 93, + "st": 32, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "redCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 35, + "s": [ + 1132, + -305, + 0 + ], + "to": [ + 244, + 436, + 0 + ], + "ti": [ + -304, + -272, + 0 + ] + }, + { + "t": 101, + "s": [ + 1200, + 1211, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48.744, + 48.744, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 35, + "op": 101, + "st": 35, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "yellowCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 31, + "s": [ + 600, + -110, + 0 + ], + "to": [ + -200, + 428, + 0 + ], + "ti": [ + 244, + -308, + 0 + ] + }, + { + "t": 101, + "s": [ + 600, + 1210, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 68.326, + 68.326, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 31, + "op": 101, + "st": 31, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "purpleCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 28, + "s": [ + 926, + -55, + 0 + ], + "to": [ + 184, + 256, + 0 + ], + "ti": [ + -344, + -344, + 0 + ] + }, + { + "t": 78, + "s": [ + 1054, + 1253, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48, + 48, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196078431, + 0.23137254902, + 0.752941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 79, + "st": 28, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 27, + "s": [ + 1654, + -58, + 0 + ], + "to": [ + -124, + 373.333, + 0 + ], + "ti": [ + 168, + -557.333, + 0 + ] + }, + { + "t": 99, + "s": [ + 1654, + 1222, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 60, + 60, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 1, + 0.705882370472, + 0, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 27, + "op": 115, + "st": 27, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 23, + "s": [ + 1414, + -58, + 0 + ], + "to": [ + 248, + 409.333, + 0 + ], + "ti": [ + -236, + -493.333, + 0 + ] + }, + { + "t": 100, + "s": [ + 1414, + 1222, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 60, + 60, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.490196108818, + 0.231372565031, + 0.752941250801, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 23, + "op": 111, + "st": 23, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 8, + "s": [ + 1924, + 1216, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 396, + 250.667, + 0 + ] + }, + { + "t": 18, + "s": [ + 1416, + -248, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 90.321, + 90.321, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.098039224744, + 0.694117665291, + 1, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 8, + "op": 19, + "st": 8, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.772 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 26, + "s": [ + 1930, + 1130, + 0 + ], + "to": [ + -52.667, + -198, + 0 + ], + "ti": [ + 122.667, + -28.333, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.278 + }, + "t": 51, + "s": [ + 1496, + 196, + 0 + ], + "to": [ + -122.667, + 28.333, + 0 + ], + "ti": [ + 29.333, + -462.667, + 0 + ] + }, + { + "t": 90, + "s": [ + 1194, + 1300, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.111 + ] + }, + "t": 26, + "s": [ + 30, + 30, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 51, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 101, + "s": [ + 15, + 15, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.098039224744, + 0.694117665291, + 1, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 26, + "op": 101, + "st": 26, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.801 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 12, + "s": [ + 1940, + 1124, + 0 + ], + "to": [ + -108.333, + -166, + 0 + ], + "ti": [ + 149.333, + -4, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.297 + }, + "t": 37, + "s": [ + 1290, + 128, + 0 + ], + "to": [ + -149.333, + 4, + 0 + ], + "ti": [ + -48.667, + -412.667, + 0 + ] + }, + { + "t": 76, + "s": [ + 1044, + 1148, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + -10 + ] + }, + "t": 12, + "s": [ + 80, + 80, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 37, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 87, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 1, + 0.705882370472, + 0, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 12, + "op": 88, + "st": 12, + "bm": 0 + }, + { + "ddd": 0, + "ind": 10, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 2004, + 1152, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 98.667, + 749.333, + 0 + ] + }, + { + "t": 10, + "s": [ + 1060, + -120, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 80, + 80, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.098039224744, + 0.694117665291, + 1, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 0, + "op": 11, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 11, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.756 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 13, + "s": [ + 1930, + 1130, + 0 + ], + "to": [ + -52.667, + -198, + 0 + ], + "ti": [ + 140, + -6.333, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.386 + }, + "t": 38, + "s": [ + 1308, + 398, + 0 + ], + "to": [ + -140, + 6.333, + 0 + ], + "ti": [ + 13.333, + -434.667, + 0 + ] + }, + { + "t": 77, + "s": [ + 1090, + 1168, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 2.083 + ] + }, + "t": 13, + "s": [ + 50, + 50, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 38, + "s": [ + 75, + 75, + 100 + ] + }, + { + "t": 88, + "s": [ + 30, + 30, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.490196108818, + 0.231372565031, + 0.752941250801, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 13, + "op": 89, + "st": 13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 12, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.819 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 3, + "s": [ + 1924, + 1148, + 0 + ], + "to": [ + -72.667, + -550, + 0 + ], + "ti": [ + 137.333, + -0.667, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.293 + }, + "t": 28, + "s": [ + 1216, + 80, + 0 + ], + "to": [ + -137.333, + 0.667, + 0 + ], + "ti": [ + -12.667, + -380.667, + 0 + ] + }, + { + "t": 67, + "s": [ + 1100, + 1152, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.833 + ] + }, + "t": 3, + "s": [ + 60, + 60, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 28, + "s": [ + 80, + 80, + 100 + ] + }, + { + "t": 78, + "s": [ + 20, + 20, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 1, + 0.282352954149, + 0.282352954149, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 3, + "op": 79, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 13, + "ty": 4, + "nm": "redCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 2, + "s": [ + 1920, + 1124, + 0 + ], + "to": [ + -156.667, + -332.667, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 14, + "s": [ + 1652, + -32, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 2, + "op": 15, + "st": 2, + "bm": 0 + }, + { + "ddd": 0, + "ind": 14, + "ty": 4, + "nm": "blueCircle02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.783 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 6, + "s": [ + 1840, + 1196, + 0 + ], + "to": [ + -77.333, + -182, + 0 + ], + "ti": [ + 133.798, + 40.438, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.359 + }, + "t": 20, + "s": [ + 1410, + 770, + 0 + ], + "to": [ + -137.2, + -41.466, + 0 + ], + "ti": [ + 19.333, + -100.667, + 0 + ] + }, + { + "t": 41, + "s": [ + 1208, + 1168, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 6, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 20, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 41, + "s": [ + 30, + 30, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 6, + "op": 42, + "st": 6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 15, + "ty": 4, + "nm": "redCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.782 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 15, + "s": [ + 1940, + 1168, + 0 + ], + "to": [ + -23.667, + -194.667, + 0 + ], + "ti": [ + 128.339, + 4.912, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.29 + }, + "t": 36, + "s": [ + 1606, + 328, + 0 + ], + "to": [ + -69.797, + -2.671, + 0 + ], + "ti": [ + 19.333, + -210.667, + 0 + ] + }, + { + "t": 67, + "s": [ + 1410, + 1186, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.794 + ] + }, + "t": 15, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 36, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 67, + "s": [ + 20, + 20, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 15, + "op": 68, + "st": 15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 16, + "ty": 4, + "nm": "yellowCircle02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.674 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 11, + "s": [ + 1904, + 1140, + 0 + ], + "to": [ + 3.333, + -94, + 0 + ], + "ti": [ + 163, + 8, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.443 + }, + "t": 32, + "s": [ + 1660, + 608, + 0 + ], + "to": [ + -71.027, + -3.486, + 0 + ], + "ti": [ + -12.667, + -150.667, + 0 + ] + }, + { + "t": 63, + "s": [ + 1530, + 1160, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 11, + "s": [ + 30, + 30, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 32, + "s": [ + 70, + 70, + 100 + ] + }, + { + "t": 63, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196108351, + 0.231372563979, + 0.752941236309, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 64, + "st": 11, + "bm": 0 + }, + { + "ddd": 0, + "ind": 17, + "ty": 4, + "nm": "purpleCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.77 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 6, + "s": [ + 1908, + 1116, + 0 + ], + "to": [ + -9.333, + -174, + 0 + ], + "ti": [ + 154.699, + 0.945, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.302 + }, + "t": 31, + "s": [ + 1588, + 152, + 0 + ], + "to": [ + -126.052, + -0.77, + 0 + ], + "ti": [ + 3.333, + -368.667, + 0 + ] + }, + { + "t": 70, + "s": [ + 1392, + 1184, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 8.333 + ] + }, + "t": 6, + "s": [ + 50, + 50, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 31, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 81, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 20, + 20 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 6, + "op": 71, + "st": 6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 18, + "ty": 4, + "nm": "blueCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.796 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 1920, + 1160, + 0 + ], + "to": [ + 6.667, + -266, + 0 + ], + "ti": [ + 131.904, + -46.241, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.326 + }, + "t": 25, + "s": [ + 1300, + 212, + 0 + ], + "to": [ + -201.2, + 70.534, + 0 + ], + "ti": [ + -4.667, + -388.667, + 0 + ] + }, + { + "t": 64, + "s": [ + 1144, + 1156, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 0, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 25, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 64, + "s": [ + 30, + 30, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 65, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 19, + "ty": 4, + "nm": "redCircle02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.79 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 7, + "s": [ + 2072, + 1216, + 0 + ], + "to": [ + -99, + -124.667, + 0 + ], + "ti": [ + 111.667, + 12.667, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.368 + }, + "t": 28, + "s": [ + 1478, + 468, + 0 + ], + "to": [ + -111.667, + -12.667, + 0 + ], + "ti": [ + -0.667, + -122.667, + 0 + ] + }, + { + "t": 59, + "s": [ + 1402, + 1140, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.794 + ] + }, + "t": 7, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 28, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 59, + "s": [ + 20, + 20, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 7, + "op": 72, + "st": 7, + "bm": 0 + }, + { + "ddd": 0, + "ind": 20, + "ty": 4, + "nm": "yellowCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.716 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 3, + "s": [ + 1972, + 1216, + 0 + ], + "to": [ + -72, + -110.667, + 0 + ], + "ti": [ + 136.12, + -24.434, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.457 + }, + "t": 21.8, + "s": [ + 1540, + 552, + 0 + ], + "to": [ + -163.485, + 29.346, + 0 + ], + "ti": [ + -20.667, + -116.667, + 0 + ] + }, + { + "t": 50, + "s": [ + 1336, + 1140, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 3, + "s": [ + 30, + 30, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 21.8, + "s": [ + 70, + 70, + 100 + ] + }, + { + "t": 50, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 3, + "op": 51, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 21, + "ty": 4, + "nm": "purpleCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.777 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 1640, + 1140, + 0 + ], + "to": [ + -32.667, + -164, + 0 + ], + "ti": [ + 117.335, + -17.843, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.342 + }, + "t": 25, + "s": [ + 1104, + 228, + 0 + ], + "to": [ + -144.667, + 22, + 0 + ], + "ti": [ + -24.667, + -180.667, + 0 + ] + }, + { + "t": 64, + "s": [ + 1032, + 1148, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + -5.556 + ] + }, + "t": 0, + "s": [ + 80, + 80, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 25, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 75, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 15, + 15 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196078431, + 0.23137254902, + 0.752941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 65, + "st": 0, + "bm": 0 + } + ] + }, + { + "id": "comp_2", + "layers": [ + { + "ddd": 1, + "ind": 1, + "ty": 4, + "nm": "triangle", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "rx": { + "a": 0, + "k": 0, + "ix": 8, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 150);" + }, + "ry": { + "a": 0, + "k": 0, + "ix": 9, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 50);" + }, + "rz": { + "a": 0, + "k": 0, + "ix": 10, + "x": "var $bm_rt;\n$bm_rt = $bm_mul(time, 10);" + }, + "or": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 7 + }, + "p": { + "a": 0, + "k": [ + 50, + 57, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 80, + 80, + 80 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "sr", + "sy": 2, + "d": 1, + "pt": { + "a": 0, + "k": 3, + "ix": 3 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 4 + }, + "r": { + "a": 0, + "k": 0, + "ix": 5 + }, + "or": { + "a": 0, + "k": 30, + "ix": 7 + }, + "os": { + "a": 0, + "k": 0, + "ix": 9 + }, + "ix": 1, + "nm": "Polystar Path 1", + "mn": "ADBE Vector Shape - Star", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 1, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Polystar 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 375, + "st": 0, + "bm": 0 + } + ] + }, + { + "id": "comp_3", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "blueCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 40, + "s": [ + 1773, + -59, + 0 + ], + "to": [ + -116, + 296, + 0 + ], + "ti": [ + 168, + -388, + 0 + ] + }, + { + "t": 101, + "s": [ + 1733, + 1213, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 45.849, + 45.849, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 40, + "op": 101, + "st": 40, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "redCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 35, + "s": [ + 1452, + -305, + 0 + ], + "to": [ + 244, + 436, + 0 + ], + "ti": [ + -304, + -272, + 0 + ] + }, + { + "t": 101, + "s": [ + 1520, + 1211, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48.744, + 48.744, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 35, + "op": 101, + "st": 35, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "yellowCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 31, + "s": [ + 920, + -110, + 0 + ], + "to": [ + -200, + 428, + 0 + ], + "ti": [ + 244, + -308, + 0 + ] + }, + { + "t": 101, + "s": [ + 920, + 1210, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 68.326, + 68.326, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 31, + "op": 101, + "st": 31, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "purpleCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 26, + "s": [ + 1246, + -55, + 0 + ], + "to": [ + 184, + 256, + 0 + ], + "ti": [ + -344, + -344, + 0 + ] + }, + { + "t": 76, + "s": [ + 1374, + 1253, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48, + 48, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196078431, + 0.23137254902, + 0.752941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 26, + "op": 77, + "st": 26, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 27, + "s": [ + 1654, + -58, + 0 + ], + "to": [ + -124, + 373.333, + 0 + ], + "ti": [ + 168, + -557.333, + 0 + ] + }, + { + "t": 99, + "s": [ + 1654, + 1222, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 60, + 60, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 1, + 0.705882370472, + 0, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 27, + "op": 115, + "st": 27, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 23, + "s": [ + 1414, + -58, + 0 + ], + "to": [ + 248, + 409.333, + 0 + ], + "ti": [ + -236, + -493.333, + 0 + ] + }, + { + "t": 100, + "s": [ + 1414, + 1222, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 60, + 60, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.490196108818, + 0.231372565031, + 0.752941250801, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 23, + "op": 111, + "st": 23, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 13, + "s": [ + 1988, + 1208, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 95.333, + 242.667, + 0 + ] + }, + { + "t": 23, + "s": [ + 1416, + -248, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + -10 + ] + }, + "t": 13, + "s": [ + 80, + 80, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 38, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 88, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.098039224744, + 0.694117665291, + 1, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 13, + "op": 101, + "st": 13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.81 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 26, + "s": [ + 1930, + 1130, + 0 + ], + "to": [ + -52.667, + -198, + 0 + ], + "ti": [ + 171.333, + -3, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.262 + }, + "t": 51, + "s": [ + 1360, + 32, + 0 + ], + "to": [ + -171.333, + 3, + 0 + ], + "ti": [ + 61.333, + -430.667, + 0 + ] + }, + { + "t": 90, + "s": [ + 902, + 1148, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.111 + ] + }, + "t": 26, + "s": [ + 30, + 30, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 51, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 101, + "s": [ + 15, + 15, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.098039224744, + 0.694117665291, + 1, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 26, + "op": 101, + "st": 26, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.801 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 16, + "s": [ + 1940, + 1124, + 0 + ], + "to": [ + -108.333, + -166, + 0 + ], + "ti": [ + 149.333, + -4, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.297 + }, + "t": 41, + "s": [ + 1290, + 128, + 0 + ], + "to": [ + -149.333, + 4, + 0 + ], + "ti": [ + -48.667, + -412.667, + 0 + ] + }, + { + "t": 80, + "s": [ + 1044, + 1148, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + -10 + ] + }, + "t": 16, + "s": [ + 80, + 80, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 41, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 91, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 1, + 0.705882370472, + 0, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 16, + "op": 101, + "st": 16, + "bm": 0 + }, + { + "ddd": 0, + "ind": 10, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 1988, + 1208, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 154.667, + 221.333, + 0 + ] + }, + { + "t": 10, + "s": [ + 1060, + -120, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + -10 + ] + }, + "t": 0, + "s": [ + 80, + 80, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 25, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 75, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.098039224744, + 0.694117665291, + 1, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 0, + "op": 101, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 11, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.754 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 13, + "s": [ + 1930, + 1130, + 0 + ], + "to": [ + -52.667, + -198, + 0 + ], + "ti": [ + 171.333, + -3, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.328 + }, + "t": 38, + "s": [ + 1396, + 346, + 0 + ], + "to": [ + -171.333, + 3, + 0 + ], + "ti": [ + 61.333, + -430.667, + 0 + ] + }, + { + "t": 77, + "s": [ + 902, + 1148, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 2.083 + ] + }, + "t": 13, + "s": [ + 50, + 50, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 38, + "s": [ + 75, + 75, + 100 + ] + }, + { + "t": 88, + "s": [ + 30, + 30, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 0.490196108818, + 0.231372565031, + 0.752941250801, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 13, + "op": 101, + "st": 13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 12, + "ty": 0, + "nm": "triangleComp01", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.816 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 3, + "s": [ + 1940, + 1124, + 0 + ], + "to": [ + -120.667, + -178, + 0 + ], + "ti": [ + 149.333, + -4, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.284 + }, + "t": 28, + "s": [ + 1216, + 56, + 0 + ], + "to": [ + -149.333, + 4, + 0 + ], + "ti": [ + -48.667, + -412.667, + 0 + ] + }, + { + "t": 67, + "s": [ + 1044, + 1148, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 50, + 50, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.833 + ] + }, + "t": 3, + "s": [ + 60, + 60, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 28, + "s": [ + 80, + 80, + 100 + ] + }, + { + "t": 78, + "s": [ + 20, + 20, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "ef": [ + { + "ty": 21, + "nm": "Fill", + "np": 9, + "mn": "ADBE Fill", + "ix": 1, + "en": 1, + "ef": [ + { + "ty": 10, + "nm": "Fill Mask", + "mn": "ADBE Fill-0001", + "ix": 1, + "v": { + "a": 0, + "k": 0, + "ix": 1 + } + }, + { + "ty": 7, + "nm": "All Masks", + "mn": "ADBE Fill-0007", + "ix": 2, + "v": { + "a": 0, + "k": 0, + "ix": 2 + } + }, + { + "ty": 2, + "nm": "Color", + "mn": "ADBE Fill-0002", + "ix": 3, + "v": { + "a": 0, + "k": [ + 1, + 0.282352954149, + 0.282352954149, + 1 + ], + "ix": 3 + } + }, + { + "ty": 7, + "nm": "Invert", + "mn": "ADBE Fill-0006", + "ix": 4, + "v": { + "a": 0, + "k": 0, + "ix": 4 + } + }, + { + "ty": 0, + "nm": "Horizontal Feather", + "mn": "ADBE Fill-0003", + "ix": 5, + "v": { + "a": 0, + "k": 0, + "ix": 5 + } + }, + { + "ty": 0, + "nm": "Vertical Feather", + "mn": "ADBE Fill-0004", + "ix": 6, + "v": { + "a": 0, + "k": 0, + "ix": 6 + } + }, + { + "ty": 0, + "nm": "Opacity", + "mn": "ADBE Fill-0005", + "ix": 7, + "v": { + "a": 0, + "k": 1, + "ix": 7 + } + } + ] + } + ], + "w": 100, + "h": 100, + "ip": 3, + "op": 79, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 13, + "ty": 4, + "nm": "redCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 1, + "s": [ + 1920, + 1124, + 0 + ], + "to": [ + -121.333, + -216.667, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 13, + "s": [ + 1192, + -176, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 1, + "op": 14, + "st": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 14, + "ty": 4, + "nm": "blueCircle02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.779 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 6, + "s": [ + 1920, + 1160, + 0 + ], + "to": [ + 6.667, + -266, + 0 + ], + "ti": [ + 131.904, + -46.241, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.326 + }, + "t": 31, + "s": [ + 1434, + 234, + 0 + ], + "to": [ + -201.2, + 70.534, + 0 + ], + "ti": [ + 39.333, + -396.667, + 0 + ] + }, + { + "t": 70, + "s": [ + 1208, + 1168, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 6, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 31, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 70, + "s": [ + 30, + 30, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 6, + "op": 71, + "st": 6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 15, + "ty": 4, + "nm": "redCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.761 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 15, + "s": [ + 1992, + 1216, + 0 + ], + "to": [ + -89, + -107.333, + 0 + ], + "ti": [ + 106.333, + 12.333, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.427 + }, + "t": 36, + "s": [ + 1458, + 572, + 0 + ], + "to": [ + -106.333, + -12.333, + 0 + ], + "ti": [ + -0.667, + -122.667, + 0 + ] + }, + { + "t": 67, + "s": [ + 1354, + 1142, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.794 + ] + }, + "t": 15, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 36, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 67, + "s": [ + 20, + 20, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 15, + "op": 80, + "st": 15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 16, + "ty": 4, + "nm": "yellowCircle02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.711 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 11, + "s": [ + 1820, + 1152, + 0 + ], + "to": [ + -69.333, + -91.333, + 0 + ], + "ti": [ + 97, + -0.667, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.436 + }, + "t": 32, + "s": [ + 1404, + 604, + 0 + ], + "to": [ + -97, + 0.667, + 0 + ], + "ti": [ + 27.333, + -98.667, + 0 + ] + }, + { + "t": 63, + "s": [ + 1238, + 1156, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 11, + "s": [ + 30, + 30, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 32, + "s": [ + 70, + 70, + 100 + ] + }, + { + "t": 63, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196108351, + 0.231372563979, + 0.752941236309, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 76, + "st": 11, + "bm": 0 + }, + { + "ddd": 0, + "ind": 17, + "ty": 4, + "nm": "purpleCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.824 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 6, + "s": [ + 1944, + 1104, + 0 + ], + "to": [ + -154, + -164.667, + 0 + ], + "ti": [ + 169.333, + -8, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.302 + }, + "t": 31, + "s": [ + 1020, + 116, + 0 + ], + "to": [ + -169.333, + 8, + 0 + ], + "ti": [ + 175.333, + -340.667, + 0 + ] + }, + { + "t": 70, + "s": [ + 928, + 1152, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 8.333 + ] + }, + "t": 6, + "s": [ + 50, + 50, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 31, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 81, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 20, + 20 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 6, + "op": 71, + "st": 6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 18, + "ty": 4, + "nm": "blueCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.796 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 1920, + 1160, + 0 + ], + "to": [ + 6.667, + -266, + 0 + ], + "ti": [ + 131.904, + -46.241, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.326 + }, + "t": 25, + "s": [ + 1300, + 212, + 0 + ], + "to": [ + -201.2, + 70.534, + 0 + ], + "ti": [ + -4.667, + -388.667, + 0 + ] + }, + { + "t": 64, + "s": [ + 1144, + 1156, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 0, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 25, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 64, + "s": [ + 30, + 30, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 65, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 19, + "ty": 4, + "nm": "redCircle02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.79 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 9, + "s": [ + 1992, + 1216, + 0 + ], + "to": [ + -99, + -124.667, + 0 + ], + "ti": [ + 111.667, + 12.667, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.368 + }, + "t": 30, + "s": [ + 1398, + 468, + 0 + ], + "to": [ + -111.667, + -12.667, + 0 + ], + "ti": [ + -0.667, + -122.667, + 0 + ] + }, + { + "t": 61, + "s": [ + 1322, + 1140, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.794 + ] + }, + "t": 9, + "s": [ + 20, + 20, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 30, + "s": [ + 50, + 50, + 100 + ] + }, + { + "t": 61, + "s": [ + 20, + 20, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 9, + "op": 74, + "st": 9, + "bm": 0 + }, + { + "ddd": 0, + "ind": 20, + "ty": 4, + "nm": "yellowCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.748 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 5, + "s": [ + 1732, + 1216, + 0 + ], + "to": [ + -72, + -110.667, + 0 + ], + "ti": [ + 106, + 12.667, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.398 + }, + "t": 26, + "s": [ + 1300, + 552, + 0 + ], + "to": [ + -106, + -12.667, + 0 + ], + "ti": [ + -20.667, + -116.667, + 0 + ] + }, + { + "t": 57, + "s": [ + 1096, + 1140, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 1.667 + ] + }, + "t": 5, + "s": [ + 30, + 30, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 26, + "s": [ + 70, + 70, + 100 + ] + }, + { + "t": 57, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 5, + "op": 70, + "st": 5, + "bm": 0 + }, + { + "ddd": 0, + "ind": 21, + "ty": 4, + "nm": "purpleCircle01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.17, + "y": 0.824 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 1944, + 1104, + 0 + ], + "to": [ + -154, + -164.667, + 0 + ], + "ti": [ + 169.333, + -8, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.72, + "y": 0.302 + }, + "t": 25, + "s": [ + 1020, + 116, + 0 + ], + "to": [ + -169.333, + 8, + 0 + ], + "ti": [ + 175.333, + -340.667, + 0 + ] + }, + { + "t": 64, + "s": [ + 928, + 1152, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667, + 0.667 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 8.333 + ] + }, + "t": 0, + "s": [ + 50, + 50, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 1, + 1, + 1 + ] + }, + "o": { + "x": [ + 0.695, + 0.695, + 0.333 + ], + "y": [ + 0, + 0, + 0 + ] + }, + "t": 25, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 75, + "s": [ + 50, + 50, + 100 + ] + } + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 15, + 15 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196078431, + 0.23137254902, + 0.752941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 65, + "st": 0, + "bm": 0 + } + ] + }, + { + "id": "comp_4", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "ribbonRed02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 8.9, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 655.204, + 920.201, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 787.204, + 316.201, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 65.331, + 101.626 + ], + [ + 52.275, + 60.987 + ], + [ + 18, + -36 + ], + [ + -30, + -2 + ], + [ + 100.811, + 137.103 + ], + [ + -12, + 136 + ], + [ + -54, + 80 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -54, + -84 + ], + [ + -24, + -28 + ], + [ + -26.593, + 53.186 + ], + [ + 147.726, + 9.848 + ], + [ + -100, + -136 + ], + [ + 10.968, + -124.305 + ], + [ + 51.52, + -76.326 + ] + ], + "v": [ + [ + 736, + 520 + ], + [ + 810, + 346 + ], + [ + 830, + 138 + ], + [ + 716, + 140 + ], + [ + 780, + 226 + ], + [ + 836, + -92 + ], + [ + 656, + -334 + ], + [ + 887.095, + -676.42 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.156862745098, + 0.215686289469, + 0.470588265213, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 15, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 11, + "s": [ + 0 + ] + }, + { + "t": 44, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 4, + "s": [ + 0 + ] + }, + { + "t": 37, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 4, + "op": 45, + "st": 4, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "ribbonPurple02", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + -7.674, + 145.812 + ], + [ + -56.367, + 71.74 + ], + [ + -43.907, + 167.129 + ], + [ + 8, + 194 + ], + [ + -62, + 210 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 8, + -151.999 + ], + [ + 110, + -140.001 + ], + [ + 62, + -236 + ], + [ + -13.292, + -322.34 + ], + [ + 26.075, + -88.318 + ] + ], + "v": [ + [ + 524, + 624 + ], + [ + 370, + 412 + ], + [ + 544, + 236.001 + ], + [ + 462, + -40 + ], + [ + 48, + -256 + ], + [ + -174, + -752 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.270588235294, + 0.909803981407, + 0.639215686275, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 3.615 + ] + }, + "t": 10, + "s": [ + 20 + ] + }, + { + "t": 39, + "s": [ + 15 + ] + } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 10, + "s": [ + 0 + ] + }, + { + "t": 43, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 3, + "s": [ + 0 + ] + }, + { + "t": 36, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 3, + "op": 44, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "ribbonRed01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 65.331, + 101.626 + ], + [ + -26, + 76 + ], + [ + 32, + -58 + ], + [ + -148, + 4 + ], + [ + 100.811, + 137.103 + ], + [ + 16, + 156 + ], + [ + -92, + 4 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -54, + -84 + ], + [ + 36.079, + -105.462 + ], + [ + -42.346, + 76.752 + ], + [ + 148, + -4 + ], + [ + -100, + -136 + ], + [ + -16, + -156 + ], + [ + 92, + -4 + ] + ], + "v": [ + [ + 1006, + 496 + ], + [ + 688, + 340 + ], + [ + 704, + 96 + ], + [ + 568, + 46 + ], + [ + 738, + 200 + ], + [ + 832, + -158 + ], + [ + 600, + -414 + ], + [ + 832, + -664 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 0.282352911257, + 0.282352911257, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 15, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 9, + "s": [ + 0 + ] + }, + { + "t": 42, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 2, + "s": [ + 0 + ] + }, + { + "t": 35, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 2, + "op": 43, + "st": 2, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "ribbonBlue01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + -140, + 200 + ], + [ + -170.503, + 113.669 + ], + [ + -133.881, + 0 + ], + [ + 41.726, + 121.7 + ], + [ + -224, + 52 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 140, + -200 + ], + [ + 138, + -92 + ], + [ + 178, + 0 + ], + [ + -24, + -70 + ], + [ + 224, + -52 + ] + ], + "v": [ + [ + -1072, + 420 + ], + [ + -688, + 316 + ], + [ + -484, + -136 + ], + [ + 18, + -62 + ], + [ + 178, + -370 + ], + [ + 308, + -692 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.239215701234, + 0.823529471603, + 0.976470648074, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 2 + ] + }, + "t": 12, + "s": [ + 25 + ] + }, + { + "t": 28, + "s": [ + 15 + ] + } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 8, + "s": [ + 0 + ] + }, + { + "t": 41, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 1, + "s": [ + 0 + ] + }, + { + "t": 34, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 1, + "op": 42, + "st": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "ribbonPurple01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + -92, + 162 + ], + [ + -114.383, + 228.764 + ], + [ + -106, + -10 + ], + [ + 56.871, + 70.411 + ], + [ + -98, + 116 + ], + [ + -28.331, + 59.644 + ], + [ + -90, + 44 + ], + [ + 182, + 50 + ] + ], + "o": [ + [ + 112.375, + -197.878 + ], + [ + 56, + -111.999 + ], + [ + 155.323, + 14.654 + ], + [ + -42, + -52 + ], + [ + 79.909, + -94.586 + ], + [ + 38, + -80 + ], + [ + 92.11, + -45.031 + ], + [ + -107.186, + -29.447 + ] + ], + "v": [ + [ + -596, + 612 + ], + [ + -704, + 247.999 + ], + [ + -446, + 120 + ], + [ + -298, + -16 + ], + [ + -430, + -158 + ], + [ + -482, + -330 + ], + [ + -246, + -366 + ], + [ + -308, + -652 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.488552078546, + 0.230311404957, + 0.752941176471, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.17 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 8, + "s": [ + 35 + ] + }, + { + "t": 38, + "s": [ + 10 + ] + } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 8, + "s": [ + 0 + ] + }, + { + "t": 41, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 5, + "s": [ + 0 + ] + }, + { + "t": 38, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 0, + "op": 42, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "ribbonYellow01", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + -15.347, + 130.161 + ], + [ + -186.562, + 101.072 + ], + [ + 112.01, + 0.14 + ], + [ + -68.075, + 42.572 + ], + [ + -51.603, + 62.412 + ], + [ + 43.211, + 79.208 + ], + [ + 219.412, + -10.282 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 15.347, + -130.162 + ], + [ + 180.162, + -97.604 + ], + [ + -112.01, + -0.142 + ], + [ + 88.184, + -55.147 + ], + [ + 57.188, + -69.166 + ], + [ + -43.211, + -79.208 + ], + [ + -190.794, + 8.941 + ] + ], + "v": [ + [ + -996.479, + 553.827 + ], + [ + -814.281, + 359.159 + ], + [ + -768.857, + 64.753 + ], + [ + -717.951, + 160.649 + ], + [ + -716.405, + -79.549 + ], + [ + -503.734, + -159.43 + ], + [ + -491.038, + -358.543 + ], + [ + -963.412, + -597.718 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 0.706519751455, + 0, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.17 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 0, + "s": [ + 25 + ] + }, + { + "t": 33, + "s": [ + 10 + ] + } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Shape 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 7, + "s": [ + 0 + ] + }, + { + "t": 40, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.18 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "t": 33, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 0, + "op": 41, + "st": 0, + "bm": 0 + } + ] + } + ], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "frills", + "refId": "comp_0", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 1920, + "h": 1080, + "ip": 16, + "op": 166, + "st": 16, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "blueCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 32, + "s": [ + 1186.352, + -230.499, + 0 + ], + "to": [ + -144, + 723.333, + 0 + ], + "ti": [ + 272, + -583.333, + 0 + ] + }, + { + "t": 83, + "s": [ + 1186.352, + 1205.501, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 45.849, + 45.849, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 32, + "op": 83, + "st": 32, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "redCircle05", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 19, + "s": [ + 794.727, + -24.984, + 0 + ], + "to": [ + 224, + 403.333, + 0 + ], + "ti": [ + -284, + -435.333, + 0 + ] + }, + { + "t": 76, + "s": [ + 794.727, + 1411.016, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48.744, + 48.744, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 19, + "op": 77, + "st": 19, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "yellowCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 26, + "s": [ + 108.041, + -39.458, + 0 + ], + "to": [ + -76, + 411.333, + 0 + ], + "ti": [ + 184, + -591.333, + 0 + ] + }, + { + "t": 71, + "s": [ + 108.041, + 1396.542, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 68.326, + 68.326, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 26, + "op": 73, + "st": 26, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "purpleCircle05", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 32, + "s": [ + 448.767, + -228.23, + 0 + ], + "to": [ + 236, + 683.333, + 0 + ], + "ti": [ + -160, + -439.333, + 0 + ] + }, + { + "t": 89, + "s": [ + 448.767, + 1207.77, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48, + 48, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196078431, + 0.23137254902, + 0.752941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 32, + "op": 90, + "st": 32, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "blueCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 39, + "s": [ + 1430.352, + -242.499, + 0 + ], + "to": [ + -144, + 723.333, + 0 + ], + "ti": [ + 272, + -583.333, + 0 + ] + }, + { + "t": 88, + "s": [ + 1430.352, + 1193.501, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 45.849, + 45.849, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.098039223166, + 0.694117647059, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 90, + "st": 39, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "redCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 16, + "s": [ + 1038.727, + -36.984, + 0 + ], + "to": [ + 224, + 403.333, + 0 + ], + "ti": [ + -284, + -435.333, + 0 + ] + }, + { + "t": 73, + "s": [ + 1038.727, + 1399.016, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48.744, + 48.744, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.282352941176, + 0.282352941176, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 16, + "op": 74, + "st": 16, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 4, + "nm": "yellowCircle03", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 30, + "s": [ + 352.041, + -51.458, + 0 + ], + "to": [ + -76, + 411.333, + 0 + ], + "ti": [ + 184, + -591.333, + 0 + ] + }, + { + "t": 87, + "s": [ + 352.041, + 1384.542, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 68.326, + 68.326, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.705882352941, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 30, + "op": 88, + "st": 30, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 4, + "nm": "purpleCircle04", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 23, + "s": [ + 692.767, + -240.23, + 0 + ], + "to": [ + 236, + 683.333, + 0 + ], + "ti": [ + -160, + -439.333, + 0 + ] + }, + { + "t": 80, + "s": [ + 692.767, + 1195.77, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 48, + 48, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 30, + 30 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.490196078431, + 0.23137254902, + 0.752941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + -0.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 23, + "op": 81, + "st": 23, + "bm": 0 + }, + { + "ddd": 0, + "ind": 10, + "ty": 0, + "nm": "Particles02", + "refId": "comp_1", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + -100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 1920, + "h": 1080, + "ip": 0, + "op": 101, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 11, + "ty": 0, + "nm": "Particles02", + "refId": "comp_1", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 1920, + "h": 1080, + "ip": 3, + "op": 104, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 12, + "ty": 0, + "nm": "Particles01", + "refId": "comp_3", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + -100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 1920, + "h": 1080, + "ip": 7, + "op": 108, + "st": 7, + "bm": 0 + }, + { + "ddd": 0, + "ind": 13, + "ty": 0, + "nm": "Particles01", + "refId": "comp_3", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 1920, + "h": 1080, + "ip": 14, + "op": 115, + "st": 14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 14, + "ty": 0, + "nm": "ribbons", + "refId": "comp_4", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 960, + 540, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 1920, + "h": 1080, + "ip": 0, + "op": 44, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index b640b9e..a4ec50a 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #74980F + #74970F \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e538404..dde10a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ material3 = "1.4.0" uiUnit = "1.10.2" [libraries] +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }