1 /*
<lambda>null2 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4
5 import kotlinx.coroutines.*
6 import kotlinx.html.*
7 import kotlinx.html.div
8 import kotlinx.html.dom.*
9 import kotlinx.html.js.onClickFunction
10 import org.w3c.dom.*
11 import kotlinx.browser.*
12 import kotlin.coroutines.*
13 import kotlin.math.*
14 import kotlin.random.Random
15
16 external fun require(resource: String)
17
18 fun main() {
19 require("style.css")
20 println("Starting example application...")
21 document.addEventListener("DOMContentLoaded", {
22 Application().start()
23 })
24 }
25
26 val Double.px get() = "${this}px"
27
setSizenull28 private fun HTMLElement.setSize(w: Double, h: Double) {
29 with(style) {
30 width = w.px
31 height = h.px
32 }
33 }
34
HTMLElementnull35 private fun HTMLElement.setPosition(x: Double, y: Double) {
36 with(style) {
37 left = x.px
38 top = y.px
39 }
40 }
41
42 class Application : CoroutineScope {
43 private val body get() = document.body!!
44 private val scene get() = document.getElementById("scene") as HTMLElement
45 private val sw = 800.0
46 private val sh = 600.0
47 private var animationIndex = 0
48 private var job = Job()
49 override val coroutineContext: CoroutineContext
50 get() = job
51
startnull52 fun start() {
53 body.append.div("content") {
54 h1 {
55 +"Kotlin Coroutines JS Example"
56 }
57 div {
58 button {
59 +"Rect"
60 onClickFunction = { onRect() }
61 }
62 button {
63 +"Circle"
64 onClickFunction = { onCircle() }
65 }
66 button {
67 +"Clear"
68 onClickFunction = { onClear() }
69 }
70 }
71 div {
72 id = "scene"
73 }
74 }
75 scene.setSize(sw, sh)
76 }
77
animationnull78 private fun animation(cls: String, size: Double, block: suspend CoroutineScope.(HTMLElement) -> Unit) {
79 val elem = scene.append.div(cls)
80 elem.setSize(size, size)
81 val job = launch {
82 block(elem)
83 }
84
85 job.invokeOnCompletion { scene.removeChild(elem) }
86 }
87
onRectnull88 private fun onRect() {
89 val index = ++animationIndex
90 val speed = 0.3
91 val rs = 20.0
92 val turnAfter = 5000.0 // seconds
93 val maxX = sw - rs
94 val maxY = sh - rs
95 animation("rect", rs) { rect ->
96 println("Started new 'rect' coroutine #$index")
97 val timer = AnimationTimer()
98 var turnTime = timer.time + turnAfter
99 val turnTimePhase = turnTime - floor(turnTime / turnAfter) * turnAfter
100 var vx = speed
101 var vy = speed
102 var x = 0.0
103 var y = 0.0
104 while (true) {
105 val dt = timer.await()
106 x += vx * dt
107 y += vy * dt
108 if (x > maxX) {
109 x = 2 * maxX - x
110 vx = -vx
111 }
112 if (x < 0) {
113 x = -x
114 vx = -vx
115 }
116 if (y > maxY) {
117 y = 2 * maxY - y
118 vy = -vy
119 }
120 if (y < 0) {
121 y = -y
122 vy = -vy
123 }
124 rect.setPosition(x, y)
125 if (timer.time >= turnTime) {
126 timer.delay(1000) // pause a bit
127 // flip direction
128 val t = vx
129 if (Random.nextDouble() > 0.5) {
130 vx = vy
131 vy = -t
132 } else {
133 vx = -vy
134 vy = t
135 }
136 // reset time, but keep turning time phase
137 turnTime = ceil(timer.reset() / turnAfter) * turnAfter + turnTimePhase
138 println("Delayed #$index for a while at ${timer.time}, resumed and turned")
139 }
140 }
141 }
142 }
143
onCirclenull144 private fun onCircle() {
145 val index = ++animationIndex
146 val acceleration = 5e-4
147 val initialRange = 0.7
148 val maxSpeed = 0.4
149 val initialSpeed = 0.1
150 val radius = 20.0
151 animation("circle", radius) { circle ->
152 println("Started new 'circle' coroutine #$index")
153 val timer = AnimationTimer()
154 val initialAngle = Random.nextDouble() * 2 * PI
155 var vx = sin(initialAngle) * initialSpeed
156 var vy = cos(initialAngle) * initialSpeed
157 var x = (Random.nextDouble() * initialRange + (1 - initialRange) / 2) * sw
158 var y = (Random.nextDouble() * initialRange + (1 - initialRange) / 2) * sh
159 while (true) {
160 val dt = timer.await()
161 val dx = sw / 2 - x
162 val dy = sh / 2 - y
163 val dn = sqrt(dx * dx + dy * dy)
164 vx += dx / dn * acceleration * dt
165 vy += dy / dn * acceleration * dt
166 val vn = sqrt(vx * vx + vy * vy)
167 val trim = vn.coerceAtMost(maxSpeed)
168 vx = vx / vn * trim
169 vy = vy / vn * trim
170 x += vx * dt
171 y += vy * dt
172 circle.setPosition(x, y)
173 }
174 }
175
176 }
177
onClearnull178 private fun onClear() {
179 job.cancel()
180 job = Job()
181 }
182 }
183
184 class AnimationTimer {
185 var time = window.performance.now()
186
awaitnull187 suspend fun await(): Double {
188 val newTime = window.awaitAnimationFrame()
189 val dt = newTime - time
190 time = newTime
191 return dt.coerceAtMost(200.0) // at most 200ms
192 }
193
resetnull194 fun reset(): Double {
195 time = window.performance.now()
196 return time
197 }
198
delaynull199 suspend fun delay(i: Int) {
200 var dt = 0.0
201 while (dt < i) {
202 dt += await()
203 }
204 }
205 }
206