RuneGridBackground.kt

4.7 KB · raw

1package com.peachpasswords.ui.components23import androidx.compose.animation.core.LinearEasing4import androidx.compose.animation.core.animateFloat5import androidx.compose.animation.core.infiniteRepeatable6import androidx.compose.animation.core.rememberInfiniteTransition7import androidx.compose.animation.core.tween8import androidx.compose.foundation.Canvas9import androidx.compose.runtime.Composable10import androidx.compose.runtime.getValue11import androidx.compose.runtime.remember12import androidx.compose.ui.Modifier13import androidx.compose.ui.geometry.Offset14import androidx.compose.ui.graphics.Color15import androidx.compose.ui.text.TextStyle16import androidx.compose.ui.text.drawText17import androidx.compose.ui.text.rememberTextMeasurer18import androidx.compose.ui.unit.Dp19import androidx.compose.ui.unit.dp20import androidx.compose.ui.unit.sp21import com.peachpasswords.ui.theme.LocalReduceMotion22import com.peachpasswords.ui.theme.PeachType23import com.peachpasswords.ui.theme.peachColors24import kotlin.math.ceil2526private const val RUNE_COUNT = 25627private const val PATTERN_COLUMNS = 162829/**30 * Continuous PeachScript rune grid for onboarding only. This is deliberately31 * separate from LockRuneString, which is a static lock-screen accent row.32 */33@Composable34fun RuneGridBackground(modifier: Modifier = Modifier) {35    val reduceMotion = LocalReduceMotion.current36    val baseColor = peachColors.T037    val back = rememberRuneLayer(38        color = baseColor.copy(alpha = 0.085f),39        fontSize = 24,40        tileWidth = 54.dp,41        rowHeight = 42.dp,42        speedDpPerSecond = 8f,43        seed = 19,44    )45    val front = rememberRuneLayer(46        color = baseColor.copy(alpha = 0.135f),47        fontSize = 29,48        tileWidth = 62.dp,49        rowHeight = 48.dp,50        speedDpPerSecond = 13f,51        seed = 73,52    )5354    val backOffset = rememberRuneOffset(back.patternWidthDp, back.speedDpPerSecond, reduceMotion)55    val frontOffset = rememberRuneOffset(front.patternWidthDp, front.speedDpPerSecond, reduceMotion)5657    Canvas(modifier) {58        drawRuneLayer(back, backOffset)59        drawRuneLayer(front, frontOffset)60    }61}6263@Composable64private fun rememberRuneLayer(65    color: Color,66    fontSize: Int,67    tileWidth: Dp,68    rowHeight: Dp,69    speedDpPerSecond: Float,70    seed: Int,71): RuneLayer {72    val measurer = rememberTextMeasurer()73    val style = remember(color, fontSize) {74        TextStyle(fontFamily = PeachType.Script, fontSize = fontSize.sp, color = color)75    }76    val glyphs = remember(style, measurer) {77        List(RUNE_COUNT) { index ->78            measurer.measure((0xE000 + index).toChar().toString(), style)79        }80    }81    return RuneLayer(82        glyphs = glyphs,83        tileWidth = tileWidth,84        rowHeight = rowHeight,85        patternWidthDp = tileWidth * PATTERN_COLUMNS,86        speedDpPerSecond = speedDpPerSecond,87        seed = seed,88    )89}9091@Composable92private fun rememberRuneOffset(93    patternWidth: Dp,94    speedDpPerSecond: Float,95    reduceMotion: Boolean,96): Float {97    if (reduceMotion) return 0f98    val durationMillis = ((patternWidth.value / speedDpPerSecond) * 1000f).toInt().coerceAtLeast(1)99    val transition = rememberInfiniteTransition(label = "rune_grid")100    val offset by transition.animateFloat(101        initialValue = 0f,102        targetValue = patternWidth.value,103        animationSpec = infiniteRepeatable(tween(durationMillis, easing = LinearEasing)),104        label = "rune_grid_offset",105    )106    return offset107}108109private data class RuneLayer(110    val glyphs: List<androidx.compose.ui.text.TextLayoutResult>,111    val tileWidth: Dp,112    val rowHeight: Dp,113    val patternWidthDp: Dp,114    val speedDpPerSecond: Float,115    val seed: Int,116)117118private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawRuneLayer(119    layer: RuneLayer,120    offsetDp: Float,121) {122    val tileWidth = layer.tileWidth.toPx()123    val rowHeight = layer.rowHeight.toPx()124    val patternWidth = layer.patternWidthDp.toPx()125    val offset = (offsetDp / layer.patternWidthDp.value) * patternWidth126    val columns = ceil(size.width / tileWidth).toInt() + PATTERN_COLUMNS * 2127    val rows = ceil(size.height / rowHeight).toInt() + 2128    val startColumn = -PATTERN_COLUMNS129130    for (row in -1 until rows) {131        val y = row * rowHeight132        for (i in 0..columns) {133            val column = startColumn + i134            val x = column * tileWidth - offset135            val patternColumn = positiveMod(column, PATTERN_COLUMNS)136            val glyphIndex = positiveMod(patternColumn * 31 + row * 17 + layer.seed, RUNE_COUNT)137            drawText(layer.glyphs[glyphIndex], topLeft = Offset(x, y))138        }139    }140}141142private fun positiveMod(value: Int, modulus: Int): Int = ((value % modulus) + modulus) % modulus