1package com.peachpasswords.ui.components23import androidx.compose.foundation.Canvas4import androidx.compose.runtime.Composable5import androidx.compose.runtime.LaunchedEffect6import androidx.compose.runtime.mutableFloatStateOf7import androidx.compose.runtime.withFrameMillis8import androidx.compose.runtime.remember9import androidx.compose.ui.Modifier10import androidx.compose.ui.geometry.Offset11import androidx.compose.ui.graphics.Color12import androidx.compose.ui.text.TextStyle13import androidx.compose.ui.text.drawText14import androidx.compose.ui.text.rememberTextMeasurer15import androidx.compose.ui.unit.Dp16import androidx.compose.ui.unit.dp17import androidx.compose.ui.unit.sp18import com.peachpasswords.ui.theme.LocalReduceMotion19import com.peachpasswords.ui.theme.PeachType20import com.peachpasswords.ui.theme.peachColors21import kotlin.math.ceil2223private const val RUNE_COUNT = 25624private const val PATTERN_COLUMNS = 162526272829303132333435@Composable36fun RuneGridBackground(modifier: Modifier = Modifier) {37 val reduceMotion = LocalReduceMotion.current38 val layer = rememberRuneLayer(39 color = peachColors.T0.copy(alpha = 0.07f),40 fontSize = 24,41 tileWidth = 54.dp,42 rowHeight = 42.dp,43 seed = 19,44 )4546 47 48 49 50 51 val elapsedSeconds = remember { mutableFloatStateOf(0f) }52 LaunchedEffect(reduceMotion) {53 if (reduceMotion) return@LaunchedEffect54 var last = withFrameMillis { it }55 while (true) {56 val now = withFrameMillis { it }57 elapsedSeconds.floatValue += (now - last).coerceAtMost(64L) / 1000f58 last = now59 }60 }6162 Canvas(modifier) {63 drawRuneLayer(layer, elapsedSeconds.floatValue)64 }65}6667@Composable68private fun rememberRuneLayer(69 color: Color,70 fontSize: Int,71 tileWidth: Dp,72 rowHeight: Dp,73 seed: Int,74): RuneLayer {75 val measurer = rememberTextMeasurer()76 val style = remember(color, fontSize) {77 TextStyle(fontFamily = PeachType.Script, fontSize = fontSize.sp, color = color)78 }79 val glyphs = remember(style, measurer) {80 List(RUNE_COUNT) { index ->81 measurer.measure((0xE000 + index).toChar().toString(), style)82 }83 }84 return RuneLayer(85 glyphs = glyphs,86 tileWidth = tileWidth,87 rowHeight = rowHeight,88 patternWidthDp = tileWidth * PATTERN_COLUMNS,89 seed = seed,90 )91}9293private data class RuneLayer(94 val glyphs: List<androidx.compose.ui.text.TextLayoutResult>,95 val tileWidth: Dp,96 val rowHeight: Dp,97 val patternWidthDp: Dp,98 val seed: Int,99)100101102private val ROW_SPEEDS_DP = floatArrayOf(8f, 11f, 6f)103104private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawRuneLayer(105 layer: RuneLayer,106 elapsedSeconds: Float,107) {108 val tileWidth = layer.tileWidth.toPx()109 val rowHeight = layer.rowHeight.toPx()110 val patternWidth = layer.patternWidthDp.toPx()111 val columns = ceil(size.width / tileWidth).toInt() + PATTERN_COLUMNS * 2112 val rows = ceil(size.height / rowHeight).toInt() + 2113 val startColumn = -PATTERN_COLUMNS114115 for (row in -1 until rows) {116 val y = row * rowHeight117 118 119 val speedPx = ROW_SPEEDS_DP[positiveMod(row, ROW_SPEEDS_DP.size)] * density120 val offset = (elapsedSeconds * speedPx) % patternWidth121 for (i in 0..columns) {122 val column = startColumn + i123 val x = column * tileWidth - offset124 val patternColumn = positiveMod(column, PATTERN_COLUMNS)125 val glyphIndex = positiveMod(patternColumn * 31 + row * 17 + layer.seed, RUNE_COUNT)126 drawText(layer.glyphs[glyphIndex], topLeft = Offset(x, y))127 }128 }129}130131private fun positiveMod(value: Int, modulus: Int): Int = ((value % modulus) + modulus) % modulus