diff --git a/android/app/src/main/java/pt/naps/maestro/ui/sidebar/ActivityRail.kt b/android/app/src/main/java/pt/naps/maestro/ui/sidebar/ActivityRail.kt index dbe24c7..ccc0023 100644 --- a/android/app/src/main/java/pt/naps/maestro/ui/sidebar/ActivityRail.kt +++ b/android/app/src/main/java/pt/naps/maestro/ui/sidebar/ActivityRail.kt @@ -2,12 +2,12 @@ package pt.naps.maestro.ui.sidebar import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.InfiniteTransition -import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight @@ -52,18 +52,51 @@ fun activityStateWord(activity: ActivityState): String = ActivityState.Error -> "Error" } -/** A held bar that pulses its alpha between 1 and [minAlpha] every - * [periodMs] — the blink/flash motion `AwaitingInput`/`Error` earn. */ +/** A hard on/off blink, one cycle per 1200ms — mirrors the web rail's + * `maestro-rail-blink` keyframes (docs/SIDEBAR.md, web/src/components/sidebar/sidebar.css): + * held at full alpha for the first half of the cycle, then snapped to 0 for + * the second half, with no crossfade. */ @Composable -private fun InfiniteTransition.pulsingAlpha(minAlpha: Float, periodMs: Int): Float { +private fun InfiniteTransition.blinkAlpha(): Float { val alpha by animateFloat( initialValue = 1f, - targetValue = minAlpha, + targetValue = 0f, animationSpec = infiniteRepeatable( - animation = tween(durationMillis = periodMs, easing = LinearEasing), - repeatMode = RepeatMode.Reverse, + animation = keyframes { + durationMillis = 1200 + 1f at 0 + 1f at 599 + 0f at 600 + 0f at 1199 + }, + repeatMode = RepeatMode.Restart, ), - label = "activity-rail-alpha", + label = "activity-rail-blink", + ) + return alpha +} + +/** A fast double-flash every 1400ms — mirrors the web rail's + * `maestro-rail-flash` keyframes: two dips to quarter alpha early in the + * cycle, then held at full alpha for the remainder. */ +@Composable +private fun InfiniteTransition.flashAlpha(): Float { + val alpha by animateFloat( + initialValue = 1f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 1400 + 1f at 0 using LinearOutSlowInEasing + 0.25f at 210 using LinearOutSlowInEasing + 1f at 420 using LinearOutSlowInEasing + 0.25f at 630 using LinearOutSlowInEasing + 1f at 840 using LinearOutSlowInEasing + 1f at 1400 + }, + repeatMode = RepeatMode.Restart, + ), + label = "activity-rail-flash", ) return alpha } @@ -77,8 +110,8 @@ fun ActivityRail(activity: ActivityState, modifier: Modifier = Modifier) { val color by animateColorAsState(targetValue = activityColor(activity), label = "activity-rail-color") val infinite = rememberInfiniteTransition(label = "activity-rail-motion") val alpha = when (activity) { - ActivityState.AwaitingInput -> infinite.pulsingAlpha(minAlpha = 0.35f, periodMs = 600) - ActivityState.Error -> infinite.pulsingAlpha(minAlpha = 0.25f, periodMs = 220) + ActivityState.AwaitingInput -> infinite.blinkAlpha() + ActivityState.Error -> infinite.flashAlpha() else -> 1f } Box(