mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 23:39:15 +01:00
randomise blob layout per cover, pick a distinct accent, gradient title bar
This commit is contained in:
@@ -54,6 +54,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -129,6 +130,7 @@ fun AlbumDetailScreen(
|
||||
BoxWithConstraints(modifier.fillMaxSize().background(Color.Black)) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
val gradientHeight = maxHeight * GRADIENT_SCREENS
|
||||
val density = LocalDensity.current
|
||||
val statusBarTop = WindowInsets.statusBars.getTop(density)
|
||||
val navBarBottom = WindowInsets.navigationBars.getBottom(density)
|
||||
@@ -166,7 +168,7 @@ fun AlbumDetailScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(maxHeight * GRADIENT_SCREENS)
|
||||
.requiredHeight(gradientHeight)
|
||||
.offset { IntOffset(0, -scrollPx.roundToInt()) }
|
||||
.alpha(gradientAlpha),
|
||||
)
|
||||
@@ -216,7 +218,7 @@ fun AlbumDetailScreen(
|
||||
.height(statusBarDp + 8.dp)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(Color.Black.copy(alpha = 0.8f), Color.Transparent),
|
||||
listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent),
|
||||
),
|
||||
).alpha(1f - barFraction),
|
||||
)
|
||||
@@ -225,19 +227,25 @@ fun AlbumDetailScreen(
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxWidth()
|
||||
.height(statusBarDp)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent),
|
||||
),
|
||||
).alpha(1f - barFraction),
|
||||
)
|
||||
.height(with(density) { barHeightPx.toDp() })
|
||||
.clipToBounds()
|
||||
.alpha(barFraction),
|
||||
) {
|
||||
HeroGradient(
|
||||
colors = artwork,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(gradientHeight)
|
||||
.offset { IntOffset(0, -scrollPx.roundToInt()) },
|
||||
)
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = album?.name.orEmpty(),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(barFraction),
|
||||
@@ -252,7 +260,6 @@ fun AlbumDetailScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.background((artwork?.gradientHigh ?: Color.Black).copy(alpha = barFraction))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
@@ -291,7 +298,7 @@ private fun AlbumHeader(
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Text(
|
||||
text = album?.name.orEmpty(),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.palette.graphics.Palette
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.compose.LocalPlatformContext
|
||||
@@ -25,15 +26,19 @@ import com.subtracks.data.model.CoverArtRef
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val SEED_ART_SIZE_PX = 128
|
||||
private const val GRADIENT_FADE_START = 1.3f / 2.0f
|
||||
private const val BLOB_ZONE = 0.62f
|
||||
|
||||
data class ArtworkColors(
|
||||
val scheme: ColorScheme,
|
||||
val gradientHigh: Color,
|
||||
val gradientLow: Color,
|
||||
val accents: List<Color>,
|
||||
val blobSeed: Int,
|
||||
)
|
||||
|
||||
fun artworkColorsFromSeed(seed: Int): ArtworkColors = artworkColorsFromSeeds(seed, null)
|
||||
@@ -44,13 +49,13 @@ fun artworkColorsFromSeeds(
|
||||
): ArtworkColors {
|
||||
val (hue, saturation, _) = Color(primarySeed or 0xFF000000.toInt()).toHsl()
|
||||
val s = saturation.coerceAtMost(0.85f)
|
||||
val secondary = secondarySeed?.let { Color(it or 0xFF000000.toInt()) }
|
||||
|
||||
fun tone(
|
||||
h: Float,
|
||||
sat: Float,
|
||||
l: Float,
|
||||
) = Color.hsl(h % 360f, sat.coerceIn(0f, 1f), l.coerceIn(0f, 1f))
|
||||
|
||||
val primary = tone(hue, s, 0.62f)
|
||||
val background = tone(hue, (s * 0.35f).coerceAtMost(0.20f), 0.06f)
|
||||
val onBackground = tone(hue, (s * 0.10f).coerceAtMost(0.08f), 0.95f)
|
||||
@@ -85,11 +90,19 @@ fun artworkColorsFromSeeds(
|
||||
outlineVariant = tone(hue, s * 0.18f, 0.28f),
|
||||
)
|
||||
|
||||
val secondary =
|
||||
secondarySeed?.let { seed ->
|
||||
val color = Color(seed or 0xFF000000.toInt())
|
||||
val (sh, ss, sl) = color.toHsl()
|
||||
if (sl > 0.55f) Color.hsl(sh, ss, 0.30f) else color
|
||||
} ?: tone(hue, s, 0.08f)
|
||||
|
||||
return ArtworkColors(
|
||||
scheme = scheme,
|
||||
gradientHigh = tone(hue, (s * 0.6f).coerceAtMost(0.50f), 0.22f),
|
||||
gradientLow = tone(hue, s * 0.30f, 0.03f),
|
||||
accents = listOf(tone(hue, s * 0.8f, 0.90f), secondary ?: tone(hue, s, 0.10f)),
|
||||
accents = listOf(tone(hue, s * 0.8f, 0.90f), secondary),
|
||||
blobSeed = primarySeed,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,12 +150,31 @@ private fun seedsFrom(bitmap: Bitmap): Pair<Int, Int?>? {
|
||||
?: palette.mutedSwatch
|
||||
?: palette.dominantSwatch
|
||||
?: return null
|
||||
val primaryColor = Color(primary.rgb or 0xFF000000.toInt())
|
||||
val secondary =
|
||||
palette.darkVibrantSwatch
|
||||
?: palette.mutedSwatch
|
||||
?: palette.darkMutedSwatch
|
||||
?: palette.dominantSwatch
|
||||
return primary.rgb to secondary?.takeIf { it.rgb != primary.rgb }?.rgb
|
||||
listOfNotNull(
|
||||
palette.vibrantSwatch,
|
||||
palette.lightVibrantSwatch,
|
||||
palette.darkVibrantSwatch,
|
||||
palette.mutedSwatch,
|
||||
palette.lightMutedSwatch,
|
||||
palette.darkMutedSwatch,
|
||||
palette.dominantSwatch,
|
||||
).map { it.rgb }
|
||||
.distinct()
|
||||
.filter { it != primary.rgb }
|
||||
.maxByOrNull { distinctness(primaryColor, Color(it or 0xFF000000.toInt())) }
|
||||
return primary.rgb to secondary
|
||||
}
|
||||
|
||||
private fun distinctness(
|
||||
a: Color,
|
||||
b: Color,
|
||||
): Float {
|
||||
val ha = a.toHsl().first
|
||||
val hb = b.toHsl().first
|
||||
val hueDistance = minOf(abs(ha - hb), 360f - abs(ha - hb)) / 180f
|
||||
return hueDistance + abs(a.luminance() - b.luminance()) * 2f
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -166,6 +198,7 @@ fun HeroGradient(
|
||||
val high = colors?.gradientHigh ?: MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
val low = colors?.gradientLow ?: MaterialTheme.colorScheme.background
|
||||
val accents = colors?.accents.orEmpty()
|
||||
val seed = colors?.blobSeed ?: 0
|
||||
|
||||
Canvas(modifier) {
|
||||
drawRect(
|
||||
@@ -176,44 +209,28 @@ fun HeroGradient(
|
||||
end = Offset(size.width * 0.8f, size.height),
|
||||
),
|
||||
)
|
||||
accents.getOrNull(0)?.let { accent ->
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = 0.85f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.20f, size.height * 0.00f),
|
||||
radius = size.width * 0.60f,
|
||||
),
|
||||
)
|
||||
if (accents.isNotEmpty()) {
|
||||
val random = Random(seed)
|
||||
val zone = size.height * BLOB_ZONE
|
||||
repeat(4) { index ->
|
||||
val center =
|
||||
Offset(
|
||||
size.width * (0.10f + random.nextFloat() * 0.80f),
|
||||
zone * random.nextFloat(),
|
||||
)
|
||||
val radius = size.width * (0.45f + random.nextFloat() * 0.35f)
|
||||
val alpha = 0.50f + random.nextFloat() * 0.32f
|
||||
val accent = accents[index % accents.size]
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = alpha), accent.copy(alpha = 0f)),
|
||||
center = center,
|
||||
radius = radius,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
accents.getOrNull(1)?.let { accent ->
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = 0.80f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.90f, size.height * 0.26f),
|
||||
radius = size.width * 0.70f,
|
||||
),
|
||||
)
|
||||
}
|
||||
accents.getOrNull(0)?.let { accent ->
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = 0.55f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.82f, size.height * 0.00f),
|
||||
radius = size.width * 0.45f,
|
||||
),
|
||||
)
|
||||
}
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(high.copy(alpha = 0.55f), high.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.40f, size.height * 0.50f),
|
||||
radius = size.width * 0.80f,
|
||||
),
|
||||
)
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.verticalGradient(
|
||||
|
||||
Reference in New Issue
Block a user