Skip to content

Commit

Permalink
add some feature flags to example
Browse files Browse the repository at this point in the history
  • Loading branch information
Zhirkevich Alexander Y authored and Zhirkevich Alexander Y committed Feb 12, 2025
1 parent c038a21 commit 463c894
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 4 deletions.
13 changes: 13 additions & 0 deletions example/shared/src/androidMain/kotlin/BlurHashDecoder.android.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import android.graphics.Bitmap
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap

internal actual fun ImageBitmap.Companion.fromPixmap(
width: Int,
height: Int,
colors: IntArray
) : ImageBitmap {
return Bitmap
.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888)
.asImageBitmap()
}
146 changes: 146 additions & 0 deletions example/shared/src/commonMain/kotlin/BlurHashDecoder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toArgb
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign

internal object BlurHashDecoder {

fun decode(
blurHash: String,
width: Int,
height: Int,
punch: Float = 1f
): ImageBitmap {
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
require(blurHash.length == 4 + 2 * numCompX * numCompY) {
"invalid hash"
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}

return composeBitmap(width, height, numCompX, numCompY, colors)
}

private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}

private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
}

private fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}

private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}

private fun signedPow2(value: Float) = value.pow(2f).withSign(value)

private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
colors: Array<FloatArray>
): ImageBitmap {
// use an array for better performance when writing pixel colors
val imageArray = IntArray(width * height)
val cosinesX = DoubleArray(width * numCompY)
val cosinesY = DoubleArray(height * numCompY)

for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val cosX = cosinesX.getCos(i, numCompX, x, width)
val cosY = cosinesY.getCos(j, numCompY, y, height)
val basis = (cosX * cosY).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
imageArray[x + width * y] =
Color(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)).toArgb()
}
}

return ImageBitmap.fromPixmap(width, height, imageArray)
}

private fun DoubleArray.getCos(
x: Int,
numComp: Int,
y: Int,
size: Int
): Double {
return cos(PI * y * x / size).also {
this[x + numComp * y] = it
}
}

private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}

private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
).mapIndexed { i, c -> c to i }.toMap()

}

internal expect fun ImageBitmap.Companion.fromPixmap(
width : Int, height : Int, colors : IntArray
) : ImageBitmap
65 changes: 65 additions & 0 deletions example/shared/src/commonMain/kotlin/BlurHashPainter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
import kotlin.math.sqrt

@Composable
public fun rememberBlurHashPainter(
hash : String,
width : Int = 0,
height : Int = 0,
quality : Float = 1f,
intensity : Float = 1f
) : BlurHashPainter {
return remember(hash, width, height, quality, intensity) {
BlurHashPainter(hash, width, height, quality, intensity)
}
}

/**
* @param hash blur hash of an image
* @param width image width. Used for intrinsic size only
* @param height image height. Used for intrinsic size only
* @param quality blur quality. Must be positive
* */
public class BlurHashPainter(
public val hash : String,
width : Int = 0,
height : Int = 0,
private val quality : Float = 1f,
private val intensity : Float = 1f,
) : Painter() {

override val intrinsicSize: Size = Size(
width.takeIf { it > 0 }?.toFloat() ?: Float.NaN,
height.takeIf { it > 0 }?.toFloat() ?: Float.NaN,
)

private var cachedBitmap: ImageBitmap? = null

override fun DrawScope.onDraw() {
val pixmapSize = (3000f * quality.coerceAtLeast(0f))
.coerceAtLeast(100f)
val targetScale = sqrt(pixmapSize / (size.width * size.height)).coerceAtMost(1f)

val targetWidth = (size.width * targetScale).roundToInt()
val targetHeight = (size.height * targetScale).roundToInt()

val bitmap = cachedBitmap?.takeIf {
it.width >= targetWidth && it.width.toFloat() / it.height == targetWidth.toFloat() / targetHeight
} ?: BlurHashDecoder.decode(hash, targetWidth, targetHeight, intensity).also {
cachedBitmap = it
}

drawImage(
image = bitmap,
dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt())
)
}
}
3 changes: 1 addition & 2 deletions example/shared/src/commonMain/kotlin/TestPlayground.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import interactivecontrols.InteractiveControlsScreen
import io.github.alexzhirkevich.compottie.Compottie
import io.github.alexzhirkevich.compottie.CompottieException
import io.github.alexzhirkevich.compottie.ExperimentalCompottieApi
Expand Down Expand Up @@ -148,7 +147,7 @@ public fun TestPlayground() {
// LottieCompositionSpec.ResourceString("expr/move_horizontal.json")
// LottieCompositionSpec.ResourceString("expr/wiggle.json")
// LottieCompositionSpec.ResourceString("expr/noise.json")
LottieCompositionSpec.ResourceString(SKEWED_STROKE)
LottieCompositionSpec.ResourceString(IMAGE_ASSET)
//
// LottieCompositionSpec.Url(
// "https://assets-v2.lottiefiles.com/a/9286b092-117a-11ee-b857-2712bc869389/WSepKUr5be.lottie"
Expand Down
64 changes: 62 additions & 2 deletions example/shared/src/commonMain/kotlin/lottiefiles/LottieDetails.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalIconButton
Expand Down Expand Up @@ -63,6 +64,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
Expand Down Expand Up @@ -109,6 +112,14 @@ internal fun LottieDetails(
mutableStateOf(0)
}

var applyOpacityToLayers by remember {
mutableStateOf(false)
}

var offscreenComposing by remember {
mutableStateOf(false)
}

LaunchedEffect(
composition,
isPlaying,
Expand Down Expand Up @@ -286,10 +297,16 @@ internal fun LottieDetails(
) {

Image(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize()
.graphicsLayer {
if (offscreenComposing) {
this.compositingStrategy = CompositingStrategy.Offscreen
}
},
painter = rememberLottiePainter(
composition = composition,
progress = animatable::value
progress = animatable::value,
applyOpacityToLayers = applyOpacityToLayers
),
contentDescription = file.name
)
Expand Down Expand Up @@ -409,6 +426,25 @@ internal fun LottieDetails(
}



BooleanPreference(
checked = applyOpacityToLayers,
onCheckedChange = {
applyOpacityToLayers = it
},
label = "Apply opacity to layers"
)

BooleanPreference(
checked = offscreenComposing,
onCheckedChange = {
offscreenComposing = it
},
label = "Ofscreen composing"
)



Text(
text = "Tags",
fontWeight = FontWeight.SemiBold,
Expand Down Expand Up @@ -474,6 +510,30 @@ private val Speed = listOf(
.75f to ".75x",
)


@Composable
private fun BooleanPreference(
modifier: Modifier = Modifier,
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
label : String
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
Text(
text = label,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleMedium
)
}
}
@OptIn(ExperimentalResourceApi::class)
@Composable
private fun DownloadButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ internal class LottieFilesViewModel() : ViewModel() {
combine(search.debounce(1000), sortOrder, page) { q, s, p ->
Triple(q, s, p)
}.collectLatest { (q, s, p) ->

if (q.isBlank()){
_files.value = emptyList()
_pageCount.value = 0
_page.value = 1
return@collectLatest
}

try {
val resp = httpClient.get(
"https://lottiefiles.com/api/search/get-animations"
Expand Down
20 changes: 20 additions & 0 deletions example/shared/src/skikoMain/kotlin/BlurHashDecoder.skiko.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.asComposeImageBitmap
import androidx.compose.ui.graphics.asSkiaBitmap

internal actual fun ImageBitmap.Companion.fromPixmap(
width: Int,
height: Int,
colors: IntArray
) : ImageBitmap {

val bgra = ByteArray(colors.size * 4) {
colors[it / 4].ushr((it % 4) * 8).toByte()
}

return ImageBitmap(width, height, ImageBitmapConfig.Argb8888)
.asSkiaBitmap().apply { installPixels(bgra) }
.asComposeImageBitmap()
}

0 comments on commit 463c894

Please sign in to comment.