Skip to content

Commit

Permalink
Merge pull request #4 from MarkYav/develop
Browse files Browse the repository at this point in the history
v1.2.0
  • Loading branch information
MarkYav committed Apr 10, 2023
2 parents 4c2f718 + baa05a5 commit f31ab72
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 145 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This is the first multiplatform drawing library!
- Different image rations
- Filling tool
- Optimizing rendering (convert drawn PATHes)
- Migrate from Compose dependencies in DrawController
- Migrate from Compose dependencies in [controller folder](drawbox/src/commonMain/kotlin/io/github/markyav/drawbox/controller)

## Demo

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Library.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ object Library {

val group = "io.github.markyav.drawbox"
val artifact = "drawbox"
val version = "1.1.0"
val version = "1.2.0"

object License {
val name = "Apache-2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,34 @@ package io.github.markyav.drawbox.box
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import io.github.markyav.drawbox.controller.DrawBoxBackground
import io.github.markyav.drawbox.controller.DrawBoxSubscription
import io.github.markyav.drawbox.controller.DrawController
import io.github.markyav.drawbox.model.PathWrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow

@Composable
fun DrawBox(
controller: DrawController,
modifier: Modifier = Modifier.fillMaxSize(),
) {
val path: List<PathWrapper>? = controller.pathToDrawOnCanvas
val background: DrawBoxBackground = controller.background
val canvasAlpha: Float = controller.canvasOpacity
val path: StateFlow<List<PathWrapper>> = remember {
controller.getPathWrappersForDrawbox(DrawBoxSubscription.DynamicUpdate)
}

Box(modifier = modifier) {
DrawBoxBackground(
background = background,
background = controller.background.value,
modifier = Modifier.fillMaxSize(),
)
DrawBoxCanvas(
path = path ?: emptyList(),
alpha = canvasAlpha,
pathListWrapper = path,
alpha = controller.canvasOpacity.value,
onSizeChanged = controller::connectToDrawBox,
onTap = controller::insertNewPath,
onTap = controller::onTap,
onDragStart = controller::insertNewPath,
onDrag = controller::updateLatestPath,
onDragEnd = controller::finalizePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
Expand All @@ -18,10 +20,11 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import io.github.markyav.drawbox.model.PathWrapper
import io.github.markyav.drawbox.util.createPath
import kotlinx.coroutines.flow.StateFlow

@Composable
fun DrawBoxCanvas(
path: List<PathWrapper>,
pathListWrapper: StateFlow<List<PathWrapper>>,
alpha: Float,
onSizeChanged: (IntSize) -> Unit,
onTap: (Offset) -> Unit,
Expand All @@ -33,6 +36,7 @@ fun DrawBoxCanvas(
val onDragMapper: (change: PointerInputChange, dragAmount: Offset) -> Unit = remember {
{ change, _ -> onDrag(change.position) }
}
val path by pathListWrapper.collectAsState()

Canvas(modifier = modifier
.onSizeChanged(onSizeChanged)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,139 +1,194 @@
package io.github.markyav.drawbox.controller

import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.IntSize
import io.github.markyav.drawbox.model.PathWrapper
import io.github.markyav.drawbox.util.addNotNull
import io.github.markyav.drawbox.util.combineStates
import io.github.markyav.drawbox.util.createPath
import io.github.markyav.drawbox.util.pop
import io.github.markyav.drawbox.util.mapState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlin.reflect.KProperty

/**
* DrawController interacts with [DrawBox] and it allows you to control the canvas and all the components with it.
*/
class DrawController {
private var state: DrawBoxConnectionState by mutableStateOf(DrawBoxConnectionState.Disconnected)
private var state: MutableStateFlow<DrawBoxConnectionState> = MutableStateFlow(DrawBoxConnectionState.Disconnected)

/** A stateful list of [Path] that is drawn on the [Canvas]. */
private val drawnPaths: SnapshotStateList<PathWrapper> = mutableStateListOf()
private val drawnPaths: MutableStateFlow<List<PathWrapper>> = MutableStateFlow(emptyList())

private val completelyDrawnPaths: SnapshotStateList<PathWrapper> = mutableStateListOf()

/** A stateful list of [Path] that is drawn on the [Canvas] and is scaled for the connected bitmap. */
internal val pathToDrawOnCanvas: List<PathWrapper>? by derivedStateOf {
(state as? DrawBoxConnectionState.Connected)?.let {
drawnPaths.scale(it.size.toFloat())
}
}
private val activeDrawingPath: MutableStateFlow<List<Offset>?> = MutableStateFlow(null)

/** A stateful list of [Path] that was drawn on the [Canvas] but user retracted his action. */
private val canceledPaths: SnapshotStateList<PathWrapper> = mutableStateListOf()
private val canceledPaths: MutableStateFlow<List<PathWrapper>> = MutableStateFlow(emptyList())

/** An [canvasOpacity] of the [Canvas] in the [DrawBox] */
var canvasOpacity: Float by mutableStateOf(1f)
var canvasOpacity: MutableStateFlow<Float> = MutableStateFlow(1f)

/** An [opacity] of the stroke */
var opacity: Float by mutableStateOf(1f)
var opacity: MutableStateFlow<Float> = MutableStateFlow(1f)

/** A [strokeWidth] of the stroke */
var strokeWidth: Float by mutableStateOf(10f)
var strokeWidth: MutableStateFlow<Float> = MutableStateFlow(10f)

/** A [color] of the stroke */
var color: Color by mutableStateOf(Color.Red)
var color: MutableStateFlow<Color> = MutableStateFlow(Color.Red)

/** A [background] of the background of DrawBox */
var background: DrawBoxBackground by mutableStateOf(DrawBoxBackground.NoBackground)
var background: MutableStateFlow<DrawBoxBackground> = MutableStateFlow(DrawBoxBackground.NoBackground)

/** Indicate how many redos it is possible to do. */
val undoCount: Int by derivedStateOf { drawnPaths.size }
val undoCount = drawnPaths.mapState { it.size }

/** Indicate how many undos it is possible to do. */
val redoCount: Int by derivedStateOf { canceledPaths.size }
val redoCount = canceledPaths.mapState { it.size }

/** Executes undo the drawn path if possible. */
fun undo() {
if (drawnPaths.isNotEmpty()) {
canceledPaths.add(drawnPaths.pop())
finalizePath()
if (drawnPaths.value.isNotEmpty()) {
val _drawnPaths = drawnPaths.value.toMutableList()
val _canceledPaths = canceledPaths.value.toMutableList()

_canceledPaths.add(_drawnPaths.removeLast())

drawnPaths.value = _drawnPaths
canceledPaths.value = _canceledPaths
}
}

/** Executes redo the drawn path if possible. */
fun redo() {
if (canceledPaths.isNotEmpty()) {
drawnPaths.add(canceledPaths.pop())
finalizePath()
if (canceledPaths.value.isNotEmpty()) {
val _drawnPaths = drawnPaths.value.toMutableList()
val _canceledPaths = canceledPaths.value.toMutableList()

_drawnPaths.add(_canceledPaths.removeLast())

drawnPaths.value = _drawnPaths
canceledPaths.value = _canceledPaths
}
}

/** Clear drawn paths and the bitmap image. */
fun reset() {
drawnPaths.clear()
canceledPaths.clear()
completelyDrawnPaths.clear()
drawnPaths.value = emptyList()
canceledPaths.value = emptyList()
}

/** Call this function when user starts drawing a path. */
internal fun updateLatestPath(newPoint: Offset) {
(state as? DrawBoxConnectionState.Connected)?.let {
drawnPaths.last().points.add(newPoint.div(it.size.toFloat()))
(state.value as? DrawBoxConnectionState.Connected)?.let {
require(activeDrawingPath.value != null)
val list = activeDrawingPath.value!!.toMutableList()
list.add(newPoint.div(it.size.toFloat()))
activeDrawingPath.value = list
}
}

/** When dragging call this function to update the last path. */
internal fun insertNewPath(newPoint: Offset) {
(state as? DrawBoxConnectionState.Connected)?.let {
val pathWrapper = PathWrapper(
(state.value as? DrawBoxConnectionState.Connected)?.let {
require(activeDrawingPath.value == null)
/*val pathWrapper = PathWrapper(
points = mutableStateListOf(newPoint.div(it.size.toFloat())),
strokeColor = color,
alpha = opacity,
strokeWidth = strokeWidth.div(it.size.toFloat()),
)
drawnPaths.add(pathWrapper)
canceledPaths.clear()
strokeColor = color.value,
alpha = opacity.value,
strokeWidth = strokeWidth.value.div(it.size.toFloat()),
)*/
activeDrawingPath.value = listOf(newPoint.div(it.size.toFloat()))
canceledPaths.value = emptyList()
}
}

internal fun finalizePath() {
completelyDrawnPaths.clear()
completelyDrawnPaths.addAll(drawnPaths)
(state.value as? DrawBoxConnectionState.Connected)?.let {
require(activeDrawingPath.value != null)
val _drawnPaths = drawnPaths.value.toMutableList()

val pathWrapper = PathWrapper(
points = activeDrawingPath.value!!,
strokeColor = color.value,
alpha = opacity.value,
strokeWidth = strokeWidth.value.div(it.size.toFloat()),
)
_drawnPaths.add(pathWrapper)

drawnPaths.value = _drawnPaths
activeDrawingPath.value = null
}
}

/** Call this function to connect to the [DrawBox]. */
internal fun connectToDrawBox(size: IntSize) {
if (
size.width > 0 &&
size.height > 0 &&
size.width == size.height //&&
//state is DrawBoxConnectionState.Disconnected
size.width == size.height
) {
state = DrawBoxConnectionState.Connected(size = size.width)
state.value = DrawBoxConnectionState.Connected(size = size.width)
}
}

internal fun onTap(newPoint: Offset) {
insertNewPath(newPoint)
finalizePath()
}

private fun List<PathWrapper>.scale(size: Float): List<PathWrapper> {
return this.map { pw ->
val t = pw.points.map { it.times(size) }
pw.copy(
points = SnapshotStateList<Offset>().also { it.addAll(t) },
points = mutableListOf<Offset>().also { it.addAll(t) },
strokeWidth = pw.strokeWidth * size
)
}
}

fun getBitmap(size: Int, coroutineScope: CoroutineScope, subscription: DrawBoxSubscription): StateFlow<ImageBitmap> {
fun getDrawPath(subscription: DrawBoxSubscription): StateFlow<List<PathWrapper>> {
return when (subscription) {
is DrawBoxSubscription.DynamicUpdate -> getDynamicUpdateDrawnPath()
is DrawBoxSubscription.FinishDrawingUpdate -> drawnPaths
}
}

private fun getDynamicUpdateDrawnPath(): StateFlow<List<PathWrapper>> {
return combineStates(drawnPaths, activeDrawingPath) { a, b ->
val _a = a.toMutableList()
(state.value as? DrawBoxConnectionState.Connected)?.let {
val pathWrapper = PathWrapper(
points = activeDrawingPath.value ?: emptyList(),
strokeColor = color.value,
alpha = opacity.value,
strokeWidth = strokeWidth.value.div(it.size.toFloat()),
)
_a.addNotNull(pathWrapper)
}
_a
}
}

internal fun getPathWrappersForDrawbox(subscription: DrawBoxSubscription): StateFlow<List<PathWrapper>> {
return combineStates(getDrawPath(subscription), state) { paths, st ->
val size = (st as? DrawBoxConnectionState.Connected)?.size ?: 1
paths.scale(size.toFloat())
}
}

fun getBitmap(size: Int, subscription: DrawBoxSubscription): StateFlow<ImageBitmap> {
val initialBitmap = ImageBitmap(size, size, ImageBitmapConfig.Argb8888)
return flow {
val path = getDrawPath(subscription)
return path.mapState {
val bitmap = ImageBitmap(size, size, ImageBitmapConfig.Argb8888)
val canvas = Canvas(bitmap)
val path = when (subscription) {
is DrawBoxSubscription.DynamicUpdate -> drawnPaths
is DrawBoxSubscription.FinishDrawingUpdate -> completelyDrawnPaths
}
path.scale(size.toFloat()).forEach { pw ->
it.scale(size.toFloat()).forEach { pw ->
canvas.drawPath(
createPath(pw.points),
paint = Paint().apply {
Expand All @@ -146,7 +201,7 @@ class DrawController {
}
)
}
emit(bitmap)
}.stateIn(coroutineScope, started = SharingStarted.Eagerly, initialValue = initialBitmap)
bitmap
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color

data class PathWrapper(
var points: SnapshotStateList<Offset>,
var points: List<Offset>,
val strokeWidth: Float = 5f,
val strokeColor: Color,
val alpha: Float = 1f
Expand Down

This file was deleted.

Loading

0 comments on commit f31ab72

Please sign in to comment.