package com.peachpasswords.ui.components import androidx.compose.animation.core.FastOutSlowInEasing 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.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas 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.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import com.peachpasswords.ui.theme.LocalPeachAccent import kotlin.math.sin /** The three acts of the pairing animation, bound to the audio protocol. */ enum class PairingAnimState { WAITING, LISTENING, CONNECTED } private val BodyHi = Color(0xFFFFB082) private val BodyLo = Color(0xFFE5805C) private val Beak = Color(0xFFF2B45A) private val Eye = Color(0xFF1F0A10) /** * Two finches that drift toward each other as pairing progresses. Act 1 * (waiting) holds them apart with a slow breath-bob; Act 2 (listening) pulses * inaudible sound arcs between them; Act 3 (connected) resolves the arcs into a * glowing accent link and a synchronised flutter. Stylised from the browser * ChirpPairingFlow birds — same coral/apricot palette, simpler geometry. */ @Composable fun BirdAnimation( state: PairingAnimState, modifier: Modifier = Modifier, ) { val accent = LocalPeachAccent.current val leftX by animateFloatAsState( targetValue = when (state) { PairingAnimState.WAITING -> 0.16f PairingAnimState.LISTENING -> 0.22f PairingAnimState.CONNECTED -> 0.39f }, animationSpec = spring(dampingRatio = 0.6f, stiffness = 200f), label = "leftX", ) val rightX by animateFloatAsState( targetValue = when (state) { PairingAnimState.WAITING -> 0.84f PairingAnimState.LISTENING -> 0.78f PairingAnimState.CONNECTED -> 0.61f }, animationSpec = spring(dampingRatio = 0.6f, stiffness = 200f), label = "rightX", ) val transition = rememberInfiniteTransition(label = "bird") val bob by transition.animateFloat( initialValue = 0f, targetValue = (2f * Math.PI).toFloat(), animationSpec = infiniteRepeatable(tween(2200, easing = LinearEasing)), label = "bob", ) // Sound-arc sweep (LISTENING) and connection-glow pulse (CONNECTED). val pulse by transition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(if (state == PairingAnimState.CONNECTED) 1600 else 900, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Restart, ), label = "pulse", ) Canvas(modifier = modifier) { val h = size.height val cy = h * 0.52f val leftCenter = Offset(size.width * leftX, cy + sin(bob) * 4f) val rightCenter = Offset(size.width * rightX, cy + sin(bob + Math.PI.toFloat()) * 4f) val birdScale = (size.minDimension / 220f).coerceIn(0.5f, 1.4f) if (state == PairingAnimState.LISTENING) { drawSoundArcs(leftCenter, rightCenter, pulse, accent) } if (state == PairingAnimState.CONNECTED) { drawConnection(leftCenter, rightCenter, pulse, accent) } drawBird(leftCenter, birdScale, facingRight = true, listening = state != PairingAnimState.WAITING) drawBird(rightCenter, birdScale, facingRight = false, listening = state != PairingAnimState.WAITING) } } private fun DrawScope.drawSoundArcs(left: Offset, right: Offset, pulse: Float, accent: Color) { val mid = Offset((left.x + right.x) / 2f, (left.y + right.y) / 2f) val span = (right.x - left.x) / 2f for (i in 0 until 3) { val t = (pulse + i / 3f) % 1f val radius = span * (0.2f + t * 0.8f) val alpha = (1f - t) * 0.5f drawCircle( color = accent.copy(alpha = alpha), radius = radius, center = mid, style = Stroke(width = 1.6f), ) } } private fun DrawScope.drawConnection(left: Offset, right: Offset, pulse: Float, accent: Color) { val beakL = Offset(left.x + 26f, left.y - 4f) val beakR = Offset(right.x - 26f, right.y - 4f) val mid = Offset((beakL.x + beakR.x) / 2f, (beakL.y + beakR.y) / 2f) drawLine(accent.copy(alpha = 0.5f), beakL, beakR, strokeWidth = 1.4f) val glow = 3f + sin(pulse * 2f * Math.PI.toFloat()) * 2f + 4f drawCircle(accent.copy(alpha = 0.22f), radius = glow * 2.4f, center = mid) drawCircle(accent, radius = glow, center = mid) } /** A compact finch: body blob, wing, head, beak, eye. */ private fun DrawScope.drawBird( center: Offset, scale: Float, facingRight: Boolean, listening: Boolean, ) { val dir = if (facingRight) 1f else -1f fun p(dx: Float, dy: Float) = Offset(center.x + dx * scale * dir, center.y + dy * scale) // Shadow drawOval( color = Color(0x33000000), topLeft = Offset(center.x - 30f * scale, center.y + 42f * scale), size = Size(60f * scale, 7f * scale), ) // Body val body = Path().apply { moveTo(p(-30f, 6f).x, p(-30f, 6f).y) cubicTo(p(-36f, -22f).x, p(-36f, -22f).y, p(-8f, -44f).x, p(-8f, -44f).y, p(24f, -40f).x, p(24f, -40f).y) cubicTo(p(52f, -36f).x, p(52f, -36f).y, p(60f, -10f).x, p(60f, -10f).y, p(50f, 18f).x, p(50f, 18f).y) cubicTo(p(40f, 40f).x, p(40f, 40f).y, p(-2f, 44f).x, p(-2f, 44f).y, p(-26f, 32f).x, p(-26f, 32f).y) cubicTo(p(-34f, 24f).x, p(-34f, 24f).y, p(-32f, 14f).x, p(-32f, 14f).y, p(-30f, 6f).x, p(-30f, 6f).y) close() } drawPath(body, BodyHi) // Wing (lifts when listening) val wingLift = if (listening) -6f else 0f val wing = Path().apply { moveTo(p(-6f, -14f + wingLift).x, p(-6f, -14f + wingLift).y) cubicTo(p(14f, -22f + wingLift).x, p(14f, -22f + wingLift).y, p(40f, -10f).x, p(40f, -10f).y, p(34f, 6f).x, p(34f, 6f).y) cubicTo(p(20f, 14f).x, p(20f, 14f).y, p(-2f, 8f).x, p(-2f, 8f).y, p(-10f, -2f).x, p(-10f, -2f).y) close() } drawPath(wing, BodyLo) // Head drawCircle(BodyHi, radius = 22f * scale, center = p(34f, -30f)) // Beak val beak = Path().apply { moveTo(p(52f, -30f).x, p(52f, -30f).y) lineTo(p(72f, -27f).x, p(72f, -27f).y) lineTo(p(53f, -22f).x, p(53f, -22f).y) close() } drawPath(beak, Beak) // Eye drawCircle(Eye, radius = 4.2f * scale, center = p(42f, -33f)) drawCircle(Color(0xFFFFF0E1), radius = 1.4f * scale, center = p(43.5f, -34.5f)) }