jsMain.com.soywiz.korgw.DefaultGameWindowJs.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of korgw Show documentation
Show all versions of korgw Show documentation
Portable UI with accelerated graphics support for Kotlin
package com.soywiz.korgw
import com.soywiz.klock.PerformanceCounter
import com.soywiz.korag.*
import com.soywiz.korag.gl.*
import com.soywiz.korev.*
import com.soywiz.korev.Touch
import com.soywiz.korim.bitmap.*
import com.soywiz.korim.format.*
import com.soywiz.korio.async.*
import com.soywiz.korio.file.*
import com.soywiz.korio.util.*
import com.soywiz.korma.geom.Rectangle
import kotlinx.coroutines.*
import org.w3c.dom.events.*
import org.w3c.dom.events.MouseEvent
import kotlinx.browser.*
import org.w3c.dom.*
import org.w3c.dom.TouchEvent
import kotlin.coroutines.*
private external val navigator: dynamic
open class JsGameWindow : GameWindow() {
override fun runBlockingNoJs(coroutineContext: CoroutineContext, block: suspend () -> T): T {
error("GameWindow.unsafeRunBlocking not implemented on JS")
}
}
open class BrowserCanvasJsGameWindow : JsGameWindow() {
override val ag: AGWebgl = AGWebgl(AGConfig())
val canvas get() = ag.canvas
override val dialogInterface: DialogInterfaceJs = DialogInterfaceJs()
private var isTouchDeviceCache: Boolean? = null
fun is_touch_device(): Boolean {
if (isTouchDeviceCache == null) {
isTouchDeviceCache = try {
document.createEvent("TouchEvent")
true
} catch (e: dynamic) {
false
}
}
return isTouchDeviceCache!!
}
@Suppress("UNUSED_PARAMETER")
override fun updateGamepads() {
try {
if (navigator.getGamepads != null) {
val gamepads = navigator.getGamepads().unsafeCast>()
for (gp in gamePadUpdateEvent.gamepads) gp.connected = false
gamePadUpdateEvent.gamepadsLength = gamepads.length
for (gamepadId in 0 until gamepads.length) {
val controller = gamepads[gamepadId] ?: continue
val gamepad = gamePadUpdateEvent.gamepads.getOrNull(gamepadId) ?: continue
val mapping = knownControllers[controller.id] ?: knownControllers[controller.mapping] ?: StandardGamepadMapping
gamepad.apply {
this.connected = controller.connected
this.index = controller.index
this.name = controller.id
this.mapping = mapping
this.axesLength = controller.axes.length
this.buttonsLength = controller.buttons.length
this.rawButtonsPressed = 0
for (n in 0 until controller.buttons.length) {
val button = controller.buttons[n]
if (button.pressed) this.rawButtonsPressed = this.rawButtonsPressed or (1 shl n)
this.rawButtonsPressure[n] = button.value
}
for (n in 0 until controller.axes.length) {
this.rawAxes[n] = controller.axes[n]
}
}
}
dispatch(gamePadUpdateEvent)
}
} catch (e: dynamic) {
console.error(e)
}
}
override var quality: Quality = Quality.AUTOMATIC
set(value) {
if (field != value) {
field = value
onResized()
}
}
@PublishedApi
internal var canvasRatio = 1.0
private fun onResized() {
isTouchDeviceCache = null
if (isCanvasCreatedAndHandled) {
val scale = quality.computeTargetScale(window.innerWidth, window.innerHeight, ag.devicePixelRatio)
val canvasWidth = (window.innerWidth * scale).toInt()
val canvasHeight = (window.innerHeight * scale).toInt()
canvas.width = canvasWidth
canvas.height = canvasHeight
canvas.style.position = "absolute"
canvas.style.left = "0"
canvas.style.right = "0"
canvas.style.width = "${window.innerWidth}px"
canvas.style.height = "${window.innerHeight}px"
canvasRatio = scale
//ag.resized(canvas.width, canvas.height)
//dispatchReshapeEvent(0, 0, window.innerWidth, window.innerHeight)
} else {
canvasRatio = (canvas.width.toDouble() / canvas.clientWidth.toDouble())
}
//canvasRatio = (canvas.width.toDouble() / canvas.clientWidth.toDouble())
dispatchReshapeEvent(0, 0, canvas.width, canvas.height)
}
inline fun transformEventX(x: Double): Double = x * canvasRatio
inline fun transformEventY(y: Double): Double = y * canvasRatio
private fun keyEvent(me: KeyboardEvent) {
val key = when (me.key) {
"0" -> Key.N0; "1" -> Key.N1; "2" -> Key.N2; "3" -> Key.N3
"4" -> Key.N4; "5" -> Key.N5; "6" -> Key.N6; "7" -> Key.N7
"8" -> Key.N8; "9" -> Key.N9
"a" -> Key.A; "b" -> Key.B; "c" -> Key.C; "d" -> Key.D
"e" -> Key.E; "f" -> Key.F; "g" -> Key.G; "h" -> Key.H
"i" -> Key.I; "j" -> Key.J; "k" -> Key.K; "l" -> Key.L
"m" -> Key.M; "n" -> Key.N; "o" -> Key.O; "p" -> Key.P
"q" -> Key.Q; "r" -> Key.R; "s" -> Key.S; "t" -> Key.T
"u" -> Key.U; "v" -> Key.V; "w" -> Key.W; "x" -> Key.X
"y" -> Key.Y; "z" -> Key.Z
"F1" -> Key.F1; "F2" -> Key.F2; "F3" -> Key.F3; "F4" -> Key.F4
"F5" -> Key.F5; "F6" -> Key.F6; "F7" -> Key.F7; "F8" -> Key.F8
"F9" -> Key.F9; "F10" -> Key.F10; "F11" -> Key.F11; "F12" -> Key.F12
"F13" -> Key.F13; "F14" -> Key.F14; "F15" -> Key.F15; "F16" -> Key.F16
"F17" -> Key.F17; "F18" -> Key.F18; "F19" -> Key.F19; "F20" -> Key.F20
"F21" -> Key.F21; "F22" -> Key.F22; "F23" -> Key.F23; "F24" -> Key.F24
"F25" -> Key.F25
"+" -> Key.PLUS
"-" -> Key.MINUS
"'" -> Key.APOSTROPHE
"\"" -> Key.QUOTE
else -> when (me.code) {
"MetaLeft" -> Key.LEFT_SUPER
"MetaRight" -> Key.RIGHT_SUPER
"ShiftLeft" -> Key.LEFT_SHIFT
"ShiftRight" -> Key.RIGHT_SHIFT
"ControlLeft" -> Key.LEFT_CONTROL
"ControlRight" -> Key.RIGHT_CONTROL
"AltLeft" -> Key.LEFT_ALT
"AltRight" -> Key.RIGHT_ALT
"Space" -> Key.SPACE
"ArrowUp" -> Key.UP
"ArrowDown" -> Key.DOWN
"ArrowLeft" -> Key.LEFT
"ArrowRight" -> Key.RIGHT
"PageUp" -> Key.PAGE_UP
"PageDown" -> Key.PAGE_DOWN
"Home" -> Key.HOME
"End" -> Key.END
"Enter" -> Key.ENTER
"Escape" -> Key.ESCAPE
"Backspace" -> Key.BACKSPACE
"Delete" -> Key.DELETE
"Insert" -> Key.INSERT
"Period" -> Key.PERIOD
"Comma" -> Key.COMMA
"Semicolon" -> Key.SEMICOLON
"Slash" -> Key.SLASH
"Tab" -> Key.TAB
else -> {
if (window.asDynamic().korgwShowUnsupportedKeys) {
console.info("Unsupported key key=${me.key}, code=${me.code}")
}
Key.UNKNOWN
}
}
}
dispatch(keyEvent {
this.type = when (me.type) {
"keydown" -> KeyEvent.Type.DOWN
"keyup" -> KeyEvent.Type.UP
"keypress" -> KeyEvent.Type.TYPE
else -> error("Unsupported event type ${me.type}")
}
this.id = 0
this.keyCode = me.keyCode
this.key = key
this.shift = me.shiftKey
this.ctrl = me.ctrlKey
this.alt = me.altKey
this.meta = me.metaKey
this.character = me.charCode.toChar()
})
// @TODO: preventDefault on all causes keypress to not happen?
if (key == Key.TAB || key.isFunctionKey) {
me.preventDefault()
}
}
// JS TouchEvent contains only active touches (ie. touchend just return the list of non ended-touches)
private fun touchEvent(e: TouchEvent, type: com.soywiz.korev.TouchEvent.Type) {
val canvasBounds = canvas.getBoundingClientRect()
dispatch(touchBuilder.frame(TouchBuilder.Mode.JS, type) {
for (n in 0 until e.touches.length) {
val touch = e.touches.item(n) ?: continue
val touchId = touch.identifier
touch(
id = touchId,
x = transformEventX(touch.clientX.toDouble() - canvasBounds.left),
y = transformEventY(touch.clientY.toDouble() - canvasBounds.top),
force = touch.asDynamic().force.unsafeCast() ?: 1.0,
kind = Touch.Kind.FINGER
)
}
}.also {
//println("touchEvent=$it")
})
}
private fun mouseEvent(e: MouseEvent, type: com.soywiz.korev.MouseEvent.Type, pressingType: com.soywiz.korev.MouseEvent.Type = type) {
val canvasBounds = canvas.getBoundingClientRect()
val tx = transformEventX(e.clientX.toDouble() - canvasBounds.left).toInt()
val ty = transformEventY(e.clientY.toDouble() - canvasBounds.top).toInt()
//console.log("mouseEvent", type.toString(), e.clientX, e.clientY, tx, ty)
mouseEvent {
this.type = if (e.buttons.toInt() != 0) pressingType else type
this.scaleCoords = false
this.id = 0
this.x = tx
this.y = ty
this.button = MouseButton[e.button.toInt()]
this.buttons = e.buttons.toInt()
this.isShiftDown = e.shiftKey
this.isCtrlDown = e.ctrlKey
this.isAltDown = e.altKey
this.isMetaDown = e.metaKey
if (type == com.soywiz.korev.MouseEvent.Type.SCROLL) {
val we = e.unsafeCast()
val mode = when (we.deltaMode) {
WheelEvent.DOM_DELTA_PIXEL -> com.soywiz.korev.MouseEvent.ScrollDeltaMode.PIXEL
WheelEvent.DOM_DELTA_LINE -> com.soywiz.korev.MouseEvent.ScrollDeltaMode.LINE
WheelEvent.DOM_DELTA_PAGE -> com.soywiz.korev.MouseEvent.ScrollDeltaMode.PAGE
else -> com.soywiz.korev.MouseEvent.ScrollDeltaMode.LINE
}
//println("scrollDeltaMode: ${we.deltaMode}: $mode, ${we.deltaX}, ${we.deltaY}, ${we.deltaZ}")
val sensitivity = 0.05
//val sensitivity = 0.1
this.setScrollDelta(
mode,
x = we.deltaX * sensitivity,
y = we.deltaY * sensitivity,
z = we.deltaZ * sensitivity,
)
}
}
// If we are in a touch device, touch events will be dispatched, and then we don't want to emit mouse events, that would be duplicated
if (!is_touch_device() || type == com.soywiz.korev.MouseEvent.Type.SCROLL) {
dispatch(mouseEvent)
}
}
override var title: String
get() = document.title
set(value) { document.title = value }
override val width: Int get() = canvas.clientWidth
override val height: Int get() = canvas.clientHeight
override val bufferWidth: Int get() = canvas.width
override val bufferHeight: Int get() = canvas.height
override var cursor: ICursor = Cursor.DEFAULT
set(value) {
field = value
canvas.style.cursor = when (value) {
is Cursor -> {
when (value) {
Cursor.DEFAULT -> "default"
Cursor.CROSSHAIR -> "crosshair"
Cursor.TEXT -> "text"
Cursor.HAND -> "pointer"
Cursor.MOVE -> "move"
Cursor.WAIT -> "wait"
Cursor.RESIZE_EAST -> "e-resize"
Cursor.RESIZE_WEST -> "w-resize"
Cursor.RESIZE_SOUTH -> "s-resize"
Cursor.RESIZE_NORTH -> "n-resize"
Cursor.RESIZE_NORTH_EAST -> "ne-resize"
Cursor.RESIZE_NORTH_WEST -> "nw-resize"
Cursor.RESIZE_SOUTH_EAST -> "se-resize"
Cursor.RESIZE_SOUTH_WEST -> "sw-resize"
//Cursor.ZOOM_IN -> "zoom-in"
//Cursor.ZOOM_OUT -> "zoom-out"
else -> "default"
}
}
else -> "default"
}
}
override var icon: Bitmap? = null
set(value) {
field = value
if (value != null) {
val link: HTMLLinkElement = document.querySelector("link[rel*='icon']").unsafeCast()
link.type = "image/png"
link.rel = "shortcut icon"
//link.href = "data:image/png;base64," + PNG.encode(value).toBase64()
link.href = value.toHtmlNative().toDataURL()
document.getElementsByTagName("head")[0]?.appendChild(link)
} else {
document.querySelector("link[rel*='icon']")?.remove()
}
}
override var fullscreen: Boolean
get() = document.fullscreenElement != null
set(value) {
if (fullscreen != value) {
kotlin.runCatching {
if (value) {
canvas.requestFullscreen()
} else {
document.exitFullscreen()
}
}
}
}
override var visible: Boolean
get() = canvas.style.visibility == "visible"
set(value) { canvas.style.visibility = if (value) "visible" else "hidden" }
override fun setSize(width: Int, height: Int) {
// Do nothing!
}
internal var loopJob: Job? = null
override fun close(exitCode: Int) {
MainScope().launchImmediately {
loopJob?.cancelAndJoin()
window.close()
}
loopJob = null
}
override suspend fun loop(entry: suspend GameWindow.() -> Unit) {
loopJob = launchImmediately(getCoroutineDispatcherWithCurrentContext()) {
entry()
}
jsFrame(0.0)
}
private lateinit var jsFrame: (Double) -> Unit
init {
window.asDynamic().canvas = canvas
window.asDynamic().ag = ag
window.asDynamic().gl = ag.gl
if (isCanvasCreatedAndHandled) {
document.body?.appendChild(canvas)
document.body?.style?.margin = "0px"
document.body?.style?.padding = "0px"
document.body?.style?.overflowX = "hidden"
document.body?.style?.overflowY = "hidden"
}
canvas.addEventListener("wheel", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.SCROLL) })
canvas.addEventListener("mouseenter", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.ENTER) })
canvas.addEventListener("mouseleave", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.EXIT) })
canvas.addEventListener("mouseover", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.MOVE, com.soywiz.korev.MouseEvent.Type.DRAG) })
canvas.addEventListener("mousemove", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.MOVE, com.soywiz.korev.MouseEvent.Type.DRAG) })
canvas.addEventListener("mouseout", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.EXIT) })
canvas.addEventListener("mouseup", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.UP) })
canvas.addEventListener("mousedown", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.DOWN) })
canvas.addEventListener("click", { mouseEvent(it.unsafeCast(), com.soywiz.korev.MouseEvent.Type.CLICK) })
canvas.addEventListener("touchstart", { touchEvent(it.unsafeCast(), com.soywiz.korev.TouchEvent.Type.START) })
canvas.addEventListener("touchmove", { touchEvent(it.unsafeCast(), com.soywiz.korev.TouchEvent.Type.MOVE) })
canvas.addEventListener("touchend", { touchEvent(it.unsafeCast(), com.soywiz.korev.TouchEvent.Type.END) })
//canvas.addEventListener("touchcancel", { touchEvent(it, com.soywiz.korev.TouchEvent.Type.CANCEL) })
window.addEventListener("keypress", { keyEvent(it.unsafeCast()) })
window.addEventListener("keydown", { keyEvent(it.unsafeCast()) })
window.addEventListener("keyup", { keyEvent(it.unsafeCast()) })
window.addEventListener("gamepadconnected", { e ->
//console.log("gamepadconnected")
val e = e.unsafeCast()
dispatch(gamePadConnectionEvent.apply {
this.type = GamePadConnectionEvent.Type.CONNECTED
this.gamepad = e.gamepad.index
})
})
window.addEventListener("gamepaddisconnected", { e ->
//console.log("gamepaddisconnected")
val e = e.unsafeCast()
dispatch(gamePadConnectionEvent.apply {
this.type = GamePadConnectionEvent.Type.DISCONNECTED
this.gamepad = e.gamepad.index
})
})
window.addEventListener("resize", { onResized() })
canvas.ondragenter = {
dispatchDropfileEvent(DropFileEvent.Type.START, null)
}
canvas.ondragexit = {
dispatchDropfileEvent(DropFileEvent.Type.END, null)
}
canvas.ondragleave = {
dispatchDropfileEvent(DropFileEvent.Type.END, null)
}
canvas.ondragover = {
it.preventDefault()
}
canvas.ondragstart = {
dispatchDropfileEvent(DropFileEvent.Type.START, null)
}
canvas.ondragend = {
dispatchDropfileEvent(DropFileEvent.Type.END, null)
}
canvas.ondrop = {
it.preventDefault()
dispatchDropfileEvent(DropFileEvent.Type.END, null)
val items = it.dataTransfer!!.items
val files = (0 until items.length).mapNotNull { items[it]?.getAsFile()?.toVfs() }
dispatchDropfileEvent(DropFileEvent.Type.DROP, files)
}
onResized()
jsFrame = { step: Double ->
window.requestAnimationFrame(jsFrame) // Execute first to prevent exceptions breaking the loop, not triggering again
frame()
}
}
override val isSoftKeyboardVisible: Boolean
get() = super.isSoftKeyboardVisible
private var softKeyboardInput: HTMLInputElement? = null
private fun ensureSoftKeyboardInput() {
if (softKeyboardInput == null) {
softKeyboardInput = document.createElement("input").unsafeCast()
softKeyboardInput?.id = "softKeyboardInput"
softKeyboardInput?.type = "input"
softKeyboardInput?.style?.let { style ->
style.zIndex = "10000000"
style.position = "absolute"
style.top = "0"
style.left = "0"
style.width = "200px"
style.height = "24px"
style.background = "transparent"
//style.visibility = "hidden"
}
//val softKeyboardInput2 = document.createElement("input").unsafeCast()
//softKeyboardInput2?.id = "softKeyboardInput"
//softKeyboardInput2?.type = "input"
//softKeyboardInput2?.style?.zIndex = "10000000"
//softKeyboardInput2?.style?.position = "absolute"
//softKeyboardInput2?.style?.top = "0"
//softKeyboardInput2?.style?.left = "24px"
//softKeyboardInput2?.style?.width = "200px"
//softKeyboardInput2?.style?.height = "24px"
//softKeyboardInput2?.style?.background = "white"
//document.body?.appendChild(softKeyboardInput2!!)
//enterDebugger()
}
}
override fun setInputRectangle(windowRect: Rectangle) {
ensureSoftKeyboardInput()
softKeyboardInput?.style?.let { style ->
style.left = "${(windowRect.left / canvasRatio)}px"
style.top = "${(windowRect.top / canvasRatio) - 16}px"
style.width = "${(windowRect.width / canvasRatio)}px"
style.font = "32px Arial"
//style.height = "${windowRect.height / canvasRatio}px"
style.height = "1px"
style.opacity = "0"
style.background = "transparent"
style.color = "transparent"
//style.visibility = "hidden"
println("BOUNDS.setInputRectangle:${style.left},${style.top},${style.width},${style.height}")
}
}
override fun showSoftKeyboard(force: Boolean, config: ISoftKeyboardConfig?) {
document.body?.appendChild(softKeyboardInput!!)
softKeyboardInput?.focus()
}
override fun hideSoftKeyboard() {
softKeyboardInput?.blur()
document.body?.removeChild(softKeyboardInput!!)
//canvas.focus()
}
}
private external interface JsArray {
val length: Int
}
private inline operator fun JsArray.get(index: Int): T = this.asDynamic()[index]
private external interface JsGamepadButton {
val value: Double
val pressed: Boolean
}
private external interface JsGamePad {
val axes: JsArray
val buttons: JsArray
val connected: Boolean
val id: String
val index: Int
val mapping: String
val timestamp: Double
}
@JsName("GamepadEvent")
private external interface JsGamepadEvent {
val gamepad: JsGamePad
}
class NodeJsGameWindow : JsGameWindow()
actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = if (OS.isJsNodeJs) NodeJsGameWindow() else BrowserCanvasJsGameWindow()
/*
public external open class TouchEvent(type: String, eventInitDict: MouseEventInit = definedExternally) : UIEvent {
open val shiftKey: Boolean
open val altKey: Boolean
open val ctrlKey: Boolean
open val metaKey: Boolean
open val changedTouches: TouchList
open val touches: TouchList
open val targetTouches: TouchList
}
external class TouchList {
val length: Int
fun item(index: Int): Touch
}
external class Touch {
val identifier: Int
val screenX: Int
val screenY: Int
val clientX: Int
val clientY: Int
val pageX: Int
val pageY: Int
val target: dynamic
}
*/
object Nimbus_111_1420_Safari_GamepadMapping : GamepadMapping() {
override val id = "111-1420-Nimbus"
override fun getButtonIndex(button: GameButton): Int = when (button) {
GameButton.SELECT -> -1
GameButton.START -> -1
GameButton.SYSTEM -> -1
else -> super.getButtonIndex(button)
}
}
val knownControllers = listOf(
StandardGamepadMapping,
Nimbus_111_1420_Safari_GamepadMapping
).associateBy { it.id }
© 2015 - 2025 Weber Informatics LLC | Privacy Policy