initial commit
This commit is contained in:
commit
e365a7565d
19 changed files with 17823 additions and 0 deletions
BIN
assets/room.blend
Normal file
BIN
assets/room.blend
Normal file
Binary file not shown.
BIN
assets/room.blend1
Normal file
BIN
assets/room.blend1
Normal file
Binary file not shown.
BIN
assets/room.glb
Normal file
BIN
assets/room.glb
Normal file
Binary file not shown.
30
index.html
Normal file
30
index.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"
|
||||||
|
integrity="sha512-avLcnGxl5mqAX/wIKERdb1gFNkOLHh2W5JNCfJm5OugpEPBz7LNXJJ3BDjjwO00AxEY1MqdNjtEmiYhKC0ld7g=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<!-- <script
|
||||||
|
src="https://cdn.jsdelivr.net/gh/c-frame/aframe-physics-system@v4.2.2/dist/aframe-physics-system.js"></script> -->
|
||||||
|
<script src="/public/js/aframe-physics-system.js"></script>
|
||||||
|
<script src="/src/index.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a-scene physics="restitution: 0; stats: panel" stats tick-time-display="sceneOutputEl: #tickTimerScene"
|
||||||
|
renderer="physicallyCorrectLights: true">
|
||||||
|
<a-assets>
|
||||||
|
<a-asset-item id="room" src="/assets/room.glb"></a-asset-item>
|
||||||
|
</a-assets>
|
||||||
|
<!-- <a-entity gltf-model="#room" static-body></a-entity> -->
|
||||||
|
<a-box scale="2 2 2" position="0 -1 0" static-body></a-box>
|
||||||
|
|
||||||
|
<a-box is-controlled position="0 1.6 0" dynamic-body>
|
||||||
|
<a-entity camera="active: true"></a-entity>
|
||||||
|
</a-box>
|
||||||
|
</a-scene>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
28
key.pem
Normal file
28
key.pem
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCU1tVf88lt1Jl9
|
||||||
|
xk/3xLN9dc4mSegOXvsXa1UidgskfDea6SIuCNHQshOfB1FbByFi2rjXvh4/RWaJ
|
||||||
|
mc+ogBhWdwqIIjJ5IrS+5SSAD2tnZ47mCvEKGFo8rMBCMH+NSeGxw7FTr41o9ahz
|
||||||
|
2lD7gABlNNRz/X7KVZMjYwRU/l3RFDuR1Aa71o6adBa6cGwS08O4vylSKmuMWifs
|
||||||
|
u6ki651vtTg5K4W9Sxcqe+LHV+mok/U4+29GyR1fM6yTl6d3wK0bYrFiZgNmigel
|
||||||
|
lKgjPwwmMAr0R8w52boTi0chLBiD8AYfVieiPNrIJalgZO6la/56ec3uzUN0lpMs
|
||||||
|
/pAOKYcHAgMBAAECggEAM9O0/BLzNnPbuMCn0uJjD9JIJOwCSpLHQynkPLo2+g7o
|
||||||
|
+9+nP7lhehawH2WYbiaG+RmpWLKCbMy5PjvZY9aq4tzTxYCG68hqgLhgo9j6sMiO
|
||||||
|
KUtxEG9XaJ5M2h9yItwxo0k9w6KzDnPQIvtHzLOPL66cNgF0t3bh4+YUOZZTS7yE
|
||||||
|
ZwPQ15d/7IxSrYfqkQRv0nlTJnkhB6tHNACHIan+sAvBps2NahKt7iOZzwLcrbKT
|
||||||
|
tPF3qrZ3xJmcxI8fSfzFPAmjYH+zywr/HgqtIl9ivOuIQGjN7lyiSQVOsOiBTYhw
|
||||||
|
RT6YLpKUJt9b/79/yBvctB9tYMzmkT8h7Sp1HuvYJQKBgQDF++jhuxux8KI5ZV6r
|
||||||
|
CXH/BsNlEnzB22S8BLpXF1oZ2KEofa0+7m89/xl5zAosxnOVJB+Fh5h3VD7sPk3Q
|
||||||
|
Rq+0we7kdwOpumu/MBZeeCI3mrzewhILvTEWjamG0g76HrIs4hRqH7V0nrcp1D1f
|
||||||
|
WMtLC69gC+OsfW4LwhdBG3mHtQKBgQDAdD7MdGlbhumJoBasCv3c3vomWi7u6gEY
|
||||||
|
Yp7owyNZd1Zz77XwEyWvX5CEPua83xVh/QH1J0wJm3TM6vXDdT+fi15B3YvAqXH2
|
||||||
|
ODB45UQ0Qj0BMsDfnt90K424dDtavJC2YglWSpT29tDjEn1qkJiMK4VIFbcwVJoU
|
||||||
|
wVbj9TTRSwKBgQCv28wHPfEixHEEdIN7f19PvU0waRSCCYPX8ocmNurkjGTSF5gc
|
||||||
|
jyQmuIKDinb7QKeIMVcCf+gxDpb5t3nc5/zInX66VvDdAWcVovwrm6AhLgRYBaCL
|
||||||
|
5thEhT6xL8L37u4rKgIiJdpDJXvi3tBI8hFZrp5vvP/VxnBKZ3PKdEImBQKBgQCK
|
||||||
|
8yLoEOCEkZqJoAjefC0fy+mFyTT7j/3scj/TRBiAnrXPjPGp/NkHjpLTuGp1UQeq
|
||||||
|
MLa6Hn46rX+d98IrNbsS4NfT7DXbiztZjuYIU1dz96L9+3vfuGATCuMOmvTDpUNC
|
||||||
|
OvQM1lpJGJhmadIdH3rUMRPQoMUbYunANHscqMxSkwKBgQDE5u38mdqv3k4xb0Wf
|
||||||
|
wgmz0P9GA3C786toADZJXoPgg5f4hSAZJ0DcShD6ThRa/X3XwcrXqAYCeaD+wp4j
|
||||||
|
iGFNh8MamnqesumEZLagDbbc8QyRPmYRQZ1Uip+aFnuC89ceDPoztbK/36V03qMo
|
||||||
|
CuY1EZf360FRN880r3UJ3R1YHQ==
|
||||||
|
-----END PRIVATE KEY-----
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "messenger",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": ""
|
||||||
|
}
|
17054
public/js/aframe-physics-system.js
Normal file
17054
public/js/aframe-physics-system.js
Normal file
File diff suppressed because it is too large
Load diff
49
server.pem
Normal file
49
server.pem
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChRWdIBdkHuR19
|
||||||
|
JevHyDjgIa4H2ZuXaXvDOvjC4XtAvVrNeuYBMC6GPTqH+YgPAQ2E7wWcI90Jy+nb
|
||||||
|
6rfo4o7hH+TyaydWZXagnmuMIYNRBQ7QjrvIsdeK3ZxIOF1u86qarjN24aIp9w/+
|
||||||
|
FRwmEFBoQg7HynjV8A9sij6Rd7zrjWapMh5UiUkS0L4QGaam4WpKTJ+Nxww/IDDx
|
||||||
|
gaKOpSoKwLbanpv33dVy65XUhxkqL6m1s8eCMmgJqS/xjNEndgO+up+yOFOHVSs2
|
||||||
|
5F75F/pYzS5cnYtp8PxYq03l5XOjp7jM3l6JcK0AN5eEm9PK7YVtAoEnJmzO3p1m
|
||||||
|
6+fsJF8vAgMBAAECggEAAZEKGfPF0hZO3minla3YOqtKUN6+bRnLXfnR55BCY8gk
|
||||||
|
VQx/V3kmzjyOVU5EdGNRnWCneob2CrZR6gFHOekjVd0dc01kFpYlHXh2VNSa0Qb4
|
||||||
|
Knyf2DLuXMSBGQ7OL7JXeodU0xxYc4kjKWbWyVB8LZ+0L4NrX9dpIqM9wtBK8r2S
|
||||||
|
e7ha5B/0cFV7BEXlc2fDLxBfGBc0s+Z9wJnoomg0Nc2PJrIvwd1TdU5kxR+nXfPl
|
||||||
|
jUuU5xrr2ALpHcH38Iz0oDxiDoSWbt5iy1UWKdGI9Boxr9BjBC6cMaAVbAgukykm
|
||||||
|
+XMaT/NcYPwyna+k5YFQ1UlakVRmL1SbLSV5Xj0l4QKBgQDNOdpUtRArIQw7xc//
|
||||||
|
IDzaevzGso1yxfbYXtNU4X2mav0NuhpVr5g6F0iR1wmxSDs7afyTKlnmFjLournR
|
||||||
|
lDBffEHMsZz8e40wCkLWuyrq6ZVOlho6ptEQy0o7Sp2M/qItZ7j2L136T5kHTGGo
|
||||||
|
aLP006r7jKF56Kv+tY3Ujsd0LQKBgQDJK6JWXCHoQK3BjdlbCBKhmqhQMebgHK/r
|
||||||
|
b43CmWQ0H7u6t8wKvzypRVzkI04RHqG65qqQ7WJFoNDEaK9Mwqwc6diJVEyivZ/3
|
||||||
|
RDAWem2dr4e0KCtRZkp0zJ41NG8Quuh5pSVhL1TirqfBksF7mCXw7dZIBRiht2sl
|
||||||
|
mPpzXzJuSwKBgAL8xprZD+Uw0/bCyGZUV85uX+OckcaL7y3zI2xm4+daQ1jBzsRU
|
||||||
|
jAi8cgf9c1yrSoFw54ZU5X4d8JvGrBHK8HFTY7nRFBHw0ntSiuqJSvSkddHxCWy1
|
||||||
|
JPHzo579VRHKWx4CFoUu51VPd1DN4dYLwCLeOezzfR5DLs3EYs4nTev5AoGBAJcD
|
||||||
|
f2jO3tFHhGgxAMJ6S+oufD0/SK5iOplFc6hql7MdQ0LGaJCEwJfdLv8/lYH6ebQh
|
||||||
|
JleHK+dS8oUOhNHdrXBDBz8IHkNoj+YBtQuauiDOPNO799CyNiB53iXYh6uKc6ul
|
||||||
|
kmrjKhjEvb/tV/tc2taC5AHKeZQkaXWe4hurkeMDAoGBAJQGeheJ8wON5Cwlgufr
|
||||||
|
WVS3lSpCB6G9G1mbZuXAPrR+SELpneDlCwId+hDMos2GAoUmKQNz0p2JOBn4tn77
|
||||||
|
3iarxaZpdsGT8J4psaim1amIcnj08Xpls/ksrioGcWESqprs+dsxKVv99yB3sK/h
|
||||||
|
m1mTEq6zT9KZ4Q8hs3p07aVp
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDazCCAlOgAwIBAgIUDlkUxhN5QbSER3NibDuBtMOchlAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTAyMjYwOTE3NDRaFw0yNjAy
|
||||||
|
MjYwOTE3NDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQChRWdIBdkHuR19JevHyDjgIa4H2ZuXaXvDOvjC4XtA
|
||||||
|
vVrNeuYBMC6GPTqH+YgPAQ2E7wWcI90Jy+nb6rfo4o7hH+TyaydWZXagnmuMIYNR
|
||||||
|
BQ7QjrvIsdeK3ZxIOF1u86qarjN24aIp9w/+FRwmEFBoQg7HynjV8A9sij6Rd7zr
|
||||||
|
jWapMh5UiUkS0L4QGaam4WpKTJ+Nxww/IDDxgaKOpSoKwLbanpv33dVy65XUhxkq
|
||||||
|
L6m1s8eCMmgJqS/xjNEndgO+up+yOFOHVSs25F75F/pYzS5cnYtp8PxYq03l5XOj
|
||||||
|
p7jM3l6JcK0AN5eEm9PK7YVtAoEnJmzO3p1m6+fsJF8vAgMBAAGjUzBRMB0GA1Ud
|
||||||
|
DgQWBBSmi8GRzzJQKWE8OkGV2pJpn8AliDAfBgNVHSMEGDAWgBSmi8GRzzJQKWE8
|
||||||
|
OkGV2pJpn8AliDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB
|
||||||
|
6v7FIFsF03PORzhdxeK4opzcNHeHHcaI18fpy/sGERhWhewXHmU7Zp+PLyZArIOI
|
||||||
|
glYbDFQcT+L+NwpfQ/jk0hviOhxLlNPW9qADBbhc9zAMfBgimKd7jCdnT/kD/q9n
|
||||||
|
Rvui7NAxmaJ0FwON7a/CmmTa7lJBXtjNHK1Ree7XRQptG3AMCSQUohgMLUsa+rlH
|
||||||
|
acoCdQquEcnIlIOToByTJdK2TUdLn2yVikhVuTN+8SOB4fCPXBLl0DmpeDJ7NcNk
|
||||||
|
MpVmRzc4Jijd4On6kb2CyNSpgfSgCDRiE7fG+Mi7SeCUhgQcO3SlgFrzRwHZKAhT
|
||||||
|
pcrHNSvoXE25MUX6LQuu
|
||||||
|
-----END CERTIFICATE-----
|
18
server.py
Executable file
18
server.py
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||||
|
import ssl
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
port = 4443
|
||||||
|
|
||||||
|
httpd = HTTPServer(("localhost", port), SimpleHTTPRequestHandler)
|
||||||
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
ssl_context.load_cert_chain(Path(__file__).parent / "server.pem")
|
||||||
|
httpd.socket = ssl_context.wrap_socket(
|
||||||
|
httpd.socket,
|
||||||
|
server_side=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Serving on https://localhost:{port}")
|
||||||
|
httpd.serve_forever()
|
24
src/hash.js
Normal file
24
src/hash.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export class Hash {
|
||||||
|
#algorithm
|
||||||
|
#buffer
|
||||||
|
|
||||||
|
constructor(buf, algorithm = 'md5') {
|
||||||
|
this.#algorithm = algorithm
|
||||||
|
this.#buffer = crypto.subtle.digest(algo, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromString(str, algorithm = 'md5') {
|
||||||
|
const enc = new TextEncoder('utf8')
|
||||||
|
const buf = enc.encode(str)
|
||||||
|
return new Hash(buf, algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
get buffer() {
|
||||||
|
return this.#buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return this.#algorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
5
src/index.js
Normal file
5
src/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
AFRAME.registerComponent("is-controlled", {
|
||||||
|
tick(time, delta) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
336
src/input.js
Normal file
336
src/input.js
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
import Buffer from './ring-buffer.js'
|
||||||
|
|
||||||
|
export const Button = Object.freeze({
|
||||||
|
South: 0,
|
||||||
|
East: 1,
|
||||||
|
West: 2,
|
||||||
|
North: 3,
|
||||||
|
BumperLeft: 4,
|
||||||
|
BumperRight: 5,
|
||||||
|
TriggerLeft: 6,
|
||||||
|
TriggerRight: 7,
|
||||||
|
Select: 8,
|
||||||
|
Start: 9,
|
||||||
|
AnalogLeft: 10,
|
||||||
|
AnalogRight: 11,
|
||||||
|
DPadUp: 12,
|
||||||
|
DPadDown: 13,
|
||||||
|
DPadLeft: 14,
|
||||||
|
DPadRight: 15,
|
||||||
|
Logo: 16
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Axis = Object.freeze({
|
||||||
|
LeftX: 0,
|
||||||
|
LeftY: 1,
|
||||||
|
RightX: 2,
|
||||||
|
RightY: 3
|
||||||
|
})
|
||||||
|
|
||||||
|
export const InputState = Object.freeze({
|
||||||
|
Disabled: 0,
|
||||||
|
Waiting: 1,
|
||||||
|
Started: 2,
|
||||||
|
Performed: 3,
|
||||||
|
Canceled: 4
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Device = Object.freeze({
|
||||||
|
Any: 0,
|
||||||
|
KeyboardAndMouse: 1,
|
||||||
|
Gamepad: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
class InputEvent extends CustomEvent {
|
||||||
|
constructor(value, type, source, data) {
|
||||||
|
super('oninput', {
|
||||||
|
detail: {
|
||||||
|
value,
|
||||||
|
type,
|
||||||
|
source,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GamepadEvent extends InputEvent {
|
||||||
|
constructor(value, source) {
|
||||||
|
super(value, Device.Gamepad, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyboardEvent extends InputEvent {
|
||||||
|
constructor(value, source, state, modifiers) {
|
||||||
|
super(value, Device.KeyboardAndMouse, source, {
|
||||||
|
modifiers,
|
||||||
|
state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InputDevice extends EventTarget { }
|
||||||
|
|
||||||
|
class Gamepads extends InputDevice {
|
||||||
|
#indices
|
||||||
|
#raf
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.#indices = new Set()
|
||||||
|
this.#raf = undefined
|
||||||
|
|
||||||
|
window.addEventListener('gamepadconnected', e => {
|
||||||
|
this.#indices.add(e.gamepad.index)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('gamepaddisconnected', e => {
|
||||||
|
this.#indices.delete(e.gamepad.index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static get Button() {
|
||||||
|
return Button
|
||||||
|
}
|
||||||
|
|
||||||
|
static get Axis() {
|
||||||
|
return Axis
|
||||||
|
}
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
this.#poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
cancelAnimationFrame(this.#raf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#poll() {
|
||||||
|
this.#raf = requestAnimationFrame(() => {
|
||||||
|
this.#testInputs()
|
||||||
|
this.#poll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#testInputs() {
|
||||||
|
const gamepads = navigator.getGamepads()
|
||||||
|
for (const index of this.#indices) {
|
||||||
|
const gamepad = gamepads[index]
|
||||||
|
for (const button of gamepad.buttons) {
|
||||||
|
this.dispatchEvent(new GamepadEvent(button, gamepad))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeyState = Object.freeze({
|
||||||
|
Up: 0,
|
||||||
|
Down: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
export class KeyModifier {
|
||||||
|
static Alt = 1
|
||||||
|
static Ctrl = 2
|
||||||
|
static Meta = 4
|
||||||
|
static Shift = 8
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
throw new TypeError("can't use 'new' with an enum")
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromString(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.reduce((acc, x) => acc | KeyModifier.fromString(x), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (value.toLowerCase()) {
|
||||||
|
case "alt": return 1
|
||||||
|
case "ctrl": return 2
|
||||||
|
case "meta": return 4
|
||||||
|
case "shift": return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromEvent(e) {
|
||||||
|
let result = 0
|
||||||
|
|
||||||
|
if (e.altKey) { result |= KeyModifier.Alt }
|
||||||
|
if (e.ctrlKey) { result |= KeyModifier.Ctrl }
|
||||||
|
if (e.metaKey) { result |= KeyModifier.Meta }
|
||||||
|
if (e.shiftKey) { result |= KeyModifier.Shift }
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static has(flags, value) {
|
||||||
|
return flags & value
|
||||||
|
}
|
||||||
|
|
||||||
|
static set(flags, value) {
|
||||||
|
return flags | value
|
||||||
|
}
|
||||||
|
|
||||||
|
static toString(value) {
|
||||||
|
let result = []
|
||||||
|
|
||||||
|
if (value & 1) { result += "Alt" }
|
||||||
|
if (value & 2) { result += "Ctrl" }
|
||||||
|
if (value & 4) { result += "Meta" }
|
||||||
|
if (value & 8) { result += "Shift" }
|
||||||
|
|
||||||
|
return result.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Keyboard extends InputDevice {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
const state = e.repeat ? InputState.Performed : InputState.Started
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new KeyboardEvent(e.code, e, state, KeyModifier.fromEvent(e))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('keyup', e => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new KeyboardEvent(e.code, e, InputState.Canceled, KeyModifier.fromEvent(e))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterProp = (prop, value, xs) => xs.filter(x => x[prop] === value)
|
||||||
|
|
||||||
|
class BaseAction {
|
||||||
|
#values
|
||||||
|
|
||||||
|
constructor(values) {
|
||||||
|
this.#values = new Set(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
get values() {
|
||||||
|
return this.#values
|
||||||
|
}
|
||||||
|
|
||||||
|
get device() {
|
||||||
|
return Device.Any
|
||||||
|
}
|
||||||
|
|
||||||
|
*find(inputs) {
|
||||||
|
for (const value of this.#values) {
|
||||||
|
for (const input of inputs) {
|
||||||
|
if (input.value === value) {
|
||||||
|
yield input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findFirst(inputs) {
|
||||||
|
return this.find(inputs).next()
|
||||||
|
}
|
||||||
|
|
||||||
|
test(input) {
|
||||||
|
return this.findFirst(input) ? true : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeyboardAction extends BaseAction {
|
||||||
|
#modifiers
|
||||||
|
|
||||||
|
constructor(values) {
|
||||||
|
const parts = values.split('+')
|
||||||
|
super(parts.slice(-1))
|
||||||
|
this.#modifiers = KeyModifier.fromString(parts.slice(0, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
get device() {
|
||||||
|
return Device.KeyboardAndMouse
|
||||||
|
}
|
||||||
|
|
||||||
|
test(input) {
|
||||||
|
const filtered = input.filter(v => v.type === this.device && v.state === InputState.Started)
|
||||||
|
for (const match of this.find(filtered)) {
|
||||||
|
if (match.modifiers == this.#modifiers) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GamepadAction extends BaseAction {
|
||||||
|
constructor(values) {
|
||||||
|
super(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
get device() {
|
||||||
|
return Device.Gamepad
|
||||||
|
}
|
||||||
|
|
||||||
|
test(input) {
|
||||||
|
super.test(
|
||||||
|
filterProp('device', this.device, input)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Action {
|
||||||
|
static keyboard(input) {
|
||||||
|
return new KeyboardAction(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
static gamepad(input) {
|
||||||
|
return new GamepadAction(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ActionMap {
|
||||||
|
constructor(devices, bufferSize = 16) {
|
||||||
|
this.buffer = Buffer(bufferSize)
|
||||||
|
this.actions = new Map()
|
||||||
|
this.devices = !devices ? this.initializeDefaults() : devices
|
||||||
|
|
||||||
|
this.devices.forEach(device => device.addEventListener('oninput', this.#handle.bind(this)))
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDefaults() {
|
||||||
|
return [
|
||||||
|
new Keyboard(),
|
||||||
|
new Gamepads()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(action, callback) {
|
||||||
|
this.actions.set(action, callback)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind(action) {
|
||||||
|
this.actions.delete(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
#handle(e) {
|
||||||
|
this.buffer.enqueue(e.detail)
|
||||||
|
this.#test()
|
||||||
|
}
|
||||||
|
|
||||||
|
#test() {
|
||||||
|
for (const [action, callback] of this.actions.entries()) {
|
||||||
|
if (action.test(this.buffer.valueOf())) {
|
||||||
|
this.buffer.clear()
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBuffer() {
|
||||||
|
this.buffer.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
src/pubsub.js
Normal file
22
src/pubsub.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const topics = new Map()
|
||||||
|
|
||||||
|
export const publish = (topic, data) => {
|
||||||
|
topics.has(topic) && topics.get(topic).forEach(sub => {
|
||||||
|
sub(data !== undefined ? data : {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subscribe = (topic, subscription) => {
|
||||||
|
if (!topics.has(topic)) {
|
||||||
|
topics.set(topic, new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
topics.get(topic).add(subscription)
|
||||||
|
|
||||||
|
return {
|
||||||
|
remove: () => {
|
||||||
|
topics.get(topic).delete(subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
66
src/ring-buffer.js
Normal file
66
src/ring-buffer.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
export default (capacity, ctr = Array) => {
|
||||||
|
const buffer = new ctr(capacity)
|
||||||
|
let head = 0
|
||||||
|
let tail = 0
|
||||||
|
let size = 0
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
|
||||||
|
enqueue(item) {
|
||||||
|
if (this.isFull()) {
|
||||||
|
tail = (tail + 1) % capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[head] = item
|
||||||
|
head = (head + 1) % capacity
|
||||||
|
size = Math.min(capacity, size + 1)
|
||||||
|
},
|
||||||
|
dequeue() {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = buffer[tail]
|
||||||
|
tail = (tail + 1) % capacity
|
||||||
|
size--
|
||||||
|
return item
|
||||||
|
},
|
||||||
|
|
||||||
|
peek() {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer[tail]
|
||||||
|
},
|
||||||
|
|
||||||
|
isFull: () => capacity === size,
|
||||||
|
isEmpty: () => size === 0,
|
||||||
|
size() { return size },
|
||||||
|
|
||||||
|
valueOf() {
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const x of this) {
|
||||||
|
result.push(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
head = 0
|
||||||
|
tail = 0
|
||||||
|
size = 0
|
||||||
|
},
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
let i = tail
|
||||||
|
for (let j = 0; j < size; j++) {
|
||||||
|
yield buffer[i]
|
||||||
|
i = (i + 1) % capacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
58
test.html
Normal file
58
test.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script type="module">
|
||||||
|
import van from "https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.5.3.min.js"
|
||||||
|
import * as Tests from '/test/units/index.js'
|
||||||
|
|
||||||
|
const kebab = str => str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (x, ofs) => (ofs ? "-" : "") + x.toLowerCase())
|
||||||
|
const toCss = s => Object.entries(s).reduce((acc, [k, v]) => acc + `${kebab(k)}:${v};`, '')
|
||||||
|
|
||||||
|
const {div, ul, li, pre, p, span, h1} = van.tags
|
||||||
|
|
||||||
|
const Check = '\u2713'
|
||||||
|
const Cross = '\u2717'
|
||||||
|
const pathname = url => new URL(url).pathname
|
||||||
|
|
||||||
|
const Err = err => pre(`${pathname(err.fileName)} ${err.lineNumber}:${err.columnNumber} ${err}`)
|
||||||
|
const Item = (icon, color, item) => li({
|
||||||
|
style: `display: list-item; list-style-type: '${icon}'; color: ${color}`
|
||||||
|
}, item)
|
||||||
|
|
||||||
|
const Pass = ({description}) => Item(Check, 'green', description)
|
||||||
|
const Fail = ({description, error}) => Item(
|
||||||
|
Cross,
|
||||||
|
'red',
|
||||||
|
div(
|
||||||
|
description,
|
||||||
|
Err(error)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const Test = test => {
|
||||||
|
const result = test()
|
||||||
|
return result.success ? Pass(result) : Fail(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Suite = (name, tests) => div(
|
||||||
|
h1(name),
|
||||||
|
ul(...tests.map(Test))
|
||||||
|
)
|
||||||
|
|
||||||
|
const App = () => div(
|
||||||
|
Object.entries(Tests)
|
||||||
|
.map(([name, tests]) => Suite(name, tests.default))
|
||||||
|
)
|
||||||
|
|
||||||
|
document.body.style = toCss({backgroundColor: '#2a2a2a', color: 'white'})
|
||||||
|
|
||||||
|
van.add(document.body, App())
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
48
test/test.js
Normal file
48
test/test.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
export class AssertionError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assert = (value, message = `assertion error: ${value} is not true`) => {
|
||||||
|
assertEq(value, true, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assertEq = (a, b, message = `assertion error: ${a} !== ${b}`) => {
|
||||||
|
if (a === b) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else if (Array.isArray(b) && a.length === b.length) {
|
||||||
|
b.forEach((n, i) => assertEq(a[i], n))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw new AssertionError(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assertCallback = () => {
|
||||||
|
let result = false
|
||||||
|
setTimeout(() => assert(result), 0)
|
||||||
|
return () => result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const it = (description, test) => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
test()
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
description,
|
||||||
|
error: undefined
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
description,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
test/units/buffer.js
Normal file
34
test/units/buffer.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { it, assert, assertEq } from '../test.js'
|
||||||
|
import RingBuffer from '/src/ring-buffer.js'
|
||||||
|
|
||||||
|
const enqueue = (buf, xs) => {
|
||||||
|
if (Array.isArray(xs)) {
|
||||||
|
return xs.forEach(x => enqueue(buf, x))
|
||||||
|
}
|
||||||
|
buf.enqueue(xs)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [
|
||||||
|
it('should enqueue and dequeue items', () => {
|
||||||
|
const buf = RingBuffer(5)
|
||||||
|
enqueue(buf, [0, 1, 2, 3, 4])
|
||||||
|
const result = buf.valueOf()
|
||||||
|
assertEq(result, [0, 1, 2, 3, 4])
|
||||||
|
}),
|
||||||
|
|
||||||
|
it('should wrap on overflow', () => {
|
||||||
|
const buf = RingBuffer(5)
|
||||||
|
enqueue(buf, [0, 1, 2, 3, 4, 5])
|
||||||
|
const result = buf.valueOf()
|
||||||
|
assertEq(result, [1, 2, 3, 4, 5])
|
||||||
|
}),
|
||||||
|
|
||||||
|
it('should be empty on clear', () => {
|
||||||
|
const buf = RingBuffer(5)
|
||||||
|
enqueue(buf, [0, 1, 2, 3, 4])
|
||||||
|
buf.clear()
|
||||||
|
assert(buf.isEmpty(), 'buffer is not empty')
|
||||||
|
assertEq(buf.size(), 0, 'buffer size is greater than 0')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
3
test/units/index.js
Normal file
3
test/units/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * as Buffer from './buffer.js'
|
||||||
|
export * as Input from './input.js'
|
||||||
|
|
36
test/units/input.js
Normal file
36
test/units/input.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { Action } from '../../src/input.js'
|
||||||
|
import { it, assert, assertCallback } from '../test.js'
|
||||||
|
import { ActionMap, Button } from '/src/input.js'
|
||||||
|
|
||||||
|
const dispatch = (name, event) => {
|
||||||
|
document.dispatchEvent(name, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyEvent = (type, code, args = {}) => {
|
||||||
|
dispatch(new KeyboardEvent(type, { code, ...args }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const pressKey = (code, args) => {
|
||||||
|
keyEvent('keydown', code, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseKey = (code, args) => {
|
||||||
|
keyEvent('keyup', code, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [
|
||||||
|
it('should detect keyboard presses', () => {
|
||||||
|
const map = new ActionMap()
|
||||||
|
map.bind(Action.keyboard('KeyV'), assertCallback())
|
||||||
|
pressKey('KeyV')
|
||||||
|
|
||||||
|
map.bind(Action.keyboard('Ctrl+Shift+KeyV'), assertCallback())
|
||||||
|
pressKey('KeyV', { ctrlKey: true, shiftKey: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
it('should detect gamepad presses', () => {
|
||||||
|
const map = new ActionMap()
|
||||||
|
map.bind(Action.gamepad([Button.ButtonSouth]), () => console.log('a'))
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
Loading…
Add table
Reference in a new issue