From 9fe316d5baea920b8db828d0b959be1eb4798f0c Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:11:15 +0000 Subject: [PATCH] fade between artwork themes instead of snapping --- .../subtracks/ui/library/AlbumDetailScreen.kt | 7 +- .../ui/theme/AnimatedArtworkColors.kt | 116 ++++++++++++++++++ .../com/subtracks/ui/theme/ArtworkTheme.kt | 5 +- .../kotlin/com/subtracks/ui/theme/Theme.kt | 2 +- 4 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index 973115fc..ba5f7316 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt b/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt new file mode 100644 index 00000000..d697710f --- /dev/null +++ b/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt @@ -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), + ) diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt index 7625d7dc..8a1504b8 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt @@ -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 diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/Theme.kt b/app/src/main/kotlin/com/subtracks/ui/theme/Theme.kt index 12c66b45..c8e89bca 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/Theme.kt @@ -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,