package com.vgmlr.kiln
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import kotlin.random.Random
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.unit.Dp
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.FloatState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.platform.LocalContext
import kotlin.math.abs
private const val DROP_COUNT = 24
private const val LENGTH_MS = 2600
private const val SPEED_MIN = 1.0f
private const val SPEED_VAR = 0.6f
private const val LAUNCH_SPREAD = 0.35f
private const val DRIFT = 0.05f
private const val EXIT_FADE = 2f
private val DROP_SIZE = 20.dp
private val DROP_COLOR = Color(0xFFBB0000)
private const val START_JITTER = 0.5f
private const val DEPTH_BANDS = 3
private const val SCALE_FAR = 0.5f
private const val SCALE_NEAR = 1.35f
private val BLUR_FAR = 4.dp
private const val SWAY_MIN = 0.7f
private const val SWAY_VAR = 0.6f
private const val SHAKE_GAIN = 0.15f
private const val SHAKE_SPRING = 30f
private const val SHAKE_DAMP = 4f
private const val SHAKE_MAX = 0.28f
private const val SHAKE_GATE = 1.6f
private const val GRAVITY_LP = 0.85f
private const val MAX_DT = 0.05f
private fun blurOf(band: Int): Dp =
BLUR_FAR * ((DEPTH_BANDS - 1 - band) / (DEPTH_BANDS - 1f))
@Composable
internal fun KilnCelebration(runId: Int, onFinished: () -> Unit) {
if (runId == 0) return
val done by rememberUpdatedState(onFinished)
val bands = remember(runId) { dropField(Random(runId)) }
val progress = remember(runId) { Animatable(0f) }
val painter = rememberVectorPainter(Icons.Filled.WaterDrop)
val sway = rememberSway()
LaunchedEffect(runId) {
progress.animateTo(1f, tween(LENGTH_MS, easing = LinearEasing))
done()
}
Box(Modifier.fillMaxSize()) {
bands.forEachIndexed { band, drops ->
val blur = blurOf(band)
Canvas(
Modifier.fillMaxSize().then(
if (blur > 0.dp) Modifier.blur(blur, BlurredEdgeTreatment.Unbounded)
else Modifier
)
) {
val base = DROP_SIZE.toPx()
val p = progress.value
val push = sway.floatValue * size.width
for (d in drops) {
val t = ((p - d.delay) / (1f - d.delay) * d.speed).coerceAtLeast(0f)
if (t <= 0f || t >= 1f) continue
val s = base * d.scale
val top = -s * (1f + d.startY)
val fadeFrom = size.height - s
val span = s * EXIT_FADE
val y = top + t * (fadeFrom + span - top)
val x = d.x * (size.width - s) + d.drift * size.width * t + push * d.sway
val alpha = (1f - (y - fadeFrom) / span).coerceIn(0f, 1f)
translate(x, y) {
with(painter) { draw(Size(s, s), alpha, ColorFilter.tint(DROP_COLOR)) }
}
}
}
}
}
}
@Composable
private fun rememberSway(): FloatState {
val context = LocalContext.current
val impulse = remember { mutableFloatStateOf(0f) }
val sway = remember { mutableFloatStateOf(0f) }
DisposableEffect(context) {
val sm = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
val sensor = sm?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
var gravity = 0f
val listener = object : SensorEventListener {
override fun onSensorChanged(e: SensorEvent) {
// high-pass: drop tilt, keep the jolt; negate for inertial lag
gravity = GRAVITY_LP * gravity + (1f - GRAVITY_LP) * e.values[0]
val a = e.values[0] - gravity
impulse.floatValue = if (abs(a) < SHAKE_GATE) 0f else -a
}
override fun onAccuracyChanged(s: Sensor?, accuracy: Int) = Unit
}
if (sensor != null) sm.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME)
onDispose { sm?.unregisterListener(listener) }
}
LaunchedEffect(Unit) {
var last = 0L
var v = 0f
while (true) {
withFrameNanos { now ->
val dt = if (last == 0L) 0f else ((now - last) / 1e9f).coerceAtMost(MAX_DT)
last = now
val x = sway.floatValue
v += (impulse.floatValue * SHAKE_GAIN - x * SHAKE_SPRING - v * SHAKE_DAMP) * dt
sway.floatValue = (x + v * dt).coerceIn(-SHAKE_MAX, SHAKE_MAX)
}
}
}
return sway
}
private class Drop(
val x: Float,
val startY: Float,
val delay: Float,
val speed: Float,
val scale: Float,
val drift: Float,
val sway: Float
)
private fun dropField(r: Random): List<List<Drop>> {
val bands = List(DEPTH_BANDS) { mutableListOf<Drop>() }
repeat(DROP_COUNT) {
val depth = r.nextFloat()
val band = (depth * DEPTH_BANDS).toInt().coerceIn(0, DEPTH_BANDS - 1)
val scale = SCALE_FAR + (SCALE_NEAR - SCALE_FAR) * depth
bands[band] += Drop(
x = r.nextFloat(),
startY = r.nextFloat() * START_JITTER,
delay = r.nextFloat() * LAUNCH_SPREAD,
speed = SPEED_MIN + r.nextFloat() * SPEED_VAR,
scale = scale,
drift = (r.nextFloat() - 0.5f) * 2f * DRIFT,
sway = scale * (SWAY_MIN + r.nextFloat() * SWAY_VAR)
)
}
return bands
}