package com.peachpasswords.ui.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.material3.Text 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.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import com.peachpasswords.ui.theme.LocalReduceMotion import com.peachpasswords.ui.theme.PeachMotion import kotlinx.coroutines.delay /** * Per-character entrance: each glyph fades in and rises into place with a * staggered delay. Port of the browser CharEnterText (PeachMotion.CharDelay / * CharDuration). Respects reduce-motion by rendering instantly. */ @Composable fun CharEnterText( text: String, style: TextStyle, modifier: Modifier = Modifier, ) { val reduceMotion = LocalReduceMotion.current var visible by remember(text) { mutableStateOf(reduceMotion) } LaunchedEffect(text) { if (!reduceMotion) { delay(50); visible = true } } val density = LocalDensity.current val riseStartPx = with(density) { (style.fontSize.toPx() * 0.22f) } Layout( modifier = modifier, content = { text.forEachIndexed { index, char -> val alpha by animateFloatAsState( targetValue = if (visible) 1f else 0f, animationSpec = tween(PeachMotion.CharDuration, index * PeachMotion.CharDelay, PeachMotion.Spring), label = "char-alpha", ) val rise by animateFloatAsState( targetValue = if (visible) 0f else riseStartPx, animationSpec = tween(PeachMotion.CharDuration, index * PeachMotion.CharDelay, PeachMotion.Spring), label = "char-rise", ) Text( text = char.toString(), style = style, modifier = Modifier.charState(alpha, rise), ) } }, ) { measurables, constraints -> val placeables = measurables.map { it.measure(constraints) } val width = placeables.sumOf { it.width } val height = placeables.maxOfOrNull { it.height } ?: 0 layout(width, height) { var x = 0 placeables.forEach { p -> p.placeRelativeWithLayer(x, 0) x += p.width } } } } private fun Modifier.charState(alpha: Float, riseY: Float): Modifier = this.graphicsLayer { this.alpha = alpha; this.translationY = riseY }