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/CHANGELOG.md +1050 -0
- package/LICENSE +19 -0
- package/README.md +209 -0
- package/dist/global.d.ts +237 -0
- package/dist/global.js +0 -0
- package/dist/kaboom.cjs +56 -0
- package/dist/kaboom.cjs.map +7 -0
- package/dist/kaboom.d.ts +5364 -0
- package/dist/kaboom.js +57 -0
- package/dist/kaboom.js.map +7 -0
- package/dist/kaboom.mjs +56 -0
- package/dist/kaboom.mjs.map +7 -0
- package/package.json +62 -0
- package/src/app.ts +906 -0
- package/src/assets/bean.png +0 -0
- package/src/assets/boom.png +0 -0
- package/src/assets/burp.mp3 +0 -0
- package/src/assets/index.d.ts +9 -0
- package/src/assets/ka.png +0 -0
- package/src/assets.ts +131 -0
- package/src/easings.ts +94 -0
- package/src/gamepad.json +111 -0
- package/src/gfx.ts +524 -0
- package/src/kaboom.ts +6539 -0
- package/src/math.ts +1118 -0
- package/src/texPacker.ts +73 -0
- package/src/types.ts +5364 -0
- package/src/utils.ts +525 -0
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
|
+
}
|