CharEnterText.kt

2.8 KB · raw

1package com.peachpasswords.ui.components23import androidx.compose.animation.core.animateFloatAsState4import androidx.compose.animation.core.tween5import androidx.compose.material3.Text6import androidx.compose.runtime.Composable7import androidx.compose.runtime.LaunchedEffect8import androidx.compose.runtime.getValue9import androidx.compose.runtime.mutableStateOf10import androidx.compose.runtime.remember11import androidx.compose.runtime.setValue12import androidx.compose.ui.Modifier13import androidx.compose.ui.graphics.graphicsLayer14import androidx.compose.ui.layout.Layout15import androidx.compose.ui.platform.LocalDensity16import androidx.compose.ui.text.TextStyle17import com.peachpasswords.ui.theme.LocalReduceMotion18import com.peachpasswords.ui.theme.PeachMotion19import kotlinx.coroutines.delay2021/**22 * Per-character entrance: each glyph fades in and rises into place with a23 * staggered delay. Port of the browser CharEnterText (PeachMotion.CharDelay /24 * CharDuration). Respects reduce-motion by rendering instantly.25 */26@Composable27fun CharEnterText(28    text: String,29    style: TextStyle,30    modifier: Modifier = Modifier,31) {32    val reduceMotion = LocalReduceMotion.current33    var visible by remember(text) { mutableStateOf(reduceMotion) }34    LaunchedEffect(text) {35        if (!reduceMotion) { delay(50); visible = true }36    }37    val density = LocalDensity.current38    val riseStartPx = with(density) { (style.fontSize.toPx() * 0.22f) }3940    Layout(41        modifier = modifier,42        content = {43            text.forEachIndexed { index, char ->44                val alpha by animateFloatAsState(45                    targetValue = if (visible) 1f else 0f,46                    animationSpec = tween(PeachMotion.CharDuration, index * PeachMotion.CharDelay, PeachMotion.Spring),47                    label = "char-alpha",48                )49                val rise by animateFloatAsState(50                    targetValue = if (visible) 0f else riseStartPx,51                    animationSpec = tween(PeachMotion.CharDuration, index * PeachMotion.CharDelay, PeachMotion.Spring),52                    label = "char-rise",53                )54                Text(55                    text = char.toString(),56                    style = style,57                    modifier = Modifier.charState(alpha, rise),58                )59            }60        },61    ) { measurables, constraints ->62        val placeables = measurables.map { it.measure(constraints) }63        val width = placeables.sumOf { it.width }64        val height = placeables.maxOfOrNull { it.height } ?: 065        layout(width, height) {66            var x = 067            placeables.forEach { p ->68                p.placeRelativeWithLayer(x, 0)69                x += p.width70            }71        }72    }73}7475private fun Modifier.charState(alpha: Float, riseY: Float): Modifier =76    this.graphicsLayer { this.alpha = alpha; this.translationY = riseY }