├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .idea └── .gitignore ├── CODE_OF_CONDUCT.md ├── DrawBox ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── ak1 │ └── drawbox │ ├── DrawBox.kt │ ├── DrawController.kt │ └── Helper.kt ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── ak1 │ │ └── drawboxsample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── ak1 │ │ │ └── drawboxsample │ │ │ ├── MainActivity.kt │ │ │ ├── data │ │ │ └── local │ │ │ │ └── DataStore.kt │ │ │ ├── helper │ │ │ └── Helper.kt │ │ │ └── ui │ │ │ ├── components │ │ │ ├── CustomViews.kt │ │ │ └── RootView.kt │ │ │ ├── screens │ │ │ └── HomeScreen.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_color.xml │ │ ├── ic_download.xml │ │ ├── ic_redo.xml │ │ ├── ic_refresh.xml │ │ ├── ic_size.xml │ │ └── ic_undo.xml │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── io │ └── ak1 │ └── drawboxsample │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── media ├── banner.gif ├── media.gif └── small_icon.png ├── settings.gradle └── web-page ├── index.html └── styles.css /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Browser [e.g. stock browser, safari] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | /compiler.xml 5 | /gradle.xml 6 | /misc.xml 7 | /vcs.xml 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | fxn769@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /DrawBox/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /DrawBox/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'com.vanniktech.maven.publish' 5 | } 6 | 7 | android { 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 33 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | sourceSets { 18 | main { 19 | java { 20 | srcDir 'src/main/java' 21 | } 22 | resources { 23 | srcDir 'src/../lib' 24 | } 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | 39 | composeOptions { 40 | kotlinCompilerExtensionVersion "1.2.0" 41 | } 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | buildFeatures { 46 | compose true 47 | } 48 | namespace 'io.ak1.drawbox' 49 | } 50 | 51 | dependencies { 52 | 53 | implementation 'androidx.core:core-ktx:1.9.0' 54 | implementation 'androidx.appcompat:appcompat:1.6.1' 55 | implementation 'com.google.android.material:material:1.8.0' 56 | implementation "androidx.compose.ui:ui:$compose_version" 57 | implementation "androidx.compose.material:material:1.3.1" 58 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 59 | } 60 | task clearJar(type: Delete) { 61 | delete 'build/outputs/ProjectName.jar' 62 | } 63 | 64 | task makeJar(type: Copy) { 65 | from('build/intermediates/bundles/release/') 66 | into('build/outputs/') 67 | include('classes.jar') 68 | rename ('classes.jar', 'ProjectName.jar') 69 | } 70 | 71 | makeJar.dependsOn(clearJar, build) -------------------------------------------------------------------------------- /DrawBox/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/DrawBox/consumer-rules.pro -------------------------------------------------------------------------------- /DrawBox/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=io.ak1 2 | POM_ARTIFACT_ID=drawbox 3 | VERSION_NAME=1.0.3 4 | POM_NAME=drawbox 5 | POM_PACKAGING=aar 6 | POM_DESCRIPTION=draw pad created on jetpack compose 7 | POM_INCEPTION_YEAR=2022 8 | POM_URL=https://github.com/akshay2211/DrawBox 9 | POM_SCM_URL=https://github.com/akshay2211/DrawBox 10 | POM_SCM_CONNECTION=scm:git@github.com:akshay2211/DrawBox.git 11 | POM_SCM_DEV_CONNECTION=scm:git@github.com:akshay2211/DrawBox.git 12 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 13 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 14 | POM_LICENCE_DIST=repo 15 | POM_DEVELOPER_ID=akshay2211 16 | POM_DEVELOPER_NAME=Akshay Sharma 17 | POM_DEVELOPER_URL=https://github.com/akshay2211 -------------------------------------------------------------------------------- /DrawBox/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 -------------------------------------------------------------------------------- /DrawBox/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /DrawBox/src/main/java/io/ak1/drawbox/DrawBox.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawbox 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.gestures.detectDragGestures 6 | import androidx.compose.foundation.gestures.detectTapGestures 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.ImageBitmap 14 | import androidx.compose.ui.graphics.StrokeCap 15 | import androidx.compose.ui.graphics.StrokeJoin 16 | import androidx.compose.ui.graphics.drawscope.Stroke 17 | import androidx.compose.ui.input.pointer.pointerInput 18 | import androidx.compose.ui.platform.ComposeView 19 | import androidx.compose.ui.viewinterop.AndroidView 20 | 21 | /** 22 | * Created by akshay on 10/12/21 23 | * https://ak1.io 24 | */ 25 | 26 | 27 | @Composable 28 | fun DrawBox( 29 | drawController: DrawController, 30 | modifier: Modifier = Modifier.fillMaxSize(), 31 | backgroundColor: Color = MaterialTheme.colors.background, 32 | bitmapCallback: (ImageBitmap?, Throwable?) -> Unit, 33 | trackHistory: (undoCount: Int, redoCount: Int) -> Unit = { _, _ -> } 34 | ) = AndroidView( 35 | factory = { 36 | ComposeView(it).apply { 37 | setContent { 38 | LaunchedEffect(drawController) { 39 | drawController.changeBgColor(backgroundColor) 40 | drawController.trackBitmaps(this@apply, this, bitmapCallback) 41 | drawController.trackHistory(this, trackHistory) 42 | } 43 | Canvas(modifier = modifier 44 | .background(drawController.bgColor) 45 | .pointerInput(Unit) { 46 | detectTapGestures( 47 | onTap = {offset-> 48 | // println("TAP!") 49 | drawController.insertNewPath(offset) 50 | drawController.updateLatestPath(offset) 51 | drawController.pathList 52 | } 53 | ) 54 | } 55 | .pointerInput(Unit) { 56 | detectDragGestures( 57 | onDragStart = { offset -> 58 | drawController.insertNewPath(offset) 59 | // println("DRAG!") 60 | } 61 | ) { change, _ -> 62 | val newPoint = change.position 63 | drawController.updateLatestPath(newPoint) 64 | } 65 | 66 | }) { 67 | drawController.pathList.forEach { pw -> 68 | drawPath( 69 | createPath(pw.points), 70 | color = pw.strokeColor, 71 | alpha = pw.alpha, 72 | style = Stroke( 73 | width = pw.strokeWidth, 74 | cap = StrokeCap.Round, 75 | join = StrokeJoin.Round 76 | ) 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | }, 83 | modifier = modifier 84 | ) 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /DrawBox/src/main/java/io/ak1/drawbox/DrawController.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawbox 2 | 3 | import android.graphics.Bitmap 4 | import android.view.View 5 | import androidx.compose.runtime.* 6 | import androidx.compose.runtime.snapshots.SnapshotStateList 7 | import androidx.compose.ui.geometry.Offset 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.ImageBitmap 10 | import androidx.compose.ui.graphics.asImageBitmap 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.flow.* 13 | 14 | /** 15 | * Created by akshay on 18/01/22 16 | * https://ak1.io 17 | */ 18 | class DrawController internal constructor(val trackHistory: (undoCount: Int, redoCount: Int) -> Unit = { _, _ -> }) { 19 | 20 | private val _redoPathList = mutableStateListOf() 21 | private val _undoPathList = mutableStateListOf() 22 | internal val pathList: SnapshotStateList = _undoPathList 23 | 24 | 25 | private val _historyTracker = MutableSharedFlow(extraBufferCapacity = 1) 26 | private val historyTracker = _historyTracker.asSharedFlow() 27 | 28 | fun trackHistory( 29 | scope: CoroutineScope, 30 | trackHistory: (undoCount: Int, redoCount: Int) -> Unit 31 | ) { 32 | historyTracker 33 | .onEach { trackHistory(_undoPathList.size, _redoPathList.size) } 34 | .launchIn(scope) 35 | } 36 | 37 | 38 | private val _bitmapGenerators = MutableSharedFlow(extraBufferCapacity = 1) 39 | private val bitmapGenerators = _bitmapGenerators.asSharedFlow() 40 | 41 | fun saveBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888) = 42 | _bitmapGenerators.tryEmit(config) 43 | 44 | var opacity by mutableStateOf(1f) 45 | private set 46 | 47 | var strokeWidth by mutableStateOf(10f) 48 | private set 49 | 50 | var color by mutableStateOf(Color.Red) 51 | private set 52 | 53 | var bgColor by mutableStateOf(Color.Black) 54 | private set 55 | 56 | fun changeOpacity(value: Float) { 57 | opacity = value 58 | } 59 | 60 | fun changeColor(value: Color) { 61 | color = value 62 | } 63 | 64 | fun changeBgColor(value: Color) { 65 | bgColor = value 66 | } 67 | 68 | fun changeStrokeWidth(value: Float) { 69 | strokeWidth = value 70 | } 71 | 72 | fun importPath(drawBoxPayLoad: DrawBoxPayLoad) { 73 | reset() 74 | bgColor = drawBoxPayLoad.bgColor 75 | _undoPathList.addAll(drawBoxPayLoad.path) 76 | _historyTracker.tryEmit("${_undoPathList.size}") 77 | } 78 | 79 | fun exportPath() = DrawBoxPayLoad(bgColor, pathList.toList()) 80 | 81 | 82 | fun unDo() { 83 | if (_undoPathList.isNotEmpty()) { 84 | val last = _undoPathList.last() 85 | _redoPathList.add(last) 86 | _undoPathList.remove(last) 87 | trackHistory(_undoPathList.size, _redoPathList.size) 88 | _historyTracker.tryEmit("Undo - ${_undoPathList.size}") 89 | } 90 | } 91 | 92 | fun reDo() { 93 | if (_redoPathList.isNotEmpty()) { 94 | val last = _redoPathList.last() 95 | _undoPathList.add(last) 96 | _redoPathList.remove(last) 97 | trackHistory(_undoPathList.size, _redoPathList.size) 98 | _historyTracker.tryEmit("Redo - ${_redoPathList.size}") 99 | } 100 | } 101 | 102 | 103 | fun reset() { 104 | _redoPathList.clear() 105 | _undoPathList.clear() 106 | _historyTracker.tryEmit("-") 107 | } 108 | 109 | fun updateLatestPath(newPoint: Offset) { 110 | val index = _undoPathList.lastIndex 111 | _undoPathList[index].points.add(newPoint) 112 | } 113 | 114 | fun insertNewPath(newPoint: Offset) { 115 | val pathWrapper = PathWrapper( 116 | points = mutableStateListOf(newPoint), 117 | strokeColor = color, 118 | alpha = opacity, 119 | strokeWidth = strokeWidth, 120 | ) 121 | _undoPathList.add(pathWrapper) 122 | _redoPathList.clear() 123 | _historyTracker.tryEmit("${_undoPathList.size}") 124 | } 125 | 126 | fun trackBitmaps( 127 | it: View, 128 | coroutineScope: CoroutineScope, 129 | onCaptured: (ImageBitmap?, Throwable?) -> Unit 130 | ) = bitmapGenerators 131 | .mapNotNull { config -> it.drawBitmapFromView(it.context, config) } 132 | .onEach { bitmap -> onCaptured(bitmap.asImageBitmap(), null) } 133 | .catch { error -> onCaptured(null, error) } 134 | .launchIn(coroutineScope) 135 | } 136 | 137 | @Composable 138 | fun rememberDrawController(): DrawController { 139 | return remember { DrawController() } 140 | } -------------------------------------------------------------------------------- /DrawBox/src/main/java/io/ak1/drawbox/Helper.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawbox 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import android.os.Build 7 | import android.os.Handler 8 | import android.os.Looper 9 | import android.view.PixelCopy 10 | import android.view.View 11 | import androidx.compose.runtime.snapshots.SnapshotStateList 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.Path 15 | import androidx.core.view.doOnLayout 16 | import androidx.core.view.drawToBitmap 17 | import kotlin.coroutines.resume 18 | import kotlin.coroutines.resumeWithException 19 | import kotlin.coroutines.suspendCoroutine 20 | 21 | /** 22 | * Created by akshay on 24/12/21 23 | * https://ak1.io 24 | */ 25 | 26 | 27 | private fun View.getRect(x: Int, y: Int): android.graphics.Rect { 28 | val viewWidth = this.width 29 | val viewHeight = this.height 30 | return android.graphics.Rect(x, y, viewWidth + x, viewHeight + y) 31 | } 32 | 33 | 34 | //Model 35 | data class PathWrapper( 36 | var points: SnapshotStateList, 37 | val strokeWidth: Float = 5f, 38 | val strokeColor: Color, 39 | val alpha: Float = 1f 40 | ) 41 | 42 | data class DrawBoxPayLoad(val bgColor: Color, val path: List) 43 | 44 | fun createPath(points: List) = Path().apply { 45 | if (points.size > 1) { 46 | var oldPoint: Offset? = null 47 | this.moveTo(points[0].x, points[0].y) 48 | for (i in 1 until points.size) { 49 | val point: Offset = points[i] 50 | oldPoint?.let { 51 | val midPoint = calculateMidpoint(it, point) 52 | if (i == 1) { 53 | this.lineTo(midPoint.x, midPoint.y) 54 | } else { 55 | this.quadraticBezierTo(it.x, it.y, midPoint.x, midPoint.y) 56 | } 57 | } 58 | oldPoint = point 59 | } 60 | oldPoint?.let { this.lineTo(it.x, oldPoint.y) } 61 | } 62 | } 63 | 64 | private fun calculateMidpoint(start: Offset, end: Offset) = 65 | Offset((start.x + end.x) / 2, (start.y + end.y) / 2) 66 | 67 | 68 | internal suspend fun View.drawBitmapFromView(context: Context, config: Bitmap.Config): Bitmap = 69 | suspendCoroutine { continuation -> 70 | doOnLayout { view -> 71 | if (Build.VERSION_CODES.O > Build.VERSION.SDK_INT) { 72 | continuation.resume(view.drawToBitmap(config)) 73 | return@doOnLayout 74 | } 75 | 76 | val window = 77 | (context as? Activity)?.window ?: error("Can't get window from the Context") 78 | 79 | Bitmap.createBitmap(width, height, config).apply { 80 | val (x, y) = IntArray(2).apply { view.getLocationInWindow(this) } 81 | PixelCopy.request( 82 | window, 83 | getRect(x, y), 84 | this, 85 | { copyResult -> 86 | if (copyResult == PixelCopy.SUCCESS) continuation.resume(this) else continuation.resumeWithException( 87 | RuntimeException("Bitmap generation failed") 88 | ) 89 | }, 90 | Handler(Looper.getMainLooper()) 91 | ) 92 | } 93 | } 94 | } 95 | 96 | 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # DrawBox 4 | 5 | [![Android Weekly](https://img.shields.io/badge/Featured%20in%20androidweekly.net-Issue%20%23502-blue.svg?style=flat-square)](https://androidweekly.net/issues/issue-502) 6 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-DrawBox-green.svg?style=flat-square)](https://android-arsenal.com/details/1/8292) 7 | [![Kotlin Weekly](https://img.shields.io/badge/Kotlin%20Weekly-DrawBox-purple.svg?style=flat-square)](https://mailchi.mp/kotlinweekly/kotlin-weekly-294) 8 | [![Maven Central](https://img.shields.io/maven-central/v/io.ak1/drawbox?style=flat-square)](https://search.maven.org/artifact/io.ak1/drawbox) 9 | [![Google Dev Library](https://img.shields.io/badge/Google%20Dev%20Library-DrawBox-brightgreen.svg?style=flat-square)](https://devlibrary.withgoogle.com/products/android/repos/akshay2211-DrawBox) 10 | 11 | DrawBox is a multi-purpose tool to draw anything on canvas, written completely on jetpack compose. 12 | 13 | ## Features 14 | * Customisable stoke size and color 15 | * Inbuilt Undo and Redo options 16 | * Reset option 17 | * Easy Implementations 18 | * Export feature to store history localy 19 | * Written on Jetpack-Compose 20 | 21 | ## Demo 22 | 23 | 24 | ## Usage 25 | ```kotlin 26 | val controller = rememberDrawController() 27 | 28 | DrawBox(drawController = controller, modifier = Modifier.fillMaxSize().weight(1f, true)) 29 | ``` 30 | With multiple methods in DrawController 31 | ```kotlin 32 | * setStrokeColor(color: Color) 33 | * setStrokeWidth(width: Float) 34 | * unDo() 35 | * reDo() 36 | * reset() 37 | * getDrawBoxBitmap() // gives the result bitmap from canvas 38 | * importPath(path) 39 | * exportPath() 40 | ``` 41 | 42 | ## Download 43 | [![Download](https://img.shields.io/badge/Download-blue.svg?style=flat-square)](https://search.maven.org/artifact/io.ak1/drawbox) or grab via Gradle: 44 | 45 | include in app level build.gradle 46 | ```groovy 47 | repositories { 48 | mavenCentral() 49 | } 50 | ``` 51 | ```groovy 52 | implementation 'io.ak1:drawbox:1.0.3' 53 | ``` 54 | or Maven: 55 | ```xml 56 | 57 | io.ak1 58 | drawbox 59 | 1.0.3 60 | pom 61 | 62 | ``` 63 | or ivy: 64 | ```xml 65 | 66 | 67 | 68 | ``` 69 | 70 | ## Thanks to 71 | [RangVikalp](https://github.com/akshay2211/rang-vikalp) for the beautiful color picker used in DrawBox 72 | 73 | ## License 74 | Licensed under the Apache License, Version 2.0, [click here for the full license](/LICENSE). 75 | 76 | ## Author & support 77 | This project was created by [Akshay Sharma](https://akshay2211.github.io/). 78 | 79 | > If you appreciate my work, consider buying me a cup of :coffee: to keep me recharged :metal: by [PayPal](https://www.paypal.me/akshay2211) 80 | 81 | > I love using my work and I'm available for contract work. Freelancing helps to maintain and keep [my open source projects](https://github.com/akshay2211/) up to date! 82 | 83 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 33 8 | 9 | defaultConfig { 10 | applicationId "io.ak1.drawboxsample" 11 | minSdk 21 12 | targetSdk 33 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | vectorDrawables { 18 | useSupportLibrary true 19 | } 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | } 35 | buildFeatures { 36 | compose true 37 | } 38 | composeOptions { 39 | kotlinCompilerExtensionVersion "1.2.0" 40 | } 41 | packagingOptions { 42 | resources { 43 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 44 | } 45 | } 46 | namespace 'io.ak1.drawboxsample' 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.9.0' 52 | implementation 'androidx.appcompat:appcompat:1.6.1' 53 | implementation 'com.google.android.material:material:1.8.0' 54 | implementation "androidx.compose.ui:ui:$compose_version" 55 | implementation "androidx.compose.material:material:1.3.1" 56 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 57 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' 58 | implementation 'androidx.activity:activity-compose:1.6.1' 59 | implementation 'androidx.datastore:datastore-preferences:1.0.0' 60 | implementation 'com.google.code.gson:gson:2.10.1' 61 | implementation 'io.ak1:rang-vikalp:1.0.0-alpha02' 62 | implementation project(path: ':drawbox') 63 | testImplementation 'junit:junit:4.13.2' 64 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 66 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 67 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 68 | } -------------------------------------------------------------------------------- /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/io/ak1/drawboxsample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample 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("io.ak1.drawboxsample", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import io.ak1.drawboxsample.helper.activityChooser 7 | import io.ak1.drawboxsample.helper.checkAndAskPermission 8 | import io.ak1.drawboxsample.helper.saveImage 9 | import io.ak1.drawboxsample.ui.components.Root 10 | import io.ak1.drawboxsample.ui.screens.HomeScreen 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | 16 | 17 | class MainActivity : ComponentActivity() { 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContent { 22 | Root(window = window) { 23 | HomeScreen { 24 | checkAndAskPermission { 25 | CoroutineScope(Dispatchers.IO).launch { 26 | val uri = saveImage(it) 27 | withContext(Dispatchers.Main) { 28 | startActivity(activityChooser(uri)) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/data/local/DataStore.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.data.local 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.toArgb 7 | import androidx.core.graphics.alpha 8 | import androidx.core.graphics.blue 9 | import androidx.core.graphics.green 10 | import androidx.core.graphics.red 11 | import androidx.datastore.core.DataStore 12 | import androidx.datastore.preferences.core.Preferences 13 | import androidx.datastore.preferences.core.intPreferencesKey 14 | import androidx.datastore.preferences.preferencesDataStore 15 | import kotlinx.coroutines.flow.map 16 | 17 | /** 18 | * Created by akshay on 28/12/21 19 | * https://ak1.io 20 | */ 21 | // DataStore Instance 22 | val Context.dataStore: DataStore by preferencesDataStore(name = "settings") 23 | 24 | // Preference Keys 25 | val themePreferenceKey = intPreferencesKey("list_theme") 26 | 27 | internal val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) 28 | internal const val PERMISSION_CODE = 100 29 | 30 | // Retrieving functions 31 | /** 32 | * extension [isDarkThemeOn] checks the saved theme from preference 33 | * and returns boolean 34 | */ 35 | fun Context.isDarkThemeOn() = dataStore.data 36 | .map { preferences -> 37 | // No type safety. 38 | preferences[themePreferenceKey] ?: 0 39 | } 40 | 41 | fun Color.convertToOldColor(): Int { 42 | val color = this.toArgb() 43 | return android.graphics.Color.argb( 44 | color.alpha, 45 | color.red, 46 | color.green, 47 | color.blue 48 | ) 49 | } -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/helper/Helper.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.helper 2 | 3 | import android.app.Activity 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.graphics.Bitmap 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.os.Environment 12 | import android.provider.MediaStore 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.content.ContextCompat 15 | import io.ak1.drawboxsample.data.local.PERMISSION_CODE 16 | import io.ak1.drawboxsample.data.local.permissions 17 | import java.io.File 18 | 19 | /** 20 | * Created by akshay on 29/12/21 21 | * https://ak1.io 22 | */ 23 | 24 | internal fun Activity.checkAndAskPermission(continueNext: () -> Unit) { 25 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, 26 | permissions[0]) != PackageManager.PERMISSION_GRANTED 27 | ) { 28 | ActivityCompat.requestPermissions(this, 29 | permissions, 30 | PERMISSION_CODE) 31 | return 32 | } 33 | continueNext() 34 | } 35 | 36 | internal fun activityChooser(uri: Uri?) = Intent.createChooser(Intent().apply { 37 | type = "image/*" 38 | action = Intent.ACTION_VIEW 39 | data = uri 40 | }, "Select Gallery App") 41 | 42 | //writing files to storage via scope and normal manner acc. to Api level 43 | internal fun Context.saveImage(bitmap: Bitmap): Uri? { 44 | var uri: Uri? = null 45 | try { 46 | val fileName = System.nanoTime().toString() + ".png" 47 | val values = ContentValues().apply { 48 | put(MediaStore.Images.Media.DISPLAY_NAME, fileName) 49 | put(MediaStore.Images.Media.MIME_TYPE, "image/png") 50 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 51 | put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/") 52 | put(MediaStore.MediaColumns.IS_PENDING, 1) 53 | } else { 54 | val directory = 55 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) 56 | val file = File(directory, fileName) 57 | put(MediaStore.MediaColumns.DATA, file.absolutePath) 58 | } 59 | } 60 | 61 | uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) 62 | uri?.let { 63 | contentResolver.openOutputStream(it).use { output -> 64 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, output) 65 | } 66 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 67 | values.apply { 68 | clear() 69 | put(MediaStore.Audio.Media.IS_PENDING, 0) 70 | } 71 | contentResolver.update(uri, values, null, null) 72 | } 73 | } 74 | return uri 75 | } catch (e: java.lang.Exception) { 76 | if (uri != null) { 77 | // Don't leave an orphan entry in the MediaStore 78 | contentResolver.delete(uri, null, null) 79 | } 80 | throw e 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/components/CustomViews.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.components 2 | 3 | import android.widget.SeekBar 4 | import androidx.annotation.DrawableRes 5 | import androidx.compose.animation.* 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.Icon 10 | import androidx.compose.material.IconButton 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.platform.LocalDensity 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.viewinterop.AndroidView 23 | import androidx.core.graphics.BlendModeColorFilterCompat 24 | import androidx.core.graphics.BlendModeCompat 25 | import io.ak1.drawbox.DrawController 26 | import io.ak1.drawboxsample.R 27 | 28 | 29 | /** 30 | * Created by akshay on 29/12/21 31 | * https://ak1.io 32 | */ 33 | 34 | @Composable 35 | fun ControlsBar( 36 | drawController: DrawController, 37 | onDownloadClick: () -> Unit, 38 | onColorClick: () -> Unit, 39 | onBgColorClick: () -> Unit, 40 | onSizeClick: () -> Unit, 41 | undoVisibility: MutableState, 42 | redoVisibility: MutableState, 43 | colorValue: MutableState, 44 | bgColorValue: MutableState, 45 | sizeValue: MutableState 46 | ) { 47 | Row(modifier = Modifier.padding(12.dp), horizontalArrangement = Arrangement.SpaceAround) { 48 | MenuItems( 49 | R.drawable.ic_download, 50 | "download", 51 | if (undoVisibility.value) MaterialTheme.colors.primary else MaterialTheme.colors.primaryVariant 52 | ) { 53 | if (undoVisibility.value) onDownloadClick() 54 | } 55 | MenuItems( 56 | R.drawable.ic_undo, 57 | "undo", 58 | if (undoVisibility.value) MaterialTheme.colors.primary else MaterialTheme.colors.primaryVariant 59 | ) { 60 | if (undoVisibility.value) drawController.unDo() 61 | } 62 | MenuItems( 63 | R.drawable.ic_redo, 64 | "redo", 65 | if (redoVisibility.value) MaterialTheme.colors.primary else MaterialTheme.colors.primaryVariant 66 | ) { 67 | if (redoVisibility.value) drawController.reDo() 68 | } 69 | MenuItems( 70 | R.drawable.ic_refresh, 71 | "reset", 72 | if (redoVisibility.value || undoVisibility.value) MaterialTheme.colors.primary else MaterialTheme.colors.primaryVariant 73 | ) { 74 | drawController.reset() 75 | } 76 | MenuItems(R.drawable.ic_color, "background color", bgColorValue.value, bgColorValue.value == MaterialTheme.colors.background) { 77 | onBgColorClick() 78 | } 79 | MenuItems(R.drawable.ic_color, "stroke color", colorValue.value) { 80 | onColorClick() 81 | } 82 | MenuItems(R.drawable.ic_size, "stroke size", MaterialTheme.colors.primary) { 83 | onSizeClick() 84 | } 85 | } 86 | } 87 | 88 | @Composable 89 | fun RowScope.MenuItems( 90 | @DrawableRes resId: Int, 91 | desc: String, 92 | colorTint: Color, 93 | border: Boolean = false, 94 | onClick: () -> Unit 95 | ) { 96 | val modifier = Modifier.size(24.dp) 97 | IconButton( 98 | onClick = onClick, modifier = Modifier.weight(1f, true) 99 | ) { 100 | Icon( 101 | painterResource(id = resId), 102 | contentDescription = desc, 103 | tint = colorTint, 104 | modifier = if (border) modifier.border( 105 | 0.5.dp, 106 | Color.White, 107 | shape = CircleShape 108 | ) else modifier 109 | ) 110 | } 111 | } 112 | 113 | @Composable 114 | fun CustomSeekbar( 115 | isVisible: Boolean, 116 | max: Int = 200, 117 | progress: Int = max, 118 | progressColor: Int, 119 | thumbColor: Int, 120 | onProgressChanged: (Int) -> Unit 121 | ) { 122 | val density = LocalDensity.current 123 | AnimatedVisibility( 124 | visible = isVisible, 125 | enter = slideInVertically { 126 | // Slide in from 40 dp from the top. 127 | with(density) { -40.dp.roundToPx() } 128 | } + expandVertically( 129 | // Expand from the top. 130 | expandFrom = Alignment.Top 131 | ) + fadeIn( 132 | // Fade in with the initial alpha of 0.3f. 133 | initialAlpha = 0.3f 134 | ), 135 | exit = slideOutVertically() + shrinkVertically() + fadeOut() 136 | ) { 137 | val context = LocalContext.current 138 | Column( 139 | modifier = Modifier 140 | .height(100.dp) 141 | .fillMaxWidth(), 142 | verticalArrangement = Arrangement.SpaceEvenly 143 | ) { 144 | Text(text = "Stroke Width", modifier = Modifier.padding(12.dp, 0.dp, 0.dp, 0.dp)) 145 | AndroidView( 146 | { SeekBar(context) }, 147 | modifier = Modifier 148 | .fillMaxWidth() 149 | ) { 150 | it.progressDrawable.colorFilter = 151 | BlendModeColorFilterCompat.createBlendModeColorFilterCompat( 152 | progressColor, 153 | BlendModeCompat.SRC_ATOP 154 | ) 155 | it.thumb.colorFilter = 156 | BlendModeColorFilterCompat.createBlendModeColorFilterCompat( 157 | thumbColor, 158 | BlendModeCompat.SRC_ATOP 159 | ) 160 | it.max = max 161 | it.progress = progress 162 | it.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 163 | override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) { 164 | 165 | } 166 | 167 | override fun onStartTrackingTouch(p0: SeekBar?) {} 168 | override fun onStopTrackingTouch(p0: SeekBar?) { 169 | onProgressChanged(p0?.progress ?: it.progress) 170 | } 171 | }) 172 | } 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/components/RootView.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.components 2 | 3 | import android.view.Window 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Surface 6 | import androidx.compose.runtime.Composable 7 | import io.ak1.drawboxsample.ui.theme.DrawBoxTheme 8 | import io.ak1.drawboxsample.ui.theme.isSystemInDarkThemeCustom 9 | import io.ak1.drawboxsample.ui.theme.StatusBarConfig 10 | 11 | /** 12 | * Created by akshay on 29/12/21 13 | * https://ak1.io 14 | */ 15 | @Composable 16 | fun Root(window: Window, content: @Composable () -> Unit) { 17 | val isDark = isSystemInDarkThemeCustom() 18 | DrawBoxTheme(isDark) { 19 | window.StatusBarConfig(isDark) 20 | Surface(color = MaterialTheme.colors.background) { 21 | content.invoke() 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/screens/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.screens 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.asAndroidBitmap 13 | import io.ak1.drawbox.DrawBox 14 | import io.ak1.drawbox.rememberDrawController 15 | import io.ak1.drawboxsample.data.local.convertToOldColor 16 | import io.ak1.drawboxsample.ui.components.ControlsBar 17 | import io.ak1.drawboxsample.ui.components.CustomSeekbar 18 | import io.ak1.rangvikalp.RangVikalp 19 | import io.ak1.rangvikalp.defaultSelectedColor 20 | 21 | 22 | /** 23 | * Created by akshay on 29/12/21 24 | * https://ak1.io 25 | */ 26 | 27 | @Composable 28 | fun HomeScreen(save: (Bitmap) -> Unit) { 29 | val undoVisibility = remember { mutableStateOf(false) } 30 | val redoVisibility = remember { mutableStateOf(false) } 31 | val colorBarVisibility = remember { mutableStateOf(false) } 32 | val sizeBarVisibility = remember { mutableStateOf(false) } 33 | val currentColor = remember { mutableStateOf(defaultSelectedColor) } 34 | val bg = MaterialTheme.colors.background 35 | val currentBgColor = remember { mutableStateOf(bg) } 36 | val currentSize = remember { mutableStateOf(10) } 37 | val colorIsBg = remember { mutableStateOf(false) } 38 | val drawController = rememberDrawController() 39 | 40 | Box { 41 | Column { 42 | DrawBox( 43 | drawController = drawController, 44 | backgroundColor = currentBgColor.value, 45 | modifier = Modifier 46 | .fillMaxSize() 47 | .weight(1f, fill = false), 48 | bitmapCallback = { imageBitmap, error -> 49 | imageBitmap?.let { 50 | save(it.asAndroidBitmap()) 51 | } 52 | } 53 | ) { undoCount, redoCount -> 54 | sizeBarVisibility.value = false 55 | colorBarVisibility.value = false 56 | undoVisibility.value = undoCount != 0 57 | redoVisibility.value = redoCount != 0 58 | } 59 | 60 | ControlsBar( 61 | drawController = drawController, 62 | { 63 | drawController.saveBitmap() 64 | }, 65 | { 66 | colorBarVisibility.value = when (colorBarVisibility.value) { 67 | false -> true 68 | colorIsBg.value -> true 69 | else -> false 70 | } 71 | colorIsBg.value = false 72 | sizeBarVisibility.value = false 73 | }, 74 | { 75 | colorBarVisibility.value = when (colorBarVisibility.value) { 76 | false -> true 77 | !colorIsBg.value -> true 78 | else -> false 79 | } 80 | colorIsBg.value = true 81 | sizeBarVisibility.value = false 82 | }, 83 | { 84 | sizeBarVisibility.value = !sizeBarVisibility.value 85 | colorBarVisibility.value = false 86 | }, 87 | undoVisibility = undoVisibility, 88 | redoVisibility = redoVisibility, 89 | colorValue = currentColor, 90 | bgColorValue = currentBgColor, 91 | sizeValue = currentSize 92 | ) 93 | RangVikalp(isVisible = colorBarVisibility.value, showShades = true) { 94 | if (colorIsBg.value) { 95 | currentBgColor.value = it 96 | drawController.changeBgColor(it) 97 | } else { 98 | currentColor.value = it 99 | drawController.changeColor(it) 100 | } 101 | } 102 | CustomSeekbar( 103 | isVisible = sizeBarVisibility.value, 104 | progress = currentSize.value, 105 | progressColor = MaterialTheme.colors.primary.convertToOldColor(), 106 | thumbColor = currentColor.value.convertToOldColor() 107 | ) { 108 | currentSize.value = it 109 | drawController.changeStrokeWidth(it.toFloat()) 110 | } 111 | } 112 | 113 | } 114 | } 115 | /* 116 | var path: String = "" 117 | val json = GsonBuilder().create() 118 | if(path.isNotBlank()){ 119 | val listOfMyClassObject = object : TypeToken>() {}.type 120 | drawController.importPath(json.fromJson(path,listOfMyClassObject)) 121 | path = "" 122 | }else{ 123 | path = json.toJson(drawController.exportPath()) 124 | Log.e("to string","${path}") 125 | } 126 | */ 127 | -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Accent = Color(0xFF4E33FF) 6 | val WhiteDark = Color(0xFFF9F9F9) 7 | val WhiteLite = Color(0xFFFFFFFF) 8 | val BlackDark = Color(0xFF0E121B) 9 | val BlackLite = Color(0xFF171C26) 10 | val Grey = Color(0xFF586070) 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.theme 2 | 3 | import android.content.res.Configuration 4 | import android.view.Window 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.darkColors 8 | import androidx.compose.material.lightColors 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.collectAsState 11 | import androidx.compose.ui.graphics.toArgb 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.core.view.WindowInsetsControllerCompat 14 | import io.ak1.drawboxsample.data.local.dataStore 15 | import io.ak1.drawboxsample.data.local.isDarkThemeOn 16 | import io.ak1.drawboxsample.data.local.themePreferenceKey 17 | import kotlinx.coroutines.flow.first 18 | import kotlinx.coroutines.runBlocking 19 | 20 | private val DarkColorPalette = darkColors( 21 | primary = WhiteDark, 22 | primaryVariant = Grey, 23 | secondary = Accent, 24 | secondaryVariant = WhiteDark, 25 | background = BlackDark, 26 | surface = BlackLite, 27 | onPrimary = BlackDark, 28 | onSecondary = BlackDark, 29 | onBackground = WhiteLite, 30 | onSurface = WhiteDark, 31 | ) 32 | 33 | private val LightColorPalette = lightColors( 34 | primary = BlackDark, 35 | primaryVariant = Grey, 36 | secondary = Accent, 37 | secondaryVariant = BlackDark, 38 | background = WhiteDark, 39 | surface = WhiteLite, 40 | onPrimary = WhiteDark, 41 | onSecondary = WhiteDark, 42 | onBackground = BlackDark, 43 | onSurface = BlackDark, 44 | ) 45 | 46 | @Composable 47 | fun DrawBoxTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 48 | val colors = if (darkTheme) { 49 | DarkColorPalette 50 | } else { 51 | LightColorPalette 52 | } 53 | 54 | MaterialTheme( 55 | colors = colors, 56 | typography = Typography, 57 | shapes = Shapes, 58 | content = content 59 | ) 60 | } 61 | 62 | @Composable 63 | fun isSystemInDarkThemeCustom(): Boolean { 64 | val context = LocalContext.current 65 | val exampleData = runBlocking { context.dataStore.data.first() } 66 | val theme = context.isDarkThemeOn().collectAsState(initial = exampleData[themePreferenceKey] ?: 0) 67 | return when (theme.value) { 68 | 2 -> true 69 | 1 -> false 70 | else -> context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES 71 | } 72 | } 73 | 74 | @Composable 75 | fun Window.StatusBarConfig(darkTheme: Boolean) { 76 | WindowInsetsControllerCompat(this, this.decorView).isAppearanceLightStatusBars = 77 | !darkTheme 78 | this.statusBarColor = MaterialTheme.colors.background.toArgb() 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/io/ak1/drawboxsample/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample.ui.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_color.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_redo.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_size.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_undo.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF3700B3 5 | #FF03DAC5 6 | #FF000000 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DrawBox 3 | Button description 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/test/java/io/ak1/drawboxsample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.ak1.drawboxsample 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 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | compose_version = '1.3.3' 5 | } 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.4.1' 12 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0' 13 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.18.0' 14 | } 15 | } 16 | 17 | allprojects { 18 | plugins.withId("com.vanniktech.maven.publish") { 19 | mavenPublish { 20 | sonatypeHost = "S01" 21 | } 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 23 14:53:11 IST 2021 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 | -------------------------------------------------------------------------------- /media/banner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/media/banner.gif -------------------------------------------------------------------------------- /media/media.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/media/media.gif -------------------------------------------------------------------------------- /media/small_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akshay2211/DrawBox/9fba49e2272b89d6572db3eaa0995498252ca915/media/small_icon.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | maven { url "https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven" } 7 | maven { url 'https://jitpack.io' } 8 | } 9 | } 10 | rootProject.name = "DrawBoxSample" 11 | include ':app' 12 | include ':drawbox' 13 | -------------------------------------------------------------------------------- /web-page/index.html: -------------------------------------------------------------------------------- 1 | DrawBox

Something Exciting is Coming

We're building a powerful real-time collaboration platform to transform the way teams and educators brainstorm, plan, and create.

→ Seamless teamwork
→ Effortless brainstorming
→ Visual collaboration reimagined

Stay tuned – launching soon!

-------------------------------------------------------------------------------- /web-page/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #ffffff; 3 | --text-color: #2d3748; 4 | --gradient-1: #e77f67; 5 | --gradient-2: #63cdda; 6 | --gradient-3: #818cf8; 7 | --shadow-color: rgba(79, 70, 229, 0.1); 8 | } 9 | 10 | [data-theme="dark"] { 11 | --bg-color: #1a1c2e; 12 | --text-color: #e2e8f0; 13 | --gradient-1: #9f7aea; 14 | --gradient-2: #7ab8ea; 15 | --gradient-3: #7aeac5; 16 | --shadow-color: rgba(0, 0, 0, 0.2); 17 | } 18 | 19 | * { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | } 24 | 25 | body { 26 | background: var(--bg-color); 27 | color: var(--text-color); 28 | font-family: "Outfit", sans-serif; 29 | min-height: 100vh; 30 | display: flex; 31 | transition: all 0.3s ease; 32 | } 33 | 34 | a { 35 | color: var(--gradient-1); 36 | text-decoration: none; 37 | } 38 | 39 | #particles-js { 40 | position: fixed; 41 | width: 100%; 42 | height: 100%; 43 | z-index: 1; 44 | } 45 | 46 | .content { 47 | position: relative; 48 | z-index: 2; 49 | padding: 4rem; 50 | max-width: 1200px; 51 | margin: auto; 52 | } 53 | 54 | .theme-toggle { 55 | position: fixed; 56 | top: 2rem; 57 | right: 2rem; 58 | background: linear-gradient(45deg, var(--gradient-1), var(--gradient-2)); 59 | border: none; 60 | width: 3.5rem; 61 | height: 3.5rem; 62 | border-radius: 50%; 63 | cursor: pointer; 64 | z-index: 3; 65 | transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | font-size: 1.5rem; 70 | box-shadow: 0 4px 15px var(--shadow-color); 71 | } 72 | 73 | .theme-toggle:hover { 74 | transform: scale(1.1) rotate(15deg); 75 | box-shadow: 0 6px 20px var(--shadow-color); 76 | } 77 | 78 | h1 { 79 | font-family: "Syne", sans-serif; 80 | font-size: 6rem; 81 | font-weight: 600; 82 | margin-bottom: 2rem; 83 | line-height: 1.1; 84 | background: linear-gradient( 85 | 90deg, 86 | var(--gradient-1), 87 | var(--gradient-2), 88 | var(--gradient-3) 89 | ); 90 | -webkit-background-clip: text; 91 | background-clip: text; 92 | color: transparent; 93 | background-size: 200% auto; 94 | animation: fadeUp 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards, 95 | textShine 3s linear infinite; 96 | opacity: 0; 97 | transform: translateY(30px); 98 | } 99 | 100 | p { 101 | font-size: 1.5rem; 102 | line-height: 1.6; 103 | margin-bottom: 2rem; 104 | opacity: 0; 105 | transform: translateY(30px); 106 | animation: fadeUp 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards 0.2s; 107 | } 108 | 109 | .highlight { 110 | font-family: "Syne", sans-serif; 111 | font-size: 2.5rem; 112 | font-weight: 600; 113 | background: linear-gradient( 114 | 90deg, 115 | var(--gradient-2), 116 | var(--gradient-3), 117 | var(--gradient-1) 118 | ); 119 | -webkit-background-clip: text; 120 | background-clip: text; 121 | color: transparent; 122 | background-size: 200% auto; 123 | opacity: 0; 124 | transform: translateY(30px); 125 | animation: fadeUp 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards 0.4s, 126 | textShine 3s linear infinite; 127 | } 128 | 129 | .features { 130 | margin-top: 4rem; 131 | position: relative; 132 | height: 4rem; 133 | } 134 | 135 | .feature { 136 | position: absolute; 137 | font-size: 1.5rem; 138 | opacity: 0; 139 | transform: translateX(-30px); 140 | transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); 141 | width: 100%; 142 | } 143 | 144 | .feature.active { 145 | opacity: 1; 146 | transform: translateX(0); 147 | } 148 | 149 | .feature span { 150 | background: linear-gradient(90deg, var(--gradient-3), var(--gradient-1)); 151 | -webkit-background-clip: text; 152 | background-clip: text; 153 | color: transparent; 154 | background-size: 200% auto; 155 | animation: textShine 3s linear infinite; 156 | font-weight: 600; 157 | } 158 | .footer { 159 | position: fixed; 160 | bottom: 0; 161 | right: 0; 162 | z-index: 2; 163 | padding: 1rem; 164 | font-size: 0.875rem; 165 | color: var(--text-color); 166 | opacity: 0.7; 167 | background: var(--bg-color); 168 | } 169 | 170 | @media (max-width: 768px) { 171 | .content { 172 | padding: 2rem; 173 | } 174 | h1 { 175 | font-size: 3rem; 176 | } 177 | .highlight { 178 | font-size: 2rem; 179 | } 180 | p { 181 | font-size: 1.25rem; 182 | } 183 | } 184 | 185 | @keyframes textShine { 186 | 0% { 187 | background-position: 0% 50%; 188 | } 189 | 100% { 190 | background-position: 100% 50%; 191 | } 192 | } 193 | 194 | @keyframes fadeUp { 195 | to { 196 | opacity: 1; 197 | transform: translateY(0); 198 | } 199 | } 200 | 201 | @media (max-width: 768px) { 202 | .content { 203 | padding: 2rem; 204 | } 205 | h1 { 206 | font-size: 3rem; 207 | } 208 | .highlight { 209 | font-size: 2rem; 210 | } 211 | p { 212 | font-size: 1.25rem; 213 | } 214 | } 215 | --------------------------------------------------------------------------------