package kha; import js.Browser; import js.Syntax; import js.html.CanvasElement; import js.html.ClipboardEvent; import js.html.DeviceMotionEvent; import js.html.DeviceOrientationEvent; import js.html.KeyboardEvent; import js.html.MouseEvent; import js.html.Touch; import js.html.TouchEvent; import js.html.WebSocket; import js.html.WheelEvent; import js.html.webgl.GL; import kha.System; import kha.graphics4.TextureFormat; import kha.input.Gamepad; import kha.input.KeyCode; import kha.input.Keyboard; import kha.input.Mouse; import kha.input.Sensor; import kha.input.Surface; import kha.js.AudioElementAudio; import kha.js.CanvasGraphics; import kha.js.MobileWebAudio; import kha.js.vr.VrInterface; using StringTools; class GamepadStates { public var axes: Array; public var buttons: Array; public function new() { axes = new Array(); buttons = new Array(); } } class SystemImpl { public static var gl: GL; public static var gl2: Bool; public static var halfFloat: Dynamic; public static var anisotropicFilter: Dynamic; public static var depthTexture: Dynamic; public static var drawBuffers: Dynamic; public static var elementIndexUint: Dynamic; @:noCompletion public static var _hasWebAudio: Bool; // public static var graphics(default, null): Graphics; public static var khanvas: CanvasElement; static var options: SystemOptions; public static var mobile: Bool = false; public static var ios: Bool = false; public static var mobileAudioPlaying: Bool = false; static var chrome: Bool = false; static var firefox: Bool = false; public static var safari: Bool = false; public static var ie: Bool = false; public static var insideInputEvent: Bool = false; static public var activeMouseEvent: Null; static public var activeWheelEvent: Null; static public var activeKeyEvent: Null; static var window: Window; public static var estimatedRefreshRate: Int = 60; static function errorHandler(message: String, source: String, lineno: Int, colno: Int, error: Dynamic) { Browser.console.error("Error: " + message); if (error != null) { if (Std.isOfType(error, haxe.Exception)) { var err: haxe.Exception = error; if (err.stack != null) { Browser.console.error("Stack:\n" + err.stack); } } else if (Std.isOfType(error, js.lib.Error)) { var err: js.lib.Error = error; if (err.stack != null) { Browser.console.error("Stack:\n" + err.stack); } } } return true; } public static function init(options: SystemOptions, callback: Window->Void): Void { SystemImpl.options = options; #if kha_debug_html5 Browser.window.onerror = cast errorHandler; var showWindow = Syntax.code("window.electron.showWindow"); showWindow(options.title, options.window.x, options.window.y, options.width, options.height); initSecondStep(callback); chrome = true; mobileAudioPlaying = true; #else mobile = isMobile(); ios = isIOS(); chrome = isChrome(); firefox = isFirefox(); safari = isSafari(); ie = isIE(); mobileAudioPlaying = !mobile && !chrome && !firefox; initSecondStep(callback); #end #if kha_live_reload function openWebSocket(): Void { var host = Browser.location.hostname; if (host == "") host = "localhost"; var port = Std.parseInt(Browser.location.port); if (port == null) port = 80; final ws = new WebSocket('ws://$host:${port + 1}'); ws.onmessage = () -> Browser.location.reload(); } openWebSocket(); #end } static function initSecondStep(callback: Window->Void): Void { init2(options.window.width, options.window.height); initAnimate(callback); } public static function initSensor(): Void { if (ios) { // In Safari for iOS the directions are reversed on axes x, y and z Browser.window.ondevicemotion = function(event: DeviceMotionEvent) { Sensor._changed(0, -event.accelerationIncludingGravity.x, -event.accelerationIncludingGravity.y, -event.accelerationIncludingGravity.z); }; } else { Browser.window.ondevicemotion = function(event: DeviceMotionEvent) { Sensor._changed(0, event.accelerationIncludingGravity.x, event.accelerationIncludingGravity.y, event.accelerationIncludingGravity.z); }; } Browser.window.ondeviceorientation = function(event: DeviceOrientationEvent) { Sensor._changed(1, event.beta, event.gamma, event.alpha); }; } static function isMobile(): Bool { var agent = js.Browser.navigator.userAgent; if (agent.contains("Android") || agent.contains("webOS") || agent.contains("BlackBerry") || agent.contains("Windows Phone")) { return true; } if (isIOS()) return true; return false; } static function isIOS(): Bool { var agent = js.Browser.navigator.userAgent; if (agent.contains("iPhone") || agent.contains("iPad") || agent.contains("iPod")) { return true; } return false; } static function isChrome(): Bool { var agent = js.Browser.navigator.userAgent; if (agent.contains("Chrome")) { return true; } return false; } static function isFirefox(): Bool { var agent = js.Browser.navigator.userAgent; if (agent.contains("Firefox")) { return true; } return false; } static function isSafari(): Bool { var agent = js.Browser.navigator.userAgent; // Chrome has both in UA if (agent.contains("Safari") && !agent.contains("Chrome")) { return true; } return false; } static function isIE(): Bool { var agent = js.Browser.navigator.userAgent; if (agent.contains("MSIE ") || agent.contains("Trident/")) { return true; } return false; } public static function setCanvas(canvas: CanvasElement): Void { khanvas = canvas; } public static function getScreenRotation(): ScreenRotation { return ScreenRotation.RotationNone; } public static function getTime(): Float { final now = js.Browser.window.performance != null ? js.Browser.window.performance.now() : js.lib.Date.now(); return now / 1000; } public static function getSystemId(): String { return "HTML5"; } public static function vibrate(ms: Int): Void { Browser.navigator.vibrate(ms); } public static function getLanguage(): String { final lang = Browser.navigator.language; return lang.substr(0, 2).toLowerCase(); } public static function requestShutdown(): Bool { Browser.window.close(); return true; } static inline var maxGamepads: Int = 4; static var frame: Framebuffer; static var keyboard: Keyboard = null; static var mouse: kha.input.Mouse; static var surface: Surface; static var gamepads: Array; static var gamepadStates: Array; static var minimumScroll: Int = 999; static var mouseX: Int; static var mouseY: Int; static var touchX: Int; static var touchY: Int; static var lastFirstTouchX: Int = 0; static var lastFirstTouchY: Int = 0; static function init2(defaultWidth: Int, defaultHeight: Int, ?backbufferFormat: TextureFormat) { #if !kha_no_keyboard keyboard = new Keyboard(); #end mouse = new kha.input.MouseImpl(); surface = new Surface(); gamepads = new Array(); gamepadStates = new Array(); for (i in 0...maxGamepads) { gamepads[i] = new Gamepad(i); gamepadStates[i] = new GamepadStates(); } js.Browser.window.addEventListener("gamepadconnected", (e) -> { var pad: js.html.Gamepad = e.gamepad; Gamepad.sendConnectEvent(pad.index); for (i in 0...pad.buttons.length) { gamepadStates[pad.index].buttons[i] = 0; } }); js.Browser.window.addEventListener("gamepaddisconnected", (e) -> { Gamepad.sendDisconnectEvent(e.gamepad.index); }); var sysGamepads = getGamepads(); if (sysGamepads != null) { for (i in 0...sysGamepads.length) { var pad = sysGamepads[i]; if (pad != null) { gamepads[pad.index].connected = true; } } } function onCopy(e: ClipboardEvent): Void { if (System.copyListener != null) { var data = System.copyListener(); if (data != null) e.clipboardData.setData("text/plain", data); e.preventDefault(); } } function onCut(e: ClipboardEvent): Void { if (System.cutListener != null) { var data = System.cutListener(); if (data != null) e.clipboardData.setData("text/plain", data); e.preventDefault(); } } function onPaste(e: ClipboardEvent): Void { if (System.pasteListener != null) { System.pasteListener(e.clipboardData.getData("text/plain")); e.preventDefault(); } } var document = Browser.document; document.addEventListener("copy", onCopy); document.addEventListener("cut", onCut); document.addEventListener("paste", onPaste); CanvasImage.init(); Scheduler.init(); loadFinished(defaultWidth, defaultHeight); } public static function copyToClipboard(text: String) { var textArea = Browser.document.createElement("textarea"); untyped textArea.value = text; textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; Browser.document.body.appendChild(textArea); textArea.focus(); untyped textArea.select(); try { Browser.document.execCommand("copy"); } catch (err) {} Browser.document.body.removeChild(textArea); } public static function getMouse(num: Int): Mouse { if (num != 0) return null; return mouse; } public static function getKeyboard(num: Int): Keyboard { if (num != 0) return null; return keyboard; } static function checkGamepad(pad: js.html.Gamepad) { for (i in 0...pad.axes.length) { if (pad.axes[i] != null) { var axis = pad.axes[i]; if (gamepadStates[pad.index].axes[i] != axis) { gamepadStates[pad.index].axes[i] = axis; gamepads[pad.index].sendAxisEvent(i, axis); } } } for (i in 0...pad.buttons.length) { if (pad.buttons[i] != null) { if (gamepadStates[pad.index].buttons[i] != pad.buttons[i].value) { gamepadStates[pad.index].buttons[i] = pad.buttons[i].value; gamepads[pad.index].sendButtonEvent(i, pad.buttons[i].value); } } } if (pad.axes.length <= 4 && pad.buttons.length > 7) { // Fix for the triggers not being axis in html5 gamepadStates[pad.index].axes[4] = pad.buttons[6].value; gamepads[pad.index].sendAxisEvent(4, pad.buttons[6].value); gamepadStates[pad.index].axes[5] = pad.buttons[7].value; gamepads[pad.index].sendAxisEvent(5, pad.buttons[7].value); } } static function getCanvasElement(): CanvasElement { if (khanvas != null) return khanvas; // Only consider custom canvas ID for release builds #if (kha_debug_html5 || !canvas_id) return cast Browser.document.getElementById("khanvas"); #else return cast Browser.document.getElementById(Macros.canvasId()); #end } static function loadFinished(defaultWidth: Int, defaultHeight: Int) { var canvas: CanvasElement = getCanvasElement(); canvas.style.cursor = "default"; var gl: Bool = false; #if kha_webgl try { SystemImpl.gl = canvas.getContext("webgl2", { alpha: false, antialias: options.framebuffer.samplesPerPixel > 1, stencil: true }); // preserveDrawingBuffer: true } ); Warning: preserveDrawingBuffer can cause huge performance issues on mobile browsers SystemImpl.gl.pixelStorei(GL.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1); halfFloat = {HALF_FLOAT_OES: 0x140B}; // GL_HALF_FLOAT depthTexture = {UNSIGNED_INT_24_8_WEBGL: 0x84FA}; // GL_UNSIGNED_INT_24_8 drawBuffers = {COLOR_ATTACHMENT0_WEBGL: GL.COLOR_ATTACHMENT0}; elementIndexUint = true; SystemImpl.gl.getExtension("EXT_color_buffer_float"); SystemImpl.gl.getExtension("OES_texture_float_linear"); SystemImpl.gl.getExtension("OES_texture_half_float_linear"); anisotropicFilter = SystemImpl.gl.getExtension("EXT_texture_filter_anisotropic"); if (anisotropicFilter == null) anisotropicFilter = SystemImpl.gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic"); gl = true; gl2 = true; Shaders.init(); } catch (e:Dynamic) { trace("Could not initialize WebGL 2, falling back to WebGL."); } if (!gl2) { try { SystemImpl.gl = canvas.getContext("experimental-webgl", { alpha: false, antialias: options.framebuffer.samplesPerPixel > 1, stencil: true }); // preserveDrawingBuffer: true } ); WARNING: preserveDrawingBuffer causes huge performance issues (on mobile browser)! SystemImpl.gl.pixelStorei(GL.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1); SystemImpl.gl.getExtension("OES_texture_float"); SystemImpl.gl.getExtension("OES_texture_float_linear"); halfFloat = SystemImpl.gl.getExtension("OES_texture_half_float"); SystemImpl.gl.getExtension("OES_texture_half_float_linear"); depthTexture = SystemImpl.gl.getExtension("WEBGL_depth_texture"); SystemImpl.gl.getExtension("EXT_shader_texture_lod"); SystemImpl.gl.getExtension("OES_standard_derivatives"); anisotropicFilter = SystemImpl.gl.getExtension("EXT_texture_filter_anisotropic"); if (anisotropicFilter == null) anisotropicFilter = SystemImpl.gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic"); drawBuffers = SystemImpl.gl.getExtension("WEBGL_draw_buffers"); elementIndexUint = SystemImpl.gl.getExtension("OES_element_index_uint"); gl = true; Shaders.init(); } catch (e:Dynamic) { trace("Could not initialize WebGL, falling back to ."); } } #end setCanvas(canvas); window = new Window(0, defaultWidth, defaultHeight, canvas); // var widthTransform: Float = canvas.width / Loader.the.width; // var heightTransform: Float = canvas.height / Loader.the.height; // var transform: Float = Math.min(widthTransform, heightTransform); if (gl) { var g4 = new kha.js.graphics4.Graphics(); frame = new Framebuffer(0, null, null, g4); frame.init(new kha.graphics2.Graphics1(frame), new kha.js.graphics4.Graphics2(frame), g4); // new kha.graphics1.Graphics4(frame)); } else { Syntax.code("kha_js_Font.Kravur = kha_Kravur; kha_Kravur = kha_js_Font"); var g2 = new CanvasGraphics(canvas.getContext("2d")); frame = new Framebuffer(0, null, g2, null); frame.init(new kha.graphics2.Graphics1(frame), g2, null); } // canvas.getContext("2d").scale(transform, transform); if (!mobile && kha.audio2.Audio._init()) { SystemImpl._hasWebAudio = true; kha.audio2.Audio1._init(); } else if (mobile) { SystemImpl._hasWebAudio = false; MobileWebAudio._init(); Syntax.code("kha_audio2_Audio1 = kha_js_MobileWebAudio"); } else { SystemImpl._hasWebAudio = false; Syntax.code("kha_audio2_Audio1 = kha_js_AudioElementAudio"); } kha.vr.VrInterface.instance = new VrInterface(); // Autofocus canvas.focus(); #if kha_disable_context_menu canvas.oncontextmenu = function(event: Dynamic) { event.stopPropagation(); event.preventDefault(); } #end canvas.onmousedown = mouseDown; canvas.onmousemove = mouseMove; if (keyboard != null) { canvas.onkeydown = keyDown; canvas.onkeyup = keyUp; canvas.onkeypress = keyPress; } canvas.onblur = onBlur; canvas.onfocus = onFocus; canvas.onmouseleave = mouseLeave; // IE11 does not have canvas.onwheel canvas.addEventListener("wheel", mouseWheel, false); canvas.addEventListener("touchstart", touchDown, false); canvas.addEventListener("touchend", touchUp, false); canvas.addEventListener("touchmove", touchMove, false); canvas.addEventListener("touchcancel", touchCancel, false); Browser.document.addEventListener("dragover", function(event) { event.preventDefault(); }); Browser.document.addEventListener("drop", function(event: js.html.DragEvent) { event.preventDefault(); if (event.dataTransfer != null && event.dataTransfer.files != null) { for (file in event.dataTransfer.files) { LoaderImpl.dropFiles.set(file.name, file); System.dropFiles("drop://" + file.name); } } }); Browser.window.addEventListener("unload", function() { System.shutdown(); }); } static var lastCanvasClientWidth: Int = -1; static var lastCanvasClientHeight: Int = -1; static function initAnimate(callback: Window->Void) { var canvas: CanvasElement = getCanvasElement(); var window: Dynamic = Browser.window; var requestAnimationFrame = window.requestAnimationFrame; if (requestAnimationFrame == null) requestAnimationFrame = window.mozRequestAnimationFrame; if (requestAnimationFrame == null) requestAnimationFrame = window.webkitRequestAnimationFrame; if (requestAnimationFrame == null) requestAnimationFrame = window.msRequestAnimationFrame; var isRefreshRateDetectionActive = false; var lastTimestamp = 0.0; final possibleRefreshRates = [30, 60, 75, 90, 120, 144, 240, 340, 360]; final refreshRatesCounts = [ for (_ in 0...possibleRefreshRates.length) 0 ]; function animate(timestamp) { if (requestAnimationFrame == null) Browser.window.setTimeout(animate, 1000.0 / 60.0); else requestAnimationFrame(animate); var sysGamepads = getGamepads(); if (sysGamepads != null) { for (i in 0...sysGamepads.length) { var pad = sysGamepads[i]; if (pad != null) { checkGamepad(pad); } } } Scheduler.executeFrame(); if (canvas.getContext != null) { #if !kha_html5_disable_automatic_size_adjust if (lastCanvasClientWidth != canvas.clientWidth || lastCanvasClientHeight != canvas.clientHeight) { // canvas.width is the actual backbuffer-size. // canvas.clientWidth (which is read-only and equivalent to // canvas.style.width in pixels) is the output-size // and by default gets scaled by devicePixelRatio. // We revert that scale so backbuffer and output-size // are the same. var scale = Browser.window.devicePixelRatio; var clientWidth = canvas.clientWidth; var clientHeight = canvas.clientHeight; canvas.width = clientWidth; canvas.height = clientHeight; if (scale != 1) { canvas.style.width = Std.int(clientWidth / scale) + "px"; canvas.style.height = Std.int(clientHeight / scale) + "px"; } lastCanvasClientWidth = canvas.clientWidth; lastCanvasClientHeight = canvas.clientHeight; } #end System.render([frame]); if (ie && SystemImpl.gl != null) { // Clear alpha for IE11 SystemImpl.gl.clearColor(1, 1, 1, 1); SystemImpl.gl.colorMask(false, false, false, true); SystemImpl.gl.clear(GL.COLOR_BUFFER_BIT); SystemImpl.gl.colorMask(true, true, true, true); } } if (!isRefreshRateDetectionActive) return; if (lastTimestamp == 0) { lastTimestamp = timestamp; return; } final fps = Math.floor(1000 / (timestamp - lastTimestamp)); if (estimatedRefreshRate < fps) estimatedRefreshRate = fps; lastTimestamp = timestamp; for (i => rate in possibleRefreshRates) { if (fps > rate - 3 && fps < rate + 3) refreshRatesCounts[i]++; } } Browser.window.setTimeout(() -> { isRefreshRateDetectionActive = true; Browser.window.setTimeout(() -> { isRefreshRateDetectionActive = false; // Use 60 hz if window was out of focus and throttled to 1 fps var index = possibleRefreshRates.indexOf(60); var max = 0; for (i => count in refreshRatesCounts) { if (count > max) { max = count; index = i; } } estimatedRefreshRate = possibleRefreshRates[index]; }, 1000); }, 500); Scheduler.start(); requestAnimationFrame(animate); callback(SystemImpl.window); } public static function lockMouse(): Void { untyped if (SystemImpl.khanvas.requestPointerLock) { SystemImpl.khanvas.requestPointerLock(); } else if (SystemImpl.khanvas.mozRequestPointerLock) { SystemImpl.khanvas.mozRequestPointerLock(); } else if (SystemImpl.khanvas.webkitRequestPointerLock) { SystemImpl.khanvas.webkitRequestPointerLock(); } } public static function unlockMouse(): Void { untyped if (document.exitPointerLock) { document.exitPointerLock(); } else if (document.mozExitPointerLock) { document.mozExitPointerLock(); } else if (document.webkitExitPointerLock) { document.webkitExitPointerLock(); } } public static function canLockMouse(): Bool { return Syntax.code("'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document"); } public static function isMouseLocked(): Bool { return Syntax.code("document.pointerLockElement === kha_SystemImpl.khanvas || document.mozPointerLockElement === kha_SystemImpl.khanvas || document.webkitPointerLockElement === kha_SystemImpl.khanvas"); } public static function notifyOfMouseLockChange(func: Void->Void, error: Void->Void): Void { js.Browser.document.addEventListener("pointerlockchange", func, false); js.Browser.document.addEventListener("mozpointerlockchange", func, false); js.Browser.document.addEventListener("webkitpointerlockchange", func, false); js.Browser.document.addEventListener("pointerlockerror", error, false); js.Browser.document.addEventListener("mozpointerlockerror", error, false); js.Browser.document.addEventListener("webkitpointerlockerror", error, false); } public static function removeFromMouseLockChange(func: Void->Void, error: Void->Void): Void { js.Browser.document.removeEventListener("pointerlockchange", func, false); js.Browser.document.removeEventListener("mozpointerlockchange", func, false); js.Browser.document.removeEventListener("webkitpointerlockchange", func, false); js.Browser.document.removeEventListener("pointerlockerror", error, false); js.Browser.document.removeEventListener("mozpointerlockerror", error, false); js.Browser.document.removeEventListener("webkitpointerlockerror", error, false); } static function setMouseXY(event: MouseEvent): Void { var rect = SystemImpl.khanvas.getBoundingClientRect(); var borderWidth = SystemImpl.khanvas.clientLeft; var borderHeight = SystemImpl.khanvas.clientTop; mouseX = Std.int((event.clientX - rect.left - borderWidth) * SystemImpl.khanvas.width / (rect.width - 2 * borderWidth)); mouseY = Std.int((event.clientY - rect.top - borderHeight) * SystemImpl.khanvas.height / (rect.height - 2 * borderHeight)); } static var iosSoundEnabled: Bool = false; static function unlockiOSSound(): Void { if (!ios || iosSoundEnabled) return; var buffer = MobileWebAudio._context.createBuffer(1, 1, 22050); var source = MobileWebAudio._context.createBufferSource(); source.buffer = buffer; source.connect(MobileWebAudio._context.destination); // untyped(if (source.noteOn) source.noteOn(0)); source.start(); source.stop(); iosSoundEnabled = true; } static var soundEnabled = false; static function unlockSound(): Void { if (!soundEnabled) { var context = kha.audio2.Audio._context; if (context == null) { context = Syntax.code("kha_audio2_Audio1._context"); } if (context != null) { context.resume().then(function(c) { soundEnabled = true; }).catchError(function(err) { trace(err); }); } kha.audio2.Audio.wakeChannels(); } unlockiOSSound(); } static function mouseLeave(): Void { mouse.sendLeaveEvent(0); } static function mouseWheel(event: WheelEvent): Void { unlockSound(); insideInputEvent = true; activeWheelEvent = event; switch (Mouse.wheelEventBlockBehavior) { case Full: event.preventDefault(); case Custom(func): if (func(event)) event.preventDefault(); case None: } // Deltamode == 0, deltaY is in pixels. if (event.deltaMode == 0) { if (event.deltaY < 0) { mouse.sendWheelEvent(0, -1); } else if (event.deltaY > 0) { mouse.sendWheelEvent(0, 1); } activeWheelEvent = null; insideInputEvent = false; return; } // Lines if (event.deltaMode == 1) { minimumScroll = Std.int(Math.min(minimumScroll, Math.abs(event.deltaY))); mouse.sendWheelEvent(0, Std.int(event.deltaY / minimumScroll)); activeWheelEvent = null; insideInputEvent = false; return; } activeWheelEvent = null; insideInputEvent = false; return; } static function mouseDown(event: MouseEvent): Void { insideInputEvent = true; activeMouseEvent = event; unlockSound(); setMouseXY(event); if (event.which == 1) { // left button mouse.sendDownEvent(0, 0, mouseX, mouseY); khanvas.ownerDocument.addEventListener("mousemove", documentMouseMove, true); khanvas.ownerDocument.addEventListener("mouseup", mouseLeftUp); } else if (event.which == 2) { // middle button mouse.sendDownEvent(0, 2, mouseX, mouseY); khanvas.ownerDocument.addEventListener("mouseup", mouseMiddleUp); } else if (event.which == 3) { // right button mouse.sendDownEvent(0, 1, mouseX, mouseY); khanvas.ownerDocument.addEventListener("mouseup", mouseRightUp); } else if (event.which == 4) { // backwards sidebutton mouse.sendDownEvent(0, 3, mouseX, mouseY); khanvas.ownerDocument.addEventListener("mouseup", mouseBackUp); } else if (event.which == 5) { // forwards sidebutton mouse.sendDownEvent(0, 4, mouseX, mouseY); khanvas.ownerDocument.addEventListener("mouseup", mouseForwardUp); } activeMouseEvent = null; insideInputEvent = false; } static function mouseLeftUp(event: MouseEvent): Void { unlockSound(); if (event.which != 1) return; insideInputEvent = true; khanvas.ownerDocument.removeEventListener("mouseup", mouseLeftUp); khanvas.ownerDocument.removeEventListener("mousemove", documentMouseMove, true); mouse.sendUpEvent(0, 0, mouseX, mouseY); insideInputEvent = false; } static function mouseMiddleUp(event: MouseEvent): Void { unlockSound(); if (event.which != 2) return; insideInputEvent = true; khanvas.ownerDocument.removeEventListener("mouseup", mouseMiddleUp); mouse.sendUpEvent(0, 2, mouseX, mouseY); insideInputEvent = false; } static function mouseRightUp(event: MouseEvent): Void { unlockSound(); if (event.which != 3) return; insideInputEvent = true; khanvas.ownerDocument.removeEventListener("mouseup", mouseRightUp); mouse.sendUpEvent(0, 1, mouseX, mouseY); insideInputEvent = false; } static function mouseBackUp(event: MouseEvent): Void { unlockSound(); if (event.which != 4) return; insideInputEvent = true; khanvas.ownerDocument.removeEventListener("mouseup", mouseBackUp); mouse.sendUpEvent(0, 3, mouseX, mouseY); insideInputEvent = false; } static function mouseForwardUp(event: MouseEvent): Void { unlockSound(); if (event.which != 5) return; insideInputEvent = true; khanvas.ownerDocument.removeEventListener("mouseup", mouseForwardUp); mouse.sendUpEvent(0, 4, mouseX, mouseY); insideInputEvent = false; } static function documentMouseMove(event: MouseEvent): Void { event.stopPropagation(); mouseMove(event); } static function mouseMove(event: MouseEvent): Void { insideInputEvent = true; activeMouseEvent = event; var lastMouseX = mouseX; var lastMouseY = mouseY; setMouseXY(event); var movementX = event.movementX; var movementY = event.movementY; if (event.movementX == null) { movementX = (untyped event.mozMovementX != null) ? untyped event.mozMovementX : ((untyped event.webkitMovementX != null) ? untyped event.webkitMovementX : (mouseX - lastMouseX)); movementY = (untyped event.mozMovementY != null) ? untyped event.mozMovementY : ((untyped event.webkitMovementY != null) ? untyped event.webkitMovementY : (mouseY - lastMouseY)); } // this ensures same behaviour across browser until they fix it if (firefox) { movementX = Std.int(movementX * Browser.window.devicePixelRatio); movementY = Std.int(movementY * Browser.window.devicePixelRatio); } mouse.sendMoveEvent(0, mouseX, mouseY, movementX, movementY); activeMouseEvent = null; insideInputEvent = false; } static function setTouchXY(touch: Touch): Void { var rect = SystemImpl.khanvas.getBoundingClientRect(); var borderWidth = SystemImpl.khanvas.clientLeft; var borderHeight = SystemImpl.khanvas.clientTop; touchX = Std.int((touch.clientX - rect.left - borderWidth) * SystemImpl.khanvas.width / (rect.width - 2 * borderWidth)); touchY = Std.int((touch.clientY - rect.top - borderHeight) * SystemImpl.khanvas.height / (rect.height - 2 * borderHeight)); } static var iosTouchs: Array = []; static function touchDown(event: TouchEvent): Void { insideInputEvent = true; unlockSound(); event.stopPropagation(); switch (Surface.touchDownEventBlockBehavior) { case Full: event.preventDefault(); case Custom(func): if (func(event)) event.preventDefault(); case None: } var index = 0; for (touch in event.changedTouches) { var id = touch.identifier; if (ios) { id = iosTouchs.indexOf(-1); if (id == -1) id = iosTouchs.length; iosTouchs[id] = touch.identifier; } setTouchXY(touch); mouse.sendDownEvent(0, 0, touchX, touchY); surface.sendTouchStartEvent(id, touchX, touchY); if (index == 0) { lastFirstTouchX = touchX; lastFirstTouchY = touchY; } index++; } insideInputEvent = false; } static function touchUp(event: TouchEvent): Void { insideInputEvent = true; unlockSound(); for (touch in event.changedTouches) { var id = touch.identifier; if (ios) { id = iosTouchs.indexOf(id); iosTouchs[id] = -1; } setTouchXY(touch); mouse.sendUpEvent(0, 0, touchX, touchY); surface.sendTouchEndEvent(id, touchX, touchY); } insideInputEvent = false; } static function touchMove(event: TouchEvent): Void { insideInputEvent = true; unlockSound(); var index = 0; for (touch in event.changedTouches) { setTouchXY(touch); if (index == 0) { var movementX = touchX - lastFirstTouchX; var movementY = touchY - lastFirstTouchY; lastFirstTouchX = touchX; lastFirstTouchY = touchY; mouse.sendMoveEvent(0, touchX, touchY, movementX, movementY); } var id = touch.identifier; if (ios) id = iosTouchs.indexOf(id); surface.sendMoveEvent(id, touchX, touchY); index++; } insideInputEvent = false; } static function touchCancel(event: TouchEvent): Void { insideInputEvent = true; unlockSound(); for (touch in event.changedTouches) { var id = touch.identifier; if (ios) id = iosTouchs.indexOf(id); setTouchXY(touch); mouse.sendUpEvent(0, 0, touchX, touchY); surface.sendTouchEndEvent(id, touchX, touchY); } iosTouchs = []; insideInputEvent = false; } static function onBlur() { // System.pause(); System.background(); } static function onFocus() { // System.resume(); System.foreground(); } static function keycodeToChar(key: String, keycode: Int, shift: Bool): String { if (key != null) { if (key.length == 1) return key; switch (key) { case "Add": return "+"; case "Subtract": return "-"; case "Multiply": return "*"; case "Divide": return "/"; } } switch (keycode) { case 187: if (shift) return "*"; else return "+"; case 188: if (shift) return ";"; else return ","; case 189: if (shift) return "_"; else return "-"; case 190: if (shift) return ":"; else return "."; case 191: if (shift) return "'"; else return "#"; case 226: if (shift) return ">"; else return "<"; case 106: return "*"; case 107: return "+"; case 109: return "-"; case 111: return "/"; case 49: if (shift) return "!"; else return "1"; case 50: if (shift) return "\""; else return "2"; case 51: if (shift) return "§"; else return "3"; case 52: if (shift) return "$"; else return "4"; case 53: if (shift) return "%"; else return "5"; case 54: if (shift) return "&"; else return "6"; case 55: if (shift) return "/"; else return "7"; case 56: if (shift) return "("; else return "8"; case 57: if (shift) return ")"; else return "9"; case 48: if (shift) return "="; else return "0"; case 219: if (shift) return "?"; else return "ß"; case 212: if (shift) return "`"; else return "´"; } if (keycode >= 96 && keycode <= 105) { // num block return String.fromCharCode("0".code - 96 + keycode); } if (keycode >= "A".code && keycode <= "Z".code) { if (shift) return String.fromCharCode(keycode); else return String.fromCharCode(keycode - "A".code + "a".code); } return String.fromCharCode(keycode); } static function keyDown(event: KeyboardEvent): Void { insideInputEvent = true; activeKeyEvent = event; unlockSound(); preventDefaultKeyBehavior(event); event.stopPropagation(); // prevent key repeat if (event.repeat) { event.preventDefault(); return; } var keyCode = fixedKeyCode(event); keyboard.sendDownEvent(keyCode); activeKeyEvent = null; insideInputEvent = false; } static function fixedKeyCode(event: KeyboardEvent): KeyCode { return switch (event.keyCode) { case 91, 93: Meta; // left/right in Chrome case 186: Semicolon; case 187: Equals; case 189: HyphenMinus; default: cast event.keyCode; } } static function preventDefaultKeyBehavior(event: KeyboardEvent): Void { switch (Keyboard.keyBehavior) { case Default: defaultKeyBlock(event); case Full: event.preventDefault(); case Custom(func): if (func(cast event.keyCode)) event.preventDefault(); case None: } } static function defaultKeyBlock(e: KeyboardEvent): Void { // block if ctrl key pressed if (e.ctrlKey || e.metaKey) { // except for cut-copy-paste if (e.keyCode == 67 || e.keyCode == 88 || e.keyCode == 86) { return; } // and quit on macOS if (e.metaKey && e.keyCode == 81) { return; } e.preventDefault(); return; } // allow F-keys if (e.keyCode >= 112 && e.keyCode <= 123) return; // allow char keys if (e.key == null || e.key.length == 1) return; e.preventDefault(); } static function keyUp(event: KeyboardEvent): Void { insideInputEvent = true; activeKeyEvent = event; unlockSound(); preventDefaultKeyBehavior(event); event.stopPropagation(); var keyCode = fixedKeyCode(event); keyboard.sendUpEvent(keyCode); activeKeyEvent = null; insideInputEvent = false; } static function keyPress(event: KeyboardEvent): Void { insideInputEvent = true; activeKeyEvent = event; unlockSound(); if (event.which == 0) return; // for Firefox and Safari preventDefaultKeyBehavior(event); event.stopPropagation(); keyboard.sendPressEvent(String.fromCharCode(event.which)); activeKeyEvent = null; insideInputEvent = false; } public static function canSwitchFullscreen(): Bool { return Syntax.code("'fullscreenElement ' in document || 'mozFullScreenElement' in document || 'webkitFullscreenElement' in document || 'msFullscreenElement' in document "); } public static function notifyOfFullscreenChange(func: Void->Void, error: Void->Void): Void { js.Browser.document.addEventListener("fullscreenchange", func, false); js.Browser.document.addEventListener("mozfullscreenchange", func, false); js.Browser.document.addEventListener("webkitfullscreenchange", func, false); js.Browser.document.addEventListener("MSFullscreenChange", func, false); js.Browser.document.addEventListener("fullscreenerror", error, false); js.Browser.document.addEventListener("mozfullscreenerror", error, false); js.Browser.document.addEventListener("webkitfullscreenerror", error, false); js.Browser.document.addEventListener("MSFullscreenError", error, false); } public static function removeFromFullscreenChange(func: Void->Void, error: Void->Void): Void { js.Browser.document.removeEventListener("fullscreenchange", func, false); js.Browser.document.removeEventListener("mozfullscreenchange", func, false); js.Browser.document.removeEventListener("webkitfullscreenchange", func, false); js.Browser.document.removeEventListener("MSFullscreenChange", func, false); js.Browser.document.removeEventListener("fullscreenerror", error, false); js.Browser.document.removeEventListener("mozfullscreenerror", error, false); js.Browser.document.removeEventListener("webkitfullscreenerror", error, false); js.Browser.document.removeEventListener("MSFullscreenError", error, false); } public static function setKeepScreenOn(on: Bool): Void {} public static function loadUrl(url: String): Void { js.Browser.window.open(url, "_blank"); } public static function getGamepadId(index: Int): String { var sysGamepads = getGamepads(); if (sysGamepads != null && untyped sysGamepads[index]) { return sysGamepads[index].id; } return "unknown"; } public static function getGamepadVendor(index: Int): String { return "unknown"; } public static function setGamepadRumble(index: Int, leftAmount: Float, rightAmount: Float) {} static function getGamepads(): Array { if (chrome && kha.vr.VrInterface.instance != null && kha.vr.VrInterface.instance.IsVrEnabled()) { return null; // Chrome crashes if navigator.getGamepads() is called when using VR } if (untyped navigator.getGamepads) { return js.Browser.navigator.getGamepads(); } else { return null; } } public static function getPen(num: Int): kha.input.Pen { return null; } public static function safeZone(): Float { return 1.0; } public static function login(): Void {} public static function automaticSafeZone(): Bool { return true; } public static function setSafeZone(value: Float): Void {} public static function unlockAchievement(id: Int): Void {} public static function waitingForLogin(): Bool { return false; } public static function disallowUserChange(): Void {} public static function allowUserChange(): Void {} }