1package com.peachpasswords.ui.components23import androidx.compose.animation.core.FastOutSlowInEasing4import androidx.compose.animation.core.RepeatMode5import androidx.compose.animation.core.animateFloat6import androidx.compose.animation.core.animateFloatAsState7import androidx.compose.animation.core.infiniteRepeatable8import androidx.compose.animation.core.rememberInfiniteTransition9import androidx.compose.animation.core.spring10import androidx.compose.animation.core.tween11import androidx.compose.foundation.Canvas12import androidx.compose.foundation.layout.Box13import androidx.compose.foundation.layout.BoxWithConstraints14import androidx.compose.foundation.layout.fillMaxSize15import androidx.compose.foundation.layout.offset16import androidx.compose.foundation.layout.size17import androidx.compose.runtime.Composable18import androidx.compose.runtime.getValue19import androidx.compose.ui.Alignment20import androidx.compose.ui.Modifier21import androidx.compose.ui.geometry.Offset22import androidx.compose.ui.graphics.Color23import androidx.compose.ui.graphics.drawscope.DrawScope24import androidx.compose.ui.graphics.drawscope.Stroke25import androidx.compose.ui.unit.dp26import com.peachpasswords.ui.theme.LocalPeachAccent27import kotlin.math.sin282930enum class PairingAnimState { WAITING, LISTENING, CONNECTED }31323334353637383940@Composable41fun BirdAnimation(42 state: PairingAnimState,43 modifier: Modifier = Modifier,44) {45 val accent = LocalPeachAccent.current4647 val leftX by animateFloatAsState(48 targetValue = when (state) {49 PairingAnimState.WAITING -> 0.16f50 PairingAnimState.LISTENING -> 0.22f51 PairingAnimState.CONNECTED -> 0.28f52 },53 animationSpec = spring(dampingRatio = 0.6f, stiffness = 200f),54 label = "leftX",55 )56 val rightX by animateFloatAsState(57 targetValue = when (state) {58 PairingAnimState.WAITING -> 0.84f59 PairingAnimState.LISTENING -> 0.78f60 PairingAnimState.CONNECTED -> 0.72f61 },62 animationSpec = spring(dampingRatio = 0.6f, stiffness = 200f),63 label = "rightX",64 )6566 val transition = rememberInfiniteTransition(label = "bird")67 68 val pulse by transition.animateFloat(69 initialValue = 0f, targetValue = 1f,70 animationSpec = infiniteRepeatable(71 tween(if (state == PairingAnimState.CONNECTED) 1600 else 900, easing = FastOutSlowInEasing),72 repeatMode = RepeatMode.Restart,73 ),74 label = "pulse",75 )7677 78 79 80 val finchPose = if (state == PairingAnimState.CONNECTED) BirdPose.HAPPY else BirdPose.CHIRPING81 val swallowPose = if (state == PairingAnimState.CONNECTED) BirdPose.HAPPY else BirdPose.ATTENTIVE8283 BoxWithConstraints(modifier) {84 val birdSize = (maxHeight * 0.92f).coerceAtMost(maxWidth * 0.38f)8586 Canvas(Modifier.fillMaxSize()) {87 val cy = size.height * 0.48f88 val left = Offset(size.width * leftX, cy)89 val right = Offset(size.width * rightX, cy)90 when (state) {91 PairingAnimState.LISTENING -> drawSoundArcs(left, right, pulse, accent)92 PairingAnimState.CONNECTED -> drawConnection(left, right, pulse, accent)93 PairingAnimState.WAITING -> Unit94 }95 }9697 98 99 100 val span = maxWidth - birdSize101 Box(102 Modifier103 .align(Alignment.CenterStart)104 .offset(x = (maxWidth * leftX - birdSize / 2f).coerceIn(0.dp, span))105 .size(birdSize),106 ) {107 PairingBird(BirdKind.FINCH, finchPose, Modifier.fillMaxSize())108 }109 Box(110 Modifier111 .align(Alignment.CenterStart)112 .offset(x = (maxWidth * rightX - birdSize / 2f).coerceIn(0.dp, span))113 .size(birdSize),114 ) {115 PairingBird(BirdKind.SWALLOW, swallowPose, Modifier.fillMaxSize())116 }117 }118}119120private fun DrawScope.drawSoundArcs(left: Offset, right: Offset, pulse: Float, accent: Color) {121 val mid = Offset((left.x + right.x) / 2f, (left.y + right.y) / 2f)122 val span = (right.x - left.x) / 2f123 for (i in 0 until 3) {124 val t = (pulse + i / 3f) % 1f125 val radius = span * (0.2f + t * 0.8f)126 val alpha = (1f - t) * 0.5f127 drawCircle(128 color = accent.copy(alpha = alpha),129 radius = radius,130 center = mid,131 style = Stroke(width = 1.6f),132 )133 }134}135136private fun DrawScope.drawConnection(left: Offset, right: Offset, pulse: Float, accent: Color) {137 val mid = Offset((left.x + right.x) / 2f, (left.y + right.y) / 2f)138 drawLine(accent.copy(alpha = 0.5f), left, right, strokeWidth = 1.4f)139 val glow = 3f + sin(pulse * 2f * Math.PI.toFloat()) * 2f + 4f140 drawCircle(accent.copy(alpha = 0.22f), radius = glow * 2.4f, center = mid)141 drawCircle(accent, radius = glow, center = mid)142}