| /* |
| * 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 kotlin.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() |
| } |
| } |
| } |