├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── clwater │ │ └── compose_canvas │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── clwater │ │ │ └── compose_canvas │ │ │ ├── MainActivity.kt │ │ │ ├── bezier │ │ │ ├── BezierActivity.kt │ │ │ ├── BezierPoint.kt │ │ │ └── BezierViewModel.kt │ │ │ ├── clap │ │ │ └── ClapActivity.kt │ │ │ ├── shape │ │ │ ├── ShapeActivity.kt │ │ │ └── tmp │ │ │ │ ├── GradientAlongPathAnimation.kt │ │ │ │ ├── ShapeActivity.bak │ │ │ │ └── ShapeComposable.kt │ │ │ ├── sun_moon │ │ │ └── Canvas1Activity.kt │ │ │ ├── tree │ │ │ └── TreeActivity.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── avatar.jpg │ │ ├── bezier.png │ │ ├── ic_launcher_background.xml │ │ ├── icon_hand_fill.xml │ │ ├── icon_hand_outline.xml │ │ ├── shape.png │ │ ├── sun_moon.png │ │ └── tree.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-anydpi-v33 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── clwater │ └── compose_canvas │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | # Built application files 3 | *.apk 4 | *.aar 5 | *.ap_ 6 | *.aab 7 | 8 | # Files for the ART/Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | out/ 18 | # Uncomment the following line in case you need and you don't have the release build type files in your app 19 | # release/ 20 | 21 | # Gradle files 22 | .gradle/ 23 | build/ 24 | 25 | # Local configuration file (sdk path, etc) 26 | local.properties 27 | 28 | # Proguard folder generated by Eclipse 29 | proguard/ 30 | 31 | # Log Files 32 | *.log 33 | 34 | # Android Studio Navigation editor temp files 35 | .navigation/ 36 | 37 | # Android Studio captures folder 38 | captures/ 39 | 40 | # IntelliJ 41 | *.iml 42 | .idea/workspace.xml 43 | .idea/tasks.xml 44 | .idea/gradle.xml 45 | .idea/assetWizardSettings.xml 46 | .idea/dictionaries 47 | .idea/libraries 48 | # Android Studio 3 in .gitignore file. 49 | .idea/caches 50 | .idea/modules.xml 51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 52 | .idea/navEditor.xml 53 | 54 | # Keystore files 55 | # Uncomment the following lines if you do not want to check your keystore files in. 56 | #*.jks 57 | #*.keystore 58 | 59 | # External native build folder generated in Android Studio 2.2 and later 60 | .externalNativeBuild 61 | .cxx/ 62 | 63 | # Google Services (e.g. APIs or Firebase) 64 | # google-services.json 65 | 66 | # Freeline 67 | freeline.py 68 | freeline/ 69 | freeline_project_description.json 70 | 71 | # fastlane 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots 75 | fastlane/test_output 76 | fastlane/readme.md 77 | 78 | # Version control 79 | vcs.xml 80 | 81 | # lint 82 | lint/intermediates/ 83 | lint/generated/ 84 | lint/outputs/ 85 | lint/tmp/ 86 | # lint/reports/ 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidComposeCanvas 2 | 3 | 4 | |||| 5 | |-|-|-| 6 | |Sun_Moon Button |[path](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/sun_moon)|| 7 | |Bezier Curve|[path](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/bezier)|| 8 | |Tree|[path](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/tree)|| 9 | |clap|[path](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/clap)|| 10 | |shape|[path](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/shape)|| 11 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.clwater.compose_canvas' 8 | compileSdk 34 9 | 10 | defaultConfig { 11 | applicationId "com.clwater.compose_canvas" 12 | minSdk 26 13 | targetSdk 34 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.2.0' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.7.0' 52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 53 | implementation 'androidx.activity:activity-compose:1.3.1' 54 | implementation "androidx.compose.ui:ui:$compose_version" 55 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 56 | implementation 'androidx.compose.material3:material3:1.0.0-alpha11' 57 | implementation 'androidx.interpolator:interpolator:1.0.0' 58 | implementation 'androidx.compose.ui:ui-android:1.6.2' 59 | testImplementation 'junit:junit:4.13.2' 60 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 62 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 63 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 64 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 65 | implementation "androidx.graphics:graphics-shapes:1.0.0-alpha05" 66 | implementation 'androidx.compose.animation:animation-graphics' 67 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/clwater/compose_canvas/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.clwater.compose_canvas", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Text 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.unit.dp 18 | import com.clwater.compose_canvas.bezier.BezierActivity 19 | import com.clwater.compose_canvas.clap.ClapActivity 20 | import com.clwater.compose_canvas.shape.ShapeActivity 21 | import com.clwater.compose_canvas.sun_moon.Canvas1Activity 22 | import com.clwater.compose_canvas.tree.TreeActivity 23 | import com.clwater.compose_canvas.ui.theme.AndroidComposeCanvasTheme 24 | 25 | class MainActivity : ComponentActivity() { 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | // ShapeActivity.start(this@MainActivity) 28 | super.onCreate(savedInstanceState) 29 | setContent { 30 | AndroidComposeCanvasTheme { 31 | // A surface container using the 'background' color from the theme 32 | Surface( 33 | modifier = Modifier.fillMaxSize(), 34 | color = MaterialTheme.colorScheme.background 35 | ) { 36 | Column( 37 | modifier = Modifier.padding(12.dp), 38 | ) { 39 | Row(modifier = Modifier.height(100.dp)) { 40 | Button( 41 | modifier = Modifier 42 | .weight(1f) 43 | .align(Alignment.CenterVertically), 44 | onClick = { Canvas1Activity.start(this@MainActivity) }) { 45 | Text(text = "Sun Moon") 46 | } 47 | Image( 48 | modifier = Modifier.weight(2f), 49 | painter = painterResource(id = R.drawable.sun_moon), 50 | contentDescription = "" 51 | ) 52 | } 53 | 54 | Spacer( 55 | modifier = Modifier 56 | .height(1.dp) 57 | .fillMaxWidth() 58 | .background(Color.Gray) 59 | ) 60 | 61 | Row(modifier = Modifier.height(100.dp)) { 62 | Button( 63 | modifier = Modifier 64 | .weight(1f) 65 | .align(Alignment.CenterVertically), 66 | onClick = { BezierActivity.start(this@MainActivity) }) { 67 | Text(text = "Bezier") 68 | } 69 | Image( 70 | modifier = Modifier.weight(2f), 71 | painter = painterResource(id = R.drawable.bezier), 72 | contentDescription = "" 73 | ) 74 | } 75 | Spacer( 76 | modifier = Modifier 77 | .height(1.dp) 78 | .fillMaxWidth() 79 | .background(Color.Gray) 80 | ) 81 | 82 | Row(modifier = Modifier.height(100.dp)) { 83 | Button( 84 | modifier = Modifier 85 | .weight(1f) 86 | .align(Alignment.CenterVertically), 87 | onClick = { ClapActivity.start(this@MainActivity) }) { 88 | Text(text = "Clap") 89 | } 90 | Image( 91 | modifier = Modifier 92 | .weight(2f) 93 | .align(Alignment.CenterVertically), 94 | painter = painterResource(id = R.drawable.icon_hand_fill), 95 | contentDescription = "" 96 | ) 97 | } 98 | Spacer( 99 | modifier = Modifier 100 | .height(1.dp) 101 | .fillMaxWidth() 102 | .background(Color.Gray) 103 | ) 104 | 105 | Row(modifier = Modifier.height(100.dp)) { 106 | Button( 107 | modifier = Modifier 108 | .weight(1f) 109 | .align(Alignment.CenterVertically), 110 | onClick = { TreeActivity.start(this@MainActivity) }) { 111 | Text(text = "Tree") 112 | } 113 | Image( 114 | modifier = Modifier.weight(2f), 115 | painter = painterResource(id = R.drawable.tree), 116 | contentDescription = "" 117 | ) 118 | } 119 | Spacer( 120 | modifier = Modifier 121 | .height(1.dp) 122 | .fillMaxWidth() 123 | .background(Color.Gray) 124 | ) 125 | 126 | Row(modifier = Modifier.height(100.dp)) { 127 | Button( 128 | modifier = Modifier 129 | .weight(1f) 130 | .align(Alignment.CenterVertically), 131 | onClick = { ShapeActivity.start(this@MainActivity) }) { 132 | Text(text = "Shape") 133 | } 134 | Image( 135 | modifier = Modifier.weight(2f), 136 | painter = painterResource(id = R.drawable.shape), 137 | contentDescription = "" 138 | ) 139 | } 140 | Spacer( 141 | modifier = Modifier 142 | .height(1.dp) 143 | .fillMaxWidth() 144 | .background(Color.Gray) 145 | ) 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/bezier/BezierActivity.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.bezier 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Paint 6 | import android.os.Bundle 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.activity.viewModels 10 | import androidx.compose.foundation.Canvas 11 | import androidx.compose.foundation.border 12 | import androidx.compose.foundation.gestures.detectDragGestures 13 | import androidx.compose.foundation.gestures.detectTapGestures 14 | import androidx.compose.foundation.layout.* 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.* 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.geometry.Offset 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.RectangleShape 23 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 24 | import androidx.compose.ui.graphics.nativeCanvas 25 | import androidx.compose.ui.graphics.toArgb 26 | import androidx.compose.ui.input.pointer.pointerInput 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | import com.clwater.compose_canvas.ui.theme.AndroidComposeCanvasTheme 30 | import kotlin.math.roundToInt 31 | 32 | class BezierActivity : ComponentActivity() { 33 | companion object { 34 | fun start(context: Context) { 35 | context.startActivity(Intent(context, BezierActivity::class.java)) 36 | } 37 | } 38 | 39 | private val mPointRadius = 15.dp 40 | private val mLineWidth = 10.dp 41 | private val mTextSize = 16.sp 42 | 43 | private val model by viewModels() 44 | 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | setContent { 48 | AndroidComposeCanvasTheme { 49 | // A surface container using the 'background' color from the theme 50 | Surface( 51 | modifier = Modifier.fillMaxSize(), 52 | color = MaterialTheme.colorScheme.background 53 | ) { 54 | DefaultView() 55 | } 56 | } 57 | } 58 | } 59 | 60 | @Composable 61 | fun DefaultView() { 62 | Column(modifier = Modifier.padding(12.dp)) { 63 | ControlView() 64 | BezierView() 65 | } 66 | } 67 | 68 | @Composable 69 | fun BezierView() { 70 | model.clear() 71 | Canvas( 72 | modifier = Modifier 73 | .border(width = 1.dp, color = Color.Black, shape = RectangleShape) 74 | .fillMaxSize() 75 | .pointerInput(Unit) { 76 | detectDragGestures( 77 | onDragStart = { 78 | model.pointDragStart(it) 79 | }, 80 | onDragEnd = { 81 | model.pointDragEnd() 82 | } 83 | ) { _, dragAmount -> 84 | model.pointDragProgress(dragAmount) 85 | } 86 | } 87 | .pointerInput(Unit) { 88 | detectTapGestures { 89 | model.addPoint(it.x, it.y) 90 | } 91 | } 92 | ) { 93 | lateinit var preBezierPoint: BezierPoint 94 | val paint = Paint() 95 | paint.textSize = mTextSize.toPx() 96 | 97 | for (pointList in model.mBezierDrawPoints) { 98 | if (pointList == model.mBezierDrawPoints.first() || 99 | (model.mInAuxiliary.value && !model.mInChange.value) 100 | ) { 101 | for (point in pointList) { 102 | if (point != pointList.first()) { 103 | drawLine( 104 | color = Color(point.color), 105 | start = Offset(point.x.value, point.y.value), 106 | end = Offset(preBezierPoint.x.value, preBezierPoint.y.value), 107 | strokeWidth = mLineWidth.value 108 | ) 109 | } 110 | preBezierPoint = point 111 | 112 | drawCircle( 113 | color = Color(point.color), 114 | radius = mPointRadius.value, 115 | center = Offset(point.x.value, point.y.value) 116 | ) 117 | paint.color = Color(point.color).toArgb() 118 | drawIntoCanvas { 119 | it.nativeCanvas.drawText( 120 | point.name, 121 | point.x.value - mPointRadius.value, 122 | point.y.value - mPointRadius.value * 1.5f, 123 | paint 124 | ) 125 | } 126 | } 127 | } 128 | } 129 | 130 | for (linePoint in model.mBezierLinePoints.toList()) { 131 | if (linePoint.first <= model.mProgress.value) { 132 | drawCircle( 133 | color = Color.Red, 134 | radius = mPointRadius.value / 2f, 135 | center = Offset(linePoint.second.first, linePoint.second.second) 136 | ) 137 | } 138 | } 139 | } 140 | } 141 | 142 | @Composable 143 | fun ControlView() { 144 | Column() { 145 | Row(modifier = Modifier.fillMaxWidth()) { 146 | Button( 147 | onClick = { model.start() }, 148 | shape = RoundedCornerShape(4.dp), 149 | modifier = Modifier 150 | .padding(8.dp) 151 | .weight(1f), 152 | enabled = !model.mInChange.value 153 | ) { 154 | Text(text = "Start") 155 | } 156 | Button( 157 | onClick = { model.clear() }, 158 | shape = RoundedCornerShape(4.dp), 159 | modifier = Modifier 160 | .padding(8.dp) 161 | .weight(1f), 162 | enabled = !model.mInChange.value 163 | ) { 164 | Text(text = "Clear") 165 | } 166 | } 167 | Row(modifier = Modifier.fillMaxWidth()) { 168 | Button( 169 | onClick = { model.changeMovePoint() }, 170 | shape = RoundedCornerShape(4.dp), 171 | modifier = Modifier 172 | .padding(4.dp) 173 | .weight(1f) 174 | ) { 175 | Text(text = "Change Point ${if (model.mInChange.value){"On"}else {"Off"}}") 176 | } 177 | Button( 178 | onClick = { model.changeAuxiliary() }, 179 | shape = RoundedCornerShape(4.dp), 180 | modifier = Modifier 181 | .padding(4.dp) 182 | .weight(1f), 183 | enabled = !model.mInChange.value 184 | ) { 185 | Text(text = "${if (model.mInAuxiliary.value){"Close"}else {"Open"}} Auxiliary") 186 | } 187 | Button( 188 | onClick = { model.changeMore() }, 189 | shape = RoundedCornerShape(4.dp), 190 | modifier = Modifier 191 | .padding(4.dp) 192 | .weight(1f), 193 | enabled = !model.mInChange.value 194 | ) { 195 | Text(text = "More Point ${if (model.mInMore.value){"On"}else {"Off"}}") 196 | } 197 | } 198 | Row( 199 | verticalAlignment = Alignment.CenterVertically 200 | ) { 201 | Text( 202 | text = "index(${model.mIndexRange.first}~${model.mIndexRange.second}): " + 203 | "${model.mIndex.value}", 204 | modifier = Modifier.weight(1f) 205 | ) 206 | Slider( 207 | modifier = Modifier.weight(2f), 208 | enabled = !model.mInChange.value, 209 | value = model.mIndex.value.toFloat(), 210 | onValueChange = { 211 | model.clear() 212 | model.mIndex.value = it.roundToInt() 213 | }, 214 | steps = (model.mIndexRange.second - model.mIndexRange.first - 1), 215 | valueRange = model.mIndexRange.first.toFloat()..model.mIndexRange.second.toFloat() // ktlint-disable max-line-length 216 | ) 217 | } 218 | Row( 219 | verticalAlignment = Alignment.CenterVertically 220 | ) { 221 | Text( 222 | text = "index(${model.mTimeRange.first}~${model.mTimeRange.second}): " + 223 | "${model.mTime.value}", 224 | modifier = Modifier.weight(1f) 225 | ) 226 | Slider( 227 | modifier = Modifier.weight(2f), 228 | enabled = !model.mInChange.value, 229 | value = model.mTime.value.toFloat(), 230 | onValueChange = { 231 | model.mTime.value = it.roundToInt() 232 | }, 233 | steps = (model.mTimeRange.second - model.mTimeRange.first - 1), 234 | valueRange = model.mTimeRange.first.toFloat()..model.mTimeRange.second.toFloat() 235 | ) 236 | } 237 | 238 | Row( 239 | verticalAlignment = Alignment.CenterVertically 240 | ) { 241 | Text( 242 | text = "progress: ${(model.mProgress.value * 100).roundToInt() / 100f}", 243 | modifier = Modifier.weight(1f) 244 | ) 245 | Slider( 246 | modifier = Modifier.weight(2f), 247 | enabled = !model.mInChange.value, 248 | value = model.mProgress.value, 249 | onValueChange = { 250 | model.mProgress.value = it 251 | model.calculate() 252 | }, 253 | steps = 100 254 | ) 255 | } 256 | } 257 | } 258 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/bezier/BezierPoint.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.bezier 2 | 3 | import androidx.compose.runtime.MutableState 4 | 5 | data class BezierPoint(var x: MutableState, var y: MutableState, var deep: Int, var name: String, var color: Long = 0x7F000000) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/bezier/BezierViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.bezier 2 | 3 | import android.animation.AnimatorSet 4 | import android.animation.ValueAnimator 5 | import android.view.animation.LinearInterpolator 6 | import androidx.compose.runtime.mutableStateListOf 7 | import androidx.compose.runtime.mutableStateMapOf 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.ui.geometry.Offset 10 | import androidx.lifecycle.ViewModel 11 | 12 | class BezierViewModel : ViewModel() { 13 | // max Bezier point size 14 | var mIndex = mutableStateOf(3) 15 | 16 | // animation time 17 | var mTime = mutableStateOf(3) 18 | 19 | // Bezier points(User input) 20 | var mBezierPoints = mutableStateListOf() 21 | 22 | // Bezier points for draw(Parent and Child points) 23 | var mBezierDrawPoints = mutableStateListOf(mutableListOf()) 24 | 25 | // Bezier line progress 26 | var mProgress = mutableStateOf(0f) 27 | 28 | // Bezier line points for draw(real Bezier line points) 29 | var mBezierLinePoints = mutableStateMapOf(Pair(0f, Pair(0f, 0f))) 30 | 31 | // in change mode for change position 32 | var mInChange = mutableStateOf(false) 33 | 34 | // in auxiliary mode for draw auxiliary line 35 | var mInAuxiliary = mutableStateOf(true) 36 | 37 | // in more mode for add more point 38 | var mInMore = mutableStateOf(false) 39 | 40 | // current choose for change position 41 | private var bezierPoint: BezierPoint? = null 42 | 43 | val mIndexRange = Pair(2, 15) 44 | val mTimeRange = Pair(1, 10) 45 | 46 | // deep point text 47 | private val mCharSequence = listOf( 48 | "P", 49 | "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N" 50 | ) 51 | 52 | // deep line colors 53 | private val mColorSequence = listOf( 54 | 0xff000000, 55 | 0xff1BFFF8, 56 | 0xff17FF89, 57 | 0xff25FF2F, 58 | 0xffA7FF05, 59 | 0xffFFE61E, 60 | 0xffFF9B0C, 61 | 0xffFF089F, 62 | 0xffE524FF, 63 | 0xff842BFF, 64 | 0xff090BFF, 65 | 0xff0982FF, 66 | 0xffF8A1FF, 67 | 0xffF7FFA7, 68 | 0xffAAFFEC 69 | ) 70 | 71 | /** 72 | * add point for user input 73 | */ 74 | fun addPoint(x: Float, y: Float) { 75 | val pointSize = mBezierPoints.size 76 | if (pointSize < mIndex.value || mInMore.value) { 77 | mBezierPoints.add( 78 | BezierPoint( 79 | mutableStateOf(x), 80 | mutableStateOf(y), 81 | 0, 82 | "${mCharSequence[0]}$pointSize", 83 | mColorSequence[0] 84 | ) 85 | ) 86 | } 87 | mBezierDrawPoints.clear() 88 | mBezierDrawPoints.add(mBezierPoints) 89 | } 90 | 91 | /** 92 | * clear all info 93 | */ 94 | fun clear() { 95 | mBezierPoints.clear() 96 | mBezierDrawPoints.clear() 97 | mBezierLinePoints.clear() 98 | mProgress.value = 0f 99 | } 100 | 101 | /** 102 | * start animation 103 | */ 104 | fun start() { 105 | val process = ValueAnimator.ofFloat(0f, 1f) 106 | process.addUpdateListener { 107 | mProgress.value = it.animatedValue as Float 108 | calculate() 109 | } 110 | 111 | val set = AnimatorSet() 112 | set.play(process) 113 | set.duration = mTime.value * 1000L 114 | set.interpolator = LinearInterpolator() 115 | set.start() 116 | } 117 | 118 | /** 119 | * calculate Bezier points 120 | */ 121 | fun calculate() { 122 | mBezierDrawPoints.clear() 123 | mBezierDrawPoints.add(mBezierPoints) 124 | calculateBezierPoint(0, mBezierPoints.toList()) 125 | } 126 | 127 | /** 128 | * calculate Bezier line points 129 | */ 130 | private fun calculateBezierPoint(deep: Int, parentList: List) { 131 | if (parentList.size > 1) { 132 | val childList = mutableListOf() 133 | for (i in 0 until parentList.size - 1) { 134 | val point1 = parentList[i] 135 | val point2 = parentList[i + 1] 136 | val x = point1.x.value + (point2.x.value - point1.x.value) * mProgress.value 137 | val y = point1.y.value + (point2.y.value - point1.y.value) * mProgress.value 138 | if (parentList.size == 2) { 139 | mBezierLinePoints[mProgress.value] = Pair(x, y) 140 | return 141 | } else { 142 | val point = BezierPoint( 143 | mutableStateOf(x), 144 | mutableStateOf(y), 145 | deep + 1, 146 | "${mCharSequence.getOrElse(deep + 1){"Z"}}$i", 147 | mColorSequence.getOrElse(deep + 1) { 0xff000000 } 148 | ) 149 | childList.add(point) 150 | } 151 | } 152 | mBezierDrawPoints.add(childList) 153 | calculateBezierPoint(deep + 1, childList) 154 | } else { 155 | return 156 | } 157 | } 158 | 159 | /** 160 | * change point position start, check if have point in range 161 | */ 162 | fun pointDragStart(position: Offset) { 163 | if (!mInChange.value) { 164 | return 165 | } 166 | if (mBezierPoints.isEmpty()) { 167 | return 168 | } 169 | mBezierPoints.firstOrNull() { 170 | position.x > it.x.value - 50 && position.x < it.x.value + 50 && 171 | position.y > it.y.value - 50 && position.y < it.y.value + 50 172 | }.let { 173 | bezierPoint = it 174 | } 175 | } 176 | 177 | /** 178 | * change point position end 179 | */ 180 | fun pointDragEnd() { 181 | bezierPoint = null 182 | } 183 | 184 | /** 185 | * change point position progress 186 | */ 187 | fun pointDragProgress(drag: Offset) { 188 | if (!mInChange.value || bezierPoint == null) { 189 | return 190 | } else { 191 | bezierPoint!!.x.value += drag.x 192 | bezierPoint!!.y.value += drag.y 193 | calculate() 194 | } 195 | } 196 | 197 | fun changeMovePoint() { 198 | mInChange.value = !mInChange.value 199 | if (mInChange.value) { 200 | clearWithOutBasePoint() 201 | mBezierDrawPoints.add(mBezierPoints) 202 | } 203 | } 204 | 205 | fun changeMore() { 206 | clear() 207 | mInMore.value = !mInMore.value 208 | if (mInMore.value) { 209 | mInAuxiliary.value = false 210 | } 211 | } 212 | 213 | fun changeAuxiliary() { 214 | mInAuxiliary.value = !mInAuxiliary.value 215 | } 216 | 217 | /** 218 | * clear all info without base point 219 | */ 220 | private fun clearWithOutBasePoint() { 221 | mBezierDrawPoints.clear() 222 | mBezierLinePoints.clear() 223 | mProgress.value = 0f 224 | } 225 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/clap/ClapActivity.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.clap 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.view.MotionEvent 8 | import android.view.animation.AnticipateOvershootInterpolator 9 | import androidx.activity.ComponentActivity 10 | import androidx.activity.compose.setContent 11 | import androidx.compose.animation.core.Animatable 12 | import androidx.compose.animation.core.tween 13 | import androidx.compose.foundation.Canvas 14 | import androidx.compose.foundation.Image 15 | import androidx.compose.foundation.background 16 | import androidx.compose.foundation.layout.Arrangement 17 | import androidx.compose.foundation.layout.Box 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.foundation.layout.fillMaxWidth 21 | import androidx.compose.foundation.layout.height 22 | import androidx.compose.foundation.layout.offset 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.layout.size 25 | import androidx.compose.foundation.shape.RoundedCornerShape 26 | import androidx.compose.material3.ExperimentalMaterial3Api 27 | import androidx.compose.material3.MaterialTheme 28 | import androidx.compose.material3.Scaffold 29 | import androidx.compose.material3.SnackbarHost 30 | import androidx.compose.material3.SnackbarHostState 31 | import androidx.compose.material3.Surface 32 | import androidx.compose.material3.Text 33 | import androidx.compose.runtime.* 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.ExperimentalComposeUiApi 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.draw.alpha 38 | import androidx.compose.ui.draw.rotate 39 | import androidx.compose.ui.draw.scale 40 | import androidx.compose.ui.geometry.Offset 41 | import androidx.compose.ui.graphics.Color 42 | import androidx.compose.ui.graphics.Path 43 | import androidx.compose.ui.input.pointer.pointerInteropFilter 44 | import androidx.compose.ui.platform.LocalLayoutDirection 45 | import androidx.compose.ui.res.painterResource 46 | import androidx.compose.ui.unit.LayoutDirection 47 | import androidx.compose.ui.unit.dp 48 | import androidx.core.animation.addListener 49 | import com.clwater.compose_canvas.R 50 | import com.clwater.compose_canvas.ui.theme.AndroidComposeCanvasTheme 51 | import java.lang.Exception 52 | import kotlin.math.cos 53 | import kotlin.math.sin 54 | import kotlin.random.Random 55 | import kotlinx.coroutines.delay 56 | import kotlinx.coroutines.launch 57 | 58 | class ClapActivity : ComponentActivity() { 59 | companion object { 60 | fun start(context: Context) { 61 | context.startActivity(Intent(context, ClapActivity::class.java)) 62 | } 63 | const val mMaxClap = 50 64 | } 65 | override fun onCreate(savedInstanceState: Bundle?) { 66 | super.onCreate(savedInstanceState) 67 | setContent { 68 | AndroidComposeCanvasTheme { 69 | // A surface container using the 'background' color from the theme 70 | Surface( 71 | modifier = Modifier.fillMaxSize(), 72 | color = MaterialTheme.colorScheme.background 73 | ) { 74 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { 75 | Clap() 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | @OptIn( 83 | ExperimentalMaterial3Api::class, 84 | ExperimentalComposeUiApi::class 85 | ) 86 | @Composable 87 | fun Clap(startClapCount: Int = 0) { 88 | var isInit by remember { 89 | mutableStateOf(false) 90 | } 91 | val snackbarHostState = SnackbarHostState() 92 | 93 | var showFill by remember { 94 | mutableStateOf(false) 95 | } 96 | 97 | var clapCount by remember { 98 | mutableStateOf(startClapCount) 99 | } 100 | 101 | var inTouch by remember { 102 | mutableStateOf(false) 103 | } 104 | 105 | var scale by remember { 106 | mutableStateOf(1f) 107 | } 108 | 109 | var inAnimator by remember { 110 | mutableStateOf(false) 111 | } 112 | 113 | val animator = ValueAnimator.ofFloat(1f, 0.95f, 1.1f, 1f).apply { 114 | duration = 700 115 | repeatCount = 0 116 | addUpdateListener { 117 | scale = it.animatedValue as Float 118 | } 119 | interpolator = AnticipateOvershootInterpolator() 120 | } 121 | animator.addListener(onEnd = { 122 | if (inTouch) { 123 | animator.start() 124 | } else { 125 | inAnimator = false 126 | } 127 | }) 128 | 129 | LaunchedEffect(clapCount) { 130 | if (clapCount > 0) { 131 | showFill = true 132 | } 133 | } 134 | 135 | LaunchedEffect(inAnimator) { 136 | if (!inAnimator) { 137 | return@LaunchedEffect 138 | } 139 | if (!animator.isStarted && !animator.isRunning && inTouch) { 140 | animator.start() 141 | } 142 | } 143 | 144 | LaunchedEffect(inTouch) { 145 | if (!inTouch) { 146 | return@LaunchedEffect 147 | } else { 148 | clapCount++ 149 | } 150 | while (true) { 151 | inAnimator = true 152 | delay(300) 153 | clapCount++ 154 | } 155 | } 156 | 157 | Scaffold( 158 | snackbarHost = { SnackbarHost(hostState = snackbarHostState) } 159 | ) { 160 | Column( 161 | modifier = Modifier.fillMaxSize().padding(it), 162 | verticalArrangement = Arrangement.Center 163 | ) { 164 | Column( 165 | modifier = Modifier 166 | .align( 167 | alignment = Alignment.CenterHorizontally 168 | ) 169 | .fillMaxWidth() 170 | ) { 171 | Box( 172 | modifier = Modifier 173 | .height(50.dp) 174 | .align(Alignment.CenterHorizontally) 175 | ) { 176 | if (clapCount != startClapCount) { 177 | TopTips(clapCount) 178 | } 179 | } 180 | Box( 181 | modifier = Modifier 182 | .align(Alignment.CenterHorizontally) 183 | .size(100.dp) 184 | ) { 185 | if (isInit) { 186 | ClapFlowers(clapCount - startClapCount + 3) 187 | } 188 | Image( 189 | painter = if (showFill) { 190 | painterResource(id = R.drawable.icon_hand_fill) 191 | } else { 192 | painterResource(id = R.drawable.icon_hand_outline) 193 | }, 194 | contentDescription = "Hand", 195 | 196 | modifier = Modifier 197 | .scale(scale) 198 | .size(100.dp) 199 | .pointerInteropFilter { 200 | when (it.action) { 201 | MotionEvent.ACTION_DOWN -> { 202 | if (!isInit) { 203 | isInit = true 204 | } 205 | inTouch = true 206 | } 207 | 208 | MotionEvent.ACTION_UP -> { 209 | inTouch = false 210 | } 211 | 212 | else -> false 213 | } 214 | true 215 | } 216 | 217 | ) 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | @Composable 225 | fun TopTips(clapCount: Int) { 226 | var showTopTips by remember { 227 | mutableStateOf(true) 228 | } 229 | 230 | LaunchedEffect(clapCount) { 231 | showTopTips = true 232 | delay(1000) 233 | showTopTips = false 234 | } 235 | 236 | if (showTopTips) { 237 | Box( 238 | modifier = Modifier.background( 239 | color = Color.Black, 240 | shape = RoundedCornerShape(100.dp) 241 | ) 242 | ) { 243 | val showCount = 244 | if (clapCount > mMaxClap) { 245 | "+$mMaxClap" 246 | } else { 247 | "+$clapCount" 248 | } 249 | Text( 250 | text = showCount, 251 | modifier = Modifier 252 | .padding(10.dp) 253 | .align(Alignment.Center), 254 | color = Color.White 255 | ) 256 | } 257 | } 258 | } 259 | 260 | @Composable 261 | fun ClapFlowers(clapCount: Int) { 262 | var flower_1 by remember { 263 | mutableStateOf(0) 264 | } 265 | var flower_2 by remember { 266 | mutableStateOf(0) 267 | } 268 | 269 | var flower_show_1 by remember { 270 | mutableStateOf(false) 271 | } 272 | var flower_show_2 by remember { 273 | mutableStateOf(false) 274 | } 275 | 276 | LaunchedEffect(clapCount) { 277 | flower_1 = ((clapCount - 0) / 3f).toInt() 278 | flower_2 = ((clapCount - 1) / 3f).toInt() 279 | } 280 | 281 | LaunchedEffect(flower_1) { 282 | try { 283 | flower_show_1 = true 284 | delay(800) 285 | flower_show_1 = false 286 | } catch (e: Exception) { 287 | flower_show_1 = false 288 | } 289 | } 290 | LaunchedEffect(flower_2) { 291 | try { 292 | flower_show_2 = true 293 | delay(800) 294 | flower_show_2 = false 295 | } catch (e: Exception) { 296 | flower_show_2 = false 297 | } 298 | } 299 | 300 | if (flower_show_1) { 301 | ClapFlower() 302 | } 303 | if (flower_show_2) { 304 | ClapFlower() 305 | } 306 | } 307 | 308 | @Composable 309 | fun ClapFlower() { 310 | val offsetRotate = Random(System.currentTimeMillis()).nextInt(0, 72) 311 | val flowersWidth = 100.dp 312 | val flowersHeight = 100.dp 313 | 314 | val alpha = remember { 315 | Animatable(0f) 316 | } 317 | val distance = remember { 318 | Animatable(-1f) 319 | } 320 | 321 | LaunchedEffect(Unit) { 322 | launch { 323 | alpha.animateTo(1f, animationSpec = tween(300)) 324 | distance.animateTo(1f, animationSpec = tween(200)) 325 | delay(700) 326 | alpha.animateTo(0f, animationSpec = tween(300)) 327 | } 328 | } 329 | 330 | for (i in 0..4) { 331 | val childRotate = i * 72.0 + offsetRotate 332 | val offsetX = flowersWidth / 2f * 1.1f * sin(Math.toRadians(childRotate)).toFloat() 333 | val offsetY = flowersHeight / 2f * 1.1f * cos(Math.toRadians(childRotate)).toFloat() 334 | Canvas( 335 | Modifier.offset( 336 | flowersWidth / 2f + offsetX, 337 | flowersHeight / 2f + offsetY 338 | ).rotate(-childRotate.toFloat()) 339 | .alpha(alpha.value) 340 | ) { 341 | drawCircle( 342 | color = Color(0xFF59A5B3), 343 | radius = 10f, 344 | center = Offset(10f, 0f + flowersWidth.toPx() / 20f * distance.value) 345 | ) 346 | val tripPath = Path() 347 | tripPath.moveTo(0f, 40f + flowersWidth.toPx() / 20f * distance.value) 348 | tripPath.lineTo(-10f, 5f + flowersWidth.toPx() / 20f * distance.value) 349 | tripPath.lineTo(-20f, 40f + flowersWidth.toPx() / 20f * distance.value) 350 | drawPath( 351 | path = tripPath, 352 | color = Color(0xFFF29394) 353 | ) 354 | } 355 | } 356 | } 357 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/shape/tmp/GradientAlongPathAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.shape.tmp 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.Canvas 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Brush 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.graphics.PathMeasure 19 | import androidx.compose.ui.graphics.StrokeCap 20 | import androidx.compose.ui.graphics.asAndroidPath 21 | import androidx.compose.ui.graphics.lerp 22 | import androidx.compose.ui.graphics.vector.PathParser 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.core.graphics.flatten 25 | import kotlin.math.floor 26 | 27 | 28 | @Composable 29 | @Preview 30 | fun GradientAlongPathAnimation() { 31 | val path = remember { 32 | HelloPath.test.toPath() 33 | } 34 | val bounds = path.getBounds() 35 | 36 | val totalLength = remember { 37 | val pathMeasure = PathMeasure() 38 | pathMeasure.setPath(path, false) 39 | pathMeasure.length 40 | } 41 | val lines = remember { 42 | path.asAndroidPath().flatten(0.5f) 43 | } 44 | val progress = remember { 45 | Animatable(0f) 46 | } 47 | LaunchedEffect(Unit) { 48 | progress.animateTo( 49 | 1f, 50 | animationSpec = infiniteRepeatable(tween(10000)) 51 | ) 52 | } 53 | Box(modifier = Modifier.fillMaxSize()) { 54 | Canvas(modifier = Modifier 55 | .align(Alignment.Center) 56 | .fillMaxSize() 57 | .aspectRatio(bounds.width / bounds.height) 58 | , 59 | onDraw = { 60 | val currentLength = totalLength * progress.value 61 | lines.forEach { line -> 62 | if (line.startFraction * totalLength < currentLength) { 63 | val startColor = interpolateColors(line.startFraction, colors) 64 | val endColor = interpolateColors(line.endFraction, colors) 65 | drawLine( 66 | brush = Brush.linearGradient(listOf(startColor, endColor)), 67 | start = Offset(line.start.x, line.start.y), 68 | end = Offset(line.end.x, line.end.y), 69 | strokeWidth = 10f, 70 | cap = StrokeCap.Round 71 | ) 72 | } 73 | } 74 | }) 75 | } 76 | } 77 | 78 | private val colors = listOf( 79 | Color(0xFF3FCEBC), 80 | Color(0xFF3CBCEB), 81 | Color(0xFF5F96E7), 82 | Color(0xFF816FE3), 83 | Color(0xFF9F5EE2), 84 | Color(0xFFBD4CE0), 85 | Color(0xFFDE589F), 86 | Color(0xFFFF645E), 87 | Color(0xFFFDA859), 88 | Color(0xFFFAEC54), 89 | Color(0xFF9EE671), 90 | Color(0xFF67E282), 91 | Color(0xFF3FCEBC) 92 | ) 93 | 94 | private object HelloPath { 95 | val test = PathParser().parsePathString("" + 96 | "M470.74687,-40.2642v33.77539h3.28881v-69.53758h-3.28881v33.77539h-37.82132v-33.77539h-3.28881v69.53758h3.28881v-33.77539zM489.2838,-42.74768v-31.19257h36.62539v-2.08613h-39.9142v69.53758h39.9142v-2.08613h-36.62539v-32.18596h30.79522v-1.98679zM575.9888,-6.4888v-2.08613h-35.13047v-67.45145h-3.28881v69.53758zM625.76943,-6.4888v-2.08613h-35.13047v-67.45145h-3.28881v69.53758zM636.98128,-23.57518c0,9.73526 6.27864,17.58307 22.12472,17.58307c16.14507,0 22.42371,-7.84781 22.42371,-17.58307v-35.36482c0,-9.73526 -6.27864,-17.58307 -22.42371,-17.58307c-15.84609,0 -22.12472,7.84781 -22.12472,17.58307zM640.27009,-59.03934c0,-8.54319 4.93322,-15.49695 18.83591,-15.49695c14.05219,0 19.1349,6.95376 19.1349,15.49695v35.5635c0,8.54319 -5.08271,15.49695 -19.1349,15.49695c-13.9027,0 -18.83591,-6.95376 -18.83591,-15.49695zM192.8424,112.71847v-67.45145h22.87218v-2.08613h-49.03317v2.08613h22.87218v67.45145zM266.39216,78.94308v33.77539h3.28881v-69.53758h-3.28881v33.77539h-37.82132v-33.77539h-3.28881v69.53758h3.28881v-33.77539zM281.64028,43.1809v69.53758h3.28881v-69.53758zM318.56465,42.6842c-15.09863,0.09934 -21.52676,7.0531 -21.52676,17.18572c0,19.1725 40.06369,18.57647 40.06369,36.15954c0,8.44385 -4.93322,15.09959 -18.68642,15.09959c-13.75321,0 -18.68642,-6.65574 -18.68642,-15.09959v-3.47688h-3.13932v3.37754c0,9.63592 5.97966,17.28505 21.82574,17.28505c15.99558,0 21.97523,-7.64913 21.97523,-17.28505c0,-18.87449 -40.06369,-18.37779 -40.06369,-36.15954c0,-8.24517 4.63423,-15.09959 18.38744,-15.09959c13.60372,0 18.23795,6.95376 18.23795,15.19893v1.39075h3.28881v-1.29141c0,-9.53658 -5.83016,-17.28505 -21.67625,-17.28505zM375.22188,43.1809v69.53758h3.28881v-69.53758zM412.14625,42.6842c-15.09863,0.09934 -21.52676,7.0531 -21.52676,17.18572c0,19.1725 40.06369,18.57647 40.06369,36.15954c0,8.44385 -4.93322,15.09959 -18.68642,15.09959c-13.75321,0 -18.68642,-6.65574 -18.68642,-15.09959v-3.47688h-3.13932v3.37754c0,9.63592 5.97966,17.28505 21.82574,17.28505c15.99558,0 21.97523,-7.64913 21.97523,-17.28505c0,-18.87449 -40.06369,-18.37779 -40.06369,-36.15954c0,-8.24517 4.63423,-15.09959 18.38744,-15.09959c13.60372,0 18.23795,6.95376 18.23795,15.19893v1.39075h3.28881v-1.29141c0,-9.53658 -5.83016,-17.28505 -21.67625,-17.28505zM490.47973,42.6842c-15.84609,0 -21.82574,7.94715 -21.82574,17.58307v35.36482c0,9.63592 5.97966,17.58307 21.82574,17.58307c15.99558,0 21.97523,-7.94715 21.97523,-17.58307v-7.54979h-3.28881v7.64913c0,8.44385 -4.78372,15.39761 -18.53693,15.39761c-13.9027,0 -18.68642,-6.95376 -18.68642,-15.39761v-35.5635c0,-8.44385 4.78372,-15.49695 18.68642,-15.49695c13.75321,0 18.53693,7.0531 18.53693,15.49695v5.46367h3.28881v-5.36433c0,-9.63592 -5.97966,-17.58307 -21.97523,-17.58307zM562.53458,112.71847v-2.08613h-35.13047v-67.45145h-3.28881v69.53758zM612.4647,48.34655l16.89253,64.37193h4.33525l18.53693,-69.53758h-2.98983l-17.63998,66.16004l-17.04202,-65.76268h-4.03627l-18.08846,66.16004l-18.23795,-66.55739h-3.28881l18.83591,69.53758h4.78372zM707.39172,96.32747l6.12915,16.391h3.28881l-25.86201,-69.63692h-4.63423l-25.26404,69.63692h2.98983l6.12915,-16.391zM688.55581,45.66438l18.23795,48.6763h-36.02742zM745.51202,112.71847v-67.45145h22.87218v-2.08613h-48.88368v2.08613h22.72269v67.45145zM780.79198,76.45959v-31.19257h36.62539v-2.08613h-39.9142v69.53758h39.9142v-2.08613h-36.62539v-32.18596h30.79522v-1.98679zM829.0777,43.1809v69.53758h3.28881v-32.08662h12.85626c13.60372,0 22.87218,2.68216 22.87218,12.91412v10.92733c0,2.98018 0.44847,6.0597 2.54135,8.24517h3.58779c-2.39186,-2.18547 -2.84034,-5.56301 -2.84034,-8.24517v-10.92733c0,-7.64913 -4.93322,-12.71544 -16.59354,-13.90752c11.51084,-1.39075 16.59354,-6.0597 16.59354,-14.60289v-6.25838c0,-9.53658 -5.97966,-15.59629 -21.52676,-15.59629zM832.36651,78.54572v-33.2787h17.341c13.45422,0 18.38744,5.26499 18.38744,13.70884v6.35772c0,10.72865 -8.37152,13.21214 -22.87218,13.21214zM885.88442,43.08156l0.14949,15.99364h2.69084l0.29898,-15.99364zM922.6593,42.6842c-15.09863,0.09934 -21.52676,7.0531 -21.52676,17.18572c0,19.1725 40.06369,18.57647 40.06369,36.15954c0,8.44385 -4.93322,15.09959 -18.68642,15.09959c-13.75321,0 -18.68642,-6.65574 -18.68642,-15.09959v-3.47688h-3.13932v3.37754c0,9.63592 5.97966,17.28505 21.82574,17.28505c15.99558,0 21.97523,-7.64913 21.97523,-17.28505c0,-18.87449 -40.06369,-18.37779 -40.06369,-36.15954c0,-8.24517 4.63423,-15.09959 18.38744,-15.09959c13.60372,0 18.23795,6.95376 18.23795,15.19893v1.39075h3.28881v-1.29141c0,-9.53658 -5.83016,-17.28505 -21.67625,-17.28505zM78.25726,161.89147c-15.09863,0.09934 -21.52676,7.0531 -21.52676,17.18572c0,19.1725 40.06369,18.57647 40.06369,36.15954c0,8.44385 -4.93322,15.09959 -18.68642,15.09959c-13.75321,0 -18.68642,-6.65574 -18.68642,-15.09959v-3.47688h-3.13932v3.37754c0,9.63592 5.97966,17.28505 21.82574,17.28505c15.99558,0 21.97523,-7.64913 21.97523,-17.28505c0,-18.87449 -40.06369,-18.37779 -40.06369,-36.15954c0,-8.24517 4.63423,-15.09959 18.38744,-15.09959c13.60372,0 18.23795,6.95376 18.23795,15.19893v1.39075h3.28881v-1.29141c0,-9.53658 -5.83016,-17.28505 -21.67625,-17.28505zM152.85346,198.15035v33.77539h3.28881v-69.53758h-3.28881v33.77539h-37.82132v-33.77539h-3.28881v69.53758h3.28881v-33.77539zM213.99543,215.53475l6.12915,16.391h3.28881l-25.86201,-69.63692h-4.63423l-25.26404,69.63692h2.98983l6.12915,-16.391zM195.15952,164.87166l18.23795,48.6763h-36.02742zM234.92423,162.38817v69.53758h3.28881v-29.5038h14.94914c16.59354,0 24.06811,-5.26499 24.06811,-16.29166v-6.95376c0,-9.73526 -5.53118,-16.78836 -21.67625,-16.78836zM238.21304,200.33582v-35.86152h17.341c14.05219,0 18.38744,6.0597 18.38744,14.60289v7.15244c0,9.8346 -6.12915,14.10619 -20.7793,14.10619zM291.88044,195.66687v-31.19257h36.62539v-2.08613h-39.9142v69.53758h39.9142v-2.08613h-36.62539v-32.18596h30.79522v-1.98679zM363.33732,162.38817v69.53758h3.28881v-29.5038h14.94914c16.59354,0 24.06811,-5.26499 24.06811,-16.29166v-6.95376c0,-9.73526 -5.53118,-16.78836 -21.67625,-16.78836zM366.62613,200.33582v-35.86152h17.341c14.05219,0 18.38744,6.0597 18.38744,14.60289v7.15244c0,9.8346 -6.12915,14.10619 -20.7793,14.10619zM461.10468,215.53475l6.12915,16.391h3.28881l-25.86201,-69.63692h-4.63423l-25.26404,69.63692h2.98983l6.12915,-16.391zM442.26876,164.87166l18.23795,48.6763h-36.02742zM499.22498,231.92575v-67.45145h22.87218v-2.08613h-48.88368v2.08613h22.72269v67.45145zM572.32626,198.15035v33.77539h3.28881v-69.53758h-3.28881v33.77539h-37.82132v-33.77539h-3.28881v69.53758h3.28881v-33.77539zM656.6394,215.53475l6.12915,16.391h3.28881l-25.86201,-69.63692h-4.63423l-25.26404,69.63692h2.98983l6.12915,-16.391zM637.80348,164.87166l18.23795,48.6763h-36.02742zM680.70751,165.56703l37.37284,66.35872h4.03627v-69.53758h-3.13932v65.0673l-36.77488,-65.0673h-4.63423v69.53758h3.13932zM734.07593,162.38817v69.53758h3.28881v-69.53758zM807.62569,165.66637v66.25938h3.28881v-69.53758h-5.08271l-25.71252,65.96136l-25.71252,-65.96136h-5.08271v69.53758h2.98983v-66.25938l25.86201,66.0607h3.73728zM868.76766,215.53475l6.12915,16.391h3.28881l-25.86201,-69.63692h-4.63423l-25.26404,69.63692h2.98983l6.12915,-16.391zM849.93175,164.87166l18.23795,48.6763h-36.02742zM906.88796,231.92575v-67.45145h22.87218v-2.08613h-48.88368v2.08613h22.72269v67.45145zM938.87911,162.38817v69.53758h3.28881v-69.53758zM953.97774,214.83937c0,9.73526 6.27864,17.58307 22.12472,17.58307c16.14507,0 22.42371,-7.84781 22.42371,-17.58307v-35.36482c0,-9.73526 -6.27864,-17.58307 -22.42371,-17.58307c-15.84609,0 -22.12472,7.84781 -22.12472,17.58307zM957.26655,179.37521c0,-8.54319 4.93322,-15.49695 18.83591,-15.49695c14.05219,0 19.1349,6.95376 19.1349,15.49695v35.5635c0,8.54319 -5.08271,15.49695 -19.1349,15.49695c-13.9027,0 -18.83591,-6.95376 -18.83591,-15.49695zM1013.47531,165.56703l37.37284,66.35872h4.03627v-69.53758h-3.13932v65.0673l-36.77488,-65.0673h-4.63423v69.53758h3.13932z" 97 | ) 98 | val path = PathParser().parsePathString( 99 | "M13.63 248.31C13.63 248.31 51.84 206.67 84.21 169.31C140" + 100 | ".84 103.97 202.79 27.66 150.14 14.88C131.01 10.23 116.36 29.88 107.26 45.33C69.7 108.92 58.03 214.33 57.54 302.57C67.75 271.83 104.43 190.85 140.18 193.08C181.47 195.65 145.26 257.57 154.53 284.39C168.85 322.18 208.22 292.83 229.98 277.45C265.92 252.03 288.98 231.22 288.98 200.45C288.98 161.55 235.29 174.02 223.3 205.14C213.93 229.44 214.3 265.89 229.3 284.14C247.49 306.28 287.67 309.93 312.18 288.46C337 266.71 354.66 234.56 368.68 213.03C403.92 158.87 464.36 86.15 449.06 30.03C446.98 22.4 440.36 16.57 432.46 16.26C393.62 14.75 381.84 99.18 375.35 129.31C368.78 159.83 345.17 261.31 373.11 293.06C404.43 328.58 446.29 262.4 464.66 231.67C468.66 225.31 472.59 218.43 476.08 213.07C511.33 158.91 571.77 86.19 556.46 30.07C554.39 22.44 547.77 16.61 539.87 16.3C501.03 14.79 489.25 99.22 482.76 129.35C476.18 159.87 452.58 261.35 480.52 293.1C511.83 328.62 562.4 265.53 572.64 232.86C587.34 185.92 620.94 171.58 660.91 180.29C616 166.66 580.86 199.67 572.64 233.16C566.81 256.93 573.52 282.16 599.25 295.77C668.54 332.41 742.8 211.69 660.91 180.29C643.67 181.89 636.15 204.77 643.29 227.78C654.29 263.97 704.29 268.27 733.08 256" 101 | ) 102 | val riggarooPath = PathParser().parsePathString( 103 | """ 104 | M320 448.5C308.333 483 288.5 563.4 302.5 609C320 666 417.981 379.805 438.5 448.5C461.5 525.5 424 552.5 471 552.5C508.6 552.5 539.333 478.167 550 443.5C536 504.667 500 642.112 550 632C594.5 623 636.5 600.5 648.5 552.5C654.5 506.5 691.5 468.5 751.5 463.5C699.051 488 658 497 648.5 580.5C639 664 740.701 626.877 756.5 592C783 533.5 778.5 502.5 783 463.5C773.5 567.5 756.5 740 709 817C676.667 869.413 629 862.5 620 834C611 805.5 648.5 726.5 737.5 669.5C778.403 643.304 836.5 592 873.5 580.5C891 491 922.5 479.5 983.5 463.5C935.5 484.5 888.5 516 881 580.5C873.5 645 935 648 969 606C996.2 572.4 1010 497 1013.5 463.5C1003.67 564.333 976.6 776.2 947 817C910 868 842.766 870.039 846 834C853 756 924.5 683.5 977 660.5C1029.5 637.5 1086.5 580 1108.5 567.5C1130.5 555 1111.5 463.5 1206.5 463.5C1206.5 469.5 1140.5 456 1122 567.5C1106.57 660.5 1194.5 632 1206.5 600C1220.19 563.506 1248.5 455.5 1248.5 455.5C1248.5 455.5 1232.5 583.5 1248.5 621C1264.5 658.5 1349.5 603.5 1354 567.5C1358.5 531.5 1370.5 455.5 1370.5 455.5C1370.5 455.5 1333.5 633 1360 628C1386.5 623 1432.5 416.5 1480 469.5C1527.5 522.5 1474.5 584 1525 552C1582.05 515.851 1581 438 1660 442.5C1626.5 457.5 1564.08 465.399 1588 578C1608.5 674.5 1713 627.126 1713 547C1713 482.5 1696.5 470.5 1679 442.5C1679 442.5 1776.31 558.119 1798 525C1821.58 489 1832.5 435.5 1905 448.5M1905 448.5C1877.5 447 1817.9 456.7 1809.5 529.5C1799 620.5 1828.5 644.5 1872 630.5C1915.5 616.5 1936.5 569.5 1938 535C1939.5 500.5 1926 455.5 1905 448.5Z 105 | """.trimIndent() 106 | ) 107 | } 108 | 109 | private fun interpolateColors( 110 | progress: Float, 111 | colorsInput: List, 112 | ): Color { 113 | if (progress == 1f) return colorsInput.last() 114 | 115 | val scaledProgress = progress * (colorsInput.size - 1) 116 | val oldColor = colorsInput[scaledProgress.toInt()] 117 | val newColor = colorsInput[(scaledProgress + 1f).toInt()] 118 | val newScaledAnimationValue = scaledProgress - floor(scaledProgress) 119 | return lerp(start = oldColor, stop = newColor, fraction = newScaledAnimationValue) 120 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/shape/tmp/ShapeActivity.bak: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.shape 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.animation.core.LinearEasing 8 | import androidx.compose.animation.core.RepeatMode 9 | import androidx.compose.animation.core.animateFloat 10 | import androidx.compose.animation.core.infiniteRepeatable 11 | import androidx.compose.animation.core.rememberInfiniteTransition 12 | import androidx.compose.animation.core.tween 13 | import androidx.compose.foundation.layout.Arrangement 14 | import androidx.compose.foundation.layout.Box 15 | import androidx.compose.foundation.layout.Column 16 | import androidx.compose.foundation.layout.Row 17 | import androidx.compose.foundation.layout.fillMaxSize 18 | import androidx.compose.foundation.layout.fillMaxWidth 19 | import androidx.compose.foundation.layout.height 20 | import androidx.compose.foundation.layout.offset 21 | import androidx.compose.foundation.layout.padding 22 | import androidx.compose.foundation.rememberScrollState 23 | import androidx.compose.foundation.verticalScroll 24 | import androidx.compose.material.icons.Icons 25 | import androidx.compose.material.icons.filled.PlayArrow 26 | import androidx.compose.material3.Button 27 | import androidx.compose.material3.Icon 28 | import androidx.compose.material3.MaterialTheme 29 | import androidx.compose.material3.Slider 30 | import androidx.compose.material3.Surface 31 | import androidx.compose.material3.Text 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.runtime.CompositionLocalProvider 34 | import androidx.compose.runtime.getValue 35 | import androidx.compose.runtime.mutableStateOf 36 | import androidx.compose.runtime.remember 37 | import androidx.compose.ui.Alignment 38 | import androidx.compose.ui.Modifier 39 | import androidx.compose.ui.draw.drawWithCache 40 | import androidx.compose.ui.geometry.Offset 41 | import androidx.compose.ui.geometry.Size 42 | import androidx.compose.ui.graphics.Color 43 | import androidx.compose.ui.graphics.Path 44 | import androidx.compose.ui.platform.LocalConfiguration 45 | import androidx.compose.ui.platform.LocalLayoutDirection 46 | import androidx.compose.ui.unit.LayoutDirection 47 | import androidx.compose.ui.unit.dp 48 | import androidx.graphics.shapes.Cubic 49 | import androidx.graphics.shapes.Morph 50 | import androidx.graphics.shapes.RoundedPolygon 51 | import androidx.graphics.shapes.star 52 | import com.clwater.compose_canvas.shape.tmp.toComposePath 53 | import com.clwater.compose_canvas.ui.theme.AndroidComposeCanvasTheme 54 | 55 | class ShapeActivity : ComponentActivity() { 56 | companion object { 57 | fun start(activity: ComponentActivity) { 58 | activity.startActivity(Intent(activity, ShapeActivity::class.java)) 59 | } 60 | } 61 | 62 | override fun onCreate(savedInstanceState: Bundle?) { 63 | super.onCreate(savedInstanceState) 64 | setContent { 65 | AndroidComposeCanvasTheme { 66 | // A surface container using the 'background' color from the theme 67 | Surface( 68 | modifier = Modifier.fillMaxSize(), 69 | color = MaterialTheme.colorScheme.background, 70 | ) { 71 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { 72 | ShapeCustoms() 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | @Composable 80 | fun ShapeCustoms() { 81 | val configuration = LocalConfiguration.current 82 | val screenWidth = configuration.screenWidthDp.dp 83 | val screenHeight = configuration.screenHeightDp.dp 84 | 85 | val shapeParams_1 = remember { 86 | mutableStateOf( 87 | ShapeParams(5) 88 | ) 89 | } 90 | 91 | val shapeParams_2 = remember { 92 | mutableStateOf( 93 | ShapeParams(10) 94 | ) 95 | } 96 | 97 | val animatedProcessManual = remember { 98 | mutableStateOf(0f) 99 | } 100 | 101 | 102 | val convertShapeMode = remember { 103 | mutableStateOf(ConvertShapeMode.Manual) 104 | } 105 | 106 | 107 | val shapeSize = Size(screenWidth.value / 2f, screenWidth.value / 2f) 108 | 109 | 110 | val infiniteTransition = rememberInfiniteTransition("infinite outline movement") 111 | val animatedProgress = infiniteTransition.animateFloat( 112 | initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( 113 | tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Reverse 114 | ), label = "animatedMorphProgress" 115 | ) 116 | 117 | 118 | val startPolygon = RoundedPolygon( 119 | numVertices = shapeParams_1.value.vertices, 120 | radius = shapeSize.minDimension, 121 | centerX = shapeSize.width, 122 | centerY = shapeSize.height 123 | ) 124 | val endPolygon = RoundedPolygon.star( 125 | numVerticesPerRadius = 5, 126 | // numVertices = shapeParams_2.value.vertices, 127 | radius = shapeSize.minDimension, 128 | centerX = shapeSize.width, 129 | centerY = shapeSize.height 130 | ) 131 | 132 | 133 | 134 | Column( 135 | modifier = Modifier 136 | .padding(8.dp) 137 | .verticalScroll(rememberScrollState()) 138 | ) { 139 | Column( 140 | 141 | ) { 142 | Column( 143 | modifier = Modifier.fillMaxWidth(), 144 | verticalArrangement = Arrangement.Center, 145 | horizontalAlignment = Alignment.CenterHorizontally 146 | ) { 147 | Box(modifier = Modifier 148 | .height(shapeSize.height.dp) 149 | .offset(x = -shapeSize.width.dp / 2f) 150 | .drawWithCache { 151 | val morph = Morph(start = startPolygon, end = endPolygon) 152 | val morphPath = morph.toComposePath( 153 | progress = if (convertShapeMode.value == ConvertShapeMode.Auto) { 154 | animatedProgress.value 155 | } else { 156 | animatedProcessManual.value 157 | } 158 | ) 159 | 160 | onDrawBehind { 161 | drawPath(morphPath, color = Color.Black) 162 | } 163 | }) 164 | } 165 | 166 | 167 | Row { 168 | Slider( 169 | enabled = convertShapeMode.value == ConvertShapeMode.Manual, 170 | value = if (convertShapeMode.value == ConvertShapeMode.Auto) { 171 | animatedProgress.value 172 | } else { 173 | animatedProcessManual.value 174 | }, 175 | onValueChange = { 176 | if (convertShapeMode.value == ConvertShapeMode.Manual) { 177 | animatedProcessManual.value = it 178 | } 179 | }, 180 | modifier = Modifier.weight(3f), 181 | steps = 100, 182 | ) 183 | 184 | Button(modifier = Modifier.weight(1.5f), onClick = { 185 | convertShapeMode.value = ConvertShapeMode.convert(convertShapeMode.value) 186 | }) { 187 | Text(text = "" + convertShapeMode.value) 188 | if (convertShapeMode.value == ConvertShapeMode.Manual) { 189 | Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = "") 190 | } 191 | } 192 | } 193 | } 194 | 195 | Row { 196 | Box(modifier = Modifier 197 | .height(screenWidth / 2f) 198 | .weight(1f) 199 | .drawWithCache { 200 | 201 | val roundedPolygonPath = startPolygon.cubics.toPath() 202 | onDrawBehind { 203 | drawPath(roundedPolygonPath, color = Color.Blue) 204 | } 205 | }) 206 | Box(modifier = Modifier 207 | .height(screenWidth / 2f) 208 | .weight(1f) 209 | .drawWithCache { 210 | val roundedPolygonPath = endPolygon.cubics.toPath() 211 | onDrawBehind { 212 | drawPath(roundedPolygonPath, color = Color.Blue) 213 | } 214 | }) 215 | } 216 | } 217 | } 218 | 219 | 220 | data class ShapeParams(var vertices: Int) 221 | enum class ConvertShapeMode { 222 | Auto, Manual; 223 | 224 | companion object { 225 | fun convert(value: ConvertShapeMode): ConvertShapeMode { 226 | return if (value == Auto) { 227 | Manual 228 | } else { 229 | Auto 230 | } 231 | } 232 | } 233 | } 234 | 235 | 236 | fun List.toPath(path: Path = Path(), scale: Float = 1f): Path { 237 | path.rewind() 238 | firstOrNull()?.let { first -> 239 | path.moveTo(first.anchor0X * scale, first.anchor0Y * scale) 240 | } 241 | for (bezier in this) { 242 | path.cubicTo( 243 | bezier.control0X * scale, 244 | bezier.control0Y * scale, 245 | bezier.control1X * scale, 246 | bezier.control1Y * scale, 247 | bezier.anchor1X * scale, 248 | bezier.anchor1Y * scale 249 | ) 250 | } 251 | path.close() 252 | return path 253 | } 254 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/shape/tmp/ShapeComposable.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.shape.tmp 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.drawWithCache 16 | import androidx.compose.ui.geometry.Offset 17 | import androidx.compose.ui.graphics.Brush 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.Path 20 | import androidx.compose.ui.graphics.PathMeasure 21 | import androidx.compose.ui.graphics.StrokeCap 22 | import androidx.compose.ui.graphics.asAndroidPath 23 | import androidx.compose.ui.graphics.drawscope.rotate 24 | import androidx.compose.ui.graphics.drawscope.translate 25 | import androidx.compose.ui.unit.dp 26 | import androidx.core.graphics.flatten 27 | import androidx.graphics.shapes.CornerRounding 28 | import androidx.graphics.shapes.Cubic 29 | import androidx.graphics.shapes.Morph 30 | import androidx.graphics.shapes.RoundedPolygon 31 | import androidx.graphics.shapes.circle 32 | import androidx.graphics.shapes.star 33 | 34 | @Composable 35 | fun ShapeComposable() { 36 | val pathMeasurer = remember { 37 | PathMeasure() 38 | } 39 | val infiniteTransition = rememberInfiniteTransition(label = "infinite") 40 | val progress = infiniteTransition.animateFloat( 41 | initialValue = 0f, 42 | targetValue = 1f, 43 | animationSpec = infiniteRepeatable( 44 | tween(4000, easing = LinearEasing), 45 | repeatMode = RepeatMode.Reverse 46 | ), 47 | label = "progress" 48 | ) 49 | val rotation = infiniteTransition.animateFloat( 50 | initialValue = 0f, 51 | targetValue = 360f, 52 | animationSpec = infiniteRepeatable( 53 | tween(4000, easing = LinearEasing), 54 | repeatMode = RepeatMode.Reverse 55 | ), 56 | label = "rotation" 57 | ) 58 | val starPolygon = remember { 59 | RoundedPolygon.star( 60 | numVerticesPerRadius = 12, 61 | innerRadius = 2f / 3f, 62 | rounding = CornerRounding(1f / 6f) 63 | ) 64 | } 65 | val circlePolygon = remember { 66 | RoundedPolygon.circle( 67 | numVertices = 12 68 | ) 69 | } 70 | val morph = remember { 71 | Morph(starPolygon, circlePolygon) 72 | } 73 | var morphPath = remember { 74 | Path() 75 | } 76 | 77 | Box( 78 | modifier = Modifier 79 | .padding(16.dp) 80 | .drawWithCache { 81 | morphPath = morph 82 | .toComposePath( 83 | progress = progress.value, 84 | scale = size.minDimension / 2f, 85 | path = morphPath 86 | ) 87 | val morphAndroidPath = morphPath.asAndroidPath() 88 | val flattenedStarPath = morphAndroidPath.flatten() 89 | 90 | pathMeasurer.setPath(morphPath, false) 91 | val totalLength = pathMeasurer.length 92 | 93 | onDrawBehind { 94 | 95 | rotate(rotation.value) { 96 | translate(size.width / 2f, size.height / 2f) { 97 | val brush = Brush.sweepGradient(colors, center = Offset(0.5f, 0.5f)) 98 | 99 | val currentLength = totalLength * progress.value 100 | flattenedStarPath.forEach { line -> 101 | if (line.startFraction * totalLength < currentLength) { 102 | if (progress.value > line.endFraction) { 103 | drawLine( 104 | brush = brush, 105 | start = Offset(line.start.x, line.start.y), 106 | end = Offset(line.end.x, line.end.y), 107 | strokeWidth = 16.dp.toPx(), 108 | cap = StrokeCap.Round 109 | ) 110 | } else { 111 | val endX = mapValue( 112 | progress.value, 113 | line.startFraction, 114 | line.endFraction, 115 | line.start.x, 116 | line.end.x 117 | ) 118 | val endY = mapValue( 119 | progress.value, 120 | line.startFraction, 121 | line.endFraction, 122 | line.start.y, 123 | line.end.y 124 | ) 125 | drawLine( 126 | brush =brush, 127 | start = Offset(line.start.x, line.start.y), 128 | end = Offset(endX, endY), 129 | strokeWidth = 16.dp.toPx(), 130 | cap = StrokeCap.Round 131 | ) 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | .fillMaxSize() 140 | ) 141 | } 142 | 143 | private val colors = listOf( 144 | Color(0xFF3FCEBC), 145 | Color(0xFF3CBCEB), 146 | Color(0xFF5F96E7), 147 | Color(0xFF816FE3), 148 | Color(0xFF9F5EE2), 149 | Color(0xFFBD4CE0), 150 | Color(0xFFDE589F), 151 | Color(0xFF3FCEBC), 152 | ) 153 | 154 | private fun mapValue( 155 | value: Float, 156 | fromRangeStart: Float, 157 | fromRangeEnd: Float, 158 | toRangeStart: Float, 159 | toRangeEnd: Float 160 | ): Float { 161 | val ratio = 162 | (value - fromRangeStart) / (fromRangeEnd - fromRangeStart) 163 | return toRangeStart + ratio * (toRangeEnd - toRangeStart) 164 | } 165 | 166 | fun List.toPath(path: Path = Path(), scale: Float = 1f): Path { 167 | path.rewind() 168 | firstOrNull()?.let { first -> 169 | path.moveTo(first.anchor0X * scale, first.anchor0Y * scale) 170 | } 171 | for (bezier in this) { 172 | path.cubicTo( 173 | bezier.control0X * scale, bezier.control0Y * scale, 174 | bezier.control1X * scale, bezier.control1Y * scale, 175 | bezier.anchor1X * scale, bezier.anchor1Y * scale 176 | ) 177 | } 178 | path.close() 179 | return path 180 | } 181 | 182 | fun Morph.toComposePath(progress: Float, scale: Float = 1f, path: Path = Path()): Path { 183 | var first = true 184 | path.rewind() 185 | forEachCubic(progress) { bezier -> 186 | if (first) { 187 | path.moveTo(bezier.anchor0X * scale, bezier.anchor0Y * scale) 188 | first = false 189 | } 190 | path.cubicTo( 191 | bezier.control0X * scale, bezier.control0Y * scale, 192 | bezier.control1X * scale, bezier.control1Y * scale, 193 | bezier.anchor1X * scale, bezier.anchor1Y * scale 194 | ) 195 | } 196 | path.close() 197 | return path 198 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/sun_moon/Canvas1Activity.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.sun_moon 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.view.animation.DecelerateInterpolator 8 | import androidx.activity.ComponentActivity 9 | import androidx.activity.compose.setContent 10 | import androidx.activity.viewModels 11 | import androidx.compose.animation.core.* 12 | import androidx.compose.foundation.Canvas 13 | import androidx.compose.foundation.background 14 | import androidx.compose.foundation.layout.* 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.* 17 | import androidx.compose.runtime.* 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.alpha 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.draw.clipToBounds 22 | import androidx.compose.ui.geometry.Offset 23 | import androidx.compose.ui.graphics.* 24 | import androidx.compose.ui.graphics.drawscope.Stroke 25 | import androidx.compose.ui.platform.LocalLayoutDirection 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.LayoutDirection 28 | import androidx.compose.ui.unit.dp 29 | import androidx.core.animation.addListener 30 | import androidx.core.animation.doOnEnd 31 | import androidx.lifecycle.ViewModel 32 | import com.clwater.compose_canvas.ui.theme.AndroidComposeCanvasTheme 33 | import kotlin.random.Random 34 | 35 | class Canvas1Activity : ComponentActivity() { 36 | companion object { 37 | fun start(context: Context) { 38 | context.startActivity(Intent(context, Canvas1Activity::class.java)) 39 | } 40 | } 41 | 42 | /** 43 | * Night Star Info 44 | */ 45 | class NightStar { 46 | // position and radius 47 | var x = mutableStateOf(0f) 48 | var y = mutableStateOf(0f) 49 | var radius = mutableStateOf(0f) 50 | var alpha = mutableStateOf(0f) 51 | var status = mutableStateOf(NightStarStatus.Start) 52 | } 53 | 54 | /** 55 | * Night Star Status 56 | */ 57 | enum class NightStarStatus { 58 | Start, 59 | End, 60 | Lighting, 61 | } 62 | 63 | /** 64 | * Star Status 65 | */ 66 | enum class Star { 67 | Sun, 68 | Moon, 69 | ToSun, 70 | ToMoon, 71 | } 72 | 73 | class Canvas1ViewModel : ViewModel() { 74 | var progress = mutableStateOf(0f) 75 | var startStatus = mutableStateOf(Star.ToSun) 76 | var nightStar = mutableStateListOf() 77 | } 78 | 79 | private val model by viewModels() 80 | 81 | override fun onCreate(savedInstanceState: Bundle?) { 82 | super.onCreate(savedInstanceState) 83 | setContent { 84 | AndroidComposeCanvasTheme { 85 | // A surface container using the 'background' color from the theme 86 | Surface( 87 | modifier = Modifier.fillMaxSize(), 88 | color = MaterialTheme.colorScheme.background, 89 | ) { 90 | Column( 91 | modifier = Modifier 92 | .fillMaxSize() 93 | .background(Color.White), 94 | verticalArrangement = Arrangement.Center, 95 | ) { 96 | Row( 97 | modifier = Modifier.fillMaxWidth(1f), 98 | horizontalArrangement = Arrangement.Center, 99 | ) { 100 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { 101 | Canvas_1() 102 | } 103 | } 104 | Slider( 105 | value = model.progress.value, 106 | onValueChange = { 107 | model.progress.value = it 108 | }, 109 | modifier = Modifier 110 | .padding(horizontal = 20.dp) 111 | .fillMaxWidth(), 112 | steps = 100, 113 | ) 114 | Row( 115 | modifier = Modifier.fillMaxWidth(1f), 116 | horizontalArrangement = Arrangement.Center, 117 | ) { 118 | Button( 119 | onClick = { 120 | model.startStatus.value = Star.ToMoon 121 | model.progress.value = 0f 122 | val valueAnimator = ValueAnimator.ofFloat(0f, 1f) 123 | valueAnimator.duration = 1000 124 | valueAnimator.interpolator = DecelerateInterpolator() 125 | valueAnimator.addUpdateListener { 126 | val value = it.animatedValue as Float 127 | model.progress.value = value 128 | } 129 | valueAnimator.addListener { 130 | it.doOnEnd { 131 | model.startStatus.value = Star.Moon 132 | } 133 | } 134 | valueAnimator.start() 135 | }, 136 | ) { 137 | Text("To Moon") 138 | } 139 | Spacer(modifier = Modifier.width(20.dp)) 140 | Button(onClick = { 141 | model.startStatus.value = Star.ToSun 142 | model.progress.value = 1f 143 | val valueAnimator = ValueAnimator.ofFloat(1f, 0f) 144 | valueAnimator.duration = 1000 145 | valueAnimator.interpolator = DecelerateInterpolator() 146 | valueAnimator.addUpdateListener { 147 | val value = it.animatedValue as Float 148 | model.progress.value = value 149 | } 150 | valueAnimator.addListener { 151 | it.doOnEnd { 152 | model.startStatus.value = Star.Sun 153 | } 154 | } 155 | valueAnimator.start() 156 | }) { 157 | Text("To Sun") 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | // base canvas draw info 167 | val mCanvasWidth = 160.dp 168 | val mCanvasHeight = 60.dp 169 | val mCanvasRadius = mCanvasHeight / 2f 170 | val mButtonHeight = mCanvasHeight - mCanvasHeight / 10f * 2 171 | val mSunCloudRadius = mCanvasRadius - mCanvasHeight / 10f 172 | val mPerDistance = 0.2f 173 | 174 | val mLightBackgroundColor = listOf( 175 | Color(0xFF1565C0), 176 | Color(0xFF1E88E5), 177 | Color(0xFF2196F3), 178 | Color(0xFF42A5F5), 179 | ) 180 | val mNightBackgroundColor = listOf( 181 | Color(0xFF1C1E2B), 182 | Color(0xFF2E323C), 183 | Color(0xFF3E424E), 184 | Color(0xFF4F555D), 185 | ) 186 | 187 | val mSunColor = Color(0xFFFFD54F) 188 | val mSunColorDeep = Color(0xFFFFA726) 189 | val mSunTopShadowColor = Color(0xCCFFFFFF) 190 | val mSunBottomShadowColor = Color(0x80827717) 191 | 192 | val mMoonColor = Color(0xFFC3C9D1) 193 | val mMoonTopShadowColor = Color(0xCCFFFFFF) 194 | val mMoonBottomShadowColor = Color(0xFF5E5E5E) 195 | val mMoonDownColor = Color(0xFF73777E) 196 | 197 | val mStarRadius = mSunCloudRadius * 0.9f 198 | 199 | val mStarMove = mCanvasWidth - (mCanvasHeight - mStarRadius * 2f) - mStarRadius * 2f 200 | 201 | @Preview 202 | @Composable 203 | fun Canvas_1() { 204 | Box( 205 | modifier = Modifier 206 | .width(mCanvasWidth) 207 | .height(mCanvasHeight), 208 | ) { 209 | Background(model.progress.value) 210 | SunCloud(model.progress.value) 211 | NightStarts(model.progress.value) 212 | SunAndMoon(model.progress.value, model.startStatus.value) 213 | } 214 | } 215 | 216 | /** 217 | * Sun and Moon Group 218 | * when sun to moon, moon is up, sun is down 219 | */ 220 | @Composable 221 | fun SunAndMoon(progress: Float, star: Star) { 222 | Box( 223 | modifier = Modifier 224 | .width(mCanvasWidth) 225 | .height(mCanvasHeight), 226 | ) { 227 | Box( 228 | modifier = Modifier 229 | .height(mStarRadius * 2) 230 | .width(mStarRadius * 2), 231 | ) { 232 | when (star) { 233 | Star.Sun -> 234 | Sun() 235 | Star.Moon -> 236 | Moon() 237 | Star.ToSun -> { 238 | Moon(progress, true) 239 | Sun(progress, true) 240 | } 241 | Star.ToMoon -> { 242 | Sun(progress, false) 243 | Moon(progress, false) 244 | } 245 | } 246 | } 247 | } 248 | } 249 | 250 | /** 251 | * Sun Cloud 252 | * two canvas, one is cloud , one is cloud shadow 253 | */ 254 | @Composable 255 | fun SunCloud(progress: Float) { 256 | val cloudOffsetX = (mCanvasWidth - mStarRadius * 1.1f) / 7f 257 | val cloudOffsetY = mCanvasHeight / 2f / 10f 258 | val baseOffsetX = -mSunCloudRadius / 5f 259 | val baseOffsetY = mCanvasHeight / 6f 260 | val cloudShadowOffsetY = -mCanvasHeight / 8f 261 | 262 | val cloudColor = Color(0xFFFFFFFF) 263 | val cloudColorShadow = Color(0xFFFFFFFF) 264 | 265 | // this list is cloud(shadow) offset 266 | val offsetRadius = listOf(1f, 0.8f, 0.6f, 0.5f, 0.6f, 0.8f, 0.6f) 267 | val offsetX = listOf(0, 2, 4, 6, 7, 8, 8) 268 | val shadowOffsetY = listOf(1f, 2f, 2f, 2f, 1f, 1f, 1f) 269 | val shadowOffsetX = listOf(0f, 0f, 0f, 0f, 0f, 0f, -0.8f) 270 | 271 | val infiniteTransition = rememberInfiniteTransition() 272 | val animationOffsetX by infiniteTransition.animateFloat( 273 | initialValue = -1f, 274 | targetValue = 1f, 275 | animationSpec = infiniteRepeatable( 276 | animation = tween( 277 | durationMillis = 3100, 278 | easing = LinearEasing, 279 | ), 280 | repeatMode = RepeatMode.Reverse, 281 | ), 282 | ) 283 | 284 | val animationOffsetY by infiniteTransition.animateFloat( 285 | initialValue = -1f, 286 | targetValue = 1f, 287 | animationSpec = infiniteRepeatable( 288 | animation = tween( 289 | durationMillis = 2900, 290 | easing = LinearEasing, 291 | ), 292 | repeatMode = RepeatMode.Reverse, 293 | ), 294 | ) 295 | 296 | val animationOffsetRadius by infiniteTransition.animateFloat( 297 | initialValue = -1f, 298 | targetValue = 1f, 299 | animationSpec = infiniteRepeatable( 300 | animation = tween( 301 | durationMillis = 3000, 302 | easing = LinearEasing, 303 | ), 304 | repeatMode = RepeatMode.Reverse, 305 | ), 306 | ) 307 | 308 | // for sun/moon change, this is Cloud move animation 309 | val progressY = if (progress < mPerDistance) { 310 | 0f 311 | } else if (progress > (1f - mPerDistance)) { 312 | 1f 313 | } else { 314 | (progress - mPerDistance) / (1f - mPerDistance * 2f) 315 | } 316 | 317 | Box(modifier = Modifier.clip(RoundedCornerShape(mCanvasRadius))) { 318 | Canvas( 319 | modifier = Modifier 320 | .width(mCanvasWidth) 321 | .height(mCanvasHeight) 322 | .offset(y = mCanvasHeight * progressY) 323 | .alpha(0.5f), 324 | ) { 325 | for (i in 0..6) { 326 | drawCircle( 327 | color = cloudColorShadow, 328 | radius = mSunCloudRadius.toPx() * offsetRadius[i] + mSunCloudRadius.toPx() * 0.08f * animationOffsetRadius, 329 | center = Offset( 330 | size.width - cloudOffsetX.toPx() * i + baseOffsetX.toPx() - baseOffsetX.toPx() * shadowOffsetX[i] + size.width * 0.05f * animationOffsetX, 331 | size.height / 2f + cloudOffsetY.toPx() * offsetX[i] + baseOffsetY.toPx() + cloudShadowOffsetY.toPx() * shadowOffsetY[i] + size.height / 2f * 0.05f * animationOffsetY, 332 | ), 333 | ) 334 | } 335 | } 336 | 337 | Canvas( 338 | modifier = Modifier 339 | .width(mCanvasWidth) 340 | .height(mCanvasHeight) 341 | .offset(y = mCanvasHeight * progressY), 342 | ) { 343 | for (i in 0..6) { 344 | drawCircle( 345 | color = cloudColor, 346 | radius = mSunCloudRadius.toPx() * offsetRadius[i] + mSunCloudRadius.toPx() * 0.06f * animationOffsetRadius, 347 | center = Offset( 348 | size.width - cloudOffsetX.toPx() * i + baseOffsetX.toPx() + size.width * 0.04f * animationOffsetX, 349 | size.height / 2f + cloudOffsetY.toPx() * offsetX[i] + baseOffsetY.toPx() + size.height / 2f * 0.04f * animationOffsetY, 350 | ), 351 | ) 352 | } 353 | } 354 | } 355 | } 356 | 357 | /** 358 | * get random NightStar info 359 | */ 360 | @Stable 361 | fun getRandomStart(): NightStar { 362 | val star = NightStar() 363 | star.x.value = getRandom(0f, 1f) 364 | star.y.value = getRandom(0f, 1f) 365 | star.radius.value = getRandom(0f, 1f) 366 | star.status.value = NightStarStatus.Start 367 | return star 368 | } 369 | 370 | /** 371 | * Night Stars 372 | */ 373 | @Composable 374 | fun NightStarts(progress: Float) { 375 | if (model.nightStar.isEmpty()) { 376 | model.nightStar.clear() 377 | for (i in 0..10) { 378 | model.nightStar.add(getRandomStart()) 379 | } 380 | } 381 | 382 | // for sun/moon change, this is NightStar move animation 383 | val progressY = if (progress < mPerDistance) { 384 | 0f 385 | } else if (progress > (1f - mPerDistance)) { 386 | 1f 387 | } else { 388 | (progress - mPerDistance) / (1f - mPerDistance * 2f) 389 | } 390 | 391 | for (nightStar in model.nightStar) { 392 | NightStart(nightStar, progressY) 393 | } 394 | } 395 | 396 | /** 397 | * one Night Star 398 | */ 399 | @Composable 400 | fun NightStart(nightStar: NightStar, progress: Float) { 401 | // if NightStar is not lighting, then start lighting animation 402 | if (nightStar.status.value == NightStarStatus.Start) { 403 | nightStar.status.value = NightStarStatus.Lighting 404 | val valueAnimator = ValueAnimator.ofFloat(0f, 1f) 405 | valueAnimator.duration = getRandom(3000, 6000) 406 | valueAnimator.repeatMode = ValueAnimator.REVERSE 407 | valueAnimator.repeatCount = 2 408 | valueAnimator.interpolator = DecelerateInterpolator() 409 | valueAnimator.addUpdateListener { 410 | val value = it.animatedValue as Float 411 | nightStar.alpha.value = value 412 | } 413 | valueAnimator.addListener { 414 | it.doOnEnd { 415 | nightStar.status.value = NightStarStatus.End 416 | model.nightStar.remove(nightStar) 417 | if (model.nightStar.size < 10) { 418 | model.nightStar.add(getRandomStart()) 419 | } 420 | } 421 | } 422 | valueAnimator.start() 423 | } 424 | 425 | // an simple path to draw a little star 426 | Canvas( 427 | modifier = Modifier 428 | .width(mCanvasWidth) 429 | .height(mCanvasHeight) 430 | .offset(y = -mCanvasHeight + mCanvasHeight * progress) 431 | .alpha(nightStar.alpha.value), 432 | ) { 433 | val temp = Pair( 434 | (mCanvasHeight.toPx() - mStarRadius.toPx() * 2f) / 2f + 435 | (mCanvasWidth.toPx() / 2f - (mCanvasHeight.toPx() - mStarRadius.toPx() * 2f) / 2f) * nightStar.x.value, 436 | (mCanvasHeight.toPx() - mStarRadius.toPx() * 2f) / 2f + 437 | (mButtonHeight.toPx() - (mCanvasHeight.toPx() - mStarRadius.toPx() * 2f) / 2f) * nightStar.y.value, 438 | ) 439 | // you can check the start position is not too nearly with other stars 440 | val x = temp.first 441 | val y = temp.second 442 | val radius = 443 | mCanvasHeight.toPx() / 30f + (mCanvasHeight.toPx() / 60f) * nightStar.radius.value 444 | val path = Path() 445 | path.moveTo(x, y + radius) 446 | path.lineTo(x + radius / 3f, y + radius / 3f) 447 | path.lineTo(x + radius, y) 448 | path.lineTo(x + radius / 3f, y - radius / 3f) 449 | path.lineTo(x, y - radius) 450 | path.lineTo(x - radius / 3f, y - radius / 3f) 451 | path.lineTo(x - radius, y) 452 | path.lineTo(x - radius / 3f, y + radius / 3f) 453 | path.close() 454 | 455 | drawPath( 456 | path = path, 457 | color = Color.White, 458 | style = Stroke(width = radius / 2f), 459 | ) 460 | } 461 | } 462 | 463 | /** 464 | * Moon 465 | * with 4 layer, from bottom to top 466 | * 1: top shadow 467 | * 2: moon 468 | * 3: moon crater 469 | * 4: bottom shadow 470 | */ 471 | @Composable 472 | fun Moon(progress: Float = 1f, reversal: Boolean = false) { 473 | val infiniteTransition = rememberInfiniteTransition() 474 | val offset by infiniteTransition.animateFloat( 475 | initialValue = -1f, 476 | targetValue = 1f, 477 | animationSpec = infiniteRepeatable( 478 | animation = tween( 479 | durationMillis = 1000, 480 | easing = LinearEasing, 481 | ), 482 | repeatMode = RepeatMode.Reverse, 483 | ), 484 | ) 485 | 486 | val offsetMoonDown by infiniteTransition.animateFloat( 487 | initialValue = 0f, 488 | targetValue = 1f, 489 | animationSpec = infiniteRepeatable( 490 | animation = tween( 491 | durationMillis = 5000, 492 | easing = LinearEasing, 493 | ), 494 | repeatMode = RepeatMode.Restart, 495 | ), 496 | ) 497 | 498 | // for sun/moon change, this is Moon move animation 499 | val progressX = if (reversal) { 500 | 0.dp 501 | } else { 502 | if (progress <= mPerDistance) { 503 | mStarRadius * 2.5f 504 | } else if (progress >= (1 - mPerDistance)) { 505 | 0.dp 506 | } else { 507 | mStarRadius * 2f - mStarRadius * 2f * (progress - mPerDistance) * (1 / (1 - mPerDistance * 2)) 508 | } 509 | } 510 | 511 | Canvas( 512 | modifier = Modifier 513 | .width(mStarRadius * 2f) 514 | .height(mStarRadius * 2f) 515 | .offset( 516 | x = (mCanvasHeight - mStarRadius * 2f) / 2f + mStarMove * progress, 517 | y = (mCanvasHeight - mStarRadius * 2f) / 2f, 518 | ) 519 | .graphicsLayer(alpha = 0.99f) 520 | .clip(RoundedCornerShape(mCanvasRadius)) 521 | .clipToBounds(), 522 | ) { 523 | // 1: top shadow 524 | with(drawContext.canvas.nativeCanvas) { 525 | val checkPoint = saveLayer(null, null) 526 | drawCircle( 527 | color = mMoonTopShadowColor, 528 | radius = mStarRadius.toPx() + mStarRadius.toPx() * 0.1f, 529 | center = Offset(size.width / 2f + progressX.toPx(), size.height / 2f), 530 | ) 531 | drawCircle( 532 | color = Color.Transparent, 533 | radius = mStarRadius.toPx() * 1.05f, 534 | center = Offset( 535 | size.width / 2f + mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 536 | progressX.toPx(), 537 | size.height / 2f + mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 538 | ), 539 | blendMode = BlendMode.Clear, 540 | ) 541 | restoreToCount(checkPoint) 542 | } 543 | 544 | // 2: moon 545 | drawCircle( 546 | color = mMoonColor, 547 | radius = mStarRadius.toPx() * 1.05f, 548 | center = Offset( 549 | size.width / 2f + mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 550 | progressX.toPx(), 551 | size.height / 2f + mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 552 | ), 553 | ) 554 | 555 | // 3: moon crater 556 | with(drawContext.canvas.nativeCanvas) { 557 | val checkPoint = saveLayer(null, null) 558 | drawCircle( 559 | color = mMoonColor, 560 | radius = mStarRadius.toPx() * 1.05f, 561 | center = Offset( 562 | size.width / 2f + mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 563 | progressX.toPx(), 564 | size.height / 2f + mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 565 | ), 566 | ) 567 | drawCircle( 568 | color = mMoonDownColor, 569 | radius = mStarRadius.toPx() / 3f, 570 | center = Offset( 571 | size.width / 2f - height / 4f + size.width * offsetMoonDown - size.width, 572 | size.height / 5f * 3f, 573 | ), 574 | blendMode = BlendMode.SrcIn, 575 | 576 | ) 577 | drawCircle( 578 | color = mMoonDownColor, 579 | radius = mStarRadius.toPx() / 3f, 580 | center = Offset( 581 | size.width / 2f - height / 4f + size.width * offsetMoonDown, 582 | size.height / 5f * 3f, 583 | ), 584 | blendMode = BlendMode.SrcIn, 585 | ) 586 | // 587 | drawCircle( 588 | color = mMoonDownColor, 589 | radius = mStarRadius.toPx() / 4f, 590 | center = Offset( 591 | size.width / 2f + height / 6f + size.width * offsetMoonDown - size.width, 592 | size.height / 4f * 1f, 593 | ), 594 | blendMode = BlendMode.SrcIn, 595 | ) 596 | 597 | drawCircle( 598 | color = mMoonDownColor, 599 | radius = mStarRadius.toPx() / 4f, 600 | center = Offset( 601 | size.width / 2f + height / 6f + size.width * offsetMoonDown, 602 | size.height / 4f * 1f, 603 | ), 604 | blendMode = BlendMode.SrcIn, 605 | ) 606 | 607 | drawCircle( 608 | color = mMoonDownColor, 609 | radius = mStarRadius.toPx() / 4f, 610 | center = Offset( 611 | size.width / 2f + height / 8f + size.width * offsetMoonDown - size.width, 612 | size.height / 4f * 3f, 613 | ), 614 | blendMode = BlendMode.SrcIn, 615 | ) 616 | 617 | drawCircle( 618 | color = mMoonDownColor, 619 | radius = mStarRadius.toPx() / 4f, 620 | center = Offset( 621 | size.width / 2f + height / 8f + size.width * offsetMoonDown, 622 | size.height / 4f * 3f, 623 | ), 624 | blendMode = BlendMode.SrcIn, 625 | ) 626 | 627 | drawCircle( 628 | color = mMoonDownColor, 629 | radius = mStarRadius.toPx() / 6f, 630 | center = Offset( 631 | height / 8f + size.width * offsetMoonDown - size.width, 632 | size.height / 5f * 1f, 633 | ), 634 | blendMode = BlendMode.SrcIn, 635 | ) 636 | 637 | drawCircle( 638 | color = mMoonDownColor, 639 | radius = mStarRadius.toPx() / 6f, 640 | center = Offset( 641 | height / 8f + size.width * offsetMoonDown, 642 | size.height / 5f * 1f, 643 | ), 644 | blendMode = BlendMode.SrcIn, 645 | ) 646 | 647 | restoreToCount(checkPoint) 648 | } 649 | 650 | // 4: bottom shadow 651 | with(drawContext.canvas.nativeCanvas) { 652 | val checkPoint = saveLayer(null, null) 653 | drawCircle( 654 | color = mMoonBottomShadowColor, 655 | radius = mStarRadius.toPx() + mStarRadius.toPx() * 0.1f, 656 | center = Offset(size.width / 2f + progressX.toPx(), size.height / 2f), 657 | ) 658 | drawCircle( 659 | color = Color.Transparent, 660 | radius = mStarRadius.toPx(), 661 | center = Offset( 662 | size.width / 2f - mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 663 | progressX.toPx(), 664 | size.height / 2f - mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 665 | ), 666 | blendMode = BlendMode.SrcIn, 667 | ) 668 | restoreToCount(checkPoint) 669 | } 670 | } 671 | } 672 | 673 | /** 674 | * Sun 675 | * with 3 layer, from bottom to top: 676 | * 1: top shadow 677 | * 2: sun 678 | * 3: bottom shadow 679 | */ 680 | @Composable 681 | fun Sun(progress: Float = 0f, reversal: Boolean = false) { 682 | val infiniteTransition = rememberInfiniteTransition() 683 | val offset by infiniteTransition.animateFloat( 684 | initialValue = -1f, 685 | targetValue = 1f, 686 | animationSpec = infiniteRepeatable( 687 | animation = tween( 688 | durationMillis = 1000, 689 | easing = LinearEasing, 690 | ), 691 | repeatMode = RepeatMode.Reverse, 692 | ), 693 | ) 694 | 695 | val animationOffsetSun by infiniteTransition.animateFloat( 696 | initialValue = 0f, 697 | targetValue = 1f, 698 | animationSpec = infiniteRepeatable( 699 | animation = tween( 700 | durationMillis = 5000, 701 | easing = LinearEasing, 702 | ), 703 | repeatMode = RepeatMode.Reverse, 704 | ), 705 | ) 706 | 707 | // for sun/moon change, this is Moon move animation 708 | val progressX = if (reversal) { 709 | if (progress <= mPerDistance) { 710 | 0.dp 711 | } else if (progress >= (1 - mPerDistance)) { 712 | mStarRadius * 2.5f 713 | } else { 714 | -mStarRadius * 2f * (progress - mPerDistance) * (1 / (1 - mPerDistance * 2)) 715 | } 716 | } else { 717 | 0.dp 718 | } 719 | 720 | Canvas( 721 | modifier = Modifier 722 | .width(mStarRadius * 2f) 723 | .height(mStarRadius * 2f) 724 | .offset( 725 | x = (mCanvasHeight - mStarRadius * 2f) / 2f + mStarMove * progress, 726 | // x = (mCanvasHeight - mStarRadius * 2f) / 2f, 727 | y = (mCanvasHeight - mStarRadius * 2f) / 2f, 728 | ) 729 | .graphicsLayer(alpha = 0.99f) 730 | .clip(RoundedCornerShape(mCanvasRadius)) 731 | .clipToBounds(), 732 | ) { 733 | // 1: top shadow 734 | with(drawContext.canvas.nativeCanvas) { 735 | val checkPoint = saveLayer(null, null) 736 | drawCircle( 737 | color = mSunTopShadowColor, 738 | radius = mStarRadius.toPx() + mStarRadius.toPx() * 0.1f, 739 | center = Offset(size.width / 2f + progressX.toPx(), size.height / 2f), 740 | ) 741 | drawCircle( 742 | color = Color.Transparent, 743 | radius = mStarRadius.toPx() * 1.05f, 744 | center = Offset( 745 | size.width / 2f + mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 746 | progressX.toPx(), 747 | size.height / 2f + mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 748 | ), 749 | blendMode = BlendMode.Clear, 750 | ) 751 | restoreToCount(checkPoint) 752 | } 753 | 754 | // 2: sun 755 | drawCircle( 756 | color = offsetColor(mSunColor, mSunColorDeep, animationOffsetSun), 757 | radius = mStarRadius.toPx() * 1.05f, 758 | center = Offset( 759 | size.width / 2f + mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 760 | progressX.toPx(), 761 | size.height / 2f + mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 762 | ), 763 | ) 764 | 765 | // 3: bottom shadow 766 | with(drawContext.canvas.nativeCanvas) { 767 | val checkPoint = saveLayer(null, null) 768 | drawCircle( 769 | color = mSunBottomShadowColor, 770 | radius = mStarRadius.toPx() + mStarRadius.toPx() * 0.1f, 771 | center = Offset(size.width / 2f + progressX.toPx(), size.height / 2f), 772 | ) 773 | drawCircle( 774 | color = Color.Transparent, 775 | radius = mStarRadius.toPx(), 776 | center = Offset( 777 | size.width / 2f - mStarRadius.toPx() * 0.05f + mStarRadius.toPx() * 0.005f * offset + 778 | progressX.toPx(), 779 | size.height / 2f - mStarRadius.toPx() * 0.1f + mStarRadius.toPx() * 0.005f * offset, 780 | ), 781 | blendMode = BlendMode.SrcIn, 782 | ) 783 | restoreToCount(checkPoint) 784 | } 785 | } 786 | } 787 | 788 | /** 789 | * A tool for get 2 colors offset color 790 | */ 791 | private fun offsetColor(colorStart: Color, colorEnd: Color, progress: Float): Color { 792 | val offsetColor = if (progress < mPerDistance) { 793 | 0f 794 | } else if (progress > (1 - mPerDistance)) { 795 | 1f 796 | } else { 797 | (progress - mPerDistance) * (1 / (1 - mPerDistance * 2)) 798 | } 799 | val red = 800 | (((colorStart.red + (colorEnd.red - colorStart.red) * offsetColor) * 0xFF).toInt()) 801 | val green = 802 | (((colorStart.green + (colorEnd.green - colorStart.green) * offsetColor) * 0xFF).toInt()) 803 | val blue = 804 | (((colorStart.blue + (colorEnd.blue - colorStart.blue) * offsetColor) * 0xFF).toInt()) 805 | val color = ((0xFF and 0xFF) shl 24) or 806 | ((red and 0xFF) shl 16) or 807 | ((green and 0xFF) shl 8) or 808 | (blue and 0xFF) 809 | return Color(color) 810 | } 811 | 812 | /** 813 | * Background 814 | */ 815 | @Composable 816 | fun Background(progress: Float) { 817 | val infiniteTransition = rememberInfiniteTransition() 818 | val offset1 by infiniteTransition.animateFloat( 819 | initialValue = 0.95f, 820 | targetValue = 1.05f, 821 | animationSpec = infiniteRepeatable( 822 | animation = tween( 823 | durationMillis = getRandom(3000, 5000).toInt(), 824 | easing = LinearEasing, 825 | ), 826 | repeatMode = RepeatMode.Reverse, 827 | ), 828 | ) 829 | val offset2 by infiniteTransition.animateFloat( 830 | initialValue = 0.9f, 831 | targetValue = 1.1f, 832 | animationSpec = infiniteRepeatable( 833 | animation = tween( 834 | durationMillis = getRandom(5000, 7000).toInt(), 835 | easing = LinearEasing, 836 | ), 837 | repeatMode = RepeatMode.Reverse, 838 | ), 839 | ) 840 | val offset3 by infiniteTransition.animateFloat( 841 | initialValue = 0.85f, 842 | targetValue = 1.15f, 843 | animationSpec = infiniteRepeatable( 844 | animation = tween( 845 | durationMillis = getRandom(1000, 10000).toInt(), 846 | easing = LinearEasing, 847 | ), 848 | repeatMode = RepeatMode.Reverse, 849 | ), 850 | ) 851 | 852 | val backgroundMove = mCanvasWidth - (mCanvasHeight - mStarRadius * 2f) - mStarRadius * 2 853 | val offsetX = 854 | (mCanvasHeight - mStarRadius * 2f) / 2f + backgroundMove * progress + mStarRadius 855 | Canvas( 856 | modifier = Modifier 857 | .width(mCanvasWidth) 858 | .height(mCanvasHeight) 859 | .clip(RoundedCornerShape(mCanvasRadius)) 860 | .clipToBounds(), 861 | onDraw = { 862 | val maxRadius = mCanvasWidth.toPx() - mCanvasRadius.toPx() * 1.5f 863 | val minRadius = maxRadius * 0.3f 864 | 865 | drawCircle( 866 | color = offsetColor( 867 | mLightBackgroundColor[0], 868 | mNightBackgroundColor[0], 869 | progress, 870 | ), 871 | radius = maxRadius * 1.2f, 872 | center = Offset( 873 | offsetX.toPx(), 874 | mCanvasHeight.toPx() / 2f, 875 | ), 876 | ) 877 | 878 | drawCircle( 879 | color = offsetColor( 880 | mLightBackgroundColor[1], 881 | mNightBackgroundColor[1], 882 | progress, 883 | ), 884 | radius = (minRadius + (maxRadius - minRadius) / 7f * 4f) * offset1, 885 | center = Offset( 886 | offsetX.toPx(), 887 | mCanvasHeight.toPx() / 2f, 888 | ), 889 | ) 890 | 891 | drawCircle( 892 | color = offsetColor( 893 | mLightBackgroundColor[2], 894 | mNightBackgroundColor[2], 895 | progress, 896 | ), 897 | radius = (minRadius + (maxRadius - minRadius) / 7f * 2f) * offset2, 898 | center = Offset( 899 | offsetX.toPx(), 900 | mCanvasHeight.toPx() / 2f, 901 | ), 902 | ) 903 | 904 | drawCircle( 905 | color = offsetColor( 906 | mLightBackgroundColor[3], 907 | mNightBackgroundColor[3], 908 | progress, 909 | ), 910 | radius = minRadius * offset3, 911 | center = Offset( 912 | offsetX.toPx(), 913 | mCanvasHeight.toPx() / 2f, 914 | ), 915 | ) 916 | }, 917 | ) 918 | } 919 | 920 | private fun getRandom(min: Float, max: Float): Float { 921 | return Random.nextFloat() * (max - min) + min 922 | } 923 | 924 | private fun getRandom(min: Int, max: Int): Long { 925 | return (Random.nextFloat() * (max - min) + min).toLong() 926 | } 927 | } 928 | -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/tree/TreeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.tree 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.animation.core.Easing 9 | import androidx.compose.animation.core.LinearEasing 10 | import androidx.compose.animation.core.RepeatMode 11 | import androidx.compose.animation.core.animateFloat 12 | import androidx.compose.animation.core.infiniteRepeatable 13 | import androidx.compose.animation.core.rememberInfiniteTransition 14 | import androidx.compose.animation.core.tween 15 | import androidx.compose.foundation.Canvas 16 | import androidx.compose.foundation.background 17 | import androidx.compose.foundation.layout.Box 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.Row 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.foundation.layout.fillMaxWidth 22 | import androidx.compose.foundation.layout.height 23 | import androidx.compose.foundation.layout.offset 24 | import androidx.compose.foundation.layout.padding 25 | import androidx.compose.foundation.layout.width 26 | import androidx.compose.foundation.shape.CircleShape 27 | import androidx.compose.material3.Button 28 | import androidx.compose.material3.ButtonDefaults 29 | import androidx.compose.material3.MaterialTheme 30 | import androidx.compose.material3.Surface 31 | import androidx.compose.material3.Text 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.runtime.CompositionLocalProvider 34 | import androidx.compose.runtime.LaunchedEffect 35 | import androidx.compose.runtime.getValue 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.draw.alpha 42 | import androidx.compose.ui.draw.clip 43 | import androidx.compose.ui.draw.rotate 44 | import androidx.compose.ui.geometry.CornerRadius 45 | import androidx.compose.ui.geometry.Offset 46 | import androidx.compose.ui.geometry.Size 47 | import androidx.compose.ui.graphics.Brush 48 | import androidx.compose.ui.graphics.Color 49 | import androidx.compose.ui.graphics.Path 50 | import androidx.compose.ui.graphics.graphicsLayer 51 | import androidx.compose.ui.platform.LocalDensity 52 | import androidx.compose.ui.platform.LocalLayoutDirection 53 | import androidx.compose.ui.unit.Dp 54 | import androidx.compose.ui.unit.LayoutDirection 55 | import androidx.compose.ui.unit.dp 56 | import com.clwater.compose_canvas.ui.theme.AndroidComposeCanvasTheme 57 | import kotlinx.coroutines.delay 58 | import java.util.ArrayDeque 59 | import java.util.Queue 60 | import kotlin.math.cos 61 | import kotlin.math.sin 62 | import kotlin.math.sqrt 63 | import kotlin.random.Random 64 | 65 | 66 | // Tree Node Type 67 | enum class TreeType { 68 | TREE, 69 | FLOWER, 70 | FRUIT, 71 | } 72 | 73 | enum class Season { 74 | Spring, 75 | Summer, 76 | Autumn, 77 | Winter, 78 | } 79 | 80 | // data Class TreeNode 81 | data class TreeNode( 82 | var deep: Int = 0, 83 | var angle: Float = 0f, 84 | var type: TreeType = TreeType.TREE, 85 | var child: List = listOf(), 86 | 87 | var length: Dp = 0.dp, 88 | 89 | // Increased in a loop rather than recursively 90 | var startOffset: Offset = Offset(0f, 0f) 91 | ) 92 | 93 | data class LightNode( 94 | var offset: Offset = Offset(0f, 0f), 95 | var next: LightNode? = null 96 | ) 97 | 98 | 99 | class TreeActivity : ComponentActivity() { 100 | companion object { 101 | fun start(context: Context) { 102 | context.startActivity(Intent(context, TreeActivity::class.java)) 103 | } 104 | 105 | // const Color 106 | val cloudColor = Color(0xFFF5F5F5) 107 | val treeColor = Color(0xFF412e1f) 108 | val flowerColor = Color(0xFFFFFFFF) 109 | val flowerColorAutumn = Color(0xFF128604) 110 | val fruitColor = Color(0xFFe66e4a) 111 | val fruitColorEnd = Color(0x1AE66E4A) 112 | val seasonSpring = Color(0xFF7FDF69) 113 | val seasonSummer = Color(0xFFEE4F4F) 114 | val seasonAutumn = Color(0xFFE6A23C) 115 | val seasonWinter = Color(0xFFB8CAC6) 116 | 117 | val skyColorSpring = Color(0xFF69ADA3) 118 | val landColorSpring = Color(0xFF59C255) 119 | val rainColor = Color(0x99CCD5CC) 120 | val lightColor = Color(0xFFEED709) 121 | val lightSkyColor = Color(0xFF70CFC1) 122 | 123 | val skyColorSummer = Color(0xFF4D59AF) 124 | val landColorSummer = Color(0xFF1E1F44) 125 | val skyColorAutumn = Color(0xFFFAC164) 126 | val landColorAutumn = Color(0xFF612D1C) 127 | val skyColorWinter = Color(0xFF9dbeb7) 128 | val landColorWinter = Color(0xFFE7EEEC) 129 | } 130 | 131 | private lateinit var random: Random 132 | 133 | private var mBaseCircle = 0.dp 134 | private var mBaseCirclePx = 0f 135 | private var flowerCount = 0 136 | private var minLength: Float = 0.0f 137 | 138 | 139 | override fun onCreate(savedInstanceState: Bundle?) { 140 | super.onCreate(savedInstanceState) 141 | 142 | setContent { 143 | AndroidComposeCanvasTheme { 144 | // A surface container using the 'background' color from the theme 145 | Surface( 146 | modifier = Modifier.fillMaxSize(), 147 | color = MaterialTheme.colorScheme.background 148 | ) { 149 | TreeLayout() 150 | } 151 | } 152 | } 153 | } 154 | 155 | // Generate new Mei Tree 156 | private fun genNewTrees(seed: Int): TreeNode { 157 | random = Random(seed) 158 | val treeNode = TreeNode() 159 | treeNode.angle = 0f 160 | treeNode.deep = 0 161 | treeNode.type = TreeType.TREE 162 | treeNode.length = mBaseCircle / 4f 163 | 164 | for (i in 0 until random.nextInt(3) + 1) { 165 | treeNode.child += genNewTree(1, treeNode.length) 166 | } 167 | return treeNode 168 | } 169 | 170 | // recursively new tree node 171 | private fun genNewTree(deep: Int, length: Dp): TreeNode { 172 | val treeNode = TreeNode() 173 | 174 | treeNode.deep = deep 175 | 176 | if (length < minLength.dp) { 177 | flowerCount++ 178 | treeNode.type = if (flowerCount % 100 == 0) { 179 | TreeType.FRUIT 180 | } else { 181 | TreeType.FLOWER 182 | } 183 | return treeNode 184 | } 185 | 186 | treeNode.type = TreeType.TREE 187 | 188 | treeNode.length = length * (random.nextInt(2) / 10f + 0.6f) 189 | treeNode.angle = 190 | (if (random.nextFloat() > 0.5f) 1f else -1f) * (random.nextInt(20 + deep * 5) + 45) 191 | for (i in 0 until random.nextInt(3) + 1) { 192 | treeNode.child += genNewTree(deep + 1, treeNode.length) 193 | } 194 | 195 | return treeNode 196 | } 197 | 198 | @Composable 199 | fun TreeLayout() { 200 | with(LocalDensity.current) { 201 | mBaseCircle = resources.displayMetrics.widthPixels.toFloat().toDp() * 0.9f 202 | mBaseCirclePx = mBaseCircle.toPx() 203 | } 204 | 205 | var season by remember { 206 | mutableStateOf(Season.Winter) 207 | } 208 | 209 | var seed by remember { 210 | mutableStateOf(-1) 211 | } 212 | 213 | random = Random(seed) 214 | minLength = mBaseCircle.value / 40f 215 | 216 | Column { 217 | Box( 218 | modifier = Modifier 219 | .weight(1f) 220 | .fillMaxWidth(), 221 | contentAlignment = Alignment.Center 222 | ) { 223 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { 224 | TreeCanvas(seed, season) 225 | } 226 | } 227 | 228 | Column { 229 | Button(onClick = { 230 | seed = random.nextInt(1000) 231 | }) { 232 | Text( 233 | text = "Generate New Tree", 234 | ) 235 | } 236 | Column( 237 | modifier = Modifier.padding(vertical = 4.dp) 238 | ) { 239 | Row { 240 | Button( 241 | onClick = { 242 | season = Season.Spring 243 | }, 244 | modifier = Modifier 245 | .weight(1f), 246 | colors = ButtonDefaults.textButtonColors( 247 | containerColor = if (season == Season.Spring) { 248 | seasonSpring 249 | } else { 250 | MaterialTheme.colorScheme.primary 251 | }, 252 | contentColor = Color.White 253 | ) 254 | ) { 255 | Text( 256 | text = "Spring", 257 | ) 258 | } 259 | 260 | Button( 261 | onClick = { 262 | season = Season.Summer 263 | }, 264 | modifier = Modifier 265 | .weight(1f), 266 | colors = ButtonDefaults.textButtonColors( 267 | containerColor = if (season == Season.Summer) { 268 | seasonSummer 269 | } else { 270 | MaterialTheme.colorScheme.primary 271 | }, 272 | contentColor = Color.White 273 | ) 274 | ) { 275 | Text( 276 | text = "Summer", 277 | ) 278 | } 279 | } 280 | 281 | 282 | Row { 283 | Button( 284 | onClick = { 285 | season = Season.Autumn 286 | }, 287 | modifier = Modifier 288 | .weight(1f), 289 | colors = ButtonDefaults.textButtonColors( 290 | containerColor = if (season == Season.Autumn) { 291 | seasonAutumn 292 | } else { 293 | MaterialTheme.colorScheme.primary 294 | }, 295 | contentColor = Color.White 296 | ) 297 | ) { 298 | Text( 299 | text = "Autumn", 300 | ) 301 | } 302 | 303 | Button( 304 | onClick = { 305 | season = Season.Winter 306 | }, 307 | modifier = Modifier 308 | .weight(1f), 309 | colors = ButtonDefaults.textButtonColors( 310 | containerColor = if (season == Season.Winter) { 311 | seasonWinter 312 | } else { 313 | MaterialTheme.colorScheme.primary 314 | }, 315 | contentColor = Color.White 316 | ) 317 | ) { 318 | Text( 319 | text = "Winter", 320 | ) 321 | } 322 | } 323 | 324 | 325 | } 326 | 327 | } 328 | 329 | } 330 | 331 | } 332 | 333 | @Composable 334 | fun TreeCanvas(seed: Int, season: Season) { 335 | Box( 336 | modifier = Modifier 337 | .width(mBaseCircle) 338 | .height(mBaseCircle) 339 | .clip(CircleShape) 340 | .background( 341 | when (season) { 342 | Season.Spring -> skyColorSpring 343 | Season.Summer -> skyColorSummer 344 | Season.Autumn -> skyColorAutumn 345 | Season.Winter -> skyColorWinter 346 | } 347 | ), 348 | contentAlignment = Alignment.Center 349 | ) { 350 | when (season) { 351 | Season.Spring -> { 352 | Light() 353 | SpringRain() 354 | } 355 | 356 | Season.Autumn -> { 357 | Cloud_1() 358 | Cloud_2() 359 | } 360 | 361 | Season.Summer -> { 362 | Starts(seed) 363 | Meteor() 364 | } 365 | 366 | Season.Winter -> { 367 | Snows(seed) 368 | SnowMan() 369 | } 370 | 371 | } 372 | TreeLand(season) 373 | Tree(seed, season) 374 | } 375 | 376 | 377 | } 378 | 379 | @Composable 380 | fun SnowMan() { 381 | val delayTime = 4000L 382 | val runTime = 4000L 383 | var showSnowMan by remember { 384 | mutableStateOf(false) 385 | } 386 | 387 | val infiniteTransition = rememberInfiniteTransition() 388 | val offset by infiniteTransition.animateFloat( 389 | initialValue = -1f, 390 | targetValue = 1f, 391 | animationSpec = infiniteRepeatable( 392 | animation = tween( 393 | durationMillis = runTime.toInt(), 394 | easing = LinearEasing, 395 | ), 396 | repeatMode = RepeatMode.Restart, 397 | ), 398 | ) 399 | 400 | LaunchedEffect(Unit) { 401 | while (true) { 402 | delay(delayTime) 403 | showSnowMan = true 404 | delay(runTime) 405 | showSnowMan = false 406 | } 407 | } 408 | 409 | if (!showSnowMan) { 410 | return 411 | } 412 | 413 | Canvas( 414 | modifier = Modifier 415 | .width(mBaseCircle) 416 | .height(mBaseCircle) 417 | .rotate(90 * offset) 418 | .offset(x = mBaseCircle / 2f, y = mBaseCircle / 4f * 3 + mBaseCircle / 20f), 419 | 420 | 421 | ) 422 | { 423 | drawCircle( 424 | color = Color.White, 425 | radius = mBaseCirclePx / 20f, 426 | center = Offset( 427 | x = (0f + sin(Math.toRadians(90.0) * offset) * mBaseCirclePx / 2f).toFloat() + mBaseCirclePx / 4f, 428 | y = (0f + -cos(Math.toRadians(90.0) * offset) * mBaseCirclePx / 4f).toFloat() + mBaseCirclePx / 4f, 429 | ), 430 | ) 431 | drawCircle( 432 | color = Color.White, 433 | radius = mBaseCirclePx / 30f, 434 | center = Offset( 435 | x = (0f + sin(Math.toRadians(90.0) * offset) * mBaseCirclePx / 2f).toFloat() + mBaseCirclePx / 4f, 436 | y = (0f + -cos(Math.toRadians(90.0) * offset) * mBaseCirclePx / 4f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 15f, 437 | ), 438 | ) 439 | // eyes 440 | drawCircle( 441 | color = Color.Black, 442 | radius = mBaseCirclePx / 30f / 5f, 443 | center = Offset( 444 | x = (0f + sin(Math.toRadians(90.0) * offset) * mBaseCirclePx / 2f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 30f / 2f, 445 | y = (0f + -cos(Math.toRadians(90.0) * offset) * mBaseCirclePx / 4f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 15f - mBaseCirclePx / 30f / 3f, 446 | ), 447 | ) 448 | drawCircle( 449 | color = Color.Black, 450 | radius = mBaseCirclePx / 30f / 5f, 451 | center = Offset( 452 | x = (0f + sin(Math.toRadians(90.0) * offset) * mBaseCirclePx / 2f).toFloat() + mBaseCirclePx / 4f + mBaseCirclePx / 30f / 2f, 453 | y = (0f + -cos(Math.toRadians(90.0) * offset) * mBaseCirclePx / 4f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 15f - mBaseCirclePx / 30f / 3f, 454 | ), 455 | ) 456 | drawCircle( 457 | color = Color.Red, 458 | radius = mBaseCirclePx / 30f / 10f, 459 | center = Offset( 460 | x = (0f + sin(Math.toRadians(90.0) * offset) * mBaseCirclePx / 2f).toFloat() + mBaseCirclePx / 4f, 461 | y = (0f + -cos(Math.toRadians(90.0) * offset) * mBaseCirclePx / 4f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 15f - mBaseCirclePx / 30f / 6f, 462 | ), 463 | ) 464 | 465 | drawArc( 466 | color = Color.Black, 467 | startAngle = 0f, 468 | sweepAngle = 180f, 469 | useCenter = false, 470 | topLeft = Offset( 471 | x = (0f + sin(Math.toRadians(90.0) * offset) * mBaseCirclePx / 2f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 40f / 2f, 472 | y = (0f + -cos(Math.toRadians(90.0) * offset) * mBaseCirclePx / 4f).toFloat() + mBaseCirclePx / 4f - mBaseCirclePx / 15f - mBaseCirclePx / 30f / 3f, 473 | ), 474 | size = Size(mBaseCirclePx / 40f, mBaseCirclePx / 40f), 475 | 476 | ) 477 | 478 | } 479 | } 480 | 481 | @Composable 482 | fun Snows(seed: Int) { 483 | val maxSnow = 200 484 | val infiniteTransition = rememberInfiniteTransition() 485 | val offsetYList: MutableList = mutableListOf() 486 | val offsetList = mutableMapOf() 487 | random = Random(seed) 488 | for (i in 0..maxSnow) { 489 | offsetList[i] = Offset( 490 | -mBaseCirclePx / 2f + random.nextInt(mBaseCirclePx.toInt()), 491 | -mBaseCirclePx / 10f 492 | ) 493 | } 494 | 495 | 496 | for (i in 0..maxSnow) { 497 | val offsetY: Float by infiniteTransition.animateFloat( 498 | initialValue = 0f, 499 | targetValue = 1f, 500 | animationSpec = infiniteRepeatable( 501 | animation = tween( 502 | durationMillis = 3011, 503 | easing = LinearEasing, 504 | delayMillis = (random.nextInt(3011 * 2) - 3011 / 2f).toInt() 505 | ), 506 | repeatMode = RepeatMode.Restart, 507 | ), 508 | ) 509 | offsetYList.add(offsetY) 510 | } 511 | 512 | Canvas( 513 | modifier = Modifier 514 | .width(mBaseCircle) 515 | .height(mBaseCircle) 516 | .offset(x = mBaseCircle / 2f, y = 0.dp), 517 | ) { 518 | for (i in 0..maxSnow) { 519 | 520 | drawCircle( 521 | color = Color.White, 522 | radius = 5f, 523 | center = Offset( 524 | x = offsetList[i]!!.x, 525 | y = offsetList[i]!!.y + mBaseCirclePx * offsetYList[i] 526 | ), 527 | ) 528 | } 529 | } 530 | 531 | } 532 | 533 | @Composable 534 | fun Meteor() { 535 | val delayTime = 3000L 536 | val runTime = 800L 537 | var showMeteor by remember { 538 | mutableStateOf(true) 539 | } 540 | var rotate by remember { 541 | mutableStateOf(0f) 542 | } 543 | var offsetY by remember { 544 | mutableStateOf(0f) 545 | } 546 | 547 | 548 | val infiniteTransition = rememberInfiniteTransition() 549 | val offset by infiniteTransition.animateFloat( 550 | initialValue = -1f, 551 | targetValue = 1f, 552 | animationSpec = infiniteRepeatable( 553 | animation = tween( 554 | durationMillis = runTime.toInt(), 555 | easing = LinearEasing, 556 | delayMillis = delayTime.toInt() 557 | ), 558 | repeatMode = RepeatMode.Restart, 559 | ), 560 | 561 | ) 562 | 563 | LaunchedEffect(Unit) { 564 | while (true) { 565 | delay(delayTime) 566 | showMeteor = true 567 | rotate = -30 + Random(rotate.toInt()).nextInt(90).toFloat() 568 | offsetY = 569 | -mBaseCirclePx / 3f + Random(offsetY.toInt()).nextInt(10) / 10f * mBaseCirclePx / 6f 570 | delay(runTime) 571 | showMeteor = false 572 | } 573 | } 574 | 575 | Canvas( 576 | modifier = Modifier 577 | .width(mBaseCircle) 578 | .height(mBaseCircle) 579 | .rotate(rotate) 580 | .offset(mBaseCircle / 2f, mBaseCircle / 2f) 581 | .graphicsLayer { 582 | translationX = mBaseCircle.toPx() * offset 583 | translationY = offsetY 584 | }, 585 | 586 | ) { 587 | if (showMeteor) { 588 | var meterSize = 15f 589 | var meterOffset = -meterSize / 2 590 | 591 | for (i in 0..10) { 592 | meterSize *= 0.8f 593 | if (meterSize < 5f) { 594 | meterSize = 5f 595 | } 596 | val path = Path() 597 | path.moveTo(meterOffset + 0f, meterSize) 598 | path.lineTo(meterOffset + meterSize, 0f) 599 | path.lineTo(meterOffset + 0f, -meterSize) 600 | path.lineTo(meterOffset + -meterSize, 0f) 601 | path.lineTo(meterOffset + 0f, meterSize) 602 | drawPath(path = path, color = Color.White) 603 | 604 | meterOffset += if (i < 3) -meterSize else -meterSize * random.nextInt(3) + 1 605 | } 606 | } 607 | } 608 | } 609 | 610 | @Composable 611 | fun Starts(seed: Int) { 612 | val maxStart = 50 613 | val infiniteTransition = rememberInfiniteTransition() 614 | val alphaList: MutableList = mutableListOf() 615 | val offsetList = mutableMapOf() 616 | random = Random(seed) 617 | for (i in 0..maxStart) { 618 | offsetList[i] = Offset( 619 | -mBaseCirclePx / 4f + random.nextInt(mBaseCirclePx.toInt()) / 4f * 3f, 620 | -mBaseCirclePx / 4f + random.nextInt(mBaseCirclePx.toInt()) / 4f * 3f, 621 | ) 622 | } 623 | 624 | 625 | for (i in 0..maxStart) { 626 | val alpha: Float by infiniteTransition.animateFloat( 627 | initialValue = 0f, 628 | targetValue = 1f, 629 | animationSpec = infiniteRepeatable( 630 | animation = tween( 631 | durationMillis = 3011, 632 | easing = LinearEasing, 633 | delayMillis = if (random.nextInt(3011 * 2) - 3011 / 2f < 0) { 634 | 0 635 | } else { 636 | random.nextInt(3011 * 2) - 3011 / 2f 637 | }.toInt(), 638 | ), 639 | repeatMode = RepeatMode.Reverse, 640 | ), 641 | ) 642 | alphaList.add(alpha) 643 | } 644 | 645 | 646 | Canvas( 647 | modifier = Modifier 648 | .width(mBaseCircle) 649 | .height(mBaseCircle) 650 | .offset(x = mBaseCircle / 2f, y = mBaseCircle / 2f), 651 | ) { 652 | for (i in 0..maxStart) { 653 | 654 | val startLengthOut = mBaseCirclePx / 70f 655 | val startOffsetOut = startLengthOut / 3F 656 | 657 | val pathOut = Path() 658 | pathOut.moveTo(offsetList[i]!!.x + 0F, offsetList[i]!!.y + startLengthOut) 659 | pathOut.lineTo( 660 | offsetList[i]!!.x + startOffsetOut, 661 | offsetList[i]!!.y + startOffsetOut 662 | ) 663 | pathOut.lineTo(offsetList[i]!!.x + startLengthOut, offsetList[i]!!.y + 0F) 664 | pathOut.lineTo( 665 | offsetList[i]!!.x + startOffsetOut, 666 | offsetList[i]!!.y + -startOffsetOut 667 | ) 668 | pathOut.lineTo(offsetList[i]!!.x + 0F, offsetList[i]!!.y + -startLengthOut) 669 | pathOut.lineTo( 670 | offsetList[i]!!.x + -startOffsetOut, 671 | offsetList[i]!!.y + -startOffsetOut 672 | ) 673 | pathOut.lineTo(offsetList[i]!!.x + -startLengthOut, offsetList[i]!!.y + 0F) 674 | pathOut.lineTo( 675 | offsetList[i]!!.x + -startOffsetOut, 676 | offsetList[i]!!.y + startOffsetOut 677 | ) 678 | pathOut.lineTo(offsetList[i]!!.x + 0F, offsetList[i]!!.y + startLengthOut) 679 | 680 | drawPath(path = pathOut, color = Color.White, alpha = alphaList[i]) 681 | 682 | 683 | val startLengthInner = startLengthOut * 0.3f 684 | val startOffsetInner = startLengthInner / 3F 685 | 686 | val pathInner = Path() 687 | pathInner.moveTo(offsetList[i]!!.x + 0F, offsetList[i]!!.y + startLengthInner) 688 | pathInner.lineTo( 689 | offsetList[i]!!.x + startOffsetInner, 690 | offsetList[i]!!.y + startOffsetInner 691 | ) 692 | pathInner.lineTo(offsetList[i]!!.x + startLengthInner, offsetList[i]!!.y + 0F) 693 | pathInner.lineTo( 694 | offsetList[i]!!.x + startOffsetInner, 695 | offsetList[i]!!.y + -startOffsetInner 696 | ) 697 | pathInner.lineTo(offsetList[i]!!.x + 0F, offsetList[i]!!.y + -startLengthInner) 698 | pathInner.lineTo( 699 | offsetList[i]!!.x + -startOffsetInner, 700 | offsetList[i]!!.y + -startOffsetInner 701 | ) 702 | pathInner.lineTo(offsetList[i]!!.x + -startLengthInner, offsetList[i]!!.y + 0F) 703 | pathInner.lineTo( 704 | offsetList[i]!!.x + -startOffsetInner, 705 | offsetList[i]!!.y + startOffsetInner 706 | ) 707 | pathInner.lineTo(offsetList[i]!!.x + 0F, offsetList[i]!!.y + startLengthInner) 708 | 709 | drawPath(path = pathInner, color = skyColorSummer, alpha = alphaList[i]) 710 | 711 | 712 | } 713 | } 714 | } 715 | 716 | @Composable 717 | fun Light() { 718 | var showLight by remember { 719 | mutableStateOf(false) 720 | } 721 | var lights by remember { 722 | mutableStateOf(LightNode()) 723 | } 724 | 725 | lights = generateLights(mBaseCirclePx) 726 | 727 | 728 | LaunchedEffect(Unit) { 729 | while (true) { 730 | delay(3000) 731 | showLight = true 732 | delay(300) 733 | showLight = false 734 | } 735 | } 736 | 737 | 738 | 739 | if (showLight) { 740 | Canvas( 741 | modifier = Modifier 742 | .width(mBaseCircle) 743 | .height(mBaseCircle) 744 | .background(if (showLight) lightSkyColor else Color.Transparent) 745 | .offset(mBaseCircle / 2f, mBaseCircle) 746 | 747 | ) { 748 | var currentLight = lights 749 | while (currentLight.next != null) { 750 | drawLine( 751 | color = lightColor, 752 | start = currentLight.offset, 753 | end = currentLight.next!!.offset, 754 | strokeWidth = 8f, 755 | ) 756 | currentLight = currentLight.next!! 757 | } 758 | } 759 | } 760 | 761 | } 762 | 763 | private fun generateLights(light: LightNode): LightNode { 764 | if (light.next == null) { 765 | return light 766 | } 767 | val next = light.next!! 768 | 769 | val distance = next.offset - light.offset 770 | 771 | if (distance.x * distance.x + distance.y * distance.y > 100) { 772 | val newLight = LightNode() 773 | newLight.offset = Offset( 774 | x = light.offset.x + distance.x / 2f, 775 | y = light.offset.y + distance.y / 2f, 776 | ) 777 | 778 | val newDistanceOffset = newLight.offset - light.offset 779 | val newDistance = 780 | sqrt((newDistanceOffset.x * newDistanceOffset.x + newDistanceOffset.y * newDistanceOffset.y).toDouble()) / 2f 781 | 782 | newLight.offset = Offset( 783 | x = (newLight.offset.x + newDistance * sin( 784 | Math.toRadians( 785 | random.nextInt(360).toDouble() 786 | ) 787 | )).toFloat(), 788 | y = (newLight.offset.y + newDistance * cos( 789 | Math.toRadians( 790 | random.nextInt(360).toDouble() 791 | ) 792 | )).toFloat() 793 | 794 | ) 795 | newLight.next = next 796 | light.next = newLight 797 | return generateLights(light) 798 | } else { 799 | light.next = generateLights(next) 800 | return light 801 | } 802 | } 803 | 804 | private fun generateLights(height: Float): LightNode { 805 | 806 | var lights = LightNode() 807 | lights.offset = Offset(0f, -height) 808 | 809 | val next = LightNode() 810 | next.offset = Offset(0f, 0f) 811 | lights.next = next 812 | 813 | lights = generateLights(lights) 814 | 815 | return lights 816 | } 817 | 818 | @Composable 819 | private fun SpringRain() { 820 | val infiniteTransition = rememberInfiniteTransition() 821 | val offset by infiniteTransition.animateFloat( 822 | initialValue = -1f, 823 | targetValue = 1f, 824 | animationSpec = infiniteRepeatable( 825 | animation = tween( 826 | durationMillis = 4000, 827 | easing = LinearEasing, 828 | ), 829 | repeatMode = RepeatMode.Restart, 830 | ), 831 | ) 832 | val maxRains = 100 833 | val rainOffset = mutableMapOf() 834 | 835 | for (index in 0 until maxRains) { 836 | rainOffset[index] = Offset( 837 | x = -2f * mBaseCircle.value + 4f * random.nextInt(mBaseCircle.value.toInt()), 838 | y = -1f * mBaseCircle.value + 2f * random.nextInt(mBaseCircle.value.toInt()) 839 | ) 840 | } 841 | 842 | Canvas( 843 | modifier = Modifier 844 | .width(mBaseCircle) 845 | .height(mBaseCircle) 846 | .offset(mBaseCircle / 2f, mBaseCircle / 2f) 847 | .rotate(10f) 848 | .graphicsLayer { 849 | }, 850 | 851 | ) { 852 | for (i in -2..2) { 853 | for (j in 0 until maxRains) { 854 | drawRoundRect( 855 | color = rainColor, 856 | size = Size(mBaseCirclePx / 400f, mBaseCirclePx / 20f), 857 | cornerRadius = CornerRadius(size.minDimension / 2f), 858 | topLeft = Offset( 859 | x = rainOffset[j]!!.x, 860 | y = mBaseCircle.value * offset + i * mBaseCircle.value + rainOffset[j]!!.y 861 | ), 862 | ) 863 | } 864 | } 865 | } 866 | } 867 | 868 | @Composable 869 | fun Tree(seed: Int, season: Season) { 870 | 871 | val infiniteTransition = rememberInfiniteTransition() 872 | 873 | val offsetPosition: Float by infiniteTransition.animateFloat( 874 | initialValue = 0f, 875 | targetValue = 1f, 876 | animationSpec = infiniteRepeatable( 877 | animation = tween( 878 | durationMillis = 1001, 879 | easing = EaseOutBounce, 880 | delayMillis = 4000, 881 | ), 882 | repeatMode = RepeatMode.Restart, 883 | ), 884 | ) 885 | 886 | 887 | 888 | val tree = genNewTrees(seed) 889 | val baseTreeLength = mBaseCircle / 4f 890 | Canvas( 891 | modifier = Modifier 892 | .width(mBaseCircle) 893 | .height(mBaseCircle) 894 | .offset(mBaseCircle / 2f, mBaseCircle), 895 | 896 | ) { 897 | 898 | drawLine( 899 | color = treeColor, 900 | start = Offset(x = 0f, y = -mBaseCirclePx / 20f), 901 | end = Offset(0f, -baseTreeLength.toPx() - mBaseCirclePx / 20f), 902 | strokeWidth = 10f, 903 | ) 904 | val treeQueue: Queue = ArrayDeque() 905 | val flowerQueue: Queue = ArrayDeque() 906 | val fruitQueue: Queue = ArrayDeque() 907 | var downTreeNode: TreeNode? = null 908 | 909 | 910 | for (treeNode in tree.child) { 911 | treeNode.startOffset = Offset(0f, -baseTreeLength.toPx() - mBaseCirclePx / 20f) 912 | treeQueue.offer(treeNode) 913 | } 914 | 915 | // Increased in a loop rather than recursively 916 | while (treeQueue.isNotEmpty()) { 917 | val treeNode = treeQueue.poll() ?: break 918 | val angle = treeNode.angle 919 | val deep = treeNode.deep 920 | val type = treeNode.type 921 | val length = treeNode.length 922 | 923 | if (type == TreeType.TREE) { 924 | var treeWidth = 15f 925 | for (i in 0..deep) { 926 | treeWidth *= 0.8f 927 | } 928 | 929 | // calculate the position for child node 930 | val startOffset = treeNode.startOffset 931 | val currentEnd = Offset( 932 | x = startOffset.x + length.toPx() * sin(Math.toRadians(angle.toDouble())) 933 | .toFloat(), 934 | y = startOffset.y - length.toPx() * cos(Math.toRadians(angle.toDouble())) 935 | .toFloat(), 936 | ) 937 | 938 | drawLine( 939 | color = treeColor, 940 | start = startOffset, 941 | end = currentEnd, 942 | strokeWidth = treeWidth, 943 | ) 944 | treeNode.child.forEach { 945 | it.startOffset = currentEnd 946 | treeQueue.offer(it) 947 | } 948 | } 949 | 950 | // offer the flower/fruit child to queue 951 | if (type == TreeType.FLOWER) { 952 | if (downTreeNode == null) { 953 | downTreeNode = treeNode 954 | } 955 | flowerQueue.offer(treeNode) 956 | } else if (type == TreeType.FRUIT && season != Season.Autumn) { 957 | fruitQueue.offer(treeNode) 958 | } 959 | } 960 | 961 | // draw flowers 962 | if (season == Season.Summer) { 963 | return@Canvas 964 | } 965 | while (flowerQueue.isNotEmpty()) { 966 | val treeNode = flowerQueue.poll() ?: break 967 | if (season != Season.Autumn) { 968 | drawCircle( 969 | color = flowerColor, 970 | radius = 10f, 971 | center = treeNode.startOffset, 972 | ) 973 | } else { 974 | drawCircle( 975 | color = flowerColorAutumn, 976 | radius = 6f, 977 | center = treeNode.startOffset, 978 | ) 979 | } 980 | 981 | } 982 | 983 | while (fruitQueue.isNotEmpty()) { 984 | val treeNode = fruitQueue.poll() ?: break 985 | drawCircle( 986 | brush = Brush.radialGradient( 987 | 0.0f to fruitColor, 988 | 0.5f to fruitColor, 989 | 1f to fruitColorEnd, 990 | center = treeNode.startOffset, 991 | radius = 20f 992 | 993 | ), 994 | center = treeNode.startOffset, 995 | radius = 20f 996 | ) 997 | 998 | } 999 | if (season == Season.Autumn){ 1000 | downTreeNode?.let { 1001 | drawCircle( 1002 | color = flowerColorAutumn, 1003 | radius = 6f, 1004 | center = Offset(x = it.startOffset.x, 1005 | y = it.startOffset.y + mBaseCirclePx / 3f * offsetPosition), 1006 | ) 1007 | } 1008 | } 1009 | 1010 | } 1011 | 1012 | } 1013 | 1014 | @Composable 1015 | fun Cloud_1() { 1016 | val infiniteTransition = rememberInfiniteTransition() 1017 | val offset by infiniteTransition.animateFloat( 1018 | initialValue = -0.7f, 1019 | targetValue = 0.7f, 1020 | animationSpec = infiniteRepeatable( 1021 | animation = tween( 1022 | durationMillis = 4003, 1023 | easing = LinearEasing, 1024 | ), 1025 | repeatMode = RepeatMode.Reverse, 1026 | ), 1027 | ) 1028 | 1029 | 1030 | Canvas( 1031 | modifier = Modifier 1032 | .width(mBaseCircle) 1033 | .height(mBaseCircle) 1034 | .offset( 1035 | x = mBaseCircle / 2f * offset, 1036 | y = -mBaseCircle / 6f, 1037 | ) 1038 | .alpha(0.8f), 1039 | ) 1040 | { 1041 | drawRoundRect( 1042 | color = cloudColor, 1043 | size = Size(width = size.width / 7f * 4f, height = size.height / 4f), 1044 | cornerRadius = CornerRadius(size.minDimension / 2f), 1045 | topLeft = Offset(x = center.x - size.width / 4f, y = center.y), 1046 | ) 1047 | drawCircle( 1048 | color = cloudColor, 1049 | radius = size.minDimension / 10f, 1050 | center = Offset(x = center.x - size.width / 20f, y = center.y + size.height / 40f), 1051 | ) 1052 | drawCircle( 1053 | color = cloudColor, 1054 | radius = size.minDimension / 8f, 1055 | center = Offset(x = center.x + size.width / 10f, y = center.y + size.height / 40f), 1056 | ) 1057 | } 1058 | } 1059 | 1060 | @Composable 1061 | fun Cloud_2() { 1062 | val infiniteTransition = rememberInfiniteTransition() 1063 | val offset by infiniteTransition.animateFloat( 1064 | initialValue = -0.8f, 1065 | targetValue = 0.8f, 1066 | animationSpec = infiniteRepeatable( 1067 | animation = tween( 1068 | durationMillis = 5007, 1069 | easing = LinearEasing, 1070 | ), 1071 | repeatMode = RepeatMode.Reverse, 1072 | ), 1073 | ) 1074 | 1075 | 1076 | Canvas( 1077 | modifier = Modifier 1078 | .width(mBaseCircle) 1079 | .height(mBaseCircle) 1080 | .offset( 1081 | x = mBaseCircle / 2f * offset, 1082 | y = -mBaseCircle / 3f, 1083 | ) 1084 | .alpha(0.8f), 1085 | ) 1086 | { 1087 | drawRoundRect( 1088 | color = cloudColor, 1089 | size = Size(width = size.width / 7f * 6f, height = size.height / 4f * 1.25f), 1090 | cornerRadius = CornerRadius(size.minDimension / 2f), 1091 | topLeft = Offset(x = center.x - size.width / 2f, y = center.y), 1092 | ) 1093 | drawCircle( 1094 | color = cloudColor, 1095 | radius = size.minDimension / 6f, 1096 | center = Offset(x = center.x - size.width / 5f, y = center.y + size.height / 40f), 1097 | ) 1098 | drawCircle( 1099 | color = cloudColor, 1100 | radius = size.minDimension / 5f, 1101 | center = Offset(x = center.x + size.width / 10f, y = center.y + size.height / 40f), 1102 | ) 1103 | } 1104 | } 1105 | 1106 | @Composable 1107 | fun TreeLand(season: Season) { 1108 | Canvas( 1109 | modifier = Modifier 1110 | .width(mBaseCircle) 1111 | .height(mBaseCircle) 1112 | .offset(y = mBaseCircle / 4f * 3), 1113 | ) { 1114 | drawCircle( 1115 | color = when (season) { 1116 | Season.Spring -> landColorSpring 1117 | Season.Summer -> landColorSummer 1118 | Season.Autumn -> landColorAutumn 1119 | Season.Winter -> landColorWinter 1120 | }, 1121 | radius = size.minDimension / 2f 1122 | ) 1123 | } 1124 | } 1125 | 1126 | private val EaseOutBounce = Easing { fraction -> 1127 | val n1 = 7.5625f 1128 | val d1 = 2.75f 1129 | var newFraction = fraction 1130 | 1131 | return@Easing if (newFraction < 1f / d1) { 1132 | n1 * newFraction * newFraction 1133 | } else if (newFraction < 2f / d1) { 1134 | newFraction -= 1.5f / d1 1135 | n1 * newFraction * newFraction + 0.75f 1136 | } else if (newFraction < 2.5f / d1) { 1137 | newFraction -= 2.25f / d1 1138 | n1 * newFraction * newFraction + 0.9375f 1139 | } else { 1140 | newFraction -= 2.625f / d1 1141 | n1 * newFraction * newFraction + 0.984375f 1142 | } 1143 | } 1144 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun AndroidComposeCanvasTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 59 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/clwater/compose_canvas/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.clwater.compose_canvas.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/drawable/avatar.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/bezier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/drawable/bezier.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_hand_fill.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_hand_outline.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/drawable/shape.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/sun_moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/drawable/sun_moon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/drawable/tree.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidComposeCanvas 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |