LNXSDK/Kha/Backends/HTML5/kha/SystemImpl.hx
2025-01-22 16:18:30 +01:00

1347 lines
37 KiB
Haxe
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Float>;
public var buttons: Array<Float>;
public function new() {
axes = new Array<Float>();
buttons = new Array<Float>();
}
}
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<MouseEvent>;
static public var activeWheelEvent: Null<WheelEvent>;
static public var activeKeyEvent: Null<KeyboardEvent>;
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<Gamepad>;
static var gamepadStates: Array<GamepadStates>;
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<Gamepad>();
gamepadStates = new Array<GamepadStates>();
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 <canvas>.");
}
}
#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<Int> = [];
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<js.html.Gamepad> {
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 {}
}