├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── codescape │ │ └── canvas │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── codescape │ │ │ └── canvas │ │ │ ├── MainActivity.kt │ │ │ └── ui │ │ │ ├── animations │ │ │ ├── PathAnimation.kt │ │ │ └── Transformations.kt │ │ │ ├── blendmodes │ │ │ ├── BlendModes.kt │ │ │ └── DrawWithContent.kt │ │ │ ├── navigation │ │ │ └── Navigation.kt │ │ │ ├── painter │ │ │ └── VectorPainter.kt │ │ │ ├── rendereffects │ │ │ └── BlurEffect.kt │ │ │ ├── shaders │ │ │ ├── Shader.kt │ │ │ ├── ShaderBrush.kt │ │ │ └── ShaderScripts.kt │ │ │ ├── text │ │ │ └── TextWithShadow.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── android_robot.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── lines.jpg │ │ └── palms.jpg │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ └── codescape │ └── canvas │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── blend_modes.gif ├── blur_effect.png ├── draw_with_content.gif ├── path_animation.gif ├── shader_brush.gif ├── shaders.gif ├── text_with_shadow.gif ├── transformations.gif └── vector_painter.gif └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example project for Android Bangkok 2023 GDG event. 2 | 3 | [![Kotlin](https://img.shields.io/badge/kotlin-1.9.10-7F52FF.svg?logo=kotlin)](http://kotlinlang.org) 4 | [![Compose](https://img.shields.io/badge/compose-1.5.1-4285F4.svg?logo=jetpack-compose)](https://developer.android.com/jetpack/compose) 5 | 6 | This is an example project for Hidden Powers of Compose Canvas presentation. 7 | 8 | ### 1. Path Animation 9 | Path Animation 10 | 11 | ### 2. Transformations 12 | Transformations 13 | 14 | ### 3. Blend Modes 15 | Blend Modes 16 | 17 | ### 4. Draw With Content 18 | Draw With Content 19 | 20 | ### 5. Text With Shadow 21 | Text With Shadow 22 | 23 | ### 6. Blur Effect 24 | Blur Effect 25 | 26 | ### 7. Shader Bursh 27 | Shader 28 | 29 | ### 8. Shader 30 | Shader 31 | 32 | ### 9. Vector Painter 33 | Shader -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.agp) 3 | alias(libs.plugins.kotlin) 4 | } 5 | 6 | android { 7 | namespace = "com.codescape.canvas" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "com.codescape.canvas" 12 | minSdk = 24 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 | isMinifyEnabled = false 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), 28 | "proguard-rules.pro" 29 | ) 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_11 34 | targetCompatibility = JavaVersion.VERSION_11 35 | } 36 | kotlinOptions { 37 | jvmTarget = "11" 38 | } 39 | buildFeatures { 40 | compose = true 41 | } 42 | composeOptions { 43 | kotlinCompilerExtensionVersion = "1.5.3" 44 | } 45 | packaging { 46 | resources { 47 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | 54 | implementation(libs.core.ktx) 55 | implementation(libs.lifecycle.runtime.ktx) 56 | implementation(libs.activity.compose) 57 | implementation(platform(libs.compose.bom)) 58 | implementation(libs.ui) 59 | implementation(libs.ui.graphics) 60 | implementation(libs.ui.tooling.preview) 61 | implementation(libs.material3) 62 | implementation(libs.material.icons) 63 | implementation(libs.navigation.compose) 64 | testImplementation(libs.junit) 65 | androidTestImplementation(libs.androidx.test.ext.junit) 66 | androidTestImplementation(libs.espresso.core) 67 | androidTestImplementation(platform(libs.compose.bom)) 68 | androidTestImplementation(libs.ui.test.junit4) 69 | debugImplementation(libs.ui.tooling) 70 | debugImplementation(libs.ui.test.manifest) 71 | } -------------------------------------------------------------------------------- /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/codescape/canvas/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.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.codescape.canvas", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.annotation.RequiresApi 8 | import com.codescape.canvas.ui.navigation.Navigation 9 | import com.codescape.canvas.ui.theme.CanvasTheme 10 | 11 | class MainActivity : ComponentActivity() { 12 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContent { 16 | CanvasTheme { 17 | Navigation() 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/animations/PathAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.animations 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.Canvas 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.aspectRatio 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.Brush 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.Path 23 | import androidx.compose.ui.graphics.PathMeasure 24 | import androidx.compose.ui.graphics.StrokeCap 25 | import androidx.compose.ui.graphics.StrokeJoin 26 | import androidx.compose.ui.graphics.drawscope.Stroke 27 | import androidx.compose.ui.graphics.vector.PathParser 28 | import androidx.compose.ui.platform.LocalDensity 29 | import androidx.compose.ui.platform.LocalInspectionMode 30 | import androidx.compose.ui.tooling.preview.Preview 31 | import androidx.compose.ui.unit.dp 32 | import kotlin.time.Duration.Companion.seconds 33 | import kotlin.time.DurationUnit 34 | 35 | const val SVG_PATH = "m0,205.49825l216.31496,0l66.84299,-205.49825l66.84302,205.49825l216.31492,0l-175.00216,127.00345l66.84644,205.49825l-175.00223,-127.00691l-175.00219,127.00691l66.84646,-205.49825l-175.00221,-127.00345z" 36 | 37 | @Composable 38 | fun PathAnimation() { 39 | 40 | // Step 1. Check if the current composition is being inspected (preview mode). 41 | val isPreview = LocalInspectionMode.current 42 | 43 | // Step 2. Create infinite transition to play animation infinitely. 44 | val infiniteTransition = rememberInfiniteTransition(label = "infinite_transition") 45 | 46 | // Step 3. Indefinitely animate float from 0f to 1f. If in preview mode, skip the animation. 47 | val pathProgress by infiniteTransition.animateFloat( 48 | initialValue = if (isPreview) 1f else 0f, 49 | targetValue = 1f, 50 | animationSpec = infiniteRepeatable( 51 | animation = tween( 52 | durationMillis = 5.seconds.toInt(DurationUnit.MILLISECONDS), 53 | easing = LinearEasing 54 | ), 55 | repeatMode = RepeatMode.Reverse 56 | ), 57 | label = "path_progress" 58 | ) 59 | 60 | // Step 4. Read the path from SVG string and create a Path. 61 | val path = remember { PathParser().parsePathString(pathData = SVG_PATH).toPath() } 62 | 63 | // Step 5. Create PathMeasure. 64 | val pathMeasure = remember { PathMeasure() } 65 | 66 | // Step 6. Check the size of the Path. 67 | val size = LocalDensity.current.run { path.getBounds().maxDimension.toDp() } 68 | Box( 69 | modifier = Modifier 70 | .fillMaxWidth() 71 | .aspectRatio(1f), 72 | contentAlignment = Alignment.Center 73 | ) { 74 | Canvas(modifier = Modifier.size(size)) { 75 | 76 | // Step 7. Create a new Path. 77 | val animatedPath = Path() 78 | 79 | // Step 8. Set Path to PathMeasure. 80 | pathMeasure.setPath(path, false) 81 | 82 | // Step 9. Get a segment (part) of the path based on the progress of animation. 83 | pathMeasure.getSegment( 84 | 0f, 85 | pathProgress * pathMeasure.length, 86 | animatedPath 87 | ) 88 | 89 | // Step 10. Draw our new animated Path. 90 | drawPath( 91 | path = animatedPath, 92 | brush = Brush.linearGradient(listOf(Color.Red, Color.Blue)), 93 | style = Stroke( 94 | width = 14.dp.toPx(), 95 | cap = StrokeCap.Round, 96 | join = StrokeJoin.Round 97 | ) 98 | ) 99 | } 100 | } 101 | } 102 | 103 | @Preview( 104 | showBackground = true, 105 | backgroundColor = 0xFFFFFFFF 106 | ) 107 | @Composable 108 | fun PathAnimationComponentPreview() { 109 | PathAnimation() 110 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/animations/Transformations.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.animations 2 | 3 | import androidx.compose.animation.core.RepeatMode 4 | import androidx.compose.animation.core.VectorConverter 5 | import androidx.compose.animation.core.animateValue 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.keyframes 8 | import androidx.compose.animation.core.rememberInfiniteTransition 9 | import androidx.compose.foundation.Canvas 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.geometry.Offset 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.StrokeCap 17 | import androidx.compose.ui.graphics.drawscope.rotate 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.Dp 20 | import androidx.compose.ui.unit.dp 21 | 22 | @Composable 23 | fun Spinner( 24 | modifier: Modifier = Modifier, 25 | sections: Int = 12, 26 | color: Color = Color.White, 27 | sectionLength: Dp = 6.dp, 28 | sectionWidth: Dp = 6.dp 29 | ) { 30 | 31 | // Step 1. Create infinite transition to play animation infinitely. 32 | val infiniteTransition = rememberInfiniteTransition(label = "infinite_transition") 33 | 34 | // Step 2. Establish the initial and final position of offset. Animate Int from 0 to number of sections. 35 | val sectionOffset by infiniteTransition.animateValue( 36 | initialValue = 0, 37 | targetValue = sections, 38 | typeConverter = Int.VectorConverter, 39 | animationSpec = infiniteRepeatable( 40 | keyframes { 41 | durationMillis = 1000 42 | }, 43 | repeatMode = RepeatMode.Restart 44 | ), 45 | label = "angle_animation" 46 | ) 47 | Canvas(modifier = modifier) { 48 | 49 | // Step 3. Define rotation radius and angle separation for each section. 50 | val radius = size.height / 2 51 | val angle = 360f / sections 52 | val alpha = 1f / sections 53 | 54 | // Step 4. Rotate the animated angle. 55 | rotate(sectionOffset * angle) { 56 | 57 | // Step 5. For each section, rotate and draw a line with graduated opacity. 58 | for (i in 1..sections) { 59 | rotate(angle * i) { 60 | drawLine( 61 | color = color.copy(alpha = alpha * i), 62 | strokeWidth = sectionWidth.toPx(), 63 | start = Offset( 64 | x = radius, 65 | y = sectionLength.toPx() 66 | ), 67 | end = Offset( 68 | x = radius, 69 | y = sectionLength.toPx() * 2 70 | ), 71 | cap = StrokeCap.Round 72 | ) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | @Preview( 80 | showBackground = true, 81 | backgroundColor = 0xFF0000000 82 | ) 83 | @Composable 84 | fun SpinnerPreview() { 85 | Spinner(modifier = Modifier.size(64.dp)) 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/blendmodes/BlendModes.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.blendmodes 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.compose.animation.core.Animatable 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.Canvas 7 | import androidx.compose.foundation.layout.aspectRatio 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.graphics.BlendMode 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.CompositingStrategy 17 | import androidx.compose.ui.graphics.drawscope.Stroke 18 | import androidx.compose.ui.graphics.graphicsLayer 19 | import androidx.compose.ui.platform.LocalInspectionMode 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.Dp 22 | import androidx.compose.ui.unit.dp 23 | import kotlin.math.cos 24 | import kotlin.math.sin 25 | 26 | data class PieSlice( 27 | val color: Color, 28 | @FloatRange(from = 0.0, to = 1.0) 29 | val size: Float, 30 | val name: String 31 | ) 32 | 33 | @Composable 34 | fun PieChart( 35 | modifier: Modifier = Modifier, 36 | slices: List, 37 | strokeWidth: Dp = 24.dp, 38 | dividerWidth: Dp = 24.dp, 39 | animationDuration: Int = 1000 40 | ) { 41 | 42 | // Step 1. Check if the current composition is being inspected (preview mode). 43 | val isPreview = LocalInspectionMode.current 44 | 45 | // Step 2. Remember the animated angles for each slice. If in preview mode, skip the animation. 46 | val animatedSweepAngles = remember(slices) { 47 | slices.map { Animatable(if (isPreview) it.size * 360 else 0f) } 48 | } 49 | 50 | // Step 3. Animate the sweep angles to their final positions when the composition is launched. 51 | LaunchedEffect(animatedSweepAngles) { 52 | slices.forEachIndexed { index, slice -> 53 | animatedSweepAngles[index].animateTo( 54 | targetValue = slice.size * 360, 55 | animationSpec = tween(animationDuration) 56 | ) 57 | } 58 | } 59 | 60 | // Step 4. Draw the pie chart on a Canvas, with an aspect ratio of 1:1 (circle). 61 | Canvas( 62 | modifier = modifier 63 | .aspectRatio(1f) 64 | .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) 65 | ) { 66 | 67 | // Step 5. Calculate the radius of the pie chart (half the minimum dimension of the Canvas). 68 | val radius = size.minDimension / 2 69 | var sliceAngle = 0f 70 | 71 | // Step 6. For each slice, draw an arc of the pie chart with the appropriate color and size. 72 | for ((index, slice) in slices.withIndex()) { 73 | drawArc( 74 | color = slice.color, 75 | startAngle = sliceAngle, 76 | sweepAngle = animatedSweepAngles[index].value, 77 | useCenter = false, 78 | topLeft = center - Offset( 79 | radius - strokeWidth.toPx() / 2, 80 | radius - strokeWidth.toPx() / 2 81 | ), 82 | size = size.copy( 83 | width = size.width - strokeWidth.toPx(), 84 | height = size.height - strokeWidth.toPx() 85 | ), 86 | style = Stroke(width = strokeWidth.toPx()) 87 | ) 88 | sliceAngle += animatedSweepAngles[index].value 89 | } 90 | 91 | // Step 7. Draw dividers for each slice to clearly separate them. 92 | var spacerAngle = 0f 93 | for (slice in slices) { 94 | val angleRadian = Math.toRadians(spacerAngle.toDouble()) 95 | val endX = center.x + radius * cos(angleRadian).toFloat() 96 | val endY = center.y + radius * sin(angleRadian).toFloat() 97 | drawLine( 98 | color = Color.Black, 99 | start = Offset(center.x, center.y), 100 | end = Offset(endX, endY), 101 | strokeWidth = dividerWidth.toPx(), 102 | blendMode = BlendMode.DstOut 103 | ) 104 | spacerAngle += slice.size * 360 105 | } 106 | } 107 | } 108 | 109 | @Preview( 110 | showBackground = true, 111 | backgroundColor = 0xFFFFFFFF 112 | ) 113 | @Composable 114 | fun PieChartPreview() { 115 | val slices = listOf( 116 | PieSlice( 117 | color = Color.Red, 118 | size = 0.5f, 119 | name = "first" 120 | ), 121 | PieSlice( 122 | color = Color.Blue, 123 | size = 0.3f, 124 | name = "two" 125 | ), 126 | PieSlice( 127 | color = Color.Green, 128 | size = 0.2f, 129 | name = "three" 130 | ) 131 | ) 132 | PieChart( 133 | modifier = Modifier 134 | .aspectRatio(1f) 135 | .padding(24.dp), 136 | slices = slices, 137 | strokeWidth = 64.dp 138 | ) 139 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/blendmodes/DrawWithContent.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.blendmodes 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.keyframes 8 | import androidx.compose.animation.core.rememberInfiniteTransition 9 | import androidx.compose.animation.core.tween 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.rounded.AirplaneTicket 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.drawWithContent 24 | import androidx.compose.ui.geometry.Offset 25 | import androidx.compose.ui.geometry.Size 26 | import androidx.compose.ui.graphics.BlendMode 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.CompositingStrategy 29 | import androidx.compose.ui.graphics.ImageBitmap 30 | import androidx.compose.ui.graphics.LinearGradientShader 31 | import androidx.compose.ui.graphics.Shader 32 | import androidx.compose.ui.graphics.ShaderBrush 33 | import androidx.compose.ui.graphics.TileMode 34 | import androidx.compose.ui.graphics.drawscope.scale 35 | import androidx.compose.ui.graphics.graphicsLayer 36 | import androidx.compose.ui.res.imageResource 37 | import androidx.compose.ui.text.font.FontWeight 38 | import androidx.compose.ui.tooling.preview.Preview 39 | import androidx.compose.ui.unit.dp 40 | import androidx.compose.ui.unit.sp 41 | import com.codescape.canvas.R 42 | 43 | // Image by Quang Nguyen vinh from Pixabay: https://pixabay.com/photos/palm-trees-coconut-trees-tropical-3058728/ 44 | 45 | @Composable 46 | fun DrawWithContent() { 47 | 48 | // Step 1. Create an infinite transition with desired label 49 | val infiniteTransition = rememberInfiniteTransition(label = "infinite_transition") 50 | 51 | // Step 2. Animate vertical gradient offset using defined infinite transition 52 | val gradientOffset by infiniteTransition.animateFloat( 53 | initialValue = 1f, 54 | targetValue = 0f, 55 | animationSpec = infiniteRepeatable( 56 | animation = tween( 57 | durationMillis = 5000, 58 | easing = LinearEasing 59 | ), 60 | repeatMode = RepeatMode.Restart 61 | ), 62 | label = "gradient_progress" 63 | ) 64 | 65 | // Step 3. Animate scale changes for the image 66 | val imageScale by infiniteTransition.animateFloat( 67 | initialValue = 1f, 68 | targetValue = 1.25f, 69 | animationSpec = infiniteRepeatable( 70 | animation = keyframes { 71 | durationMillis = 5000 72 | }, 73 | repeatMode = RepeatMode.Reverse 74 | ), 75 | label = "indicator_scale" 76 | ) 77 | 78 | // Step 4. Create a Linear Gradient Brush with dynamic offsets dependent on the infinite transition 79 | val brush = remember(gradientOffset) { 80 | object : ShaderBrush() { 81 | override fun createShader(size: Size): Shader { 82 | val widthOffset = size.width * gradientOffset 83 | val heightOffset = size.height * gradientOffset 84 | return LinearGradientShader( 85 | colors = listOf(Color.Yellow, Color.Blue, Color.Yellow), 86 | from = Offset( 87 | x = widthOffset, 88 | y = heightOffset + size.height 89 | ), 90 | to = Offset( 91 | x = widthOffset, 92 | y = heightOffset 93 | ), 94 | tileMode = TileMode.Mirror 95 | ) 96 | } 97 | } 98 | } 99 | 100 | // Step 5. Load image resource that will have applied animations and effects 101 | val image = ImageBitmap.imageResource(id = R.drawable.palms) 102 | 103 | // Step 6. Draw a Box with nested content including the image, gradient effect and design elements 104 | Box( 105 | modifier = Modifier 106 | .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) 107 | .drawWithContent { 108 | drawContent() 109 | scale(imageScale) { 110 | drawImage(image = image, blendMode = BlendMode.SrcIn) 111 | } 112 | drawRect( 113 | brush = brush, 114 | blendMode = BlendMode.SrcAtop, 115 | alpha = 0.5f 116 | ) 117 | }, 118 | contentAlignment = Alignment.Center 119 | ) { 120 | Column( 121 | modifier = Modifier.padding(24.dp), 122 | horizontalAlignment = Alignment.CenterHorizontally 123 | ) { 124 | Icon( 125 | modifier = Modifier.size(148.dp), 126 | imageVector = Icons.Rounded.AirplaneTicket, 127 | contentDescription = "Place" 128 | ) 129 | Text( 130 | text = "Travel", 131 | fontSize = 100.sp, 132 | fontWeight = FontWeight.W900 133 | ) 134 | } 135 | } 136 | } 137 | 138 | @Preview( 139 | showBackground = true, 140 | backgroundColor = 0xFFFFFFFF 141 | ) 142 | @Composable 143 | fun DrawWithContentPreview() { 144 | DrawWithContent() 145 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/navigation/Navigation.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.navigation 2 | 3 | import android.os.Build 4 | import androidx.annotation.RequiresApi 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.items 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.rounded.ChevronLeft 17 | import androidx.compose.material3.Button 18 | import androidx.compose.material3.CenterAlignedTopAppBar 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.IconButton 22 | import androidx.compose.material3.Scaffold 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.unit.dp 31 | import androidx.navigation.compose.NavHost 32 | import androidx.navigation.compose.composable 33 | import androidx.navigation.compose.currentBackStackEntryAsState 34 | import androidx.navigation.compose.rememberNavController 35 | import com.codescape.canvas.ui.animations.PathAnimation 36 | import com.codescape.canvas.ui.animations.Spinner 37 | import com.codescape.canvas.ui.blendmodes.DrawWithContent 38 | import com.codescape.canvas.ui.blendmodes.PieChart 39 | import com.codescape.canvas.ui.blendmodes.PieSlice 40 | import com.codescape.canvas.ui.painter.VectorPainter 41 | import com.codescape.canvas.ui.rendereffects.BlurEffectComponent 42 | import com.codescape.canvas.ui.shaders.SHADER_1 43 | import com.codescape.canvas.ui.shaders.ShaderBrushGradient 44 | import com.codescape.canvas.ui.shaders.ShaderComponent 45 | import com.codescape.canvas.ui.text.TextWithShadow 46 | 47 | sealed class Screen( 48 | val route: String, 49 | val title: String, 50 | val showBackButton: Boolean = true 51 | ) { 52 | data object Menu: Screen( 53 | route = "menu", 54 | title = "Menu", 55 | showBackButton = false 56 | ) 57 | data object PathAnimation: Screen( 58 | route = "path_animation", 59 | title = "Path Animation" 60 | ) 61 | data object Transformations: Screen( 62 | route = "transformations", 63 | title = "Transformations" 64 | ) 65 | data object BlendModes: Screen( 66 | route = "blend_modes", 67 | title = "Blend Modes" 68 | ) 69 | data object DrawWithContent: Screen( 70 | route = "draw_with_content", 71 | title = "Draw With Content" 72 | ) 73 | data object TextWithShadow: Screen( 74 | route = "text_with_shadow", 75 | title = "Text With Shadow" 76 | ) 77 | data object BlurEffect: Screen( 78 | route = "blur_effect", 79 | title = "Blur Effect" 80 | ) 81 | data object ShaderBrush: Screen( 82 | route = "shader_brush", 83 | title = "Shader Brush" 84 | ) 85 | data object Shader: Screen( 86 | route = "shader", 87 | title = "Shader" 88 | ) 89 | data object VectorPainter: Screen( 90 | route = "vector_painter", 91 | title = "VectorPainter" 92 | ) 93 | } 94 | 95 | val allScreens = listOf( 96 | Screen.PathAnimation, 97 | Screen.Transformations, 98 | Screen.BlendModes, 99 | Screen.DrawWithContent, 100 | Screen.TextWithShadow, 101 | Screen.BlurEffect, 102 | Screen.ShaderBrush, 103 | Screen.Shader, 104 | Screen.VectorPainter 105 | ) 106 | 107 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 108 | @OptIn(ExperimentalMaterial3Api::class) 109 | @Composable 110 | fun Navigation() { 111 | val navController = rememberNavController() 112 | val navBackStackEntry by navController.currentBackStackEntryAsState() 113 | val route = remember(navBackStackEntry) { navBackStackEntry?.destination?.route } 114 | val currentScreen = remember(route) { 115 | when (route) { 116 | Screen.BlurEffect.route -> Screen.BlurEffect 117 | Screen.BlendModes.route -> Screen.BlendModes 118 | Screen.DrawWithContent.route -> Screen.DrawWithContent 119 | Screen.Menu.route -> Screen.Menu 120 | Screen.PathAnimation.route -> Screen.PathAnimation 121 | Screen.Shader.route -> Screen.Shader 122 | Screen.ShaderBrush.route -> Screen.ShaderBrush 123 | Screen.Transformations.route -> Screen.Transformations 124 | Screen.TextWithShadow.route -> Screen.TextWithShadow 125 | Screen.VectorPainter.route -> Screen.VectorPainter 126 | else -> null 127 | } 128 | } 129 | Scaffold( 130 | topBar = { 131 | CenterAlignedTopAppBar( 132 | title = { 133 | currentScreen?.let { 134 | Text(text = currentScreen.title) 135 | } 136 | }, 137 | navigationIcon = { 138 | if (currentScreen?.showBackButton == true) { 139 | IconButton(onClick = { navController.navigateUp() }) { 140 | Icon( 141 | imageVector = Icons.Rounded.ChevronLeft, 142 | contentDescription = "Chevron Left" 143 | ) 144 | } 145 | } 146 | } 147 | ) 148 | }, 149 | modifier = Modifier.fillMaxSize() 150 | ) { padding -> 151 | NavHost( 152 | modifier = Modifier 153 | .fillMaxSize() 154 | .padding(padding), 155 | navController = navController, 156 | startDestination = Screen.Menu.route, 157 | ) { 158 | composable(route = Screen.Menu.route) { 159 | LazyColumn( 160 | modifier = Modifier.fillMaxSize(), 161 | contentPadding = PaddingValues(24.dp), 162 | verticalArrangement = Arrangement.spacedBy( 163 | space = 16.dp, 164 | alignment = Alignment.CenterVertically 165 | ) 166 | ) { 167 | items( 168 | items = allScreens, 169 | key = { screen -> screen.route } 170 | ) { screen -> 171 | Button( 172 | modifier = Modifier.fillMaxWidth(), 173 | onClick = { navController.navigate(route = screen.route) } 174 | ) { 175 | Text(text = screen.title) 176 | } 177 | } 178 | } 179 | } 180 | composable(route = Screen.PathAnimation.route) { 181 | Box( 182 | modifier = Modifier.fillMaxSize(), 183 | contentAlignment = Alignment.Center 184 | ) { 185 | PathAnimation() 186 | } 187 | } 188 | composable(route = Screen.Transformations.route) { 189 | Box( 190 | modifier = Modifier.fillMaxSize(), 191 | contentAlignment = Alignment.Center 192 | ) { 193 | Spinner( 194 | modifier = Modifier.size(120.dp), 195 | color = Color.Black, 196 | sectionLength = 12.dp, 197 | sectionWidth = 12.dp 198 | ) 199 | } 200 | } 201 | composable(route = Screen.BlendModes.route) { 202 | Box( 203 | modifier = Modifier.fillMaxSize(), 204 | contentAlignment = Alignment.Center 205 | ) { 206 | val slices = remember { 207 | listOf( 208 | PieSlice( 209 | color = Color.Red, 210 | size = 0.5f, 211 | name = "first" 212 | ), 213 | PieSlice( 214 | color = Color.Blue, 215 | size = 0.3f, 216 | name = "two" 217 | ), 218 | PieSlice( 219 | color = Color.Green, 220 | size = 0.2f, 221 | name = "three" 222 | ) 223 | ) 224 | } 225 | PieChart( 226 | modifier = Modifier 227 | .aspectRatio(1f) 228 | .padding(24.dp), 229 | slices = slices, 230 | strokeWidth = 64.dp 231 | ) 232 | } 233 | } 234 | composable(route = Screen.DrawWithContent.route) { 235 | Box( 236 | modifier = Modifier.fillMaxSize(), 237 | contentAlignment = Alignment.Center 238 | ) { 239 | DrawWithContent() 240 | } 241 | } 242 | composable(route = Screen.TextWithShadow.route) { 243 | Box( 244 | modifier = Modifier.fillMaxSize(), 245 | contentAlignment = Alignment.Center 246 | ) { 247 | TextWithShadow(text = "Text") 248 | } 249 | } 250 | composable(route = Screen.BlurEffect.route) { 251 | Box( 252 | modifier = Modifier.fillMaxSize(), 253 | contentAlignment = Alignment.Center 254 | ) { 255 | BlurEffectComponent() 256 | } 257 | } 258 | composable(route = Screen.ShaderBrush.route) { 259 | Box( 260 | modifier = Modifier.fillMaxSize(), 261 | contentAlignment = Alignment.Center 262 | ) { 263 | ShaderBrushGradient(text = "Click Me") 264 | } 265 | } 266 | composable(route = Screen.Shader.route) { 267 | Box( 268 | modifier = Modifier.fillMaxSize(), 269 | contentAlignment = Alignment.Center 270 | ) { 271 | ShaderComponent(shaderScript = SHADER_1) 272 | } 273 | } 274 | composable(route = Screen.VectorPainter.route) { 275 | Box( 276 | modifier = Modifier.fillMaxSize(), 277 | contentAlignment = Alignment.Center 278 | ) { 279 | VectorPainter(modifier = Modifier.fillMaxSize()) 280 | } 281 | } 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/painter/VectorPainter.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.painter 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.Canvas 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.ColorFilter 16 | import androidx.compose.ui.graphics.drawscope.inset 17 | import androidx.compose.ui.graphics.drawscope.scale 18 | import androidx.compose.ui.graphics.vector.ImageVector 19 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 20 | import androidx.compose.ui.res.vectorResource 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import com.codescape.canvas.R 23 | 24 | @Composable 25 | fun VectorPainter(modifier: Modifier = Modifier) { 26 | 27 | // Step 1. Create infinite transition to play animation infinitely. 28 | val infiniteTransition = rememberInfiniteTransition(label = "infinite_transition") 29 | 30 | // Step 2. Animate the scale of the shadow 1f to 1.5.dp indefinitely. 31 | val backgroundScale by infiniteTransition.animateFloat( 32 | initialValue = 1f, 33 | targetValue = 1.5f, 34 | animationSpec = infiniteRepeatable( 35 | animation = tween( 36 | durationMillis = 1000, 37 | easing = LinearEasing 38 | ), 39 | repeatMode = RepeatMode.Reverse 40 | ), 41 | label = "gradient_rotation" 42 | ) 43 | 44 | // Step 3. Create VectorPainter from drawable resource. 45 | val vectorPainter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.android_robot)) 46 | 47 | Canvas(modifier = modifier) { 48 | with(vectorPainter) { 49 | // Step 4. Scale the image 50 | val scale = size.width / vectorPainter.intrinsicSize.width 51 | val scaledSize = vectorPainter.intrinsicSize * scale / 2f 52 | inset( 53 | horizontal = (size.width - scaledSize.width) / 2f, 54 | vertical = (size.height - scaledSize.height) / 2f 55 | ) { 56 | // Step 5. Use VectorPainter DrawScope.draw() function 57 | scale(backgroundScale) { 58 | draw( 59 | size = scaledSize, 60 | alpha = 0.5f, 61 | colorFilter = ColorFilter.tint(color = Color.LightGray) 62 | ) 63 | } 64 | draw(size = scaledSize) 65 | } 66 | } 67 | } 68 | } 69 | 70 | @Preview( 71 | showBackground = true, 72 | backgroundColor = 0xFFFFFFFF 73 | ) 74 | @Composable 75 | fun VectorPainterPreview() { 76 | VectorPainter(modifier = Modifier.fillMaxSize()) 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/rendereffects/BlurEffect.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.rendereffects 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.ArrowBack 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.shadow 14 | import androidx.compose.ui.graphics.BlurEffect 15 | import androidx.compose.ui.graphics.graphicsLayer 16 | import androidx.compose.ui.layout.FixedScale 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import com.codescape.canvas.R 21 | 22 | // Image by David Yu from Pixabay: https://pixabay.com/photos/lines-symmetry-art-architecture-3659995/ 23 | 24 | @Composable 25 | fun BlurEffectComponent() { 26 | Box( 27 | modifier = Modifier.aspectRatio(1f), 28 | contentAlignment = Alignment.Center 29 | ) { 30 | 31 | // Step 1. Add an Image composable to show as background. Force the image to use its original size. 32 | Image( 33 | painter = painterResource(id = R.drawable.lines), 34 | contentScale = FixedScale(1f), 35 | contentDescription = "Background" 36 | ) 37 | 38 | // Step 2. Add another Image composable to show as a blur card. Force the image to use its original size. 39 | Image( 40 | modifier = Modifier 41 | .padding(48.dp) 42 | .shadow( 43 | elevation = 18.dp, 44 | shape = RoundedCornerShape(24.dp) 45 | ) 46 | .graphicsLayer { 47 | // Step 3. Add a blur effect graphic layer. Define horizontal and vertical blur radius. 48 | renderEffect = BlurEffect( 49 | radiusX = 4.dp.toPx(), 50 | radiusY = 4.dp.toPx() 51 | ) 52 | // Step 4. Set the shape of the graphic layer. 53 | shape = RoundedCornerShape(24.dp) 54 | // Step 5. Clip the content to the defined shape. 55 | clip = true 56 | }, 57 | painter = painterResource(id = R.drawable.lines), 58 | contentScale = FixedScale(1f), 59 | contentDescription = "Blur Card" 60 | ) 61 | } 62 | } 63 | 64 | @Preview( 65 | showBackground = true, 66 | backgroundColor = 0xFFFFFFFF 67 | ) 68 | @Composable 69 | fun BlurEffectComponentPreview() { 70 | BlurEffectComponent() 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/shaders/Shader.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.shaders 2 | 3 | import android.graphics.Matrix 4 | import android.graphics.RuntimeShader 5 | import android.os.Build 6 | import androidx.annotation.RequiresApi 7 | import androidx.compose.animation.core.withInfiniteAnimationFrameNanos 8 | import androidx.compose.foundation.gestures.detectDragGestures 9 | import androidx.compose.foundation.gestures.detectTapGestures 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.aspectRatio 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.produceState 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.drawBehind 21 | import androidx.compose.ui.geometry.Offset 22 | import androidx.compose.ui.geometry.Size 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.graphics.ShaderBrush 25 | import androidx.compose.ui.input.pointer.pointerInput 26 | import androidx.compose.ui.layout.onGloballyPositioned 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import org.intellij.lang.annotations.Language 29 | 30 | // Online Shader editor: https://thebookofshaders.com/edit.php 31 | 32 | @Language("AGSL") 33 | const val SIMPLE_SHADER = 34 | """ 35 | uniform float2 iResolution; 36 | uniform float iTime; 37 | uniform float2 iMouse; 38 | half4 main(float2 fragCoord) { 39 | float2 pct = fragCoord/iResolution.xy; 40 | float d = distance(pct, iMouse.xy/iResolution.xy); 41 | return half4(half3(1, 0, 0)*(1-d)*(1-d), 1); 42 | } 43 | """ 44 | 45 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 46 | @Composable 47 | fun ShaderComponent(shaderScript: String) { 48 | // Step 1. Create a RuntimeShader. 49 | val runtimeShader = remember(shaderScript) { RuntimeShader(shaderScript) } 50 | 51 | // Step 2. Create a ShaderBrush from the RuntimeShader. 52 | val shaderBrush = remember(runtimeShader) { ShaderBrush(runtimeShader) } 53 | 54 | // Step 3. Set an animator to continuously update the time state every second. 55 | val time by produceState(0f) { 56 | while (true) { 57 | withInfiniteAnimationFrameNanos { frameTimeNanos -> 58 | value = frameTimeNanos / 1000000000f 59 | } 60 | } 61 | } 62 | 63 | // Step 4. Create a position state to store the position of the touch event. 64 | var position by remember { mutableStateOf(Offset.Zero) } 65 | Box( 66 | modifier = Modifier 67 | .fillMaxWidth() 68 | .aspectRatio(1f) 69 | .pointerInput(Unit) { 70 | // Step 5. Add a gesture detector to update the position state on touch events. 71 | detectTapGestures { 72 | position = Offset( 73 | x = it.x, 74 | y = size.height - it.y 75 | ) 76 | } 77 | } 78 | .pointerInput(Unit) { 79 | // Step 6. Add a gesture detector to update the position state when drag events occur. 80 | detectDragGestures { _, dragAmount -> 81 | position = Offset( 82 | x = position.x + dragAmount.x, 83 | y = position.y - dragAmount.y 84 | ) 85 | } 86 | } 87 | // Step 7. Update the size state when the component is placed in the Compose hierarchy. 88 | .drawBehind { 89 | // Step 8. Set global constants for the Shader. 90 | runtimeShader.setFloatUniform("iResolution", size.width, size.height) 91 | runtimeShader.setFloatUniform("iTime", time) 92 | runtimeShader.setFloatUniform("iMouse", position.x, position.y) 93 | 94 | // Step 9. Transform the AGSL to GLSL coordinate space. 95 | val localMatrix = Matrix() 96 | localMatrix.postScale(1.0f, -1.0f) 97 | localMatrix.postTranslate(0.0f, size.height) 98 | runtimeShader.setLocalMatrix(localMatrix) 99 | 100 | // Step 10. Draw the shader rect. 101 | drawRect(brush = shaderBrush) 102 | } 103 | ) 104 | } 105 | 106 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 107 | @Preview( 108 | showBackground = true, 109 | backgroundColor = 0xFFFFFFFF 110 | ) 111 | @Composable 112 | fun ShaderComponentPreview() { 113 | ShaderComponent(shaderScript = SIMPLE_SHADER) 114 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/shaders/ShaderBrush.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.shaders 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.padding 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.drawBehind 17 | import androidx.compose.ui.geometry.CornerRadius 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.geometry.Size 20 | import androidx.compose.ui.geometry.center 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.LinearGradientShader 23 | import androidx.compose.ui.graphics.Shader 24 | import androidx.compose.ui.graphics.ShaderBrush 25 | import androidx.compose.ui.graphics.StrokeJoin 26 | import androidx.compose.ui.graphics.TileMode 27 | import androidx.compose.ui.graphics.drawscope.Stroke 28 | import androidx.compose.ui.graphics.drawscope.inset 29 | import androidx.compose.ui.tooling.preview.Preview 30 | import androidx.compose.ui.unit.Dp 31 | import androidx.compose.ui.unit.dp 32 | import androidx.compose.ui.unit.sp 33 | import kotlin.math.cos 34 | import kotlin.math.sin 35 | 36 | /** 37 | * Created by Fedor Erofeev on 23.10.2023 38 | */ 39 | 40 | @Composable 41 | fun ShaderBrushGradient( 42 | strokeWidth: Dp = 16.dp, 43 | cornerRadius: Dp = 16.dp, 44 | text: String 45 | ) { 46 | // Step 1: Create an infinite transition for the gradient rotation. 47 | val infiniteTransition = rememberInfiniteTransition(label = "infinite_transition") 48 | 49 | // Step 2: Animate the gradient rotation from 0 to 360 degrees. 50 | val gradientRotation by infiniteTransition.animateFloat( 51 | initialValue = 0f, 52 | targetValue = 360f, 53 | animationSpec = infiniteRepeatable( 54 | animation = tween( 55 | durationMillis = 5000, 56 | easing = LinearEasing 57 | ), 58 | repeatMode = RepeatMode.Restart 59 | ), 60 | label = "gradient_rotation" 61 | ) 62 | 63 | // Step 3: Define a LinearGradientShader to rotate the linear gradient. 64 | val brush = remember(gradientRotation) { 65 | object : ShaderBrush() { 66 | override fun createShader(size: Size): Shader { 67 | val angleRadian = Math.toRadians(gradientRotation.toDouble()) 68 | val endX = size.center.x + (size.maxDimension / 2) * cos(angleRadian).toFloat() 69 | val endY = size.center.y + (size.maxDimension / 2) * sin(angleRadian).toFloat() 70 | return LinearGradientShader( 71 | colors = listOf(Color.Green, Color.Blue), 72 | from = size.center, 73 | to = Offset( 74 | x = endX, 75 | y = endY 76 | ), 77 | tileMode = TileMode.Mirror 78 | ) 79 | } 80 | } 81 | } 82 | Box( 83 | modifier = Modifier 84 | .drawBehind { 85 | inset( 86 | top = strokeWidth.toPx() / 2, 87 | right = strokeWidth.toPx() / 2, 88 | bottom = strokeWidth.toPx() / 2, 89 | left = strokeWidth.toPx() / 2 90 | ) { 91 | // Step 4. Draw round rectangle with animated gradient brush. 92 | drawRoundRect( 93 | cornerRadius = CornerRadius( 94 | x = cornerRadius.toPx(), 95 | y = cornerRadius.toPx() 96 | ), 97 | brush = brush, 98 | style = Stroke( 99 | width = strokeWidth.toPx(), 100 | join = StrokeJoin.Round 101 | ) 102 | ) 103 | } 104 | } 105 | .padding(strokeWidth * 2) 106 | ) { 107 | Text( 108 | text = text, 109 | fontSize = 50.sp 110 | ) 111 | } 112 | } 113 | 114 | @Preview( 115 | showBackground = true, 116 | backgroundColor = 0xFFFFFFFF 117 | ) 118 | @Composable 119 | fun ShaderBrushGradientPreview() { 120 | ShaderBrushGradient(text = "Click Me") 121 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/shaders/ShaderScripts.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.shaders 2 | 3 | import org.intellij.lang.annotations.Language 4 | 5 | // From Skia Shaders Playground: https://shaders.skia.org/?id=de2a4d7d893a7251eb33129ddf9d76ea517901cec960db116a1bbd7832757c1f 6 | @Language("AGSL") 7 | const val SHADER_1 = 8 | """ 9 | uniform float2 iResolution; 10 | uniform float iTime; 11 | uniform float2 iMouse; 12 | 13 | // Source: @notargs https://twitter.com/notargs/status/1250468645030858753 14 | float f(vec3 p) { 15 | p.z -= iTime * 10.; 16 | float a = p.z * .1; 17 | p.xy *= mat2(cos(a), sin(a), -sin(a), cos(a)); 18 | return .1 - length(cos(p.xy) + sin(p.yz)); 19 | } 20 | 21 | half4 main(vec2 fragcoord) { 22 | vec3 d = .5 - fragcoord.xy1 / iResolution.y; 23 | vec3 p=vec3(0); 24 | for (int i = 0; i < 32; i++) { 25 | p += f(p) * d; 26 | } 27 | return ((sin(p) + vec3(2, 5, 9)) / length(p)).xyz1; 28 | } 29 | """ 30 | 31 | // From Skia Shaders Playground: https://shaders.skia.org/?id=e0ec9ef204763445036d8a157b1b5c8929829c3e1ee0a265ed984aeddc8929e2 32 | @Language("AGSL") 33 | const val SHADER_2 = 34 | """ 35 | uniform float2 iResolution; 36 | uniform float iTime; 37 | uniform float2 iMouse; 38 | 39 | // Star Nest by Pablo Roman Andrioli 40 | 41 | // This content is under the MIT License. 42 | 43 | const int iterations = 17; 44 | const float formuparam = 0.53; 45 | 46 | const int volsteps = 20; 47 | const float stepsize = 0.1; 48 | 49 | const float zoom = 0.800; 50 | const float tile = 0.850; 51 | const float speed =0.010 ; 52 | 53 | const float brightness =0.0015; 54 | const float darkmatter =0.300; 55 | const float distfading =0.730; 56 | const float saturation =0.850; 57 | 58 | 59 | half4 main( in vec2 fragCoord ) 60 | { 61 | //get coords and direction 62 | vec2 uv=fragCoord.xy/iResolution.xy-.5; 63 | uv.y*=iResolution.y/iResolution.x; 64 | vec3 dir=vec3(uv*zoom,1.); 65 | float time=iTime*speed+.25; 66 | 67 | //mouse rotation 68 | float a1=.5+iMouse.x/iResolution.x*2.; 69 | float a2=.8+iMouse.y/iResolution.y*2.; 70 | mat2 rot1=mat2(cos(a1),sin(a1),-sin(a1),cos(a1)); 71 | mat2 rot2=mat2(cos(a2),sin(a2),-sin(a2),cos(a2)); 72 | dir.xz*=rot1; 73 | dir.xy*=rot2; 74 | vec3 from=vec3(1.,.5,0.5); 75 | from+=vec3(time*2.,time,-2.); 76 | from.xz*=rot1; 77 | from.xy*=rot2; 78 | 79 | //volumetric rendering 80 | float s=0.1,fade=1.; 81 | vec3 v=vec3(0.); 82 | for (int r=0; r6) fade*=1.-dm; // dark matter, don't render near 94 | //v+=vec3(dm,dm*.5,0.); 95 | v+=fade; 96 | v+=vec3(s,s*s,s*s*s*s)*a*brightness*fade; // coloring based on distance 97 | fade*=distfading; // distance fading 98 | s+=stepsize; 99 | } 100 | v=mix(vec3(length(v)),v,saturation); //color adjust 101 | return vec4(v*.01,1.); 102 | 103 | } 104 | """ 105 | 106 | // From Skia Shaders Playground: https://shaders.skia.org/?id=2bee4488820c3253cd8861e85ce3cab86f482cfddd79cfd240591bf64f7bcc38 107 | @Language("AGSL") 108 | const val SHADER_3 = 109 | """ 110 | uniform float2 iResolution; 111 | uniform float iTime; 112 | uniform float2 iMouse; 113 | 114 | // Source: @XorDev https://twitter.com/XorDev/status/1475524322785640455 115 | vec4 main(vec2 FC) { 116 | vec4 o = vec4(0); 117 | vec2 p = vec2(0), c=p, u=FC.xy*2.-iResolution.xy; 118 | float a; 119 | for (float i=0; i<4e2; i++) { 120 | a = i/2e2-1.; 121 | p = cos(i*2.4+iTime+vec2(0,11))*sqrt(1.-a*a); 122 | c = u/iResolution.y+vec2(p.x,a)/(p.y+2.); 123 | o += (cos(i+vec4(0,2,4,0))+1.)/dot(c,c)*(1.-p.y)/3e4; 124 | } 125 | return o; 126 | } 127 | """ 128 | -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/text/TextWithShadow.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.canvas.ui.text 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.Canvas 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.Shadow 18 | import androidx.compose.ui.graphics.drawscope.translate 19 | import androidx.compose.ui.platform.LocalDensity 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.drawText 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.text.rememberTextMeasurer 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import androidx.compose.ui.unit.toSize 29 | 30 | /** 31 | * Created by Fedor Erofeev on 29.10.2023 32 | */ 33 | 34 | @Composable 35 | fun TextWithShadow( 36 | modifier: Modifier = Modifier, 37 | text: String, 38 | textStyle: TextStyle = TextStyle.Default.copy( 39 | fontSize = 100.sp, 40 | fontWeight = FontWeight.W600 41 | ), 42 | shadowSize: Dp = 8.dp 43 | ) { 44 | 45 | // Step 1. Create infinite transition to play animation infinitely. 46 | val infiniteTransition = rememberInfiniteTransition(label = "infinite_transition") 47 | 48 | // Step 2. Animate the offset of the shadow from -16.dp to 0.dp indefinitely. 49 | val offset by infiniteTransition.animateFloat( 50 | initialValue = LocalDensity.current.run { -16.dp.toPx() }, 51 | targetValue = LocalDensity.current.run { 0.dp.toPx() }, 52 | animationSpec = infiniteRepeatable( 53 | animation = tween( 54 | durationMillis = 1000, 55 | easing = LinearEasing 56 | ), 57 | repeatMode = RepeatMode.Reverse 58 | ), 59 | label = "gradient_rotation" 60 | ) 61 | 62 | // Step 3. Create a TextMeasurer to measure text metrics for text layout. 63 | val textMeasurer = rememberTextMeasurer() 64 | 65 | // Step 4. Measure the text and produce a TextLayoutResult. 66 | val textLayoutResult = remember(text) { 67 | textMeasurer.measure( 68 | text = text, 69 | style = textStyle 70 | ) 71 | } 72 | 73 | // Step 5. Determine the size of the Text based on its measured dimensions. 74 | val textSize = LocalDensity.current.run { textLayoutResult.size.toSize().toDpSize() } 75 | 76 | // Step 6. Create a Canvas to draw the Text. 77 | Canvas( 78 | modifier = modifier 79 | .size(textSize * 2) 80 | ) { 81 | 82 | // Step 7. Apply a translation transformation to the drawing operations within this block 83 | translate(top = offset) { 84 | 85 | // Step 8. Draw the Text with the specified layout result, color and shadow 86 | drawText( 87 | textLayoutResult = textLayoutResult, 88 | topLeft = Offset( 89 | x = textLayoutResult.size.width.toFloat() / 2, 90 | y = textLayoutResult.size.height.toFloat() / 2 91 | ), 92 | color = Color.Black, 93 | shadow = Shadow( 94 | color = Color.Gray, 95 | offset = Offset( 96 | x = 2.dp.toPx(), 97 | y = offset * -2 98 | ), 99 | blurRadius = shadowSize.toPx() 100 | ) 101 | ) 102 | } 103 | } 104 | } 105 | 106 | @Preview( 107 | showBackground = true, 108 | backgroundColor = 0xFFFFFFFF 109 | ) 110 | @Composable 111 | fun TextWithShadowPreview() { 112 | TextWithShadow(text = "Text") 113 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.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/codescape/canvas/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.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.WindowCompat 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 CanvasTheme( 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 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codescape/canvas/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.codescape.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/android_robot.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /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/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/drawable/lines.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/palms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/drawable/palms.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logomount/canvas/6a8057935b73d07365624d84266bce44fef798f8/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 | Canvas 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |