blob: 67c6ef04e7c7f3464503369a38dab58fe7aee305 [file] [log] [blame]
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
import kotlinx.coroutines.*
import kotlinx.html.*
import kotlinx.html.div
import kotlinx.html.dom.*
import kotlinx.html.js.onClickFunction
import org.w3c.dom.*
import kotlinx.browser.*
import kotlin.coroutines.*
import kotlin.math.*
import kotlin.random.Random
external fun require(resource: String)
fun main() {
require("style.css")
println("Starting example application...")
document.addEventListener("DOMContentLoaded", {
Application().start()
})
}
val Double.px get() = "${this}px"
private fun HTMLElement.setSize(w: Double, h: Double) {
with(style) {
width = w.px
height = h.px
}
}
private fun HTMLElement.setPosition(x: Double, y: Double) {
with(style) {
left = x.px
top = y.px
}
}
class Application : CoroutineScope {
private val body get() = document.body!!
private val scene get() = document.getElementById("scene") as HTMLElement
private val sw = 800.0
private val sh = 600.0
private var animationIndex = 0
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job
fun start() {
body.append.div("content") {
h1 {
+"Kotlin Coroutines JS Example"
}
div {
button {
+"Rect"
onClickFunction = { onRect() }
}
button {
+"Circle"
onClickFunction = { onCircle() }
}
button {
+"Clear"
onClickFunction = { onClear() }
}
}
div {
id = "scene"
}
}
scene.setSize(sw, sh)
}
private fun animation(cls: String, size: Double, block: suspend CoroutineScope.(HTMLElement) -> Unit) {
val elem = scene.append.div(cls)
elem.setSize(size, size)
val job = launch {
block(elem)
}
job.invokeOnCompletion { scene.removeChild(elem) }
}
private fun onRect() {
val index = ++animationIndex
val speed = 0.3
val rs = 20.0
val turnAfter = 5000.0 // seconds
val maxX = sw - rs
val maxY = sh - rs
animation("rect", rs) { rect ->
println("Started new 'rect' coroutine #$index")
val timer = AnimationTimer()
var turnTime = timer.time + turnAfter
val turnTimePhase = turnTime - floor(turnTime / turnAfter) * turnAfter
var vx = speed
var vy = speed
var x = 0.0
var y = 0.0
while (true) {
val dt = timer.await()
x += vx * dt
y += vy * dt
if (x > maxX) {
x = 2 * maxX - x
vx = -vx
}
if (x < 0) {
x = -x
vx = -vx
}
if (y > maxY) {
y = 2 * maxY - y
vy = -vy
}
if (y < 0) {
y = -y
vy = -vy
}
rect.setPosition(x, y)
if (timer.time >= turnTime) {
timer.delay(1000) // pause a bit
// flip direction
val t = vx
if (Random.nextDouble() > 0.5) {
vx = vy
vy = -t
} else {
vx = -vy
vy = t
}
// reset time, but keep turning time phase
turnTime = ceil(timer.reset() / turnAfter) * turnAfter + turnTimePhase
println("Delayed #$index for a while at ${timer.time}, resumed and turned")
}
}
}
}
private fun onCircle() {
val index = ++animationIndex
val acceleration = 5e-4
val initialRange = 0.7
val maxSpeed = 0.4
val initialSpeed = 0.1
val radius = 20.0
animation("circle", radius) { circle ->
println("Started new 'circle' coroutine #$index")
val timer = AnimationTimer()
val initialAngle = Random.nextDouble() * 2 * PI
var vx = sin(initialAngle) * initialSpeed
var vy = cos(initialAngle) * initialSpeed
var x = (Random.nextDouble() * initialRange + (1 - initialRange) / 2) * sw
var y = (Random.nextDouble() * initialRange + (1 - initialRange) / 2) * sh
while (true) {
val dt = timer.await()
val dx = sw / 2 - x
val dy = sh / 2 - y
val dn = sqrt(dx * dx + dy * dy)
vx += dx / dn * acceleration * dt
vy += dy / dn * acceleration * dt
val vn = sqrt(vx * vx + vy * vy)
val trim = vn.coerceAtMost(maxSpeed)
vx = vx / vn * trim
vy = vy / vn * trim
x += vx * dt
y += vy * dt
circle.setPosition(x, y)
}
}
}
private fun onClear() {
job.cancel()
job = Job()
}
}
class AnimationTimer {
var time = window.performance.now()
suspend fun await(): Double {
val newTime = window.awaitAnimationFrame()
val dt = newTime - time
time = newTime
return dt.coerceAtMost(200.0) // at most 200ms
}
fun reset(): Double {
time = window.performance.now()
return time
}
suspend fun delay(i: Int) {
var dt = 0.0
while (dt < i) {
dt += await()
}
}
}