Skip to content

Commit c429468

Browse files
Implemented Confetti Burst UI, VM, and Repo
1 parent 4382061 commit c429468

4 files changed

Lines changed: 232 additions & 1 deletion

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.cornellappdev.uplift.data.repositories
2+
3+
import com.cornellappdev.uplift.ui.viewmodels.profile.ConfettiViewModel
4+
import com.cornellappdev.uplift.util.UIEvent
5+
import kotlinx.coroutines.flow.MutableStateFlow
6+
import kotlinx.coroutines.flow.asStateFlow
7+
import javax.inject.Inject
8+
import javax.inject.Singleton
9+
10+
//Source: Resell
11+
12+
@Singleton
13+
class ConfettiRepository @Inject constructor() {
14+
private val _showConfettiEvent: MutableStateFlow<UIEvent<ConfettiViewModel.ConfettiUiState>?> =
15+
MutableStateFlow(null)
16+
val showConfettiEvent = _showConfettiEvent.asStateFlow()
17+
18+
fun showConfetti (event: ConfettiViewModel.ConfettiUiState) {
19+
_showConfettiEvent.value = UIEvent(event)
20+
}
21+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
}

app/src/main/java/com/cornellappdev/uplift/ui/theme/Color.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ object AppColors {
1414
val LightYellow = Color(0xFFFCF5A4)
1515
val TextPrimary = Color(0xFF1B1F23)
1616
val Gray01 = Color(0xFFE5ECED)
17-
}
17+
}
18+
19+
object ConfettiColors{
20+
val Yellow1 = Color(0xFFFFF176)
21+
val Yellow2 = Color(0xFFFFEB3B)
22+
val Yellow3 = Color(0xFFFFD54F)
23+
val Yellow4 = Color(0xFFFFF59D)
24+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.cornellappdev.uplift.ui.viewmodels.profile
2+
3+
import com.cornellappdev.uplift.data.repositories.ConfettiRepository
4+
import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel
5+
import dagger.hilt.android.lifecycle.HiltViewModel
6+
import javax.inject.Inject
7+
8+
//Source: Resell
9+
10+
/** A [ConfettiViewModel] listens for confetti events and exposes animation state to the UI. */
11+
@HiltViewModel
12+
class ConfettiViewModel @Inject constructor(
13+
confettiRepository: ConfettiRepository
14+
) :
15+
UpliftViewModel<ConfettiViewModel.ConfettiUiState>(
16+
initialUiState = ConfettiUiState()
17+
) {
18+
data class ConfettiUiState(
19+
val showing: Boolean = false
20+
)
21+
22+
/** Sets UI state to show the confetti animation. */
23+
fun onShow() {
24+
applyMutation { copy(showing = true) }
25+
}
26+
27+
/** Resets UI state after the animation completes to hide confetti. */
28+
fun onAnimationFinished() {
29+
applyMutation { copy(showing = false) }
30+
}
31+
32+
init {
33+
asyncCollect(confettiRepository.showConfettiEvent) { event ->
34+
event?.consume {
35+
applyMutation {
36+
copy(
37+
showing = true
38+
)
39+
}
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)