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 = 162829303132333435363738@Composable39fun RuneGridBackground(modifier: Modifier = Modifier) {40 val reduceMotion = LocalReduceMotion.current41 val layer = rememberRuneLayer(42 color = peachColors.T0.copy(alpha = 0.07f),43 fontSize = 24,44 tileWidth = 54.dp,45 rowHeight = 42.dp,46 speedDpPerSecond = 8f,47 seed = 19,48 )49 val offset = rememberRuneOffset(layer.patternWidthDp, layer.speedDpPerSecond, reduceMotion)5051 Canvas(modifier) {52 drawRuneLayer(layer, offset)53 }54}5556@Composable57private fun rememberRuneLayer(58 color: Color,59 fontSize: Int,60 tileWidth: Dp,61 rowHeight: Dp,62 speedDpPerSecond: Float,63 seed: Int,64): RuneLayer {65 val measurer = rememberTextMeasurer()66 val style = remember(color, fontSize) {67 TextStyle(fontFamily = PeachType.Script, fontSize = fontSize.sp, color = color)68 }69 val glyphs = remember(style, measurer) {70 List(RUNE_COUNT) { index ->71 measurer.measure((0xE000 + index).toChar().toString(), style)72 }73 }74 return RuneLayer(75 glyphs = glyphs,76 tileWidth = tileWidth,77 rowHeight = rowHeight,78 patternWidthDp = tileWidth * PATTERN_COLUMNS,79 speedDpPerSecond = speedDpPerSecond,80 seed = seed,81 )82}8384@Composable85private fun rememberRuneOffset(86 patternWidth: Dp,87 speedDpPerSecond: Float,88 reduceMotion: Boolean,89): Float {90 if (reduceMotion) return 0f91 val durationMillis = ((patternWidth.value / speedDpPerSecond) * 1000f).toInt().coerceAtLeast(1)92 val transition = rememberInfiniteTransition(label = "rune_grid")93 val offset by transition.animateFloat(94 initialValue = 0f,95 targetValue = patternWidth.value,96 animationSpec = infiniteRepeatable(tween(durationMillis, easing = LinearEasing)),97 label = "rune_grid_offset",98 )99 return offset100}101102private data class RuneLayer(103 val glyphs: List<androidx.compose.ui.text.TextLayoutResult>,104 val tileWidth: Dp,105 val rowHeight: Dp,106 val patternWidthDp: Dp,107 val speedDpPerSecond: Float,108 val seed: Int,109)110111private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawRuneLayer(112 layer: RuneLayer,113 offsetDp: Float,114) {115 val tileWidth = layer.tileWidth.toPx()116 val rowHeight = layer.rowHeight.toPx()117 val patternWidth = layer.patternWidthDp.toPx()118 val offset = (offsetDp / layer.patternWidthDp.value) * patternWidth119 val columns = ceil(size.width / tileWidth).toInt() + PATTERN_COLUMNS * 2120 val rows = ceil(size.height / rowHeight).toInt() + 2121 val startColumn = -PATTERN_COLUMNS122123 for (row in -1 until rows) {124 val y = row * rowHeight125 for (i in 0..columns) {126 val column = startColumn + i127 val x = column * tileWidth - offset128 val patternColumn = positiveMod(column, PATTERN_COLUMNS)129 val glyphIndex = positiveMod(patternColumn * 31 + row * 17 + layer.seed, RUNE_COUNT)130 drawText(layer.glyphs[glyphIndex], topLeft = Offset(x, y))131 }132 }133}134135private fun positiveMod(value: Int, modulus: Int): Int = ((value % modulus) + modulus) % modulus