fade between artwork themes instead of snapping

This commit is contained in:
austinried
2026-10-04 00:29:53 +00:00
parent a3d7bf7234
commit 9fe316d5ba
4 changed files with 121 additions and 9 deletions
@@ -162,11 +162,6 @@ fun AlbumDetailScreen(
}
}
}
val gradientAlpha by animateFloatAsState(
targetValue = if (artwork != null) 1f else 0f,
animationSpec = tween(durationMillis = 600),
label = "gradientAlpha",
)
val barColor =
artwork?.let {
val mid = scrollPx + barHeightPx / 2f
@@ -180,7 +175,7 @@ fun AlbumDetailScreen(
HeroGradient(
colors = artwork,
scrollPx = { scrollPx },
modifier = Modifier.fillMaxSize().alpha(gradientAlpha),
modifier = Modifier.fillMaxSize(),
)
LazyColumn(
@@ -0,0 +1,116 @@
package com.subtracks.ui.theme
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.material3.ColorScheme
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.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
private const val THEME_TRANSITION_MS = 600
fun baseArtworkColors(): ArtworkColors =
ArtworkColors(
scheme = SubtracksColorScheme,
gradientHigh = SubtracksColorScheme.surfaceContainerHigh,
gradientLow = SubtracksColorScheme.background,
accents = listOf(SubtracksColorScheme.surfaceContainerHighest, SubtracksColorScheme.surfaceContainerLow),
darkPrimary = SubtracksColorScheme.background,
blobSeed = 0,
)
@Composable
fun rememberAnimatedArtworkColors(target: ArtworkColors?): ArtworkColors {
val base = remember { baseArtworkColors() }
var from by remember { mutableStateOf(target ?: base) }
var to by remember { mutableStateOf(target ?: base) }
val progress = remember { Animatable(1f) }
LaunchedEffect(target) {
val next = target ?: base
if (next !== to) {
from = to
to = next
progress.snapTo(0f)
progress.animateTo(1f, tween(durationMillis = THEME_TRANSITION_MS))
}
}
val fraction = progress.value
return remember(from, to, fraction) { lerpArtworkColors(from, to, fraction) }
}
private fun lerpArtworkColors(
a: ArtworkColors,
b: ArtworkColors,
t: Float,
): ArtworkColors =
ArtworkColors(
scheme = lerpScheme(a.scheme, b.scheme, t),
gradientHigh = lerp(a.gradientHigh, b.gradientHigh, t),
gradientLow = lerp(a.gradientLow, b.gradientLow, t),
accents = b.accents.mapIndexed { index, color -> lerp(a.accents.getOrElse(index) { color }, color, t) },
darkPrimary = lerp(a.darkPrimary, b.darkPrimary, t),
blobSeed = b.blobSeed,
)
private fun lerpScheme(
a: ColorScheme,
b: ColorScheme,
t: Float,
): ColorScheme =
a.copy(
primary = lerp(a.primary, b.primary, t),
onPrimary = lerp(a.onPrimary, b.onPrimary, t),
primaryContainer = lerp(a.primaryContainer, b.primaryContainer, t),
onPrimaryContainer = lerp(a.onPrimaryContainer, b.onPrimaryContainer, t),
inversePrimary = lerp(a.inversePrimary, b.inversePrimary, t),
secondary = lerp(a.secondary, b.secondary, t),
onSecondary = lerp(a.onSecondary, b.onSecondary, t),
secondaryContainer = lerp(a.secondaryContainer, b.secondaryContainer, t),
onSecondaryContainer = lerp(a.onSecondaryContainer, b.onSecondaryContainer, t),
tertiary = lerp(a.tertiary, b.tertiary, t),
onTertiary = lerp(a.onTertiary, b.onTertiary, t),
tertiaryContainer = lerp(a.tertiaryContainer, b.tertiaryContainer, t),
onTertiaryContainer = lerp(a.onTertiaryContainer, b.onTertiaryContainer, t),
background = lerp(a.background, b.background, t),
onBackground = lerp(a.onBackground, b.onBackground, t),
surface = lerp(a.surface, b.surface, t),
onSurface = lerp(a.onSurface, b.onSurface, t),
surfaceVariant = lerp(a.surfaceVariant, b.surfaceVariant, t),
onSurfaceVariant = lerp(a.onSurfaceVariant, b.onSurfaceVariant, t),
surfaceTint = lerp(a.surfaceTint, b.surfaceTint, t),
inverseSurface = lerp(a.inverseSurface, b.inverseSurface, t),
inverseOnSurface = lerp(a.inverseOnSurface, b.inverseOnSurface, t),
error = lerp(a.error, b.error, t),
onError = lerp(a.onError, b.onError, t),
errorContainer = lerp(a.errorContainer, b.errorContainer, t),
onErrorContainer = lerp(a.onErrorContainer, b.onErrorContainer, t),
outline = lerp(a.outline, b.outline, t),
outlineVariant = lerp(a.outlineVariant, b.outlineVariant, t),
scrim = lerp(a.scrim, b.scrim, t),
surfaceBright = lerp(a.surfaceBright, b.surfaceBright, t),
surfaceDim = lerp(a.surfaceDim, b.surfaceDim, t),
surfaceContainer = lerp(a.surfaceContainer, b.surfaceContainer, t),
surfaceContainerHigh = lerp(a.surfaceContainerHigh, b.surfaceContainerHigh, t),
surfaceContainerHighest = lerp(a.surfaceContainerHighest, b.surfaceContainerHighest, t),
surfaceContainerLow = lerp(a.surfaceContainerLow, b.surfaceContainerLow, t),
surfaceContainerLowest = lerp(a.surfaceContainerLowest, b.surfaceContainerLowest, t),
primaryFixed = lerp(a.primaryFixed, b.primaryFixed, t),
primaryFixedDim = lerp(a.primaryFixedDim, b.primaryFixedDim, t),
onPrimaryFixed = lerp(a.onPrimaryFixed, b.onPrimaryFixed, t),
onPrimaryFixedVariant = lerp(a.onPrimaryFixedVariant, b.onPrimaryFixedVariant, t),
secondaryFixed = lerp(a.secondaryFixed, b.secondaryFixed, t),
secondaryFixedDim = lerp(a.secondaryFixedDim, b.secondaryFixedDim, t),
onSecondaryFixed = lerp(a.onSecondaryFixed, b.onSecondaryFixed, t),
onSecondaryFixedVariant = lerp(a.onSecondaryFixedVariant, b.onSecondaryFixedVariant, t),
tertiaryFixed = lerp(a.tertiaryFixed, b.tertiaryFixed, t),
tertiaryFixedDim = lerp(a.tertiaryFixedDim, b.tertiaryFixedDim, t),
onTertiaryFixed = lerp(a.onTertiaryFixed, b.onTertiaryFixed, t),
onTertiaryFixedVariant = lerp(a.onTertiaryFixedVariant, b.onTertiaryFixedVariant, t),
)
@@ -158,9 +158,10 @@ private fun blendHue(
}
@Composable
fun rememberArtworkColors(ref: CoverArtRef?): ArtworkColors? {
fun rememberArtworkColors(ref: CoverArtRef?): ArtworkColors {
val seeds by rememberArtworkSeed(ref)
return remember(seeds) { seeds?.let { (primary, secondary) -> artworkColorsFromSeeds(primary, secondary) } }
val target = remember(seeds) { seeds?.let { (primary, secondary) -> artworkColorsFromSeeds(primary, secondary) } }
return rememberAnimatedArtworkColors(target)
}
@Composable
@@ -4,7 +4,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
private val SubtracksColorScheme =
internal val SubtracksColorScheme =
darkColorScheme(
primary = White,
onPrimary = Black,