From 4fd750f39aba6f9c3689a4c70bf3dffc908413f6 Mon Sep 17 00:00:00 2001 From: JEphron Date: Sun, 18 Apr 2021 23:57:28 -0700 Subject: [PATCH 1/2] Party time --- .../io/github/plastix/buzz/detail/Confetti.kt | 72 +++++++++++++++++++ .../plastix/buzz/detail/PuzzleDetailUi.kt | 2 + .../io/github/plastix/buzz/util/MathUtils.kt | 13 ++++ 3 files changed, 87 insertions(+) create mode 100644 app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt create mode 100644 app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt diff --git a/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt b/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt new file mode 100644 index 0000000..39ac480 --- /dev/null +++ b/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt @@ -0,0 +1,72 @@ +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 io.github.plastix.buzz.util.radians +import io.github.plastix.buzz.util.randomFloat +import kotlin.math.cos +import kotlin.math.sin + +data class Projectile(val origin: Offset, val angle: Float, val speed: Float, val color: Color) { + val G = -60.8f; + fun position(t: Float): Offset { + val x = speed * t * cos(angle) + val y = (speed * t * sin(angle)) - (0.5f * G * t * t) + return origin + Offset(x, y) + } +} + +@Composable +fun ConfettiCanvas(trigger: Boolean) { + if (!trigger) return; + + val animationDuration = 1000 + val confettiColors = remember { listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow) } + + val infiniteTransition = rememberInfiniteTransition() + val alpha by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = tween(animationDuration, easing = FastOutLinearInEasing), + ) + ) + + val t by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 10f, + animationSpec = infiniteRepeatable( + animation = tween(animationDuration, easing = LinearEasing), + ) + ) + + val xSpread = 100f + val ySpread = 10f + val angleSpread = 15f; + + val projectiles = remember { + List(50) { + Projectile( + Offset(randomFloat(-xSpread, xSpread), randomFloat(-ySpread, ySpread)), + randomFloat(270f - angleSpread, 270f + angleSpread).radians(), + randomFloat(150f, 300f), + confettiColors.random() + ) + } + } + + Canvas(modifier = Modifier.fillMaxSize()) { + projectiles.forEach { + val prevPos = center + it.position(t - 0.1f) + val pos = center + it.position(t) + drawLine(it.color.copy(alpha = alpha), prevPos, pos, strokeWidth = 8f) + } + } +} diff --git a/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt b/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt index 3485fdc..9820246 100644 --- a/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt +++ b/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt @@ -89,6 +89,7 @@ fun PuzzleDetailUi( } } + @Composable fun PuzzleDetailScreen(viewModel: PuzzleDetailViewModel) { when (val state = @@ -96,6 +97,7 @@ fun PuzzleDetailScreen(viewModel: PuzzleDetailViewModel) { is PuzzleDetailViewState.Loading -> PuzzleDetailLoadingState() is PuzzleDetailViewState.Success -> { val gameState = state.boardGameState + ConfettiCanvas(gameState.activeWordToast != null) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { PuzzleBoardLandscape( gameState, diff --git a/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt b/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt new file mode 100644 index 0000000..cc8d841 --- /dev/null +++ b/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt @@ -0,0 +1,13 @@ +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) +} + From 15510ef1c8b817cd27ebf8c1501bdcc8f9e31834 Mon Sep 17 00:00:00 2001 From: JEphron Date: Mon, 19 Apr 2021 19:40:51 -0700 Subject: [PATCH 2/2] Fireworks --- .../io/github/plastix/buzz/detail/Confetti.kt | 162 +++++++++++++++--- .../plastix/buzz/detail/PuzzleDetailUi.kt | 3 +- .../io/github/plastix/buzz/util/MathUtils.kt | 7 + 3 files changed, 147 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt b/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt index 39ac480..944d2c3 100644 --- a/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt +++ b/app/src/main/java/io/github/plastix/buzz/detail/Confetti.kt @@ -9,33 +9,70 @@ 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 G = -60.8f; +data class Projectile(val origin: Offset, val angle: Float, val speed: Float, val color: Color, val gravity: Float) { fun position(t: Float): Offset { val x = speed * t * cos(angle) - val y = (speed * t * sin(angle)) - (0.5f * G * t * t) + val y = (speed * t * sin(angle)) - (0.5f * gravity * t * t) return origin + Offset(x, y) } } -@Composable -fun ConfettiCanvas(trigger: Boolean) { - if (!trigger) return; +data class ConfettiState( + val projectiles: List, + val time: Float, + val globalAlpha: Float, + val emitter: Emitter, + val particleInfo: ParticleInfo +) - val animationDuration = 1000 - val confettiColors = remember { listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow) } +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, + 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( - initialValue = 1f, + initialValue = 0f, targetValue = 0f, animationSpec = infiniteRepeatable( - animation = tween(animationDuration, easing = FastOutLinearInEasing), +// 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 + } + ) ) @@ -43,30 +80,107 @@ fun ConfettiCanvas(trigger: Boolean) { initialValue = 0f, targetValue = 10f, animationSpec = infiniteRepeatable( - animation = tween(animationDuration, easing = LinearEasing), + animation = tween(particleInfo.lifespan, easing = LinearEasing, delayMillis = delay), ) ) - val xSpread = 100f - val ySpread = 10f - val angleSpread = 15f; - val projectiles = remember { - List(50) { + List(particleCount) { Projectile( - Offset(randomFloat(-xSpread, xSpread), randomFloat(-ySpread, ySpread)), - randomFloat(270f - angleSpread, 270f + angleSpread).radians(), - randomFloat(150f, 300f), - confettiColors.random() + 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() + val n = 10; + + for (i in 0..n) { + states += 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()) { - projectiles.forEach { - val prevPos = center + it.position(t - 0.1f) - val pos = center + it.position(t) - drawLine(it.color.copy(alpha = alpha), prevPos, pos, strokeWidth = 8f) + states.forEach { + drawConfetti(it) } } } diff --git a/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt b/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt index 9820246..24b2a72 100644 --- a/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt +++ b/app/src/main/java/io/github/plastix/buzz/detail/PuzzleDetailUi.kt @@ -97,7 +97,8 @@ fun PuzzleDetailScreen(viewModel: PuzzleDetailViewModel) { is PuzzleDetailViewState.Loading -> PuzzleDetailLoadingState() is PuzzleDetailViewState.Success -> { val gameState = state.boardGameState - ConfettiCanvas(gameState.activeWordToast != null) +// ConfettiCanvas(gameState.activeWordToast != null) + FireworksCanvas(gameState.activeWordToast != null) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { PuzzleBoardLandscape( gameState, diff --git a/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt b/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt index cc8d841..0e2beed 100644 --- a/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt +++ b/app/src/main/java/io/github/plastix/buzz/util/MathUtils.kt @@ -11,3 +11,10 @@ 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) +} \ No newline at end of file