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. 51
      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" 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()))
)
}
}
}

51
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. * 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.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import bouncingBalls.BouncingBallsApp
import org.jetbrains.skiko.wasm.onWasmReady import org.jetbrains.skiko.wasm.onWasmReady
object JsTime : Time { object JsTime : Time {
@ -15,9 +25,46 @@ object JsTime : Time {
fun main() { fun main() {
onWasmReady { onWasmReady {
Window("Falling Balls") { Window("Falling Balls") {
val game = remember { Game(JsTime) } val selectedExample = remember { mutableStateOf(Examples.FallingBalls) }
FallingBalls(game)
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