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