Browse Source

add BouncingBalls example to falling-balls-mpp (#1699)

* add BouncingBalls example to falling-balls-mpp

* add new bouncing balls at click position instead of a random coordinates

* review improvements

Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>
pull/1703/head
Oleksandr Karpovich 3 years ago committed by GitHub
parent
commit
479add7c89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      examples/falling-balls-mpp/build.gradle.kts
  2. 191
      examples/falling-balls-mpp/src/commonMain/kotlin/bouncingBalls/BouncingBalls.kt
  3. 47
      examples/falling-balls-mpp/src/jsMain/kotlin/main.js.kt

8
examples/falling-balls-mpp/build.gradle.kts

@ -174,3 +174,11 @@ afterEvaluate {
nodeVersion = "16.0.0"
}
}
// TODO: remove when https://youtrack.jetbrains.com/issue/KT-50778 fixed
project.tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile::class.java).configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-Xir-dce-runtime-diagnostic=log"
)
}

191
examples/falling-balls-mpp/src/commonMain/kotlin/bouncingBalls/BouncingBalls.kt

@ -0,0 +1,191 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package bouncingBalls
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.sin
import kotlin.random.Random
private inline fun Modifier.noRippleClickable(crossinline onClick: (Offset) -> Unit): Modifier =
composed {
clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }) {
}.pointerInput(Unit) {
detectTapGestures(onTap = {
println("tap offset = $it")
onClick(it)
})
}
}
private var areaWidth = 0
private var areaHeight = 0
@Composable
fun BouncingBallsApp(initialBallsCount: Int = 5) {
val items = remember {
val list = mutableStateListOf<BouncingBall>()
list.addAll(generateSequence {
BouncingBall.createBouncingBall()
}.take(initialBallsCount))
list
}
Box(
modifier = Modifier.fillMaxWidth()
.fillMaxHeight()
.border(width = 1.dp, color = Color.Black)
.noRippleClickable {
items += BouncingBall.createBouncingBall(offset = it)
}.onSizeChanged {
areaWidth = it.width
areaHeight = it.height
}
) {
Balls(items)
}
LaunchedEffect(Unit) {
var lastTime = 0L
var dt = 0L
while (true) {
withFrameNanos { time ->
dt = time - lastTime
if (lastTime == 0L) {
dt = 0
}
lastTime = time
items.forEach {
it.recalculate(areaWidth, areaHeight, dt.toFloat())
}
}
}
}
}
@Composable
private fun Balls(items: List<BouncingBall>) {
items.forEachIndexed { ix, ball ->
key(ix) {
Box(
modifier = Modifier
.offset(
x = (ball.circle.x.value - ball.circle.r).dp,
y = (ball.circle.y.value - ball.circle.r).dp
).size((2 * ball.circle.r).dp)
.background(ball.color, CircleShape)
)
}
}
}
private class Circle(
var x: MutableState<Float>,
var y: MutableState<Float>,
val r: Float
) {
constructor(x: Float, y: Float, r: Float) : this(
mutableStateOf(x), mutableStateOf(y), r
)
fun moveCircle(s: Float, angle: Float, width: Int, height: Int, r: Float) {
x.value = (x.value + s * sin(angle)).coerceAtLeast(r).coerceAtMost(width.toFloat() - r)
y.value = (y.value + s * cos(angle)).coerceAtLeast(r).coerceAtMost(height.toFloat() - r)
}
}
private fun calculatePosition(circle: Circle, boundingWidth: Int, boundingHeight: Int): Position {
val southmost = circle.y.value + circle.r
val northmost = circle.y.value - circle.r
val westmost = circle.x.value - circle.r
val eastmost = circle.x.value + circle.r
return when {
southmost >= boundingHeight -> Position.TOUCHES_SOUTH
northmost <= 0 -> Position.TOUCHES_NORTH
eastmost >= boundingWidth -> Position.TOUCHES_EAST
westmost <= 0 -> Position.TOUCHES_WEST
else -> Position.INSIDE
}
}
private enum class Position {
INSIDE,
TOUCHES_SOUTH,
TOUCHES_NORTH,
TOUCHES_WEST,
TOUCHES_EAST
}
private class BouncingBall(
val circle: Circle,
val velocity: Float,
var angle: Double,
val color: Color = Color.Red
) {
fun recalculate(width: Int, height: Int, dt: Float) {
val position = calculatePosition(circle, width, height)
val dtMillis = dt / 1000000
when (position) {
Position.TOUCHES_SOUTH -> angle = PI - angle
Position.TOUCHES_EAST -> angle = -angle
Position.TOUCHES_WEST -> angle = -angle
Position.TOUCHES_NORTH -> angle = PI - angle
Position.INSIDE -> angle
}
circle.moveCircle(
velocity * (dtMillis.coerceAtMost(500f) / 1000),
angle.toFloat(),
width,
height,
circle.r
)
}
companion object {
private val random = Random(100)
private val angles = listOf(PI / 4, -PI / 3, 3 * PI / 4, -PI / 6, -1.1 * PI)
private val colors = listOf(Color.Red, Color.Black, Color.Green, Color.Magenta)
private fun randomOffset(): Offset {
return Offset(
x = random.nextInt(100, 700).toFloat(),
y = random.nextInt(100, 500).toFloat()
)
}
fun createBouncingBall(offset: Offset = randomOffset()): BouncingBall {
return BouncingBall(
circle = Circle(x = offset.x, y = offset.y, r = random.nextInt(10, 50).toFloat()),
velocity = random.nextInt(100, 200).toFloat(),
angle = angles.random(),
color = colors.random().copy(alpha = max(0.3f, random.nextFloat()))
)
}
}
}

47
examples/falling-balls-mpp/src/jsMain/kotlin/main.js.kt

@ -3,9 +3,19 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import androidx.compose.foundation.layout.*
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window
import bouncingBalls.BouncingBallsApp
import org.jetbrains.skiko.wasm.onWasmReady
object JsTime : Time {
@ -15,9 +25,46 @@ object JsTime : Time {
fun main() {
onWasmReady {
Window("Falling Balls") {
val selectedExample = remember { mutableStateOf(Examples.FallingBalls) }
Column(modifier = Modifier.fillMaxSize()) {
ExamplesChooser(selectedExample)
Spacer(modifier = Modifier.height(24.dp))
when (selectedExample.value) {
Examples.FallingBalls -> {
val game = remember { Game(JsTime) }
FallingBalls(game)
}
Examples.BouncingBalls -> {
BouncingBallsApp(10)
}
}
}
}
}
}
@Composable
private fun ExamplesChooser(selected: MutableState<Examples>) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Choose an example: ", fontSize = 16.sp)
Examples.values().forEach {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = selected.value == it, onClick = {
selected.value = it
})
Text(it.name)
}
}
}
}
}
private enum class Examples {
FallingBalls,
BouncingBalls
}

Loading…
Cancel
Save