Skip to content

Commit 532be2c

Browse files
0xadam-browncodex
andcommitted
feat(android): Add Buddy attention ornaments
Draw playful animated flames for high-severity Buddy attention states and sparks for medium-severity states on the floating bubble. Co-Authored-By: OpenAI GPT-5.5 <noreply@openai.com>
1 parent 02f33d7 commit 532be2c

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

‎sentry-android-buddy/src/main/java/io/sentry/android/buddy/SentryBuddyUi.kt‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
1515
import androidx.compose.animation.core.tween
1616
import androidx.compose.animation.fadeIn
1717
import androidx.compose.animation.fadeOut
18+
import androidx.compose.foundation.Canvas
1819
import androidx.compose.foundation.ExperimentalFoundationApi
1920
import androidx.compose.foundation.background
2021
import androidx.compose.foundation.border
@@ -68,6 +69,7 @@ import androidx.compose.ui.Modifier
6869
import androidx.compose.ui.draw.shadow
6970
import androidx.compose.ui.geometry.Offset
7071
import androidx.compose.ui.graphics.Color
72+
import androidx.compose.ui.graphics.Path
7173
import androidx.compose.ui.graphics.graphicsLayer
7274
import androidx.compose.ui.input.pointer.pointerInput
7375
import androidx.compose.ui.layout.onGloballyPositioned
@@ -84,7 +86,9 @@ import androidx.compose.ui.text.style.TextAlign
8486
import androidx.compose.ui.unit.IntOffset
8587
import androidx.compose.ui.unit.dp
8688
import java.util.Locale
89+
import kotlin.math.PI
8790
import kotlin.math.roundToInt
91+
import kotlin.math.sin
8892
import kotlinx.coroutines.Dispatchers
8993
import kotlinx.coroutines.delay
9094
import 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
431566
private fun BoxScope.TransientRecordingText(
432567
event: TransientRecordingEvent?,

0 commit comments

Comments
 (0)