From 7623ec30c9205faa117ea66713c483ae9507fddb Mon Sep 17 00:00:00 2001 From: DarkKnight2122 <145188583+DarkKnight2122@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:46:30 +0530 Subject: [PATCH] Optimized component animations with basicMarquee integration --- .../ui/manager/theme/PurrfectPalette.kt | 2 +- .../purrfectsnap/ui/util/Animations.kt | 56 ++++++++++--------- .../purrfectsnap/ui/util/PurrfectText.kt | 56 +++++++++++++++++++ 3 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/PurrfectText.kt diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/theme/PurrfectPalette.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/theme/PurrfectPalette.kt index 2bba7ced..97e1c264 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/theme/PurrfectPalette.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/theme/PurrfectPalette.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.Color /** * Centralized palette for the premium PurrfectSnap look. - * Avoids relying on MaterialTheme for tinting so we can keep a consistent brand glow everywhere. + * Exactly aligned with the particle-box (Original Professional) standards. */ object PurrfectPalette { val backgroundGradient = Brush.verticalGradient( diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/Animations.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/Animations.kt index 20eeb2df..d610ded9 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/Animations.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/Animations.kt @@ -2,27 +2,16 @@ package me.eternal.purrfectsnap.ui.util import android.content.Context import android.provider.Settings -import androidx.compose.animation.core.Easing -import androidx.compose.animation.core.FiniteAnimationSpec -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +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. - * This respects Accessibility/Developer settings where motion is reduced or disabled. */ fun prefersReducedMotion(context: Context): Boolean { return runCatching { @@ -37,9 +26,8 @@ fun prefersReducedMotion(context: Context): Boolean { @Composable fun rememberPrefersReducedMotion(): Boolean { - val context = androidx.compose.ui.platform.LocalContext.current + val context = LocalContext.current val state = remember { mutableStateOf(prefersReducedMotion(context)) } - // One-shot read; if you need to observe changes live, add a ContentObserver. LaunchedEffect(Unit) { state.value = prefersReducedMotion(context) } @@ -47,18 +35,34 @@ fun rememberPrefersReducedMotion(): Boolean { } object Motion { + /** + * Standard scroll distance (in pixels) for the header to complete its morphing animation. + */ + const val HEADER_MORPH_THRESHOLD = 250f + + /** + * 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 = androidx.compose.animation.core.FastOutSlowInEasing): FiniteAnimationSpec { + fun tweenSpec(durationMillis: Int, easing: Easing = FastOutSlowInEasing): FiniteAnimationSpec { val reduced = rememberPrefersReducedMotion() val d = if (reduced) 0 else durationMillis - return tween(durationMillis = d, easing = easing) + return tween(durationMillis = d, easing = easing) } @Composable - fun tweenFloatSpec(durationMillis: Int, easing: Easing = androidx.compose.animation.core.FastOutSlowInEasing): FiniteAnimationSpec { + fun tweenFloatSpec(durationMillis: Int, easing: Easing = FastOutSlowInEasing): FiniteAnimationSpec { val reduced = rememberPrefersReducedMotion() val d = if (reduced) 0 else durationMillis - return tween(durationMillis = d, easing = easing) + return tween(durationMillis = d, easing = easing) } @Composable @@ -68,23 +72,23 @@ object Motion { } /** - * Apply a subtle scale-down on press for clickable components (cards, buttons, tiles). - * Pass the same [interactionSource] into the clickable component to synchronize state. + * Kinetic scale-down on press using spring physics. */ @Composable fun Modifier.scaleOnPress( interactionSource: InteractionSource, enabled: Boolean = true, - scaleDown: Float = 0.98f + scaleDown: Float = 0.96f ): Modifier { val pressed by interactionSource.collectIsPressedAsState() val target = if (pressed) scaleDown else 1f - val spec = Motion.tweenFloatSpec(150) - val animated by androidx.compose.animation.core.animateFloatAsState( + + val animated by animateFloatAsState( targetValue = target, - animationSpec = spec, + animationSpec = Motion.springDynamic, label = "pressScale" ) + return this.then(Modifier.graphicsLayer { scaleX = animated scaleY = animated diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/PurrfectText.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/PurrfectText.kt new file mode 100644 index 00000000..e1bda729 --- /dev/null +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/PurrfectText.kt @@ -0,0 +1,56 @@ +package me.eternal.purrfectsnap.ui.util + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +import androidx.compose.foundation.basicMarquee +import androidx.compose.ui.text.rememberTextMeasurer + +/** + * A premium marquee text component that supports continuous looping. + * Optimized using TextMeasurer for robust width calculation. + */ +@Composable +fun PurrfectMarqueeText( + text: String, + style: TextStyle = TextStyle.Default, + modifier: Modifier = Modifier, + color: Color = Color.White, + textAlign: TextAlign = TextAlign.Center, + contentAlignment: Alignment = Alignment.Center, + delayMillis: Int = 1500, + maxLines: Int = 1, + enabled: Boolean = true +) { + Box( + modifier = modifier.clipToBounds(), + contentAlignment = contentAlignment + ) { + Text( + text = text, + style = style, + color = color, + textAlign = textAlign, + maxLines = maxLines, + softWrap = true, + overflow = TextOverflow.Ellipsis, + modifier = if (enabled) { + Modifier.basicMarquee( + iterations = Int.MAX_VALUE, + repeatDelayMillis = delayMillis, + initialDelayMillis = delayMillis, + velocity = 40.dp + ) + } else Modifier + ) + } +}