1+ package com.cornellappdev.uplift.ui.components.general
2+
3+ import androidx.compose.animation.core.LinearEasing
4+ import androidx.compose.animation.core.animateFloatAsState
5+ import androidx.compose.animation.core.tween
6+ import androidx.compose.foundation.Canvas
7+ import androidx.compose.foundation.layout.fillMaxSize
8+ import androidx.compose.runtime.*
9+ import androidx.compose.ui.Modifier
10+ import androidx.compose.ui.geometry.CornerRadius
11+ import androidx.compose.ui.geometry.Offset
12+ import androidx.compose.ui.geometry.Rect
13+ import androidx.compose.ui.geometry.Size
14+ import androidx.compose.ui.graphics.Brush
15+ import androidx.compose.ui.graphics.Color
16+ import androidx.compose.ui.graphics.drawscope.withTransform
17+ import com.cornellappdev.uplift.ui.theme.ConfettiColors
18+ import kotlinx.coroutines.delay
19+ import com.cornellappdev.uplift.ui.viewmodels.profile.ConfettiViewModel
20+ import kotlin.math.cos
21+ import kotlin.math.sin
22+ import kotlin.random.Random
23+
24+ /* *
25+ * Shape of a confetti particle used by [ConfettiBurst]
26+ */
27+ enum class ConfettiShape { CIRCLE , RECTANGLE }
28+
29+ /* *
30+ * UI state backing the Check-In pop-up
31+ *
32+ * @param start Emission point where the particle spawns
33+ * @param vx Initial horizontal velocity. Positive is to the right.
34+ * @param vy0 Initial vertical velocity (px/s). Negative values launch upward.
35+ * @param size Base size (px). Used as the radius for circles and scale for rectangles.
36+ * @param color Color for the particle.
37+ * @param shape Geometric shape to render for this particle.
38+ * @param rotation Initial rotation (degrees).
39+ */
40+ private data class ConfettiParticle2D (
41+ val start : Offset ,
42+ val vx : Float ,
43+ val vy0 : Float ,
44+ val size : Float ,
45+ val color : Color ,
46+ val shape : ConfettiShape ,
47+ val rotation : Float
48+ )
49+
50+ @Composable
51+ fun ConfettiBurst (
52+ confettiViewModel : ConfettiViewModel ,
53+ originRectInRoot : Rect ? ,
54+ modifier : Modifier = Modifier ,
55+ particleCount : Int = 30,
56+ colors : List <Color > = listOf(
57+ ConfettiColors .Yellow1 ,
58+ ConfettiColors .Yellow2 ,
59+ ConfettiColors .Yellow3 ,
60+ ConfettiColors .Yellow4
61+ )
62+ ) {
63+ val uiState = confettiViewModel.collectUiStateValue()
64+
65+ if (! uiState.showing || originRectInRoot == null ) {
66+ return
67+ }
68+
69+ val rect = originRectInRoot
70+
71+ var started by remember(uiState.showing) { mutableStateOf(false ) }
72+
73+ LaunchedEffect (uiState.showing) {
74+ if (uiState.showing) started = true
75+ }
76+ val progress by animateFloatAsState(
77+ targetValue = if (started) 1f else 0f ,
78+ animationSpec = tween(durationMillis = 1200 , easing = LinearEasing ),
79+ label = " confettiProgress"
80+ )
81+
82+ val particles = remember((uiState.showing)) {
83+ List (particleCount) {
84+ val x = Random .nextFloat() * rect.width + rect.left
85+ val y = Random .nextFloat() * rect.height + rect.top
86+ val start: Offset = Offset (x,y)
87+ val angle = ((- 90f + Random .nextFloat() * 110f ) * Math .PI / 180f ).toFloat()
88+ val speed = Random .nextFloat() * 700f + 400f
89+ val vx = cos(angle) * speed
90+ val vy0 = sin(angle) * speed
91+ val size = Random .nextInt(18 , 34 ).toFloat()
92+ ConfettiParticle2D (
93+ start = start,
94+ vx = vx,
95+ vy0 = vy0,
96+ size = size,
97+ color = colors.random(),
98+ shape = if (Random .nextFloat() < 0.25f ) ConfettiShape .CIRCLE else ConfettiShape .RECTANGLE ,
99+ rotation = Random .nextFloat() * 360f
100+ )
101+ }
102+ }
103+
104+
105+ LaunchedEffect (uiState.showing) {
106+ if (uiState.showing) {
107+ delay(1300 )
108+ confettiViewModel.onAnimationFinished()
109+ }
110+ }
111+
112+ Canvas (modifier = modifier.fillMaxSize()) {
113+ val g = 1750f
114+ val t = progress * 1.2f
115+
116+ particles.forEach { particle ->
117+ val x = particle.start.x + particle.vx * t
118+ val y = particle.start.y + (particle.vy0 * t + 0.5f * g * t * t)
119+
120+ val alpha = 1f - progress
121+
122+ val brush = Brush .linearGradient(
123+ colors = colors,
124+ start = Offset (x - particle.size * 0.8f , y- particle.size * 0.8f ),
125+ end = Offset (x + particle.size* 0.8f , y+ particle.size * 0.8f )
126+ )
127+
128+ when (particle.shape) {
129+ ConfettiShape .CIRCLE -> {
130+ drawCircle(
131+ brush = brush,
132+ radius = particle.size.toFloat(),
133+ center = Offset (x, y),
134+ alpha = alpha
135+ )
136+ }
137+
138+ ConfettiShape .RECTANGLE -> {
139+ withTransform({
140+ rotate(degrees = particle.rotation, pivot = Offset (x, y))
141+ }) {
142+ drawRoundRect(
143+ brush = brush,
144+ topLeft = Offset (
145+ x - (particle.size / 8f ),
146+ y - (particle.size / 2f )
147+ ),
148+ size = Size (
149+ width = particle.size * 0.6f ,
150+ height = particle.size * 1.8f
151+ ),
152+ cornerRadius = CornerRadius (2f , 2f ),
153+ alpha = alpha
154+ )
155+ }
156+ }
157+ }
158+ }
159+ }
160+ }
0 commit comments