Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Party time #5

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package io.github.plastix.buzz.detail

import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import io.github.plastix.buzz.util.radians
import io.github.plastix.buzz.util.randomFloat
import io.github.plastix.buzz.util.randomInt
import io.github.plastix.buzz.util.remap
import kotlin.math.cos
import kotlin.math.sin

data class Projectile(val origin: Offset, val angle: Float, val speed: Float, val color: Color, val gravity: Float) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about making these data classes and helper functions private?

fun position(t: Float): Offset {
val x = speed * t * cos(angle)
val y = (speed * t * sin(angle)) - (0.5f * gravity * t * t)
return origin + Offset(x, y)
}
}

data class ConfettiState(
val projectiles: List<Projectile>,
val time: Float,
val globalAlpha: Float,
val emitter: Emitter,
val particleInfo: ParticleInfo
)

data class Emitter(
val width: Float,
val height: Float,
val angle: Float,
val aperture: Float,
val minSpeed: Float,
val maxSpeed: Float
)

data class ParticleInfo(
val colors: List<Color>,
val width: Float,
val lengthEpsilon: Float,
val lifespan: Int,
val gravity: Float = -60.8f
)

@Composable
fun rememberConfettiState(
origin: Offset,
emitter: Emitter,
particleInfo: ParticleInfo,
particleCount: Int = 50,
delay: Int = 0
): ConfettiState {

val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotta remind me to swap out the infiniteTransition for a normal animation. It's easier to test if you can let it loop, but no reason to leave it like that.

initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
// animation = tween(particleInfo.lifespan, easing = FastOutLinearInEasing, delayMillis = delay),
animation = keyframes {
durationMillis = particleInfo.lifespan
0.0f at 0
0.0f at delay-1
1.0f at delay
0.0f at particleInfo.lifespan with FastOutLinearInEasing
}

)
)

val t by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 10f,
animationSpec = infiniteRepeatable(
animation = tween(particleInfo.lifespan, easing = LinearEasing, delayMillis = delay),
)
)

val projectiles = remember {
List(particleCount) {
Projectile(
origin = origin + Offset(
randomFloat(-emitter.width, emitter.width),
randomFloat(-emitter.height, emitter.height)
),
angle = randomFloat(
emitter.angle - emitter.aperture,
emitter.angle + emitter.aperture
).radians(),
speed = randomFloat(emitter.minSpeed, emitter.maxSpeed),
color = particleInfo.colors.random(),
gravity = particleInfo.gravity
)
}
}

return ConfettiState(
projectiles = projectiles,
time = t,
globalAlpha = alpha,
particleInfo = particleInfo,
emitter = emitter
)
}


fun DrawScope.drawConfetti(state: ConfettiState) {
state.projectiles.forEach {
val prevPos = center + it.position(state.time - state.particleInfo.lengthEpsilon)
val pos = center + it.position(state.time)
drawLine(
it.color.copy(
alpha = state.globalAlpha
), prevPos, pos,
strokeWidth = state.particleInfo.width
)
}
}

@Composable
fun ConfettiCanvas(trigger: Boolean) {
if (!trigger) return

val confettiColors = remember { listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow) }
val confettiState = rememberConfettiState(
Offset.Zero,
emitter = Emitter(
width = 500f, height = 10f,
angle = 270f, aperture = 15f,
minSpeed = 150f, maxSpeed = 300f
),
particleInfo = ParticleInfo(
colors = confettiColors,
width = 8f,
lengthEpsilon = 0.1f,
lifespan = 1000
)
)

Canvas(modifier = Modifier.fillMaxSize()) {
drawConfetti(confettiState)
}
}

@Composable
fun FireworksCanvas(trigger: Boolean) {
if (!trigger) return

val confettiColors = remember { listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow) }
val states = mutableListOf<ConfettiState>()
val n = 10;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semi-colon ;)


for (i in 0..n) {
states += rememberConfettiState(
Comment on lines +161 to +162
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    val states = List(n) { i ->
        rememberConfettiState(

Offset(remap(i.toFloat(), 0f, n.toFloat(), -500f, 500f), -500f),
emitter = Emitter(
width = 0f, height = 0f,
angle = 0f, aperture = 180f,
minSpeed = 50f, maxSpeed = 70f
),
particleInfo = ParticleInfo(
colors = confettiColors,
width = 5f,
lengthEpsilon = 0.5f,
lifespan = 1000,
gravity=-10f
),
particleCount = 10,
delay=randomInt(0, 300)
)
}

Canvas(modifier = Modifier.fillMaxSize()) {
states.forEach {
drawConfetti(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,16 @@ fun PuzzleDetailUi(
}
}


@Composable
fun PuzzleDetailScreen(viewModel: PuzzleDetailViewModel) {
when (val state =
viewModel.viewStates.observeAsState(PuzzleDetailViewState.Loading).value) {
is PuzzleDetailViewState.Loading -> PuzzleDetailLoadingState()
is PuzzleDetailViewState.Success -> {
val gameState = state.boardGameState
// ConfettiCanvas(gameState.activeWordToast != null)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the Confetti canvas! It would be nice if the animation duration wasn't tied to word toast length

FireworksCanvas(gameState.activeWordToast != null)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to put this after the dialog code in this block so the confetti renders over all the other UI elements

if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
PuzzleBoardLandscape(
gameState,
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.plastix.buzz.util

import kotlin.math.PI
import kotlin.random.Random

fun Float.radians(): Float {
return (this / 180f * PI).toFloat()
}

fun randomFloat(min: Float = 0f, max: Float = 1f): Float {
return min + Random.nextFloat() * (max - min)
}

fun randomInt(min: Int = 0, max: Int = 1): Int {
return Random.nextInt(min, max)
}

fun remap(x: Float, a0: Float, a1: Float, b0: Float, b1: Float): Float {
return b0 + (x - a0) * (b1 - b0) / (a1 - a0)
}