package com.rizoma.orbita.ui import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloatAsState 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.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin private data class Blob( val periodMs: Int, val ax: Float, val ay: Float, // amplitud relativa del vaivén val ox: Float, val oy: Float, // origen relativo val freqY: Float, // relación de frecuencia vertical (Lissajous) val phase: Float, ) private val blobs = listOf( Blob(49_000, 0.34f, 0.30f, 0.42f, 0.40f, 1.30f, 0f), Blob(67_000, 0.38f, 0.26f, 0.58f, 0.62f, 0.80f, 2.1f), Blob(83_000, 0.30f, 0.34f, 0.50f, 0.30f, 1.70f, 4.0f), ) private const val FADE_MS = 4_000 /** * Fondo dinámico congruente con el rizoma: manchas de luz que derivan en trayectorias * de Lissajous y laten suavemente, mezcladas de forma aditiva sobre el vacío. Los colores * vienen de [palette] (hora del día + clima) y transicionan despacio entre cielos. */ @Composable fun DynamicBackground(modifier: Modifier = Modifier, palette: SkyPalette = DefaultSky) { val transition = rememberInfiniteTransition(label = "fondo") val tau = (2 * PI).toFloat() // Una fase por mancha (llamadas directas: animateFloat es @Composable, no cabe en un map/función local). val p0 by transition.animateFloat( 0f, tau, infiniteRepeatable(tween(blobs[0].periodMs, easing = LinearEasing), RepeatMode.Restart), label = "p0", ) val p1 by transition.animateFloat( 0f, tau, infiniteRepeatable(tween(blobs[1].periodMs, easing = LinearEasing), RepeatMode.Restart), label = "p1", ) val p2 by transition.animateFloat( 0f, tau, infiniteRepeatable(tween(blobs[2].periodMs, easing = LinearEasing), RepeatMode.Restart), label = "p2", ) val phases = floatArrayOf(p0, p1, p2) // El cambio de cielo (hora/clima) se difumina; el movimiento sigue siendo continuo. val fade = tween(FADE_MS, easing = LinearEasing) val base by animateColorAsState(palette.base, fade, label = "base") val cA by animateColorAsState(palette.a, fade, label = "a") val cB by animateColorAsState(palette.b, fade, label = "b") val cC by animateColorAsState(palette.c, fade, label = "c") val intensity by animateFloatAsState(palette.intensity, tween(FADE_MS, easing = LinearEasing), label = "intensity") val colors = listOf(cA, cB, cC) Canvas(modifier.fillMaxSize()) { drawRect(base) val w = size.width val h = size.height val span = maxOf(w, h) val alpha = (0.16f * intensity).coerceIn(0f, 1f) blobs.forEachIndexed { i, blob -> val t = phases[i] + blob.phase val cx = w * (blob.ox + blob.ax * cos(t)) val cy = h * (blob.oy + blob.ay * sin(t * blob.freqY)) val pulse = 0.82f + 0.18f * sin(t * 1.7f) val radius = span * 0.62f * pulse drawCircle( brush = Brush.radialGradient( colors = listOf(colors[i].copy(alpha = alpha), Color.Transparent), center = Offset(cx, cy), radius = radius, ), radius = radius, center = Offset(cx, cy), blendMode = BlendMode.Plus, ) } } }