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