e6295790f7af

feat: Add onboarding rune grid background (v2.3.59)

time
2026-07-08 19:36 UTC · 3 months ago
machine
koh2
parent
8c27be41146f
3 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/components/RuneGridBackground.kt+142 −0app/src/main/java/com/peachpasswords/ui/onboarding/OnboardingScreen.kt+2 −75
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 23580020-        versionName "2.3.58"19+        versionCode 23590020+        versionName "2.3.59"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/components/RuneGridBackground.kt+142 −0
@@ -0,0 +1,142 @@1+package com.peachpasswords.ui.components2+3+import androidx.compose.animation.core.LinearEasing4+import androidx.compose.animation.core.animateFloat5+import androidx.compose.animation.core.infiniteRepeatable6+import androidx.compose.animation.core.rememberInfiniteTransition7+import androidx.compose.animation.core.tween8+import androidx.compose.foundation.Canvas9+import androidx.compose.runtime.Composable10+import androidx.compose.runtime.getValue11+import androidx.compose.runtime.remember12+import androidx.compose.ui.Modifier13+import androidx.compose.ui.geometry.Offset14+import androidx.compose.ui.graphics.Color15+import androidx.compose.ui.text.TextStyle16+import androidx.compose.ui.text.drawText17+import androidx.compose.ui.text.rememberTextMeasurer18+import androidx.compose.ui.unit.Dp19+import androidx.compose.ui.unit.dp20+import androidx.compose.ui.unit.sp21+import com.peachpasswords.ui.theme.LocalReduceMotion22+import com.peachpasswords.ui.theme.PeachType23+import com.peachpasswords.ui.theme.peachColors24+import kotlin.math.ceil25+26+private const val RUNE_COUNT = 25627+private const val PATTERN_COLUMNS = 1628+29+/**30+ * Continuous PeachScript rune grid for onboarding only. This is deliberately31+ * separate from LockRuneString, which is a static lock-screen accent row.32+ */33+@Composable34+fun 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+    )53+54+    val backOffset = rememberRuneOffset(back.patternWidthDp, back.speedDpPerSecond, reduceMotion)55+    val frontOffset = rememberRuneOffset(front.patternWidthDp, front.speedDpPerSecond, reduceMotion)56+57+    Canvas(modifier) {58+        drawRuneLayer(back, backOffset)59+        drawRuneLayer(front, frontOffset)60+    }61+}62+63+@Composable64+private 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+}90+91+@Composable92+private 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+}108+109+private 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+)117+118+private 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_COLUMNS129+130+    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+}141+142+private fun positiveMod(value: Int, modulus: Int): Int = ((value % modulus) + modulus) % modulus

view file

app/src/main/java/com/peachpasswords/ui/onboarding/OnboardingScreen.kt+2 −75
@@ -1,12 +1,8 @@11 package com.peachpasswords.ui.onboarding22 33 import androidx.compose.animation.core.Animatable4-import androidx.compose.animation.core.LinearEasing54 import androidx.compose.animation.core.animateFloat6-import androidx.compose.animation.core.infiniteRepeatable7-import androidx.compose.animation.core.rememberInfiniteTransition85 import androidx.compose.animation.core.tween9-import androidx.compose.foundation.Canvas106 import androidx.compose.foundation.background117 import androidx.compose.foundation.border128 import androidx.compose.foundation.clickable@@ -36,25 +32,21 @@3632 import androidx.compose.ui.draw.clip3733 import androidx.compose.ui.draw.drawBehind3834 import androidx.compose.ui.geometry.Offset39-import androidx.compose.ui.graphics.Color4035 import androidx.compose.ui.graphics.PathEffect4136 import androidx.compose.ui.platform.LocalContext4237 import androidx.compose.ui.platform.LocalDensity4338 import androidx.compose.ui.text.TextStyle44-import androidx.compose.ui.text.drawText4539 import androidx.compose.ui.text.font.FontStyle46-import androidx.compose.ui.text.rememberTextMeasurer4740 import androidx.compose.ui.unit.IntOffset4841 import androidx.compose.ui.unit.dp4942 import androidx.compose.ui.unit.em5043 import androidx.compose.ui.unit.sp5144 import androidx.compose.material3.Text45+import com.peachpasswords.ui.components.RuneGridBackground5246 import com.peachpasswords.ui.theme.LocalPeachAccent53-import com.peachpasswords.ui.theme.LocalReduceMotion5447 import com.peachpasswords.ui.theme.peachColors5548 import com.peachpasswords.ui.theme.PeachType5649 import com.peachpasswords.ui.theme.ShellBackground57-import kotlin.random.Random5850 5951 /**6052  * One onboarding card's copy. Drives [OnboardingScreen] so the wording is a@@ -123,8 +115,7 @@123115     }124116 125117     ShellBackground {126-        // Ambient PeachScript rain behind everything.127-        PeachScriptRainCanvas(modifier = Modifier.fillMaxSize(), accent = accent)118+        RuneGridBackground(modifier = Modifier.fillMaxSize())128119 129120         // Wordmark, top-left — matches LockScreen.130121         Text(@@ -159,70 +150,6 @@159150                 onBack = { if (card > 0) card-- },160151                 onNext = { if (card < cardCount - 1) card++ else finish() },161152             )162-        }163-    }164-}165-166-// ── PeachScript rain ─────────────────────────────────────────────────────────167-168-private const val RAIN_COLUMNS = 26169-170-private class RainColumn(171-    val glyph: String,172-    val xFraction: Float,173-    val speed: Float,174-    val startOffset: Float,175-)176-177-/**178- * A field of ~26 PeachScript glyphs drifting very slowly downward — ambient, not179- * Matrix: each column has its own x, speed (≈40–80s per cycle), and start phase,180- * drawn in the accent at ~9% opacity. Honors reduce-motion by holding every glyph181- * at its start position (still rendered, just not animated).182- */183-@Composable184-fun PeachScriptRainCanvas(modifier: Modifier = Modifier, accent: Color) {185-    val reduceMotion = LocalReduceMotion.current186-    val measurer = rememberTextMeasurer()187-    val columns = remember {188-        List(RAIN_COLUMNS) {189-            RainColumn(190-                glyph = (0xE000 + Random.nextInt(256)).toChar().toString(),191-                xFraction = Random.nextFloat(),192-                // 0.5–1.0 → with the 80s base cycle, ≈40–80s per full fall.193-                speed = 0.5f + Random.nextFloat() * 0.5f,194-                startOffset = Random.nextFloat(),195-            )196-        }197-    }198-    val glyphStyle = remember(accent) {199-        TextStyle(fontFamily = PeachType.Script, fontSize = 30.sp, color = accent.copy(alpha = 0.09f))200-    }201-    val measured = remember(columns, glyphStyle, measurer) {202-        columns.map { measurer.measure(it.glyph, glyphStyle) }203-    }204-205-    val phase: Float = if (reduceMotion) {206-        0f207-    } else {208-        val transition = rememberInfiniteTransition(label = "peachscript_rain")209-        val animated by transition.animateFloat(210-            initialValue = 0f,211-            targetValue = 1f,212-            animationSpec = infiniteRepeatable(tween(80_000, easing = LinearEasing)),213-            label = "rain_phase",214-        )215-        animated216-    }217-218-    Canvas(modifier) {219-        val glyphPx = 30.sp.toPx()220-        val travel = size.height + glyphPx * 2221-        columns.forEachIndexed { i, col ->222-            val x = col.xFraction * size.width223-            val progress = ((phase * col.speed) + col.startOffset) % 1f224-            val y = progress * travel - glyphPx225-            drawText(measured[i], topLeft = Offset(x, y))226153         }227154     }228155 }

view file