@@ -15,6 +15,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
1515import androidx.compose.animation.core.tween
1616import androidx.compose.animation.fadeIn
1717import androidx.compose.animation.fadeOut
18+ import androidx.compose.foundation.Canvas
1819import androidx.compose.foundation.ExperimentalFoundationApi
1920import androidx.compose.foundation.background
2021import androidx.compose.foundation.border
@@ -68,6 +69,7 @@ import androidx.compose.ui.Modifier
6869import androidx.compose.ui.draw.shadow
6970import androidx.compose.ui.geometry.Offset
7071import androidx.compose.ui.graphics.Color
72+ import androidx.compose.ui.graphics.Path
7173import androidx.compose.ui.graphics.graphicsLayer
7274import androidx.compose.ui.input.pointer.pointerInput
7375import androidx.compose.ui.layout.onGloballyPositioned
@@ -84,7 +86,9 @@ import androidx.compose.ui.text.style.TextAlign
8486import androidx.compose.ui.unit.IntOffset
8587import androidx.compose.ui.unit.dp
8688import java.util.Locale
89+ import kotlin.math.PI
8790import kotlin.math.roundToInt
91+ import kotlin.math.sin
8892import kotlinx.coroutines.Dispatchers
8993import kotlinx.coroutines.delay
9094import kotlinx.coroutines.launch
@@ -271,8 +275,11 @@ private fun BoxScope.BuddyBubble(
271275 val bubbleColor = if (isRecording) BuddyRed else BuddyPurple
272276 val attentionItem = liveFeed.latestUnviewedAdverseItem
273277 val attentionColor = attentionItem?.let { severityColor(it.severity) }
278+ val showAttentionFlames = ! isRecording && attentionItem?.severity == Severity .HIGH
279+ val showAttentionSparks = ! isRecording && attentionItem?.severity == Severity .MEDIUM
274280 val pulseScale = remember { Animatable (1f ) }
275281 val stopTransition = rememberInfiniteTransition(label = " buddy-floating-stop-button" )
282+ val attentionTransition = rememberInfiniteTransition(label = " buddy-attention-ornaments" )
276283 val stopHaloScale by
277284 stopTransition.animateFloat(
278285 initialValue = 1.0f ,
@@ -295,6 +302,17 @@ private fun BoxScope.BuddyBubble(
295302 ),
296303 label = " buddy-floating-stop-button-halo-alpha" ,
297304 )
305+ val attentionOrnamentPhase by
306+ attentionTransition.animateFloat(
307+ initialValue = 0f ,
308+ targetValue = 1f ,
309+ animationSpec =
310+ infiniteRepeatable(
311+ animation = tween(durationMillis = 950 , easing = FastOutSlowInEasing ),
312+ repeatMode = RepeatMode .Reverse ,
313+ ),
314+ label = " buddy-attention-ornament-phase" ,
315+ )
298316 var bubbleOffset by remember { mutableStateOf<Offset ?>(null ) }
299317
300318 LaunchedEffect (attentionItem?.id) {
@@ -351,6 +369,23 @@ private fun BoxScope.BuddyBubble(
351369 modifier = Modifier .size(64 .dp),
352370 contentAlignment = Alignment .Center ,
353371 ) {
372+ if (showAttentionFlames) {
373+ AttentionFlames (
374+ phase = attentionOrnamentPhase,
375+ modifier =
376+ Modifier .size(width = 82 .dp, height = 34 .dp)
377+ .align(Alignment .TopCenter )
378+ .offset(y = (- 27 ).dp),
379+ )
380+ } else if (showAttentionSparks) {
381+ AttentionSparks (
382+ phase = attentionOrnamentPhase,
383+ modifier =
384+ Modifier .size(width = 86 .dp, height = 38 .dp)
385+ .align(Alignment .TopCenter )
386+ .offset(y = (- 29 ).dp),
387+ )
388+ }
354389 if (isRecording) {
355390 Box (
356391 modifier =
@@ -427,6 +462,106 @@ private fun BoxScope.BuddyBubble(
427462 )
428463}
429464
465+ @Composable
466+ private fun AttentionFlames (phase : Float , modifier : Modifier = Modifier ) {
467+ Canvas (modifier = modifier) {
468+ val baseY = size.height * 0.92f
469+ val centers = listOf (0.28f , 0.50f , 0.72f )
470+ centers.forEachIndexed { index, centerFraction ->
471+ val wave = sin(((phase + index * 0.23f ) * 2f * PI ).toFloat())
472+ val centerX = size.width * centerFraction + wave * 2.4f
473+ val height = size.height * (0.56f + index * 0.08f ) + wave * 2f
474+ val width = size.width * (0.085f + index * 0.012f )
475+ val tipY = baseY - height
476+ val outerFlame =
477+ Path ().apply {
478+ moveTo(centerX, tipY)
479+ cubicTo(
480+ centerX - width * 1.35f ,
481+ tipY + height * 0.38f ,
482+ centerX - width,
483+ baseY,
484+ centerX,
485+ baseY,
486+ )
487+ cubicTo(
488+ centerX + width,
489+ baseY,
490+ centerX + width * 1.35f ,
491+ tipY + height * 0.38f ,
492+ centerX,
493+ tipY,
494+ )
495+ }
496+ drawPath(outerFlame, BuddyRed .copy(alpha = 0.86f ))
497+
498+ val innerHeight = height * 0.58f
499+ val innerWidth = width * 0.52f
500+ val innerTipY = baseY - innerHeight
501+ val innerFlame =
502+ Path ().apply {
503+ moveTo(centerX, innerTipY)
504+ cubicTo(
505+ centerX - innerWidth,
506+ innerTipY + innerHeight * 0.45f ,
507+ centerX - innerWidth * 0.78f ,
508+ baseY,
509+ centerX,
510+ baseY,
511+ )
512+ cubicTo(
513+ centerX + innerWidth * 0.78f ,
514+ baseY,
515+ centerX + innerWidth,
516+ innerTipY + innerHeight * 0.45f ,
517+ centerX,
518+ innerTipY,
519+ )
520+ }
521+ drawPath(innerFlame, BuddyGold .copy(alpha = 0.88f ))
522+ }
523+ }
524+ }
525+
526+ @Composable
527+ private fun AttentionSparks (phase : Float , modifier : Modifier = Modifier ) {
528+ Canvas (modifier = modifier) {
529+ val sparks =
530+ listOf (
531+ Spark (0.22f , 0.66f , BuddyGold , 0.0f ),
532+ Spark (0.38f , 0.36f , BuddyRed .copy(alpha = 0.75f ), 0.35f ),
533+ Spark (0.61f , 0.30f , BuddyGold , 0.62f ),
534+ Spark (0.78f , 0.64f , BuddyPurple .copy(alpha = 0.70f ), 0.20f ),
535+ )
536+ sparks.forEach { spark ->
537+ val wave = sin(((phase + spark.offset) * 2f * PI ).toFloat())
538+ val twinkle = (0.58f + 0.32f * wave).coerceIn(0.35f , 0.95f )
539+ val center = Offset (size.width * spark.x, size.height * spark.y - wave * 2.2f )
540+ val radius = 2.6f + twinkle * 2f
541+ val color = spark.color.copy(alpha = twinkle)
542+ drawLine(
543+ color = color,
544+ start = Offset (center.x - radius, center.y),
545+ end = Offset (center.x + radius, center.y),
546+ strokeWidth = 2.2f ,
547+ )
548+ drawLine(
549+ color = color,
550+ start = Offset (center.x, center.y - radius),
551+ end = Offset (center.x, center.y + radius),
552+ strokeWidth = 2.2f ,
553+ )
554+ drawCircle(
555+ spark.color.copy(alpha = 0.35f * twinkle),
556+ radius = radius * 0.52f ,
557+ center = center,
558+ )
559+ }
560+ }
561+ }
562+
563+ private data class Spark (val x : Float , val y : Float , val color : Color , val offset : Float )
564+
430565@Composable
431566private fun BoxScope.TransientRecordingText (
432567 event : TransientRecordingEvent ? ,
0 commit comments