package me.eternal.purrfect.ui.util import android.content.Context import android.provider.Settings import androidx.compose.animation.core.* import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext /** * Returns true if the user has disabled animator duration scale at the system level. */ fun prefersReducedMotion(context: Context): Boolean { return runCatching { val scale = Settings.Global.getFloat( context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f ) scale == 0f }.getOrDefault(false) } @Composable fun rememberPrefersReducedMotion(): Boolean { val context = LocalContext.current val state = remember { mutableStateOf(prefersReducedMotion(context)) } LaunchedEffect(Unit) { state.value = prefersReducedMotion(context) } return state.value } object Motion { /** * Standard scroll distance (in pixels) for the header to complete its morphing animation. */ const val HEADER_MORPH_THRESHOLD = 300f /** * Milliseconds per pixel for marquee scrolling speed. */ const val MARQUEE_CADENCE = 20 // 1. High-end Spring Physics for "Alive" UI val springDynamic = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessMediumLow ) @Composable fun tweenSpec(durationMillis: Int, easing: Easing = FastOutSlowInEasing): FiniteAnimationSpec { val reduced = rememberPrefersReducedMotion() val d = if (reduced) 0 else durationMillis return tween(durationMillis = d, easing = easing) } @Composable fun tweenFloatSpec(durationMillis: Int, easing: Easing = FastOutSlowInEasing): FiniteAnimationSpec { val reduced = rememberPrefersReducedMotion() val d = if (reduced) 0 else durationMillis return tween(durationMillis = d, easing = easing) } @Composable fun springDefault(): FiniteAnimationSpec { return spring(stiffness = Spring.StiffnessMedium) } } /** * Kinetic scale-down on press using spring physics. */ @Composable fun Modifier.scaleOnPress( interactionSource: InteractionSource, enabled: Boolean = true, scaleDown: Float = 0.96f ): Modifier { val pressed by interactionSource.collectIsPressedAsState() val target = if (pressed) scaleDown else 1f val animated by animateFloatAsState( targetValue = target, animationSpec = Motion.springDynamic, label = "pressScale" ) return this.then(Modifier.graphicsLayer { scaleX = animated scaleY = animated }) }