⎇ 6f37139d565f0b409d34afbcb8c6f9f2bacdb3bc
launcher/src/main/java/com/rizoma/orbita/ui/DynamicBackground.kt
Historia
crudo
Órbita: quitar apps del átomo (sin desinstalar) + fondo según hora y clima
1package com.rizoma.orbita.ui
2
3import androidx.compose.animation.animateColorAsState
4import androidx.compose.animation.core.LinearEasing
5import androidx.compose.animation.core.RepeatMode
6import androidx.compose.animation.core.animateFloat
7import androidx.compose.animation.core.animateFloatAsState
8import androidx.compose.animation.core.infiniteRepeatable
9import androidx.compose.animation.core.rememberInfiniteTransition
10import androidx.compose.animation.core.tween
11import androidx.compose.foundation.Canvas
12import androidx.compose.foundation.layout.fillMaxSize
13import androidx.compose.runtime.Composable
14import androidx.compose.runtime.getValue
15import androidx.compose.ui.Modifier
16import androidx.compose.ui.geometry.Offset
17import androidx.compose.ui.graphics.BlendMode
18import androidx.compose.ui.graphics.Brush
19import androidx.compose.ui.graphics.Color
20import kotlin.math.PI
21import kotlin.math.cos
22import kotlin.math.sin
23
24private data class Blob(
25 val periodMs: Int,
26 val ax: Float, val ay: Float, // amplitud relativa del vaivén
27 val ox: Float, val oy: Float, // origen relativo
28 val freqY: Float, // relación de frecuencia vertical (Lissajous)
29 val phase: Float,
30)
31
32private val blobs = listOf(
33 Blob(49_000, 0.34f, 0.30f, 0.42f, 0.40f, 1.30f, 0f),
34 Blob(67_000, 0.38f, 0.26f, 0.58f, 0.62f, 0.80f, 2.1f),
35 Blob(83_000, 0.30f, 0.34f, 0.50f, 0.30f, 1.70f, 4.0f),
36)
37
38private const val FADE_MS = 4_000
39
40/**
41 * Fondo dinámico congruente con el rizoma: manchas de luz que derivan en trayectorias
42 * de Lissajous y laten suavemente, mezcladas de forma aditiva sobre el vacío. Los colores
43 * vienen de [palette] (hora del día + clima) y transicionan despacio entre cielos.
44 */
45@Composable
46fun DynamicBackground(modifier: Modifier = Modifier, palette: SkyPalette = DefaultSky) {
47 val transition = rememberInfiniteTransition(label = "fondo")
48 val tau = (2 * PI).toFloat()
49 // Una fase por mancha (llamadas directas: animateFloat es @Composable, no cabe en un map/función local).
50 val p0 by transition.animateFloat(
51 0f, tau, infiniteRepeatable(tween(blobs[0].periodMs, easing = LinearEasing), RepeatMode.Restart), label = "p0",
52 )
53 val p1 by transition.animateFloat(
54 0f, tau, infiniteRepeatable(tween(blobs[1].periodMs, easing = LinearEasing), RepeatMode.Restart), label = "p1",
55 )
56 val p2 by transition.animateFloat(
57 0f, tau, infiniteRepeatable(tween(blobs[2].periodMs, easing = LinearEasing), RepeatMode.Restart), label = "p2",
58 )
59 val phases = floatArrayOf(p0, p1, p2)
60
61 // El cambio de cielo (hora/clima) se difumina; el movimiento sigue siendo continuo.
62 val fade = tween<Color>(FADE_MS, easing = LinearEasing)
63 val base by animateColorAsState(palette.base, fade, label = "base")
64 val cA by animateColorAsState(palette.a, fade, label = "a")
65 val cB by animateColorAsState(palette.b, fade, label = "b")
66 val cC by animateColorAsState(palette.c, fade, label = "c")
67 val intensity by animateFloatAsState(palette.intensity, tween(FADE_MS, easing = LinearEasing), label = "intensity")
68 val colors = listOf(cA, cB, cC)
69
70 Canvas(modifier.fillMaxSize()) {
71 drawRect(base)
72 val w = size.width
73 val h = size.height
74 val span = maxOf(w, h)
75 val alpha = (0.16f * intensity).coerceIn(0f, 1f)
76 blobs.forEachIndexed { i, blob ->
77 val t = phases[i] + blob.phase
78 val cx = w * (blob.ox + blob.ax * cos(t))
79 val cy = h * (blob.oy + blob.ay * sin(t * blob.freqY))
80 val pulse = 0.82f + 0.18f * sin(t * 1.7f)
81 val radius = span * 0.62f * pulse
82 drawCircle(
83 brush = Brush.radialGradient(
84 colors = listOf(colors[i].copy(alpha = alpha), Color.Transparent),
85 center = Offset(cx, cy),
86 radius = radius,
87 ),
88 radius = radius,
89 center = Offset(cx, cy),
90 blendMode = BlendMode.Plus,
91 )
92 }
93 }
94}