diff --git a/README.md b/README.md index 9301f3b..e4572af 100644 --- a/README.md +++ b/README.md @@ -1 +1,246 @@ -# Frontend \ No newline at end of file +# Tiggle Android App + +Tiggle은 AR 기술을 활용한 기부 플랫폼 Android 애플리케이션입니다. + +## 📱 프로젝트 개요 + +- **앱 이름**: Tiggle +- **패키지명**: `com.ssafy.tiggle` +- **최소 SDK**: API 24 (Android 7.0) +- **타겟 SDK**: API 36 (Android 14) +- **아키텍처**: MVVM + Clean Architecture +- **UI 프레임워크**: Jetpack Compose + +## 🛠️ 개발환경 요구사항 + +### 필수 소프트웨어 + +- **Android Studio**: Hedgehog | 2023.1.1 이상 +- **JDK**: 11 이상 +- **Gradle**: 8.13 +- **Kotlin**: 2.0.21 +- **Android Gradle Plugin**: 8.11.1 + +### 시스템 요구사항 + +- **운영체제**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+ +- **RAM**: 최소 8GB (16GB 권장) +- **저장공간**: 최소 10GB 여유공간 +- **Android SDK**: API 24-36 설치 + +## 🚀 프로젝트 설정 및 실행 + +### 1. 저장소 클론 + +```bash +git clone +cd tiggle-fe +``` + +### 2. Android Studio에서 프로젝트 열기 + +1. Android Studio 실행 +2. `File` → `Open` 선택 +3. 프로젝트 루트 폴더(`tiggle-fe`) 선택 +4. 프로젝트 로딩 완료까지 대기 + +### 3. Gradle 동기화 + +프로젝트를 열면 자동으로 Gradle 동기화가 시작됩니다. 수동으로 동기화하려면: + +- **Windows/Linux**: `Ctrl + Shift + O` +- **macOS**: `Cmd + Shift + O` + +또는 툴바의 `Sync Project with Gradle Files` 버튼 클릭 + +### 4. Firebase 설정 + +프로젝트에는 이미 `google-services.json` 파일이 포함되어 있습니다. Firebase 프로젝트 설정이 필요한 경우: + +1. [Firebase Console](https://console.firebase.google.com/) 접속 +2. 프로젝트 생성 또는 기존 프로젝트 선택 +3. Android 앱 추가 +4. `google-services.json` 파일 다운로드 +5. `app/` 폴더에 파일 복사 + +### 5. 앱 실행 + +#### 에뮬레이터에서 실행 + +1. **AVD Manager** 열기: `Tools` → `AVD Manager` +2. **Create Virtual Device** 클릭 +3. 디바이스 선택 (예: Pixel 7) +4. 시스템 이미지 선택 (API 30 이상 권장) +5. AVD 생성 완료 +6. **Run** 버튼 클릭 또는 `Shift + F10` + +#### 실제 디바이스에서 실행 + +1. Android 디바이스에서 **개발자 옵션** 활성화 +2. **USB 디버깅** 활성화 +3. USB 케이블로 컴퓨터 연결 +4. 디바이스 인증 확인 +5. **Run** 버튼 클릭 + +## 📦 빌드 + +### Debug 빌드 + +```bash +# 터미널에서 실행 +./gradlew assembleDebug + +# 또는 Android Studio에서 +Build → Build Bundle(s) / APK(s) → Build APK(s) +``` + +### Release 빌드 + +```bash +# 터미널에서 실행 +./gradlew assembleRelease + +# 또는 Android Studio에서 +Build → Generate Signed Bundle / APK +``` + +### APK 위치 + +빌드된 APK는 다음 경로에서 확인할 수 있습니다: +``` +app/build/outputs/apk/debug/app-debug.apk +app/build/outputs/apk/release/app-release.apk +``` + +## 🏗️ 프로젝트 구조 + +``` +app/src/main/java/com/ssafy/tiggle/ +├── 📱 presentation/ # UI 레이어 +│ ├── ui/ # Compose UI 화면들 +│ │ ├── donation/ # 기부 관련 화면 +│ │ ├── user/ # 사용자 관련 화면 +│ │ └── ... +│ └── navigation/ # 네비게이션 +├── 🏢 domain/ # 비즈니스 로직 레이어 +│ ├── entity/ # 도메인 엔티티 +│ ├── repository/ # Repository 인터페이스 +│ └── usecase/ # UseCase 클래스들 +├── 💾 data/ # 데이터 레이어 +│ ├── model/ # DTO 클래스들 +│ ├── datasource/ # 데이터 소스 +│ └── repository/ # Repository 구현체 +├── 🔧 di/ # 의존성 주입 +├── 🔧 core/ # 공통 유틸리티 +└── TiggleApplication.kt # Application 클래스 +``` + +## 🛠️ 주요 기술 스택 + +### UI & 아키텍처 +- **Jetpack Compose**: 모던 Android UI 개발 +- **MVVM**: 아키텍처 패턴 +- **Clean Architecture**: 레이어 분리 +- **Navigation Compose**: 화면 네비게이션 + +### 의존성 주입 & 비동기 +- **Hilt**: 의존성 주입 +- **Kotlin Coroutines**: 비동기 프로그래밍 +- **Flow**: 반응형 스트림 + +### 네트워킹 +- **Retrofit**: HTTP 클라이언트 +- **OkHttp**: 네트워크 라이브러리 +- **Gson**: JSON 직렬화 + +### 미디어 & 3D +- **ExoPlayer**: 비디오 재생 +- **Filament**: 3D 렌더링 엔진 +- **Lottie**: 애니메이션 + +### 기타 +- **Firebase**: 푸시 알림, 분석 +- **Room**: 로컬 데이터베이스 + +## 🔧 개발 도구 + +### 코드 스타일 +- **Kotlin**: 공식 코딩 컨벤션 준수 +- **ktlint**: 코드 포맷팅 +- **Android Studio**: 기본 린터 사용 + +### 버전 관리 +- **Git**: 소스 코드 버전 관리 +- **GitHub**: 원격 저장소 + +## 🐛 문제 해결 + +### 일반적인 문제들 + +#### 1. Gradle 동기화 실패 +```bash +# Gradle 캐시 정리 +./gradlew clean +./gradlew --refresh-dependencies +``` + +#### 2. 빌드 오류 +```bash +# 프로젝트 클린 빌드 +./gradlew clean build +``` + +#### 3. 메모리 부족 오류 +`gradle.properties`에서 JVM 힙 크기 증가: +```properties +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 +``` + +#### 4. Kotlin 버전 충돌 +`gradle/libs.versions.toml`에서 Kotlin 버전 확인: +```toml +kotlin = "2.0.21" +``` + +### 로그 확인 + +```bash +# 실시간 로그 확인 +adb logcat | grep "com.ssafy.tiggle" +``` + +## 📱 앱 기능 + +### 주요 기능 +- **AR 기부**: AR 기술을 활용한 기부 경험 +- **사용자 관리**: 회원가입, 로그인, 프로필 관리 +- **기부 관리**: 기부 내역 조회, 기부 상태 추적 +- **푸시 알림**: Firebase Cloud Messaging +- **3D 모델 뷰어**: Filament 엔진을 활용한 3D 렌더링 + +### 권한 요청 +- **인터넷**: 네트워크 통신 +- **네트워크 상태**: 연결 상태 확인 +- **알림**: 푸시 알림 수신 +- **AR 카메라**: AR 기능 사용 (선택적) + +## 🤝 기여하기 + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## 📄 라이선스 + +이 프로젝트는 SSAFY 프로젝트입니다. + +## 📞 문의 + +프로젝트 관련 문의사항이 있으시면 팀원에게 연락해주세요. + +--- + +**개발팀**: SSAFY Tiggle Team +**최종 업데이트**: 2024년 8월 \ No newline at end of file diff --git a/app/src/main/assets/videos/README.md b/app/src/main/assets/videos/README.md new file mode 100644 index 0000000..cfee4fc --- /dev/null +++ b/app/src/main/assets/videos/README.md @@ -0,0 +1,30 @@ +# 로컬 비디오 파일 사용 가이드 + +이 폴더에 숏폼 영상 파일들을 넣어주세요. + +## 지원하는 파일 형식 +- MP4 (권장) +- WebM +- 3GP + +## 파일명 규칙 +- sample1.mp4 +- sample2.mp4 +- sample3.mp4 +- sample4.mp4 +- sample5.mp4 + +## 파일 크기 권장사항 +- 각 파일: 10MB 이하 +- 해상도: 720p (1280x720) 또는 1080p (1920x1080) +- 길이: 15초 ~ 60초 + +## 샘플 비디오 다운로드 +테스트용으로 다음 URL에서 샘플 비디오를 다운로드할 수 있습니다: +- https://sample-videos.com/ +- https://www.learningcontainer.com/sample-video-files-for-testing/ + +## 주의사항 +- 파일명에 공백이나 특수문자를 사용하지 마세요 +- 한글 파일명은 피해주세요 +- 파일 크기가 너무 크면 앱 성능에 영향을 줄 수 있습니다 diff --git a/app/src/main/assets/videos/video1.mp4 b/app/src/main/assets/videos/video1.mp4 new file mode 100644 index 0000000..1608854 Binary files /dev/null and b/app/src/main/assets/videos/video1.mp4 differ diff --git a/app/src/main/assets/videos/video2.mp4 b/app/src/main/assets/videos/video2.mp4 new file mode 100644 index 0000000..86b24f1 Binary files /dev/null and b/app/src/main/assets/videos/video2.mp4 differ diff --git a/app/src/main/assets/videos/video3.mp4 b/app/src/main/assets/videos/video3.mp4 new file mode 100644 index 0000000..ba8d5d9 Binary files /dev/null and b/app/src/main/assets/videos/video3.mp4 differ diff --git a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/GrowthApiService.kt b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/GrowthApiService.kt index d09e341..ef6b809 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/GrowthApiService.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/GrowthApiService.kt @@ -1,12 +1,15 @@ package com.ssafy.tiggle.data.datasource.remote import com.ssafy.tiggle.data.model.BaseResponse -import com.ssafy.tiggle.data.model.dutchpay.response.UserSummaryDto +import com.ssafy.tiggle.data.model.growth.ClickHeartResponseDto import com.ssafy.tiggle.data.model.growth.GrowthResponseDto -import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.POST interface GrowthApiService { - @GET("donation/growth") + @GET("/api/donation/growth") suspend fun getGrowthResult(): BaseResponse + + @POST("/api/donation/heart") + suspend fun clickHeart(): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/growth/ClickHeartResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/growth/ClickHeartResponseDto.kt new file mode 100644 index 0000000..aa31963 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/growth/ClickHeartResponseDto.kt @@ -0,0 +1,18 @@ +package com.ssafy.tiggle.data.model.growth + +import com.ssafy.tiggle.domain.entity.growth.HeartResult + +data class ClickHeartResponseDto( + val experiencePoints: Int = 0, + val toNextLevel: Int = 0, + val level: Int = 0, + val heart: Int = 0 +) + +fun ClickHeartResponseDto.toDomain(): HeartResult = + HeartResult( + experiencePoints = experiencePoints, + toNextLevel = toNextLevel, + level = level, + heart = heart + ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt index cb1741d..0989fa0 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt @@ -2,15 +2,19 @@ package com.ssafy.tiggle.data.model.growth import com.ssafy.tiggle.domain.entity.growth.GrowthResult -data class GrowthResponseDto ( - val totalAmount:Long=0L, - val toNextLevel:Int=0, - val level:Int=0 +data class GrowthResponseDto( + val totalAmount: Long = 0L, + val experiencePoints: Int = 0, + val toNextLevel: Int = 0, + val level: Int = 0, + val heart: Int = 0, ) fun GrowthResponseDto.toDomain(): GrowthResult = GrowthResult( - totalAmount=totalAmount, - toNextLevel=toNextLevel, - level=level + totalAmount = totalAmount, + experiencePoints = experiencePoints, + toNextLevel = toNextLevel, + level = level, + heart = heart ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt b/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt index 242fe4d..7fb82b3 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt @@ -1,12 +1,9 @@ package com.ssafy.tiggle.data.repository import com.ssafy.tiggle.data.datasource.remote.GrowthApiService -import com.ssafy.tiggle.data.datasource.remote.UserApiService import com.ssafy.tiggle.data.model.growth.toDomain -import com.ssafy.tiggle.data.model.piggybank.response.toDomain -import com.ssafy.tiggle.domain.repository.UserRepository -import com.ssafy.tiggle.domain.entity.dutchpay.UserSummary import com.ssafy.tiggle.domain.entity.growth.GrowthResult +import com.ssafy.tiggle.domain.entity.growth.HeartResult import com.ssafy.tiggle.domain.repository.GrowthRepository import javax.inject.Inject import javax.inject.Singleton @@ -30,4 +27,18 @@ class GrowthRepositoryImpl @Inject constructor( } } + override suspend fun clickHeart(): Result { + return try { + val response = growthApiService.clickHeart() + + if (response.result && response.data != null) { + Result.success(response.data.toDomain()) + } else { + Result.failure(Exception(response.message ?: "하트 클릭에 실패했습니다.")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt index a35b41f..ca7cd58 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt @@ -1,7 +1,9 @@ package com.ssafy.tiggle.domain.entity.growth -data class GrowthResult ( - val totalAmount:Long=0L, - val toNextLevel:Int=0, - val level:Int=0 +data class GrowthResult( + val totalAmount: Long = 0L, + val experiencePoints: Int = 0, + val toNextLevel: Int = 0, + val level: Int = 0, + val heart: Int = 0 ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/HeartResult.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/HeartResult.kt new file mode 100644 index 0000000..5fae1a5 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/HeartResult.kt @@ -0,0 +1,8 @@ +package com.ssafy.tiggle.domain.entity.growth + +data class HeartResult( + val experiencePoints: Int = 0, + val toNextLevel: Int = 0, + val level: Int = 0, + val heart: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt b/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt index a7ee8f1..110adec 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt @@ -1,8 +1,10 @@ package com.ssafy.tiggle.domain.repository import com.ssafy.tiggle.domain.entity.growth.GrowthResult +import com.ssafy.tiggle.domain.entity.growth.HeartResult interface GrowthRepository { suspend fun getGrowthResult(): Result + suspend fun clickHeart(): Result } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/ClickHeartUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/ClickHeartUseCase.kt new file mode 100644 index 0000000..444fbe2 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/ClickHeartUseCase.kt @@ -0,0 +1,13 @@ +package com.ssafy.tiggle.domain.usecase.growth + +import com.ssafy.tiggle.domain.entity.growth.HeartResult +import com.ssafy.tiggle.domain.repository.GrowthRepository +import javax.inject.Inject + +class ClickHeartUseCase @Inject constructor( + private val growthRepository: GrowthRepository +) { + suspend operator fun invoke(): Result { + return growthRepository.clickHeart() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt index f6f0bb9..e76e876 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt @@ -3,5 +3,6 @@ package com.ssafy.tiggle.domain.usecase.growth import javax.inject.Inject data class GrowthUseCases @Inject constructor( - val getGrowthResultUseCase: GetGrowthResultUseCase + val getGrowthResultUseCase: GetGrowthResultUseCase, + val clickHeartUseCase: ClickHeartUseCase ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt index 16a9e3c..fc5597b 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt @@ -20,9 +20,9 @@ import com.ssafy.tiggle.presentation.ui.donation.DonationHistoryScreen import com.ssafy.tiggle.presentation.ui.donation.DonationRankingScreen import com.ssafy.tiggle.presentation.ui.donation.DonationStatusScreen import com.ssafy.tiggle.presentation.ui.dutchpay.CreateDutchPayScreen -import com.ssafy.tiggle.presentation.ui.dutchpay.DutchpayRecieveScreen -import com.ssafy.tiggle.presentation.ui.dutchpay.DutchPayStatusScreen import com.ssafy.tiggle.presentation.ui.dutchpay.DutchPayDetailScreen +import com.ssafy.tiggle.presentation.ui.dutchpay.DutchPayStatusScreen +import com.ssafy.tiggle.presentation.ui.dutchpay.DutchpayRecieveScreen import com.ssafy.tiggle.presentation.ui.growth.GrowthScreen import com.ssafy.tiggle.presentation.ui.piggybank.MainAccountDetailScreen import com.ssafy.tiggle.presentation.ui.piggybank.OpenAccountMode @@ -200,7 +200,7 @@ fun NavigationGraph( is Screen.MainAccountDetail -> NavEntry(key) { MainAccountDetailScreen( accountNo = key.accountNo, - onBackClick = {navBackStack.removeLastOrNull()} + onBackClick = { navBackStack.removeLastOrNull() } ) } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleLoadingIndicator.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleLoadingIndicator.kt new file mode 100644 index 0000000..18ddb48 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleLoadingIndicator.kt @@ -0,0 +1,167 @@ +package com.ssafy.tiggle.presentation.ui.components + +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.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition + +/** + * Lottie 기반 로딩 인디케이터 (버전 호환: progress/contentDescription 미사용) + * + * 사용 예) + * 1) 인라인: + * TiggleLoadingIndicator.InlineRaw(R.raw.loading_spinner, size = 32.dp, speed = 0.8f) + * + * 2) 전체 화면 오버레이: + * TiggleLoadingIndicator.FullscreenRaw( + * isVisible = uiState.isLoading, + * rawRes = R.raw.loading_spinner, + * size = 180.dp, + * speed = 1.0f + * ) + */ +object TiggleLoadingIndicator { + + // -------- 인라인 (raw 리소스) -------- + @Composable + fun InlineRaw( + rawRes: Int, + modifier: Modifier = Modifier, + size: Dp = 42.dp, + speed: Float = 1f, + iterations: Int = LottieConstants.IterateForever + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(rawRes)) + if (composition != null) { + LottieAnimation( + composition = composition, + modifier = modifier.size(size), + // 아래 파라미터들은 대부분의 버전에서 공통 지원 + iterations = iterations, + isPlaying = true, + speed = speed + ) + } + } + + // -------- 인라인 (assets 폴더) -------- + @Composable + fun InlineAsset( + assetName: String, // 예: "loading.json" (src/main/assets/) + modifier: Modifier = Modifier, + size: Dp = 42.dp, + speed: Float = 1f, + iterations: Int = LottieConstants.IterateForever + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.Asset(assetName)) + if (composition != null) { + LottieAnimation( + composition = composition, + modifier = modifier.size(size), + iterations = iterations, + isPlaying = true, + speed = speed + ) + } + } + + // -------- 인라인 (URL) -------- + @Composable + fun InlineUrl( + url: String, + modifier: Modifier = Modifier, + size: Dp = 42.dp, + speed: Float = 1f, + iterations: Int = LottieConstants.IterateForever + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.Url(url)) + if (composition != null) { + LottieAnimation( + composition = composition, + modifier = modifier.size(size), + iterations = iterations, + isPlaying = true, + speed = speed + ) + } + } + + // -------- 전체 화면 오버레이 (raw 리소스) -------- + @Composable + fun FullscreenRaw( + isVisible: Boolean, + rawRes: Int, + modifier: Modifier = Modifier, + size: Dp = 160.dp, + speed: Float = 1f, + backgroundColor: Color = Color.Black.copy(alpha = 0.35f) + ) { + if (!isVisible) return + + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(rawRes)) + Box( + modifier = modifier + .fillMaxSize() + .background(backgroundColor) + // 터치 차단을 위해 클릭 이벤트 소비 + .clickable(onClick = {}) + ) { + if (composition != null) { + LottieAnimation( + composition = composition, + modifier = Modifier + .align(Alignment.Center) + .size(size), + iterations = LottieConstants.IterateForever, + isPlaying = true, + speed = speed + ) + } + } + } + + // -------- 전체 화면 오버레이 (URL) -------- + @Composable + fun FullscreenUrl( + isVisible: Boolean, + url: String, + modifier: Modifier = Modifier, + size: Dp = 160.dp, + speed: Float = 1f, + backgroundColor: Color = Color.Black.copy(alpha = 0.35f) + ) { + if (!isVisible) return + + val composition by rememberLottieComposition(LottieCompositionSpec.Url(url)) + Box( + modifier = modifier + .fillMaxSize() + .background(backgroundColor) + .clickable(onClick = {}) + ) { + if (composition != null) { + LottieAnimation( + composition = composition, + modifier = Modifier + .align(Alignment.Center) + .size(size), + iterations = LottieConstants.IterateForever, + isPlaying = true, + speed = speed + ) + } + } + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt index f2eac5b..b54f6c3 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt @@ -2,29 +2,44 @@ package com.ssafy.tiggle.presentation.ui.growth import android.annotation.SuppressLint import android.content.Context +import android.os.Handler +import android.os.Looper import android.view.Choreographer import android.view.MotionEvent import android.view.SurfaceView +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.viewinterop.AndroidView import com.google.android.filament.EntityManager import com.google.android.filament.LightManager +import com.google.android.filament.gltfio.Animator +import com.google.android.filament.gltfio.FilamentAsset import com.google.android.filament.gltfio.ResourceLoader import com.google.android.filament.utils.ModelViewer import com.google.android.filament.utils.Utils +import com.ssafy.tiggle.R +import kotlinx.coroutines.delay import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.math.PI /** - * 레벨에 따라 다른 GLB를 로드하는 3D 캐릭터 컴포저블 + * 레벨에 따라 다른 GLB를 로드하는 3D 캐릭터 컴포저블 (이미지 플레이스홀더 사용) */ // 전역 변수 private var baseTransform: FloatArray? = null @@ -44,85 +59,223 @@ fun Character3D( val context = LocalContext.current var modelViewer by remember { mutableStateOf(null) } var currentLevel by remember { mutableStateOf(level) } + var isModelLoaded by remember { mutableStateOf(false) } + var shouldStartLoading by remember { mutableStateOf(false) } + + // 페이드 애니메이션 + val alpha by animateFloatAsState( + targetValue = if (isModelLoaded) 1f else 0f, + animationSpec = tween(durationMillis = 300), + label = "model_fade" + ) + + // 컴포지션 완료 후 로딩 시작 + LaunchedEffect(Unit) { + delay(50) // UI 렌더링 후 모델 로딩 시작 + shouldStartLoading = true + } + + // 레벨 변경 시 로딩 상태 리셋 + LaunchedEffect(level) { + if (currentLevel != level) { + isModelLoaded = false + currentLevel = level + } + } - // 렌더링 프레임 콜백 - val choreographer = remember { Choreographer.getInstance() } - val frameCallback = remember { - object : Choreographer.FrameCallback { - override fun doFrame(frameTimeNanos: Long) { - // ── 애니메이션이 있으면 시간계산해서 적용 ── - modelViewer?.let { mv -> - val animator = mv.animator - if (animIndex >= 0 && animator != null && animator.animationCount > 0) { - if (animStartNanos < 0L) animStartNanos = frameTimeNanos - val tSec = ((frameTimeNanos - animStartNanos) / 1_000_000_000.0f) - // duration이 0일 가능성 방지 - val dur = if (animDurationSec > 1e-4f) animDurationSec else 1f - val loopTime = (tSec % dur) - animator.applyAnimation(animIndex, loopTime) - animator.updateBoneMatrices() + Box(modifier = modifier) { + // 이미지 플레이스홀더 (모델 로딩 전/중) + if (!isModelLoaded) { + Image( + painter = painterResource(id = R.drawable.heart), + contentDescription = "캐릭터", + modifier = Modifier + .fillMaxSize() + .alpha(1f - alpha), + contentScale = ContentScale.Fit + ) + } + + // 3D 모델 뷰 + if (shouldStartLoading) { + // 렌더링 프레임 콜백 + val choreographer = remember { Choreographer.getInstance() } + val frameCallback = remember { + object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + // ── 애니메이션이 있으면 시간계산해서 적용 ── + modelViewer?.let { mv -> + val animator = mv.animator + if (animIndex >= 0 && animator != null && animator.animationCount > 0) { + if (animStartNanos < 0L) animStartNanos = frameTimeNanos + val tSec = ((frameTimeNanos - animStartNanos) / 1_000_000_000.0f) + // duration이 0일 가능성 방지 + val dur = if (animDurationSec > 1e-4f) animDurationSec else 1f + val loopTime = (tSec % dur) + animator.applyAnimation(animIndex, loopTime) + animator.updateBoneMatrices() + } + } + modelViewer?.render(frameTimeNanos) + choreographer.postFrameCallback(this) } } - modelViewer?.render(frameTimeNanos) - choreographer.postFrameCallback(this) } - } - } - AndroidView( - modifier = modifier, - factory = { ctx -> - val surfaceView = SurfaceView(ctx) + AndroidView( + modifier = Modifier + .fillMaxSize() + .alpha(alpha), + factory = { ctx -> + val surfaceView = SurfaceView(ctx) - // SurfaceView 투명 설정 - surfaceView.holder.setFormat(android.graphics.PixelFormat.TRANSLUCENT) - surfaceView.setZOrderOnTop(false) // 다른 뷰 뒤에 배치 + // SurfaceView 투명 설정 + surfaceView.holder.setFormat(android.graphics.PixelFormat.TRANSLUCENT) + surfaceView.setZOrderOnTop(false) // 다른 뷰 뒤에 배치 - try { - // Filament 초기화 - Utils.init() + try { + // Filament 초기화 + Utils.init() - // ModelViewer 생성 (기본 생성자 사용) - val viewer = ModelViewer(surfaceView) - modelViewer = viewer + // ModelViewer 생성 (기본 생성자 사용) + val viewer = ModelViewer(surfaceView) + modelViewer = viewer - // 투명 배경 설정 - setupTransparentBackground(viewer) + // 투명 배경 설정 + setupTransparentBackground(viewer) - // 개선된 조명 설정 - setupFrontLight(viewer) + // 개선된 조명 설정 + setupFrontLight(viewer) - // 터치 이벤트 처리 - 직접 구현 - if (enableOrbit) { - enableHorizontalDragRotation(surfaceView, viewer) - } + // 터치 이벤트 처리 - 직접 구현 + if (enableOrbit) { + enableHorizontalDragRotation(surfaceView, viewer) + } - // 모델 로드 - loadModelForLevel(ctx, viewer, level) + // 비동기로 모델 로드 + loadModelAsync(ctx, viewer, level) { + isModelLoaded = true + } - // 렌더링 시작 - choreographer.postFrameCallback(frameCallback) + // 렌더링 시작 + choreographer.postFrameCallback(frameCallback) - } catch (e: Exception) { - e.printStackTrace() - } + } catch (e: Exception) { + e.printStackTrace() + } + + surfaceView + }, + update = { _ -> + // 레벨 변경 시 모델 재로드 + if (currentLevel != level) { + currentLevel = level + isModelLoaded = false + modelViewer?.let { viewer -> + loadModelAsync(context, viewer, level) { + isModelLoaded = true + } + } + } + }, + onRelease = { + choreographer.removeFrameCallback(frameCallback) + modelViewer = null + isModelLoaded = false + } + ) + } + } +} - surfaceView - }, - update = { _ -> - // 레벨 변경 시 모델 재로드 - if (currentLevel != level) { - currentLevel = level - modelViewer?.let { viewer -> - loadModelForLevel(context, viewer, level) +/** + * 비동기 모델 로딩 (콜백으로 완료 알림) + */ +private fun loadModelAsync( + context: Context, + modelViewer: ModelViewer, + level: Int, + onLoadComplete: () -> Unit +) { + // 백그라운드 스레드에서 모델 로딩 + Thread { + try { + val assetPath = LevelModels.assetPathFor(level) + val buffer = readAssetFile(context, assetPath) + + if (buffer != null) { + // 메인 스레드에서 Filament 작업 수행 + Handler(Looper.getMainLooper()).post { + try { + modelViewer.loadModelGlb(buffer) + + // 리소스 로더 호출 (텍스처/머티리얼 GPU 업로드) + modelViewer.asset?.let { asset -> + ResourceLoader(modelViewer.engine).loadResources(asset) + } + + val asset = modelViewer.asset + val animator = modelViewer.animator + val animationCount = animator?.animationCount ?: 0 + val hasAnimation = animationCount > 0 + + if (hasAnimation) { + setupAnimatedModel(modelViewer, asset!!, animator!!) + } else { + // 기존처럼 모델 크기 정규화 + modelViewer.transformToUnitCube() + } + + // root 트랜스폼 저장 + saveBaseTransform(modelViewer) + + // 로딩 완료 콜백 호출 + onLoadComplete() + + } catch (e: Exception) { + e.printStackTrace() + } } } - }, - onRelease = { - choreographer.removeFrameCallback(frameCallback) - modelViewer = null + } catch (e: Exception) { + e.printStackTrace() } + }.start() +} + +private fun setupAnimatedModel(modelViewer: ModelViewer, asset: FilamentAsset, animator: Animator) { + // 애니메이션이 있으니까 크기 줄이기 + val tm = modelViewer.engine.transformManager + val root = asset.root + val rootInst = tm.getInstance(root) + + val scale = 0.28f + val translateY = -0.20f + + // column-major 4x4, translation은 [12],[13],[14] 슬롯 + val trs = floatArrayOf( + scale, 0f, 0f, 0f, + 0f, scale, 0f, 0f, + 0f, 0f, scale, 0f, + 0f, translateY, 0f, 1f ) + tm.setTransform(rootInst, trs) + + // 공통 프레임 루프에서 돌리도록 애니메이션 정보만 세팅 + animIndex = 0 + animDurationSec = animator.getAnimationDuration(0) + animStartNanos = -1L // 다음 프레임에서 시작점 리셋 +} + +private fun saveBaseTransform(modelViewer: ModelViewer) { + modelViewer.asset?.let { asset -> + val tm = modelViewer.engine.transformManager + val ti = tm.getInstance(asset.root) + if (ti != 0) { + baseTransform = FloatArray(16) + tm.getTransform(ti, baseTransform) + } + } } private fun setupFrontLight(modelViewer: ModelViewer) { @@ -130,10 +283,6 @@ private fun setupFrontLight(modelViewer: ModelViewer) { modelViewer.scene.skybox = null modelViewer.scene.indirectLight = null // ✅ 간접광도 제거 (정면 라이트만) - // 기존에 씬에 있던 라이트가 있다면 정리(선택) - // Filament는 라이트 엔티티를 추적하지 않으므로, - // 새 씬이 아니라면 별도 관리가 필요합니다. 일단 추가만 하는 상황이면 생략 가능. - // 정면에서 살짝 내려 비추는 한 개의 방향광 val key = EntityManager.get().create() LightManager.Builder(LightManager.Type.DIRECTIONAL) @@ -169,84 +318,6 @@ private fun setupTransparentBackground(modelViewer: ModelViewer) { } } -/** - * 레벨에 맞는 모델 로드 (안전한 방식) - */ -private fun loadModelForLevel(context: Context, modelViewer: ModelViewer, level: Int) { - try { - val assetPath = LevelModels.assetPathFor(level) - val buffer = readAssetFile(context, assetPath) ?: return - - modelViewer.loadModelGlb(buffer) - - // 리소스 로더 호출 (텍스처/머티리얼 GPU 업로드) - modelViewer.asset?.let { asset -> - ResourceLoader(modelViewer.engine).loadResources(asset) - } - val asset = modelViewer.asset - val animator = modelViewer.animator - val animationCount = animator?.animationCount ?: 0 - val hasAnimation = animationCount > 0 - - if (hasAnimation) { - // 애니메이션이 있으니까 크기 줄이기 - val tm = modelViewer.engine.transformManager - val root = asset!!.root - val rootInst = tm.getInstance(root) - - val scale = 0.28f - val translateY = -0.20f - - // column-major 4x4, translation은 [12],[13],[14] 슬롯 - val trs = floatArrayOf( - scale, 0f, 0f, 0f, - 0f, scale, 0f, 0f, - 0f, 0f, scale, 0f, - 0f, translateY, 0f, 1f - ) - tm.setTransform(rootInst, trs) - - // 공통 프레임 루프에서 돌리도록 애니메이션 정보만 세팅 - animIndex = 0 - animDurationSec = animator?.getAnimationDuration(0) ?: 1f - animStartNanos = -1L // 다음 프레임에서 시작점 리셋 - - - var animationTime = 0f - val choreographer = Choreographer.getInstance() - val cb = object : Choreographer.FrameCallback { - override fun doFrame(frameTimeNanos: Long) { - animationTime += 0.016f // 프레임당 약 16ms 진행 - - animator?.applyAnimation(0, animationTime) // 0번 애니메이션 적용 - animator?.updateBoneMatrices() - - modelViewer.render(frameTimeNanos) - choreographer.postFrameCallback(this) - } - } - choreographer.postFrameCallback(cb) - } else { - // 기존처럼 모델 크기 정규화 - modelViewer.transformToUnitCube() - } - - // root 트랜스폼 저장 - modelViewer.asset?.let { asset -> - val tm = modelViewer.engine.transformManager - val ti = tm.getInstance(asset.root) - if (ti != 0) { - baseTransform = FloatArray(16) - tm.getTransform(ti, baseTransform) - } - } - - } catch (e: Exception) { - e.printStackTrace() - } -} - - /** * assets에서 GLB 파일 읽기 */ @@ -330,4 +401,4 @@ private fun multiplyMat4(a: FloatArray, b: FloatArray): FloatArray { } } return out -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt index 9244311..965a0e8 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt @@ -30,12 +30,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput @@ -72,6 +74,12 @@ fun GrowthScreen( viewModel: GrowthViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + + // 상태 변화 감지를 위한 LaunchedEffect + LaunchedEffect(uiState.growth.level, uiState.growth.heart, uiState.growth.experiencePoints) { + // 상태가 변경될 때마다 로그 출력 (디버깅용) + android.util.Log.d("GrowthScreen", "🔄 UI 상태 업데이트: 레벨=${uiState.growth.level}, 하트=${uiState.growth.heart}, 경험치=${uiState.growth.experiencePoints}") + } TiggleScreenLayout( showBackButton = false, @@ -105,7 +113,8 @@ fun GrowthScreen( onDonationHistoryClick = onDonationHistoryClick, onDonationStatusClick = onDonationStatusClick, onDonationRankingClick = onDonationRankingClick, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + viewModel = viewModel ) } } @@ -173,8 +182,19 @@ private fun GrowthCard( onDonationHistoryClick: () -> Unit, onDonationStatusClick: () -> Unit, onDonationRankingClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: GrowthViewModel = hiltViewModel() ) { + // 진행률 계산 - experiencePoints 기반으로 개선 + val progress = remember( + uiState.growth.experiencePoints, + uiState.growth.toNextLevel + ) { + val currentExp = uiState.growth.experiencePoints.toFloat() + val totalExp = currentExp + uiState.growth.toNextLevel + if (totalExp == 0f) 0f else currentExp / totalExp + } + Card( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), @@ -198,20 +218,44 @@ private fun GrowthCard( .heightIn(min = 240.dp) .background(Color.Transparent) ) { - // 1) 캐릭터 - Character3D(level = uiState.growth.level, modifier = Modifier.fillMaxSize()) + // 캐릭터 + key(uiState.growth.level, uiState.growth.experiencePoints) { + Character3D(level = uiState.growth.level, modifier = Modifier.fillMaxSize()) + } + + // 레벨업 애니메이션 + if (uiState.isLevelUp) { + Box( + modifier = Modifier + .align(Alignment.Center) + .fillMaxSize() + ) { + // 레벨업 축하 텍스트 + Text( + text = "레벨업! 🎉", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = TiggleBlue, + modifier = Modifier + .align(Alignment.Center) + .background( + Color.White.copy(alpha = 0.9f), + RoundedCornerShape(16.dp) + ) + .padding(horizontal = 20.dp, vertical = 12.dp) + ) + } + } - // 2) 중앙 오버레이 Lottie (방법2: 진행도 직접 애니메이션) - val LOTTIE_SIZE_DP = 260.dp // ← 더 크게 - val LOTTIE_DURATION_MS = 3000 // ← 원하는 재생 시간(ms) 여기서 조절 + // Lottie 애니메이션 + val LOTTIE_SIZE_DP = 260.dp + val LOTTIE_DURATION_MS = 3000 val SLOW_PORTION = 0.85f val MID_PROGRESS = 0.60f var playLottie by remember { mutableStateOf(false) } val composition by rememberLottieComposition( - LottieCompositionSpec.RawRes(R.raw.heart2) // res/raw/heart.json + LottieCompositionSpec.RawRes(R.raw.heart2) ) - - // 진행도 0f → 1f 를 내가 정한 시간에 맞춰 애니메이션 val lottieProgress = remember { Animatable(0f) } LaunchedEffect(playLottie, composition) { @@ -222,8 +266,6 @@ private fun GrowthCard( targetValue = MID_PROGRESS, animationSpec = tween(durationMillis = t1, easing = LinearEasing) ) - - // 2) 뒤 구간: 남은 시간에 MID_PROGRESS → 1.0 (막판에 몰아서 빨라짐) val t2 = LOTTIE_DURATION_MS - t1 lottieProgress.animateTo( targetValue = 1f, @@ -232,7 +274,7 @@ private fun GrowthCard( easing = FastOutLinearInEasing ) ) - playLottie = false // 1회 재생 후 종료 + playLottie = false } } @@ -240,7 +282,7 @@ private fun GrowthCard( Box( modifier = Modifier .align(Alignment.Center) - .size(LOTTIE_SIZE_DP) // ← 크기 키움 + .size(LOTTIE_SIZE_DP) ) { LottieAnimation( composition = composition, @@ -249,62 +291,97 @@ private fun GrowthCard( } } - // 3) 드래그 하트 (중앙 드롭 성공 시 Lottie 트리거 + 하트 원위치 복귀) - DraggableHeartDropTrigger( - iconRes = R.drawable.heart, - iconSize = 50.dp, - triggerRadius = 80.dp, - startOffsetBottomPadding = 16.dp, - onDropInCenter = { playLottie = true } - ) + // 드래그 하트 + key(uiState.growth.heart) { + DraggableHeartDropTrigger( + iconRes = R.drawable.heart, + iconSize = 50.dp, + triggerRadius = 80.dp, + startOffsetBottomPadding = 16.dp, + enabled = uiState.growth.heart > 0, // 0개면 비활성화 + onDropInCenter = { + playLottie = true + viewModel.useHeart() // 하트 사용 API 호출 + } + ) + } } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "레벨 ${uiState.growth.level + 1}", - fontSize = 14.sp, - color = TiggleGrayText, - modifier = Modifier - .background(Color.White.copy(alpha = 0.7f), RoundedCornerShape(12.dp)) - .padding(horizontal = 12.dp, vertical = 4.dp) - ) - Spacer(Modifier.width(12.dp)) - Text( - text = "쏠", - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = TiggleBlue - ) + // 레벨 + 하트 개수 표시 + key(uiState.growth.level, uiState.growth.heart) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "레벨 ${uiState.growth.level}", + fontSize = 14.sp, + color = TiggleGrayText, + modifier = Modifier + .background( + Color.White.copy(alpha = 0.7f), + RoundedCornerShape(12.dp) + ) + .padding(horizontal = 12.dp, vertical = 4.dp) + ) + Spacer(Modifier.width(12.dp)) + Text( + text = "쏠", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = TiggleBlue + ) + } + Text( + text = "❤️ ${uiState.growth.heart}", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = if (uiState.growth.heart > 0) Color.Red else TiggleGrayText + ) + } } Spacer(Modifier.height(16.dp)) - Text( - text = "총 티끌: ${Formatter.formatCurrency(uiState.growth.totalAmount)}", - fontSize = 16.sp, - color = Color.Black, - fontWeight = FontWeight.Medium - ) + key(uiState.growth.experiencePoints, uiState.growth.toNextLevel) { + Text( + text = "총 티끌: ${Formatter.formatCurrency(uiState.growth.totalAmount)}", + fontSize = 16.sp, + color = Color.Black, + fontWeight = FontWeight.Medium + ) + + Spacer(Modifier.height(4.dp)) + + Text( + text = "경험치: ${uiState.growth.experiencePoints}", + fontSize = 14.sp, + color = TiggleGrayText, + fontWeight = FontWeight.Medium + ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(12.dp)) - LinearProgressIndicator( - progress = { 0.7f }, // TODO: 실제 진행률 - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .clip(RoundedCornerShape(4.dp)), - color = TiggleBlue, - trackColor = Color.White.copy(alpha = 0.3f) - ) + LinearProgressIndicator( + progress = { progress }, // 실제 진행률 적용 + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(4.dp)), + color = TiggleBlue, + trackColor = Color.White.copy(alpha = 0.3f) + ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(8.dp)) - Text( - text = "다음 레벨까지 ${Formatter.formatCurrency(uiState.growth.toNextLevel.toLong())}", - fontSize = 12.sp, - color = TiggleGrayText - ) + Text( + text = "다음 레벨까지 ${uiState.growth.toNextLevel} 경험치", + fontSize = 12.sp, + color = TiggleGrayText + ) + } } } } @@ -319,7 +396,8 @@ private fun DraggableHeartDropTrigger( iconSize: Dp, triggerRadius: Dp, startOffsetBottomPadding: Dp = 16.dp, - onDropInCenter: () -> Unit + onDropInCenter: () -> Unit, + enabled: Boolean ) { val density = LocalDensity.current val scope = rememberCoroutineScope() @@ -358,35 +436,37 @@ private fun DraggableHeartDropTrigger( modifier = Modifier .size(iconSize) .offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) } - .pointerInput(parentW, parentH, iconSizePx) { - detectDragGestures( - onDrag = { change, drag -> - // change.consume() // 버전에 따라 경고 나면 생략해도 OK - val nx = (offsetX.value + drag.x) - .coerceIn(0f, (parentW - iconSizePx).coerceAtLeast(0f)) - val ny = (offsetY.value + drag.y) - .coerceIn(0f, (parentH - iconSizePx).coerceAtLeast(0f)) - scope.launch { offsetX.snapTo(nx) } - scope.launch { offsetY.snapTo(ny) } - }, - onDragEnd = { - val centerX = parentW / 2f - val centerY = parentH / 2f - val heartCenterX = offsetX.value + iconSizePx / 2f - val heartCenterY = offsetY.value + iconSizePx / 2f - val dist = kotlin.math.hypot( - heartCenterX - centerX, heartCenterY - centerY - ) + .alpha(if (enabled) 1f else 0.3f) + .then( + if (enabled) Modifier.pointerInput(parentW, parentH, iconSizePx) { + detectDragGestures( + onDrag = { _, drag -> + val nx = (offsetX.value + drag.x) + .coerceIn(0f, (parentW - iconSizePx).coerceAtLeast(0f)) + val ny = (offsetY.value + drag.y) + .coerceIn(0f, (parentH - iconSizePx).coerceAtLeast(0f)) + scope.launch { offsetX.snapTo(nx) } + scope.launch { offsetY.snapTo(ny) } + }, + onDragEnd = { + val centerX = parentW / 2f + val centerY = parentH / 2f + val heartCenterX = offsetX.value + iconSizePx / 2f + val heartCenterY = offsetY.value + iconSizePx / 2f + val dist = kotlin.math.hypot( + heartCenterX - centerX, heartCenterY - centerY + ) - if (dist <= triggerRadiusPx) onDropInCenter() + if (dist <= triggerRadiusPx) onDropInCenter() - val sx = parentW / 2f - iconSizePx / 2f - val sy = parentH - iconSizePx - bottomPadPx - scope.launch { offsetX.animateTo(sx, tween(220)) } - scope.launch { offsetY.animateTo(sy, tween(220)) } - } - ) - } + val sx = parentW / 2f - iconSizePx / 2f + val sy = parentH - iconSizePx - bottomPadPx + scope.launch { offsetX.animateTo(sx, tween(220)) } + scope.launch { offsetY.animateTo(sy, tween(220)) } + } + ) + } else Modifier + ) ) } } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt index 3297df6..ae30686 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt @@ -1,17 +1,21 @@ package com.ssafy.tiggle.presentation.ui.growth import com.ssafy.tiggle.domain.entity.growth.GrowthResult +import com.ssafy.tiggle.domain.entity.growth.HeartResult /** * 성장 화면의 UI 상태 */ data class GrowthUiState( val isLoading: Boolean = false, - val growth: GrowthResult= GrowthResult(), + val growth: GrowthResult = GrowthResult(), val characterStatus: String = "행복", // 캐릭터 상태 val donationHistory: List = emptyList(), val donationRanking: List = emptyList(), - val errorMessage: String? = null + val heart: HeartResult = HeartResult(), + val errorMessage: String? = null, + val isLevelUp: Boolean = false, // 레벨업 상태 + val previousLevel: Int = 0 // 이전 레벨 ) /** diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt index 1c24089..181768b 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt @@ -1,12 +1,13 @@ package com.ssafy.tiggle.presentation.ui.growth +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ssafy.tiggle.domain.usecase.growth.GrowthUseCases +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import retrofit2.HttpException @@ -16,15 +17,15 @@ import javax.inject.Inject class GrowthViewModel @Inject constructor( val growthUseCases: GrowthUseCases ) : ViewModel() { - + private val _uiState = MutableStateFlow(GrowthUiState()) val uiState: StateFlow = _uiState.asStateFlow() - + init { // 초기 데이터 로드 (나중에 실제 API 연결) loadGrowthData() } - + private fun loadGrowthData() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, errorMessage = null) } @@ -33,6 +34,7 @@ class GrowthViewModel @Inject constructor( result .onSuccess { growth -> + Log.d("GrowthViewModel", "✅ 성장 데이터 로드 성공: $growth") _uiState.update { it.copy( isLoading = false, @@ -43,6 +45,7 @@ class GrowthViewModel @Inject constructor( } .onFailure { e -> val isNotFound = (e is HttpException && e.code() == 404) + Log.e("GrowthViewModel", "❌ 성장 데이터 로드 실패: ${e.message}") _uiState.update { it.copy( isLoading = false, @@ -53,15 +56,74 @@ class GrowthViewModel @Inject constructor( } } + fun useHeart() { + viewModelScope.launch { + Log.d("GrowthViewModel", "💖 하트 사용 시작") + val result = growthUseCases.clickHeartUseCase() + result + .onSuccess { heartResult -> + // 이전 레벨 저장 + val previousLevel = _uiState.value.growth.level + Log.d("GrowthViewModel", "✅ 하트 사용 성공: $heartResult, 이전 레벨: $previousLevel") + + // 상태를 한 번에 업데이트하여 화면 갱신 보장 + _uiState.update { currentState -> + currentState.copy( + heart = heartResult, + growth = currentState.growth.copy( + experiencePoints = heartResult.experiencePoints, + level = heartResult.level, + toNextLevel = heartResult.toNextLevel, + heart = heartResult.heart + ), + // 에러 메시지 초기화 + errorMessage = null + ) + } + + // 레벨업 체크 및 처리 + if (heartResult.level > previousLevel) { + Log.d("GrowthViewModel", "🎉 레벨업 발생: $previousLevel → ${heartResult.level}") + handleLevelUp(previousLevel, heartResult.level) + } + } + .onFailure { e -> + Log.e("GrowthViewModel", "❌ 하트 사용 실패: ${e.message}") + _uiState.update { + it.copy(errorMessage = e.message ?: "하트 사용 실패") + } + } + } + } + + /** + * 레벨업 처리 로직 + */ + private fun handleLevelUp(previousLevel: Int, newLevel: Int) { + Log.d("GrowthViewModel", "🎊 레벨업 처리: $previousLevel → $newLevel") + _uiState.update { currentState -> + currentState.copy( + isLevelUp = true, + previousLevel = previousLevel + ) + } + + // 3초 후 레벨업 상태 초기화 + viewModelScope.launch { + kotlinx.coroutines.delay(3000) + _uiState.update { it.copy(isLevelUp = false) } + Log.d("GrowthViewModel", "⏰ 레벨업 애니메이션 종료") + } + } fun onDonationHistoryClick() { // TODO: 기부 기록 화면으로 이동 } - + fun onDonationStatusClick() { // TODO: 기부 현황 화면으로 이동 } - + fun onDonationRankingClick() { // TODO: 기부 랭킹 화면으로 이동 } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt index 9ff9083..0dda530 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt @@ -3,9 +3,9 @@ package com.ssafy.tiggle.presentation.ui.growth object LevelModels { fun assetPathFor(level: Int): String { val bucket = when { - level == 0 -> "level_1.glb" - level == 1 -> "level_2.glb" - level == 2 -> "level_3.glb" + level == 1 -> "level_1.glb" + level == 2 -> "level_2.glb" + level == 3 -> "level_3.glb" else -> "level_4.glb" } return "models/$bucket" diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsScreen.kt index 86e1883..78eb214 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsScreen.kt @@ -134,7 +134,25 @@ private fun ShortsVideoItem( LaunchedEffect(isCurrentItem) { if (isCurrentItem) { try { - exoPlayer.setMediaItem(MediaItem.fromUri(video.videoUrl)) + // 로컬 비디오 파일 처리 + val mediaItem = if (video.videoUrl.startsWith("asset://")) { + // assets 폴더의 파일인 경우 + val assetPath = video.videoUrl.removePrefix("asset://") + try { + // 파일 존재 여부 확인 + context.assets.open(assetPath).use { } + MediaItem.fromUri("file:///android_asset/$assetPath") + } catch (e: Exception) { + Log.w("ShortsScreen", "로컬 비디오 파일을 찾을 수 없습니다: $assetPath, fallback URL 사용") + // 로컬 파일이 없으면 fallback URL 사용 + MediaItem.fromUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4") + } + } else { + // 일반 URL인 경우 + MediaItem.fromUri(video.videoUrl) + } + + exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.playWhenReady = true exoPlayer.repeatMode = Player.REPEAT_MODE_ONE diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsViewModel.kt index 1aa45ce..45bb95c 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/shorts/ShortsViewModel.kt @@ -1,5 +1,6 @@ package com.ssafy.tiggle.presentation.ui.shorts +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -27,11 +28,11 @@ class ShortsViewModel @Inject constructor( viewModelScope.launch { try { - // TODO: 실제 비디오 데이터는 Repository에서 가져오기 - val mockVideos = generateMockVideos() + // 로컬 비디오 파일 사용 + val localVideos = generateLocalVideos() _uiState.update { it.copy( - videos = mockVideos, + videos = localVideos, isLoading = false ) } @@ -62,8 +63,8 @@ class ShortsViewModel @Inject constructor( viewModelScope.launch { try { - // TODO: 더 많은 비디오 로드 로직 - val moreVideos = generateMockVideos(startIndex = _uiState.value.videos.size) + // 더 많은 로컬 비디오 로드 + val moreVideos = generateLocalVideos(startIndex = _uiState.value.videos.size) _uiState.update { currentState -> currentState.copy( videos = currentState.videos + moreVideos, @@ -102,13 +103,19 @@ class ShortsViewModel @Inject constructor( _uiState.update { it.copy(errorMessage = null) } } - // Mock 데이터 생성 (나중에 제거) - private fun generateMockVideos(startIndex: Int = 0): List { - val mockVideos = mutableListOf() + // 로컬 비디오 파일 사용 + private fun generateLocalVideos(startIndex: Int = 0): List { + val localVideos = mutableListOf() - // 테스트용 샘플 비디오 URL들 - 안정적인 Google 스토리지 사용 - // 실제 프로젝트에서는 로컬 파일이나 자체 서버 URL 사용 권장 - val sampleVideoUrls = listOf( + // 로컬 비디오 파일 경로들 (assets 폴더 기준) + val localVideoFiles = listOf( + "videos/video1.mp4", + "videos/video2.mp4", + "videos/video3.mp4", + ) + + // 만약 로컬 파일이 없다면 기본 샘플 비디오 사용 + val fallbackVideoUrls = listOf( "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", @@ -117,21 +124,28 @@ class ShortsViewModel @Inject constructor( ) for (i in startIndex until startIndex + 10) { - mockVideos.add( + // 로컬 파일 우선 사용, 없으면 fallback URL 사용 + val videoUrl = if (i < localVideoFiles.size) { + "asset://${localVideoFiles[i % localVideoFiles.size]}" + } else { + fallbackVideoUrls[i % fallbackVideoUrls.size] + } + + localVideos.add( ShortsVideo( - id = "video_$i", - videoUrl = sampleVideoUrls[i % sampleVideoUrls.size], - title = "재미있는 숏폼 영상 #$i\n금융 꿀팁과 일상 브이로그", + id = "local_video_$i", + videoUrl = videoUrl, + title = "로컬 숏폼 영상 #$i\n기부와 관련된 영상입니다", username = "티끌유저${i + 1}", likeCount = (100..10000).random(), shareCount = (10..1000).random(), viewCount = (1000..100000).random(), - hashtags = listOf("금융", "절약", "투자", "일상", "브이로그", "꿀팁").shuffled().take(3), + hashtags = listOf("기부", "나눔", "봉사", "일상", "브이로그", "꿀팁").shuffled().take(3), isLiked = false ) ) } - return mockVideos + return localVideos } } diff --git a/app/src/main/res/raw/loading.json b/app/src/main/res/raw/loading.json new file mode 100644 index 0000000..18430e9 --- /dev/null +++ b/app/src/main/res/raw/loading.json @@ -0,0 +1 @@ +{"nm":"","v":"5.5.2","ip":0,"op":45,"fr":30,"w":100,"h":100,"assets":[],"layers":[{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"ind":0,"sr":1},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"ind":1,"sr":1,"parent":0},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"ind":2,"sr":1,"parent":1},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"ind":3,"sr":1,"parent":1},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"ind":4,"sr":1,"parent":3},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[61.7,-19.7],"h":1},{"t":1,"s":[73.5,-16.7],"h":1},{"t":2,"s":[85.7,-11.1],"h":1},{"t":3,"s":[97.5,-2.4],"h":1},{"t":4,"s":[107.9,9.4],"h":1},{"t":5,"s":[115.9,24.3],"h":1},{"t":6,"s":[120.2,41.5],"h":1},{"t":7,"s":[120,60.2],"h":1},{"t":8,"s":[114.7,78.6],"h":1},{"t":9,"s":[104.3,95.3],"h":1},{"t":10,"s":[89.7,108.5],"h":1},{"t":11,"s":[72.3,117.1],"h":1},{"t":12,"s":[53.6,120.6],"h":1},{"t":13,"s":[35.3,119.2],"h":1},{"t":14,"s":[18.7,113.4],"h":1},{"t":15,"s":[4.9,104.5],"h":1},{"t":16,"s":[-5.8,93.4],"h":1},{"t":17,"s":[-13.4,81.4],"h":1},{"t":18,"s":[-18,69.3],"h":1},{"t":19,"s":[-20.3,57.7],"h":1},{"t":20,"s":[-20.7,47.1],"h":1},{"t":21,"s":[-19.6,37.7],"h":1},{"t":22,"s":[-17.7,29.5],"h":1},{"t":23,"s":[-15.2,22.6],"h":1},{"t":24,"s":[-12.5,16.9],"h":1},{"t":25,"s":[-9.8,12.2],"h":1},{"t":26,"s":[-7.3,8.5],"h":1},{"t":27,"s":[-5,5.6],"h":1},{"t":28,"s":[-3.2,3.4],"h":1},{"t":29,"s":[-1.7,1.8],"h":1},{"t":30,"s":[-0.7,0.7],"h":1},{"t":31,"s":[-0.1,0.1],"h":1},{"t":32,"s":[0,0],"h":1},{"t":33,"s":[0.3,-0.3],"h":1},{"t":34,"s":[1,-1],"h":1},{"t":35,"s":[2.3,-2.2],"h":1},{"t":36,"s":[4.1,-3.8],"h":1},{"t":37,"s":[6.5,-5.8],"h":1},{"t":38,"s":[9.7,-8.1],"h":1},{"t":39,"s":[13.8,-10.7],"h":1},{"t":40,"s":[18.8,-13.5],"h":1},{"t":41,"s":[24.9,-16.1],"h":1},{"t":42,"s":[32.2,-18.4],"h":1},{"t":43,"s":[40.8,-20.1],"h":1},{"t":44,"s":[50.7,-20.7],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[54.5],"h":1},{"t":1,"s":[64.4],"h":1},{"t":2,"s":[75.3],"h":1},{"t":3,"s":[87.2],"h":1},{"t":4,"s":[100],"h":1},{"t":5,"s":[113.7],"h":1},{"t":6,"s":[128.1],"h":1},{"t":7,"s":[143.3],"h":1},{"t":8,"s":[158.9],"h":1},{"t":9,"s":[174.8],"h":1},{"t":10,"s":[190.8],"h":1},{"t":11,"s":[206.6],"h":1},{"t":12,"s":[222.1],"h":1},{"t":13,"s":[237],"h":1},{"t":14,"s":[251.2],"h":1},{"t":15,"s":[264.6],"h":1},{"t":16,"s":[277.1],"h":1},{"t":17,"s":[288.6],"h":1},{"t":18,"s":[299.2],"h":1},{"t":19,"s":[308.7],"h":1},{"t":20,"s":[317.4],"h":1},{"t":21,"s":[325],"h":1},{"t":22,"s":[331.8],"h":1},{"t":23,"s":[337.8],"h":1},{"t":24,"s":[342.9],"h":1},{"t":25,"s":[347.3],"h":1},{"t":26,"s":[350.9],"h":1},{"t":27,"s":[353.9],"h":1},{"t":28,"s":[356.3],"h":1},{"t":29,"s":[358],"h":1},{"t":30,"s":[359.2],"h":1},{"t":31,"s":[359.8],"h":1},{"t":32,"s":[0],"h":1},{"t":33,"s":[0.3],"h":1},{"t":34,"s":[1.2],"h":1},{"t":35,"s":[2.5],"h":1},{"t":36,"s":[4.5],"h":1},{"t":37,"s":[7.1],"h":1},{"t":38,"s":[10.3],"h":1},{"t":39,"s":[14.2],"h":1},{"t":40,"s":[18.8],"h":1},{"t":41,"s":[24.2],"h":1},{"t":42,"s":[30.5],"h":1},{"t":43,"s":[37.6],"h":1},{"t":44,"s":[45.6],"h":1}]}},"ind":5,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[29.2,68.3],[24.7,70.9],[23.2,68.3],[27.7,65.7],[29.2,68.3]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-134.5]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[29.2,68.3],[24.7,70.9],[23.2,68.3],[27.7,65.7],[29.2,68.3]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-134.5]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.9,0.9,0.9],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.9,0.9,0.9],"h":1}]}}],"ind":6,"sr":1,"parent":5},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[42.1,-20.3],"h":1},{"t":1,"s":[52.1,-20.7],"h":1},{"t":2,"s":[63.2,-19.5],"h":1},{"t":3,"s":[75.1,-16.1],"h":1},{"t":4,"s":[87.3,-10.1],"h":1},{"t":5,"s":[98.9,-1.1],"h":1},{"t":6,"s":[109.1,11.2],"h":1},{"t":7,"s":[116.7,26.4],"h":1},{"t":8,"s":[120.5,43.9],"h":1},{"t":9,"s":[119.6,62.6],"h":1},{"t":10,"s":[113.6,81],"h":1},{"t":11,"s":[102.6,97.2],"h":1},{"t":12,"s":[87.6,109.9],"h":1},{"t":13,"s":[69.9,117.9],"h":1},{"t":14,"s":[51.1,120.7],"h":1},{"t":15,"s":[33,118.6],"h":1},{"t":16,"s":[16.8,112.4],"h":1},{"t":17,"s":[3.3,103.1],"h":1},{"t":18,"s":[-7,91.9],"h":1},{"t":19,"s":[-14.1,79.8],"h":1},{"t":20,"s":[-18.5,67.7],"h":1},{"t":21,"s":[-20.4,56.3],"h":1},{"t":22,"s":[-20.6,45.8],"h":1},{"t":23,"s":[-19.4,36.5],"h":1},{"t":24,"s":[-17.4,28.6],"h":1},{"t":25,"s":[-14.9,21.8],"h":1},{"t":26,"s":[-12.1,16.2],"h":1},{"t":27,"s":[-9.4,11.7],"h":1},{"t":28,"s":[-6.9,8.1],"h":1},{"t":29,"s":[-4.8,5.3],"h":1},{"t":30,"s":[-2.9,3.1],"h":1},{"t":31,"s":[-1.6,1.6],"h":1},{"t":32,"s":[-0.6,0.6],"h":1},{"t":33,"s":[-0.1,0.1],"h":1},{"t":34,"s":[0,0],"h":1},{"t":35,"s":[0.4,-0.4],"h":1},{"t":36,"s":[1.2,-1.1],"h":1},{"t":37,"s":[2.5,-2.4],"h":1},{"t":38,"s":[4.4,-4],"h":1},{"t":39,"s":[6.9,-6.1],"h":1},{"t":40,"s":[10.2,-8.4],"h":1},{"t":41,"s":[14.4,-11.1],"h":1},{"t":42,"s":[19.5,-13.8],"h":1},{"t":43,"s":[25.8,-16.4],"h":1},{"t":44,"s":[33.3,-18.7],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[38.6],"h":1},{"t":1,"s":[46.7],"h":1},{"t":2,"s":[55.7],"h":1},{"t":3,"s":[65.8],"h":1},{"t":4,"s":[76.8],"h":1},{"t":5,"s":[88.8],"h":1},{"t":6,"s":[101.7],"h":1},{"t":7,"s":[115.5],"h":1},{"t":8,"s":[130.1],"h":1},{"t":9,"s":[145.3],"h":1},{"t":10,"s":[161],"h":1},{"t":11,"s":[176.9],"h":1},{"t":12,"s":[192.9],"h":1},{"t":13,"s":[208.7],"h":1},{"t":14,"s":[224.1],"h":1},{"t":15,"s":[238.9],"h":1},{"t":16,"s":[253],"h":1},{"t":17,"s":[266.3],"h":1},{"t":18,"s":[278.7],"h":1},{"t":19,"s":[290.1],"h":1},{"t":20,"s":[300.5],"h":1},{"t":21,"s":[309.9],"h":1},{"t":22,"s":[318.4],"h":1},{"t":23,"s":[326],"h":1},{"t":24,"s":[332.6],"h":1},{"t":25,"s":[338.5],"h":1},{"t":26,"s":[343.5],"h":1},{"t":27,"s":[347.8],"h":1},{"t":28,"s":[351.4],"h":1},{"t":29,"s":[354.2],"h":1},{"t":30,"s":[356.5],"h":1},{"t":31,"s":[358.2],"h":1},{"t":32,"s":[359.3],"h":1},{"t":33,"s":[359.9],"h":1},{"t":34,"s":[0],"h":1},{"t":35,"s":[0.4],"h":1},{"t":36,"s":[1.3],"h":1},{"t":37,"s":[2.8],"h":1},{"t":38,"s":[4.8],"h":1},{"t":39,"s":[7.4],"h":1},{"t":40,"s":[10.7],"h":1},{"t":41,"s":[14.7],"h":1},{"t":42,"s":[19.5],"h":1},{"t":43,"s":[25],"h":1},{"t":44,"s":[31.3],"h":1}]}},"ind":7,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[34.7,73.8],[30.2,76.4],[28.7,73.8],[33.2,71.2],[34.7,73.8]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-145.5]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[34.7,73.8],[30.2,76.4],[28.7,73.8],[33.2,71.2],[34.7,73.8]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-145.5]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.8,0.9,0.9],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.8,0.9,0.9],"h":1}]}}],"ind":8,"sr":1,"parent":7},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[26.5,-16.7],"h":1},{"t":1,"s":[34.1,-18.9],"h":1},{"t":2,"s":[43,-20.4],"h":1},{"t":3,"s":[53.1,-20.6],"h":1},{"t":4,"s":[64.3,-19.2],"h":1},{"t":5,"s":[76.3,-15.6],"h":1},{"t":6,"s":[88.5,-9.3],"h":1},{"t":7,"s":[100,0],"h":1},{"t":8,"s":[110,12.6],"h":1},{"t":9,"s":[117.2,28.1],"h":1},{"t":10,"s":[120.6,45.8],"h":1},{"t":11,"s":[119.2,64.5],"h":1},{"t":12,"s":[112.7,82.7],"h":1},{"t":13,"s":[101.3,98.7],"h":1},{"t":14,"s":[85.9,110.9],"h":1},{"t":15,"s":[68,118.4],"h":1},{"t":16,"s":[49.2,120.7],"h":1},{"t":17,"s":[31.2,118.2],"h":1},{"t":18,"s":[15.3,111.6],"h":1},{"t":19,"s":[2.1,102],"h":1},{"t":20,"s":[-7.8,90.7],"h":1},{"t":21,"s":[-14.7,78.6],"h":1},{"t":22,"s":[-18.8,66.5],"h":1},{"t":23,"s":[-20.5,55.2],"h":1},{"t":24,"s":[-20.5,44.8],"h":1},{"t":25,"s":[-19.2,35.7],"h":1},{"t":26,"s":[-17.1,27.8],"h":1},{"t":27,"s":[-14.6,21.2],"h":1},{"t":28,"s":[-11.9,15.7],"h":1},{"t":29,"s":[-9.2,11.3],"h":1},{"t":30,"s":[-6.7,7.8],"h":1},{"t":31,"s":[-4.6,5],"h":1},{"t":32,"s":[-2.8,3],"h":1},{"t":33,"s":[-1.4,1.5],"h":1},{"t":34,"s":[-0.5,0.5],"h":1},{"t":35,"s":[-0.1,0.1],"h":1},{"t":36,"s":[0,0],"h":1},{"t":37,"s":[0.4,-0.4],"h":1},{"t":38,"s":[1.3,-1.2],"h":1},{"t":39,"s":[2.6,-2.5],"h":1},{"t":40,"s":[4.6,-4.2],"h":1},{"t":41,"s":[7.2,-6.3],"h":1},{"t":42,"s":[10.6,-8.7],"h":1},{"t":43,"s":[14.8,-11.4],"h":1},{"t":44,"s":[20.1,-14.1],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[25.6],"h":1},{"t":1,"s":[32],"h":1},{"t":2,"s":[39.3],"h":1},{"t":3,"s":[47.5],"h":1},{"t":4,"s":[56.7],"h":1},{"t":5,"s":[66.8],"h":1},{"t":6,"s":[78],"h":1},{"t":7,"s":[90],"h":1},{"t":8,"s":[103.1],"h":1},{"t":9,"s":[116.9],"h":1},{"t":10,"s":[131.6],"h":1},{"t":11,"s":[146.8],"h":1},{"t":12,"s":[162.6],"h":1},{"t":13,"s":[178.5],"h":1},{"t":14,"s":[194.5],"h":1},{"t":15,"s":[210.3],"h":1},{"t":16,"s":[225.6],"h":1},{"t":17,"s":[240.4],"h":1},{"t":18,"s":[254.4],"h":1},{"t":19,"s":[267.6],"h":1},{"t":20,"s":[279.9],"h":1},{"t":21,"s":[291.2],"h":1},{"t":22,"s":[301.5],"h":1},{"t":23,"s":[310.8],"h":1},{"t":24,"s":[319.2],"h":1},{"t":25,"s":[326.7],"h":1},{"t":26,"s":[333.3],"h":1},{"t":27,"s":[339],"h":1},{"t":28,"s":[344],"h":1},{"t":29,"s":[348.2],"h":1},{"t":30,"s":[351.7],"h":1},{"t":31,"s":[354.5],"h":1},{"t":32,"s":[356.7],"h":1},{"t":33,"s":[358.3],"h":1},{"t":34,"s":[359.4],"h":1},{"t":35,"s":[359.9],"h":1},{"t":36,"s":[0],"h":1},{"t":37,"s":[0.5],"h":1},{"t":38,"s":[1.4],"h":1},{"t":39,"s":[2.9],"h":1},{"t":40,"s":[5],"h":1},{"t":41,"s":[7.7],"h":1},{"t":42,"s":[11.1],"h":1},{"t":43,"s":[15.2],"h":1},{"t":44,"s":[20],"h":1}]}},"ind":9,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[41.5,77.7],[37,80.3],[35.5,77.7],[40,75.1],[41.5,77.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-153.4]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[41.5,77.7],[37,80.3],[35.5,77.7],[40,75.1],[41.5,77.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-153.4]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.6,0.7,0.8],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.6,0.7,0.8],"h":1}]}}],"ind":10,"sr":1,"parent":9},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[15.5,-11.7],"h":1},{"t":1,"s":[20.9,-14.4],"h":1},{"t":2,"s":[27.4,-17],"h":1},{"t":3,"s":[35.2,-19.1],"h":1},{"t":4,"s":[44.3,-20.5],"h":1},{"t":5,"s":[54.5,-20.6],"h":1},{"t":6,"s":[65.9,-18.9],"h":1},{"t":7,"s":[77.9,-15],"h":1},{"t":8,"s":[90,-8.3],"h":1},{"t":9,"s":[101.5,1.5],"h":1},{"t":10,"s":[111.1,14.5],"h":1},{"t":11,"s":[117.9,30.3],"h":1},{"t":12,"s":[120.7,48.2],"h":1},{"t":13,"s":[118.6,67],"h":1},{"t":14,"s":[111.5,85],"h":1},{"t":15,"s":[99.5,100.5],"h":1},{"t":16,"s":[83.7,112.2],"h":1},{"t":17,"s":[65.6,119],"h":1},{"t":18,"s":[46.8,120.6],"h":1},{"t":19,"s":[29,117.5],"h":1},{"t":20,"s":[13.4,110.5],"h":1},{"t":21,"s":[0.6,100.6],"h":1},{"t":22,"s":[-8.9,89.1],"h":1},{"t":23,"s":[-15.4,77],"h":1},{"t":24,"s":[-19.1,65],"h":1},{"t":25,"s":[-20.6,53.7],"h":1},{"t":26,"s":[-20.4,43.5],"h":1},{"t":27,"s":[-19,34.6],"h":1},{"t":28,"s":[-16.8,26.9],"h":1},{"t":29,"s":[-14.2,20.4],"h":1},{"t":30,"s":[-11.5,15.1],"h":1},{"t":31,"s":[-8.8,10.8],"h":1},{"t":32,"s":[-6.4,7.4],"h":1},{"t":33,"s":[-4.3,4.7],"h":1},{"t":34,"s":[-2.6,2.7],"h":1},{"t":35,"s":[-1.3,1.3],"h":1},{"t":36,"s":[-0.5,0.5],"h":1},{"t":37,"s":[0,0],"h":1},{"t":38,"s":[0.1,-0.1],"h":1},{"t":39,"s":[0.5,-0.5],"h":1},{"t":40,"s":[1.4,-1.4],"h":1},{"t":41,"s":[2.9,-2.7],"h":1},{"t":42,"s":[4.9,-4.4],"h":1},{"t":43,"s":[7.6,-6.6],"h":1},{"t":44,"s":[11.1,-9],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[15.8],"h":1},{"t":1,"s":[20.7],"h":1},{"t":2,"s":[26.4],"h":1},{"t":3,"s":[32.9],"h":1},{"t":4,"s":[40.3],"h":1},{"t":5,"s":[48.7],"h":1},{"t":6,"s":[58],"h":1},{"t":7,"s":[68.2],"h":1},{"t":8,"s":[79.5],"h":1},{"t":9,"s":[91.7],"h":1},{"t":10,"s":[104.8],"h":1},{"t":11,"s":[118.8],"h":1},{"t":12,"s":[133.5],"h":1},{"t":13,"s":[148.9],"h":1},{"t":14,"s":[164.6],"h":1},{"t":15,"s":[180.6],"h":1},{"t":16,"s":[196.6],"h":1},{"t":17,"s":[212.3],"h":1},{"t":18,"s":[227.6],"h":1},{"t":19,"s":[242.3],"h":1},{"t":20,"s":[256.2],"h":1},{"t":21,"s":[269.3],"h":1},{"t":22,"s":[281.4],"h":1},{"t":23,"s":[292.6],"h":1},{"t":24,"s":[302.8],"h":1},{"t":25,"s":[312],"h":1},{"t":26,"s":[320.2],"h":1},{"t":27,"s":[327.6],"h":1},{"t":28,"s":[334.1],"h":1},{"t":29,"s":[339.7],"h":1},{"t":30,"s":[344.6],"h":1},{"t":31,"s":[348.7],"h":1},{"t":32,"s":[352.1],"h":1},{"t":33,"s":[354.8],"h":1},{"t":34,"s":[357],"h":1},{"t":35,"s":[358.5],"h":1},{"t":36,"s":[359.5],"h":1},{"t":37,"s":[359.9],"h":1},{"t":38,"s":[0.1],"h":1},{"t":39,"s":[0.6],"h":1},{"t":40,"s":[1.6],"h":1},{"t":41,"s":[3.2],"h":1},{"t":42,"s":[5.4],"h":1},{"t":43,"s":[8.1],"h":1},{"t":44,"s":[11.6],"h":1}]}},"ind":11,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[49.1,79.7],[44.6,82.3],[43.1,79.7],[47.6,77.1],[49.1,79.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-157.4]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[49.1,79.7],[44.6,82.3],[43.1,79.7],[47.6,77.1],[49.1,79.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-157.4]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.5,0.6,0.8],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.5,0.6,0.8],"h":1}]}}],"ind":12,"sr":1,"parent":11},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[7.9,-6.8],"h":1},{"t":1,"s":[11.5,-9.3],"h":1},{"t":2,"s":[16,-12],"h":1},{"t":3,"s":[21.5,-14.7],"h":1},{"t":4,"s":[28.2,-17.3],"h":1},{"t":5,"s":[36.1,-19.3],"h":1},{"t":6,"s":[45.2,-20.6],"h":1},{"t":7,"s":[55.6,-20.5],"h":1},{"t":8,"s":[67.1,-18.6],"h":1},{"t":9,"s":[79.1,-14.4],"h":1},{"t":10,"s":[91.2,-7.4],"h":1},{"t":11,"s":[102.5,2.7],"h":1},{"t":12,"s":[112,15.9],"h":1},{"t":13,"s":[118.4,32],"h":1},{"t":14,"s":[120.7,50.1],"h":1},{"t":15,"s":[118.2,68.8],"h":1},{"t":16,"s":[110.5,86.7],"h":1},{"t":17,"s":[98,101.9],"h":1},{"t":18,"s":[81.9,113.1],"h":1},{"t":19,"s":[63.7,119.4],"h":1},{"t":20,"s":[45,120.5],"h":1},{"t":21,"s":[27.3,117],"h":1},{"t":22,"s":[12,109.6],"h":1},{"t":23,"s":[-0.5,99.5],"h":1},{"t":24,"s":[-9.7,87.9],"h":1},{"t":25,"s":[-15.9,75.8],"h":1},{"t":26,"s":[-19.3,63.8],"h":1},{"t":27,"s":[-20.7,52.7],"h":1},{"t":28,"s":[-20.3,42.6],"h":1},{"t":29,"s":[-18.8,33.8],"h":1},{"t":30,"s":[-16.6,26.2],"h":1},{"t":31,"s":[-14,19.9],"h":1},{"t":32,"s":[-11.2,14.6],"h":1},{"t":33,"s":[-8.6,10.4],"h":1},{"t":34,"s":[-6.2,7.1],"h":1},{"t":35,"s":[-4.1,4.5],"h":1},{"t":36,"s":[-2.4,2.6],"h":1},{"t":37,"s":[-1.2,1.2],"h":1},{"t":38,"s":[-0.4,0.4],"h":1},{"t":39,"s":[0,0],"h":1},{"t":40,"s":[0.1,-0.1],"h":1},{"t":41,"s":[0.6,-0.6],"h":1},{"t":42,"s":[1.5,-1.5],"h":1},{"t":43,"s":[3,-2.9],"h":1},{"t":44,"s":[5.1,-4.6],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[8.5],"h":1},{"t":1,"s":[12],"h":1},{"t":2,"s":[16.2],"h":1},{"t":3,"s":[21.2],"h":1},{"t":4,"s":[27],"h":1},{"t":5,"s":[33.6],"h":1},{"t":6,"s":[41.1],"h":1},{"t":7,"s":[49.6],"h":1},{"t":8,"s":[59],"h":1},{"t":9,"s":[69.3],"h":1},{"t":10,"s":[80.7],"h":1},{"t":11,"s":[93],"h":1},{"t":12,"s":[106.2],"h":1},{"t":13,"s":[120.3],"h":1},{"t":14,"s":[135.1],"h":1},{"t":15,"s":[150.4],"h":1},{"t":16,"s":[166.2],"h":1},{"t":17,"s":[182.2],"h":1},{"t":18,"s":[198.2],"h":1},{"t":19,"s":[213.8],"h":1},{"t":20,"s":[229.1],"h":1},{"t":21,"s":[243.7],"h":1},{"t":22,"s":[257.5],"h":1},{"t":23,"s":[270.5],"h":1},{"t":24,"s":[282.6],"h":1},{"t":25,"s":[293.6],"h":1},{"t":26,"s":[303.7],"h":1},{"t":27,"s":[312.8],"h":1},{"t":28,"s":[321],"h":1},{"t":29,"s":[328.3],"h":1},{"t":30,"s":[334.7],"h":1},{"t":31,"s":[340.2],"h":1},{"t":32,"s":[345],"h":1},{"t":33,"s":[349],"h":1},{"t":34,"s":[352.4],"h":1},{"t":35,"s":[355.1],"h":1},{"t":36,"s":[357.1],"h":1},{"t":37,"s":[358.6],"h":1},{"t":38,"s":[359.5],"h":1},{"t":39,"s":[360],"h":1},{"t":40,"s":[0.1],"h":1},{"t":41,"s":[0.7],"h":1},{"t":42,"s":[1.7],"h":1},{"t":43,"s":[3.4],"h":1},{"t":44,"s":[5.6],"h":1}]}},"ind":13,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[56.9,79.7],[52.4,82.3],[50.9,79.7],[55.4,77.1],[56.9,79.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-157.4]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[56.9,79.7],[52.4,82.3],[50.9,79.7],[55.4,77.1],[56.9,79.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-157.4]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.4,0.5,0.7],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.4,0.5,0.7],"h":1}]}}],"ind":14,"sr":1,"parent":13},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[3.3,-3.1],"h":1},{"t":1,"s":[5.4,-4.9],"h":1},{"t":2,"s":[8.3,-7.1],"h":1},{"t":3,"s":[12,-9.6],"h":1},{"t":4,"s":[16.6,-12.3],"h":1},{"t":5,"s":[22.3,-15.1],"h":1},{"t":6,"s":[29.1,-17.6],"h":1},{"t":7,"s":[37.2,-19.5],"h":1},{"t":8,"s":[46.5,-20.6],"h":1},{"t":9,"s":[57.1,-20.4],"h":1},{"t":10,"s":[68.6,-18.2],"h":1},{"t":11,"s":[80.7,-13.7],"h":1},{"t":12,"s":[92.8,-6.3],"h":1},{"t":13,"s":[103.9,4.2],"h":1},{"t":14,"s":[113,17.9],"h":1},{"t":15,"s":[118.9,34.3],"h":1},{"t":16,"s":[120.7,52.5],"h":1},{"t":17,"s":[117.4,71.3],"h":1},{"t":18,"s":[109.1,88.8],"h":1},{"t":19,"s":[96.1,103.6],"h":1},{"t":20,"s":[79.6,114.2],"h":1},{"t":21,"s":[61.2,119.8],"h":1},{"t":22,"s":[42.6,120.3],"h":1},{"t":23,"s":[25.2,116.2],"h":1},{"t":24,"s":[10.2,108.4],"h":1},{"t":25,"s":[-1.8,98.1],"h":1},{"t":26,"s":[-10.7,86.3],"h":1},{"t":27,"s":[-16.5,74.2],"h":1},{"t":28,"s":[-19.6,62.3],"h":1},{"t":29,"s":[-20.7,51.3],"h":1},{"t":30,"s":[-20.2,41.4],"h":1},{"t":31,"s":[-18.6,32.7],"h":1},{"t":32,"s":[-16.3,25.3],"h":1},{"t":33,"s":[-13.6,19.1],"h":1},{"t":34,"s":[-10.9,14],"h":1},{"t":35,"s":[-8.3,9.9],"h":1},{"t":36,"s":[-5.9,6.7],"h":1},{"t":37,"s":[-3.9,4.2],"h":1},{"t":38,"s":[-2.3,2.4],"h":1},{"t":39,"s":[-1.1,1.1],"h":1},{"t":40,"s":[-0.3,0.3],"h":1},{"t":41,"s":[0,0],"h":1},{"t":42,"s":[0.1,-0.1],"h":1},{"t":43,"s":[0.7,-0.7],"h":1},{"t":44,"s":[1.7,-1.6],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[3.6],"h":1},{"t":1,"s":[5.9],"h":1},{"t":2,"s":[8.9],"h":1},{"t":3,"s":[12.5],"h":1},{"t":4,"s":[16.8],"h":1},{"t":5,"s":[21.9],"h":1},{"t":6,"s":[27.8],"h":1},{"t":7,"s":[34.6],"h":1},{"t":8,"s":[42.2],"h":1},{"t":9,"s":[50.7],"h":1},{"t":10,"s":[60.3],"h":1},{"t":11,"s":[70.8],"h":1},{"t":12,"s":[82.2],"h":1},{"t":13,"s":[94.6],"h":1},{"t":14,"s":[108],"h":1},{"t":15,"s":[122.2],"h":1},{"t":16,"s":[137],"h":1},{"t":17,"s":[152.5],"h":1},{"t":18,"s":[168.3],"h":1},{"t":19,"s":[184.3],"h":1},{"t":20,"s":[200.2],"h":1},{"t":21,"s":[215.9],"h":1},{"t":22,"s":[231],"h":1},{"t":23,"s":[245.6],"h":1},{"t":24,"s":[259.3],"h":1},{"t":25,"s":[272.2],"h":1},{"t":26,"s":[284.1],"h":1},{"t":27,"s":[295],"h":1},{"t":28,"s":[305],"h":1},{"t":29,"s":[314],"h":1},{"t":30,"s":[322],"h":1},{"t":31,"s":[329.2],"h":1},{"t":32,"s":[335.4],"h":1},{"t":33,"s":[340.9],"h":1},{"t":34,"s":[345.6],"h":1},{"t":35,"s":[349.5],"h":1},{"t":36,"s":[352.8],"h":1},{"t":37,"s":[355.4],"h":1},{"t":38,"s":[357.4],"h":1},{"t":39,"s":[358.8],"h":1},{"t":40,"s":[359.6],"h":1},{"t":41,"s":[360],"h":1},{"t":42,"s":[0.1],"h":1},{"t":43,"s":[0.8],"h":1},{"t":44,"s":[1.9],"h":1}]}},"ind":15,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[64.5,77.7],[60,80.3],[58.5,77.7],[63,75.1],[64.5,77.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-153.4]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[64.5,77.7],[60,80.3],[58.5,77.7],[63,75.1],[64.5,77.7]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-153.4]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.3,0.5,0.7],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.3,0.5,0.7],"h":1}]}}],"ind":16,"sr":1,"parent":15},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[0.8,-0.7],"h":1},{"t":1,"s":[1.8,-1.8],"h":1},{"t":2,"s":[3.5,-3.2],"h":1},{"t":3,"s":[5.7,-5.1],"h":1},{"t":4,"s":[8.7,-7.4],"h":1},{"t":5,"s":[12.4,-9.9],"h":1},{"t":6,"s":[17.1,-12.6],"h":1},{"t":7,"s":[22.9,-15.3],"h":1},{"t":8,"s":[29.9,-17.8],"h":1},{"t":9,"s":[38.1,-19.7],"h":1},{"t":10,"s":[47.5,-20.7],"h":1},{"t":11,"s":[58.2,-20.2],"h":1},{"t":12,"s":[69.8,-17.9],"h":1},{"t":13,"s":[81.9,-13.1],"h":1},{"t":14,"s":[93.9,-5.4],"h":1},{"t":15,"s":[104.9,5.4],"h":1},{"t":16,"s":[113.8,19.4],"h":1},{"t":17,"s":[119.3,36.1],"h":1},{"t":18,"s":[120.6,54.4],"h":1},{"t":19,"s":[116.8,73.1],"h":1},{"t":20,"s":[108,90.5],"h":1},{"t":21,"s":[94.6,104.9],"h":1},{"t":22,"s":[77.8,115],"h":1},{"t":23,"s":[59.3,120.1],"h":1},{"t":24,"s":[40.7,120.1],"h":1},{"t":25,"s":[23.5,115.6],"h":1},{"t":26,"s":[8.8,107.5],"h":1},{"t":27,"s":[-2.9,97],"h":1},{"t":28,"s":[-11.4,85.1],"h":1},{"t":29,"s":[-16.9,73],"h":1},{"t":30,"s":[-19.8,61.2],"h":1},{"t":31,"s":[-20.7,50.2],"h":1},{"t":32,"s":[-20.1,40.4],"h":1},{"t":33,"s":[-18.4,31.9],"h":1},{"t":34,"s":[-16,24.6],"h":1},{"t":35,"s":[-13.3,18.5],"h":1},{"t":36,"s":[-10.6,13.6],"h":1},{"t":37,"s":[-8,9.6],"h":1},{"t":38,"s":[-5.7,6.4],"h":1},{"t":39,"s":[-3.7,4],"h":1},{"t":40,"s":[-2.1,2.2],"h":1},{"t":41,"s":[-1,1],"h":1},{"t":42,"s":[-0.3,0.3],"h":1},{"t":43,"s":[0,0],"h":1},{"t":44,"s":[0.2,-0.2],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[0.9],"h":1},{"t":1,"s":[2.1],"h":1},{"t":2,"s":[3.8],"h":1},{"t":3,"s":[6.2],"h":1},{"t":4,"s":[9.2],"h":1},{"t":5,"s":[12.9],"h":1},{"t":6,"s":[17.3],"h":1},{"t":7,"s":[22.5],"h":1},{"t":8,"s":[28.5],"h":1},{"t":9,"s":[35.3],"h":1},{"t":10,"s":[43],"h":1},{"t":11,"s":[51.7],"h":1},{"t":12,"s":[61.3],"h":1},{"t":13,"s":[71.9],"h":1},{"t":14,"s":[83.4],"h":1},{"t":15,"s":[95.9],"h":1},{"t":16,"s":[109.4],"h":1},{"t":17,"s":[123.6],"h":1},{"t":18,"s":[138.6],"h":1},{"t":19,"s":[154.1],"h":1},{"t":20,"s":[169.9],"h":1},{"t":21,"s":[185.9],"h":1},{"t":22,"s":[201.8],"h":1},{"t":23,"s":[217.4],"h":1},{"t":24,"s":[232.5],"h":1},{"t":25,"s":[247],"h":1},{"t":26,"s":[260.6],"h":1},{"t":27,"s":[273.4],"h":1},{"t":28,"s":[285.2],"h":1},{"t":29,"s":[296.1],"h":1},{"t":30,"s":[305.9],"h":1},{"t":31,"s":[314.8],"h":1},{"t":32,"s":[322.8],"h":1},{"t":33,"s":[329.8],"h":1},{"t":34,"s":[336],"h":1},{"t":35,"s":[341.4],"h":1},{"t":36,"s":[346],"h":1},{"t":37,"s":[349.9],"h":1},{"t":38,"s":[353.1],"h":1},{"t":39,"s":[355.6],"h":1},{"t":40,"s":[357.5],"h":1},{"t":41,"s":[358.9],"h":1},{"t":42,"s":[359.7],"h":1},{"t":43,"s":[360],"h":1},{"t":44,"s":[0.2],"h":1}]}},"ind":17,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[71.3,73.8],[66.8,76.4],[65.3,73.8],[69.8,71.2],[71.3,73.8]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-145.5]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[71.3,73.8],[66.8,76.4],[65.3,73.8],[69.8,71.2],[71.3,73.8]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-145.5]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0.1,0.4,0.7],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0.1,0.4,0.7],"h":1}]}}],"ind":18,"sr":1,"parent":17},{"ty":3,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[0,0],"h":1},{"t":1,"s":[0.2,-0.2],"h":1},{"t":2,"s":[0.9,-0.9],"h":1},{"t":3,"s":[2,-1.9],"h":1},{"t":4,"s":[3.7,-3.5],"h":1},{"t":5,"s":[6,-5.4],"h":1},{"t":6,"s":[9.1,-7.7],"h":1},{"t":7,"s":[13,-10.3],"h":1},{"t":8,"s":[17.8,-13],"h":1},{"t":9,"s":[23.8,-15.7],"h":1},{"t":10,"s":[30.9,-18.1],"h":1},{"t":11,"s":[39.2,-19.9],"h":1},{"t":12,"s":[48.9,-20.7],"h":1},{"t":13,"s":[59.7,-20],"h":1},{"t":14,"s":[71.4,-17.4],"h":1},{"t":15,"s":[83.5,-12.3],"h":1},{"t":16,"s":[95.5,-4.2],"h":1},{"t":17,"s":[106.2,7.1],"h":1},{"t":18,"s":[114.7,21.5],"h":1},{"t":19,"s":[119.7,38.4],"h":1},{"t":20,"s":[120.4,56.9],"h":1},{"t":21,"s":[116,75.5],"h":1},{"t":22,"s":[106.5,92.6],"h":1},{"t":23,"s":[92.6,106.5],"h":1},{"t":24,"s":[75.5,116],"h":1},{"t":25,"s":[56.9,120.4],"h":1},{"t":26,"s":[38.4,119.7],"h":1},{"t":27,"s":[21.5,114.7],"h":1},{"t":28,"s":[7.1,106.2],"h":1},{"t":29,"s":[-4.2,95.5],"h":1},{"t":30,"s":[-12.3,83.5],"h":1},{"t":31,"s":[-17.4,71.4],"h":1},{"t":32,"s":[-20,59.7],"h":1},{"t":33,"s":[-20.7,48.9],"h":1},{"t":34,"s":[-19.9,39.2],"h":1},{"t":35,"s":[-18.1,30.9],"h":1},{"t":36,"s":[-15.7,23.8],"h":1},{"t":37,"s":[-13,17.8],"h":1},{"t":38,"s":[-10.3,13],"h":1},{"t":39,"s":[-7.7,9.1],"h":1},{"t":40,"s":[-5.4,6],"h":1},{"t":41,"s":[-3.5,3.7],"h":1},{"t":42,"s":[-1.9,2],"h":1},{"t":43,"s":[-0.9,0.9],"h":1},{"t":44,"s":[-0.2,0.2],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"h":1},{"t":1,"s":[100,100],"h":1},{"t":2,"s":[100,100],"h":1},{"t":3,"s":[100,100],"h":1},{"t":4,"s":[100,100],"h":1},{"t":5,"s":[100,100],"h":1},{"t":6,"s":[100,100],"h":1},{"t":7,"s":[100,100],"h":1},{"t":8,"s":[100,100],"h":1},{"t":9,"s":[100,100],"h":1},{"t":10,"s":[100,100],"h":1},{"t":11,"s":[100,100],"h":1},{"t":12,"s":[100,100],"h":1},{"t":13,"s":[100,100],"h":1},{"t":14,"s":[100,100],"h":1},{"t":15,"s":[100,100],"h":1},{"t":16,"s":[100,100],"h":1},{"t":17,"s":[100,100],"h":1},{"t":18,"s":[100,100],"h":1},{"t":19,"s":[100,100],"h":1},{"t":20,"s":[100,100],"h":1},{"t":21,"s":[100,100],"h":1},{"t":22,"s":[100,100],"h":1},{"t":23,"s":[100,100],"h":1},{"t":24,"s":[100,100],"h":1},{"t":25,"s":[100,100],"h":1},{"t":26,"s":[100,100],"h":1},{"t":27,"s":[100,100],"h":1},{"t":28,"s":[100,100],"h":1},{"t":29,"s":[100,100],"h":1},{"t":30,"s":[100,100],"h":1},{"t":31,"s":[100,100],"h":1},{"t":32,"s":[100,100],"h":1},{"t":33,"s":[100,100],"h":1},{"t":34,"s":[100,100],"h":1},{"t":35,"s":[100,100],"h":1},{"t":36,"s":[100,100],"h":1},{"t":37,"s":[100,100],"h":1},{"t":38,"s":[100,100],"h":1},{"t":39,"s":[100,100],"h":1},{"t":40,"s":[100,100],"h":1},{"t":41,"s":[100,100],"h":1},{"t":42,"s":[100,100],"h":1},{"t":43,"s":[100,100],"h":1},{"t":44,"s":[100,100],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":1,"s":[0.2],"h":1},{"t":2,"s":[1],"h":1},{"t":3,"s":[2.3],"h":1},{"t":4,"s":[4.1],"h":1},{"t":5,"s":[6.6],"h":1},{"t":6,"s":[9.7],"h":1},{"t":7,"s":[13.4],"h":1},{"t":8,"s":[17.9],"h":1},{"t":9,"s":[23.2],"h":1},{"t":10,"s":[29.3],"h":1},{"t":11,"s":[36.2],"h":1},{"t":12,"s":[44.1],"h":1},{"t":13,"s":[52.9],"h":1},{"t":14,"s":[62.6],"h":1},{"t":15,"s":[73.3],"h":1},{"t":16,"s":[85],"h":1},{"t":17,"s":[97.7],"h":1},{"t":18,"s":[111.2],"h":1},{"t":19,"s":[125.5],"h":1},{"t":20,"s":[140.6],"h":1},{"t":21,"s":[156.1],"h":1},{"t":22,"s":[172],"h":1},{"t":23,"s":[188],"h":1},{"t":24,"s":[203.9],"h":1},{"t":25,"s":[219.4],"h":1},{"t":26,"s":[234.5],"h":1},{"t":27,"s":[248.8],"h":1},{"t":28,"s":[262.3],"h":1},{"t":29,"s":[275],"h":1},{"t":30,"s":[286.7],"h":1},{"t":31,"s":[297.4],"h":1},{"t":32,"s":[307.1],"h":1},{"t":33,"s":[315.9],"h":1},{"t":34,"s":[323.8],"h":1},{"t":35,"s":[330.7],"h":1},{"t":36,"s":[336.8],"h":1},{"t":37,"s":[342.1],"h":1},{"t":38,"s":[346.6],"h":1},{"t":39,"s":[350.3],"h":1},{"t":40,"s":[353.4],"h":1},{"t":41,"s":[355.9],"h":1},{"t":42,"s":[357.7],"h":1},{"t":43,"s":[359],"h":1},{"t":44,"s":[359.8],"h":1}]}},"ind":19,"sr":1,"parent":3},{"ddd":0,"ty":4,"st":0,"ip":0,"op":45,"w":100,"h":100,"ks":{"ty":"tr"},"shapes":[{"ty":"sh","ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[76.8,68.3],[72.3,70.9],[70.8,68.3],[75.3,65.7],[76.8,68.3]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-134.5]]}],"h":1},{"t":44,"s":[{"c":false,"v":[[76.8,68.3],[72.3,70.9],[70.8,68.3],[75.3,65.7],[76.8,68.3]],"i":[[0,0],[2,1.2],[0,1.1],[-2,-1.2],[0,-1.1]],"o":[[0,2.3],[-0.9,-0.5],[0,-2.3],[0.9,0.5],[-0.6,-134.5]]}],"h":1}]}},{"ty":"fl","r":1,"o":{"a":1,"k":[{"t":0,"s":[100],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[100],"h":1}]},"c":{"a":1,"k":[{"t":0,"s":[0,0.3,0.7],"h":1,"o":{"x":[0],"y":[0]},"i":{"x":[1],"y":[1]}},{"t":44,"s":[0,0.3,0.7],"h":1}]}}],"ind":20,"sr":1,"parent":19}]} \ No newline at end of file