chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.image.toolbox.library)
|
||||
alias(libs.plugins.image.toolbox.feature)
|
||||
alias(libs.plugins.image.toolbox.hilt)
|
||||
alias(libs.plugins.image.toolbox.compose)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.imagetoolbox.feature.gradient_maker"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.feature.compare)
|
||||
implementation(projects.feature.pickColor)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.data
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathMeasure
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.VertexMode
|
||||
import androidx.compose.ui.graphics.Vertices
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.core.graphics.createBitmap
|
||||
import com.t8rin.imagetoolbox.core.data.utils.safeConfig
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientMaker
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientState
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.MeshGradientState
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AndroidGradientMaker @Inject constructor(
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : DispatchersHolder by dispatchersHolder,
|
||||
GradientMaker<Bitmap, ShaderBrush, Size, Color, TileMode, Offset> {
|
||||
|
||||
override suspend fun createGradient(
|
||||
integerSize: IntegerSize,
|
||||
gradientState: GradientState<ShaderBrush, Size, Color, TileMode, Offset>
|
||||
): Bitmap? = createGradient(
|
||||
src = integerSize.toSize().run {
|
||||
createBitmap(
|
||||
width = width.toInt(),
|
||||
height = height.toInt()
|
||||
)
|
||||
},
|
||||
gradientState = gradientState,
|
||||
gradientAlpha = 1f
|
||||
)
|
||||
|
||||
override suspend fun createGradient(
|
||||
src: Bitmap,
|
||||
gradientState: GradientState<ShaderBrush, Size, Color, TileMode, Offset>,
|
||||
gradientAlpha: Float
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
val size = IntegerSize(
|
||||
src.width,
|
||||
src.height
|
||||
).toSize()
|
||||
gradientState.getBrush(size)?.let { brush ->
|
||||
src.copy(src.safeConfig, true).apply {
|
||||
setHasAlpha(true)
|
||||
|
||||
Canvas(asImageBitmap()).apply {
|
||||
drawImage(asImageBitmap(), Offset.Zero, Paint())
|
||||
drawRect(
|
||||
paint = Paint().apply {
|
||||
shader = brush.createShader(size)
|
||||
alpha = gradientAlpha
|
||||
},
|
||||
rect = Rect(offset = Offset.Zero, size = size)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createMeshGradient(
|
||||
integerSize: IntegerSize,
|
||||
gradientState: MeshGradientState<Color, Offset>
|
||||
): Bitmap? = createMeshGradient(
|
||||
src = integerSize.toSize().run {
|
||||
createBitmap(
|
||||
width = width.toInt(),
|
||||
height = height.toInt()
|
||||
)
|
||||
},
|
||||
gradientState = gradientState,
|
||||
gradientAlpha = 1f
|
||||
)
|
||||
|
||||
override suspend fun createMeshGradient(
|
||||
src: Bitmap,
|
||||
gradientState: MeshGradientState<Color, Offset>,
|
||||
gradientAlpha: Float
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
src.copy(src.safeConfig, true).apply {
|
||||
setHasAlpha(true)
|
||||
|
||||
val paint = Paint().apply {
|
||||
alpha = gradientAlpha
|
||||
}
|
||||
|
||||
Canvas(asImageBitmap()).apply {
|
||||
drawImage(asImageBitmap(), Offset.Zero, Paint())
|
||||
drawMeshGradient(
|
||||
pointData = PointData(
|
||||
points = gradientState.points,
|
||||
stepsX = gradientState.resolutionX,
|
||||
stepsY = gradientState.resolutionY
|
||||
),
|
||||
size = Size(width.toFloat(), height.toFloat()),
|
||||
paint = paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun IntegerSize.toSize(): Size = Size(
|
||||
width.coerceAtLeast(1).toFloat(),
|
||||
height.coerceAtLeast(1).toFloat(),
|
||||
)
|
||||
|
||||
|
||||
private fun Canvas.drawMeshGradient(
|
||||
pointData: PointData,
|
||||
indicesModifier: (List<Int>) -> List<Int> = { it },
|
||||
size: Size,
|
||||
paint: Paint
|
||||
) {
|
||||
CanvasDrawScope().apply {
|
||||
drawContext.canvas = this@drawMeshGradient
|
||||
drawContext.size = size
|
||||
|
||||
with(drawContext.canvas) {
|
||||
scale(
|
||||
scaleX = size.width,
|
||||
scaleY = size.height,
|
||||
pivot = Offset.Zero
|
||||
) {
|
||||
drawVertices(
|
||||
vertices = Vertices(
|
||||
vertexMode = VertexMode.Triangles,
|
||||
positions = pointData.offsets,
|
||||
textureCoordinates = pointData.offsets,
|
||||
colors = pointData.colors,
|
||||
indices = indicesModifier(pointData.indices)
|
||||
),
|
||||
blendMode = BlendMode.Dst,
|
||||
paint = paint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class PointData(
|
||||
private val points: List<List<Pair<Offset, Color>>>,
|
||||
private val stepsX: Int,
|
||||
private val stepsY: Int
|
||||
) {
|
||||
val offsets: MutableList<Offset>
|
||||
val colors: MutableList<Color>
|
||||
val indices: List<Int>
|
||||
private val xLength: Int = (points[0].size * stepsX) - (stepsX - 1)
|
||||
private val yLength: Int = (points.size * stepsY) - (stepsY - 1)
|
||||
private val measure = PathMeasure()
|
||||
|
||||
private val indicesBlocks: List<IndicesBlock>
|
||||
|
||||
init {
|
||||
offsets = buildList {
|
||||
repeat((xLength - 0) * (yLength - 0)) {
|
||||
add(Offset(0f, 0f))
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
colors = buildList {
|
||||
repeat((xLength - 0) * (yLength - 0)) {
|
||||
add(Color.Transparent)
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
indicesBlocks =
|
||||
buildList {
|
||||
for (y in 0..yLength - 2) {
|
||||
for (x in 0..xLength - 2) {
|
||||
|
||||
val a = (y * xLength) + x
|
||||
val b = a + 1
|
||||
val c = ((y + 1) * xLength) + x
|
||||
val d = c + 1
|
||||
|
||||
add(
|
||||
IndicesBlock(
|
||||
indices = buildList {
|
||||
add(a)
|
||||
add(c)
|
||||
add(d)
|
||||
|
||||
add(a)
|
||||
add(b)
|
||||
add(d)
|
||||
},
|
||||
x = x, y = y
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indices = indicesBlocks.flatMap { it.indices }
|
||||
generateInterpolatedOffsets()
|
||||
}
|
||||
|
||||
private fun generateInterpolatedOffsets() {
|
||||
for (y in 0..points.lastIndex) {
|
||||
for (x in 0..points[y].lastIndex) {
|
||||
this[x * stepsX, y * stepsY] = points[y][x].first
|
||||
this[x * stepsX, y * stepsY] = points[y][x].second
|
||||
|
||||
if (x != points[y].lastIndex) {
|
||||
val path = cubicPathX(
|
||||
point1 = points[y][x].first,
|
||||
point2 = points[y][x + 1].first,
|
||||
when (x) {
|
||||
0 -> 0
|
||||
points[y].lastIndex - 1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
)
|
||||
measure.setPath(path, false)
|
||||
|
||||
for (i in 1..<stepsX) {
|
||||
measure.getPosition(i / stepsX.toFloat() * measure.length).let {
|
||||
this[(x * stepsX) + i, (y * stepsY)] = Offset(it.x, it.y)
|
||||
this[(x * stepsX) + i, (y * stepsY)] =
|
||||
lerp(
|
||||
points[y][x].second,
|
||||
points[y][x + 1].second,
|
||||
i / stepsX.toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (y in 0..<points.lastIndex) {
|
||||
for (x in 0..<this.xLength) {
|
||||
val path = cubicPathY(
|
||||
point1 = this[x, y * stepsY].let { Offset(it.x, it.y) },
|
||||
point2 = this[x, (y + 1) * stepsY].let { Offset(it.x, it.y) },
|
||||
when (y) {
|
||||
0 -> 0
|
||||
points[y].lastIndex - 1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
)
|
||||
measure.setPath(path, false)
|
||||
for (i in (1..<stepsY)) {
|
||||
val point3 = measure.getPosition(i / stepsY.toFloat() * measure.length).let {
|
||||
Offset(it.x, it.y)
|
||||
}
|
||||
|
||||
this[x, ((y * stepsY) + i)] = point3
|
||||
|
||||
this[x, ((y * stepsY) + i)] = lerp(
|
||||
this.getColor(x, y * stepsY),
|
||||
this.getColor(x, (y + 1) * stepsY),
|
||||
i / stepsY.toFloat(),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class IndicesBlock(
|
||||
val indices: List<Int>,
|
||||
val x: Int,
|
||||
val y: Int
|
||||
)
|
||||
|
||||
operator fun get(
|
||||
x: Int,
|
||||
y: Int
|
||||
): Offset {
|
||||
val index = (y * xLength) + x
|
||||
return offsets[index]
|
||||
}
|
||||
|
||||
private fun getColor(
|
||||
x: Int,
|
||||
y: Int
|
||||
): Color {
|
||||
val index = (y * xLength) + x
|
||||
return colors[index]
|
||||
}
|
||||
|
||||
private operator fun set(
|
||||
x: Int,
|
||||
y: Int,
|
||||
offset: Offset
|
||||
) {
|
||||
val index = (y * xLength) + x
|
||||
offsets[index] = Offset(offset.x, offset.y)
|
||||
}
|
||||
|
||||
private operator fun set(
|
||||
x: Int,
|
||||
y: Int,
|
||||
color: Color
|
||||
) {
|
||||
val index = (y * xLength) + x
|
||||
colors[index] = color
|
||||
}
|
||||
}
|
||||
|
||||
private fun cubicPathX(
|
||||
point1: Offset,
|
||||
point2: Offset,
|
||||
position: Int
|
||||
): Path {
|
||||
val path = Path().apply {
|
||||
moveTo(point1.x, point1.y)
|
||||
val delta = (point2.x - point1.x) * .5f
|
||||
when (position) {
|
||||
0 -> cubicTo(
|
||||
point1.x, point1.y,
|
||||
point2.x - delta, point2.y,
|
||||
point2.x, point2.y
|
||||
)
|
||||
|
||||
2 -> cubicTo(
|
||||
point1.x + delta, point1.y,
|
||||
point2.x, point2.y,
|
||||
point2.x, point2.y
|
||||
)
|
||||
|
||||
else -> cubicTo(
|
||||
point1.x + delta, point1.y,
|
||||
point2.x - delta, point2.y,
|
||||
point2.x, point2.y
|
||||
)
|
||||
}
|
||||
|
||||
lineTo(point2.x, point2.y)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private fun cubicPathY(
|
||||
point1: Offset,
|
||||
point2: Offset,
|
||||
position: Int
|
||||
): Path {
|
||||
val path = Path().apply {
|
||||
moveTo(point1.x, point1.y)
|
||||
val delta = (point2.y - point1.y) * .5f
|
||||
when (position) {
|
||||
0 -> cubicTo(
|
||||
point1.x, point1.y,
|
||||
point2.x, point2.y - delta,
|
||||
point2.x, point2.y
|
||||
)
|
||||
|
||||
2 -> cubicTo(
|
||||
point1.x, point1.y + delta,
|
||||
point2.x, point2.y,
|
||||
point2.x, point2.y
|
||||
)
|
||||
|
||||
else -> cubicTo(
|
||||
point1.x, point1.y + delta,
|
||||
point2.x, point2.y - delta,
|
||||
point2.x, point2.y
|
||||
)
|
||||
}
|
||||
|
||||
lineTo(point2.x, point2.y)
|
||||
}
|
||||
return path
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.di
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.data.AndroidGradientMaker
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientMaker
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface GradientMakerModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideGradientMaker(
|
||||
maker: AndroidGradientMaker
|
||||
): GradientMaker<Bitmap, ShaderBrush, Size, Color, TileMode, Offset>
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.domain
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
|
||||
interface GradientMaker<Image, Brush, Size, Color, TileMode, Offset> {
|
||||
|
||||
suspend fun createGradient(
|
||||
integerSize: IntegerSize,
|
||||
gradientState: GradientState<Brush, Size, Color, TileMode, Offset>
|
||||
): Image?
|
||||
|
||||
suspend fun createGradient(
|
||||
src: Image,
|
||||
gradientState: GradientState<Brush, Size, Color, TileMode, Offset>,
|
||||
gradientAlpha: Float
|
||||
): Image?
|
||||
|
||||
suspend fun createMeshGradient(
|
||||
integerSize: IntegerSize,
|
||||
gradientState: MeshGradientState<Color, Offset>,
|
||||
): Image?
|
||||
|
||||
suspend fun createMeshGradient(
|
||||
src: Image,
|
||||
gradientState: MeshGradientState<Color, Offset>,
|
||||
gradientAlpha: Float
|
||||
): Image?
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.domain
|
||||
|
||||
interface GradientState<Brush, Size, Color, TileMode, Offset> {
|
||||
var size: Size
|
||||
|
||||
val brush: Brush?
|
||||
get() = getBrush(size)
|
||||
|
||||
fun getBrush(size: Size): Brush?
|
||||
|
||||
var gradientType: GradientType
|
||||
|
||||
val colorStops: List<Pair<Float, Color>>
|
||||
|
||||
var tileMode: TileMode
|
||||
|
||||
var linearGradientAngle: Float
|
||||
|
||||
var centerFriction: Offset
|
||||
|
||||
var radiusFriction: Float
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.domain
|
||||
|
||||
enum class GradientType {
|
||||
Linear, Radial, Sweep
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.domain
|
||||
|
||||
interface MeshGradientState<Color, Offset> {
|
||||
val points: List<List<Pair<Offset, Color>>>
|
||||
var resolutionX: Int
|
||||
var resolutionY: Int
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberImagePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.Clipboard
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.AdaptiveLayoutScreen
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.ShareButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.ShowOriginalButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.ExitWithoutSavingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.LoadingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.TopAppBarEmoji
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.sheets.ProcessImagesPreferenceSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.TopAppBarTitle
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.GradientMakerAppColorSchemeHandler
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.GradientMakerBottomButtons
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.GradientMakerCompareButton
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.GradientMakerControls
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.GradientMakerImagePreview
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.GradientMakerNoDataControls
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.GradientMakerType
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
fun GradientMakerContent(
|
||||
component: GradientMakerComponent
|
||||
) {
|
||||
val screenType = component.screenType
|
||||
|
||||
GradientMakerAppColorSchemeHandler(component)
|
||||
|
||||
LaunchedEffect(screenType) {
|
||||
if (screenType == null) {
|
||||
component.resetState()
|
||||
}
|
||||
}
|
||||
val goBack = {
|
||||
if (screenType != null) {
|
||||
component.resetState()
|
||||
} else {
|
||||
component.onGoBack()
|
||||
}
|
||||
}
|
||||
|
||||
var showExitDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val imagePicker = rememberImagePicker { uris: List<Uri> ->
|
||||
component.setUris(uris)
|
||||
component.updateGradientAlpha(0.5f)
|
||||
}
|
||||
|
||||
AdaptiveLayoutScreen(
|
||||
shouldDisableBackHandler = screenType == null,
|
||||
canShowScreenData = screenType != null,
|
||||
title = {
|
||||
TopAppBarTitle(
|
||||
title = when (screenType) {
|
||||
null, GradientMakerType.Default -> stringResource(R.string.gradient_maker)
|
||||
GradientMakerType.Overlay -> stringResource(R.string.gradient_maker_type_image)
|
||||
GradientMakerType.Mesh -> stringResource(R.string.mesh_gradients)
|
||||
GradientMakerType.MeshOverlay -> stringResource(R.string.gradient_maker_type_image_mesh)
|
||||
},
|
||||
input = Unit,
|
||||
isLoading = false,
|
||||
size = null
|
||||
)
|
||||
},
|
||||
onGoBack = {
|
||||
if (component.haveChanges) showExitDialog = true
|
||||
else goBack()
|
||||
},
|
||||
actions = {
|
||||
if (component.uris.isNotEmpty()) {
|
||||
ShowOriginalButton(
|
||||
onStateChange = component::setShowOriginal
|
||||
)
|
||||
}
|
||||
var editSheetData by remember {
|
||||
mutableStateOf(listOf<Uri>())
|
||||
}
|
||||
ShareButton(
|
||||
enabled = component.brush != null,
|
||||
onShare = component::shareBitmaps,
|
||||
onCopy = {
|
||||
component.cacheCurrentImage(Clipboard::copy)
|
||||
},
|
||||
onEdit = {
|
||||
component.cacheImages {
|
||||
editSheetData = it
|
||||
}
|
||||
}
|
||||
)
|
||||
ProcessImagesPreferenceSheet(
|
||||
uris = editSheetData,
|
||||
visible = editSheetData.isNotEmpty(),
|
||||
onDismiss = {
|
||||
editSheetData = emptyList()
|
||||
},
|
||||
onNavigate = component.onNavigate
|
||||
)
|
||||
},
|
||||
topAppBarPersistentActions = {
|
||||
if (screenType == null) {
|
||||
TopAppBarEmoji()
|
||||
}
|
||||
|
||||
GradientMakerCompareButton(component)
|
||||
},
|
||||
imagePreview = {
|
||||
GradientMakerImagePreview(component)
|
||||
},
|
||||
controls = {
|
||||
GradientMakerControls(component)
|
||||
},
|
||||
insetsForNoData = WindowInsets(0),
|
||||
noDataControls = {
|
||||
GradientMakerNoDataControls(component)
|
||||
},
|
||||
buttons = { actions ->
|
||||
GradientMakerBottomButtons(
|
||||
component = component,
|
||||
actions = actions,
|
||||
imagePicker = imagePicker
|
||||
)
|
||||
},
|
||||
forceImagePreviewToMax = component.showOriginal,
|
||||
contentPadding = animateDpAsState(
|
||||
if (screenType == null) 12.dp
|
||||
else 20.dp
|
||||
).value
|
||||
)
|
||||
|
||||
LoadingDialog(
|
||||
visible = component.isSaving || component.isImageLoading,
|
||||
done = component.done,
|
||||
left = component.left,
|
||||
onCancelLoading = component::cancelSaving,
|
||||
canCancel = component.isSaving,
|
||||
)
|
||||
|
||||
ExitWithoutSavingDialog(
|
||||
onExit = goBack,
|
||||
onDismiss = { showExitDialog = false },
|
||||
visible = showExitDialog
|
||||
)
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Delete
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.MiniEdit
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Palette
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.mixedContainer
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.color_picker.ColorInfo
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.color_picker.ColorPickerSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedSlider
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.hapticsClickable
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.ExpandableItem
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.RevealDirection
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.RevealValue
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.SwipeToReveal
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.rememberRevealState
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.saver.ColorSaver
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.TitleItem
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.value.ValueDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.value.ValueText
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ColorStopSelection(
|
||||
colorStops: List<Pair<Float, Color>>,
|
||||
onRemoveClick: (Int) -> Unit,
|
||||
onValueChange: (Int, Pair<Float, Color>) -> Unit,
|
||||
onAddColorStop: (Pair<Float, Color>) -> Unit,
|
||||
colorPickerBitmap: Bitmap?
|
||||
) {
|
||||
var showColorPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
ExpandableItem(
|
||||
initialState = true,
|
||||
modifier = Modifier.padding(1.dp),
|
||||
shape = ShapeDefaults.extraLarge,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
visibleContent = {
|
||||
TitleItem(text = stringResource(R.string.color_stops))
|
||||
},
|
||||
expandableContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
colorStops.forEachIndexed { index, (value, color) ->
|
||||
ColorStopSelectionItem(
|
||||
value = value,
|
||||
color = color,
|
||||
onRemoveClick = {
|
||||
onRemoveClick(index)
|
||||
},
|
||||
onValueChange = {
|
||||
onValueChange(index, it)
|
||||
},
|
||||
canDelete = colorStops.size > 2,
|
||||
colorPickerBitmap = colorPickerBitmap
|
||||
)
|
||||
}
|
||||
EnhancedButton(
|
||||
containerColor = MaterialTheme.colorScheme.mixedContainer,
|
||||
onClick = {
|
||||
showColorPicker = true
|
||||
},
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Palette,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(id = R.string.add_color))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
var color by rememberSaveable(stateSaver = ColorSaver) {
|
||||
mutableStateOf(Color.Red)
|
||||
}
|
||||
ColorPickerSheet(
|
||||
visible = showColorPicker,
|
||||
onDismiss = { showColorPicker = false },
|
||||
color = color,
|
||||
onColorSelected = {
|
||||
color = it
|
||||
onAddColorStop(1f to it)
|
||||
},
|
||||
allowAlpha = true,
|
||||
additionalContent = { onColorChange ->
|
||||
GradientMakerColorPickerAdditionalContent(
|
||||
bitmap = colorPickerBitmap,
|
||||
onColorChange = onColorChange
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorStopSelectionItem(
|
||||
value: Float,
|
||||
color: Color,
|
||||
onRemoveClick: () -> Unit,
|
||||
onValueChange: (Pair<Float, Color>) -> Unit,
|
||||
canDelete: Boolean,
|
||||
colorPickerBitmap: Bitmap?
|
||||
) {
|
||||
var showColorPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberRevealState()
|
||||
SwipeToReveal(
|
||||
state = state,
|
||||
enableSwipe = canDelete,
|
||||
revealedContentEnd = {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.container(
|
||||
color = MaterialTheme.colorScheme.errorContainer,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
autoShadowElevation = 0.dp,
|
||||
resultPadding = 0.dp
|
||||
)
|
||||
.hapticsClickable {
|
||||
scope.launch {
|
||||
state.animateTo(RevealValue.Default)
|
||||
}
|
||||
onRemoveClick()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.padding(end = 8.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
tint = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
},
|
||||
directions = setOf(RevealDirection.EndToStart),
|
||||
swipeableContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.container(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
resultPadding = 8.dp
|
||||
)
|
||||
.then(
|
||||
if (canDelete) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val time = System.currentTimeMillis()
|
||||
awaitRelease()
|
||||
if (System.currentTimeMillis() - time >= 200) {
|
||||
scope.launch {
|
||||
state.animateTo(RevealValue.FullyRevealedStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
ColorInfo(
|
||||
color = color,
|
||||
onColorChange = {
|
||||
onValueChange(value to it)
|
||||
},
|
||||
supportButtonIcon = Icons.Rounded.MiniEdit,
|
||||
onSupportButtonClick = {
|
||||
showColorPicker = true
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
EnhancedSlider(
|
||||
value = value,
|
||||
onValueChange = { onValueChange(it to color) },
|
||||
valueRange = 0f..1f,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
var showValueDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val initialValue = rememberSaveable { value }
|
||||
ValueText(
|
||||
modifier = Modifier,
|
||||
value = (value * 100).toInt(),
|
||||
onClick = {
|
||||
showValueDialog = true
|
||||
},
|
||||
onLongClick = {
|
||||
onValueChange(initialValue to color)
|
||||
}
|
||||
)
|
||||
ValueDialog(
|
||||
roundTo = 0,
|
||||
valueRange = 0f..100f,
|
||||
valueState = (value * 100).toInt().toString(),
|
||||
expanded = showValueDialog,
|
||||
onDismiss = {
|
||||
showValueDialog = false
|
||||
},
|
||||
onValueUpdate = {
|
||||
onValueChange(it.roundToInt() / 100f to color)
|
||||
showValueDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ColorPickerSheet(
|
||||
visible = showColorPicker,
|
||||
onDismiss = { showColorPicker = false },
|
||||
color = color,
|
||||
onColorSelected = {
|
||||
onValueChange(value to it)
|
||||
},
|
||||
allowAlpha = true,
|
||||
additionalContent = { onColorChange ->
|
||||
GradientMakerColorPickerAdditionalContent(
|
||||
bitmap = colorPickerBitmap,
|
||||
onColorChange = onColorChange
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.blend
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.utils.AutoContentBasedColors
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerAppColorSchemeHandler(component: GradientMakerComponent) {
|
||||
AutoContentBasedColors(
|
||||
model = Triple(component.brush, component.meshPoints, component.selectedUri),
|
||||
selector = { (_, uri) ->
|
||||
component.createGradientBitmap(
|
||||
data = uri,
|
||||
integerSize = IntegerSize(1000, 1000)
|
||||
)
|
||||
},
|
||||
allowChangeColor = component.screenType != null
|
||||
)
|
||||
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
LaunchedEffect(component.colorStops) {
|
||||
if (component.colorStops.isEmpty()) {
|
||||
colorScheme.apply {
|
||||
component.addColorStop(
|
||||
pair = 0f to primary.blend(primaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
component.addColorStop(
|
||||
pair = 0.5f to secondary.blend(secondaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
component.addColorStop(
|
||||
pair = 1f to tertiary.blend(tertiaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.ImagePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.Picker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.BottomButtonsBlock
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.OneTimeImagePickingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.canPickImage
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerBottomButtons(
|
||||
component: GradientMakerComponent,
|
||||
actions: @Composable RowScope.() -> Unit,
|
||||
imagePicker: ImagePicker
|
||||
) {
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
val saveBitmap: (oneTimeSaveLocationUri: String?) -> Unit = {
|
||||
if (component.brush != null) {
|
||||
component.saveBitmaps(
|
||||
oneTimeSaveLocationUri = it
|
||||
)
|
||||
}
|
||||
}
|
||||
var showFolderSelectionDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showOneTimeImagePickingDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
BottomButtonsBlock(
|
||||
isNoData = component.screenType == null,
|
||||
onSecondaryButtonClick = imagePicker::pickImage,
|
||||
isSecondaryButtonVisible = component.screenType.canPickImage(),
|
||||
isPrimaryButtonVisible = component.brush != null,
|
||||
showNullDataButtonAsContainer = true,
|
||||
onPrimaryButtonClick = {
|
||||
saveBitmap(null)
|
||||
},
|
||||
onPrimaryButtonLongClick = {
|
||||
showFolderSelectionDialog = true
|
||||
},
|
||||
actions = {
|
||||
if (isPortrait) actions()
|
||||
},
|
||||
onSecondaryButtonLongClick = {
|
||||
showOneTimeImagePickingDialog = true
|
||||
}
|
||||
)
|
||||
OneTimeSaveLocationSelectionDialog(
|
||||
visible = showFolderSelectionDialog,
|
||||
onDismiss = { showFolderSelectionDialog = false },
|
||||
onSaveRequest = saveBitmap,
|
||||
formatForFilenameSelection = component.getFormatForFilenameSelection()
|
||||
)
|
||||
OneTimeImagePickingDialog(
|
||||
onDismiss = { showOneTimeImagePickingDialog = false },
|
||||
picker = Picker.Multiple,
|
||||
imagePicker = imagePicker,
|
||||
visible = showOneTimeImagePickingDialog
|
||||
)
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Eyedropper
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.mixedContainer
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.onMixedContainer
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.hapticsClickable
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.saver.ColorSaver
|
||||
import com.t8rin.imagetoolbox.feature.pick_color.presentation.components.PickColorFromImageSheet
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerColorPickerAdditionalContent(
|
||||
bitmap: Bitmap?,
|
||||
onColorChange: (Color) -> Unit
|
||||
) {
|
||||
bitmap ?: return
|
||||
|
||||
var showPickColorSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var pipetteColor by rememberSaveable(stateSaver = ColorSaver) {
|
||||
mutableStateOf(Color.Black)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.container(
|
||||
resultPadding = 0.dp,
|
||||
color = MaterialTheme.colorScheme.mixedContainer.copy(0.7f),
|
||||
shape = ShapeDefaults.extraLarge
|
||||
)
|
||||
.hapticsClickable {
|
||||
showPickColorSheet = true
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.pipette),
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.onMixedContainer
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Eyedropper,
|
||||
contentDescription = stringResource(R.string.pipette),
|
||||
tint = MaterialTheme.colorScheme.onMixedContainer
|
||||
)
|
||||
}
|
||||
|
||||
PickColorFromImageSheet(
|
||||
visible = showPickColorSheet,
|
||||
onDismiss = {
|
||||
showPickColorSheet = false
|
||||
},
|
||||
bitmap = bitmap,
|
||||
onColorChange = {
|
||||
pipetteColor = it
|
||||
onColorChange(it)
|
||||
},
|
||||
color = pipetteColor
|
||||
)
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.CompareButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.Picture
|
||||
import com.t8rin.imagetoolbox.feature.compare.presentation.components.CompareSheet
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.canPickImage
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.isMesh
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerCompareButton(component: GradientMakerComponent) {
|
||||
var showCompareSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val screenType = component.screenType
|
||||
|
||||
CompareButton(
|
||||
onClick = { showCompareSheet = true },
|
||||
visible = component.brush != null && screenType.canPickImage() && component.selectedUri != Uri.EMPTY
|
||||
)
|
||||
|
||||
CompareSheet(
|
||||
beforeContent = {
|
||||
Picture(
|
||||
model = component.selectedUri,
|
||||
modifier = Modifier.aspectRatio(
|
||||
component.imageAspectRatio
|
||||
)
|
||||
)
|
||||
},
|
||||
afterContent = {
|
||||
if (screenType.isMesh()) {
|
||||
MeshGradientPreview(
|
||||
meshGradientState = component.meshGradientState,
|
||||
gradientAlpha = component.gradientAlpha,
|
||||
allowPickingImage = screenType.canPickImage(),
|
||||
gradientSize = component.gradientSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio,
|
||||
shape = RectangleShape
|
||||
)
|
||||
} else {
|
||||
val gradientState = rememberGradientState()
|
||||
LaunchedEffect(component.brush) {
|
||||
gradientState.gradientType = component.gradientType
|
||||
gradientState.linearGradientAngle = component.angle
|
||||
gradientState.centerFriction = component.centerFriction
|
||||
gradientState.radiusFriction = component.radiusFriction
|
||||
gradientState.colorStops.apply {
|
||||
clear()
|
||||
addAll(component.colorStops)
|
||||
}
|
||||
gradientState.tileMode = component.tileMode
|
||||
}
|
||||
GradientPreview(
|
||||
brush = gradientState.brush,
|
||||
gradientAlpha = component.gradientAlpha,
|
||||
allowPickingImage = screenType.canPickImage(),
|
||||
gradientSize = component.gradientSize,
|
||||
onSizeChanged = {
|
||||
gradientState.size = it
|
||||
},
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio,
|
||||
shape = RectangleShape
|
||||
)
|
||||
}
|
||||
},
|
||||
visible = showCompareSheet,
|
||||
onDismiss = {
|
||||
showCompareSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Build
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.DensitySmall
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.GridOn
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.SaveExifWidget
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.AlphaSelector
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.ImageFormatSelector
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedSliderItem
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.ImageCounter
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.sheets.PickImageFromUrisSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.TitleItem
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.canPickImage
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.isMesh
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerControls(component: GradientMakerComponent) {
|
||||
var showPickImageFromUrisSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val screenType = component.screenType
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ImageCounter(
|
||||
imageCount = component.uris.size.takeIf { it > 1 },
|
||||
onRepick = {
|
||||
showPickImageFromUrisSheet = true
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
screenType != null && !screenType.canPickImage()
|
||||
) { canChangeSize ->
|
||||
if (canChangeSize) {
|
||||
GradientSizeSelector(
|
||||
value = component.gradientSize,
|
||||
onWidthChange = component::updateWidth,
|
||||
onHeightChange = component::updateHeight
|
||||
)
|
||||
} else {
|
||||
AlphaSelector(
|
||||
value = component.gradientAlpha,
|
||||
onValueChange = component::updateGradientAlpha,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (screenType.isMesh()) {
|
||||
Column(
|
||||
modifier = Modifier.container(
|
||||
resultPadding = 0.dp
|
||||
)
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
TitleItem(
|
||||
text = stringResource(R.string.points_customization),
|
||||
icon = Icons.Rounded.Build,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp
|
||||
)
|
||||
)
|
||||
MeshGradientEditor(
|
||||
state = component.meshGradientState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.padding(16.dp),
|
||||
colorPickerBitmap = component.colorPickerBitmap
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnhancedSliderItem(
|
||||
value = component.meshGradientState.gridSize,
|
||||
title = stringResource(R.string.grid_size),
|
||||
icon = Icons.Outlined.GridOn,
|
||||
valueRange = 2f..6f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = { value ->
|
||||
if (value.roundToInt() != component.meshGradientState.gridSize) {
|
||||
val size = value.roundToInt()
|
||||
component.setResolution(lerp(1f, 16f, 2f / size))
|
||||
component.meshGradientState.points.apply {
|
||||
clear()
|
||||
addAll(generateMesh(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnhancedSliderItem(
|
||||
value = component.meshResolutionX,
|
||||
title = stringResource(R.string.resolution),
|
||||
icon = Icons.Rounded.DensitySmall,
|
||||
valueRange = 1f..64f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = component::setResolution
|
||||
)
|
||||
} else {
|
||||
GradientTypeSelector(
|
||||
value = component.gradientType,
|
||||
onValueChange = component::setGradientType
|
||||
) {
|
||||
GradientPropertiesSelector(
|
||||
gradientType = component.gradientType,
|
||||
linearAngle = component.angle,
|
||||
onLinearAngleChange = component::updateLinearAngle,
|
||||
centerFriction = component.centerFriction,
|
||||
radiusFriction = component.radiusFriction,
|
||||
onRadialDimensionsChange = component::setRadialProperties
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ColorStopSelection(
|
||||
colorStops = component.colorStops,
|
||||
onRemoveClick = component::removeColorStop,
|
||||
onValueChange = component::updateColorStop,
|
||||
onAddColorStop = component::addColorStop,
|
||||
colorPickerBitmap = component.colorPickerBitmap
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TileModeSelector(
|
||||
value = component.tileMode,
|
||||
onValueChange = component::setTileMode
|
||||
)
|
||||
}
|
||||
if (screenType.canPickImage()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SaveExifWidget(
|
||||
checked = component.keepExif,
|
||||
imageFormat = component.imageFormat,
|
||||
onCheckedChange = component::toggleKeepExif
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ImageFormatSelector(
|
||||
value = component.imageFormat,
|
||||
forceEnabled = screenType != null && !screenType.canPickImage(),
|
||||
onValueChange = component::setImageFormat
|
||||
)
|
||||
}
|
||||
|
||||
val transformations by remember(
|
||||
component.brush,
|
||||
screenType.isMesh(),
|
||||
component.meshPoints,
|
||||
component.meshResolutionX,
|
||||
component.meshResolutionY,
|
||||
component.gradientAlpha
|
||||
) {
|
||||
derivedStateOf {
|
||||
listOf(
|
||||
component.getGradientTransformation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
PickImageFromUrisSheet(
|
||||
transformations = transformations,
|
||||
visible = showPickImageFromUrisSheet,
|
||||
onDismiss = {
|
||||
showPickImageFromUrisSheet = false
|
||||
},
|
||||
uris = component.uris,
|
||||
selectedUri = component.selectedUri,
|
||||
onUriPicked = component::updateSelectedUri,
|
||||
onUriRemoved = component::updateUrisSilently,
|
||||
columns = if (isPortrait) 2 else 4,
|
||||
)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.detectSwipes
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.canPickImage
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.isMesh
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerImagePreview(component: GradientMakerComponent) {
|
||||
val screenType = component.screenType
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.detectSwipes(
|
||||
onSwipeRight = component::selectLeftUri,
|
||||
onSwipeLeft = component::selectRightUri
|
||||
)
|
||||
.container()
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (screenType.isMesh()) {
|
||||
MeshGradientPreview(
|
||||
meshGradientState = component.meshGradientState,
|
||||
gradientAlpha = if (component.showOriginal) 0f else component.gradientAlpha,
|
||||
allowPickingImage = screenType.canPickImage(),
|
||||
gradientSize = component.gradientSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
} else {
|
||||
GradientPreview(
|
||||
brush = component.brush,
|
||||
gradientAlpha = if (component.showOriginal) 0f else component.gradientAlpha,
|
||||
allowPickingImage = screenType.canPickImage(),
|
||||
gradientSize = component.gradientSize,
|
||||
onSizeChanged = component::setPreviewSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.ImageOverlay
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.ImagesMode
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.MeshDownload
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.MeshGradient
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberImagePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.navigation.Screen
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.withModifier
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.preferences.PreferenceItem
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.GradientMakerType
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerNoDataControls(
|
||||
component: GradientMakerComponent
|
||||
) {
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
var requestedType by rememberSaveable(component.screenType) {
|
||||
mutableStateOf<GradientMakerType?>(null)
|
||||
}
|
||||
|
||||
val imagePicker = rememberImagePicker { uris: List<Uri> ->
|
||||
component.setScreenType(requestedType)
|
||||
component.setUris(uris)
|
||||
component.updateGradientAlpha(0.5f)
|
||||
}
|
||||
|
||||
val preference1 = @Composable {
|
||||
val screen = remember {
|
||||
Screen.GradientMaker()
|
||||
}
|
||||
PreferenceItem(
|
||||
title = stringResource(screen.title),
|
||||
subtitle = stringResource(screen.subtitle),
|
||||
startIcon = screen.icon,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setScreenType(GradientMakerType.Default)
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference2 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.gradient_maker_type_image),
|
||||
subtitle = stringResource(R.string.gradient_maker_type_image_sub),
|
||||
startIcon = Icons.Outlined.ImagesMode,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
requestedType = GradientMakerType.Overlay
|
||||
imagePicker.pickImage()
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference3 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.mesh_gradients),
|
||||
subtitle = stringResource(R.string.mesh_gradients_sub),
|
||||
startIcon = Icons.Outlined.MeshGradient,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setScreenType(GradientMakerType.Mesh)
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference4 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.gradient_maker_type_image_mesh),
|
||||
subtitle = stringResource(R.string.gradient_maker_type_image_mesh_sub),
|
||||
startIcon = Icons.Outlined.ImageOverlay,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
requestedType = GradientMakerType.MeshOverlay
|
||||
imagePicker.pickImage()
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference5 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.collection_mesh_gradients),
|
||||
subtitle = stringResource(R.string.collection_mesh_gradients_sub),
|
||||
startIcon = Icons.Outlined.MeshDownload,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.onNavigate(Screen.MeshGradients)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isPortrait) {
|
||||
Column {
|
||||
preference1()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference2()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference3()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference4()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference5()
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)
|
||||
)
|
||||
) {
|
||||
preference1.withModifier(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
preference2.withModifier(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)
|
||||
)
|
||||
) {
|
||||
preference3.withModifier(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
preference4.withModifier(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference5.withModifier(modifier = Modifier.fillMaxWidth(0.5f))
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.t8rin.colors.util.roundToTwoDigits
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.Picture
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.shimmer
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.transparencyChecker
|
||||
|
||||
@Composable
|
||||
internal fun GradientPreview(
|
||||
brush: ShaderBrush?,
|
||||
gradientAlpha: Float,
|
||||
allowPickingImage: Boolean?,
|
||||
gradientSize: IntegerSize,
|
||||
onSizeChanged: (Size) -> Unit,
|
||||
imageAspectRatio: Float,
|
||||
selectedUri: Uri,
|
||||
shape: Shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
val alpha by animateFloatAsState(
|
||||
if (brush == null) 1f
|
||||
else gradientAlpha
|
||||
)
|
||||
val solidBrush = SolidColor(MaterialTheme.colorScheme.surfaceContainer)
|
||||
AnimatedContent(
|
||||
targetState = if (allowPickingImage == true) {
|
||||
imageAspectRatio
|
||||
} else {
|
||||
gradientSize
|
||||
.aspectRatio
|
||||
.roundToTwoDigits()
|
||||
.coerceIn(0.01f..100f)
|
||||
}
|
||||
) { aspectRatio ->
|
||||
Box {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.aspectRatio(aspectRatio)
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (allowPickingImage != true) {
|
||||
Modifier.transparencyChecker()
|
||||
} else Modifier
|
||||
)
|
||||
.drawBehind {
|
||||
drawRect(
|
||||
brush = brush ?: solidBrush,
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
.onGloballyPositioned {
|
||||
onSizeChanged(
|
||||
Size(
|
||||
it.size.width.toFloat(),
|
||||
it.size.height.toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
.zIndex(2f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.shimmer(visible = brush == null)
|
||||
)
|
||||
}
|
||||
if (allowPickingImage == true) {
|
||||
Picture(
|
||||
model = selectedUri,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
shape = shape
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.t8rin.colors.util.roundToTwoDigits
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedSliderItem
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientType
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun GradientPropertiesSelector(
|
||||
gradientType: GradientType,
|
||||
linearAngle: Float,
|
||||
centerFriction: Offset,
|
||||
radiusFriction: Float,
|
||||
onLinearAngleChange: (Float) -> Unit,
|
||||
onRadialDimensionsChange: (Offset, Float) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = gradientType,
|
||||
modifier = modifier
|
||||
) { type ->
|
||||
when (type) {
|
||||
GradientType.Linear -> {
|
||||
EnhancedSliderItem(
|
||||
behaveAsContainer = false,
|
||||
value = linearAngle,
|
||||
title = stringResource(id = R.string.angle),
|
||||
valueRange = 0f..360f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = {
|
||||
onLinearAngleChange(it.roundToInt().toFloat())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
GradientType.Radial,
|
||||
GradientType.Sweep -> {
|
||||
var centerX by remember { mutableFloatStateOf(centerFriction.x) }
|
||||
var centerY by remember { mutableFloatStateOf(centerFriction.y) }
|
||||
var radius by remember { mutableFloatStateOf(radiusFriction) }
|
||||
|
||||
onRadialDimensionsChange(Offset(centerX, centerY), radius)
|
||||
|
||||
Column {
|
||||
EnhancedSliderItem(
|
||||
value = centerX,
|
||||
title = stringResource(id = R.string.center_x),
|
||||
internalStateTransformation = {
|
||||
it.roundToTwoDigits()
|
||||
},
|
||||
onValueChange = {
|
||||
centerX = it
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
behaveAsContainer = false
|
||||
)
|
||||
EnhancedSliderItem(
|
||||
value = centerY,
|
||||
title = stringResource(id = R.string.center_y),
|
||||
internalStateTransformation = {
|
||||
it.roundToTwoDigits()
|
||||
},
|
||||
onValueChange = {
|
||||
centerY = it
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
behaveAsContainer = false
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = type != GradientType.Sweep
|
||||
) {
|
||||
EnhancedSliderItem(
|
||||
value = radius,
|
||||
title = stringResource(id = R.string.radius),
|
||||
internalStateTransformation = {
|
||||
it.roundToTwoDigits()
|
||||
},
|
||||
onValueChange = {
|
||||
radius = it
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
behaveAsContainer = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ImageUtils.restrict
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.RoundedTextField
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.RoundedTextFieldColors
|
||||
|
||||
@Composable
|
||||
fun GradientSizeSelector(
|
||||
value: IntegerSize,
|
||||
onWidthChange: (Int) -> Unit,
|
||||
onHeightChange: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.container(
|
||||
shape = ShapeDefaults.extraLarge
|
||||
)
|
||||
) {
|
||||
val (width, height) = value
|
||||
RoundedTextField(
|
||||
value = width.takeIf { it != 0 }?.toString() ?: "",
|
||||
onValueChange = {
|
||||
onWidthChange(it.restrict(8192).toIntOrNull() ?: 0)
|
||||
},
|
||||
shape = ShapeDefaults.smallStart,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
label = {
|
||||
Text(stringResource(R.string.width, " "))
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = 8.dp,
|
||||
top = 8.dp,
|
||||
bottom = 8.dp,
|
||||
end = 2.dp
|
||||
),
|
||||
colors = RoundedTextFieldColors(
|
||||
isError = false,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
)
|
||||
RoundedTextField(
|
||||
value = height.takeIf { it != 0 }?.toString() ?: "",
|
||||
onValueChange = {
|
||||
onHeightChange(it.restrict(8192).toIntOrNull() ?: 0)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
shape = ShapeDefaults.smallEnd,
|
||||
label = {
|
||||
Text(stringResource(R.string.height, " "))
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = 2.dp,
|
||||
top = 8.dp,
|
||||
bottom = 8.dp,
|
||||
end = 8.dp
|
||||
),
|
||||
colors = RoundedTextFieldColors(
|
||||
isError = false,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Tune
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButtonGroup
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.animateContentSizeNoClip
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.ExpandableItem
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.TitleItem
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientType
|
||||
|
||||
@Composable
|
||||
fun GradientTypeSelector(
|
||||
value: GradientType,
|
||||
onValueChange: (GradientType) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
propertiesContent: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.container(
|
||||
shape = ShapeDefaults.extraLarge
|
||||
)
|
||||
.animateContentSizeNoClip(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EnhancedButtonGroup(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
enabled = true,
|
||||
items = GradientType.entries.map { it.translatedName },
|
||||
selectedIndex = GradientType.entries.indexOf(value),
|
||||
title = stringResource(id = R.string.gradient_type),
|
||||
onIndexChange = {
|
||||
onValueChange(GradientType.entries[it])
|
||||
}
|
||||
)
|
||||
ExpandableItem(
|
||||
visibleContent = {
|
||||
TitleItem(
|
||||
text = stringResource(id = R.string.properties),
|
||||
icon = Icons.Rounded.Tune
|
||||
)
|
||||
},
|
||||
expandableContent = {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
propertiesContent()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(end = 8.dp, start = 8.dp, bottom = 8.dp),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val GradientType.translatedName: String
|
||||
@Composable
|
||||
get() = when (this) {
|
||||
GradientType.Linear -> stringResource(id = R.string.gradient_type_linear)
|
||||
GradientType.Radial -> stringResource(id = R.string.gradient_type_radial)
|
||||
GradientType.Sweep -> stringResource(id = R.string.gradient_type_sweep)
|
||||
}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.SaverScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.asComposePaint
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.nativePaint
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.MiniEditLarge
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.inverseByLuma
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.color_picker.ColorPickerSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.meshGradient
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.tappable
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.transparencyChecker
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.saver.OffsetSaver
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@Composable
|
||||
internal fun MeshGradientEditor(
|
||||
state: UiMeshGradientState,
|
||||
modifier: Modifier = Modifier,
|
||||
colorPickerBitmap: Bitmap? = null
|
||||
) {
|
||||
val selectedPoint = rememberSaveable(
|
||||
stateSaver = PairOffsetColorSaver
|
||||
) { mutableStateOf(null) }
|
||||
|
||||
val dragOffset = rememberSaveable(
|
||||
stateSaver = OffsetSaver
|
||||
) { mutableStateOf(null) }
|
||||
|
||||
val showColorPicker = rememberSaveable { mutableStateOf(false) }
|
||||
val isDragging = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(ShapeDefaults.extraSmall)
|
||||
.transparencyChecker()
|
||||
.meshGradient(
|
||||
points = state.points,
|
||||
resolutionX = state.resolutionX,
|
||||
resolutionY = state.resolutionY
|
||||
)
|
||||
.tappable { tapOffset ->
|
||||
val tappedPoint = state.points.flatten()
|
||||
.firstOrNull { (offset, _) ->
|
||||
Offset(offset.x * size.width, offset.y * size.height).getDistance(
|
||||
tapOffset
|
||||
) < 60f
|
||||
}
|
||||
|
||||
showColorPicker.value = tappedPoint == selectedPoint.value
|
||||
|
||||
selectedPoint.value = tappedPoint
|
||||
if (tappedPoint == null) dragOffset.value = null
|
||||
}
|
||||
) {
|
||||
val painter = rememberVectorPainter(Icons.Outlined.MiniEditLarge)
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
if (selectedPoint.value != null) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { startOffset ->
|
||||
val tappedPoint = state.points.flatten()
|
||||
.firstOrNull { (offset, _) ->
|
||||
Offset(
|
||||
offset.x * size.width,
|
||||
offset.y * size.height
|
||||
).getDistance(startOffset) < 60f
|
||||
}
|
||||
|
||||
if (tappedPoint != null) {
|
||||
selectedPoint.value = tappedPoint
|
||||
dragOffset.value = tappedPoint.first
|
||||
isDragging.value = true
|
||||
showColorPicker.value = false
|
||||
}
|
||||
},
|
||||
onDrag = { _, dragAmount ->
|
||||
selectedPoint.value?.let { (oldOffset, color) ->
|
||||
val newOffset = Offset(
|
||||
((oldOffset.x * size.width + dragAmount.x) / size.width).coerceIn(
|
||||
0f,
|
||||
1f
|
||||
),
|
||||
((oldOffset.y * size.height + dragAmount.y) / size.height).coerceIn(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
state.updatePointPosition(oldOffset, newOffset)
|
||||
selectedPoint.value = newOffset to color
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
isDragging.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
state.points.flatten().forEach { (offset, color) ->
|
||||
val scaledOffset = Offset(offset.x * size.width, offset.y * size.height)
|
||||
val isSelected = selectedPoint.value?.first == offset
|
||||
val inverseColor = color.inverseByLuma()
|
||||
drawContext.canvas.apply {
|
||||
drawCircle(
|
||||
radius = if (isSelected) 32f else 27f,
|
||||
center = scaledOffset,
|
||||
paint = Paint().nativePaint.apply {
|
||||
setShadowLayer(
|
||||
if (isSelected) 36f else 31f,
|
||||
0f,
|
||||
0f,
|
||||
Color.Black.copy(alpha = if (isSelected) .8f else .4f)
|
||||
.toArgb()
|
||||
)
|
||||
}.asComposePaint()
|
||||
)
|
||||
}
|
||||
if (isSelected) {
|
||||
drawCircle(
|
||||
color = inverseColor,
|
||||
radius = 40f,
|
||||
center = scaledOffset
|
||||
)
|
||||
}
|
||||
drawCircle(
|
||||
color = color,
|
||||
radius = if (isSelected) 35f else 30f,
|
||||
center = scaledOffset
|
||||
)
|
||||
if (isSelected) {
|
||||
translate(
|
||||
scaledOffset.x - 25f,
|
||||
scaledOffset.y - 25f
|
||||
) {
|
||||
with(painter) {
|
||||
draw(
|
||||
size = Size(width = 50f, height = 50f),
|
||||
colorFilter = ColorFilter.tint(inverseColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColorPickerSheet(
|
||||
visible = showColorPicker.value,
|
||||
onDismiss = { showColorPicker.value = false },
|
||||
color = selectedPoint.value?.second,
|
||||
onColorSelected = { newColor ->
|
||||
selectedPoint.value?.let { (offset) ->
|
||||
state.updatePointColor(offset, newColor)
|
||||
selectedPoint.value = offset to newColor
|
||||
}
|
||||
},
|
||||
allowAlpha = true,
|
||||
additionalContent = { onColorChange ->
|
||||
GradientMakerColorPickerAdditionalContent(
|
||||
bitmap = colorPickerBitmap,
|
||||
onColorChange = onColorChange
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiMeshGradientState.updatePointPosition(
|
||||
oldOffset: Offset,
|
||||
newOffset: Offset
|
||||
) {
|
||||
var found = false
|
||||
points.replaceAll { row ->
|
||||
row.map {
|
||||
if (it.first == oldOffset && !found) {
|
||||
found = true
|
||||
|
||||
newOffset to it.second
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiMeshGradientState.updatePointColor(
|
||||
offset: Offset,
|
||||
newColor: Color
|
||||
) {
|
||||
var found = false
|
||||
|
||||
points.replaceAll { row ->
|
||||
row.map {
|
||||
if (it.first == offset && !found) {
|
||||
found = true
|
||||
|
||||
it.first to newColor
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.getDistance(other: Offset): Float {
|
||||
return sqrt((x - other.x).pow(2) + (y - other.y).pow(2))
|
||||
}
|
||||
|
||||
private val PairOffsetColorSaver: Saver<Pair<Offset, Color>?, List<Float>> =
|
||||
object : Saver<Pair<Offset, Color>?, List<Float>> {
|
||||
override fun restore(value: List<Float>): Pair<Offset, Color>? {
|
||||
return if (value.size == 5) {
|
||||
val offset = Offset(value[0], value[1])
|
||||
val color = Color(value[2], value[3], value[4])
|
||||
offset to color
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun SaverScope.save(value: Pair<Offset, Color>?): List<Float> {
|
||||
return if (value == null) emptyList()
|
||||
else listOf(
|
||||
value.first.x, value.first.y,
|
||||
value.second.red, value.second.green, value.second.blue
|
||||
)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.t8rin.colors.util.roundToTwoDigits
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.Picture
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.meshGradient
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.transparencyChecker
|
||||
|
||||
@Composable
|
||||
internal fun MeshGradientPreview(
|
||||
meshGradientState: UiMeshGradientState,
|
||||
gradientAlpha: Float,
|
||||
allowPickingImage: Boolean?,
|
||||
gradientSize: IntegerSize,
|
||||
imageAspectRatio: Float,
|
||||
selectedUri: Uri,
|
||||
shape: Shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
val alpha by animateFloatAsState(gradientAlpha)
|
||||
AnimatedContent(
|
||||
targetState = if (allowPickingImage == true) {
|
||||
imageAspectRatio
|
||||
} else {
|
||||
gradientSize
|
||||
.aspectRatio
|
||||
.roundToTwoDigits()
|
||||
.coerceIn(0.01f..100f)
|
||||
}
|
||||
) { aspectRatio ->
|
||||
Box {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.aspectRatio(aspectRatio)
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (allowPickingImage != true) {
|
||||
Modifier.transparencyChecker()
|
||||
} else Modifier
|
||||
)
|
||||
.meshGradient(
|
||||
points = meshGradientState.points,
|
||||
resolutionX = meshGradientState.resolutionX,
|
||||
resolutionY = meshGradientState.resolutionY,
|
||||
alpha = alpha
|
||||
)
|
||||
.zIndex(2f)
|
||||
)
|
||||
if (allowPickingImage == true) {
|
||||
Picture(
|
||||
model = selectedUri,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
shape = shape,
|
||||
size = 1500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButtonGroup
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.animateContentSizeNoClip
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
|
||||
@Composable
|
||||
fun TileModeSelector(
|
||||
value: TileMode,
|
||||
onValueChange: (TileMode) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val entries = remember {
|
||||
listOf(
|
||||
TileMode.Clamp,
|
||||
TileMode.Repeated,
|
||||
TileMode.Mirror,
|
||||
TileMode.Decal
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.container(
|
||||
shape = ShapeDefaults.extraLarge
|
||||
)
|
||||
.animateContentSizeNoClip(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EnhancedButtonGroup(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
enabled = true,
|
||||
items = entries.map { it.translatedName },
|
||||
selectedIndex = entries.indexOf(value),
|
||||
title = stringResource(id = R.string.tile_mode),
|
||||
onIndexChange = {
|
||||
onValueChange(entries[it])
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val TileMode.translatedName: String
|
||||
@Composable
|
||||
get() = when (this) {
|
||||
TileMode.Repeated -> stringResource(id = R.string.tile_mode_repeated)
|
||||
TileMode.Mirror -> stringResource(id = R.string.tile_mode_mirror)
|
||||
TileMode.Decal -> stringResource(id = R.string.tile_mode_decal)
|
||||
else -> stringResource(id = R.string.tile_mode_clamp)
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientState
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientType
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.MeshGradientState
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun rememberGradientState(
|
||||
size: DpSize = DpSize.Zero
|
||||
): UiGradientState {
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
return remember {
|
||||
val sizePx = if (size == DpSize.Zero) {
|
||||
Size.Zero
|
||||
} else {
|
||||
with(density) {
|
||||
Size(
|
||||
size.width.toPx(),
|
||||
size.height.toPx()
|
||||
)
|
||||
}
|
||||
}
|
||||
UiGradientState(sizePx)
|
||||
}
|
||||
}
|
||||
|
||||
class UiGradientState(
|
||||
size: Size = Size.Zero
|
||||
) : GradientState<ShaderBrush, Size, Color, TileMode, Offset> {
|
||||
|
||||
override var size by mutableStateOf(size)
|
||||
|
||||
override val brush: ShaderBrush?
|
||||
get() = getBrush(size)
|
||||
|
||||
override fun getBrush(size: Size): ShaderBrush? {
|
||||
if (colorStops.isEmpty()) return null
|
||||
|
||||
val colorStops = colorStops.sortedBy { it.first }.let { pairs ->
|
||||
if (gradientType != GradientType.Sweep) {
|
||||
pairs.distinctBy { it.first }
|
||||
} else pairs
|
||||
}.let {
|
||||
if (it.size == 1) {
|
||||
listOf(it.first(), it.first())
|
||||
} else it
|
||||
}.toTypedArray()
|
||||
|
||||
val brush = runCatching {
|
||||
when (gradientType) {
|
||||
GradientType.Linear -> {
|
||||
val angleRad = linearGradientAngle / 180f * PI
|
||||
val x = cos(angleRad).toFloat()
|
||||
val y = sin(angleRad).toFloat()
|
||||
|
||||
val radius = sqrt(size.width.pow(2) + size.height.pow(2)) / 2f
|
||||
val offset = size.center + Offset(x * radius, y * radius)
|
||||
|
||||
val exactOffset = Offset(
|
||||
x = min(offset.x.coerceAtLeast(0f), size.width),
|
||||
y = size.height - min(offset.y.coerceAtLeast(0f), size.height)
|
||||
)
|
||||
Brush.linearGradient(
|
||||
colorStops = colorStops,
|
||||
start = Offset(size.width, size.height) - exactOffset,
|
||||
end = exactOffset,
|
||||
tileMode = tileMode
|
||||
)
|
||||
}
|
||||
|
||||
GradientType.Radial -> Brush.radialGradient(
|
||||
colorStops = colorStops,
|
||||
center = Offset(
|
||||
x = size.width * centerFriction.x,
|
||||
y = size.height * centerFriction.y
|
||||
),
|
||||
radius = ((size.width.coerceAtLeast(size.height)) / 2 * radiusFriction)
|
||||
.coerceAtLeast(0.01f),
|
||||
tileMode = tileMode
|
||||
)
|
||||
|
||||
GradientType.Sweep -> Brush.sweepGradient(
|
||||
colorStops = colorStops,
|
||||
center = Offset(
|
||||
x = size.width * centerFriction.x,
|
||||
y = size.height * centerFriction.y
|
||||
)
|
||||
)
|
||||
} as ShaderBrush
|
||||
}.getOrNull()
|
||||
|
||||
return brush
|
||||
}
|
||||
|
||||
override var gradientType: GradientType by mutableStateOf(GradientType.Linear)
|
||||
|
||||
override val colorStops = mutableStateListOf<Pair<Float, Color>>()
|
||||
|
||||
override var tileMode by mutableStateOf(TileMode.Clamp)
|
||||
|
||||
override var linearGradientAngle by mutableFloatStateOf(0f)
|
||||
|
||||
override var centerFriction by mutableStateOf(Offset(.5f, .5f))
|
||||
override var radiusFriction by mutableFloatStateOf(.5f)
|
||||
}
|
||||
|
||||
class UiMeshGradientState : MeshGradientState<Color, Offset> {
|
||||
|
||||
override val points = mutableStateListOf<List<Pair<Offset, Color>>>().apply {
|
||||
addAll(generateMesh(2))
|
||||
}
|
||||
|
||||
val gridSize: Int
|
||||
get() = points.firstOrNull()?.size ?: 0
|
||||
|
||||
override var resolutionX: Int by mutableIntStateOf(16)
|
||||
|
||||
override var resolutionY: Int by mutableIntStateOf(16)
|
||||
|
||||
}
|
||||
|
||||
fun generateMesh(size: Int): List<List<Pair<Offset, Color>>> {
|
||||
return List(size) { y ->
|
||||
List(size) { x ->
|
||||
Offset(x / (size - 1f), y / (size - 1f)) to Color.random()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Color.Companion.random(): Color = Color(Random.nextInt()).copy(1f)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model
|
||||
|
||||
enum class GradientMakerType {
|
||||
Default,
|
||||
Overlay,
|
||||
Mesh,
|
||||
MeshOverlay
|
||||
}
|
||||
|
||||
fun GradientMakerType?.isMesh() =
|
||||
this == GradientMakerType.Mesh || this == GradientMakerType.MeshOverlay
|
||||
|
||||
fun GradientMakerType?.canPickImage() =
|
||||
this == GradientMakerType.Overlay || this == GradientMakerType.MeshOverlay
|
||||
+615
@@ -0,0 +1,615 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.gradient_maker.presentation.screenLogic
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.core.net.toUri
|
||||
import coil3.transform.Transformation
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.t8rin.imagetoolbox.core.data.utils.toCoil
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageShareProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FileController
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.model.ImageSaveTarget
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.model.SaveResult
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.model.onSuccess
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.updateProgress
|
||||
import com.t8rin.imagetoolbox.core.domain.transformation.GenericTransformation
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.ListUtils.leftFrom
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.ListUtils.rightFrom
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.smartJob
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppToastHost
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ImageUtils.safeAspectRatio
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.navigation.Screen
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.state.update
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientMaker
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.domain.GradientType
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.UiGradientState
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.UiMeshGradientState
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.GradientMakerType
|
||||
import com.t8rin.imagetoolbox.feature.gradient_maker.presentation.components.model.isMesh
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class GradientMakerComponent @AssistedInject internal constructor(
|
||||
@Assisted componentContext: ComponentContext,
|
||||
@Assisted val initialUris: List<Uri>?,
|
||||
@Assisted val onGoBack: () -> Unit,
|
||||
@Assisted val onNavigate: (Screen) -> Unit,
|
||||
private val fileController: FileController,
|
||||
private val imageCompressor: ImageCompressor<Bitmap>,
|
||||
private val shareProvider: ImageShareProvider<Bitmap>,
|
||||
private val imageGetter: ImageGetter<Bitmap>,
|
||||
private val gradientMaker: GradientMaker<Bitmap, ShaderBrush, Size, Color, TileMode, Offset>,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : BaseComponent(dispatchersHolder, componentContext) {
|
||||
|
||||
init {
|
||||
debounce {
|
||||
initialUris?.let(::setUris)
|
||||
resetState()
|
||||
}
|
||||
}
|
||||
|
||||
private val _screenType: MutableState<GradientMakerType?> = mutableStateOf(null)
|
||||
val screenType by _screenType
|
||||
|
||||
private val _showOriginal: MutableState<Boolean> = mutableStateOf(false)
|
||||
val showOriginal by _showOriginal
|
||||
|
||||
private var _gradientState = UiGradientState()
|
||||
private val gradientState: UiGradientState get() = _gradientState
|
||||
|
||||
private var _meshGradientState = UiMeshGradientState()
|
||||
val meshGradientState: UiMeshGradientState get() = _meshGradientState
|
||||
|
||||
val meshResolutionX: Int get() = meshGradientState.resolutionX
|
||||
val meshResolutionY: Int get() = meshGradientState.resolutionY
|
||||
val meshPoints: List<List<Pair<Offset, Color>>> get() = meshGradientState.points
|
||||
|
||||
val brush: ShaderBrush? get() = gradientState.brush
|
||||
val gradientType: GradientType get() = gradientState.gradientType
|
||||
val colorStops: List<Pair<Float, Color>> get() = gradientState.colorStops
|
||||
val tileMode: TileMode get() = gradientState.tileMode
|
||||
val angle: Float get() = gradientState.linearGradientAngle
|
||||
val centerFriction: Offset get() = gradientState.centerFriction
|
||||
val radiusFriction: Float get() = gradientState.radiusFriction
|
||||
|
||||
private var _gradientAlpha: MutableState<Float> = mutableFloatStateOf(1f)
|
||||
val gradientAlpha by _gradientAlpha
|
||||
|
||||
private val _keepExif = mutableStateOf(false)
|
||||
val keepExif by _keepExif
|
||||
|
||||
private val _selectedUri = mutableStateOf(Uri.EMPTY)
|
||||
val selectedUri: Uri by _selectedUri
|
||||
|
||||
private val _colorPickerBitmap: MutableState<Bitmap?> = mutableStateOf(null)
|
||||
val colorPickerBitmap: Bitmap? by _colorPickerBitmap
|
||||
|
||||
private val _uris = mutableStateOf(emptyList<Uri>())
|
||||
val uris by _uris
|
||||
|
||||
private val _imageAspectRatio: MutableState<Float> = mutableFloatStateOf(1f)
|
||||
val imageAspectRatio by _imageAspectRatio
|
||||
|
||||
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
|
||||
val isSaving: Boolean by _isSaving
|
||||
|
||||
private val _imageFormat = mutableStateOf(ImageFormat.Default)
|
||||
val imageFormat by _imageFormat
|
||||
|
||||
private val _gradientSize: MutableState<IntegerSize> = mutableStateOf(IntegerSize(1000, 1000))
|
||||
val gradientSize by _gradientSize
|
||||
|
||||
suspend fun createGradientBitmap(
|
||||
data: Any,
|
||||
integerSize: IntegerSize = gradientSize,
|
||||
useBitmapOriginalSizeIfAvailable: Boolean = false
|
||||
): Bitmap? {
|
||||
return if (selectedUri == Uri.EMPTY) {
|
||||
if (screenType.isMesh()) {
|
||||
gradientMaker.createMeshGradient(
|
||||
integerSize = integerSize,
|
||||
gradientState = meshGradientState
|
||||
)
|
||||
} else {
|
||||
gradientMaker.createGradient(
|
||||
integerSize = integerSize,
|
||||
gradientState = gradientState
|
||||
)
|
||||
}
|
||||
} else {
|
||||
imageGetter.getImage(
|
||||
data = data,
|
||||
originalSize = useBitmapOriginalSizeIfAvailable
|
||||
)?.let {
|
||||
if (screenType.isMesh()) {
|
||||
gradientMaker.createMeshGradient(
|
||||
src = it,
|
||||
gradientState = meshGradientState,
|
||||
gradientAlpha = gradientAlpha
|
||||
)
|
||||
} else {
|
||||
gradientMaker.createGradient(
|
||||
src = it,
|
||||
gradientState = gradientState,
|
||||
gradientAlpha = gradientAlpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _done: MutableState<Int> = mutableIntStateOf(0)
|
||||
val done by _done
|
||||
|
||||
private val _left: MutableState<Int> = mutableIntStateOf(-1)
|
||||
val left by _left
|
||||
|
||||
private var savingJob: Job? by smartJob {
|
||||
_isSaving.update { false }
|
||||
}
|
||||
|
||||
fun saveBitmaps(
|
||||
oneTimeSaveLocationUri: String?
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
_left.value = -1
|
||||
_isSaving.value = true
|
||||
if (uris.isEmpty()) {
|
||||
createGradientBitmap(
|
||||
data = Unit,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let { localBitmap ->
|
||||
val imageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
width = localBitmap.width,
|
||||
height = localBitmap.height
|
||||
)
|
||||
parseSaveResult(
|
||||
fileController.save(
|
||||
saveTarget = ImageSaveTarget(
|
||||
imageInfo = imageInfo,
|
||||
originalUri = "",
|
||||
sequenceNumber = null,
|
||||
data = imageCompressor.compressAndTransform(
|
||||
image = localBitmap,
|
||||
imageInfo = imageInfo
|
||||
)
|
||||
),
|
||||
keepOriginalMetadata = false,
|
||||
oneTimeSaveLocationUri = oneTimeSaveLocationUri
|
||||
).onSuccess(::registerSave)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val results = mutableListOf<SaveResult>()
|
||||
_done.value = 0
|
||||
_left.value = uris.size
|
||||
uris.forEach { uri ->
|
||||
createGradientBitmap(
|
||||
data = uri,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let { localBitmap ->
|
||||
val imageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
width = localBitmap.width,
|
||||
height = localBitmap.height,
|
||||
originalUri = uri.toString()
|
||||
)
|
||||
results.add(
|
||||
fileController.save(
|
||||
saveTarget = ImageSaveTarget(
|
||||
imageInfo = imageInfo,
|
||||
originalUri = uri.toString(),
|
||||
sequenceNumber = _done.value + 1,
|
||||
data = imageCompressor.compressAndTransform(
|
||||
image = localBitmap,
|
||||
imageInfo = imageInfo
|
||||
)
|
||||
),
|
||||
keepOriginalMetadata = keepExif,
|
||||
oneTimeSaveLocationUri = oneTimeSaveLocationUri
|
||||
)
|
||||
)
|
||||
} ?: results.add(
|
||||
SaveResult.Error.Exception(Throwable())
|
||||
)
|
||||
|
||||
_done.value += 1
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = left
|
||||
)
|
||||
}
|
||||
parseSaveResults(results.onSuccess(::registerSave))
|
||||
}
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun shareBitmaps() {
|
||||
savingJob = trackProgress {
|
||||
_left.value = -1
|
||||
_isSaving.value = true
|
||||
if (uris.isEmpty()) {
|
||||
createGradientBitmap(
|
||||
data = Unit,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let {
|
||||
shareProvider.shareImage(
|
||||
image = it,
|
||||
imageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
width = it.width,
|
||||
height = it.height
|
||||
),
|
||||
onComplete = AppToastHost::showConfetti
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_done.value = 0
|
||||
_left.value = uris.size
|
||||
shareProvider.shareImages(
|
||||
uris.map { it.toString() },
|
||||
imageLoader = { uri ->
|
||||
createGradientBitmap(
|
||||
data = uri,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let {
|
||||
it to ImageInfo(
|
||||
width = it.width,
|
||||
height = it.height,
|
||||
imageFormat = imageFormat
|
||||
)
|
||||
}
|
||||
},
|
||||
onProgressChange = {
|
||||
if (it == -1) {
|
||||
AppToastHost.showConfetti()
|
||||
_isSaving.value = false
|
||||
_done.value = 0
|
||||
} else {
|
||||
_done.value = it
|
||||
}
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = left
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
_isSaving.value = false
|
||||
_left.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSaving() {
|
||||
savingJob = null
|
||||
_isSaving.value = false
|
||||
_left.value = -1
|
||||
}
|
||||
|
||||
fun updateHeight(value: Int) {
|
||||
_gradientSize.update {
|
||||
it.copy(height = value)
|
||||
}
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun updateWidth(value: Int) {
|
||||
_gradientSize.update {
|
||||
it.copy(width = value)
|
||||
}
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setGradientType(gradientType: GradientType) {
|
||||
gradientState.gradientType = gradientType
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setPreviewSize(size: Size) {
|
||||
gradientState.size = size
|
||||
}
|
||||
|
||||
fun setImageFormat(imageFormat: ImageFormat) {
|
||||
_imageFormat.update { imageFormat }
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun updateLinearAngle(angle: Float) {
|
||||
gradientState.linearGradientAngle = angle
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setRadialProperties(
|
||||
center: Offset,
|
||||
radius: Float
|
||||
) {
|
||||
gradientState.centerFriction = center
|
||||
gradientState.radiusFriction = radius
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setTileMode(tileMode: TileMode) {
|
||||
gradientState.tileMode = tileMode
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setResolution(resolution: Float) {
|
||||
meshGradientState.resolutionX = resolution.roundToInt()
|
||||
meshGradientState.resolutionY = resolution.roundToInt()
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setScreenType(
|
||||
type: GradientMakerType?
|
||||
) {
|
||||
_screenType.update { type }
|
||||
}
|
||||
|
||||
fun addColorStop(
|
||||
pair: Pair<Float, Color>,
|
||||
isInitial: Boolean = false
|
||||
) {
|
||||
gradientState.colorStops.add(pair)
|
||||
if (!isInitial) {
|
||||
registerChanges()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateColorStop(
|
||||
index: Int,
|
||||
pair: Pair<Float, Color>
|
||||
) {
|
||||
gradientState.colorStops[index] = pair.copy()
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun removeColorStop(index: Int) {
|
||||
if (gradientState.colorStops.size > 2) {
|
||||
gradientState.colorStops.removeAt(index)
|
||||
registerChanges()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelectedUri(uri: Uri) {
|
||||
componentScope.launch {
|
||||
_selectedUri.value = uri
|
||||
_colorPickerBitmap.value = null
|
||||
_isImageLoading.value = true
|
||||
imageGetter.getImageData(
|
||||
uri = uri.toString(),
|
||||
size = 2000,
|
||||
onFailure = {
|
||||
_isImageLoading.value = false
|
||||
AppToastHost.showFailureToast(it)
|
||||
}
|
||||
)?.let { imageData ->
|
||||
_colorPickerBitmap.value = imageData.image
|
||||
_imageAspectRatio.update {
|
||||
imageData.image.safeAspectRatio
|
||||
}
|
||||
_isImageLoading.value = false
|
||||
setImageFormat(imageData.imageInfo.imageFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGradientAlpha(value: Float) {
|
||||
_gradientAlpha.update { value }
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
override fun resetState() {
|
||||
_selectedUri.update { Uri.EMPTY }
|
||||
_colorPickerBitmap.value = null
|
||||
_uris.update { emptyList() }
|
||||
_gradientAlpha.update { 1f }
|
||||
_gradientState = UiGradientState()
|
||||
_meshGradientState = UiMeshGradientState()
|
||||
setScreenType(null)
|
||||
registerChangesCleared()
|
||||
}
|
||||
|
||||
fun updateUrisSilently(
|
||||
removedUri: Uri
|
||||
) = componentScope.launch {
|
||||
if (selectedUri == removedUri) {
|
||||
val index = uris.indexOf(removedUri)
|
||||
val replacementUri = if (index == 0) {
|
||||
uris.getOrNull(1)
|
||||
} else {
|
||||
uris.getOrNull(index - 1)
|
||||
}
|
||||
if (replacementUri != null) {
|
||||
updateSelectedUri(replacementUri)
|
||||
} else {
|
||||
_selectedUri.value = Uri.EMPTY
|
||||
_colorPickerBitmap.value = null
|
||||
}
|
||||
}
|
||||
_uris.update {
|
||||
it.toMutableList().apply {
|
||||
remove(removedUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setUris(uris: List<Uri>) {
|
||||
_uris.update { uris }
|
||||
uris.firstOrNull()?.let(::updateSelectedUri)
|
||||
}
|
||||
|
||||
fun getGradientTransformation(): Transformation =
|
||||
GenericTransformation<Bitmap>(
|
||||
Triple(brush, meshPoints, screenType.isMesh())
|
||||
) { input ->
|
||||
createGradientBitmap(
|
||||
data = input,
|
||||
useBitmapOriginalSizeIfAvailable = false
|
||||
) ?: input
|
||||
}.toCoil()
|
||||
|
||||
fun toggleKeepExif(value: Boolean) {
|
||||
_keepExif.update { value }
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun cacheCurrentImage(onComplete: (Uri) -> Unit) {
|
||||
_isSaving.value = false
|
||||
savingJob?.cancel()
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
createGradientBitmap(
|
||||
data = selectedUri,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let { image ->
|
||||
shareProvider.cacheImage(
|
||||
image = image,
|
||||
imageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
width = image.width,
|
||||
height = image.height
|
||||
)
|
||||
)?.let { uri ->
|
||||
onComplete(uri.toUri())
|
||||
}
|
||||
}
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun cacheImages(
|
||||
onComplete: (List<Uri>) -> Unit
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
val list = mutableListOf<Uri>()
|
||||
|
||||
_left.value = -1
|
||||
_isSaving.value = true
|
||||
|
||||
if (uris.isEmpty()) {
|
||||
createGradientBitmap(
|
||||
data = Unit,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let { localBitmap ->
|
||||
val imageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
width = localBitmap.width,
|
||||
height = localBitmap.height
|
||||
)
|
||||
shareProvider.cacheImage(
|
||||
image = localBitmap,
|
||||
imageInfo = imageInfo
|
||||
)?.toUri()?.let(list::add)
|
||||
}
|
||||
} else {
|
||||
_done.value = 0
|
||||
_left.value = uris.size
|
||||
uris.forEach { uri ->
|
||||
createGradientBitmap(
|
||||
data = uri,
|
||||
useBitmapOriginalSizeIfAvailable = true
|
||||
)?.let { localBitmap ->
|
||||
val imageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
width = localBitmap.width,
|
||||
height = localBitmap.height,
|
||||
originalUri = uri.toString()
|
||||
)
|
||||
|
||||
shareProvider.cacheImage(
|
||||
image = localBitmap,
|
||||
imageInfo = imageInfo
|
||||
)?.toUri()?.let(list::add)
|
||||
}
|
||||
|
||||
_done.value += 1
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = left
|
||||
)
|
||||
}
|
||||
}
|
||||
_isSaving.value = false
|
||||
|
||||
onComplete(list)
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun selectLeftUri() {
|
||||
uris
|
||||
.indexOf(selectedUri)
|
||||
.takeIf { it >= 0 }
|
||||
?.let {
|
||||
uris.leftFrom(it)
|
||||
}
|
||||
?.let(::updateSelectedUri)
|
||||
}
|
||||
|
||||
fun selectRightUri() {
|
||||
uris
|
||||
.indexOf(selectedUri)
|
||||
.takeIf { it >= 0 }
|
||||
?.let {
|
||||
uris.rightFrom(it)
|
||||
}
|
||||
?.let(::updateSelectedUri)
|
||||
}
|
||||
|
||||
fun getFormatForFilenameSelection(): ImageFormat? = if (uris.size < 2) imageFormat
|
||||
else null
|
||||
|
||||
fun setShowOriginal(value: Boolean) {
|
||||
_showOriginal.update { value }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
operator fun invoke(
|
||||
componentContext: ComponentContext,
|
||||
initialUris: List<Uri>?,
|
||||
onGoBack: () -> Unit,
|
||||
onNavigate: (Screen) -> Unit,
|
||||
): GradientMakerComponent
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user