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 = 16282930313233@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