kaplay 3000.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/app.ts ADDED
@@ -0,0 +1,906 @@
1
+ // everything related to canvas, game loop and input
2
+
3
+ import type {
4
+ Cursor,
5
+ Key,
6
+ MouseButton,
7
+ GamepadButton,
8
+ GamepadStick,
9
+ GamepadDef,
10
+ KGamePad,
11
+ } from "./types"
12
+
13
+ import {
14
+ Vec2,
15
+ map,
16
+ } from "./math"
17
+
18
+ import {
19
+ EventHandler,
20
+ EventController,
21
+ overload2,
22
+ } from "./utils"
23
+
24
+ import GAMEPAD_MAP from "./gamepad.json"
25
+
26
+ export class ButtonState<T = string> {
27
+ pressed: Set<T> = new Set([])
28
+ pressedRepeat: Set<T> = new Set([])
29
+ released: Set<T> = new Set([])
30
+ down: Set<T> = new Set([])
31
+ update() {
32
+ this.pressed.clear()
33
+ this.released.clear()
34
+ this.pressedRepeat.clear()
35
+ }
36
+ press(btn: T) {
37
+ this.pressed.add(btn)
38
+ this.pressedRepeat.add(btn)
39
+ this.down.add(btn)
40
+ }
41
+ pressRepeat(btn: T) {
42
+ this.pressedRepeat.add(btn)
43
+ }
44
+ release(btn: T) {
45
+ this.down.delete(btn)
46
+ this.pressed.delete(btn)
47
+ this.released.add(btn)
48
+ }
49
+ }
50
+
51
+ class GamepadState {
52
+ buttonState: ButtonState<GamepadButton> = new ButtonState()
53
+ stickState: Map<GamepadStick, Vec2> = new Map()
54
+ }
55
+
56
+ class FPSCounter {
57
+ private dts: number[] = []
58
+ private timer: number = 0
59
+ fps: number = 0
60
+ tick(dt: number) {
61
+ this.dts.push(dt)
62
+ this.timer += dt
63
+ if (this.timer >= 1) {
64
+ this.timer = 0
65
+ this.fps = Math.round(1 / (this.dts.reduce((a, b) => a + b) / this.dts.length))
66
+ this.dts = []
67
+ }
68
+ }
69
+ }
70
+
71
+ export default (opt: {
72
+ canvas: HTMLCanvasElement,
73
+ touchToMouse?: boolean,
74
+ gamepads?: Record<string, GamepadDef>,
75
+ pixelDensity?: number,
76
+ maxFPS?: number,
77
+ }) => {
78
+
79
+ if (!opt.canvas) {
80
+ throw new Error("Please provide a canvas")
81
+ }
82
+
83
+ const state = {
84
+ canvas: opt.canvas,
85
+ loopID: null as null | number,
86
+ stopped: false,
87
+ dt: 0,
88
+ time: 0,
89
+ realTime: 0,
90
+ fpsCounter: new FPSCounter(),
91
+ timeScale: 1,
92
+ skipTime: false,
93
+ numFrames: 0,
94
+ mousePos: new Vec2(0),
95
+ mouseDeltaPos: new Vec2(0),
96
+ keyState: new ButtonState<Key>(),
97
+ mouseState: new ButtonState<MouseButton>(),
98
+ mergedGamepadState: new GamepadState(),
99
+ gamepadStates: new Map<number, GamepadState>(),
100
+ gamepads: [] as KGamePad[],
101
+ charInputted: [],
102
+ isMouseMoved: false,
103
+ lastWidth: opt.canvas.offsetWidth,
104
+ lastHeight: opt.canvas.offsetHeight,
105
+ events: new EventHandler<{
106
+ mouseMove: [],
107
+ mouseDown: [MouseButton],
108
+ mousePress: [MouseButton],
109
+ mouseRelease: [MouseButton],
110
+ charInput: [string],
111
+ keyPress: [Key],
112
+ keyDown: [Key],
113
+ keyPressRepeat: [Key],
114
+ keyRelease: [Key],
115
+ touchStart: [Vec2, Touch],
116
+ touchMove: [Vec2, Touch],
117
+ touchEnd: [Vec2, Touch],
118
+ gamepadButtonDown: [string],
119
+ gamepadButtonPress: [string],
120
+ gamepadButtonRelease: [string],
121
+ gamepadStick: [string, Vec2],
122
+ gamepadConnect: [KGamePad],
123
+ gamepadDisconnect: [KGamePad],
124
+ scroll: [Vec2],
125
+ hide: [],
126
+ show: [],
127
+ resize: [],
128
+ input: [],
129
+ }>(),
130
+ }
131
+
132
+ function dt() {
133
+ return state.dt * state.timeScale
134
+ }
135
+
136
+ function time() {
137
+ return state.time
138
+ }
139
+
140
+ function fps() {
141
+ return state.fpsCounter.fps
142
+ }
143
+
144
+ function numFrames() {
145
+ return state.numFrames
146
+ }
147
+
148
+ function screenshot(): string {
149
+ return state.canvas.toDataURL()
150
+ }
151
+
152
+ function setCursor(c: Cursor): void {
153
+ state.canvas.style.cursor = c
154
+ }
155
+
156
+ function getCursor(): Cursor {
157
+ return state.canvas.style.cursor
158
+ }
159
+
160
+ function setCursorLocked(b: boolean): void {
161
+ if (b) {
162
+ try {
163
+ const res = state.canvas.requestPointerLock() as unknown as Promise<void>
164
+ if (res.catch) {
165
+ res.catch((e) => console.error(e))
166
+ }
167
+ } catch (e) {
168
+ console.error(e)
169
+ }
170
+ } else {
171
+ document.exitPointerLock()
172
+ }
173
+ }
174
+
175
+ function isCursorLocked(): boolean {
176
+ return !!document.pointerLockElement
177
+ }
178
+
179
+ // wrappers around full screen functions to work across browsers
180
+ function enterFullscreen(el: HTMLElement) {
181
+ if (el.requestFullscreen) el.requestFullscreen()
182
+ // @ts-ignore
183
+ else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen()
184
+ }
185
+
186
+ function exitFullscreen() {
187
+ if (document.exitFullscreen) document.exitFullscreen()
188
+ // @ts-ignore
189
+ else if (document.webkitExitFullScreen) document.webkitExitFullScreen()
190
+ }
191
+
192
+ function getFullscreenElement(): Element | void {
193
+ return document.fullscreenElement
194
+ // @ts-ignore
195
+ || document.webkitFullscreenElement
196
+ }
197
+
198
+ function setFullscreen(f: boolean = true) {
199
+ if (f) {
200
+ enterFullscreen(state.canvas)
201
+ } else {
202
+ exitFullscreen()
203
+ }
204
+ }
205
+
206
+ function isFullscreen(): boolean {
207
+ return Boolean(getFullscreenElement())
208
+ }
209
+
210
+ function quit() {
211
+ state.stopped = true
212
+ for (const name in canvasEvents) {
213
+ state.canvas.removeEventListener(name, canvasEvents[name])
214
+ }
215
+ for (const name in docEvents) {
216
+ document.removeEventListener(name, docEvents[name])
217
+ }
218
+ for (const name in winEvents) {
219
+ window.removeEventListener(name, winEvents[name])
220
+ }
221
+ resizeObserver.disconnect()
222
+ }
223
+
224
+ function run(action: () => void) {
225
+
226
+ if (state.loopID !== null) {
227
+ cancelAnimationFrame(state.loopID)
228
+ }
229
+
230
+ let accumulatedDt = 0
231
+
232
+ const frame = (t: number) => {
233
+
234
+ if (state.stopped) return
235
+
236
+ // TODO: allow background actions?
237
+ if (document.visibilityState !== "visible") {
238
+ state.loopID = requestAnimationFrame(frame)
239
+ return
240
+ }
241
+
242
+ const loopTime = t / 1000
243
+ const realDt = loopTime - state.realTime
244
+ const desiredDt = opt.maxFPS ? 1 / opt.maxFPS : 0
245
+
246
+ state.realTime = loopTime
247
+ accumulatedDt += realDt
248
+
249
+ if (accumulatedDt > desiredDt) {
250
+ if (!state.skipTime) {
251
+ state.dt = accumulatedDt
252
+ state.time += dt()
253
+ state.fpsCounter.tick(state.dt)
254
+ }
255
+ accumulatedDt = 0
256
+ state.skipTime = false
257
+ state.numFrames++
258
+ processInput()
259
+ action()
260
+ resetInput()
261
+ }
262
+
263
+ state.loopID = requestAnimationFrame(frame)
264
+
265
+ }
266
+
267
+ frame(0)
268
+
269
+ }
270
+
271
+ function isTouchscreen() {
272
+ return ("ontouchstart" in window) || navigator.maxTouchPoints > 0
273
+ }
274
+
275
+ function mousePos(): Vec2 {
276
+ return state.mousePos.clone()
277
+ }
278
+
279
+ function mouseDeltaPos(): Vec2 {
280
+ return state.mouseDeltaPos.clone()
281
+ }
282
+
283
+ function isMousePressed(m: MouseButton = "left"): boolean {
284
+ return state.mouseState.pressed.has(m)
285
+ }
286
+
287
+ function isMouseDown(m: MouseButton = "left"): boolean {
288
+ return state.mouseState.down.has(m)
289
+ }
290
+
291
+ function isMouseReleased(m: MouseButton = "left"): boolean {
292
+ return state.mouseState.released.has(m)
293
+ }
294
+
295
+ function isMouseMoved(): boolean {
296
+ return state.isMouseMoved
297
+ }
298
+
299
+ function isKeyPressed(k?: Key): boolean {
300
+ return k === undefined
301
+ ? state.keyState.pressed.size > 0
302
+ : state.keyState.pressed.has(k)
303
+ }
304
+
305
+ function isKeyPressedRepeat(k?: Key): boolean {
306
+ return k === undefined
307
+ ? state.keyState.pressedRepeat.size > 0
308
+ : state.keyState.pressedRepeat.has(k)
309
+ }
310
+
311
+ function isKeyDown(k?: Key): boolean {
312
+ return k === undefined
313
+ ? state.keyState.down.size > 0
314
+ : state.keyState.down.has(k)
315
+ }
316
+
317
+ function isKeyReleased(k?: Key): boolean {
318
+ return k === undefined
319
+ ? state.keyState.released.size > 0
320
+ : state.keyState.released.has(k)
321
+ }
322
+
323
+ function isGamepadButtonPressed(btn?: GamepadButton): boolean {
324
+ return btn === undefined
325
+ ? state.mergedGamepadState.buttonState.pressed.size > 0
326
+ : state.mergedGamepadState.buttonState.pressed.has(btn)
327
+ }
328
+
329
+ function isGamepadButtonDown(btn?: GamepadButton): boolean {
330
+ return btn === undefined
331
+ ? state.mergedGamepadState.buttonState.down.size > 0
332
+ : state.mergedGamepadState.buttonState.down.has(btn)
333
+ }
334
+
335
+ function isGamepadButtonReleased(btn?: GamepadButton): boolean {
336
+ return btn === undefined
337
+ ? state.mergedGamepadState.buttonState.released.size > 0
338
+ : state.mergedGamepadState.buttonState.released.has(btn)
339
+ }
340
+
341
+ function onResize(action: () => void): EventController {
342
+ return state.events.on("resize", action)
343
+ }
344
+
345
+ // input callbacks
346
+ const onKeyDown = overload2((action: (key: Key) => void) => {
347
+ return state.events.on("keyDown", action)
348
+ }, (key: Key, action: (key: Key) => void) => {
349
+ return state.events.on("keyDown", (k) => k === key && action(key))
350
+ })
351
+
352
+ const onKeyPress = overload2((action: (key: Key) => void) => {
353
+ return state.events.on("keyPress", action)
354
+ }, (key: Key, action: (key: Key) => void) => {
355
+ return state.events.on("keyPress", (k) => k === key && action(key))
356
+ })
357
+
358
+ const onKeyPressRepeat = overload2((action: (key: Key) => void) => {
359
+ return state.events.on("keyPressRepeat", action)
360
+ }, (key: Key, action: (key: Key) => void) => {
361
+ return state.events.on("keyPressRepeat", (k) => k === key && action(key))
362
+ })
363
+
364
+ const onKeyRelease = overload2((action: (key: Key) => void) => {
365
+ return state.events.on("keyRelease", action)
366
+ }, (key: Key, action: (key: Key) => void) => {
367
+ return state.events.on("keyRelease", (k) => k === key && action(key))
368
+ })
369
+
370
+ const onMouseDown = overload2((action: (m: MouseButton) => void) => {
371
+ return state.events.on("mouseDown", (m) => action(m))
372
+ }, (mouse: MouseButton, action: (m: MouseButton) => void) => {
373
+ return state.events.on("mouseDown", (m) => m === mouse && action(m))
374
+ })
375
+
376
+ const onMousePress = overload2((action: (m: MouseButton) => void) => {
377
+ return state.events.on("mousePress", (m) => action(m))
378
+ }, (mouse: MouseButton, action: (m: MouseButton) => void) => {
379
+ return state.events.on("mousePress", (m) => m === mouse && action(m))
380
+ })
381
+
382
+ const onMouseRelease = overload2((action: (m: MouseButton) => void) => {
383
+ return state.events.on("mouseRelease", (m) => action(m))
384
+ }, (mouse: MouseButton, action: (m: MouseButton) => void) => {
385
+ return state.events.on("mouseRelease", (m) => m === mouse && action(m))
386
+ })
387
+
388
+ function onMouseMove(f: (pos: Vec2, dpos: Vec2) => void): EventController {
389
+ return state.events.on("mouseMove", () => f(mousePos(), mouseDeltaPos()))
390
+ }
391
+
392
+ function onCharInput(action: (ch: string) => void): EventController {
393
+ return state.events.on("charInput", action)
394
+ }
395
+
396
+ function onTouchStart(f: (pos: Vec2, t: Touch) => void): EventController {
397
+ return state.events.on("touchStart", f)
398
+ }
399
+
400
+ function onTouchMove(f: (pos: Vec2, t: Touch) => void): EventController {
401
+ return state.events.on("touchMove", f)
402
+ }
403
+
404
+ function onTouchEnd(f: (pos: Vec2, t: Touch) => void): EventController {
405
+ return state.events.on("touchEnd", f)
406
+ }
407
+
408
+ function onScroll(action: (delta: Vec2) => void): EventController {
409
+ return state.events.on("scroll", action)
410
+ }
411
+
412
+ function onHide(action: () => void): EventController {
413
+ return state.events.on("hide", action)
414
+ }
415
+
416
+ function onShow(action: () => void): EventController {
417
+ return state.events.on("show", action)
418
+ }
419
+
420
+ function onGamepadButtonDown(btn: GamepadButton | ((btn: GamepadButton) => void), action?: (btn: GamepadButton) => void): EventController {
421
+ if (typeof btn === "function") {
422
+ return state.events.on("gamepadButtonDown", btn)
423
+ } else if (typeof btn === "string" && typeof action === "function") {
424
+ return state.events.on("gamepadButtonDown", (b) => b === btn && action(btn))
425
+ }
426
+ }
427
+
428
+ function onGamepadButtonPress(btn: GamepadButton | ((btn: GamepadButton) => void), action?: (btn: GamepadButton) => void): EventController {
429
+ if (typeof btn === "function") {
430
+ return state.events.on("gamepadButtonPress", btn)
431
+ } else if (typeof btn === "string" && typeof action === "function") {
432
+ return state.events.on("gamepadButtonPress", (b) => b === btn && action(btn))
433
+ }
434
+ }
435
+
436
+ function onGamepadButtonRelease(btn: GamepadButton | ((btn: GamepadButton) => void), action?: (btn: GamepadButton) => void): EventController {
437
+ if (typeof btn === "function") {
438
+ return state.events.on("gamepadButtonRelease", btn)
439
+ } else if (typeof btn === "string" && typeof action === "function") {
440
+ return state.events.on("gamepadButtonRelease", (b) => b === btn && action(btn))
441
+ }
442
+ }
443
+
444
+ function onGamepadStick(stick: GamepadStick, action: (value: Vec2) => void): EventController {
445
+ return state.events.on("gamepadStick", ((a: string, v: Vec2) => a === stick && action(v)))
446
+ }
447
+
448
+ function onGamepadConnect(action: (gamepad: KGamePad) => void) {
449
+ state.events.on("gamepadConnect", action)
450
+ }
451
+
452
+ function onGamepadDisconnect(action: (gamepad: KGamePad) => void) {
453
+ state.events.on("gamepadDisconnect", action)
454
+ }
455
+
456
+ function getGamepadStick(stick: GamepadStick): Vec2 {
457
+ return state.mergedGamepadState.stickState.get(stick) || new Vec2(0)
458
+ }
459
+
460
+ function charInputted(): string[] {
461
+ return [...state.charInputted]
462
+ }
463
+
464
+ function getGamepads(): KGamePad[] {
465
+ return [...state.gamepads]
466
+ }
467
+
468
+ function processInput() {
469
+ state.events.trigger("input")
470
+ state.keyState.down.forEach((k) => state.events.trigger("keyDown", k))
471
+ state.mouseState.down.forEach((k) => state.events.trigger("mouseDown", k))
472
+ processGamepad()
473
+ }
474
+
475
+ function resetInput() {
476
+ state.keyState.update()
477
+ state.mouseState.update()
478
+ state.mergedGamepadState.buttonState.update()
479
+ state.mergedGamepadState.stickState.forEach((v, k) => {
480
+ state.mergedGamepadState.stickState.set(k, new Vec2(0))
481
+ })
482
+ state.charInputted = []
483
+ state.isMouseMoved = false
484
+
485
+ state.gamepadStates.forEach((s) => {
486
+ s.buttonState.update()
487
+ s.stickState.forEach((v, k) => {
488
+ s.stickState.set(k, new Vec2(0))
489
+ })
490
+ })
491
+ }
492
+
493
+ function registerGamepad(browserGamepad: Gamepad) {
494
+
495
+ const gamepad = {
496
+ index: browserGamepad.index,
497
+ isPressed: (btn: GamepadButton) => {
498
+ return state.gamepadStates.get(browserGamepad.index).buttonState.pressed.has(btn)
499
+ },
500
+ isDown: (btn: GamepadButton) => {
501
+ return state.gamepadStates.get(browserGamepad.index).buttonState.down.has(btn)
502
+ },
503
+ isReleased: (btn: GamepadButton) => {
504
+ return state.gamepadStates.get(browserGamepad.index).buttonState.released.has(btn)
505
+ },
506
+ getStick: (stick: GamepadStick) => {
507
+ return state.gamepadStates.get(browserGamepad.index).stickState.get(stick)
508
+ },
509
+ }
510
+
511
+ state.gamepads.push(gamepad)
512
+
513
+ state.gamepadStates.set(browserGamepad.index, {
514
+ buttonState: new ButtonState(),
515
+ stickState: new Map([
516
+ ["left", new Vec2(0)],
517
+ ["right", new Vec2(0)],
518
+ ]),
519
+ })
520
+
521
+ return gamepad
522
+
523
+ }
524
+
525
+ function removeGamepad(gamepad: Gamepad) {
526
+ state.gamepads = state.gamepads.filter((g) => g.index !== gamepad.index)
527
+ state.gamepadStates.delete(gamepad.index)
528
+ }
529
+
530
+ function processGamepad() {
531
+
532
+ for (const browserGamepad of navigator.getGamepads()) {
533
+ if (browserGamepad && !state.gamepadStates.has(browserGamepad.index)) {
534
+ registerGamepad(browserGamepad)
535
+ }
536
+ }
537
+
538
+ for (const gamepad of state.gamepads) {
539
+
540
+ const browserGamepad = navigator.getGamepads()[gamepad.index]
541
+ const customMap = opt.gamepads ?? {}
542
+ const map = customMap[browserGamepad.id] ?? GAMEPAD_MAP[browserGamepad.id] ?? GAMEPAD_MAP["default"]
543
+ const gamepadState = state.gamepadStates.get(gamepad.index)
544
+
545
+ for (let i = 0; i < browserGamepad.buttons.length; i++) {
546
+ if (browserGamepad.buttons[i].pressed) {
547
+ if (!gamepadState.buttonState.down.has(map.buttons[i])) {
548
+ state.mergedGamepadState.buttonState.press(map.buttons[i])
549
+ gamepadState.buttonState.press(map.buttons[i])
550
+ state.events.trigger("gamepadButtonPress", map.buttons[i])
551
+ }
552
+ state.events.trigger("gamepadButtonDown", map.buttons[i])
553
+ } else {
554
+ if (gamepadState.buttonState.down.has(map.buttons[i])) {
555
+ state.mergedGamepadState.buttonState.release(map.buttons[i])
556
+ gamepadState.buttonState.release(map.buttons[i])
557
+ state.events.trigger("gamepadButtonRelease", map.buttons[i])
558
+ }
559
+ }
560
+ }
561
+
562
+ for (const stickName in map.sticks) {
563
+ const stick = map.sticks[stickName]
564
+ const value = new Vec2(
565
+ browserGamepad.axes[stick.x],
566
+ browserGamepad.axes[stick.y],
567
+ )
568
+ gamepadState.stickState.set(stickName as GamepadStick, value)
569
+ state.mergedGamepadState.stickState.set(stickName as GamepadStick, value)
570
+ state.events.trigger("gamepadStick", stickName, value)
571
+ }
572
+
573
+ }
574
+
575
+ }
576
+
577
+ type EventList<M> = {
578
+ [event in keyof M]?: (event: M[event]) => void
579
+ }
580
+
581
+ const canvasEvents: EventList<HTMLElementEventMap> = {}
582
+ const docEvents: EventList<DocumentEventMap> = {}
583
+ const winEvents: EventList<WindowEventMap> = {}
584
+
585
+ const pd = opt.pixelDensity || window.devicePixelRatio || 1
586
+
587
+ canvasEvents.mousemove = (e) => {
588
+ const mousePos = new Vec2(e.offsetX, e.offsetY)
589
+ const mouseDeltaPos = new Vec2(e.movementX, e.movementY)
590
+ if (isFullscreen()) {
591
+ const cw = state.canvas.width / pd
592
+ const ch = state.canvas.height / pd
593
+ const ww = window.innerWidth
594
+ const wh = window.innerHeight
595
+ const rw = ww / wh
596
+ const rc = cw / ch
597
+ if (rw > rc) {
598
+ const ratio = wh / ch
599
+ const offset = (ww - (cw * ratio)) / 2
600
+ mousePos.x = map(e.offsetX - offset, 0, cw * ratio, 0, cw)
601
+ mousePos.y = map(e.offsetY, 0, ch * ratio, 0, ch)
602
+ } else {
603
+ const ratio = ww / cw
604
+ const offset = (wh - (ch * ratio)) / 2
605
+ mousePos.x = map(e.offsetX , 0, cw * ratio, 0, cw)
606
+ mousePos.y = map(e.offsetY - offset, 0, ch * ratio, 0, ch)
607
+ }
608
+ }
609
+ state.events.onOnce("input", () => {
610
+ state.isMouseMoved = true
611
+ state.mousePos = mousePos
612
+ state.mouseDeltaPos = mouseDeltaPos
613
+ state.events.trigger("mouseMove")
614
+ })
615
+ }
616
+
617
+ const MOUSE_BUTTONS: MouseButton[] = [
618
+ "left",
619
+ "middle",
620
+ "right",
621
+ "back",
622
+ "forward",
623
+ ]
624
+
625
+ canvasEvents.mousedown = (e) => {
626
+ state.events.onOnce("input", () => {
627
+ const m = MOUSE_BUTTONS[e.button]
628
+ if (!m) return
629
+ state.mouseState.press(m)
630
+ state.events.trigger("mousePress", m)
631
+ })
632
+ }
633
+
634
+ canvasEvents.mouseup = (e) => {
635
+ state.events.onOnce("input", () => {
636
+ const m = MOUSE_BUTTONS[e.button]
637
+ if (!m) return
638
+ state.mouseState.release(m)
639
+ state.events.trigger("mouseRelease", m)
640
+ })
641
+ }
642
+
643
+ const PREVENT_DEFAULT_KEYS = new Set([
644
+ " ",
645
+ "ArrowLeft",
646
+ "ArrowRight",
647
+ "ArrowUp",
648
+ "ArrowDown",
649
+ "Tab",
650
+ ])
651
+
652
+ // translate these key names to a simpler version
653
+ const KEY_ALIAS = {
654
+ "ArrowLeft": "left",
655
+ "ArrowRight": "right",
656
+ "ArrowUp": "up",
657
+ "ArrowDown": "down",
658
+ " ": "space",
659
+ }
660
+
661
+ canvasEvents.keydown = (e) => {
662
+ if (PREVENT_DEFAULT_KEYS.has(e.key)) {
663
+ e.preventDefault()
664
+ }
665
+ state.events.onOnce("input", () => {
666
+ const k = KEY_ALIAS[e.key] || e.key.toLowerCase()
667
+ if (k.length === 1) {
668
+ state.events.trigger("charInput", k)
669
+ state.charInputted.push(k)
670
+ } else if (k === "space") {
671
+ state.events.trigger("charInput", " ")
672
+ state.charInputted.push(" ")
673
+ }
674
+ if (e.repeat) {
675
+ state.keyState.pressRepeat(k)
676
+ state.events.trigger("keyPressRepeat", k)
677
+ } else {
678
+ state.keyState.press(k)
679
+ state.events.trigger("keyPressRepeat", k)
680
+ state.events.trigger("keyPress", k)
681
+ }
682
+ })
683
+ }
684
+
685
+ canvasEvents.keyup = (e) => {
686
+ state.events.onOnce("input", () => {
687
+ const k = KEY_ALIAS[e.key] || e.key.toLowerCase()
688
+ state.keyState.release(k)
689
+ state.events.trigger("keyRelease", k)
690
+ })
691
+ }
692
+
693
+ // TODO: handle all touches at once instead of sequentially
694
+ canvasEvents.touchstart = (e) => {
695
+ // disable long tap context menu
696
+ e.preventDefault()
697
+ state.events.onOnce("input", () => {
698
+ const touches = [...e.changedTouches]
699
+ const box = state.canvas.getBoundingClientRect()
700
+ if (opt.touchToMouse !== false) {
701
+ state.mousePos = new Vec2(
702
+ touches[0].clientX - box.x,
703
+ touches[0].clientY - box.y,
704
+ )
705
+ state.mouseState.press("left")
706
+ state.events.trigger("mousePress", "left")
707
+ }
708
+ touches.forEach((t) => {
709
+ state.events.trigger(
710
+ "touchStart",
711
+ new Vec2(t.clientX - box.x, t.clientY - box.y),
712
+ t,
713
+ )
714
+ })
715
+ })
716
+ }
717
+
718
+ canvasEvents.touchmove = (e) => {
719
+ // disable scrolling
720
+ e.preventDefault()
721
+ state.events.onOnce("input", () => {
722
+ const touches = [...e.changedTouches]
723
+ const box = state.canvas.getBoundingClientRect()
724
+ if (opt.touchToMouse !== false) {
725
+ state.mousePos = new Vec2(
726
+ touches[0].clientX - box.x,
727
+ touches[0].clientY - box.y,
728
+ )
729
+ state.events.trigger("mouseMove")
730
+ }
731
+ touches.forEach((t) => {
732
+ state.events.trigger(
733
+ "touchMove",
734
+ new Vec2(t.clientX - box.x, t.clientY - box.y),
735
+ t,
736
+ )
737
+ })
738
+ })
739
+ }
740
+
741
+ canvasEvents.touchend = (e) => {
742
+ state.events.onOnce("input", () => {
743
+ const touches = [...e.changedTouches]
744
+ const box = state.canvas.getBoundingClientRect()
745
+ if (opt.touchToMouse !== false) {
746
+ state.mousePos = new Vec2(
747
+ touches[0].clientX - box.x,
748
+ touches[0].clientY - box.y,
749
+ )
750
+ state.mouseState.release("left")
751
+ state.events.trigger("mouseRelease", "left")
752
+ }
753
+ touches.forEach((t) => {
754
+ state.events.trigger(
755
+ "touchEnd",
756
+ new Vec2(t.clientX - box.x, t.clientY - box.y),
757
+ t,
758
+ )
759
+ })
760
+ })
761
+ }
762
+
763
+ canvasEvents.touchcancel = (e) => {
764
+ state.events.onOnce("input", () => {
765
+ const touches = [...e.changedTouches]
766
+ const box = state.canvas.getBoundingClientRect()
767
+ if (opt.touchToMouse !== false) {
768
+ state.mousePos = new Vec2(
769
+ touches[0].clientX - box.x,
770
+ touches[0].clientY - box.y,
771
+ )
772
+ state.mouseState.release("left")
773
+ state.events.trigger("mouseRelease", "left")
774
+ }
775
+ touches.forEach((t) => {
776
+ state.events.trigger(
777
+ "touchEnd",
778
+ new Vec2(t.clientX - box.x, t.clientY - box.y),
779
+ t,
780
+ )
781
+ })
782
+ })
783
+ }
784
+
785
+ // TODO: option to not prevent default?
786
+ canvasEvents.wheel = (e) => {
787
+ e.preventDefault()
788
+ state.events.onOnce("input", () => {
789
+ state.events.trigger("scroll", new Vec2(e.deltaX, e.deltaY))
790
+ })
791
+ }
792
+
793
+ canvasEvents.contextmenu = (e) => e.preventDefault()
794
+
795
+ docEvents.visibilitychange = () => {
796
+ if (document.visibilityState === "visible") {
797
+ // prevent a surge of dt when switch back after the tab being hidden for a while
798
+ state.skipTime = true
799
+ state.events.trigger("show")
800
+ } else {
801
+ state.events.trigger("hide")
802
+ }
803
+ }
804
+
805
+ winEvents.gamepadconnected = (e) => {
806
+ const kbGamepad = registerGamepad(e.gamepad)
807
+ state.events.onOnce("input", () => {
808
+ state.events.trigger("gamepadConnect", kbGamepad)
809
+ })
810
+ }
811
+
812
+ winEvents.gamepaddisconnected = (e) => {
813
+ const kbGamepad = getGamepads().filter((g) => g.index === e.gamepad.index)[0]
814
+ removeGamepad(e.gamepad)
815
+ state.events.onOnce("input", () => {
816
+ state.events.trigger("gamepadDisconnect", kbGamepad)
817
+ })
818
+ }
819
+
820
+ for (const name in canvasEvents) {
821
+ state.canvas.addEventListener(name, canvasEvents[name])
822
+ }
823
+
824
+ for (const name in docEvents) {
825
+ document.addEventListener(name, docEvents[name])
826
+ }
827
+
828
+ for (const name in winEvents) {
829
+ window.addEventListener(name, winEvents[name])
830
+ }
831
+
832
+ const resizeObserver = new ResizeObserver((entries) => {
833
+ for (const entry of entries) {
834
+ if (entry.target !== state.canvas) continue
835
+ if (
836
+ state.lastWidth === state.canvas.offsetWidth
837
+ && state.lastHeight === state.canvas.offsetHeight
838
+ ) return
839
+ state.lastWidth = state.canvas.offsetWidth
840
+ state.lastHeight = state.canvas.offsetHeight
841
+ state.events.onOnce("input", () => {
842
+ state.events.trigger("resize")
843
+ })
844
+ }
845
+ })
846
+
847
+ resizeObserver.observe(state.canvas)
848
+
849
+ return {
850
+ dt,
851
+ time,
852
+ run,
853
+ canvas: state.canvas,
854
+ fps,
855
+ numFrames,
856
+ quit,
857
+ setFullscreen,
858
+ isFullscreen,
859
+ setCursor,
860
+ screenshot,
861
+ getGamepads,
862
+ getCursor,
863
+ setCursorLocked,
864
+ isCursorLocked,
865
+ isTouchscreen,
866
+ mousePos,
867
+ mouseDeltaPos,
868
+ isKeyDown,
869
+ isKeyPressed,
870
+ isKeyPressedRepeat,
871
+ isKeyReleased,
872
+ isMouseDown,
873
+ isMousePressed,
874
+ isMouseReleased,
875
+ isMouseMoved,
876
+ isGamepadButtonPressed,
877
+ isGamepadButtonDown,
878
+ isGamepadButtonReleased,
879
+ getGamepadStick,
880
+ charInputted,
881
+ onResize,
882
+ onKeyDown,
883
+ onKeyPress,
884
+ onKeyPressRepeat,
885
+ onKeyRelease,
886
+ onMouseDown,
887
+ onMousePress,
888
+ onMouseRelease,
889
+ onMouseMove,
890
+ onCharInput,
891
+ onTouchStart,
892
+ onTouchMove,
893
+ onTouchEnd,
894
+ onScroll,
895
+ onHide,
896
+ onShow,
897
+ onGamepadButtonDown,
898
+ onGamepadButtonPress,
899
+ onGamepadButtonRelease,
900
+ onGamepadStick,
901
+ onGamepadConnect,
902
+ onGamepadDisconnect,
903
+ events: state.events,
904
+ }
905
+
906
+ }