e6295790f7af
feat: Add onboarding rune grid background (v2.3.59)
- time
- 2026-07-08 19:36 UTC · 3 months ago
- machine
- koh2
- parent
- 8c27be41146f
↓ peach-android-e6295790.tar.gzatlas at this save
files changed3 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 −75app/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 {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) % modulusapp/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 }