diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/GridPuzzleVisualisation.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/GridPuzzleVisualisation.kt index 9448fd6f..5a4f6751 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/GridPuzzleVisualisation.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/GridPuzzleVisualisation.kt @@ -26,7 +26,7 @@ import kotlin.math.min import kotlin.random.Random class GridPuzzleVisualisation( - private val uiContext: UiContext, + uiContext: UiContext, defaultAnimationSpec: SpringSpec ) : BaseMotionController( uiContext = uiContext, diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Assemble.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Assemble.kt index a5bd74c5..eaca3ac0 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Assemble.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Assemble.kt @@ -13,13 +13,13 @@ class Assemble( override var mode: Operation.Mode ) : BaseOperation() { - override fun isApplicable(state: GridPuzzleModel.State): Boolean = + override fun isApplicable(state: State): Boolean = true - override fun createFromState(baseLineState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createFromState(baseLineState: State): State = baseLineState - override fun createTargetState(fromState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createTargetState(fromState: State): State = fromState.copy( puzzleMode = GridPuzzleModel.PuzzleMode.ASSEMBLED ) diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Carousel.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Carousel.kt index 43b86ae0..c124a5c6 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Carousel.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Carousel.kt @@ -13,13 +13,13 @@ class Carousel( override var mode: Operation.Mode ) : BaseOperation() { - override fun isApplicable(state: GridPuzzleModel.State): Boolean = + override fun isApplicable(state: State): Boolean = true - override fun createFromState(baseLineState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createFromState(baseLineState: State): State = baseLineState - override fun createTargetState(fromState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createTargetState(fromState: State): State = fromState.copy( puzzleMode = GridPuzzleModel.PuzzleMode.CAROUSEL ) diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Flip.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Flip.kt index ab5fe3bc..c65d62c9 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Flip.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Flip.kt @@ -13,13 +13,13 @@ class Flip( override var mode: Operation.Mode ) : BaseOperation() { - override fun isApplicable(state: GridPuzzleModel.State): Boolean = + override fun isApplicable(state: State): Boolean = true - override fun createFromState(baseLineState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createFromState(baseLineState: State): State = baseLineState - override fun createTargetState(fromState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createTargetState(fromState: State): State = fromState.copy( puzzleMode = GridPuzzleModel.PuzzleMode.FLIPPED ) diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Scatter.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Scatter.kt index 297e045d..237cc105 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Scatter.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/appyx/component/gridpuzzle/operation/Scatter.kt @@ -13,13 +13,13 @@ class Scatter( override var mode: Operation.Mode ) : BaseOperation() { - override fun isApplicable(state: GridPuzzleModel.State): Boolean = + override fun isApplicable(state: State): Boolean = true - override fun createFromState(baseLineState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createFromState(baseLineState: State): State = baseLineState - override fun createTargetState(fromState: GridPuzzleModel.State): GridPuzzleModel.State = + override fun createTargetState(fromState: State): State = fromState.copy( puzzleMode = GridPuzzleModel.PuzzleMode.SCATTERED ) diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/composable/StarFieldMessageBoard.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/composable/StarFieldMessageBoard.kt index 85ec5c96..adb5f968 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/composable/StarFieldMessageBoard.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/composable/StarFieldMessageBoard.kt @@ -3,6 +3,7 @@ package com.bumble.puzzyx.composable import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -10,15 +11,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameMillis -import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.bumble.appyx.interactions.core.ui.math.smoothstep @@ -29,78 +34,92 @@ import com.bumble.puzzyx.model.Entry import com.bumble.puzzyx.model.entries import com.bumble.puzzyx.ui.appyx_dark import kotlinx.coroutines.isActive +import kotlin.math.max +import kotlin.math.roundToInt import kotlin.random.Random @Immutable private data class StarFieldSpecs( val regularStarCounter: Int = 200, + val maxEntries: Int = 12, val speed: Float = 0.1f, - val zNewCoord: Float = 0f, + val zNewCoord: Float = -1f, val zFadeInStart: Float = 0.3f, val zFadeInEnd: Float = 0.4f, val zFadeOutStart: Float = 1.3f, val zFadeOutEnd: Float = 1.4f, +) { + val zOffset = (zFadeOutEnd - zFadeInStart) / maxEntries +} + +private data class Star( + val xCoord: Float = Random.nextDouble(-0.5, 0.5).toFloat(), + val yCoord: Float = Random.nextDouble(-0.5, 0.5).toFloat(), + val zCoord: Float, + val size: Modifier, + val type: StarType, ) @Immutable -private sealed class Star { - abstract val xCoord: Float - abstract val yCoord: Float - abstract val zCoord: Float - - data class EntryStar( - override val xCoord: Float = Random.nextDouble(-1.0, 1.0).toFloat(), - override val yCoord: Float = Random.nextDouble(-1.0, 1.0).toFloat(), - override val zCoord: Float, - val entry: Entry, - ) : Star() - - data class RegularStar( - override val xCoord: Float = Random.nextDouble(-1.0, 1.0).toFloat(), - override val yCoord: Float = Random.nextDouble(-1.0, 1.0).toFloat(), - override val zCoord: Float, - val color: Color, - ) : Star() +private sealed class StarType { + data class RegularType(val color: Color) : StarType() { + companion object { + val size: Modifier = Modifier.size(4.dp) + } + } + + data class EntryType(val entry: Entry) : StarType() { + companion object { + val size: Modifier = Modifier.fillMaxSize(0.15f).aspectRatio(1.5f) + } + } + + fun calcZNewCoord(zFadeInStart: Float, zOffset: Float, maxEntries: Int): Float = + when (this) { + is RegularType -> zFadeInStart + is EntryType -> zFadeInStart - zOffset * max(0, entries.size - maxEntries) + } } @Immutable private data class StarField( val specs: StarFieldSpecs, - val stars: ImmutableList + val stars: ImmutableList, ) { companion object { fun generateStars(starFieldSpecs: StarFieldSpecs): StarField = StarField( specs = starFieldSpecs, - stars = (entryStars(starFieldSpecs) + regularStars(starFieldSpecs)) - .shuffled() - .toImmutableList() + stars = (regularStars(starFieldSpecs) + + entryStars(starFieldSpecs) + ).toImmutableList() ) - private fun entryStars(starFieldSpecs: StarFieldSpecs) = - entries.map { - Star.EntryStar( - zCoord = Random.nextDouble( - from = starFieldSpecs.zNewCoord.toDouble(), - until = starFieldSpecs.zFadeOutEnd.toDouble(), - ).toFloat(), - entry = it, - ) - } - private fun regularStars(starFieldSpecs: StarFieldSpecs) = Array(starFieldSpecs.regularStarCounter) { - Star.RegularStar( + Star( zCoord = Random.nextDouble( from = starFieldSpecs.zNewCoord.toDouble(), until = starFieldSpecs.zFadeOutEnd.toDouble(), ).toFloat(), - color = Color( - red = Random.nextDouble(0.60, 0.66).toFloat(), - green = Random.nextDouble(0.60, 0.66).toFloat(), - blue = Random.nextDouble(0.97, 1.0).toFloat(), + size = StarType.RegularType.size, + type = StarType.RegularType( + color = Color( + red = Random.nextDouble(0.60, 0.66).toFloat(), + green = Random.nextDouble(0.60, 0.66).toFloat(), + blue = Random.nextDouble(0.97, 1.0).toFloat(), + ) ), ) + }.toList() + + private fun entryStars(starFieldSpecs: StarFieldSpecs) = + entries.reversed().mapIndexed { index, entry -> + Star( + zCoord = starFieldSpecs.zFadeInStart - index * starFieldSpecs.zOffset, + size = StarType.EntryType.size, + type = StarType.EntryType(entry = entry), + ) } } } @@ -111,12 +130,19 @@ private fun StarField.update( ): StarField = copy( stars = stars.map { star -> - val zNewCoord = - (star.zCoord + specs.speed * timeInSecs).takeIf { it < specs.zFadeOutEnd } - ?: specs.zNewCoord - when (star) { - is Star.EntryStar -> star.copy(zCoord = zNewCoord) - is Star.RegularStar -> star.copy(zCoord = zNewCoord) + val zUpdatedCoord = star.zCoord + specs.speed * timeInSecs + if (zUpdatedCoord < specs.zFadeOutEnd) { + star.copy(zCoord = zUpdatedCoord) + } else { + star.copy( + xCoord = Random.nextDouble(-0.5, 0.5).toFloat(), + yCoord = Random.nextDouble(-0.5, 0.5).toFloat(), + zCoord = star.type.calcZNewCoord( + specs.zFadeInStart, + specs.zOffset, + specs.maxEntries + ), + ) } }.toImmutableList() ) @@ -155,58 +181,71 @@ private fun StarFieldContent( starField: StarField, modifier: Modifier = Modifier, ) { - Box(modifier = modifier) { - starField.stars.forEach { star -> - val zPos = star.zCoord - val xPos = star.xCoord * zPos - val yPos = star.yCoord * zPos - val alpha = smoothstep(starField.specs.zFadeInStart, starField.specs.zFadeInEnd, zPos) - - smoothstep(starField.specs.zFadeOutStart, starField.specs.zFadeOutEnd, zPos) - - StarContent( - star, - modifier = Modifier - .scale(zPos) - .size(290.dp) - .aspectRatio(1.5f) - .align(BiasAlignment(xPos, yPos)) - .alpha(alpha) - .zIndex(zPos) - ) + var size by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = modifier.onSizeChanged { size = it }, + ) { + starField.stars.forEachIndexed { index, star -> + key(index) { + val zPos = star.zCoord + val xPos = star.xCoord * zPos + val yPos = star.yCoord * zPos + val alpha = starField.specs.calcAlpha(zPos) + if (alpha > 0f) { + StarContent( + star.type, + modifier = Modifier + .scale(zPos) + .then(star.size) + .align(Alignment.Center) + .absoluteOffset { + IntOffset( + x = (size.width * xPos).roundToInt(), + y = (size.height * yPos).roundToInt(), + ) + } + .alpha(alpha) + .zIndex(zPos) + ) + } + } } } } +private fun StarFieldSpecs.calcAlpha(zPos: Float) = + smoothstep(zFadeInStart, zFadeInEnd, zPos) - smoothstep(zFadeOutStart, zFadeOutEnd, zPos) + @Composable private fun StarContent( - star: Star, + type: StarType, modifier: Modifier = Modifier, ) { - when (star) { - is Star.EntryStar -> EntryStarContent(star, modifier) - is Star.RegularStar -> RegularStarContent(star, modifier) + when (type) { + is StarType.RegularType -> RegularStarContent(type.color, modifier) + is StarType.EntryType -> EntryStarContent(type.entry, modifier) } } @Composable private fun EntryStarContent( - star: Star.EntryStar, + entry: Entry, modifier: Modifier = Modifier, ) { EntryCard( - entry = star.entry, + entry = entry, modifier = modifier ) } @Composable private fun RegularStarContent( - star: Star.RegularStar, + color: Color, modifier: Modifier = Modifier, ) { Canvas( modifier = modifier ) { - drawCircle(color = star.color, radius = density * 2f) + drawCircle(color = color, radius = density * 2f) } } diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/model/Entries.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/model/Entries.kt index 185d4252..93921523 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/model/Entries.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/model/Entries.kt @@ -112,7 +112,7 @@ val entries = listOf( .background(color) ) } - ) + ), ) val puzzle1Entries = entries diff --git a/shared/src/commonMain/kotlin/com/bumble/puzzyx/node/app/PuzzyxAppNode.kt b/shared/src/commonMain/kotlin/com/bumble/puzzyx/node/app/PuzzyxAppNode.kt index be6445c9..4b7774a3 100644 --- a/shared/src/commonMain/kotlin/com/bumble/puzzyx/node/app/PuzzyxAppNode.kt +++ b/shared/src/commonMain/kotlin/com/bumble/puzzyx/node/app/PuzzyxAppNode.kt @@ -103,7 +103,7 @@ class PuzzyxAppNode( MessageBoard(modifier) } is StarFieldMessageBoard -> node(buildContext) { modifier -> - AutoPlayScript(initialDelayMs = 5000) { nextScreen() } + AutoPlayScript(initialDelayMs = 15000) { nextScreen() } StarFieldMessageBoard(modifier) } }