package com.peachpasswords.ui.components import androidx.compose.animation.core.FastOutSlowInEasing 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.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp 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 } /** * The two pairing birds drifting toward each other as pairing progresses. Act 1 * (waiting) holds them apart; act 2 (listening) pulses sound arcs between them * while the finch calls and the swallow listens; act 3 (connected) resolves the * arcs into a glowing accent link and both birds go happy. * * The birds themselves are the extension's own artwork — see [PairingBird]. */ @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.28f }, 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.72f }, animationSpec = spring(dampingRatio = 0.6f, stiffness = 200f), label = "rightX", ) val transition = rememberInfiniteTransition(label = "bird") // 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", ) // The extension holds the finch chirping and the swallow attentive on every // pairing screen ("the bird stage — always animating"); only the distance // between them changes. Happy on pairing is Android's own addition. val finchPose = if (state == PairingAnimState.CONNECTED) BirdPose.HAPPY else BirdPose.CHIRPING val swallowPose = if (state == PairingAnimState.CONNECTED) BirdPose.HAPPY else BirdPose.ATTENTIVE BoxWithConstraints(modifier) { val birdSize = (maxHeight * 0.92f).coerceAtMost(maxWidth * 0.38f) Canvas(Modifier.fillMaxSize()) { val cy = size.height * 0.48f val left = Offset(size.width * leftX, cy) val right = Offset(size.width * rightX, cy) when (state) { PairingAnimState.LISTENING -> drawSoundArcs(left, right, pulse, accent) PairingAnimState.CONNECTED -> drawConnection(left, right, pulse, accent) PairingAnimState.WAITING -> Unit } } // leftX/rightX are centre fractions, but a bird parked at 0.16 of a // narrow stage hangs its tail off the edge — Canvas clips, so keep the // whole box on screen rather than losing tail feathers. val span = maxWidth - birdSize Box( Modifier .align(Alignment.CenterStart) .offset(x = (maxWidth * leftX - birdSize / 2f).coerceIn(0.dp, span)) .size(birdSize), ) { PairingBird(BirdKind.FINCH, finchPose, Modifier.fillMaxSize()) } Box( Modifier .align(Alignment.CenterStart) .offset(x = (maxWidth * rightX - birdSize / 2f).coerceIn(0.dp, span)) .size(birdSize), ) { PairingBird(BirdKind.SWALLOW, swallowPose, Modifier.fillMaxSize()) } } } 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 mid = Offset((left.x + right.x) / 2f, (left.y + right.y) / 2f) drawLine(accent.copy(alpha = 0.5f), left, right, 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) }