package com.peachpasswords.ui.components import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.peachpasswords.ui.theme.LocalReduceMotion import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.ui.theme.peachColors import kotlin.math.ceil private const val RUNE_COUNT = 256 private const val PATTERN_COLUMNS = 16 /** * Continuous PeachScript rune grid for onboarding only. This is deliberately * separate from LockRuneString, which is a static lock-screen accent row. * * Single layer at exactly 0.07 opacity — the real value from the browser's * `` (RuneBackground.tsx), not the ~8-15% * range a prior pass guessed. The browser has no second/parallax layer; * matching that means not adding one here either (ONBOARDING_ADDENDUM.md §2). */ @Composable fun RuneGridBackground(modifier: Modifier = Modifier) { val reduceMotion = LocalReduceMotion.current val layer = rememberRuneLayer( color = peachColors.T0.copy(alpha = 0.07f), fontSize = 24, tileWidth = 54.dp, rowHeight = 42.dp, speedDpPerSecond = 8f, seed = 19, ) val offset = rememberRuneOffset(layer.patternWidthDp, layer.speedDpPerSecond, reduceMotion) Canvas(modifier) { drawRuneLayer(layer, offset) } } @Composable private fun rememberRuneLayer( color: Color, fontSize: Int, tileWidth: Dp, rowHeight: Dp, speedDpPerSecond: Float, seed: Int, ): RuneLayer { val measurer = rememberTextMeasurer() val style = remember(color, fontSize) { TextStyle(fontFamily = PeachType.Script, fontSize = fontSize.sp, color = color) } val glyphs = remember(style, measurer) { List(RUNE_COUNT) { index -> measurer.measure((0xE000 + index).toChar().toString(), style) } } return RuneLayer( glyphs = glyphs, tileWidth = tileWidth, rowHeight = rowHeight, patternWidthDp = tileWidth * PATTERN_COLUMNS, speedDpPerSecond = speedDpPerSecond, seed = seed, ) } @Composable private fun rememberRuneOffset( patternWidth: Dp, speedDpPerSecond: Float, reduceMotion: Boolean, ): Float { if (reduceMotion) return 0f val durationMillis = ((patternWidth.value / speedDpPerSecond) * 1000f).toInt().coerceAtLeast(1) val transition = rememberInfiniteTransition(label = "rune_grid") val offset by transition.animateFloat( initialValue = 0f, targetValue = patternWidth.value, animationSpec = infiniteRepeatable(tween(durationMillis, easing = LinearEasing)), label = "rune_grid_offset", ) return offset } private data class RuneLayer( val glyphs: List, val tileWidth: Dp, val rowHeight: Dp, val patternWidthDp: Dp, val speedDpPerSecond: Float, val seed: Int, ) private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawRuneLayer( layer: RuneLayer, offsetDp: Float, ) { val tileWidth = layer.tileWidth.toPx() val rowHeight = layer.rowHeight.toPx() val patternWidth = layer.patternWidthDp.toPx() val offset = (offsetDp / layer.patternWidthDp.value) * patternWidth val columns = ceil(size.width / tileWidth).toInt() + PATTERN_COLUMNS * 2 val rows = ceil(size.height / rowHeight).toInt() + 2 val startColumn = -PATTERN_COLUMNS for (row in -1 until rows) { val y = row * rowHeight for (i in 0..columns) { val column = startColumn + i val x = column * tileWidth - offset val patternColumn = positiveMod(column, PATTERN_COLUMNS) val glyphIndex = positiveMod(patternColumn * 31 + row * 17 + layer.seed, RUNE_COUNT) drawText(layer.glyphs[glyphIndex], topLeft = Offset(x, y)) } } } private fun positiveMod(value: Int, modulus: Int): Int = ((value % modulus) + modulus) % modulus