├── .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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/clwater/compose_canvas/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.clwater.compose_canvas
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_version = '1.6.2'
4 | }
5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
6 | plugins {
7 | id 'com.android.application' version '7.4.2' apply false
8 | id 'com.android.library' version '7.4.2' apply false
9 | id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
10 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clwater/AndroidComposeCanvas/011bbb30bb43d8ffdd3fee5de1b8deed2031b1f2/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 02 20:53:56 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "AndroidComposeCanvas"
16 | include ':app'
17 |
--------------------------------------------------------------------------------