kaplay 3001.0.0-alpha.1

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