blob: d433d95b80f6a63f11c896974b02b22601c2b3be [file] [log] [blame]
Roman Elizarovdecbc832017-12-26 23:05:20 +03001/*
2 * Copyright 2016-2017 JetBrains s.r.o.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import kotlinx.coroutines.experimental.*
18import kotlinx.html.*
19import kotlinx.html.div
20import kotlinx.html.dom.*
21import kotlinx.html.js.onClickFunction
22import org.w3c.dom.*
23import kotlin.browser.*
24import kotlin.math.*
25
26fun main(args: Array<String>) {
27 println("Starting example application...")
28 document.addEventListener("DOMContentLoaded", {
29 Application().start()
30 })
31}
32
33val Double.px get() = "${this}px"
34
35private fun HTMLElement.setSize(w: Double, h: Double) {
36 with(style) {
37 width = w.px
38 height = h.px
39 }
40}
41
42private fun HTMLElement.setPosition(x: Double, y: Double) {
43 with(style) {
44 left = x.px
45 top = y.px
46 }
47}
48
49@Suppress("DEPRECATION")
Roman Elizarovc69ec992018-01-12 20:22:13 +030050private fun random() = kotlin.js.Math.random()
Roman Elizarovdecbc832017-12-26 23:05:20 +030051
52class Application {
53 private val body get() = document.body!!
54 private val scene get() = document.getElementById("scene") as HTMLElement
55 private val sw = 800.0
56 private val sh = 600.0
57 private var animationIndex = 0
58 private val animations = mutableSetOf<Job>()
59
60 fun start() {
61 body.append.div("content") {
62 h1 {
63 +"Kotlin Coroutines JS Example"
64 }
65 div {
66 button {
67 +"Rect"
68 onClickFunction = { onRect() }
69 }
70 button {
71 +"Circle"
72 onClickFunction = { onCircle() }
73 }
74 button {
75 +"Clear"
76 onClickFunction = { onClear() }
77 }
78 }
79 div {
80 id = "scene"
81 }
82 }
83 scene.setSize(sw, sh)
84 }
85
86 private fun animation(cls: String, size: Double, block: suspend CoroutineScope.(HTMLElement) -> Unit) {
87 val elem = scene.append.div(cls)
88 elem.setSize(size, size)
89 val job = launch {
90 block(elem)
91 }
92 animations += job
93 job.invokeOnCompletion {
94 animations -= job
95 scene.removeChild(elem)
96 }
97 }
98
99 private fun onRect() {
100 val index = ++animationIndex
101 val speed = 0.3
102 val rs = 20.0
103 val turnAfter = 5000.0 // seconds
Roman Elizarovc1c04872018-01-13 11:21:13 +0300104 var maxX = sw - rs
105 val maxY = sh - rs
Roman Elizarovdecbc832017-12-26 23:05:20 +0300106 animation("rect", rs) { rect ->
107 println("Started new 'rect' coroutine #$index")
108 val timer = AnimationTimer()
Roman Elizarovc1c04872018-01-13 11:21:13 +0300109 var turnTime = timer.time + turnAfter
110 val turnTimePhase = turnTime - floor(turnTime / turnAfter) * turnAfter
Roman Elizarovdecbc832017-12-26 23:05:20 +0300111 var vx = speed
112 var vy = speed
113 var x = 0.0
114 var y = 0.0
115 while (true) {
116 val dt = timer.await()
117 x += vx * dt
118 y += vy * dt
Roman Elizarovc1c04872018-01-13 11:21:13 +0300119 if (x > maxX) {
120 x = 2 * maxX - x
Roman Elizarovdecbc832017-12-26 23:05:20 +0300121 vx = -vx
122 }
Roman Elizarovc1c04872018-01-13 11:21:13 +0300123 if (x < 0) {
124 x = -x
125 vx = -vx
126 }
127 if (y > maxY) {
128 y = 2 * maxY - y
129 vy = -vy
130 }
131 if (y < 0) {
132 y = -y
Roman Elizarovdecbc832017-12-26 23:05:20 +0300133 vy = -vy
134 }
135 rect.setPosition(x, y)
Roman Elizarovc1c04872018-01-13 11:21:13 +0300136 if (timer.time >= turnTime) {
137 timer.delay(1000) // pause a bit
Roman Elizarovdecbc832017-12-26 23:05:20 +0300138 // flip direction
139 val t = vx
140 if (random() > 0.5) {
141 vx = vy
142 vy = -t
143 } else {
144 vx = -vy
145 vy = t
146 }
Roman Elizarovc1c04872018-01-13 11:21:13 +0300147 // reset time, but keep turning time phase
148 turnTime = ceil(timer.reset() / turnAfter) * turnAfter + turnTimePhase
Roman Elizarovdecbc832017-12-26 23:05:20 +0300149 println("Delayed #$index for a while at ${timer.time}, resumed and turned")
150 }
151 }
Roman Elizarovdecbc832017-12-26 23:05:20 +0300152 }
153 }
154
155 private fun onCircle() {
156 val index = ++animationIndex
157 val acceleration = 5e-4
158 val initialRange = 0.7
159 val maxSpeed = 0.4
160 val initialSpeed = 0.1
161 val radius = 20.0
162 animation("circle", radius) { circle ->
163 println("Started new 'circle' coroutine #$index")
164 val timer = AnimationTimer()
165 val initialAngle = random() * 2 * PI
166 var vx = sin(initialAngle) * initialSpeed
167 var vy = cos(initialAngle) * initialSpeed
168 var x = (random() * initialRange + (1 - initialRange) / 2) * sw
169 var y = (random() * initialRange + (1 - initialRange) / 2) * sh
170 while (true) {
171 val dt = timer.await()
172 val dx = sw / 2 - x
173 val dy = sh / 2 - y
174 val dn = sqrt(dx * dx + dy * dy)
175 vx += dx / dn * acceleration * dt
176 vy += dy / dn * acceleration * dt
177 val vn = sqrt(vx * vx + vy * vy)
178 val trim = vn.coerceAtMost(maxSpeed)
179 vx = vx / vn * trim
180 vy = vy / vn * trim
181 x += vx * dt
182 y += vy * dt
183 circle.setPosition(x, y)
184 }
185 }
186
187 }
188
189 private fun onClear() {
190 animations.forEach { it.cancel() }
191 }
192}
193
194class AnimationTimer {
195 var time = window.performance.now()
196
197 suspend fun await(): Double {
198 val newTime = window.awaitAnimationFrame()
199 val dt = newTime - time
200 time = newTime
Roman Elizarovc1c04872018-01-13 11:21:13 +0300201 return dt.coerceAtMost(200.0) // at most 200ms
Roman Elizarovdecbc832017-12-26 23:05:20 +0300202 }
203
204 fun reset(): Double {
205 time = window.performance.now()
206 return time
207 }
Roman Elizarovc1c04872018-01-13 11:21:13 +0300208
209 suspend fun delay(i: Int) {
210 var dt = 0.0
211 while (dt < i) {
212 dt += await()
213 }
214 }
Roman Elizarovdecbc832017-12-26 23:05:20 +0300215}