littlejsengine 1.11.13 → 1.12.4

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.
Files changed (79) hide show
  1. package/dist/littlejs.d.ts +1418 -109
  2. package/dist/littlejs.esm.js +3495 -581
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +3258 -399
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +3147 -379
  7. package/examples/box2d/game.js +73 -45
  8. package/examples/box2d/gameObjects.js +96 -92
  9. package/examples/box2d/index.html +2 -7
  10. package/examples/box2d/scenes.js +82 -92
  11. package/examples/breakout/game.js +62 -30
  12. package/examples/breakout/gameObjects.js +45 -47
  13. package/examples/breakout/index.html +1 -5
  14. package/examples/breakoutTutorial/game.js +36 -29
  15. package/examples/breakoutTutorial/index.html +1 -3
  16. package/examples/empty/game.js +5 -2
  17. package/examples/empty/index.html +1 -3
  18. package/examples/htmlMenu/game.js +25 -19
  19. package/examples/htmlMenu/index.html +5 -7
  20. package/examples/index.html +2 -3
  21. package/examples/module/game.js +32 -34
  22. package/examples/module/index.html +1 -1
  23. package/examples/particles/index.html +27 -22
  24. package/examples/platformer/game.js +71 -48
  25. package/examples/platformer/gameCharacter.js +42 -34
  26. package/examples/platformer/gameEffects.js +82 -75
  27. package/examples/platformer/gameLevel.js +67 -38
  28. package/examples/platformer/{gameLevelData.js → gameLevelData.json} +1 -11
  29. package/examples/platformer/gameObjects.js +70 -64
  30. package/examples/platformer/gamePlayer.js +12 -7
  31. package/examples/platformer/index.html +1 -9
  32. package/examples/puzzle/game.js +58 -42
  33. package/examples/puzzle/index.html +1 -3
  34. package/examples/shorts/base.html +2 -2
  35. package/examples/shorts/particles.js +1 -1
  36. package/examples/shorts/platformer.js +1 -1
  37. package/examples/shorts/tileLayer.js +5 -6
  38. package/examples/shorts/tiles.png +0 -0
  39. package/examples/starter/build.js +4 -4
  40. package/examples/starter/game.js +9 -12
  41. package/examples/starter/index.html +13 -13
  42. package/examples/stress/index.html +44 -43
  43. package/examples/typescript/build.js +17 -18
  44. package/examples/typescript/game.js +32 -34
  45. package/examples/typescript/game.ts +32 -34
  46. package/examples/typescript/index.html +1 -1
  47. package/examples/uiSystem/game.js +33 -36
  48. package/examples/uiSystem/index.html +1 -5
  49. package/package.json +3 -3
  50. package/plugins/box2d.js +1556 -640
  51. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  52. package/plugins/newgrounds.js +7 -8
  53. package/plugins/pluginExport.js +50 -0
  54. package/plugins/postProcess.js +94 -93
  55. package/plugins/uiSystem.js +145 -161
  56. package/plugins/zzfxm.js +163 -0
  57. package/reference.md +29 -27
  58. package/src/engine.js +15 -20
  59. package/src/engineAudio.js +74 -204
  60. package/src/engineBuild.js +29 -4
  61. package/src/engineDebug.js +105 -9
  62. package/src/engineDraw.js +27 -14
  63. package/src/engineExport.js +12 -8
  64. package/src/engineInput.js +3 -1
  65. package/src/engineObject.js +18 -14
  66. package/src/engineParticles.js +6 -1
  67. package/src/engineRelease.js +6 -1
  68. package/src/engineSettings.js +8 -8
  69. package/src/engineTileLayer.js +179 -96
  70. package/src/engineUtilities.js +5 -11
  71. package/src/engineWebGL.js +19 -7
  72. package/examples/starter/build/index.html +0 -2
  73. package/examples/starter/build/index.js +0 -1
  74. package/examples/starter/build/tiles.png +0 -0
  75. package/examples/starter/game.zip +0 -0
  76. package/examples/typescript/build/dist/littlejs.esm.js +0 -4834
  77. package/examples/typescript/build/examples/typescript/build.js +0 -24
  78. package/examples/typescript/build/examples/typescript/game.js +0 -102
  79. /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
@@ -1,4834 +0,0 @@
1
- // LittleJS Engine - MIT License - Copyright 2021 Frank Force
2
- // https://github.com/KilledByAPixel/LittleJS
3
- 'use strict';
4
- /**
5
- * LittleJS Debug System
6
- * - Press Esc to show debug overlay with mouse pick
7
- * - Number keys toggle debug functions
8
- * - +/- apply time scale
9
- * - Debug primitive rendering
10
- * - Save a 2d canvas as a png image
11
- * @namespace Debug
12
- */
13
- /** True if debug is enabled
14
- * @type {boolean}
15
- * @default
16
- * @memberof Debug */
17
- const debug = true;
18
- /** True if asserts are enabled
19
- * @type {boolean}
20
- * @default
21
- * @memberof Debug */
22
- const enableAsserts = true;
23
- /** Size to render debug points by default
24
- * @type {number}
25
- * @default
26
- * @memberof Debug */
27
- const debugPointSize = .5;
28
- /** True if watermark with FPS should be shown, false in release builds
29
- * @type {boolean}
30
- * @default
31
- * @memberof Debug */
32
- let showWatermark = true;
33
- /** Key code used to toggle debug mode, Esc by default
34
- * @type {string}
35
- * @default
36
- * @memberof Debug */
37
- let debugKey = 'Escape';
38
- /** True if the debug overlay is active, always false in release builds
39
- * @type {boolean}
40
- * @default
41
- * @memberof Debug */
42
- let debugOverlay = false;
43
- // Engine internal variables not exposed to documentation
44
- let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugMedals = false, debugTakeScreenshot, downloadLink;
45
- ///////////////////////////////////////////////////////////////////////////////
46
- // Debug helper functions
47
- /** Asserts if the expression is false, does not do anything in release builds
48
- * @param {boolean} assert
49
- * @param {Object} [output]
50
- * @memberof Debug */
51
- function ASSERT(assert, output) {
52
- if (enableAsserts)
53
- output ? console.assert(assert, output) : console.assert(assert);
54
- }
55
- /** Draw a debug rectangle in world space
56
- * @param {Vector2} pos
57
- * @param {Vector2} [size=Vector2()]
58
- * @param {string} [color]
59
- * @param {number} [time]
60
- * @param {number} [angle]
61
- * @param {boolean} [fill]
62
- * @memberof Debug */
63
- function debugRect(pos, size = vec2(), color = '#fff', time = 0, angle = 0, fill = false) {
64
- ASSERT(typeof color == 'string', 'pass in css color strings');
65
- debugPrimitives.push({ pos, size: vec2(size), color, time: new Timer(time), angle, fill });
66
- }
67
- /** Draw a debug poly in world space
68
- * @param {Vector2} pos
69
- * @param {Array<Vector2>} points
70
- * @param {string} [color]
71
- * @param {number} [time]
72
- * @param {number} [angle]
73
- * @param {boolean} [fill]
74
- * @memberof Debug */
75
- function debugPoly(pos, points, color = '#fff', time = 0, angle = 0, fill = false) {
76
- ASSERT(typeof color == 'string', 'pass in css color strings');
77
- debugPrimitives.push({ pos, points, color, time: new Timer(time), angle, fill });
78
- }
79
- /** Draw a debug circle in world space
80
- * @param {Vector2} pos
81
- * @param {number} [radius]
82
- * @param {string} [color]
83
- * @param {number} [time]
84
- * @param {boolean} [fill]
85
- * @memberof Debug */
86
- function debugCircle(pos, radius = 0, color = '#fff', time = 0, fill = false) {
87
- ASSERT(typeof color == 'string', 'pass in css color strings');
88
- debugPrimitives.push({ pos, size: radius, color, time: new Timer(time), angle: 0, fill });
89
- }
90
- /** Draw a debug point in world space
91
- * @param {Vector2} pos
92
- * @param {string} [color]
93
- * @param {number} [time]
94
- * @param {number} [angle]
95
- * @memberof Debug */
96
- function debugPoint(pos, color, time, angle) {
97
- ASSERT(typeof color == 'string', 'pass in css color strings');
98
- debugRect(pos, undefined, color, time, angle);
99
- }
100
- /** Draw a debug line in world space
101
- * @param {Vector2} posA
102
- * @param {Vector2} posB
103
- * @param {string} [color]
104
- * @param {number} [thickness]
105
- * @param {number} [time]
106
- * @memberof Debug */
107
- function debugLine(posA, posB, color, thickness = .1, time) {
108
- const halfDelta = vec2((posB.x - posA.x) / 2, (posB.y - posA.y) / 2);
109
- const size = vec2(thickness, halfDelta.length() * 2);
110
- debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
111
- }
112
- /** Draw a debug combined axis aligned bounding box in world space
113
- * @param {Vector2} pA - position A
114
- * @param {Vector2} sA - size A
115
- * @param {Vector2} pB - position B
116
- * @param {Vector2} sB - size B
117
- * @param {string} [color]
118
- * @memberof Debug */
119
- function debugOverlap(pA, sA, pB, sB, color) {
120
- const minPos = vec2(min(pA.x - sA.x / 2, pB.x - sB.x / 2), min(pA.y - sA.y / 2, pB.y - sB.y / 2));
121
- const maxPos = vec2(max(pA.x + sA.x / 2, pB.x + sB.x / 2), max(pA.y + sA.y / 2, pB.y + sB.y / 2));
122
- debugRect(minPos.lerp(maxPos, .5), maxPos.subtract(minPos), color);
123
- }
124
- /** Draw a debug axis aligned bounding box in world space
125
- * @param {string} text
126
- * @param {Vector2} pos
127
- * @param {number} [size]
128
- * @param {string} [color]
129
- * @param {number} [time]
130
- * @param {number} [angle]
131
- * @param {string} [font]
132
- * @memberof Debug */
133
- function debugText(text, pos, size = 1, color = '#fff', time = 0, angle = 0, font = 'monospace') {
134
- ASSERT(typeof color == 'string', 'pass in css color strings');
135
- debugPrimitives.push({ text, pos, size, color, time: new Timer(time), angle, font });
136
- }
137
- /** Clear all debug primitives in the list
138
- * @memberof Debug */
139
- function debugClear() { debugPrimitives = []; }
140
- /** Trigger debug system to take a screenshot
141
- * @memberof Debug */
142
- function debugScreenshot() { debugTakeScreenshot = 1; }
143
- /** Save a canvas to disk
144
- * @param {HTMLCanvasElement} canvas
145
- * @param {string} [filename]
146
- * @param {string} [type]
147
- * @memberof Debug */
148
- function debugSaveCanvas(canvas, filename = 'screenshot', type = 'image/png') { debugSaveDataURL(canvas.toDataURL(type), filename); }
149
- /** Save a text file to disk
150
- * @param {string} text
151
- * @param {string} [filename]
152
- * @param {string} [type]
153
- * @memberof Debug */
154
- function debugSaveText(text, filename = 'text', type = 'text/plain') { debugSaveDataURL(URL.createObjectURL(new Blob([text], { 'type': type })), filename); }
155
- /** Save a data url to disk
156
- * @param {string} dataURL
157
- * @param {string} filename
158
- * @memberof Debug */
159
- function debugSaveDataURL(dataURL, filename) {
160
- downloadLink.download = filename;
161
- downloadLink.href = dataURL;
162
- downloadLink.click();
163
- }
164
- /** Show error as full page of red text
165
- * @memberof Debug */
166
- function debugShowErrors() {
167
- onunhandledrejection = (event) => showError(event.reason);
168
- onerror = (event, source, lineno, colno) => showError(`${event}\n${source}\nLn ${lineno}, Col ${colno}`);
169
- const showError = (message) => {
170
- // replace entire page with error message
171
- document.body.style.display = '';
172
- document.body.style.backgroundColor = '#111';
173
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
174
- };
175
- }
176
- ///////////////////////////////////////////////////////////////////////////////
177
- // Engine debug functions (called automatically)
178
- function debugInit() {
179
- // create link for saving screenshots
180
- downloadLink = document.createElement('a');
181
- }
182
- function debugUpdate() {
183
- if (!debug)
184
- return;
185
- if (keyWasPressed(debugKey)) // Esc
186
- debugOverlay = !debugOverlay;
187
- if (debugOverlay) {
188
- if (keyWasPressed('Digit0'))
189
- showWatermark = !showWatermark;
190
- if (keyWasPressed('Digit1'))
191
- debugPhysics = !debugPhysics, debugParticles = false;
192
- if (keyWasPressed('Digit2'))
193
- debugParticles = !debugParticles, debugPhysics = false;
194
- if (keyWasPressed('Digit3'))
195
- debugGamepads = !debugGamepads;
196
- if (keyWasPressed('Digit4'))
197
- debugRaycast = !debugRaycast;
198
- if (keyWasPressed('Digit5'))
199
- debugScreenshot();
200
- }
201
- }
202
- function debugRender() {
203
- glCopyToContext(mainContext);
204
- if (debugTakeScreenshot) {
205
- // combine canvases, remove alpha and save
206
- combineCanvases();
207
- const w = mainCanvas.width, h = mainCanvas.height;
208
- overlayContext.fillRect(0, 0, w, h);
209
- overlayContext.drawImage(mainCanvas, 0, 0);
210
- debugSaveCanvas(overlayCanvas);
211
- debugTakeScreenshot = 0;
212
- }
213
- if (debugGamepads && gamepadsEnable && navigator.getGamepads) {
214
- // gamepad debug display
215
- const gamepads = navigator.getGamepads();
216
- for (let i = gamepads.length; i--;) {
217
- const gamepad = gamepads[i];
218
- if (gamepad) {
219
- const stickScale = 1;
220
- const buttonScale = .2;
221
- const centerPos = cameraPos;
222
- const sticks = gamepadStickData[i];
223
- for (let j = sticks.length; j--;) {
224
- const drawPos = centerPos.add(vec2(j * stickScale * 2, i * stickScale * 3));
225
- const stickPos = drawPos.add(sticks[j].scale(stickScale));
226
- debugCircle(drawPos, stickScale, '#fff7', 0, true);
227
- debugLine(drawPos, stickPos, '#f00');
228
- debugPoint(stickPos, '#f00');
229
- }
230
- for (let j = gamepad.buttons.length; j--;) {
231
- const drawPos = centerPos.add(vec2(j * buttonScale * 2, i * stickScale * 3 - stickScale - buttonScale));
232
- const pressed = gamepad.buttons[j].pressed;
233
- debugCircle(drawPos, buttonScale, pressed ? '#f00' : '#fff7', 0, true);
234
- debugText('' + j, drawPos, .2);
235
- }
236
- }
237
- }
238
- }
239
- let debugObject;
240
- if (debugOverlay) {
241
- const saveContext = mainContext;
242
- mainContext = overlayContext;
243
- // draw red rectangle around screen
244
- const cameraSize = getCameraSize();
245
- debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
246
- // mouse pick
247
- let bestDistance = Infinity;
248
- for (const o of engineObjects) {
249
- if (o.destroyed)
250
- continue;
251
- if (o instanceof TileLayer)
252
- continue; // prevent tile layers from being picked
253
- o.renderDebugInfo();
254
- if (!o.size.x || !o.size.y)
255
- continue;
256
- const distance = mousePos.distanceSquared(o.pos);
257
- if (distance < bestDistance) {
258
- bestDistance = distance;
259
- debugObject = o;
260
- }
261
- }
262
- if (tileCollisionSize.x > 0 && tileCollisionSize.y > 0)
263
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(0, 0, 1, .5), 0, false);
264
- mainContext = saveContext;
265
- //glCopyToContext(mainContext = saveContext);
266
- }
267
- {
268
- // draw debug primitives
269
- overlayContext.lineWidth = 2;
270
- const pointSize = debugPointSize * cameraScale;
271
- debugPrimitives.forEach(p => {
272
- overlayContext.save();
273
- // create canvas transform from world space to screen space
274
- const pos = worldToScreen(p.pos);
275
- overlayContext.translate(pos.x | 0, pos.y | 0);
276
- overlayContext.rotate(p.angle);
277
- overlayContext.scale(1, p.text ? 1 : -1);
278
- overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
279
- if (p.text != undefined) {
280
- overlayContext.font = p.size * cameraScale + 'px ' + p.font;
281
- overlayContext.textAlign = 'center';
282
- overlayContext.textBaseline = 'middle';
283
- overlayContext.fillText(p.text, 0, 0);
284
- }
285
- else if (p.points != undefined) {
286
- // poly
287
- overlayContext.beginPath();
288
- for (const point of p.points) {
289
- const p2 = point.scale(cameraScale).floor();
290
- overlayContext.lineTo(p2.x, p2.y);
291
- }
292
- overlayContext.closePath();
293
- p.fill && overlayContext.fill();
294
- overlayContext.stroke();
295
- }
296
- else if (p.size == 0 || p.size.x === 0 && p.size.y === 0) {
297
- // point
298
- overlayContext.fillRect(-pointSize / 2, -1, pointSize, 3);
299
- overlayContext.fillRect(-1, -pointSize / 2, 3, pointSize);
300
- }
301
- else if (p.size.x != undefined) {
302
- // rect
303
- const s = p.size.scale(cameraScale).floor();
304
- const w = s.x, h = s.y;
305
- p.fill && overlayContext.fillRect(-w / 2 | 0, -h / 2 | 0, w, h);
306
- overlayContext.strokeRect(-w / 2 | 0, -h / 2 | 0, w, h);
307
- }
308
- else {
309
- // circle
310
- overlayContext.beginPath();
311
- overlayContext.arc(0, 0, p.size * cameraScale, 0, 9);
312
- p.fill && overlayContext.fill();
313
- overlayContext.stroke();
314
- }
315
- overlayContext.restore();
316
- });
317
- // remove expired primitives
318
- debugPrimitives = debugPrimitives.filter(r => r.time < 0);
319
- }
320
- if (debugObject) {
321
- const saveContext = mainContext;
322
- mainContext = overlayContext;
323
- const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
324
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0, 1, 1, .3));
325
- drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1, 0, 0, .5) : rgb(0, 1, 0, .5), false);
326
- const debugText = 'mouse pos = ' + mousePos +
327
- '\nmouse collision = ' + getTileCollisionData(mousePos) +
328
- '\n\n--- object info ---\n' +
329
- debugObject.toString();
330
- drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
331
- mainContext = saveContext;
332
- }
333
- {
334
- // draw debug overlay
335
- overlayContext.save();
336
- overlayContext.fillStyle = '#fff';
337
- overlayContext.textAlign = 'left';
338
- overlayContext.textBaseline = 'top';
339
- overlayContext.font = '28px monospace';
340
- overlayContext.shadowColor = '#000';
341
- overlayContext.shadowBlur = 9;
342
- let x = 9, y = -20, h = 30;
343
- if (debugOverlay) {
344
- overlayContext.fillText(engineName, x, y += h);
345
- overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
346
- overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
347
- overlayContext.fillText('---------', x, y += h);
348
- overlayContext.fillStyle = '#f00';
349
- overlayContext.fillText('ESC: Debug Overlay', x, y += h);
350
- overlayContext.fillStyle = debugPhysics ? '#f00' : '#fff';
351
- overlayContext.fillText('1: Debug Physics', x, y += h);
352
- overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
353
- overlayContext.fillText('2: Debug Particles', x, y += h);
354
- overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
355
- overlayContext.fillText('3: Debug Gamepads', x, y += h);
356
- overlayContext.fillStyle = debugRaycast ? '#f00' : '#fff';
357
- overlayContext.fillText('4: Debug Raycasts', x, y += h);
358
- overlayContext.fillStyle = '#fff';
359
- overlayContext.fillText('5: Save Screenshot', x, y += h);
360
- let keysPressed = '';
361
- for (const i in inputData[0]) {
362
- if (keyIsDown(i, 0))
363
- keysPressed += i + ' ';
364
- }
365
- keysPressed && overlayContext.fillText('Keys Down: ' + keysPressed, x, y += h);
366
- let buttonsPressed = '';
367
- if (inputData[1])
368
- for (const i in inputData[1]) {
369
- if (keyIsDown(i, 1))
370
- buttonsPressed += i + ' ';
371
- }
372
- buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
373
- }
374
- else {
375
- overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
376
- overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
377
- overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
378
- overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
379
- }
380
- overlayContext.restore();
381
- }
382
- }
383
- /**
384
- * LittleJS Utility Classes and Functions
385
- * - General purpose math library
386
- * - Vector2 - fast, simple, easy 2D vector class
387
- * - Color - holds a rgba color with some math functions
388
- * - Timer - tracks time automatically
389
- * - RandomGenerator - seeded random number generator
390
- * @namespace Utilities
391
- */
392
- /** A shortcut to get Math.PI
393
- * @type {number}
394
- * @default Math.PI
395
- * @memberof Utilities */
396
- const PI = Math.PI;
397
- /** Returns absolute value of value passed in
398
- * @param {number} value
399
- * @return {number}
400
- * @memberof Utilities */
401
- function abs(value) { return Math.abs(value); }
402
- /** Returns lowest of two values passed in
403
- * @param {number} valueA
404
- * @param {number} valueB
405
- * @return {number}
406
- * @memberof Utilities */
407
- function min(valueA, valueB) { return Math.min(valueA, valueB); }
408
- /** Returns highest of two values passed in
409
- * @param {number} valueA
410
- * @param {number} valueB
411
- * @return {number}
412
- * @memberof Utilities */
413
- function max(valueA, valueB) { return Math.max(valueA, valueB); }
414
- /** Returns the sign of value passed in
415
- * @param {number} value
416
- * @return {number}
417
- * @memberof Utilities */
418
- function sign(value) { return Math.sign(value); }
419
- /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
420
- * @param {number} dividend
421
- * @param {number} [divisor]
422
- * @return {number}
423
- * @memberof Utilities */
424
- function mod(dividend, divisor = 1) { return ((dividend % divisor) + divisor) % divisor; }
425
- /** Clamps the value between max and min
426
- * @param {number} value
427
- * @param {number} [min]
428
- * @param {number} [max]
429
- * @return {number}
430
- * @memberof Utilities */
431
- function clamp(value, min = 0, max = 1) { return value < min ? min : value > max ? max : value; }
432
- /** Returns what percentage the value is between valueA and valueB
433
- * @param {number} value
434
- * @param {number} valueA
435
- * @param {number} valueB
436
- * @return {number}
437
- * @memberof Utilities */
438
- function percent(value, valueA, valueB) { return (valueB -= valueA) ? clamp((value - valueA) / valueB) : 0; }
439
- /** Linearly interpolates between values passed in using percent
440
- * @param {number} percent
441
- * @param {number} valueA
442
- * @param {number} valueB
443
- * @return {number}
444
- * @memberof Utilities */
445
- function lerp(percent, valueA, valueB) { return valueA + clamp(percent) * (valueB - valueA); }
446
- /** Returns signed wrapped distance between the two values passed in
447
- * @param {number} valueA
448
- * @param {number} valueB
449
- * @param {number} [wrapSize]
450
- * @returns {number}
451
- * @memberof Utilities */
452
- function distanceWrap(valueA, valueB, wrapSize = 1) { const d = (valueA - valueB) % wrapSize; return d * 2 % wrapSize - d; }
453
- /** Linearly interpolates between values passed in with wrapping
454
- * @param {number} percent
455
- * @param {number} valueA
456
- * @param {number} valueB
457
- * @param {number} [wrapSize]
458
- * @returns {number}
459
- * @memberof Utilities */
460
- function lerpWrap(percent, valueA, valueB, wrapSize = 1) { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
461
- /** Returns signed wrapped distance between the two angles passed in
462
- * @param {number} angleA
463
- * @param {number} angleB
464
- * @returns {number}
465
- * @memberof Utilities */
466
- function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2 * PI); }
467
- /** Linearly interpolates between the angles passed in with wrapping
468
- * @param {number} percent
469
- * @param {number} angleA
470
- * @param {number} angleB
471
- * @returns {number}
472
- * @memberof Utilities */
473
- function lerpAngle(percent, angleA, angleB) { return lerpWrap(percent, angleA, angleB, 2 * PI); }
474
- /** Applies smoothstep function to the percentage value
475
- * @param {number} percent
476
- * @return {number}
477
- * @memberof Utilities */
478
- function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
479
- /** Returns the nearest power of two not less then the value
480
- * @param {number} value
481
- * @return {number}
482
- * @memberof Utilities */
483
- function nearestPowerOfTwo(value) { return 2 ** Math.ceil(Math.log2(value)); }
484
- /** Returns true if two axis aligned bounding boxes are overlapping
485
- * @param {Vector2} posA - Center of box A
486
- * @param {Vector2} sizeA - Size of box A
487
- * @param {Vector2} posB - Center of box B
488
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
489
- * @return {boolean} - True if overlapping
490
- * @memberof Utilities */
491
- function isOverlapping(posA, sizeA, posB, sizeB = vec2()) {
492
- return abs(posA.x - posB.x) * 2 < sizeA.x + sizeB.x
493
- && abs(posA.y - posB.y) * 2 < sizeA.y + sizeB.y;
494
- }
495
- /** Returns true if a line segment is intersecting an axis aligned box
496
- * @param {Vector2} start - Start of raycast
497
- * @param {Vector2} end - End of raycast
498
- * @param {Vector2} pos - Center of box
499
- * @param {Vector2} size - Size of box
500
- * @return {boolean} - True if intersecting
501
- * @memberof Utilities */
502
- function isIntersecting(start, end, pos, size) {
503
- // Liang-Barsky algorithm
504
- const boxMin = pos.subtract(size.scale(.5));
505
- const boxMax = boxMin.add(size);
506
- const delta = end.subtract(start);
507
- const a = start.subtract(boxMin);
508
- const b = start.subtract(boxMax);
509
- const p = [-delta.x, delta.x, -delta.y, delta.y];
510
- const q = [a.x, -b.x, a.y, -b.y];
511
- let tMin = 0, tMax = 1;
512
- for (let i = 4; i--;) {
513
- if (p[i]) {
514
- const t = q[i] / p[i];
515
- if (p[i] < 0) {
516
- if (t > tMax)
517
- return false;
518
- tMin = max(t, tMin);
519
- }
520
- else {
521
- if (t < tMin)
522
- return false;
523
- tMax = min(t, tMax);
524
- }
525
- }
526
- else if (q[i] < 0)
527
- return false;
528
- }
529
- return true;
530
- }
531
- /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
532
- * @param {number} [frequency] - Frequency of the wave in Hz
533
- * @param {number} [amplitude] - Amplitude (max height) of the wave
534
- * @param {number} [t=time] - Value to use for time of the wave
535
- * @return {number} - Value waving between 0 and amplitude
536
- * @memberof Utilities */
537
- function wave(frequency = 1, amplitude = 1, t = time) { return amplitude / 2 * (1 - Math.cos(t * frequency * 2 * PI)); }
538
- /** Formats seconds to mm:ss style for display purposes
539
- * @param {number} t - time in seconds
540
- * @return {string}
541
- * @memberof Utilities */
542
- function formatTime(t) { return (t / 60 | 0) + ':' + (t % 60 < 10 ? '0' : '') + (t % 60 | 0); }
543
- ///////////////////////////////////////////////////////////////////////////////
544
- /** Random global functions
545
- * @namespace Random */
546
- /** Returns a random value between the two values passed in
547
- * @param {number} [valueA]
548
- * @param {number} [valueB]
549
- * @return {number}
550
- * @memberof Random */
551
- function rand(valueA = 1, valueB = 0) { return valueB + Math.random() * (valueA - valueB); }
552
- /** Returns a floored random value between the two values passed in
553
- * The upper bound is exclusive. (If 2 is passed in, result will be 0 or 1)
554
- * @param {number} valueA
555
- * @param {number} [valueB]
556
- * @return {number}
557
- * @memberof Random */
558
- function randInt(valueA, valueB = 0) { return Math.floor(rand(valueA, valueB)); }
559
- /** Randomly returns either -1 or 1
560
- * @return {number}
561
- * @memberof Random */
562
- function randSign() { return randInt(2) * 2 - 1; }
563
- /** Returns a random Vector2 with the passed in length
564
- * @param {number} [length]
565
- * @return {Vector2}
566
- * @memberof Random */
567
- function randVector(length = 1) { return new Vector2().setAngle(rand(2 * PI), length); }
568
- /** Returns a random Vector2 within a circular shape
569
- * @param {number} [radius]
570
- * @param {number} [minRadius]
571
- * @return {Vector2}
572
- * @memberof Random */
573
- function randInCircle(radius = 1, minRadius = 0) { return radius > 0 ? randVector(radius * rand(minRadius / radius, 1) ** .5) : new Vector2; }
574
- /** Returns a random color between the two passed in colors, combine components if linear
575
- * @param {Color} [colorA=(1,1,1,1)]
576
- * @param {Color} [colorB=(0,0,0,1)]
577
- * @param {boolean} [linear]
578
- * @return {Color}
579
- * @memberof Random */
580
- function randColor(colorA = new Color, colorB = new Color(0, 0, 0, 1), linear = false) {
581
- return linear ? colorA.lerp(colorB, rand()) :
582
- new Color(rand(colorA.r, colorB.r), rand(colorA.g, colorB.g), rand(colorA.b, colorB.b), rand(colorA.a, colorB.a));
583
- }
584
- ///////////////////////////////////////////////////////////////////////////////
585
- /**
586
- * Seeded random number generator
587
- * - Can be used to create a deterministic random number sequence
588
- * @example
589
- * let r = new RandomGenerator(123); // random number generator with seed 123
590
- * let a = r.float(); // random value between 0 and 1
591
- * let b = r.int(10); // random integer between 0 and 9
592
- * r.seed = 123; // reset the seed
593
- * let c = r.float(); // the same value as a
594
- */
595
- class RandomGenerator {
596
- /** Create a random number generator with the seed passed in
597
- * @param {number} seed - Starting seed */
598
- constructor(seed) {
599
- /** @property {number} - random seed */
600
- this.seed = seed;
601
- }
602
- /** Returns a seeded random value between the two values passed in
603
- * @param {number} [valueA]
604
- * @param {number} [valueB]
605
- * @return {number} */
606
- float(valueA = 1, valueB = 0) {
607
- // xorshift algorithm
608
- this.seed ^= this.seed << 13;
609
- this.seed ^= this.seed >>> 17;
610
- this.seed ^= this.seed << 5;
611
- return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2 ** 32);
612
- }
613
- /** Returns a floored seeded random value the two values passed in
614
- * @param {number} valueA
615
- * @param {number} [valueB]
616
- * @return {number} */
617
- int(valueA, valueB = 0) { return Math.floor(this.float(valueA, valueB)); }
618
- /** Randomly returns either -1 or 1 deterministically
619
- * @return {number} */
620
- sign() { return this.float() > .5 ? 1 : -1; }
621
- }
622
- ///////////////////////////////////////////////////////////////////////////////
623
- /**
624
- * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
625
- * @param {Vector2|number} [x]
626
- * @param {number} [y]
627
- * @return {Vector2}
628
- * @example
629
- * let a = vec2(0, 1); // vector with coordinates (0, 1)
630
- * let b = vec2(a); // copy a into b
631
- * a = vec2(5); // set a to (5, 5)
632
- * b = vec2(); // set b to (0, 0)
633
- * @memberof Utilities
634
- */
635
- function vec2(x = 0, y) {
636
- return typeof x == 'number' ?
637
- new Vector2(x, y == undefined ? x : y) :
638
- new Vector2(x.x, x.y);
639
- }
640
- /**
641
- * Check if object is a valid Vector2
642
- * @param {any} v
643
- * @return {boolean}
644
- * @memberof Utilities
645
- */
646
- function isVector2(v) { return v instanceof Vector2; }
647
- /**
648
- * 2D Vector object with vector math library
649
- * - Functions do not change this so they can be chained together
650
- * @example
651
- * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
652
- * let b = new Vector2; // vector with coordinates (0, 0)
653
- * let c = vec2(4, 2); // use the vec2 function to make a Vector2
654
- * let d = a.add(b).scale(5); // operators can be chained
655
- */
656
- class Vector2 {
657
- /** Create a 2D vector with the x and y passed in, can also be created with vec2()
658
- * @param {number} [x] - X axis location
659
- * @param {number} [y] - Y axis location */
660
- constructor(x = 0, y = 0) {
661
- /** @property {number} - X axis location */
662
- this.x = x;
663
- /** @property {number} - Y axis location */
664
- this.y = y;
665
- ASSERT(this.isValid());
666
- }
667
- /** Sets values of this vector and returns self
668
- * @param {number} [x] - X axis location
669
- * @param {number} [y] - Y axis location
670
- * @return {Vector2} */
671
- set(x = 0, y = 0) {
672
- this.x = x;
673
- this.y = y;
674
- ASSERT(this.isValid());
675
- return this;
676
- }
677
- /** Returns a new vector that is a copy of this
678
- * @return {Vector2} */
679
- copy() { return new Vector2(this.x, this.y); }
680
- /** Returns a copy of this vector plus the vector passed in
681
- * @param {Vector2} v - other vector
682
- * @return {Vector2} */
683
- add(v) {
684
- ASSERT(isVector2(v));
685
- return new Vector2(this.x + v.x, this.y + v.y);
686
- }
687
- /** Returns a copy of this vector minus the vector passed in
688
- * @param {Vector2} v - other vector
689
- * @return {Vector2} */
690
- subtract(v) {
691
- ASSERT(isVector2(v));
692
- return new Vector2(this.x - v.x, this.y - v.y);
693
- }
694
- /** Returns a copy of this vector times the vector passed in
695
- * @param {Vector2} v - other vector
696
- * @return {Vector2} */
697
- multiply(v) {
698
- ASSERT(isVector2(v));
699
- return new Vector2(this.x * v.x, this.y * v.y);
700
- }
701
- /** Returns a copy of this vector divided by the vector passed in
702
- * @param {Vector2} v - other vector
703
- * @return {Vector2} */
704
- divide(v) {
705
- ASSERT(isVector2(v));
706
- return new Vector2(this.x / v.x, this.y / v.y);
707
- }
708
- /** Returns a copy of this vector scaled by the vector passed in
709
- * @param {number} s - scale
710
- * @return {Vector2} */
711
- scale(s) {
712
- ASSERT(!isVector2(s));
713
- return new Vector2(this.x * s, this.y * s);
714
- }
715
- /** Returns the length of this vector
716
- * @return {number} */
717
- length() { return this.lengthSquared() ** .5; }
718
- /** Returns the length of this vector squared
719
- * @return {number} */
720
- lengthSquared() { return this.x ** 2 + this.y ** 2; }
721
- /** Returns the distance from this vector to vector passed in
722
- * @param {Vector2} v - other vector
723
- * @return {number} */
724
- distance(v) {
725
- ASSERT(isVector2(v));
726
- return this.distanceSquared(v) ** .5;
727
- }
728
- /** Returns the distance squared from this vector to vector passed in
729
- * @param {Vector2} v - other vector
730
- * @return {number} */
731
- distanceSquared(v) {
732
- ASSERT(isVector2(v));
733
- return (this.x - v.x) ** 2 + (this.y - v.y) ** 2;
734
- }
735
- /** Returns a new vector in same direction as this one with the length passed in
736
- * @param {number} [length]
737
- * @return {Vector2} */
738
- normalize(length = 1) {
739
- const l = this.length();
740
- return l ? this.scale(length / l) : new Vector2(0, length);
741
- }
742
- /** Returns a new vector clamped to length passed in
743
- * @param {number} [length]
744
- * @return {Vector2} */
745
- clampLength(length = 1) {
746
- const l = this.length();
747
- return l > length ? this.scale(length / l) : this;
748
- }
749
- /** Returns the dot product of this and the vector passed in
750
- * @param {Vector2} v - other vector
751
- * @return {number} */
752
- dot(v) {
753
- ASSERT(isVector2(v));
754
- return this.x * v.x + this.y * v.y;
755
- }
756
- /** Returns the cross product of this and the vector passed in
757
- * @param {Vector2} v - other vector
758
- * @return {number} */
759
- cross(v) {
760
- ASSERT(isVector2(v));
761
- return this.x * v.y - this.y * v.x;
762
- }
763
- /** Returns the clockwise angle of this vector, up is angle 0
764
- * @return {number} */
765
- angle() { return Math.atan2(this.x, this.y); }
766
- /** Sets this vector with clockwise angle and length passed in
767
- * @param {number} [angle]
768
- * @param {number} [length]
769
- * @return {Vector2} */
770
- setAngle(angle = 0, length = 1) {
771
- this.x = length * Math.sin(angle);
772
- this.y = length * Math.cos(angle);
773
- return this;
774
- }
775
- /** Returns copy of this vector rotated by the clockwise angle passed in
776
- * @param {number} angle
777
- * @return {Vector2} */
778
- rotate(angle) {
779
- const c = Math.cos(-angle), s = Math.sin(-angle);
780
- return new Vector2(this.x * c - this.y * s, this.x * s + this.y * c);
781
- }
782
- /** Set the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
783
- * @param {number} [direction]
784
- * @param {number} [length] */
785
- setDirection(direction, length = 1) {
786
- direction = mod(direction, 4);
787
- ASSERT(direction == 0 || direction == 1 || direction == 2 || direction == 3);
788
- return vec2(direction % 2 ? direction - 1 ? -length : length : 0, direction % 2 ? 0 : direction ? -length : length);
789
- }
790
- /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
791
- * @return {number} */
792
- direction() { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
793
- /** Returns a copy of this vector that has been inverted
794
- * @return {Vector2} */
795
- invert() { return new Vector2(this.y, -this.x); }
796
- /** Returns a copy of this vector with each axis floored
797
- * @return {Vector2} */
798
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
799
- /** Returns the area this vector covers as a rectangle
800
- * @return {number} */
801
- area() { return abs(this.x * this.y); }
802
- /** Returns a new vector that is p percent between this and the vector passed in
803
- * @param {Vector2} v - other vector
804
- * @param {number} percent
805
- * @return {Vector2} */
806
- lerp(v, percent) {
807
- ASSERT(isVector2(v));
808
- return this.add(v.subtract(this).scale(clamp(percent)));
809
- }
810
- /** Returns true if this vector is within the bounds of an array size passed in
811
- * @param {Vector2} arraySize
812
- * @return {boolean} */
813
- arrayCheck(arraySize) {
814
- ASSERT(isVector2(arraySize));
815
- return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y;
816
- }
817
- /** Returns this vector expressed as a string
818
- * @param {number} digits - precision to display
819
- * @return {string} */
820
- toString(digits = 3) {
821
- if (debug)
822
- return `(${(this.x < 0 ? '' : ' ') + this.x.toFixed(digits)},${(this.y < 0 ? '' : ' ') + this.y.toFixed(digits)} )`;
823
- }
824
- /** Checks if this is a valid vector
825
- * @return {boolean} */
826
- isValid() {
827
- return typeof this.x == 'number' && !isNaN(this.x)
828
- && typeof this.y == 'number' && !isNaN(this.y);
829
- }
830
- }
831
- ///////////////////////////////////////////////////////////////////////////////
832
- /**
833
- * Create a color object with RGBA values, white by default
834
- * @param {number} [r=1] - red
835
- * @param {number} [g=1] - green
836
- * @param {number} [b=1] - blue
837
- * @param {number} [a=1] - alpha
838
- * @return {Color}
839
- * @memberof Utilities
840
- */
841
- function rgb(r, g, b, a) { return new Color(r, g, b, a); }
842
- /**
843
- * Create a color object with HSLA values, white by default
844
- * @param {number} [h=0] - hue
845
- * @param {number} [s=0] - saturation
846
- * @param {number} [l=1] - lightness
847
- * @param {number} [a=1] - alpha
848
- * @return {Color}
849
- * @memberof Utilities
850
- */
851
- function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
852
- /**
853
- * Check if object is a valid Color
854
- * @param {any} c
855
- * @return {boolean}
856
- * @memberof Utilities
857
- */
858
- function isColor(c) { return c instanceof Color; }
859
- /**
860
- * Color object (red, green, blue, alpha) with some helpful functions
861
- * @example
862
- * let a = new Color; // white
863
- * let b = new Color(1, 0, 0); // red
864
- * let c = new Color(0, 0, 0, 0); // transparent black
865
- * let d = rgb(0, 0, 1); // blue using rgb color
866
- * let e = hsl(.3, 1, .5); // green using hsl color
867
- */
868
- class Color {
869
- /** Create a color with the rgba components passed in, white by default
870
- * @param {number} [r] - red
871
- * @param {number} [g] - green
872
- * @param {number} [b] - blue
873
- * @param {number} [a] - alpha*/
874
- constructor(r = 1, g = 1, b = 1, a = 1) {
875
- /** @property {number} - Red */
876
- this.r = r;
877
- /** @property {number} - Green */
878
- this.g = g;
879
- /** @property {number} - Blue */
880
- this.b = b;
881
- /** @property {number} - Alpha */
882
- this.a = a;
883
- ASSERT(this.isValid());
884
- }
885
- /** Sets values of this color and returns self
886
- * @param {number} [r] - red
887
- * @param {number} [g] - green
888
- * @param {number} [b] - blue
889
- * @param {number} [a] - alpha
890
- * @return {Color} */
891
- set(r = 1, g = 1, b = 1, a = 1) {
892
- this.r = r;
893
- this.g = g;
894
- this.b = b;
895
- this.a = a;
896
- ASSERT(this.isValid());
897
- return this;
898
- }
899
- /** Returns a new color that is a copy of this
900
- * @return {Color} */
901
- copy() { return new Color(this.r, this.g, this.b, this.a); }
902
- /** Returns a copy of this color plus the color passed in
903
- * @param {Color} c - other color
904
- * @return {Color} */
905
- add(c) {
906
- ASSERT(isColor(c));
907
- return new Color(this.r + c.r, this.g + c.g, this.b + c.b, this.a + c.a);
908
- }
909
- /** Returns a copy of this color minus the color passed in
910
- * @param {Color} c - other color
911
- * @return {Color} */
912
- subtract(c) {
913
- ASSERT(isColor(c));
914
- return new Color(this.r - c.r, this.g - c.g, this.b - c.b, this.a - c.a);
915
- }
916
- /** Returns a copy of this color times the color passed in
917
- * @param {Color} c - other color
918
- * @return {Color} */
919
- multiply(c) {
920
- ASSERT(isColor(c));
921
- return new Color(this.r * c.r, this.g * c.g, this.b * c.b, this.a * c.a);
922
- }
923
- /** Returns a copy of this color divided by the color passed in
924
- * @param {Color} c - other color
925
- * @return {Color} */
926
- divide(c) {
927
- ASSERT(isColor(c));
928
- return new Color(this.r / c.r, this.g / c.g, this.b / c.b, this.a / c.a);
929
- }
930
- /** Returns a copy of this color scaled by the value passed in, alpha can be scaled separately
931
- * @param {number} scale
932
- * @param {number} [alphaScale=scale]
933
- * @return {Color} */
934
- scale(scale, alphaScale = scale) { return new Color(this.r * scale, this.g * scale, this.b * scale, this.a * alphaScale); }
935
- /** Returns a copy of this color clamped to the valid range between 0 and 1
936
- * @return {Color} */
937
- clamp() { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }
938
- /** Returns a new color that is p percent between this and the color passed in
939
- * @param {Color} c - other color
940
- * @param {number} percent
941
- * @return {Color} */
942
- lerp(c, percent) {
943
- ASSERT(isColor(c));
944
- return this.add(c.subtract(this).scale(clamp(percent)));
945
- }
946
- /** Sets this color given a hue, saturation, lightness, and alpha
947
- * @param {number} [h] - hue
948
- * @param {number} [s] - saturation
949
- * @param {number} [l] - lightness
950
- * @param {number} [a] - alpha
951
- * @return {Color} */
952
- setHSLA(h = 0, s = 0, l = 1, a = 1) {
953
- h = mod(h, 1);
954
- s = clamp(s);
955
- l = clamp(l);
956
- const q = l < .5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q, f = (p, q, t) => (t = mod(t, 1)) * 6 < 1 ? p + (q - p) * 6 * t :
957
- t * 2 < 1 ? q :
958
- t * 3 < 2 ? p + (q - p) * (4 - t * 6) : p;
959
- this.r = f(p, q, h + 1 / 3);
960
- this.g = f(p, q, h);
961
- this.b = f(p, q, h - 1 / 3);
962
- this.a = a;
963
- ASSERT(this.isValid());
964
- return this;
965
- }
966
- /** Returns this color expressed in hsla format
967
- * @return {Array<number>} */
968
- HSLA() {
969
- const r = clamp(this.r);
970
- const g = clamp(this.g);
971
- const b = clamp(this.b);
972
- const a = clamp(this.a);
973
- const max = Math.max(r, g, b);
974
- const min = Math.min(r, g, b);
975
- const l = (max + min) / 2;
976
- let h = 0, s = 0;
977
- if (max != min) {
978
- let d = max - min;
979
- s = l > .5 ? d / (2 - max - min) : d / (max + min);
980
- if (r == max)
981
- h = (g - b) / d + (g < b ? 6 : 0);
982
- else if (g == max)
983
- h = (b - r) / d + 2;
984
- else if (b == max)
985
- h = (r - g) / d + 4;
986
- }
987
- return [h / 6, s, l, a];
988
- }
989
- /** Returns a new color that has each component randomly adjusted
990
- * @param {number} [amount]
991
- * @param {number} [alphaAmount]
992
- * @return {Color} */
993
- mutate(amount = .05, alphaAmount = 0) {
994
- return new Color(this.r + rand(amount, -amount), this.g + rand(amount, -amount), this.b + rand(amount, -amount), this.a + rand(alphaAmount, -alphaAmount)).clamp();
995
- }
996
- /** Returns this color expressed as a hex color code
997
- * @param {boolean} [useAlpha] - if alpha should be included in result
998
- * @return {string} */
999
- toString(useAlpha = true) {
1000
- const toHex = (c) => ((c = clamp(c) * 255 | 0) < 16 ? '0' : '') + c.toString(16);
1001
- return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1002
- }
1003
- /** Set this color from a hex code
1004
- * @param {string} hex - html hex code
1005
- * @return {Color} */
1006
- setHex(hex) {
1007
- ASSERT(typeof hex == 'string' && hex[0] == '#');
1008
- ASSERT([4, 5, 7, 9].includes(hex.length), 'Invalid hex');
1009
- if (hex.length < 6) {
1010
- const fromHex = (c) => clamp(parseInt(hex[c], 16) / 15);
1011
- this.r = fromHex(1);
1012
- this.g = fromHex(2),
1013
- this.b = fromHex(3);
1014
- this.a = hex.length == 5 ? fromHex(4) : 1;
1015
- }
1016
- else {
1017
- const fromHex = (c) => clamp(parseInt(hex.slice(c, c + 2), 16) / 255);
1018
- this.r = fromHex(1);
1019
- this.g = fromHex(3),
1020
- this.b = fromHex(5);
1021
- this.a = hex.length == 9 ? fromHex(7) : 1;
1022
- }
1023
- ASSERT(this.isValid());
1024
- return this;
1025
- }
1026
- /** Returns this color expressed as 32 bit RGBA value
1027
- * @return {number} */
1028
- rgbaInt() {
1029
- const r = clamp(this.r) * 255 | 0;
1030
- const g = clamp(this.g) * 255 << 8;
1031
- const b = clamp(this.b) * 255 << 16;
1032
- const a = clamp(this.a) * 255 << 24;
1033
- return r + g + b + a;
1034
- }
1035
- /** Checks if this is a valid color
1036
- * @return {boolean} */
1037
- isValid() {
1038
- return typeof this.r == 'number' && !isNaN(this.r)
1039
- && typeof this.g == 'number' && !isNaN(this.g)
1040
- && typeof this.b == 'number' && !isNaN(this.b)
1041
- && typeof this.a == 'number' && !isNaN(this.a);
1042
- }
1043
- }
1044
- ///////////////////////////////////////////////////////////////////////////////
1045
- // default colors
1046
- /** Color - White #ffffff
1047
- * @type {Color}
1048
- * @memberof Utilities */
1049
- const WHITE = rgb();
1050
- /** Color - Black #000000
1051
- * @type {Color}
1052
- * @memberof Utilities */
1053
- const BLACK = rgb(0, 0, 0);
1054
- /** Color - Gray #808080
1055
- * @type {Color}
1056
- * @memberof Utilities */
1057
- const GRAY = rgb(.5, .5, .5);
1058
- /** Color - Red #ff0000
1059
- * @type {Color}
1060
- * @memberof Utilities */
1061
- const RED = rgb(1, 0, 0);
1062
- /** Color - Orange #ff8000
1063
- * @type {Color}
1064
- * @memberof Utilities */
1065
- const ORANGE = rgb(1, .5, 0);
1066
- /** Color - Yellow #ffff00
1067
- * @type {Color}
1068
- * @memberof Utilities */
1069
- const YELLOW = rgb(1, 1, 0);
1070
- /** Color - Green #00ff00
1071
- * @type {Color}
1072
- * @memberof Utilities */
1073
- const GREEN = rgb(0, 1, 0);
1074
- /** Color - Cyan #00ffff
1075
- * @type {Color}
1076
- * @memberof Utilities */
1077
- const CYAN = rgb(0, 1, 1);
1078
- /** Color - Blue #0000ff
1079
- * @type {Color}
1080
- * @memberof Utilities */
1081
- const BLUE = rgb(0, 0, 1);
1082
- /** Color - Purple #8000ff
1083
- * @type {Color}
1084
- * @memberof Utilities */
1085
- const PURPLE = rgb(.5, 0, 1);
1086
- /** Color - Magenta #ff00ff
1087
- * @type {Color}
1088
- * @memberof Utilities */
1089
- const MAGENTA = rgb(1, 0, 1);
1090
- ///////////////////////////////////////////////////////////////////////////////
1091
- /**
1092
- * Timer object tracks how long has passed since it was set
1093
- * @example
1094
- * let a = new Timer; // creates a timer that is not set
1095
- * a.set(3); // sets the timer to 3 seconds
1096
- *
1097
- * let b = new Timer(1); // creates a timer with 1 second left
1098
- * b.unset(); // unset the timer
1099
- */
1100
- class Timer {
1101
- /** Create a timer object set time passed in
1102
- * @param {number} [timeLeft] - How much time left before the timer elapses in seconds */
1103
- constructor(timeLeft) { this.time = timeLeft == undefined ? undefined : time + timeLeft; this.setTime = timeLeft; }
1104
- /** Set the timer with seconds passed in
1105
- * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
1106
- set(timeLeft = 0) { this.time = time + timeLeft; this.setTime = timeLeft; }
1107
- /** Unset the timer */
1108
- unset() { this.time = undefined; }
1109
- /** Returns true if set
1110
- * @return {boolean} */
1111
- isSet() { return this.time != undefined; }
1112
- /** Returns true if set and has not elapsed
1113
- * @return {boolean} */
1114
- active() { return time < this.time; }
1115
- /** Returns true if set and elapsed
1116
- * @return {boolean} */
1117
- elapsed() { return time >= this.time; }
1118
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1119
- * @return {number} */
1120
- get() { return this.isSet() ? time - this.time : 0; }
1121
- /** Get percentage elapsed based on time it was set to, returns 0 if not set
1122
- * @return {number} */
1123
- getPercent() { return this.isSet() ? 1 - percent(this.time - time, 0, this.setTime) : 0; }
1124
- /** Returns this timer expressed as a string
1125
- * @return {string} */
1126
- toString() { if (debug) {
1127
- return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get() < 0 ? 'before' : 'after') : 'unset';
1128
- } }
1129
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1130
- * @return {number} */
1131
- valueOf() { return this.get(); }
1132
- }
1133
- /**
1134
- * LittleJS Engine Settings
1135
- * - All settings for the engine are here
1136
- * @namespace Settings
1137
- */
1138
- ///////////////////////////////////////////////////////////////////////////////
1139
- // Camera settings
1140
- /** Position of camera in world space
1141
- * @type {Vector2}
1142
- * @default Vector2()
1143
- * @memberof Settings */
1144
- let cameraPos = vec2();
1145
- /** Scale of camera in world space
1146
- * @type {number}
1147
- * @default
1148
- * @memberof Settings */
1149
- let cameraScale = 32;
1150
- ///////////////////////////////////////////////////////////////////////////////
1151
- // Display settings
1152
- /** The max size of the canvas, centered if window is larger
1153
- * @type {Vector2}
1154
- * @default Vector2(1920,1080)
1155
- * @memberof Settings */
1156
- let canvasMaxSize = vec2(1920, 1080);
1157
- /** Fixed size of the canvas, if enabled canvas size never changes
1158
- * - you may also need to set mainCanvasSize if using screen space coords in startup
1159
- * @type {Vector2}
1160
- * @default Vector2()
1161
- * @memberof Settings */
1162
- let canvasFixedSize = vec2();
1163
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1164
- * - Must be set before startup to take effect
1165
- * - If enabled sets css image-rendering:pixelated
1166
- * @type {boolean}
1167
- * @default
1168
- * @memberof Settings */
1169
- let canvasPixelated = true;
1170
- /** Disables texture filtering for crisper pixel art
1171
- * @type {boolean}
1172
- * @default
1173
- * @memberof Settings */
1174
- let tilesPixelated = true;
1175
- /** Default font used for text rendering
1176
- * @type {string}
1177
- * @default
1178
- * @memberof Settings */
1179
- let fontDefault = 'arial';
1180
- /** Enable to show the LittleJS splash screen be shown on startup
1181
- * @type {boolean}
1182
- * @default
1183
- * @memberof Settings */
1184
- let showSplashScreen = false;
1185
- /** Disables all rendering, audio, and input for servers
1186
- * @type {boolean}
1187
- * @default
1188
- * @memberof Settings */
1189
- let headlessMode = false;
1190
- ///////////////////////////////////////////////////////////////////////////////
1191
- // WebGL settings
1192
- /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
1193
- * @type {boolean}
1194
- * @default
1195
- * @memberof Settings */
1196
- let glEnable = true;
1197
- /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
1198
- * @type {boolean}
1199
- * @default
1200
- * @memberof Settings */
1201
- let glOverlay = true;
1202
- ///////////////////////////////////////////////////////////////////////////////
1203
- // Tile sheet settings
1204
- /** Default size of tiles in pixels
1205
- * @type {Vector2}
1206
- * @default Vector2(16,16)
1207
- * @memberof Settings */
1208
- let tileSizeDefault = vec2(16);
1209
- /** How many pixels smaller to draw tiles to prevent bleeding from neighbors
1210
- * @type {number}
1211
- * @default
1212
- * @memberof Settings */
1213
- let tileFixBleedScale = 0;
1214
- ///////////////////////////////////////////////////////////////////////////////
1215
- // Object settings
1216
- /** Enable physics solver for collisions between objects
1217
- * @type {boolean}
1218
- * @default
1219
- * @memberof Settings */
1220
- let enablePhysicsSolver = true;
1221
- /** Default object mass for collision calculations (how heavy objects are)
1222
- * @type {number}
1223
- * @default
1224
- * @memberof Settings */
1225
- let objectDefaultMass = 1;
1226
- /** How much to slow velocity by each frame (0-1)
1227
- * @type {number}
1228
- * @default
1229
- * @memberof Settings */
1230
- let objectDefaultDamping = 1;
1231
- /** How much to slow angular velocity each frame (0-1)
1232
- * @type {number}
1233
- * @default
1234
- * @memberof Settings */
1235
- let objectDefaultAngleDamping = 1;
1236
- /** How much to bounce when a collision occurs (0-1)
1237
- * @type {number}
1238
- * @default
1239
- * @memberof Settings */
1240
- let objectDefaultElasticity = 0;
1241
- /** How much to slow when touching (0-1)
1242
- * @type {number}
1243
- * @default
1244
- * @memberof Settings */
1245
- let objectDefaultFriction = .8;
1246
- /** Clamp max speed to avoid fast objects missing collisions
1247
- * @type {number}
1248
- * @default
1249
- * @memberof Settings */
1250
- let objectMaxSpeed = 1;
1251
- /** How much gravity to apply to objects along the Y axis, negative is down
1252
- * @type {number}
1253
- * @default
1254
- * @memberof Settings */
1255
- let gravity = 0;
1256
- /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
1257
- * @type {number}
1258
- * @default
1259
- * @memberof Settings */
1260
- let particleEmitRateScale = 1;
1261
- ///////////////////////////////////////////////////////////////////////////////
1262
- // Input settings
1263
- /** Should gamepads be allowed
1264
- * @type {boolean}
1265
- * @default
1266
- * @memberof Settings */
1267
- let gamepadsEnable = true;
1268
- /** If true, the dpad input is also routed to the left analog stick (for better accessability)
1269
- * @type {boolean}
1270
- * @default
1271
- * @memberof Settings */
1272
- let gamepadDirectionEmulateStick = true;
1273
- /** If true the WASD keys are also routed to the direction keys (for better accessability)
1274
- * @type {boolean}
1275
- * @default
1276
- * @memberof Settings */
1277
- let inputWASDEmulateDirection = true;
1278
- /** True if touch input is enabled for mobile devices
1279
- * - Touch events will be routed to mouse events
1280
- * @type {boolean}
1281
- * @default
1282
- * @memberof Settings */
1283
- let touchInputEnable = true;
1284
- /** True if touch gamepad should appear on mobile devices
1285
- * - Supports left analog stick, 4 face buttons and start button (button 9)
1286
- * - Must be set by end of gameInit to be activated
1287
- * @type {boolean}
1288
- * @default
1289
- * @memberof Settings */
1290
- let touchGamepadEnable = false;
1291
- /** True if touch gamepad should be analog stick or false to use if 8 way dpad
1292
- * @type {boolean}
1293
- * @default
1294
- * @memberof Settings */
1295
- let touchGamepadAnalog = true;
1296
- /** Size of virtual gamepad for touch devices in pixels
1297
- * @type {number}
1298
- * @default
1299
- * @memberof Settings */
1300
- let touchGamepadSize = 99;
1301
- /** Transparency of touch gamepad overlay
1302
- * @type {number}
1303
- * @default
1304
- * @memberof Settings */
1305
- let touchGamepadAlpha = .3;
1306
- /** Allow vibration hardware if it exists
1307
- * @type {boolean}
1308
- * @default
1309
- * @memberof Settings */
1310
- let vibrateEnable = true;
1311
- ///////////////////////////////////////////////////////////////////////////////
1312
- // Audio settings
1313
- /** All audio code can be disabled and removed from build
1314
- * @type {boolean}
1315
- * @default
1316
- * @memberof Settings */
1317
- let soundEnable = true;
1318
- /** Volume scale to apply to all sound, music and speech
1319
- * @type {number}
1320
- * @default
1321
- * @memberof Settings */
1322
- let soundVolume = .3;
1323
- /** Default range where sound no longer plays
1324
- * @type {number}
1325
- * @default
1326
- * @memberof Settings */
1327
- let soundDefaultRange = 40;
1328
- /** Default range percent to start tapering off sound (0-1)
1329
- * @type {number}
1330
- * @default
1331
- * @memberof Settings */
1332
- let soundDefaultTaper = .7;
1333
- ///////////////////////////////////////////////////////////////////////////////
1334
- // Medals settings
1335
- /** How long to show medals for in seconds
1336
- * @type {number}
1337
- * @default
1338
- * @memberof Settings */
1339
- let medalDisplayTime = 5;
1340
- /** How quickly to slide on/off medals in seconds
1341
- * @type {number}
1342
- * @default
1343
- * @memberof Settings */
1344
- let medalDisplaySlideTime = .5;
1345
- /** Size of medal display
1346
- * @type {Vector2}
1347
- * @default Vector2(640,80)
1348
- * @memberof Settings */
1349
- let medalDisplaySize = vec2(640, 80);
1350
- /** Size of icon in medal display
1351
- * @type {number}
1352
- * @default
1353
- * @memberof Settings */
1354
- let medalDisplayIconSize = 50;
1355
- /** Set to stop medals from being unlockable (like if cheats are enabled)
1356
- * @type {boolean}
1357
- * @default
1358
- * @memberof Settings */
1359
- let medalsPreventUnlock = false;
1360
- ///////////////////////////////////////////////////////////////////////////////
1361
- // Setters for global variables
1362
- /** Set position of camera in world space
1363
- * @param {Vector2} pos
1364
- * @memberof Settings */
1365
- function setCameraPos(pos) { cameraPos = pos; }
1366
- /** Set scale of camera in world space
1367
- * @param {number} scale
1368
- * @memberof Settings */
1369
- function setCameraScale(scale) { cameraScale = scale; }
1370
- /** Set max size of the canvas
1371
- * @param {Vector2} size
1372
- * @memberof Settings */
1373
- function setCanvasMaxSize(size) { canvasMaxSize = size; }
1374
- /** Set fixed size of the canvas
1375
- * @param {Vector2} size
1376
- * @memberof Settings */
1377
- function setCanvasFixedSize(size) { canvasFixedSize = size; }
1378
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1379
- * @param {boolean} pixelated
1380
- * @memberof Settings */
1381
- function setCanvasPixelated(pixelated) { canvasPixelated = pixelated; }
1382
- /** Disables texture filtering for crisper pixel art
1383
- * @param {boolean} pixelated
1384
- * @memberof Settings */
1385
- function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
1386
- /** Set default font used for text rendering
1387
- * @param {string} font
1388
- * @memberof Settings */
1389
- function setFontDefault(font) { fontDefault = font; }
1390
- /** Set if the LittleJS splash screen be shown on startup
1391
- * @param {boolean} show
1392
- * @memberof Settings */
1393
- function setShowSplashScreen(show) { showSplashScreen = show; }
1394
- /** Set to disable rendering, audio, and input for servers
1395
- * @param {boolean} headless
1396
- * @memberof Settings */
1397
- function setHeadlessMode(headless) { headlessMode = headless; }
1398
- /** Set if webgl rendering is enabled
1399
- * @param {boolean} enable
1400
- * @memberof Settings */
1401
- function setGlEnable(enable) { glEnable = enable; }
1402
- /** Set to not composite the WebGL canvas
1403
- * @param {boolean} overlay
1404
- * @memberof Settings */
1405
- function setGlOverlay(overlay) { glOverlay = overlay; }
1406
- /** Set default size of tiles in pixels
1407
- * @param {Vector2} size
1408
- * @memberof Settings */
1409
- function setTileSizeDefault(size) { tileSizeDefault = size; }
1410
- /** Set to prevent tile bleeding from neighbors in pixels
1411
- * @param {number} scale
1412
- * @memberof Settings */
1413
- function setTileFixBleedScale(scale) { tileFixBleedScale = scale; }
1414
- /** Set if collisions between objects are enabled
1415
- * @param {boolean} enable
1416
- * @memberof Settings */
1417
- function setEnablePhysicsSolver(enable) { enablePhysicsSolver = enable; }
1418
- /** Set default object mass for collision calculations
1419
- * @param {number} mass
1420
- * @memberof Settings */
1421
- function setObjectDefaultMass(mass) { objectDefaultMass = mass; }
1422
- /** Set how much to slow velocity by each frame
1423
- * @param {number} damp
1424
- * @memberof Settings */
1425
- function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
1426
- /** Set how much to slow angular velocity each frame
1427
- * @param {number} damp
1428
- * @memberof Settings */
1429
- function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
1430
- /** Set how much to bounce when a collision occur
1431
- * @param {number} elasticity
1432
- * @memberof Settings */
1433
- function setObjectDefaultElasticity(elasticity) { objectDefaultElasticity = elasticity; }
1434
- /** Set how much to slow when touching
1435
- * @param {number} friction
1436
- * @memberof Settings */
1437
- function setObjectDefaultFriction(friction) { objectDefaultFriction = friction; }
1438
- /** Set max speed to avoid fast objects missing collisions
1439
- * @param {number} speed
1440
- * @memberof Settings */
1441
- function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
1442
- /** Set how much gravity to apply to objects along the Y axis
1443
- * @param {number} newGravity
1444
- * @memberof Settings */
1445
- function setGravity(newGravity) { gravity = newGravity; }
1446
- /** Set to scales emit rate of particles
1447
- * @param {number} scale
1448
- * @memberof Settings */
1449
- function setParticleEmitRateScale(scale) { particleEmitRateScale = scale; }
1450
- /** Set if gamepads are enabled
1451
- * @param {boolean} enable
1452
- * @memberof Settings */
1453
- function setGamepadsEnable(enable) { gamepadsEnable = enable; }
1454
- /** Set if the dpad input is also routed to the left analog stick
1455
- * @param {boolean} enable
1456
- * @memberof Settings */
1457
- function setGamepadDirectionEmulateStick(enable) { gamepadDirectionEmulateStick = enable; }
1458
- /** Set if true the WASD keys are also routed to the direction keys
1459
- * @param {boolean} enable
1460
- * @memberof Settings */
1461
- function setInputWASDEmulateDirection(enable) { inputWASDEmulateDirection = enable; }
1462
- /** Set if touch input is allowed
1463
- * @param {boolean} enable
1464
- * @memberof Settings */
1465
- function setTouchInputEnable(enable) { touchInputEnable = enable; }
1466
- /** Set if touch gamepad should appear on mobile devices
1467
- * @param {boolean} enable
1468
- * @memberof Settings */
1469
- function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
1470
- /** Set if touch gamepad should be analog stick or 8 way dpad
1471
- * @param {boolean} analog
1472
- * @memberof Settings */
1473
- function setTouchGamepadAnalog(analog) { touchGamepadAnalog = analog; }
1474
- /** Set size of virtual gamepad for touch devices in pixels
1475
- * @param {number} size
1476
- * @memberof Settings */
1477
- function setTouchGamepadSize(size) { touchGamepadSize = size; }
1478
- /** Set transparency of touch gamepad overlay
1479
- * @param {number} alpha
1480
- * @memberof Settings */
1481
- function setTouchGamepadAlpha(alpha) { touchGamepadAlpha = alpha; }
1482
- /** Set to allow vibration hardware if it exists
1483
- * @param {boolean} enable
1484
- * @memberof Settings */
1485
- function setVibrateEnable(enable) { vibrateEnable = enable; }
1486
- /** Set to disable all audio code
1487
- * @param {boolean} enable
1488
- * @memberof Settings */
1489
- function setSoundEnable(enable) { soundEnable = enable; }
1490
- /** Set volume scale to apply to all sound, music and speech
1491
- * @param {number} volume
1492
- * @memberof Settings */
1493
- function setSoundVolume(volume) {
1494
- soundVolume = volume;
1495
- if (soundEnable && !headlessMode && audioGainNode)
1496
- audioGainNode.gain.value = volume; // update gain immediately
1497
- }
1498
- /** Set default range where sound no longer plays
1499
- * @param {number} range
1500
- * @memberof Settings */
1501
- function setSoundDefaultRange(range) { soundDefaultRange = range; }
1502
- /** Set default range percent to start tapering off sound
1503
- * @param {number} taper
1504
- * @memberof Settings */
1505
- function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
1506
- /** Set how long to show medals for in seconds
1507
- * @param {number} time
1508
- * @memberof Settings */
1509
- function setMedalDisplayTime(time) { medalDisplayTime = time; }
1510
- /** Set how quickly to slide on/off medals in seconds
1511
- * @param {number} time
1512
- * @memberof Settings */
1513
- function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
1514
- /** Set size of medal display
1515
- * @param {Vector2} size
1516
- * @memberof Settings */
1517
- function setMedalDisplaySize(size) { medalDisplaySize = size; }
1518
- /** Set size of icon in medal display
1519
- * @param {number} size
1520
- * @memberof Settings */
1521
- function setMedalDisplayIconSize(size) { medalDisplayIconSize = size; }
1522
- /** Set to stop medals from being unlockable
1523
- * @param {boolean} preventUnlock
1524
- * @memberof Settings */
1525
- function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
1526
- /** Set if watermark with FPS should be shown
1527
- * @param {boolean} show
1528
- * @memberof Debug */
1529
- function setShowWatermark(show) { showWatermark = show; }
1530
- /** Set key code used to toggle debug mode, Esc by default
1531
- * @param {string} key
1532
- * @memberof Debug */
1533
- function setDebugKey(key) { debugKey = key; }
1534
- /**
1535
- * LittleJS Object System
1536
- */
1537
- /**
1538
- * LittleJS Object Base Object Class
1539
- * - Top level object class used by the engine
1540
- * - Automatically adds self to object list
1541
- * - Will be updated and rendered each frame
1542
- * - Renders as a sprite from a tilesheet by default
1543
- * - Can have color and additive color applied
1544
- * - 2D Physics and collision system
1545
- * - Sorted by renderOrder
1546
- * - Objects can have children attached
1547
- * - Parents are updated before children, and set child transform
1548
- * - Call destroy() to get rid of objects
1549
- *
1550
- * The physics system used by objects is simple and fast with some caveats...
1551
- * - Collision uses the axis aligned size, the object's rotation angle is only for rendering
1552
- * - Objects are guaranteed to not intersect tile collision from physics
1553
- * - If an object starts or is moved inside tile collision, it will not collide with that tile
1554
- * - Collision for objects can be set to be solid to block other objects
1555
- * - Objects may get pushed into overlapping other solid objects, if so they will push away
1556
- * - Solid objects are more performance intensive and should be used sparingly
1557
- * @example
1558
- * // create an engine object, normally you would first extend the class with your own
1559
- * const pos = vec2(2,3);
1560
- * const object = new EngineObject(pos);
1561
- */
1562
- class EngineObject {
1563
- /** Create an engine object and adds it to the list of objects
1564
- * @param {Vector2} [pos=(0,0)] - World space position of the object
1565
- * @param {Vector2} [size=(1,1)] - World space size of the object
1566
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
1567
- * @param {number} [angle] - Angle the object is rotated by
1568
- * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
1569
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
1570
- */
1571
- constructor(pos = vec2(), size = vec2(1), tileInfo, angle = 0, color = new Color, renderOrder = 0) {
1572
- // set passed in params
1573
- ASSERT(isVector2(pos) && isVector2(size), 'ensure pos and size are vec2s');
1574
- ASSERT(typeof tileInfo !== 'number' || !tileInfo, 'old style tile setup');
1575
- /** @property {Vector2} - World space position of the object */
1576
- this.pos = pos.copy();
1577
- /** @property {Vector2} - World space width and height of the object */
1578
- this.size = size;
1579
- /** @property {Vector2} - Size of object used for drawing, uses size if not set */
1580
- this.drawSize = undefined;
1581
- /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
1582
- this.tileInfo = tileInfo;
1583
- /** @property {number} - Angle to rotate the object */
1584
- this.angle = angle;
1585
- /** @property {Color} - Color to apply when rendered */
1586
- this.color = color;
1587
- /** @property {Color} - Additive color to apply when rendered */
1588
- this.additiveColor = undefined;
1589
- /** @property {boolean} - Should it flip along y axis when rendered */
1590
- this.mirror = false;
1591
- // physical properties
1592
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
1593
- this.mass = objectDefaultMass;
1594
- /** @property {number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
1595
- this.damping = objectDefaultDamping;
1596
- /** @property {number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
1597
- this.angleDamping = objectDefaultAngleDamping;
1598
- /** @property {number} [elasticity=objectDefaultElasticity] - How bouncy the object is when colliding (0-1) */
1599
- this.elasticity = objectDefaultElasticity;
1600
- /** @property {number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
1601
- this.friction = objectDefaultFriction;
1602
- /** @property {number} - How much to scale gravity by for this object */
1603
- this.gravityScale = 1;
1604
- /** @property {number} - Objects are sorted by render order */
1605
- this.renderOrder = renderOrder;
1606
- /** @property {Vector2} - Velocity of the object */
1607
- this.velocity = vec2();
1608
- /** @property {number} - Angular velocity of the object */
1609
- this.angleVelocity = 0;
1610
- /** @property {number} - Track when object was created */
1611
- this.spawnTime = time;
1612
- /** @property {Array<EngineObject>} - List of children of this object */
1613
- this.children = [];
1614
- /** @property {boolean} - Limit object speed using linear or circular math */
1615
- this.clampSpeedLinear = true;
1616
- /** @property {EngineObject} - Object we are standing on, if any */
1617
- this.groundObject = undefined;
1618
- // parent child system
1619
- /** @property {EngineObject} - Parent of object if in local space */
1620
- this.parent = undefined;
1621
- /** @property {Vector2} - Local position if child */
1622
- this.localPos = vec2();
1623
- /** @property {number} - Local angle if child */
1624
- this.localAngle = 0;
1625
- // collision flags
1626
- /** @property {boolean} - Object collides with the tile collision */
1627
- this.collideTiles = false;
1628
- /** @property {boolean} - Object collides with solid objects */
1629
- this.collideSolidObjects = false;
1630
- /** @property {boolean} - Object collides with and blocks other objects */
1631
- this.isSolid = false;
1632
- /** @property {boolean} - Object collides with raycasts */
1633
- this.collideRaycast = false;
1634
- // add to list of objects
1635
- engineObjects.push(this);
1636
- }
1637
- /** Update the object transform, called automatically by engine even when paused */
1638
- updateTransforms() {
1639
- const parent = this.parent;
1640
- if (parent) {
1641
- // copy parent pos/angle
1642
- const mirror = parent.getMirrorSign();
1643
- this.pos = this.localPos.multiply(vec2(mirror, 1)).rotate(parent.angle).add(parent.pos);
1644
- this.angle = mirror * this.localAngle + parent.angle;
1645
- }
1646
- // update children
1647
- for (const child of this.children)
1648
- child.updateTransforms();
1649
- }
1650
- /** Update the object physics, called automatically by engine once each frame */
1651
- update() {
1652
- // child objects do not have physics
1653
- if (this.parent)
1654
- return;
1655
- // limit max speed to prevent missing collisions
1656
- if (this.clampSpeedLinear) {
1657
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1658
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1659
- }
1660
- else {
1661
- const length2 = this.velocity.lengthSquared();
1662
- if (length2 > objectMaxSpeed * objectMaxSpeed) {
1663
- const s = objectMaxSpeed / length2 ** .5;
1664
- this.velocity.x *= s;
1665
- this.velocity.y *= s;
1666
- }
1667
- }
1668
- // apply physics
1669
- const oldPos = this.pos.copy();
1670
- this.velocity.x *= this.damping;
1671
- this.velocity.y *= this.damping;
1672
- if (this.mass) // don't apply gravity to static objects
1673
- this.velocity.y += gravity * this.gravityScale;
1674
- this.pos.x += this.velocity.x;
1675
- this.pos.y += this.velocity.y;
1676
- this.angle += this.angleVelocity *= this.angleDamping;
1677
- // physics sanity checks
1678
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1679
- ASSERT(this.damping >= 0 && this.damping <= 1);
1680
- if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
1681
- return;
1682
- const wasMovingDown = this.velocity.y < 0;
1683
- if (this.groundObject) {
1684
- // apply friction in local space of ground object
1685
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1686
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1687
- this.groundObject = undefined;
1688
- //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1689
- }
1690
- if (this.collideSolidObjects) {
1691
- // check collisions against solid objects
1692
- const epsilon = .001; // necessary to push slightly outside of the collision
1693
- for (const o of engineObjectsCollide) {
1694
- // non solid objects don't collide with each other
1695
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
1696
- continue;
1697
- // check collision
1698
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
1699
- continue;
1700
- // notify objects of collision and check if should be resolved
1701
- const collide1 = this.collideWithObject(o);
1702
- const collide2 = o.collideWithObject(this);
1703
- if (!collide1 || !collide2)
1704
- continue;
1705
- if (isOverlapping(oldPos, this.size, o.pos, o.size)) {
1706
- // if already was touching, try to push away
1707
- const deltaPos = oldPos.subtract(o.pos);
1708
- const length = deltaPos.length();
1709
- const pushAwayAccel = .001; // push away if already overlapping
1710
- const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel / length);
1711
- this.velocity = this.velocity.add(velocity);
1712
- if (o.mass) // push away if not fixed
1713
- o.velocity = o.velocity.subtract(velocity);
1714
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
1715
- continue;
1716
- }
1717
- // check for collision
1718
- const sizeBoth = this.size.add(o.size);
1719
- const smallStepUp = (oldPos.y - o.pos.y) * 2 > sizeBoth.y + gravity; // prefer to push up if small delta
1720
- const isBlockedX = abs(oldPos.y - o.pos.y) * 2 < sizeBoth.y;
1721
- const isBlockedY = abs(oldPos.x - o.pos.x) * 2 < sizeBoth.x;
1722
- const elasticity = max(this.elasticity, o.elasticity);
1723
- if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
1724
- {
1725
- // push outside object collision
1726
- this.pos.y = o.pos.y + (sizeBoth.y / 2 + epsilon) * sign(oldPos.y - o.pos.y);
1727
- if (o.groundObject && wasMovingDown || !o.mass) {
1728
- // set ground object if landed on something
1729
- if (wasMovingDown)
1730
- this.groundObject = o;
1731
- // bounce if other object is fixed or grounded
1732
- this.velocity.y *= -elasticity;
1733
- }
1734
- else if (o.mass) {
1735
- // inelastic collision
1736
- const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
1737
- // elastic collision
1738
- const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
1739
- + o.velocity.y * 2 * o.mass / (this.mass + o.mass);
1740
- const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
1741
- + this.velocity.y * 2 * this.mass / (this.mass + o.mass);
1742
- // lerp between elastic or inelastic based on elasticity
1743
- this.velocity.y = lerp(elasticity, inelastic, elastic0);
1744
- o.velocity.y = lerp(elasticity, inelastic, elastic1);
1745
- }
1746
- }
1747
- if (!smallStepUp && isBlockedX) // resolve x collision
1748
- {
1749
- // push outside collision
1750
- this.pos.x = o.pos.x + (sizeBoth.x / 2 + epsilon) * sign(oldPos.x - o.pos.x);
1751
- if (o.mass) {
1752
- // inelastic collision
1753
- const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
1754
- // elastic collision
1755
- const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
1756
- + o.velocity.x * 2 * o.mass / (this.mass + o.mass);
1757
- const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
1758
- + this.velocity.x * 2 * this.mass / (this.mass + o.mass);
1759
- // lerp between elastic or inelastic based on elasticity
1760
- this.velocity.x = lerp(elasticity, inelastic, elastic0);
1761
- o.velocity.x = lerp(elasticity, inelastic, elastic1);
1762
- }
1763
- else // bounce if other object is fixed
1764
- this.velocity.x *= -elasticity;
1765
- }
1766
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
1767
- }
1768
- }
1769
- if (this.collideTiles) {
1770
- // check collision against tiles
1771
- if (tileCollisionTest(this.pos, this.size, this)) {
1772
- // if already was stuck in collision, don't do anything
1773
- // this should not happen unless something starts in collision
1774
- if (!tileCollisionTest(oldPos, this.size, this)) {
1775
- // test which side we bounced off (or both if a corner)
1776
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1777
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1778
- if (isBlockedY || !isBlockedX) {
1779
- // bounce velocity
1780
- this.velocity.y *= -this.elasticity;
1781
- if (wasMovingDown) {
1782
- // adjust position to slightly above nearest tile boundary
1783
- // this prevents gap between object and ground
1784
- const epsilon = .0001;
1785
- this.pos.y = (oldPos.y - this.size.y / 2 | 0) + this.size.y / 2 + epsilon;
1786
- // set ground object to self for tile collision
1787
- this.groundObject = this;
1788
- }
1789
- else {
1790
- // move to previous position
1791
- this.pos.y = oldPos.y;
1792
- this.groundObject = undefined;
1793
- }
1794
- }
1795
- if (isBlockedX) {
1796
- // move to previous position and bounce
1797
- this.pos.x = oldPos.x;
1798
- this.velocity.x *= -this.elasticity;
1799
- }
1800
- debugOverlay && debugPhysics && debugRect(this.pos, this.size, '#f00');
1801
- }
1802
- }
1803
- }
1804
- }
1805
- /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
1806
- render() {
1807
- // default object render
1808
- drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
1809
- }
1810
- /** Destroy this object, destroy it's children, detach it's parent, and mark it for removal */
1811
- destroy() {
1812
- if (this.destroyed)
1813
- return;
1814
- // disconnect from parent and destroy children
1815
- this.destroyed = 1;
1816
- this.parent && this.parent.removeChild(this);
1817
- for (const child of this.children)
1818
- child.destroy(child.parent = 0);
1819
- }
1820
- /** Convert from local space to world space
1821
- * @param {Vector2} pos - local space point */
1822
- localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
1823
- /** Convert from world space to local space
1824
- * @param {Vector2} pos - world space point */
1825
- worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
1826
- /** Convert from local space to world space for a vector (rotation only)
1827
- * @param {Vector2} vec - local space vector */
1828
- localToWorldVector(vec) { return vec.rotate(this.angle); }
1829
- /** Convert from world space to local space for a vector (rotation only)
1830
- * @param {Vector2} vec - world space vector */
1831
- worldToLocalVector(vec) { return vec.rotate(-this.angle); }
1832
- /** Called to check if a tile collision should be resolved
1833
- * @param {number} tileData - the value of the tile at the position
1834
- * @param {Vector2} pos - tile where the collision occurred
1835
- * @return {boolean} - true if the collision should be resolved */
1836
- collideWithTile(tileData, pos) { return tileData > 0; }
1837
- /** Called to check if a object collision should be resolved
1838
- * @param {EngineObject} object - the object to test against
1839
- * @return {boolean} - true if the collision should be resolved
1840
- */
1841
- collideWithObject(object) { return true; }
1842
- /** How long since the object was created
1843
- * @return {number} */
1844
- getAliveTime() { return time - this.spawnTime; }
1845
- /** Apply acceleration to this object (adjust velocity, not affected by mass)
1846
- * @param {Vector2} acceleration */
1847
- applyAcceleration(acceleration) { if (this.mass)
1848
- this.velocity = this.velocity.add(acceleration); }
1849
- /** Apply force to this object (adjust velocity, affected by mass)
1850
- * @param {Vector2} force */
1851
- applyForce(force) { this.applyAcceleration(force.scale(1 / this.mass)); }
1852
- /** Get the direction of the mirror
1853
- * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
1854
- getMirrorSign() { return this.mirror ? -1 : 1; }
1855
- /** Attaches a child to this with a given local transform
1856
- * @param {EngineObject} child
1857
- * @param {Vector2} [localPos=(0,0)]
1858
- * @param {number} [localAngle] */
1859
- addChild(child, localPos = vec2(), localAngle = 0) {
1860
- ASSERT(!child.parent && !this.children.includes(child));
1861
- this.children.push(child);
1862
- child.parent = this;
1863
- child.localPos = localPos.copy();
1864
- child.localAngle = localAngle;
1865
- }
1866
- /** Removes a child from this one
1867
- * @param {EngineObject} child */
1868
- removeChild(child) {
1869
- ASSERT(child.parent == this && this.children.includes(child));
1870
- this.children.splice(this.children.indexOf(child), 1);
1871
- child.parent = 0;
1872
- }
1873
- /** Set how this object collides
1874
- * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
1875
- * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
1876
- * @param {boolean} [collideTiles] - Does it collide with the tile collision?
1877
- * @param {boolean} [collideRaycast] - Does it collide with raycasts? */
1878
- setCollision(collideSolidObjects = true, isSolid = true, collideTiles = true, collideRaycast = true) {
1879
- ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
1880
- this.collideSolidObjects = collideSolidObjects;
1881
- this.isSolid = isSolid;
1882
- this.collideTiles = collideTiles;
1883
- this.collideRaycast = collideRaycast;
1884
- }
1885
- /** Returns string containing info about this object for debugging
1886
- * @return {string} */
1887
- toString() {
1888
- if (debug) {
1889
- let text = 'type = ' + this.constructor.name;
1890
- if (this.pos.x || this.pos.y)
1891
- text += '\npos = ' + this.pos;
1892
- if (this.velocity.x || this.velocity.y)
1893
- text += '\nvelocity = ' + this.velocity;
1894
- if (this.size.x || this.size.y)
1895
- text += '\nsize = ' + this.size;
1896
- if (this.angle)
1897
- text += '\nangle = ' + this.angle.toFixed(3);
1898
- if (this.color)
1899
- text += '\ncolor = ' + this.color;
1900
- return text;
1901
- }
1902
- }
1903
- /** Render debug info for this object */
1904
- renderDebugInfo() {
1905
- if (debug) {
1906
- // show object info for debugging
1907
- const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
1908
- const color1 = rgb(this.collideTiles ? 1 : 0, this.collideSolidObjects ? 1 : 0, this.isSolid ? 1 : 0, this.parent ? .2 : .5);
1909
- const color2 = this.parent ? rgb(1, 1, 1, .5) : rgb(0, 0, 0, .8);
1910
- drawRect(this.pos, size, color1, this.angle, false);
1911
- drawRect(this.pos, size.scale(.8), color2, this.angle, false);
1912
- this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(0, 0, 1, .5), false);
1913
- }
1914
- }
1915
- }
1916
- /**
1917
- * LittleJS Drawing System
1918
- * - Hybrid system with both Canvas2D and WebGL available
1919
- * - Super fast tile sheet rendering with WebGL
1920
- * - Can apply rotation, mirror, color and additive color
1921
- * - Font rendering system with built in engine font
1922
- * - Many useful utility functions
1923
- *
1924
- * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
1925
- * There are 3 canvas/contexts available to draw to...
1926
- * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
1927
- * glCanvas - Used by the accelerated WebGL batch rendering system.
1928
- * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1929
- *
1930
- * The WebGL rendering system is very fast with some caveats...
1931
- * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1932
- * - Group additive rendering together using renderOrder to mitigate this issue
1933
- *
1934
- * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
1935
- * @namespace Draw
1936
- */
1937
- /** The primary 2D canvas visible to the user
1938
- * @type {HTMLCanvasElement}
1939
- * @memberof Draw */
1940
- let mainCanvas;
1941
- /** 2d context for mainCanvas
1942
- * @type {CanvasRenderingContext2D}
1943
- * @memberof Draw */
1944
- let mainContext;
1945
- /** A canvas that appears on top of everything the same size as mainCanvas
1946
- * @type {HTMLCanvasElement}
1947
- * @memberof Draw */
1948
- let overlayCanvas;
1949
- /** 2d context for overlayCanvas
1950
- * @type {CanvasRenderingContext2D}
1951
- * @memberof Draw */
1952
- let overlayContext;
1953
- /** The size of the main canvas (and other secondary canvases)
1954
- * @type {Vector2}
1955
- * @memberof Draw */
1956
- let mainCanvasSize = vec2();
1957
- /** Array containing texture info for batch rendering system
1958
- * @type {Array<TextureInfo>}
1959
- * @memberof Draw */
1960
- let textureInfos = [];
1961
- // Keep track of how many draw calls there were each frame for debugging
1962
- let drawCount;
1963
- ///////////////////////////////////////////////////////////////////////////////
1964
- /**
1965
- * Create a tile info object using a grid based system
1966
- * - This can take vecs or floats for easier use and conversion
1967
- * - If an index is passed in, the tile size and index will determine the position
1968
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
1969
- * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
1970
- * @param {number} [textureIndex] - Texture index to use
1971
- * @param {number} [padding] - How many pixels padding around tiles
1972
- * @return {TileInfo}
1973
- * @example
1974
- * tile(2) // a tile at index 2 using the default tile size of 16
1975
- * tile(5, 8) // a tile at index 5 using a tile size of 8
1976
- * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
1977
- * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
1978
- * @memberof Draw
1979
- */
1980
- function tile(pos = vec2(), size = tileSizeDefault, textureIndex = 0, padding = 0) {
1981
- if (headlessMode)
1982
- return new TileInfo;
1983
- // if size is a number, make it a vector
1984
- if (typeof size === 'number') {
1985
- ASSERT(size > 0);
1986
- size = vec2(size);
1987
- }
1988
- // use pos as a tile index
1989
- const textureInfo = textureInfos[textureIndex];
1990
- ASSERT(!!textureInfo, 'Texture not loaded');
1991
- const sizePadded = size.add(vec2(padding * 2));
1992
- if (typeof pos === 'number') {
1993
- const cols = textureInfo.size.x / sizePadded.x | 0;
1994
- pos = cols > 0 ? vec2(pos % cols, pos / cols | 0) : vec2();
1995
- }
1996
- pos = vec2(pos.x * sizePadded.x + padding, pos.y * sizePadded.y + padding);
1997
- // return a tile info object
1998
- return new TileInfo(pos, size, textureIndex, padding);
1999
- }
2000
- /**
2001
- * Tile Info - Stores info about how to draw a tile
2002
- */
2003
- class TileInfo {
2004
- /** Create a tile info object
2005
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
2006
- * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
2007
- * @param {number} [textureIndex] - Texture index to use
2008
- * @param {number} [padding] - How many pixels padding around tiles
2009
- */
2010
- constructor(pos = vec2(), size = tileSizeDefault, textureIndex = 0, padding = 0) {
2011
- /** @property {Vector2} - Top left corner of tile in pixels */
2012
- this.pos = pos.copy();
2013
- /** @property {Vector2} - Size of tile in pixels */
2014
- this.size = size.copy();
2015
- /** @property {number} - Texture index to use */
2016
- this.textureIndex = textureIndex;
2017
- /** @property {number} - How many pixels padding around tiles */
2018
- this.padding = padding;
2019
- }
2020
- /** Returns a copy of this tile offset by a vector
2021
- * @param {Vector2} offset - Offset to apply in pixels
2022
- * @return {TileInfo}
2023
- */
2024
- offset(offset) { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex); }
2025
- /** Returns a copy of this tile offset by a number of animation frames
2026
- * @param {number} frame - Offset to apply in animation frames
2027
- * @return {TileInfo}
2028
- */
2029
- frame(frame) {
2030
- ASSERT(typeof frame == 'number');
2031
- return this.offset(vec2(frame * (this.size.x + this.padding * 2), 0));
2032
- }
2033
- /** Returns the texture info for this tile
2034
- * @return {TextureInfo}
2035
- */
2036
- getTextureInfo() { return textureInfos[this.textureIndex]; }
2037
- }
2038
- /** Texture Info - Stores info about each texture */
2039
- class TextureInfo {
2040
- /**
2041
- * Create a TextureInfo, called automatically by the engine
2042
- * @param {HTMLImageElement} image
2043
- */
2044
- constructor(image) {
2045
- /** @property {HTMLImageElement} - image source */
2046
- this.image = image;
2047
- /** @property {Vector2} - size of the image */
2048
- this.size = vec2(image.width, image.height);
2049
- /** @property {WebGLTexture} - webgl texture */
2050
- this.glTexture = glEnable && glCreateTexture(image);
2051
- }
2052
- }
2053
- ///////////////////////////////////////////////////////////////////////////////
2054
- /** Convert from screen to world space coordinates
2055
- * @param {Vector2} screenPos
2056
- * @return {Vector2}
2057
- * @memberof Draw */
2058
- function screenToWorld(screenPos) {
2059
- return new Vector2((screenPos.x - mainCanvasSize.x / 2 + .5) / cameraScale + cameraPos.x, (screenPos.y - mainCanvasSize.y / 2 + .5) / -cameraScale + cameraPos.y);
2060
- }
2061
- /** Convert from world to screen space coordinates
2062
- * @param {Vector2} worldPos
2063
- * @return {Vector2}
2064
- * @memberof Draw */
2065
- function worldToScreen(worldPos) {
2066
- return new Vector2((worldPos.x - cameraPos.x) * cameraScale + mainCanvasSize.x / 2 - .5, (worldPos.y - cameraPos.y) * -cameraScale + mainCanvasSize.y / 2 - .5);
2067
- }
2068
- /** Get the camera's visible area in world space
2069
- * @return {Vector2}
2070
- * @memberof Draw */
2071
- function getCameraSize() { return mainCanvasSize.scale(1 / cameraScale); }
2072
- /** Draw textured tile centered in world space, with color applied if using WebGL
2073
- * @param {Vector2} pos - Center of the tile in world space
2074
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
2075
- * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
2076
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2077
- * @param {number} [angle] - Angle to rotate by
2078
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
2079
- * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2080
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2081
- * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2082
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2083
- * @memberof Draw */
2084
- function drawTile(pos, size = vec2(1), tileInfo, color = new Color, angle = 0, mirror, additiveColor = new Color(0, 0, 0, 0), useWebGL = glEnable, screenSpace, context) {
2085
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2086
- ASSERT(typeof tileInfo !== 'number' || !tileInfo, 'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
2087
- ASSERT(isVector2(pos) && isVector2(size));
2088
- ASSERT(isColor(color) && isColor(additiveColor));
2089
- const textureInfo = tileInfo && tileInfo.getTextureInfo();
2090
- if (useWebGL) {
2091
- if (screenSpace) {
2092
- // convert to world space
2093
- pos = screenToWorld(pos);
2094
- size = size.scale(1 / cameraScale);
2095
- }
2096
- if (textureInfo) {
2097
- // calculate uvs and render
2098
- const sizeInverse = vec2(1).divide(textureInfo.size);
2099
- const x = tileInfo.pos.x * sizeInverse.x;
2100
- const y = tileInfo.pos.y * sizeInverse.y;
2101
- const w = tileInfo.size.x * sizeInverse.x;
2102
- const h = tileInfo.size.y * sizeInverse.y;
2103
- const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2104
- glSetTexture(textureInfo.glTexture);
2105
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle, x + tileImageFixBleed.x, y + tileImageFixBleed.y, x - tileImageFixBleed.x + w, y - tileImageFixBleed.y + h, color.rgbaInt(), additiveColor.rgbaInt());
2106
- }
2107
- else {
2108
- // if no tile info, force untextured
2109
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2110
- }
2111
- }
2112
- else {
2113
- // normal canvas 2D rendering method (slower)
2114
- showWatermark && ++drawCount;
2115
- size = vec2(size.x, -size.y); // fix upside down sprites
2116
- drawCanvas2D(pos, size, angle, mirror, (context) => {
2117
- if (textureInfo) {
2118
- // calculate uvs and render
2119
- const x = tileInfo.pos.x + tileFixBleedScale;
2120
- const y = tileInfo.pos.y + tileFixBleedScale;
2121
- const w = tileInfo.size.x - 2 * tileFixBleedScale;
2122
- const h = tileInfo.size.y - 2 * tileFixBleedScale;
2123
- context.globalAlpha = color.a; // only alpha is supported
2124
- context.drawImage(textureInfo.image, x, y, w, h, -.5, -.5, 1, 1);
2125
- context.globalAlpha = 1; // set back to full alpha
2126
- }
2127
- else {
2128
- // if no tile info, force untextured
2129
- context.fillStyle = color.toString();
2130
- context.fillRect(-.5, -.5, 1, 1);
2131
- }
2132
- }, screenSpace, context);
2133
- }
2134
- }
2135
- /** Draw colored rect centered on pos
2136
- * @param {Vector2} pos
2137
- * @param {Vector2} [size=(1,1)]
2138
- * @param {Color} [color=(1,1,1,1)]
2139
- * @param {number} [angle]
2140
- * @param {boolean} [useWebGL=glEnable]
2141
- * @param {boolean} [screenSpace=false]
2142
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2143
- * @memberof Draw */
2144
- function drawRect(pos, size, color, angle, useWebGL, screenSpace, context) {
2145
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2146
- }
2147
- /** Draw colored line between two points
2148
- * @param {Vector2} posA
2149
- * @param {Vector2} posB
2150
- * @param {number} [thickness]
2151
- * @param {Color} [color=(1,1,1,1)]
2152
- * @param {boolean} [useWebGL=glEnable]
2153
- * @param {boolean} [screenSpace=false]
2154
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2155
- * @memberof Draw */
2156
- function drawLine(posA, posB, thickness = .1, color, useWebGL, screenSpace, context) {
2157
- const halfDelta = vec2((posB.x - posA.x) / 2, (posB.y - posA.y) / 2);
2158
- const size = vec2(thickness, halfDelta.length() * 2);
2159
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
2160
- }
2161
- /** Draw colored polygon using passed in points
2162
- * @param {Array<Vector2>} points - Array of Vector2 points
2163
- * @param {Color} [color=(1,1,1,1)]
2164
- * @param {number} [lineWidth=0]
2165
- * @param {Color} [lineColor=(0,0,0,1)]
2166
- * @param {boolean} [screenSpace=false]
2167
- * @param {CanvasRenderingContext2D} [context=mainContext]
2168
- * @memberof Draw */
2169
- function drawPoly(points, color = new Color, lineWidth = 0, lineColor = new Color(0, 0, 0), screenSpace, context = mainContext) {
2170
- ASSERT(isColor(color) && isColor(lineColor));
2171
- context.fillStyle = color.toString();
2172
- context.beginPath();
2173
- for (const point of screenSpace ? points : points.map(worldToScreen))
2174
- context.lineTo(point.x, point.y);
2175
- context.closePath();
2176
- context.fill();
2177
- if (lineWidth) {
2178
- context.strokeStyle = lineColor.toString();
2179
- context.lineWidth = screenSpace ? lineWidth : lineWidth * cameraScale;
2180
- context.stroke();
2181
- }
2182
- }
2183
- /** Draw colored ellipse using passed in point
2184
- * @param {Vector2} pos
2185
- * @param {number} [width=1]
2186
- * @param {number} [height=1]
2187
- * @param {number} [angle=0]
2188
- * @param {Color} [color=(1,1,1,1)]
2189
- * @param {number} [lineWidth=0]
2190
- * @param {Color} [lineColor=(0,0,0,1)]
2191
- * @param {boolean} [screenSpace=false]
2192
- * @param {CanvasRenderingContext2D} [context=mainContext]
2193
- * @memberof Draw */
2194
- function drawEllipse(pos, width = 1, height = 1, angle = 0, color = new Color, lineWidth = 0, lineColor = new Color(0, 0, 0), screenSpace, context = mainContext) {
2195
- ASSERT(isColor(color) && isColor(lineColor));
2196
- if (!screenSpace) {
2197
- pos = worldToScreen(pos);
2198
- width *= cameraScale;
2199
- height *= cameraScale;
2200
- lineWidth *= cameraScale;
2201
- }
2202
- context.fillStyle = color.toString();
2203
- context.beginPath();
2204
- context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
2205
- context.fill();
2206
- if (lineWidth) {
2207
- context.strokeStyle = lineColor.toString();
2208
- context.lineWidth = lineWidth;
2209
- context.stroke();
2210
- }
2211
- }
2212
- /** Draw colored circle using passed in point
2213
- * @param {Vector2} pos
2214
- * @param {number} [radius=1]
2215
- * @param {Color} [color=(1,1,1,1)]
2216
- * @param {number} [lineWidth=0]
2217
- * @param {Color} [lineColor=(0,0,0,1)]
2218
- * @param {boolean} [screenSpace=false]
2219
- * @param {CanvasRenderingContext2D} [context=mainContext]
2220
- * @memberof Draw */
2221
- function drawCircle(pos, radius = 1, color = new Color, lineWidth = 0, lineColor = new Color(0, 0, 0), screenSpace, context = mainContext) { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
2222
- /** Draw directly to a 2d canvas context in world space
2223
- * @param {Vector2} pos
2224
- * @param {Vector2} size
2225
- * @param {number} angle
2226
- * @param {boolean} mirror
2227
- * @param {Function} drawFunction
2228
- * @param {boolean} [screenSpace=false]
2229
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2230
- * @memberof Draw */
2231
- function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context = mainContext) {
2232
- if (!screenSpace) {
2233
- // transform from world space to screen space
2234
- pos = worldToScreen(pos);
2235
- size = size.scale(cameraScale);
2236
- }
2237
- context.save();
2238
- context.translate(pos.x + .5, pos.y + .5);
2239
- context.rotate(angle);
2240
- context.scale(mirror ? -size.x : size.x, -size.y);
2241
- drawFunction(context);
2242
- context.restore();
2243
- }
2244
- /** Draw text on main canvas in world space
2245
- * Automatically splits new lines into rows
2246
- * @param {string} text
2247
- * @param {Vector2} pos
2248
- * @param {number} [size]
2249
- * @param {Color} [color=(1,1,1,1)]
2250
- * @param {number} [lineWidth]
2251
- * @param {Color} [lineColor=(0,0,0,1)]
2252
- * @param {CanvasTextAlign} [textAlign='center']
2253
- * @param {string} [font=fontDefault]
2254
- * @param {number} [maxWidth]
2255
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2256
- * @memberof Draw */
2257
- function drawText(text, pos, size = 1, color, lineWidth = 0, lineColor, textAlign, font, maxWidth, context = mainContext) {
2258
- drawTextScreen(text, worldToScreen(pos), size * cameraScale, color, lineWidth * cameraScale, lineColor, textAlign, font, maxWidth, context);
2259
- }
2260
- /** Draw text on overlay canvas in world space
2261
- * Automatically splits new lines into rows
2262
- * @param {string} text
2263
- * @param {Vector2} pos
2264
- * @param {number} [size]
2265
- * @param {Color} [color=(1,1,1,1)]
2266
- * @param {number} [lineWidth]
2267
- * @param {Color} [lineColor=(0,0,0,1)]
2268
- * @param {CanvasTextAlign} [textAlign='center']
2269
- * @param {string} [font=fontDefault]
2270
- * @param {number} [maxWidth]
2271
- * @memberof Draw */
2272
- function drawTextOverlay(text, pos, size = 1, color, lineWidth = 0, lineColor, textAlign, font, maxWidth) {
2273
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
2274
- }
2275
- /** Draw text on overlay canvas in screen space
2276
- * Automatically splits new lines into rows
2277
- * @param {string} text
2278
- * @param {Vector2} pos
2279
- * @param {number} [size]
2280
- * @param {Color} [color=(1,1,1,1)]
2281
- * @param {number} [lineWidth]
2282
- * @param {Color} [lineColor=(0,0,0,1)]
2283
- * @param {CanvasTextAlign} [textAlign]
2284
- * @param {string} [font=fontDefault]
2285
- * @param {number} [maxWidth]
2286
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2287
- * @memberof Draw */
2288
- function drawTextScreen(text, pos, size = 1, color = new Color, lineWidth = 0, lineColor = new Color(0, 0, 0), textAlign = 'center', font = fontDefault, maxWidth = undefined, context = overlayContext) {
2289
- context.fillStyle = color.toString();
2290
- context.lineWidth = lineWidth;
2291
- context.strokeStyle = lineColor.toString();
2292
- context.textAlign = textAlign;
2293
- context.font = size + 'px ' + font;
2294
- context.textBaseline = 'middle';
2295
- context.lineJoin = 'round';
2296
- pos = pos.copy();
2297
- const lines = (text + '').split('\n');
2298
- pos.y -= (lines.length - 1) * size / 2; // center text vertically
2299
- lines.forEach(line => {
2300
- lineWidth && context.strokeText(line, pos.x, pos.y, maxWidth);
2301
- context.fillText(line, pos.x, pos.y, maxWidth);
2302
- pos.y += size;
2303
- });
2304
- }
2305
- /** Enable normal or additive blend mode
2306
- * @param {boolean} [additive]
2307
- * @param {boolean} [useWebGL=glEnable]
2308
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2309
- * @memberof Draw */
2310
- function setBlendMode(additive, useWebGL = glEnable, context) {
2311
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2312
- if (useWebGL)
2313
- glAdditive = additive;
2314
- else {
2315
- if (!context)
2316
- context = mainContext;
2317
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
2318
- }
2319
- }
2320
- /** Combines all LittleJS canvases onto the main canvas and clears them
2321
- * This is necessary for things like saving a screenshot
2322
- * @memberof Draw */
2323
- function combineCanvases() {
2324
- // combine canvases
2325
- glCopyToContext(mainContext, true);
2326
- mainContext.drawImage(overlayCanvas, 0, 0);
2327
- // clear canvases
2328
- glClearCanvas();
2329
- overlayCanvas.width |= 0;
2330
- }
2331
- ///////////////////////////////////////////////////////////////////////////////
2332
- let engineFontImage;
2333
- /**
2334
- * Font Image Object - Draw text on a 2D canvas by using characters in an image
2335
- * - 96 characters (from space to tilde) are stored in an image
2336
- * - Uses a default 8x8 font if none is supplied
2337
- * - You can also use fonts from the main tile sheet
2338
- * @example
2339
- * // use built in font
2340
- * const font = new FontImage;
2341
- *
2342
- * // draw text
2343
- * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
2344
- */
2345
- class FontImage {
2346
- /** Create an image font
2347
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2348
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2349
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2350
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2351
- */
2352
- constructor(image, tileSize = vec2(8), paddingSize = vec2(0, 1), context = overlayContext) {
2353
- // load default font image
2354
- if (!engineFontImage)
2355
- (engineFontImage = new Image).src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2356
- this.image = image || engineFontImage;
2357
- this.tileSize = tileSize;
2358
- this.paddingSize = paddingSize;
2359
- this.context = context;
2360
- }
2361
- /** Draw text in world space using the image font
2362
- * @param {string} text
2363
- * @param {Vector2} pos
2364
- * @param {number} [scale=.25]
2365
- * @param {boolean} [center]
2366
- */
2367
- drawText(text, pos, scale = 1, center) {
2368
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale * cameraScale | 0, center);
2369
- }
2370
- /** Draw text in screen space using the image font
2371
- * @param {string} text
2372
- * @param {Vector2} pos
2373
- * @param {number} [scale]
2374
- * @param {boolean} [center]
2375
- */
2376
- drawTextScreen(text, pos, scale = 4, center) {
2377
- const context = this.context;
2378
- context.save();
2379
- const size = this.tileSize;
2380
- const drawSize = size.add(this.paddingSize).scale(scale);
2381
- const cols = this.image.width / this.tileSize.x | 0;
2382
- (text + '').split('\n').forEach((line, i) => {
2383
- const centerOffset = center ? line.length * size.x * scale / 2 | 0 : 0;
2384
- for (let j = line.length; j--;) {
2385
- // draw each character
2386
- let charCode = line[j].charCodeAt(0);
2387
- if (charCode < 32 || charCode > 127)
2388
- charCode = 127; // unknown character
2389
- // get the character source location and draw it
2390
- const tile = charCode - 32;
2391
- const x = tile % cols;
2392
- const y = tile / cols | 0;
2393
- const drawPos = pos.add(vec2(j, i).multiply(drawSize));
2394
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y, drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
2395
- }
2396
- });
2397
- context.restore();
2398
- }
2399
- }
2400
- ///////////////////////////////////////////////////////////////////////////////
2401
- // Display functions
2402
- /** Returns true if fullscreen mode is active
2403
- * @return {boolean}
2404
- * @memberof Draw */
2405
- function isFullscreen() { return !!document.fullscreenElement; }
2406
- /** Toggle fullscreen mode
2407
- * @memberof Draw */
2408
- function toggleFullscreen() {
2409
- const rootElement = mainCanvas.parentElement;
2410
- if (isFullscreen()) {
2411
- if (document.exitFullscreen)
2412
- document.exitFullscreen();
2413
- }
2414
- else if (rootElement.requestFullscreen)
2415
- rootElement.requestFullscreen();
2416
- }
2417
- /** Set the cursor style
2418
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
2419
- * @memberof Draw */
2420
- function setCursor(cursorStyle = 'auto') {
2421
- const rootElement = mainCanvas.parentElement;
2422
- rootElement.style.cursor = cursorStyle;
2423
- }
2424
- /**
2425
- * LittleJS Input System
2426
- * - Tracks keyboard down, pressed, and released
2427
- * - Tracks mouse buttons, position, and wheel
2428
- * - Tracks multiple analog gamepads
2429
- * - Touch input is handled as mouse input
2430
- * - Virtual gamepad for touch devices
2431
- * @namespace Input
2432
- */
2433
- /** Returns true if device key is down
2434
- * @param {string|number} key
2435
- * @param {number} [device]
2436
- * @return {boolean}
2437
- * @memberof Input */
2438
- function keyIsDown(key, device = 0) {
2439
- ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
2440
- return inputData[device] && !!(inputData[device][key] & 1);
2441
- }
2442
- /** Returns true if device key was pressed this frame
2443
- * @param {string|number} key
2444
- * @param {number} [device]
2445
- * @return {boolean}
2446
- * @memberof Input */
2447
- function keyWasPressed(key, device = 0) {
2448
- ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
2449
- return inputData[device] && !!(inputData[device][key] & 2);
2450
- }
2451
- /** Returns true if device key was released this frame
2452
- * @param {string|number} key
2453
- * @param {number} [device]
2454
- * @return {boolean}
2455
- * @memberof Input */
2456
- function keyWasReleased(key, device = 0) {
2457
- ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
2458
- return inputData[device] && !!(inputData[device][key] & 4);
2459
- }
2460
- /** Returns input vector from arrow keys or WASD if enabled
2461
- * @return {Vector2}
2462
- * @memberof Input */
2463
- function keyDirection(up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight') {
2464
- const k = (key) => keyIsDown(key) ? 1 : 0;
2465
- return vec2(k(right) - k(left), k(up) - k(down));
2466
- }
2467
- /** Clears all input
2468
- * @memberof Input */
2469
- function clearInput() { inputData = [[]]; touchGamepadButtons = []; }
2470
- /** Returns true if mouse button is down
2471
- * @function
2472
- * @param {number} button
2473
- * @return {boolean}
2474
- * @memberof Input */
2475
- const mouseIsDown = keyIsDown;
2476
- /** Returns true if mouse button was pressed
2477
- * @function
2478
- * @param {number} button
2479
- * @return {boolean}
2480
- * @memberof Input */
2481
- const mouseWasPressed = keyWasPressed;
2482
- /** Returns true if mouse button was released
2483
- * @function
2484
- * @param {number} button
2485
- * @return {boolean}
2486
- * @memberof Input */
2487
- const mouseWasReleased = keyWasReleased;
2488
- /** Mouse pos in world space
2489
- * @type {Vector2}
2490
- * @memberof Input */
2491
- let mousePos = vec2();
2492
- /** Mouse pos in screen space
2493
- * @type {Vector2}
2494
- * @memberof Input */
2495
- let mousePosScreen = vec2();
2496
- /** Mouse wheel delta this frame
2497
- * @type {number}
2498
- * @memberof Input */
2499
- let mouseWheel = 0;
2500
- /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
2501
- * @type {boolean}
2502
- * @memberof Input */
2503
- let isUsingGamepad = false;
2504
- /** Prevents input continuing to the default browser handling (false by default)
2505
- * @type {boolean}
2506
- * @memberof Input */
2507
- let preventDefaultInput = false;
2508
- /** Returns true if gamepad button is down
2509
- * @param {number} button
2510
- * @param {number} [gamepad]
2511
- * @return {boolean}
2512
- * @memberof Input */
2513
- function gamepadIsDown(button, gamepad = 0) { return keyIsDown(button, gamepad + 1); }
2514
- /** Returns true if gamepad button was pressed
2515
- * @param {number} button
2516
- * @param {number} [gamepad]
2517
- * @return {boolean}
2518
- * @memberof Input */
2519
- function gamepadWasPressed(button, gamepad = 0) { return keyWasPressed(button, gamepad + 1); }
2520
- /** Returns true if gamepad button was released
2521
- * @param {number} button
2522
- * @param {number} [gamepad]
2523
- * @return {boolean}
2524
- * @memberof Input */
2525
- function gamepadWasReleased(button, gamepad = 0) { return keyWasReleased(button, gamepad + 1); }
2526
- /** Returns gamepad stick value
2527
- * @param {number} stick
2528
- * @param {number} [gamepad]
2529
- * @return {Vector2}
2530
- * @memberof Input */
2531
- function gamepadStick(stick, gamepad = 0) { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2532
- ///////////////////////////////////////////////////////////////////////////////
2533
- // Input system functions called automatically by engine
2534
- // input is stored as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
2535
- // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
2536
- let inputData = [[]];
2537
- function inputUpdate() {
2538
- if (headlessMode)
2539
- return;
2540
- // clear input when lost focus (prevent stuck keys)
2541
- if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
2542
- clearInput();
2543
- // update mouse world space position
2544
- mousePos = screenToWorld(mousePosScreen);
2545
- // update gamepads if enabled
2546
- gamepadsUpdate();
2547
- }
2548
- function inputUpdatePost() {
2549
- if (headlessMode)
2550
- return;
2551
- // clear input to prepare for next frame
2552
- for (const deviceInputData of inputData)
2553
- for (const i in deviceInputData)
2554
- deviceInputData[i] &= 1;
2555
- mouseWheel = 0;
2556
- }
2557
- function inputInit() {
2558
- if (headlessMode)
2559
- return;
2560
- onkeydown = (e) => {
2561
- if (!e.repeat) {
2562
- isUsingGamepad = false;
2563
- inputData[0][e.code] = 3;
2564
- if (inputWASDEmulateDirection)
2565
- inputData[0][remapKey(e.code)] = 3;
2566
- }
2567
- preventDefaultInput && e.preventDefault();
2568
- };
2569
- onkeyup = (e) => {
2570
- inputData[0][e.code] = 4;
2571
- if (inputWASDEmulateDirection)
2572
- inputData[0][remapKey(e.code)] = 4;
2573
- };
2574
- // handle remapping wasd keys to directions
2575
- function remapKey(c) {
2576
- return inputWASDEmulateDirection ?
2577
- c == 'KeyW' ? 'ArrowUp' :
2578
- c == 'KeyS' ? 'ArrowDown' :
2579
- c == 'KeyA' ? 'ArrowLeft' :
2580
- c == 'KeyD' ? 'ArrowRight' : c : c;
2581
- }
2582
- // mouse event handlers
2583
- onmousedown = (e) => {
2584
- // fix stalled audio requiring user interaction
2585
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
2586
- audioContext.resume();
2587
- isUsingGamepad = false;
2588
- inputData[0][e.button] = 3;
2589
- mousePosScreen = mouseEventToScreen(e);
2590
- e.button && e.preventDefault();
2591
- };
2592
- onmouseup = (e) => inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2593
- onmousemove = (e) => mousePosScreen = mouseEventToScreen(e);
2594
- onwheel = (e) => mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2595
- oncontextmenu = (e) => false; // prevent right click menu
2596
- onblur = (e) => clearInput(); // reset input when focus is lost
2597
- // init touch input
2598
- if (isTouchDevice && touchInputEnable)
2599
- touchInputInit();
2600
- }
2601
- // convert a mouse or touch event position to screen space
2602
- function mouseEventToScreen(mousePos) {
2603
- const rect = mainCanvas.getBoundingClientRect();
2604
- const px = percent(mousePos.x, rect.left, rect.right);
2605
- const py = percent(mousePos.y, rect.top, rect.bottom);
2606
- return vec2(px * mainCanvas.width, py * mainCanvas.height);
2607
- }
2608
- ///////////////////////////////////////////////////////////////////////////////
2609
- // Gamepad input
2610
- // gamepad internal variables
2611
- const gamepadStickData = [];
2612
- // gamepads are updated by engine every frame automatically
2613
- function gamepadsUpdate() {
2614
- const applyDeadZones = (v) => {
2615
- const min = .3, max = .8;
2616
- const deadZone = (v) => v > min ? percent(v, min, max) :
2617
- v < -min ? -percent(-v, min, max) : 0;
2618
- return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
2619
- };
2620
- // update touch gamepad if enabled
2621
- if (touchGamepadEnable && isTouchDevice) {
2622
- ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2623
- if (touchGamepadTimer.isSet()) {
2624
- // read virtual analog stick
2625
- const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2626
- sticks[0] = vec2();
2627
- if (touchGamepadAnalog)
2628
- sticks[0] = applyDeadZones(touchGamepadStick);
2629
- else if (touchGamepadStick.lengthSquared() > .3) {
2630
- // convert to 8 way dpad
2631
- sticks[0].x = Math.round(touchGamepadStick.x);
2632
- sticks[0].y = -Math.round(touchGamepadStick.y);
2633
- sticks[0] = sticks[0].clampLength();
2634
- }
2635
- // read virtual gamepad buttons
2636
- const data = inputData[1] || (inputData[1] = []);
2637
- for (let i = 10; i--;) {
2638
- const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2639
- const wasDown = gamepadIsDown(j, 0);
2640
- data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2641
- }
2642
- }
2643
- }
2644
- // return if gamepads are disabled or not supported
2645
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
2646
- return;
2647
- // only poll gamepads when focused or in debug mode
2648
- if (!debug && !document.hasFocus())
2649
- return;
2650
- // poll gamepads
2651
- const gamepads = navigator.getGamepads();
2652
- for (let i = gamepads.length; i--;) {
2653
- // get or create gamepad data
2654
- const gamepad = gamepads[i];
2655
- const data = inputData[i + 1] || (inputData[i + 1] = []);
2656
- const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2657
- if (gamepad) {
2658
- // read analog sticks
2659
- for (let j = 0; j < gamepad.axes.length - 1; j += 2)
2660
- sticks[j >> 1] = applyDeadZones(vec2(gamepad.axes[j], gamepad.axes[j + 1]));
2661
- // read buttons
2662
- for (let j = gamepad.buttons.length; j--;) {
2663
- const button = gamepad.buttons[j];
2664
- const wasDown = gamepadIsDown(j, i);
2665
- data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2666
- isUsingGamepad || (isUsingGamepad = !i && button.pressed);
2667
- }
2668
- if (gamepadDirectionEmulateStick) {
2669
- // copy dpad to left analog stick when pressed
2670
- const dpad = vec2((gamepadIsDown(15, i) && 1) - (gamepadIsDown(14, i) && 1), (gamepadIsDown(12, i) && 1) - (gamepadIsDown(13, i) && 1));
2671
- if (dpad.lengthSquared())
2672
- sticks[0] = dpad.clampLength();
2673
- }
2674
- // disable touch gamepad if using real gamepad
2675
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
2676
- }
2677
- }
2678
- }
2679
- ///////////////////////////////////////////////////////////////////////////////
2680
- /** Pulse the vibration hardware if it exists
2681
- * @param {number|Array} [pattern] - single value in ms or vibration interval array
2682
- * @memberof Input */
2683
- function vibrate(pattern = 100) { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2684
- /** Cancel any ongoing vibration
2685
- * @memberof Input */
2686
- function vibrateStop() { vibrate(0); }
2687
- ///////////////////////////////////////////////////////////////////////////////
2688
- // Touch input & virtual on screen gamepad
2689
- /** True if a touch device has been detected
2690
- * @memberof Input */
2691
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
2692
- // touch gamepad internal variables
2693
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2694
- // enable touch input mouse passthrough
2695
- function touchInputInit() {
2696
- // add non passive touch event listeners
2697
- let handleTouch = handleTouchDefault;
2698
- if (touchGamepadEnable) {
2699
- // touch input internal variables
2700
- handleTouch = handleTouchGamepad;
2701
- touchGamepadButtons = [];
2702
- touchGamepadStick = vec2();
2703
- }
2704
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
2705
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
2706
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
2707
- // override mouse events
2708
- onmousedown = onmouseup = () => 0;
2709
- // handle all touch events the same way
2710
- let wasTouching;
2711
- function handleTouchDefault(e) {
2712
- // fix stalled audio requiring user interaction
2713
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
2714
- audioContext.resume();
2715
- // check if touching and pass to mouse events
2716
- const touching = e.touches.length;
2717
- const button = 0; // all touches are left mouse button
2718
- if (touching) {
2719
- // set event pos and pass it along
2720
- const p = vec2(e.touches[0].clientX, e.touches[0].clientY);
2721
- mousePosScreen = mouseEventToScreen(p);
2722
- wasTouching ? isUsingGamepad = touchGamepadEnable : inputData[0][button] = 3;
2723
- }
2724
- else if (wasTouching)
2725
- inputData[0][button] = inputData[0][button] & 2 | 4;
2726
- // set was touching
2727
- wasTouching = touching;
2728
- // prevent default handling like copy and magnifier lens
2729
- if (document.hasFocus()) // allow document to get focus
2730
- e.preventDefault();
2731
- // must return true so the document will get focus
2732
- return true;
2733
- }
2734
- // special handling for virtual gamepad mode
2735
- function handleTouchGamepad(e) {
2736
- // clear touch gamepad input
2737
- touchGamepadStick = vec2();
2738
- touchGamepadButtons = [];
2739
- isUsingGamepad = true;
2740
- const touching = e.touches.length;
2741
- if (touching) {
2742
- touchGamepadTimer.set();
2743
- if (paused && !wasTouching) {
2744
- // touch anywhere to press start when paused
2745
- touchGamepadButtons[9] = 1;
2746
- // call default touch handler so normal touch events still work
2747
- handleTouchDefault(e);
2748
- return;
2749
- }
2750
- }
2751
- // get center of left and right sides
2752
- const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y - touchGamepadSize);
2753
- const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2754
- const startCenter = mainCanvasSize.scale(.5);
2755
- // check each touch point
2756
- for (const touch of e.touches) {
2757
- const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
2758
- if (touchPos.distance(stickCenter) < touchGamepadSize) {
2759
- // virtual analog stick
2760
- touchGamepadStick = touchPos.subtract(stickCenter).scale(2 / touchGamepadSize).clampLength();
2761
- }
2762
- else if (touchPos.distance(buttonCenter) < touchGamepadSize) {
2763
- // virtual face buttons
2764
- const button = touchPos.subtract(buttonCenter).direction();
2765
- touchGamepadButtons[button] = 1;
2766
- }
2767
- else if (touchPos.distance(startCenter) < touchGamepadSize && !wasTouching) {
2768
- // virtual start button in center
2769
- touchGamepadButtons[9] = 1;
2770
- }
2771
- }
2772
- // call default touch handler so normal touch events still work
2773
- handleTouchDefault(e);
2774
- // must return true so the document will get focus
2775
- return true;
2776
- }
2777
- }
2778
- // render the touch gamepad, called automatically by the engine
2779
- function touchGamepadRender() {
2780
- if (!touchInputEnable || !isTouchDevice || headlessMode)
2781
- return;
2782
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
2783
- return;
2784
- // fade off when not touching or paused
2785
- const alpha = percent(touchGamepadTimer.get(), 4, 3);
2786
- if (!alpha || paused)
2787
- return;
2788
- // setup the canvas
2789
- const context = overlayContext;
2790
- context.save();
2791
- context.globalAlpha = alpha * touchGamepadAlpha;
2792
- context.strokeStyle = '#fff';
2793
- context.lineWidth = 3;
2794
- // draw left analog stick
2795
- context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2796
- context.beginPath();
2797
- const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y - touchGamepadSize);
2798
- if (touchGamepadAnalog) // draw circle shaped gamepad
2799
- {
2800
- context.arc(leftCenter.x, leftCenter.y, touchGamepadSize / 2, 0, 9);
2801
- context.fill();
2802
- context.stroke();
2803
- }
2804
- else // draw cross shaped gamepad
2805
- {
2806
- for (let i = 10; i--;) {
2807
- const angle = i * PI / 4;
2808
- context.arc(leftCenter.x, leftCenter.y, touchGamepadSize * .6, angle + PI / 8, angle + PI / 8);
2809
- i % 2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize * .33, angle, angle);
2810
- i == 1 && context.fill();
2811
- }
2812
- context.stroke();
2813
- }
2814
- // draw right face buttons
2815
- const rightCenter = vec2(mainCanvasSize.x - touchGamepadSize, mainCanvasSize.y - touchGamepadSize);
2816
- for (let i = 4; i--;) {
2817
- const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize / 2));
2818
- context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2819
- context.beginPath();
2820
- context.arc(pos.x, pos.y, touchGamepadSize / 4, 0, 9);
2821
- context.fill();
2822
- context.stroke();
2823
- }
2824
- // set canvas back to normal
2825
- context.restore();
2826
- }
2827
- /**
2828
- * LittleJS Audio System
2829
- * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
2830
- * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
2831
- * - Caches sounds and music for fast playback
2832
- * - Can attenuate and apply stereo panning to sounds
2833
- * - Ability to play mp3, ogg, and wave files
2834
- * - Speech synthesis functions
2835
- * @namespace Audio
2836
- */
2837
- /** Audio context used by the engine
2838
- * @type {AudioContext}
2839
- * @memberof Audio */
2840
- let audioContext = new AudioContext;
2841
- /** Master gain node for all audio to pass through
2842
- * @type {GainNode}
2843
- * @memberof Audio */
2844
- let audioGainNode;
2845
- function audioInit() {
2846
- if (!soundEnable || headlessMode)
2847
- return;
2848
- // (createGain is more widely supported then GainNode constructor)
2849
- audioGainNode = audioContext.createGain();
2850
- audioGainNode.connect(audioContext.destination);
2851
- audioGainNode.gain.value = soundVolume; // set starting value
2852
- }
2853
- ///////////////////////////////////////////////////////////////////////////////
2854
- /**
2855
- * Sound Object - Stores a sound for later use and can be played positionally
2856
- *
2857
- * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
2858
- * @example
2859
- * // create a sound
2860
- * const sound_example = new Sound([.5,.5]);
2861
- *
2862
- * // play the sound
2863
- * sound_example.play();
2864
- */
2865
- class Sound {
2866
- /** Create a sound object and cache the zzfx samples for later use
2867
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
2868
- * @param {number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2869
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
2870
- */
2871
- constructor(zzfxSound, range = soundDefaultRange, taper = soundDefaultTaper) {
2872
- if (!soundEnable || headlessMode)
2873
- return;
2874
- /** @property {number} - World space max range of sound, will not play if camera is farther away */
2875
- this.range = range;
2876
- /** @property {number} - At what percentage of range should it start tapering off */
2877
- this.taper = taper;
2878
- /** @property {number} - How much to randomize frequency each time sound plays */
2879
- this.randomness = 0;
2880
- if (zzfxSound) {
2881
- // generate zzfx sound now for fast playback
2882
- const defaultRandomness = .05;
2883
- this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
2884
- zzfxSound[1] = 0; // generate without randomness
2885
- this.sampleChannels = [zzfxG(...zzfxSound)];
2886
- this.sampleRate = zzfxR;
2887
- }
2888
- }
2889
- /** Play the sound
2890
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
2891
- * @param {number} [volume] - How much to scale volume by (in addition to range fade)
2892
- * @param {number} [pitch] - How much to scale pitch by (also adjusted by this.randomness)
2893
- * @param {number} [randomnessScale] - How much to scale randomness
2894
- * @param {boolean} [loop] - Should the sound loop
2895
- * @return {AudioBufferSourceNode} - The audio source node
2896
- */
2897
- play(pos, volume = 1, pitch = 1, randomnessScale = 1, loop = false) {
2898
- if (!soundEnable || headlessMode)
2899
- return;
2900
- if (!this.sampleChannels)
2901
- return;
2902
- let pan;
2903
- if (pos) {
2904
- const range = this.range;
2905
- if (range) {
2906
- // apply range based fade
2907
- const lengthSquared = cameraPos.distanceSquared(pos);
2908
- if (lengthSquared > range * range)
2909
- return; // out of range
2910
- // attenuate volume by distance
2911
- volume *= percent(lengthSquared ** .5, range, range * this.taper);
2912
- }
2913
- // get pan from screen space coords
2914
- pan = worldToScreen(pos).x * 2 / mainCanvas.width - 1;
2915
- }
2916
- // play the sound
2917
- const playbackRate = pitch + pitch * this.randomness * randomnessScale * rand(-1, 1);
2918
- this.gainNode = audioContext.createGain();
2919
- this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
2920
- return this.source;
2921
- }
2922
- /** Set the sound volume of the most recently played instance of this sound
2923
- * @param {number} [volume] - How much to scale volume by
2924
- */
2925
- setVolume(volume = 1) {
2926
- if (this.gainNode)
2927
- this.gainNode.gain.value = volume;
2928
- }
2929
- /** Stop the last instance of this sound that was played */
2930
- stop() {
2931
- if (this.source)
2932
- this.source.stop();
2933
- this.source = undefined;
2934
- }
2935
- /** Get source of most recent instance of this sound that was played
2936
- * @return {AudioBufferSourceNode}
2937
- */
2938
- getSource() { return this.source; }
2939
- /** Play the sound as a note with a semitone offset
2940
- * @param {number} semitoneOffset - How many semitones to offset pitch
2941
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
2942
- * @param {number} [volume=1] - How much to scale volume by (in addition to range fade)
2943
- * @return {AudioBufferSourceNode} - The audio source node
2944
- */
2945
- playNote(semitoneOffset, pos, volume) { return this.play(pos, volume, 2 ** (semitoneOffset / 12), 0); }
2946
- /** Get how long this sound is in seconds
2947
- * @return {number} - How long the sound is in seconds (undefined if loading)
2948
- */
2949
- getDuration() { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
2950
- /** Check if sound is loading, for sounds fetched from a url
2951
- * @return {boolean} - True if sound is loading and not ready to play
2952
- */
2953
- isLoading() { return !this.sampleChannels; }
2954
- }
2955
- /**
2956
- * Sound Wave Object - Stores a wave sound for later use and can be played positionally
2957
- * - this can be used to play wave, mp3, and ogg files
2958
- * @example
2959
- * // create a sound
2960
- * const sound_example = new SoundWave('sound.mp3');
2961
- *
2962
- * // play the sound
2963
- * sound_example.play();
2964
- */
2965
- class SoundWave extends Sound {
2966
- /** Create a sound object and cache the wave file for later use
2967
- * @param {string} filename - Filename of audio file to load
2968
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
2969
- * @param {number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2970
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2971
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
2972
- */
2973
- constructor(filename, randomness = 0, range, taper, onloadCallback) {
2974
- super(undefined, range, taper);
2975
- if (!soundEnable || headlessMode)
2976
- return;
2977
- this.randomness = randomness;
2978
- fetch(filename)
2979
- .then(response => response.arrayBuffer())
2980
- .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
2981
- .then(audioBuffer => {
2982
- this.sampleChannels = [];
2983
- for (let i = audioBuffer.numberOfChannels; i--;)
2984
- this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
2985
- this.sampleRate = audioBuffer.sampleRate;
2986
- }).then(() => onloadCallback && onloadCallback(this));
2987
- }
2988
- }
2989
- /** Play an mp3, ogg, or wav audio from a local file or url
2990
- * @param {string} filename - Location of sound file to play
2991
- * @param {number} [volume] - How much to scale volume by
2992
- * @param {boolean} [loop] - True if the music should loop
2993
- * @return {SoundWave} - The sound object for this file
2994
- * @memberof Audio */
2995
- function playAudioFile(filename, volume = 1, loop = false) {
2996
- if (!soundEnable || headlessMode)
2997
- return;
2998
- return new SoundWave(filename, 0, 0, 0, s => s.play(undefined, volume, 1, 1, loop));
2999
- }
3000
- /**
3001
- * Music Object - Stores a zzfx music track for later use
3002
- *
3003
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
3004
- * @example
3005
- * // create some music
3006
- * const music_example = new Music(
3007
- * [
3008
- * [ // instruments
3009
- * [,0,400] // simple note
3010
- * ],
3011
- * [ // patterns
3012
- * [ // pattern 1
3013
- * [ // channel 0
3014
- * 0, -1, // instrument 0, left speaker
3015
- * 1, 0, 9, 1 // channel notes
3016
- * ],
3017
- * [ // channel 1
3018
- * 0, 1, // instrument 0, right speaker
3019
- * 0, 12, 17, -1 // channel notes
3020
- * ]
3021
- * ],
3022
- * ],
3023
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
3024
- * 90 // BPM
3025
- * ]);
3026
- *
3027
- * // play the music
3028
- * music_example.play();
3029
- */
3030
- class Music extends Sound {
3031
- /** Create a music object and cache the zzfx music samples for later use
3032
- * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
3033
- */
3034
- constructor(zzfxMusic) {
3035
- super(undefined);
3036
- if (!soundEnable || headlessMode)
3037
- return;
3038
- this.randomness = 0;
3039
- this.sampleChannels = zzfxM(...zzfxMusic);
3040
- this.sampleRate = zzfxR;
3041
- }
3042
- /** Play the music
3043
- * @param {number} [volume=1] - How much to scale volume by
3044
- * @param {boolean} [loop] - True if the music should loop
3045
- * @return {AudioBufferSourceNode} - The audio source node
3046
- */
3047
- playMusic(volume, loop = false) { return super.play(undefined, volume, 1, 1, loop); }
3048
- }
3049
- /** Speak text with passed in settings
3050
- * @param {string} text - The text to speak
3051
- * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
3052
- * @param {number} [volume] - How much to scale volume by
3053
- * @param {number} [rate] - How quickly to speak
3054
- * @param {number} [pitch] - How much to change the pitch by
3055
- * @return {SpeechSynthesisUtterance} - The utterance that was spoken
3056
- * @memberof Audio */
3057
- function speak(text, language = '', volume = 1, rate = 1, pitch = 1) {
3058
- if (!soundEnable || headlessMode)
3059
- return;
3060
- if (!speechSynthesis)
3061
- return;
3062
- // common languages (not supported by all browsers)
3063
- // en - english, it - italian, fr - french, de - german, es - spanish
3064
- // ja - japanese, ru - russian, zh - chinese, hi - hindi, ko - korean
3065
- // build utterance and speak
3066
- const utterance = new SpeechSynthesisUtterance(text);
3067
- utterance.lang = language;
3068
- utterance.volume = 2 * volume * soundVolume;
3069
- utterance.rate = rate;
3070
- utterance.pitch = pitch;
3071
- speechSynthesis.speak(utterance);
3072
- return utterance;
3073
- }
3074
- /** Stop all queued speech
3075
- * @memberof Audio */
3076
- function speakStop() { speechSynthesis && speechSynthesis.cancel(); }
3077
- /** Get frequency of a note on a musical scale
3078
- * @param {number} semitoneOffset - How many semitones away from the root note
3079
- * @param {number} [rootFrequency=220] - Frequency at semitone offset 0
3080
- * @return {number} - The frequency of the note
3081
- * @memberof Audio */
3082
- function getNoteFrequency(semitoneOffset, rootFrequency = 220) { return rootFrequency * 2 ** (semitoneOffset / 12); }
3083
- ///////////////////////////////////////////////////////////////////////////////
3084
- /** Play cached audio samples with given settings
3085
- * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3086
- * @param {number} [volume] - How much to scale volume by
3087
- * @param {number} [rate] - The playback rate to use
3088
- * @param {number} [pan] - How much to apply stereo panning
3089
- * @param {boolean} [loop] - True if the sound should loop when it reaches the end
3090
- * @param {number} [sampleRate=44100] - Sample rate for the sound
3091
- * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
3092
- * @return {AudioBufferSourceNode} - The audio node of the sound played
3093
- * @memberof Audio */
3094
- function playSamples(sampleChannels, volume = 1, rate = 1, pan = 0, loop = false, sampleRate = zzfxR, gainNode) {
3095
- if (!soundEnable || headlessMode)
3096
- return;
3097
- // create buffer and source
3098
- const channelCount = sampleChannels.length;
3099
- const sampleLength = sampleChannels[0].length;
3100
- const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
3101
- const source = audioContext.createBufferSource();
3102
- // copy samples to buffer and setup source
3103
- sampleChannels.forEach((c, i) => buffer.getChannelData(i).set(c));
3104
- source.buffer = buffer;
3105
- source.playbackRate.value = rate;
3106
- source.loop = loop;
3107
- // create and connect gain node
3108
- gainNode = gainNode || audioContext.createGain();
3109
- gainNode.gain.value = volume;
3110
- gainNode.connect(audioGainNode);
3111
- // connect source to stereo panner and gain
3112
- const pannerNode = new StereoPannerNode(audioContext, { 'pan': clamp(pan, -1, 1) });
3113
- source.connect(pannerNode).connect(gainNode);
3114
- // play the sound
3115
- if (audioContext.state != 'running') {
3116
- // fix stalled audio and play
3117
- audioContext.resume().then(() => source.start());
3118
- }
3119
- else
3120
- source.start();
3121
- // return sound
3122
- return source;
3123
- }
3124
- ///////////////////////////////////////////////////////////////////////////////
3125
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
3126
- /** Generate and play a ZzFX sound
3127
- *
3128
- * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
3129
- * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3130
- * @return {AudioBufferSourceNode} - The audio node of the sound played
3131
- * @memberof Audio */
3132
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3133
- /** Sample rate used for all ZzFX sounds
3134
- * @default 44100
3135
- * @memberof Audio */
3136
- const zzfxR = 44100;
3137
- /** Generate samples for a ZzFX sound
3138
- * @param {number} [volume] - Volume scale (percent)
3139
- * @param {number} [randomness] - How much to randomize frequency (percent Hz)
3140
- * @param {number} [frequency] - Frequency of sound (Hz)
3141
- * @param {number} [attack] - Attack time, how fast sound starts (seconds)
3142
- * @param {number} [sustain] - Sustain time, how long sound holds (seconds)
3143
- * @param {number} [release] - Release time, how fast sound fades out (seconds)
3144
- * @param {number} [shape] - Shape of the sound wave
3145
- * @param {number} [shapeCurve] - Squareness of wave (0=square, 1=normal, 2=pointy)
3146
- * @param {number} [slide] - How much to slide frequency (kHz/s)
3147
- * @param {number} [deltaSlide] - How much to change slide (kHz/s/s)
3148
- * @param {number} [pitchJump] - Frequency of pitch jump (Hz)
3149
- * @param {number} [pitchJumpTime] - Time of pitch jump (seconds)
3150
- * @param {number} [repeatTime] - Resets some parameters periodically (seconds)
3151
- * @param {number} [noise] - How much random noise to add (percent)
3152
- * @param {number} [modulation] - Frequency of modulation wave, negative flips phase (Hz)
3153
- * @param {number} [bitCrush] - Resamples at a lower frequency in (samples*100)
3154
- * @param {number} [delay] - Overlap sound with itself for reverb and flanger effects (seconds)
3155
- * @param {number} [sustainVolume] - Volume level for sustain (percent)
3156
- * @param {number} [decay] - Decay time, how long to reach sustain after attack (seconds)
3157
- * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
3158
- * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
3159
- * @return {Array} - Array of audio samples
3160
- * @memberof Audio
3161
- */
3162
- function zzfxG(
3163
- // parameters
3164
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0, release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0, pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0, bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0) {
3165
- // LJS Note: ZZFX modded so randomness is handled by Sound class
3166
- // init parameters
3167
- let PI2 = PI * 2, sampleRate = zzfxR, startSlide = slide *= 500 * PI2 / sampleRate / sampleRate, startFrequency = frequency *=
3168
- rand(1 + randomness, 1 - randomness) * PI2 / sampleRate, b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3169
- // biquad LP/HP filter
3170
- quality = 2, w = PI2 * abs(filter) * 2 / sampleRate, cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality, a0 = 1 + alpha, a1 = -2 * cos / a0, a2 = (1 - alpha) / a0, b0 = (1 + sign(filter) * cos) / 2 / a0, b1 = -(sign(filter) + cos) / a0, b2 = b0, x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3171
- // scale by sample rate
3172
- attack = attack * sampleRate + 9; // minimum attack to prevent pop
3173
- decay *= sampleRate;
3174
- sustain *= sampleRate;
3175
- release *= sampleRate;
3176
- delay *= sampleRate;
3177
- deltaSlide *= 500 * PI2 / sampleRate ** 3;
3178
- modulation *= PI2 / sampleRate;
3179
- pitchJump *= PI2 / sampleRate;
3180
- pitchJumpTime *= sampleRate;
3181
- repeatTime = repeatTime * sampleRate | 0;
3182
- // generate waveform
3183
- for (length = attack + decay + sustain + release + delay | 0; i < length; b[i++] = s * volume) // sample
3184
- {
3185
- if (!(++c % (bitCrush * 100 | 0))) // bit crush
3186
- {
3187
- s = shape ? shape > 1 ? shape > 2 ? shape > 3 ? // wave shape
3188
- Math.sin(t ** 3) : // 4 noise
3189
- clamp(Math.tan(t), 1, -1) : // 3 tan
3190
- 1 - (2 * t / PI2 % 2 + 2) % 2 : // 2 saw
3191
- 1 - 4 * abs(Math.round(t / PI2) - t / PI2) : // 1 triangle
3192
- Math.sin(t); // 0 sin
3193
- s = (repeatTime ?
3194
- 1 - tremolo + tremolo * Math.sin(PI2 * i / repeatTime) // tremolo
3195
- : 1) *
3196
- sign(s) * (abs(s) ** shapeCurve) * // curve
3197
- (i < attack ? i / attack : // attack
3198
- i < attack + decay ? // decay
3199
- 1 - ((i - attack) / decay) * (1 - sustainVolume) : // decay falloff
3200
- i < attack + decay + sustain ? // sustain
3201
- sustainVolume : // sustain volume
3202
- i < length - delay ? // release
3203
- (length - i - delay) / release * // release falloff
3204
- sustainVolume : // release volume
3205
- 0); // post release
3206
- s = delay ? s / 2 + (delay > i ? 0 : // delay
3207
- (i < length - delay ? 1 : (length - i) / delay) * // release delay
3208
- b[i - delay | 0] / 2 / volume) : s; // sample delay
3209
- if (filter) // apply filter
3210
- s = y1 = b2 * x2 + b1 * (x2 = x1) + b0 * (x1 = s) - a2 * y2 - a1 * (y2 = y1);
3211
- }
3212
- f = (frequency += slide += deltaSlide) * // frequency
3213
- Math.cos(modulation * tm++); // modulation
3214
- t += f + f * noise * Math.sin(i ** 5); // noise
3215
- if (j && ++j > pitchJumpTime) // pitch jump
3216
- {
3217
- frequency += pitchJump; // apply pitch jump
3218
- startFrequency += pitchJump; // also apply to start
3219
- j = 0; // stop pitch jump time
3220
- }
3221
- if (repeatTime && !(++r % repeatTime)) // repeat
3222
- {
3223
- frequency = startFrequency; // reset frequency
3224
- slide = startSlide; // reset slide
3225
- j = j || 1; // reset pitch jump time
3226
- }
3227
- }
3228
- return b;
3229
- }
3230
- ///////////////////////////////////////////////////////////////////////////////
3231
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
3232
- /** Generate samples for a ZzFM song with given parameters
3233
- * @param {Array} instruments - Array of ZzFX sound parameters
3234
- * @param {Array} patterns - Array of pattern data
3235
- * @param {Array} sequence - Array of pattern indexes
3236
- * @param {number} [BPM] - Playback speed of the song in BPM
3237
- * @return {Array} - Left and right channel sample data
3238
- * @memberof Audio */
3239
- function zzfxM(instruments, patterns, sequence, BPM = 125) {
3240
- let i, j, k;
3241
- let instrumentParameters;
3242
- let note;
3243
- let sample;
3244
- let patternChannel;
3245
- let notFirstBeat;
3246
- let stop;
3247
- let instrument;
3248
- let attenuation;
3249
- let outSampleOffset;
3250
- let isSequenceEnd;
3251
- let sampleOffset = 0;
3252
- let nextSampleOffset;
3253
- let sampleBuffer = [];
3254
- let leftChannelBuffer = [];
3255
- let rightChannelBuffer = [];
3256
- let channelIndex = 0;
3257
- let panning = 0;
3258
- let hasMore = 1;
3259
- let sampleCache = {};
3260
- let beatLength = zzfxR / BPM * 60 >> 2;
3261
- // for each channel in order until there are no more
3262
- for (; hasMore; channelIndex++) {
3263
- // reset current values
3264
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
3265
- // for each pattern in sequence
3266
- sequence.forEach((patternIndex, sequenceIndex) => {
3267
- // get pattern for current channel, use empty 1 note pattern if none found
3268
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
3269
- // check if there are more channels
3270
- hasMore |= patterns[patternIndex][channelIndex] && 1;
3271
- // get next offset, use the length of first channel
3272
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat ? 0 : 1)) * beatLength;
3273
- // for each beat in pattern, plus one extra if end of sequence
3274
- isSequenceEnd = sequenceIndex == sequence.length - 1;
3275
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
3276
- // <channel-note>
3277
- note = patternChannel[i];
3278
- // stop if end, different instrument or new note
3279
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
3280
- instrument != (patternChannel[0] || 0) || note | 0;
3281
- // fill buffer with samples for previous beat, most cpu intensive part
3282
- for (j = 0; j < beatLength && notFirstBeat;
3283
- // fade off attenuation at end of beat if stopping note, prevents clicking
3284
- j++ > beatLength - 99 && stop && attenuation < 1 ? attenuation += 1 / 99 : 0) {
3285
- // copy sample to stereo buffers with panning
3286
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
3287
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
3288
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
3289
- }
3290
- // set up for next note
3291
- if (note) {
3292
- // set attenuation
3293
- attenuation = note % 1;
3294
- panning = patternChannel[1] || 0;
3295
- if (note |= 0) {
3296
- // get cached sample
3297
- sampleBuffer = sampleCache[[
3298
- instrument = patternChannel[sampleOffset = 0] || 0,
3299
- note
3300
- ]] = sampleCache[[instrument, note]] || (
3301
- // add sample to cache
3302
- instrumentParameters = [...instruments[instrument]],
3303
- instrumentParameters[2] *= 2 ** ((note - 12) / 12),
3304
- // allow negative values to stop notes
3305
- note > 0 ? zzfxG(...instrumentParameters) : []);
3306
- }
3307
- }
3308
- }
3309
- // update the sample offset
3310
- outSampleOffset = nextSampleOffset;
3311
- });
3312
- }
3313
- return [leftChannelBuffer, rightChannelBuffer];
3314
- }
3315
- /**
3316
- * LittleJS Tile Layer System
3317
- * - Caches arrays of tiles to off screen canvas for fast rendering
3318
- * - Unlimited numbers of layers, allocates canvases as needed
3319
- * - Interfaces with EngineObject for collision
3320
- * - Collision layer is separate from visible layers
3321
- * - It is recommended to have a visible layer that matches the collision
3322
- * - Tile layers can be drawn to using their context with canvas2d
3323
- * - Drawn directly to the main canvas without using WebGL
3324
- * @namespace TileCollision
3325
- */
3326
- /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
3327
- * @type {Array<number>}
3328
- * @memberof TileCollision */
3329
- let tileCollision = [];
3330
- /** Size of the tile collision layer 2d grid
3331
- * @type {Vector2}
3332
- * @memberof TileCollision */
3333
- let tileCollisionSize = vec2();
3334
- /** Clear and initialize tile collision
3335
- * @param {Vector2} size - width and height of tile collision 2d grid
3336
- * @memberof TileCollision */
3337
- function initTileCollision(size) {
3338
- tileCollisionSize = size;
3339
- tileCollision = [];
3340
- for (let i = tileCollision.length = tileCollisionSize.area(); i--;)
3341
- tileCollision[i] = 0;
3342
- }
3343
- /** Set tile collision data for a given cell in the grid
3344
- * @param {Vector2} pos
3345
- * @param {number} [data]
3346
- * @memberof TileCollision */
3347
- function setTileCollisionData(pos, data = 0) {
3348
- pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y | 0) * tileCollisionSize.x + pos.x | 0] = data);
3349
- }
3350
- /** Get tile collision data for a given cell in the grid
3351
- * @param {Vector2} pos
3352
- * @return {number}
3353
- * @memberof TileCollision */
3354
- function getTileCollisionData(pos) {
3355
- return pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y | 0) * tileCollisionSize.x + pos.x | 0] : 0;
3356
- }
3357
- /** Check if collision with another object should occur
3358
- * @param {Vector2} pos
3359
- * @param {Vector2} [size=(0,0)]
3360
- * @param {EngineObject} [object]
3361
- * @return {boolean}
3362
- * @memberof TileCollision */
3363
- function tileCollisionTest(pos, size = vec2(), object) {
3364
- const minX = max(pos.x - size.x / 2 | 0, 0);
3365
- const minY = max(pos.y - size.y / 2 | 0, 0);
3366
- const maxX = min(pos.x + size.x / 2, tileCollisionSize.x);
3367
- const maxY = min(pos.y + size.y / 2, tileCollisionSize.y);
3368
- for (let y = minY; y < maxY; ++y)
3369
- for (let x = minX; x < maxX; ++x) {
3370
- const tileData = tileCollision[y * tileCollisionSize.x + x];
3371
- if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3372
- return true;
3373
- }
3374
- return false;
3375
- }
3376
- /** Return the center of first tile hit, undefined if nothing was hit.
3377
- * This does not return the exact intersection, but the center of the tile hit.
3378
- * @param {Vector2} posStart
3379
- * @param {Vector2} posEnd
3380
- * @param {EngineObject} [object]
3381
- * @return {Vector2}
3382
- * @memberof TileCollision */
3383
- function tileCollisionRaycast(posStart, posEnd, object) {
3384
- // test if a ray collides with tiles from start to end
3385
- // todo: a way to get the exact hit point, it must still be inside the hit tile
3386
- const delta = posEnd.subtract(posStart);
3387
- const totalLength = delta.length();
3388
- const normalizedDelta = delta.normalize();
3389
- const unit = vec2(abs(1 / normalizedDelta.x), abs(1 / normalizedDelta.y));
3390
- const flooredPosStart = posStart.floor();
3391
- // setup iteration variables
3392
- let pos = flooredPosStart;
3393
- let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3394
- let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3395
- while (true) {
3396
- // check for tile collision
3397
- const tileData = getTileCollisionData(pos);
3398
- if (tileData && (!object || object.collideWithTile(tileData, pos))) {
3399
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
3400
- debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
3401
- return pos.add(vec2(.5));
3402
- }
3403
- // check if past the end
3404
- if (xi > totalLength && yi > totalLength)
3405
- break;
3406
- // get coordinates of the next tile to check
3407
- if (xi > yi)
3408
- pos.y += sign(delta.y), yi += unit.y;
3409
- else
3410
- pos.x += sign(delta.x), xi += unit.x;
3411
- }
3412
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
3413
- }
3414
- ///////////////////////////////////////////////////////////////////////////////
3415
- // Tile Layer Rendering System
3416
- /**
3417
- * Tile layer data object stores info about how to render a tile
3418
- * @example
3419
- * // create tile layer data with tile index 0 and random orientation and color
3420
- * const tileIndex = 0;
3421
- * const direction = randInt(4)
3422
- * const mirror = randInt(2);
3423
- * const color = randColor();
3424
- * const data = new TileLayerData(tileIndex, direction, mirror, color);
3425
- */
3426
- class TileLayerData {
3427
- /** Create a tile layer data object, one for each tile in a TileLayer
3428
- * @param {number} [tile] - The tile to use, untextured if undefined
3429
- * @param {number} [direction] - Integer direction of tile, in 90 degree increments
3430
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
3431
- * @param {Color} [color] - Color of the tile */
3432
- constructor(tile, direction = 0, mirror = false, color = new Color) {
3433
- /** @property {number} - The tile to use, untextured if undefined */
3434
- this.tile = tile;
3435
- /** @property {number} - Integer direction of tile, in 90 degree increments */
3436
- this.direction = direction;
3437
- /** @property {boolean} - If the tile should be mirrored along the x axis */
3438
- this.mirror = mirror;
3439
- /** @property {Color} - Color of the tile */
3440
- this.color = color;
3441
- }
3442
- /** Set this tile to clear, it will not be rendered */
3443
- clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
3444
- }
3445
- /**
3446
- * Tile Layer - cached rendering system for tile layers
3447
- * - Each Tile layer is rendered to an off screen canvas
3448
- * - To allow dynamic modifications, layers are rendered using canvas 2d
3449
- * - Some devices like mobile phones are limited to 4k texture resolution
3450
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
3451
- * @extends EngineObject
3452
- * @example
3453
- * // create tile collision and visible tile layer
3454
- * initTileCollision(vec2(200,100));
3455
- * const tileLayer = new TileLayer();
3456
- */
3457
- class TileLayer extends EngineObject {
3458
- /** Create a tile layer object
3459
- * @param {Vector2} [position=(0,0)] - World space position
3460
- * @param {Vector2} [size=tileCollisionSize] - World space size
3461
- * @param {TileInfo} [tileInfo] - Tile info for layer
3462
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
3463
- * @param {number} [renderOrder] - Objects are sorted by renderOrder
3464
- */
3465
- constructor(position, size = tileCollisionSize, tileInfo = tile(), scale = vec2(1), renderOrder = 0) {
3466
- super(position, size, tileInfo, 0, undefined, renderOrder);
3467
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3468
- this.canvas = document.createElement('canvas');
3469
- /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3470
- this.context = this.canvas.getContext('2d');
3471
- /** @property {Vector2} - How much to scale this layer when rendered */
3472
- this.scale = scale;
3473
- /** @property {boolean} - If true this layer will render to overlay canvas and appear above all objects */
3474
- this.isOverlay = false;
3475
- // init tile data
3476
- this.data = [];
3477
- for (let j = this.size.area(); j--;)
3478
- this.data.push(new TileLayerData);
3479
- if (headlessMode) {
3480
- // disable rendering
3481
- this.redraw = () => { };
3482
- this.render = () => { };
3483
- this.redrawStart = () => { };
3484
- this.redrawEnd = () => { };
3485
- this.drawTileData = () => { };
3486
- this.drawCanvas2D = () => { };
3487
- }
3488
- }
3489
- /** Set data at a given position in the array
3490
- * @param {Vector2} layerPos - Local position in array
3491
- * @param {TileLayerData} data - Data to set
3492
- * @param {boolean} [redraw] - Force the tile to redraw if true */
3493
- setData(layerPos, data, redraw = false) {
3494
- if (layerPos.arrayCheck(this.size)) {
3495
- this.data[(layerPos.y | 0) * this.size.x + layerPos.x | 0] = data;
3496
- redraw && this.drawTileData(layerPos);
3497
- }
3498
- }
3499
- /** Get data at a given position in the array
3500
- * @param {Vector2} layerPos - Local position in array
3501
- * @return {TileLayerData} */
3502
- getData(layerPos) { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y | 0) * this.size.x + layerPos.x | 0]; }
3503
- // Tile layers are not updated
3504
- update() { }
3505
- // Render the tile layer, called automatically by the engine
3506
- render() {
3507
- ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3508
- // flush and copy gl canvas because tile canvas does not use webgl
3509
- !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3510
- // draw the entire cached level onto the canvas
3511
- let pos = worldToScreen(this.pos.add(vec2(0, this.size.y * this.scale.y)));
3512
- // fix canvas jitter in some browsers if position is not an integer
3513
- pos = pos.floor();
3514
- (this.isOverlay ? overlayContext : mainContext).drawImage(this.canvas, pos.x, pos.y, cameraScale * this.size.x * this.scale.x, cameraScale * this.size.y * this.scale.y);
3515
- }
3516
- /** Draw all the tile data to an offscreen canvas
3517
- * - This may be slow in some browsers but only needs to be done once */
3518
- redraw() {
3519
- this.redrawStart(true);
3520
- for (let x = this.size.x; x--;)
3521
- for (let y = this.size.y; y--;)
3522
- this.drawTileData(vec2(x, y), false);
3523
- this.redrawEnd();
3524
- }
3525
- /** Call to start the redraw process
3526
- * - This can be used to manually update small parts of the level
3527
- * @param {boolean} [clear] - Should it clear the canvas before drawing */
3528
- redrawStart(clear = false) {
3529
- // save current render settings
3530
- /** @type {[HTMLCanvasElement, CanvasRenderingContext2D, Vector2, Vector2, number]} */
3531
- this.savedRenderSettings = [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale];
3532
- // use webgl rendering system to render the tiles if enabled
3533
- // this works by temporally taking control of the rendering system
3534
- mainCanvas = this.canvas;
3535
- mainContext = this.context;
3536
- mainCanvasSize = this.size.multiply(this.tileInfo.size);
3537
- cameraPos = this.size.scale(.5);
3538
- cameraScale = this.tileInfo.size.x;
3539
- if (clear) {
3540
- // clear and set size
3541
- mainCanvas.width = mainCanvasSize.x;
3542
- mainCanvas.height = mainCanvasSize.y;
3543
- }
3544
- // disable smoothing for pixel art
3545
- this.context.imageSmoothingEnabled = !tilesPixelated;
3546
- // setup gl rendering if enabled
3547
- glPreRender();
3548
- }
3549
- /** Call to end the redraw process */
3550
- redrawEnd() {
3551
- ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3552
- glCopyToContext(mainContext, true);
3553
- //debugSaveCanvas(this.canvas);
3554
- // set stuff back to normal
3555
- [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
3556
- }
3557
- /** Draw the tile at a given position in the tile grid
3558
- * This can be used to clear out tiles when they are destroyed
3559
- * Tiles can also be redrawn if inside a redrawStart/End block
3560
- * @param {Vector2} layerPos
3561
- * @param {boolean} [clear] - should the old tile be cleared out
3562
- */
3563
- drawTileData(layerPos, clear = true) {
3564
- // clear out where the tile was, for full opaque tiles this can be skipped
3565
- const s = this.tileInfo.size;
3566
- if (clear) {
3567
- const pos = layerPos.multiply(s);
3568
- this.context.clearRect(pos.x, this.canvas.height - pos.y, s.x, -s.y);
3569
- }
3570
- // draw the tile if not undefined
3571
- const d = this.getData(layerPos);
3572
- if (d.tile != undefined) {
3573
- ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3574
- const pos = layerPos.add(vec2(.5));
3575
- const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
3576
- drawTile(pos, vec2(1), tileInfo, d.color, d.direction * PI / 2, d.mirror);
3577
- }
3578
- }
3579
- /** Draw directly to the 2D canvas in world space (bypass webgl)
3580
- * @param {Vector2} pos
3581
- * @param {Vector2} size
3582
- * @param {number} angle
3583
- * @param {boolean} mirror
3584
- * @param {Function} drawFunction */
3585
- drawCanvas2D(pos, size, angle, mirror, drawFunction) {
3586
- const context = this.context;
3587
- context.save();
3588
- pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
3589
- size = size.multiply(this.tileInfo.size);
3590
- context.translate(pos.x, this.canvas.height - pos.y);
3591
- context.rotate(angle);
3592
- context.scale(mirror ? -size.x : size.x, size.y);
3593
- drawFunction(context);
3594
- context.restore();
3595
- }
3596
- /** Draw a tile directly onto the layer canvas in world space
3597
- * @param {Vector2} pos
3598
- * @param {Vector2} [size=(1,1)]
3599
- * @param {TileInfo} [tileInfo]
3600
- * @param {Color} [color=(1,1,1,1)]
3601
- * @param {number} [angle=0]
3602
- * @param {boolean} [mirror=0] */
3603
- drawTile(pos, size = vec2(1), tileInfo, color = new Color, angle, mirror) {
3604
- this.drawCanvas2D(pos, size, angle, mirror, (context) => {
3605
- const textureInfo = tileInfo && tileInfo.getTextureInfo();
3606
- if (textureInfo) {
3607
- context.globalAlpha = color.a; // only alpha is supported
3608
- context.drawImage(textureInfo.image, tileInfo.pos.x, tileInfo.pos.y, tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
3609
- context.globalAlpha = 1;
3610
- }
3611
- else {
3612
- // untextured
3613
- context.fillStyle = color;
3614
- context.fillRect(-.5, -.5, 1, 1);
3615
- }
3616
- });
3617
- }
3618
- /** Draw a rectangle directly onto the layer canvas in world space
3619
- * @param {Vector2} pos
3620
- * @param {Vector2} [size=(1,1)]
3621
- * @param {Color} [color=(1,1,1,1)]
3622
- * @param {number} [angle=0] */
3623
- drawRect(pos, size, color, angle) { this.drawTile(pos, size, undefined, color, angle); }
3624
- }
3625
- /**
3626
- * LittleJS Particle System
3627
- */
3628
- /**
3629
- * Particle Emitter - Spawns particles with the given settings
3630
- * @extends EngineObject
3631
- * @example
3632
- * // create a particle emitter
3633
- * let pos = vec2(2,3);
3634
- * let particleEmitter = new ParticleEmitter
3635
- * (
3636
- * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitCone
3637
- * tile(0, 16), // tileInfo
3638
- * rgb(1,1,1), rgb(0,0,0), // colorStartA, colorStartB
3639
- * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
3640
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
3641
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
3642
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
3643
- * );
3644
- */
3645
- class ParticleEmitter extends EngineObject {
3646
- /** Create a particle system with the given settings
3647
- * @param {Vector2} position - World space position of the emitter
3648
- * @param {Number} [angle] - Angle to emit the particles
3649
- * @param {Number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
3650
- * @param {Number} [emitTime] - How long to stay alive (0 is forever)
3651
- * @param {Number} [emitRate] - How many particles per second to spawn, does not emit if 0
3652
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3653
- * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
3654
- * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
3655
- * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
3656
- * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
3657
- * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
3658
- * @param {Number} [particleTime] - How long particles live
3659
- * @param {Number} [sizeStart] - How big are particles at start
3660
- * @param {Number} [sizeEnd] - How big are particles at end
3661
- * @param {Number} [speed] - How fast are particles when spawned
3662
- * @param {Number} [angleSpeed] - How fast are particles rotating
3663
- * @param {Number} [damping] - How much to dampen particle speed
3664
- * @param {Number} [angleDamping] - How much to dampen particle angular speed
3665
- * @param {Number} [gravityScale] - How much gravity effect particles
3666
- * @param {Number} [particleConeAngle] - Cone for start particle angle
3667
- * @param {Number} [fadeRate] - How quick to fade particles at start/end in percent of life
3668
- * @param {Number} [randomness] - Apply extra randomness percent
3669
- * @param {boolean} [collideTiles] - Do particles collide against tiles
3670
- * @param {boolean} [additive] - Should particles use additive blend
3671
- * @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
3672
- * @param {Number} [renderOrder] - Render order for particles (additive is above other stuff by default)
3673
- * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
3674
- */
3675
- constructor(position, angle, emitSize = 0, emitTime = 0, emitRate = 100, emitConeAngle = PI, tileInfo, colorStartA = new Color, colorStartB = new Color, colorEndA = new Color(1, 1, 1, 0), colorEndB = new Color(1, 1, 1, 0), particleTime = .5, sizeStart = .1, sizeEnd = 1, speed = .1, angleSpeed = .05, damping = 1, angleDamping = 1, gravityScale = 0, particleConeAngle = PI, fadeRate = .1, randomness = .2, collideTiles = false, additive = false, randomColorLinear = true, renderOrder = additive ? 1e9 : 0, localSpace = false) {
3676
- super(position, vec2(), tileInfo, angle, undefined, renderOrder);
3677
- // emitter settings
3678
- /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
3679
- this.emitSize = emitSize;
3680
- /** @property {Number} - How long to stay alive (0 is forever) */
3681
- this.emitTime = emitTime;
3682
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
3683
- this.emitRate = emitRate;
3684
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
3685
- this.emitConeAngle = emitConeAngle;
3686
- // color settings
3687
- /** @property {Color} - Color at start of life 1, randomized between start colors */
3688
- this.colorStartA = colorStartA;
3689
- /** @property {Color} - Color at start of life 2, randomized between start colors */
3690
- this.colorStartB = colorStartB;
3691
- /** @property {Color} - Color at end of life 1, randomized between end colors */
3692
- this.colorEndA = colorEndA;
3693
- /** @property {Color} - Color at end of life 2, randomized between end colors */
3694
- this.colorEndB = colorEndB;
3695
- /** @property {boolean} - Should color be randomized linearly or across each component */
3696
- this.randomColorLinear = randomColorLinear;
3697
- // particle settings
3698
- /** @property {Number} - How long particles live */
3699
- this.particleTime = particleTime;
3700
- /** @property {Number} - How big are particles at start */
3701
- this.sizeStart = sizeStart;
3702
- /** @property {Number} - How big are particles at end */
3703
- this.sizeEnd = sizeEnd;
3704
- /** @property {Number} - How fast are particles when spawned */
3705
- this.speed = speed;
3706
- /** @property {Number} - How fast are particles rotating */
3707
- this.angleSpeed = angleSpeed;
3708
- /** @property {Number} - How much to dampen particle speed */
3709
- this.damping = damping;
3710
- /** @property {Number} - How much to dampen particle angular speed */
3711
- this.angleDamping = angleDamping;
3712
- /** @property {Number} - How much does gravity effect particles */
3713
- this.gravityScale = gravityScale;
3714
- /** @property {Number} - Cone for start particle angle */
3715
- this.particleConeAngle = particleConeAngle;
3716
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
3717
- this.fadeRate = fadeRate;
3718
- /** @property {Number} - Apply extra randomness percent */
3719
- this.randomness = randomness;
3720
- /** @property {boolean} - Do particles collide against tiles */
3721
- this.collideTiles = collideTiles;
3722
- /** @property {boolean} - Should particles use additive blend */
3723
- this.additive = additive;
3724
- /** @property {boolean} - Should it be in local space of emitter */
3725
- this.localSpace = localSpace;
3726
- /** @property {Number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
3727
- this.trailScale = 0;
3728
- /** @property {Function} - Callback when particle is destroyed */
3729
- this.particleDestroyCallback = undefined;
3730
- /** @property {Function} - Callback when particle is created */
3731
- this.particleCreateCallback = undefined;
3732
- /** @property {Number} - Track particle emit time */
3733
- this.emitTimeBuffer = 0;
3734
- }
3735
- /** Update the emitter to spawn particles, called automatically by engine once each frame */
3736
- update() {
3737
- // only do default update to apply parent transforms
3738
- this.parent && super.update();
3739
- // update emitter
3740
- if (!this.emitTime || this.getAliveTime() <= this.emitTime) {
3741
- // emit particles
3742
- if (this.emitRate * particleEmitRateScale) {
3743
- const rate = 1 / this.emitRate / particleEmitRateScale;
3744
- for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
3745
- this.emitParticle();
3746
- }
3747
- }
3748
- else
3749
- this.destroy();
3750
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
3751
- }
3752
- /** Spawn one particle
3753
- * @return {Particle} */
3754
- emitParticle() {
3755
- // spawn a particle
3756
- let pos = typeof this.emitSize === 'number' ? // check if number was used
3757
- randInCircle(this.emitSize / 2) // circle emitter
3758
- : vec2(rand(-.5, .5), rand(-.5, .5)) // box emitter
3759
- .multiply(this.emitSize).rotate(this.angle);
3760
- let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3761
- if (!this.localSpace) {
3762
- pos = this.pos.add(pos);
3763
- angle += this.angle;
3764
- }
3765
- // randomness scales each parameter by a percentage
3766
- const randomness = this.randomness;
3767
- const randomizeScale = (v) => v + v * rand(randomness, -randomness);
3768
- // randomize particle settings
3769
- const particleTime = randomizeScale(this.particleTime);
3770
- const sizeStart = randomizeScale(this.sizeStart);
3771
- const sizeEnd = randomizeScale(this.sizeEnd);
3772
- const speed = randomizeScale(this.speed);
3773
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3774
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3775
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3776
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3777
- const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
3778
- // build particle
3779
- const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
3780
- particle.velocity = vec2().setAngle(velocityAngle, speed);
3781
- particle.angleVelocity = angleSpeed;
3782
- particle.fadeRate = this.fadeRate;
3783
- particle.damping = this.damping;
3784
- particle.angleDamping = this.angleDamping;
3785
- particle.elasticity = this.elasticity;
3786
- particle.friction = this.friction;
3787
- particle.gravityScale = this.gravityScale;
3788
- particle.collideTiles = this.collideTiles;
3789
- particle.renderOrder = this.renderOrder;
3790
- particle.mirror = !!randInt(2);
3791
- // call particle create callback
3792
- this.particleCreateCallback && this.particleCreateCallback(particle);
3793
- // return the newly created particle
3794
- return particle;
3795
- }
3796
- // Particle emitters are not rendered, only the particles are
3797
- render() { }
3798
- }
3799
- ///////////////////////////////////////////////////////////////////////////////
3800
- /**
3801
- * Particle Object - Created automatically by Particle Emitters
3802
- * @extends EngineObject
3803
- */
3804
- class Particle extends EngineObject {
3805
- /**
3806
- * Create a particle with the passed in settings
3807
- * Typically this is created automatically by a ParticleEmitter
3808
- * @param {Vector2} position - World space position of the particle
3809
- * @param {TileInfo} tileInfo - Tile info to render particles
3810
- * @param {Number} angle - Angle to rotate the particle
3811
- * @param {Color} colorStart - Color at start of life
3812
- * @param {Color} colorEnd - Color at end of life
3813
- * @param {Number} lifeTime - How long to live for
3814
- * @param {Number} sizeStart - Size at start of life
3815
- * @param {Number} sizeEnd - Size at end of life
3816
- * @param {Number} fadeRate - How quick to fade in/out
3817
- * @param {boolean} additive - Does it use additive blend mode
3818
- * @param {Number} trailScale - If a trail, how long to make it
3819
- * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
3820
- * @param {Function} [destroyCallback] - Callback when particle dies
3821
- */
3822
- constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback) {
3823
- super(position, vec2(), tileInfo, angle);
3824
- /** @property {Color} - Color at start of life */
3825
- this.colorStart = colorStart;
3826
- /** @property {Color} - Calculated change in color */
3827
- this.colorEndDelta = colorEnd.subtract(colorStart);
3828
- /** @property {Number} - How long to live for */
3829
- this.lifeTime = lifeTime;
3830
- /** @property {Number} - Size at start of life */
3831
- this.sizeStart = sizeStart;
3832
- /** @property {Number} - Calculated change in size */
3833
- this.sizeEndDelta = sizeEnd - sizeStart;
3834
- /** @property {Number} - How quick to fade in/out */
3835
- this.fadeRate = fadeRate;
3836
- /** @property {boolean} - Is it additive */
3837
- this.additive = additive;
3838
- /** @property {Number} - If a trail, how long to make it */
3839
- this.trailScale = trailScale;
3840
- /** @property {ParticleEmitter} - Parent emitter if local space */
3841
- this.localSpaceEmitter = localSpaceEmitter;
3842
- /** @property {Function} - Called when particle dies */
3843
- this.destroyCallback = destroyCallback;
3844
- // particles use circular clamped speed
3845
- this.clampSpeedLinear = false;
3846
- }
3847
- /** Render the particle, automatically called each frame, sorted by renderOrder */
3848
- render() {
3849
- // modulate size and color
3850
- const p = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1;
3851
- const radius = this.sizeStart + p * this.sizeEndDelta;
3852
- const size = vec2(radius);
3853
- const fadeRate = this.fadeRate / 2;
3854
- const color = new Color(this.colorStart.r + p * this.colorEndDelta.r, this.colorStart.g + p * this.colorEndDelta.g, this.colorStart.b + p * this.colorEndDelta.b, (this.colorStart.a + p * this.colorEndDelta.a) *
3855
- (p < fadeRate ? p / fadeRate : p > 1 - fadeRate ? (1 - p) / fadeRate : 1)); // fade alpha
3856
- // draw the particle
3857
- this.additive && setBlendMode(true);
3858
- let pos = this.pos, angle = this.angle;
3859
- if (this.localSpaceEmitter) {
3860
- // in local space of emitter
3861
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
3862
- angle += this.localSpaceEmitter.angle;
3863
- }
3864
- if (this.trailScale) {
3865
- // trail style particles
3866
- let velocity = this.velocity;
3867
- if (this.localSpaceEmitter)
3868
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
3869
- const speed = velocity.length();
3870
- if (speed) {
3871
- const direction = velocity.scale(1 / speed);
3872
- const trailLength = speed * this.trailScale;
3873
- size.y = max(size.x, trailLength);
3874
- angle = direction.angle();
3875
- drawTile(pos.add(direction.multiply(vec2(0, -trailLength / 2))), size, this.tileInfo, color, angle, this.mirror);
3876
- }
3877
- }
3878
- else
3879
- drawTile(pos, size, this.tileInfo, color, angle, this.mirror);
3880
- this.additive && setBlendMode();
3881
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
3882
- if (p == 1) {
3883
- // destroy particle when it's time runs out
3884
- this.color = color;
3885
- this.size = size;
3886
- this.destroyCallback && this.destroyCallback(this);
3887
- this.destroyed = 1;
3888
- }
3889
- }
3890
- }
3891
- /**
3892
- * LittleJS Medal System
3893
- * - Tracks and displays medals
3894
- * - Saves medals to local storage
3895
- * - Newgrounds integration
3896
- * @namespace Medals
3897
- */
3898
- /** List of all medals
3899
- * @type {Object}
3900
- * @memberof Medals */
3901
- const medals = {};
3902
- // Engine internal variables not exposed to documentation
3903
- let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3904
- ///////////////////////////////////////////////////////////////////////////////
3905
- /** Initialize medals with a save name used for storage
3906
- * - Call this after creating all medals
3907
- * - Checks if medals are unlocked
3908
- * @param {String} saveName
3909
- * @memberof Medals */
3910
- function medalsInit(saveName) {
3911
- // check if medals are unlocked
3912
- medalsSaveName = saveName;
3913
- if (!debugMedals)
3914
- medalsForEach(medal => medal.unlocked = !!localStorage[medal.storageKey()]);
3915
- // engine automatically renders medals
3916
- engineAddPlugin(undefined, medalsRender);
3917
- function medalsRender() {
3918
- if (!medalsDisplayQueue.length)
3919
- return;
3920
- // update first medal in queue
3921
- const medal = medalsDisplayQueue[0];
3922
- const time = timeReal - medalsDisplayTimeLast;
3923
- if (!medalsDisplayTimeLast)
3924
- medalsDisplayTimeLast = timeReal;
3925
- else if (time > medalDisplayTime) {
3926
- medalsDisplayTimeLast = 0;
3927
- medalsDisplayQueue.shift();
3928
- }
3929
- else {
3930
- // slide on/off medals
3931
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
3932
- const hidePercent = time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
3933
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
3934
- medal.render(hidePercent);
3935
- }
3936
- }
3937
- }
3938
- /** Calls a function for each medal
3939
- * @param {Function} callback
3940
- * @memberof Medals */
3941
- function medalsForEach(callback) { Object.values(medals).forEach(medal => callback(medal)); }
3942
- ///////////////////////////////////////////////////////////////////////////////
3943
- /**
3944
- * Medal - Tracks an unlockable medal
3945
- * @example
3946
- * // create a medal
3947
- * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
3948
- *
3949
- * // initialize medals
3950
- * medalsInit('Example Game');
3951
- *
3952
- * // unlock the medal
3953
- * medal_example.unlock();
3954
- */
3955
- class Medal {
3956
- /** Create a medal object and adds it to the list of medals
3957
- * @param {Number} id - The unique identifier of the medal
3958
- * @param {String} name - Name of the medal
3959
- * @param {String} [description] - Description of the medal
3960
- * @param {String} [icon] - Icon for the medal
3961
- * @param {String} [src] - Image location for the medal
3962
- */
3963
- constructor(id, name, description = '', icon = '🏆', src) {
3964
- ASSERT(id >= 0 && !medals[id]);
3965
- /** @property {Number} - The unique identifier of the medal */
3966
- this.id = id;
3967
- /** @property {String} - Name of the medal */
3968
- this.name = name;
3969
- /** @property {String} - Description of the medal */
3970
- this.description = description;
3971
- /** @property {String} - Icon for the medal */
3972
- this.icon = icon;
3973
- /** @property {boolean} - Is the medal unlocked? */
3974
- this.unlocked = false;
3975
- // load the source image if provided
3976
- if (src)
3977
- (this.image = new Image).src = src;
3978
- // add this to list of medals
3979
- medals[id] = this;
3980
- }
3981
- /** Unlocks a medal if not already unlocked */
3982
- unlock() {
3983
- if (medalsPreventUnlock || this.unlocked)
3984
- return;
3985
- // save the medal
3986
- ASSERT(medalsSaveName, 'save name must be set');
3987
- localStorage[this.storageKey()] = this.unlocked = true;
3988
- medalsDisplayQueue.push(this);
3989
- }
3990
- /** Render a medal
3991
- * @param {Number} [hidePercent] - How much to slide the medal off screen
3992
- */
3993
- render(hidePercent = 0) {
3994
- const context = overlayContext;
3995
- const width = min(medalDisplaySize.x, mainCanvas.width);
3996
- const x = overlayCanvas.width - width;
3997
- const y = -medalDisplaySize.y * hidePercent;
3998
- // draw containing rect and clip to that region
3999
- context.save();
4000
- context.beginPath();
4001
- context.fillStyle = new Color(.9, .9, .9).toString();
4002
- context.strokeStyle = new Color(0, 0, 0).toString();
4003
- context.lineWidth = 3;
4004
- context.rect(x, y, width, medalDisplaySize.y);
4005
- context.fill();
4006
- context.stroke();
4007
- context.clip();
4008
- // draw the icon and text
4009
- this.renderIcon(vec2(x + 15 + medalDisplayIconSize / 2, y + medalDisplaySize.y / 2));
4010
- const pos = vec2(x + medalDisplayIconSize + 30, y + 28);
4011
- drawTextScreen(this.name, pos, 38, new Color(0, 0, 0), 0, undefined, 'left');
4012
- pos.y += 32;
4013
- drawTextScreen(this.description, pos, 24, new Color(0, 0, 0), 0, undefined, 'left');
4014
- context.restore();
4015
- }
4016
- /** Render the icon for a medal
4017
- * @param {Vector2} pos - Screen space position
4018
- * @param {Number} [size=medalDisplayIconSize] - Screen space size
4019
- */
4020
- renderIcon(pos, size = medalDisplayIconSize) {
4021
- // draw the image or icon
4022
- if (this.image)
4023
- overlayContext.drawImage(this.image, pos.x - size / 2, pos.y - size / 2, size, size);
4024
- else
4025
- drawTextScreen(this.icon, pos, size * .7, new Color(0, 0, 0));
4026
- }
4027
- // Get local storage key used by the medal
4028
- storageKey() { return medalsSaveName + '_' + this.id; }
4029
- }
4030
- /**
4031
- * LittleJS WebGL Interface
4032
- * - All webgl used by the engine is wrapped up here
4033
- * - For normal stuff you won't need to see or call anything in this file
4034
- * - For advanced stuff there are helper functions to create shaders, textures, etc
4035
- * - Can be disabled with glEnable to revert to 2D canvas rendering
4036
- * - Batches sprite rendering on GPU for incredibly fast performance
4037
- * - Sprite transform math is done in the shader where possible
4038
- * - Supports shadertoy style post processing shaders
4039
- * @namespace WebGL
4040
- */
4041
- /** The WebGL canvas which appears above the main canvas and below the overlay canvas
4042
- * @type {HTMLCanvasElement}
4043
- * @memberof WebGL */
4044
- let glCanvas;
4045
- /** 2d context for glCanvas
4046
- * @type {WebGL2RenderingContext}
4047
- * @memberof WebGL */
4048
- let glContext;
4049
- /** Should webgl be setup with anti-aliasing? must be set before calling engineInit
4050
- * @type {boolean}
4051
- * @memberof WebGL */
4052
- let glAntialias = true;
4053
- // WebGL internal variables not exposed to documentation
4054
- let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
4055
- // WebGL internal constants
4056
- const gl_MAX_INSTANCES = 1e4;
4057
- const gl_INDICES_PER_INSTANCE = 11;
4058
- const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
4059
- const gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
4060
- ///////////////////////////////////////////////////////////////////////////////
4061
- // Initialize WebGL, called automatically by the engine
4062
- function glInit() {
4063
- if (!glEnable || headlessMode)
4064
- return;
4065
- // create the canvas and textures
4066
- glCanvas = document.createElement('canvas');
4067
- glContext = glCanvas.getContext('webgl2', { antialias: glAntialias });
4068
- // some browsers are much faster without copying the gl buffer so we just overlay it instead
4069
- const rootElement = mainCanvas.parentElement;
4070
- glOverlay && rootElement.appendChild(glCanvas);
4071
- // setup vertex and fragment shaders
4072
- glShader = glCreateProgram('#version 300 es\n' + // specify GLSL ES version
4073
- 'precision highp float;' + // use highp for better accuracy
4074
- 'uniform mat4 m;' + // transform matrix
4075
- 'in vec2 g;' + // in: geometry
4076
- 'in vec4 p,u,c,a;' + // in: position/size, uvs, color, additiveColor
4077
- 'in float r;' + // in: rotation
4078
- 'out vec2 v;' + // out: uv
4079
- 'out vec4 d,e;' + // out: color, additiveColor
4080
- 'void main(){' + // shader entry point
4081
- 'vec2 s=(g-.5)*p.zw;' + // get size offset
4082
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);' + // transform position
4083
- 'v=mix(u.xw,u.zy,g);' + // pass uv to fragment shader
4084
- 'd=c;e=a;' + // pass colors to fragment shader
4085
- '}' // end of shader
4086
- , '#version 300 es\n' + // specify GLSL ES version
4087
- 'precision highp float;' + // use highp for better accuracy
4088
- 'uniform sampler2D s;' + // texture
4089
- 'in vec2 v;' + // in: uv
4090
- 'in vec4 d,e;' + // in: color, additiveColor
4091
- 'out vec4 c;' + // out: color
4092
- 'void main(){' + // shader entry point
4093
- 'c=texture(s,v)*d+e;' + // modulate texture by color plus additive
4094
- '}' // end of shader
4095
- );
4096
- // init buffers
4097
- const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
4098
- glPositionData = new Float32Array(glInstanceData);
4099
- glColorData = new Uint32Array(glInstanceData);
4100
- glArrayBuffer = glContext.createBuffer();
4101
- glGeometryBuffer = glContext.createBuffer();
4102
- // create the geometry buffer, triangle strip square
4103
- const geometry = new Float32Array([glInstanceCount = 0, 0, 1, 0, 0, 1, 1, 1]);
4104
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
4105
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
4106
- }
4107
- // Setup render each frame, called automatically by engine
4108
- function glPreRender() {
4109
- if (!glEnable || headlessMode)
4110
- return;
4111
- // set up the shader and canvas
4112
- glClearCanvas();
4113
- glContext.useProgram(glShader);
4114
- glContext.activeTexture(glContext.TEXTURE0);
4115
- if (textureInfos[0])
4116
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
4117
- // set vertex attributes
4118
- let offset = glAdditive = glBatchAdditive = 0;
4119
- let initVertexAttribArray = (name, type, typeSize, size) => {
4120
- const location = glContext.getAttribLocation(glShader, name);
4121
- const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
4122
- const divisor = typeSize && 1; // only if not geometry
4123
- const normalize = typeSize == 1; // only if color
4124
- glContext.enableVertexAttribArray(location);
4125
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
4126
- glContext.vertexAttribDivisor(location, divisor);
4127
- offset += size * typeSize;
4128
- };
4129
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
4130
- initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
4131
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
4132
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
4133
- initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
4134
- initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
4135
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
4136
- initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
4137
- initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
4138
- // build the transform matrix
4139
- const s = vec2(2 * cameraScale).divide(mainCanvasSize);
4140
- const p = vec2(-1).subtract(cameraPos.multiply(s));
4141
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false, [
4142
- s.x, 0, 0, 0,
4143
- 0, s.y, 0, 0,
4144
- 1, 1, 1, 1,
4145
- p.x, p.y, 0, 0
4146
- ]);
4147
- }
4148
- /** Clear the canvas and setup the viewport
4149
- * @memberof WebGL */
4150
- function glClearCanvas() {
4151
- // clear and set to same size as main canvas
4152
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4153
- glContext.clear(glContext.COLOR_BUFFER_BIT);
4154
- }
4155
- /** Set the WebGl texture, called automatically if using multiple textures
4156
- * - This may also flush the gl buffer resulting in more draw calls and worse performance
4157
- * @param {WebGLTexture} texture
4158
- * @memberof WebGL */
4159
- function glSetTexture(texture) {
4160
- // must flush cache with the old texture to set a new one
4161
- if (headlessMode || texture == glActiveTexture)
4162
- return;
4163
- glFlush();
4164
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = texture);
4165
- }
4166
- /** Compile WebGL shader of the given type, will throw errors if in debug mode
4167
- * @param {String} source
4168
- * @param {Number} type
4169
- * @return {WebGLShader}
4170
- * @memberof WebGL */
4171
- function glCompileShader(source, type) {
4172
- // build the shader
4173
- const shader = glContext.createShader(type);
4174
- glContext.shaderSource(shader, source);
4175
- glContext.compileShader(shader);
4176
- // check for errors
4177
- if (debug && !glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
4178
- throw glContext.getShaderInfoLog(shader);
4179
- return shader;
4180
- }
4181
- /** Create WebGL program with given shaders
4182
- * @param {String} vsSource
4183
- * @param {String} fsSource
4184
- * @return {WebGLProgram}
4185
- * @memberof WebGL */
4186
- function glCreateProgram(vsSource, fsSource) {
4187
- // build the program
4188
- const program = glContext.createProgram();
4189
- glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
4190
- glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
4191
- glContext.linkProgram(program);
4192
- // check for errors
4193
- if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
4194
- throw glContext.getProgramInfoLog(program);
4195
- return program;
4196
- }
4197
- /** Create WebGL texture from an image and init the texture settings
4198
- * @param {HTMLImageElement} image
4199
- * @return {WebGLTexture}
4200
- * @memberof WebGL */
4201
- function glCreateTexture(image) {
4202
- // build the texture
4203
- const texture = glContext.createTexture();
4204
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
4205
- if (image && image.width)
4206
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4207
- else {
4208
- // create a white texture
4209
- const whitePixel = new Uint8Array([255, 255, 255, 255]);
4210
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
4211
- }
4212
- // use point filtering for pixelated rendering
4213
- const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
4214
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
4215
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, filter);
4216
- return texture;
4217
- }
4218
- /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
4219
- * @memberof WebGL */
4220
- function glFlush() {
4221
- if (!glInstanceCount)
4222
- return;
4223
- const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
4224
- glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
4225
- glContext.enable(glContext.BLEND);
4226
- // draw all the sprites in the batch and reset the buffer
4227
- glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
4228
- glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glInstanceCount);
4229
- if (showWatermark)
4230
- drawCount += glInstanceCount;
4231
- glInstanceCount = 0;
4232
- glBatchAdditive = glAdditive;
4233
- }
4234
- /** Draw any sprites still in the buffer and copy to main canvas
4235
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4236
- * @param {boolean} [forceDraw]
4237
- * @memberof WebGL */
4238
- function glCopyToContext(context, forceDraw = false) {
4239
- if (!glEnable || !glInstanceCount && !forceDraw)
4240
- return;
4241
- glFlush();
4242
- // do not draw in overlay mode because the canvas is visible
4243
- if (!glOverlay || forceDraw)
4244
- context.drawImage(glCanvas, 0, 0);
4245
- }
4246
- /** Set anti-aliasing for webgl canvas
4247
- * @param {boolean} [antialias]
4248
- * @memberof WebGL */
4249
- function glSetAntialias(antialias = true) {
4250
- ASSERT(!glCanvas, 'must be called before engineInit');
4251
- glAntialias = antialias;
4252
- }
4253
- /** Add a sprite to the gl draw list, used by all gl draw functions
4254
- * @param {Number} x
4255
- * @param {Number} y
4256
- * @param {Number} sizeX
4257
- * @param {Number} sizeY
4258
- * @param {Number} angle
4259
- * @param {Number} uv0X
4260
- * @param {Number} uv0Y
4261
- * @param {Number} uv1X
4262
- * @param {Number} uv1Y
4263
- * @param {Number} rgba
4264
- * @param {Number} [rgbaAdditive=0]
4265
- * @memberof WebGL */
4266
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive = 0) {
4267
- ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
4268
- // flush if there is not enough room or if different blend mode
4269
- if (glInstanceCount >= gl_MAX_INSTANCES || glBatchAdditive != glAdditive)
4270
- glFlush();
4271
- let offset = glInstanceCount++ * gl_INDICES_PER_INSTANCE;
4272
- glPositionData[offset++] = x;
4273
- glPositionData[offset++] = y;
4274
- glPositionData[offset++] = sizeX;
4275
- glPositionData[offset++] = sizeY;
4276
- glPositionData[offset++] = uv0X;
4277
- glPositionData[offset++] = uv0Y;
4278
- glPositionData[offset++] = uv1X;
4279
- glPositionData[offset++] = uv1Y;
4280
- glColorData[offset++] = rgba;
4281
- glColorData[offset++] = rgbaAdditive;
4282
- glPositionData[offset++] = angle;
4283
- }
4284
- /**
4285
- * LittleJS - The Tiny Fast JavaScript Game Engine
4286
- * MIT License - Copyright 2021 Frank Force
4287
- *
4288
- * Engine Features
4289
- * - Object oriented system with base class engine object
4290
- * - Base class object handles update, physics, collision, rendering, etc
4291
- * - Engine helper classes and functions like Vector2, Color, and Timer
4292
- * - Super fast rendering system for tile sheets
4293
- * - Sound effects audio with zzfx and music with zzfxm
4294
- * - Input processing system with gamepad and touchscreen support
4295
- * - Tile layer rendering and collision system
4296
- * - Particle effect system
4297
- * - Medal system tracks and displays achievements
4298
- * - Debug tools and debug rendering system
4299
- * - Post processing effects
4300
- * - Call engineInit() to start it up!
4301
- * @namespace Engine
4302
- */
4303
- /** Name of engine
4304
- * @type {string}
4305
- * @default
4306
- * @memberof Engine */
4307
- const engineName = 'LittleJS';
4308
- /** Version of engine
4309
- * @type {string}
4310
- * @default
4311
- * @memberof Engine */
4312
- const engineVersion = '1.11.10';
4313
- /** Frames per second to update
4314
- * @type {number}
4315
- * @default
4316
- * @memberof Engine */
4317
- const frameRate = 60;
4318
- /** How many seconds each frame lasts, engine uses a fixed time step
4319
- * @type {number}
4320
- * @default 1/60
4321
- * @memberof Engine */
4322
- const timeDelta = 1 / frameRate;
4323
- /** Array containing all engine objects
4324
- * @type {Array<EngineObject>}
4325
- * @memberof Engine */
4326
- let engineObjects = [];
4327
- /** Array with only objects set to collide with other objects this frame (for optimization)
4328
- * @type {Array<EngineObject>}
4329
- * @memberof Engine */
4330
- let engineObjectsCollide = [];
4331
- /** Current update frame, used to calculate time
4332
- * @type {number}
4333
- * @memberof Engine */
4334
- let frame = 0;
4335
- /** Current engine time since start in seconds
4336
- * @type {number}
4337
- * @memberof Engine */
4338
- let time = 0;
4339
- /** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
4340
- * @type {number}
4341
- * @memberof Engine */
4342
- let timeReal = 0;
4343
- /** Is the game paused? Causes time and objects to not be updated
4344
- * @type {boolean}
4345
- * @default false
4346
- * @memberof Engine */
4347
- let paused = false;
4348
- /** Set if game is paused
4349
- * @param {boolean} isPaused
4350
- * @memberof Engine */
4351
- function setPaused(isPaused) { paused = isPaused; }
4352
- // Frame time tracking
4353
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
4354
- ///////////////////////////////////////////////////////////////////////////////
4355
- // plugin hooks
4356
- const pluginUpdateList = [], pluginRenderList = [];
4357
- /** Add a new update function for a plugin
4358
- * @param {Function} [updateFunction]
4359
- * @param {Function} [renderFunction]
4360
- * @memberof Engine */
4361
- function engineAddPlugin(updateFunction, renderFunction) {
4362
- ASSERT(!pluginUpdateList.includes(updateFunction));
4363
- ASSERT(!pluginRenderList.includes(renderFunction));
4364
- updateFunction && pluginUpdateList.push(updateFunction);
4365
- renderFunction && pluginRenderList.push(renderFunction);
4366
- }
4367
- ///////////////////////////////////////////////////////////////////////////////
4368
- // Main engine functions
4369
- /** Startup LittleJS engine with your callback functions
4370
- * @param {Function|function():Promise} gameInit - Called once after the engine starts up
4371
- * @param {Function} gameUpdate - Called every frame before objects are updated
4372
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
4373
- * @param {Function} gameRender - Called before objects are rendered, for drawing the background
4374
- * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
4375
- * @param {Array<string>} [imageSources=[]] - List of images to load
4376
- * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
4377
- * @memberof Engine */
4378
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources = [], rootElement = document.body) {
4379
- ASSERT(!mainContext, 'engine already initialized');
4380
- ASSERT(Array.isArray(imageSources), 'pass in images as array');
4381
- // allow passing in empty functions
4382
- if (!gameInit)
4383
- gameInit = () => { };
4384
- if (!gameUpdate)
4385
- gameUpdate = () => { };
4386
- if (!gameUpdatePost)
4387
- gameUpdatePost = () => { };
4388
- if (!gameRender)
4389
- gameRender = () => { };
4390
- if (!gameRenderPost)
4391
- gameRenderPost = () => { };
4392
- // Called automatically by engine to setup render system
4393
- function enginePreRender() {
4394
- // save canvas size
4395
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4396
- // disable smoothing for pixel art
4397
- overlayContext.imageSmoothingEnabled =
4398
- mainContext.imageSmoothingEnabled = !tilesPixelated;
4399
- // setup gl rendering if enabled
4400
- glPreRender();
4401
- }
4402
- // internal update loop for engine
4403
- function engineUpdate(frameTimeMS = 0) {
4404
- // update time keeping
4405
- let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4406
- frameTimeLastMS = frameTimeMS;
4407
- if (debug || showWatermark)
4408
- averageFPS = lerp(.05, averageFPS, 1e3 / (frameTimeDeltaMS || 1));
4409
- const debugSpeedUp = debug && keyIsDown('Equal'); // +
4410
- const debugSpeedDown = debug && keyIsDown('Minus'); // -
4411
- if (debug) // +/- to speed/slow time
4412
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
4413
- timeReal += frameTimeDeltaMS / 1e3;
4414
- frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
4415
- if (!debugSpeedUp)
4416
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
4417
- updateCanvas();
4418
- if (paused) {
4419
- // update object transforms even when paused
4420
- for (const o of engineObjects)
4421
- o.parent || o.updateTransforms();
4422
- inputUpdate();
4423
- pluginUpdateList.forEach(f => f());
4424
- debugUpdate();
4425
- gameUpdatePost();
4426
- inputUpdatePost();
4427
- }
4428
- else {
4429
- // apply time delta smoothing, improves smoothness of framerate in some browsers
4430
- let deltaSmooth = 0;
4431
- if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9) {
4432
- // force at least one update each frame since it is waiting for refresh
4433
- deltaSmooth = frameTimeBufferMS;
4434
- frameTimeBufferMS = 0;
4435
- }
4436
- // update multiple frames if necessary in case of slow framerate
4437
- for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate) {
4438
- // increment frame and update time
4439
- time = frame++ / frameRate;
4440
- // update game and objects
4441
- inputUpdate();
4442
- gameUpdate();
4443
- pluginUpdateList.forEach(f => f());
4444
- engineObjectsUpdate();
4445
- // do post update
4446
- debugUpdate();
4447
- gameUpdatePost();
4448
- inputUpdatePost();
4449
- }
4450
- // add the time smoothing back in
4451
- frameTimeBufferMS += deltaSmooth;
4452
- }
4453
- if (!headlessMode) {
4454
- // render sort then render while removing destroyed objects
4455
- enginePreRender();
4456
- gameRender();
4457
- engineObjects.sort((a, b) => a.renderOrder - b.renderOrder);
4458
- for (const o of engineObjects)
4459
- o.destroyed || o.render();
4460
- gameRenderPost();
4461
- pluginRenderList.forEach(f => f());
4462
- touchGamepadRender();
4463
- debugRender();
4464
- glCopyToContext(mainContext);
4465
- if (showWatermark) {
4466
- // update fps
4467
- overlayContext.textAlign = 'right';
4468
- overlayContext.textBaseline = 'top';
4469
- overlayContext.font = '1em monospace';
4470
- overlayContext.fillStyle = '#000';
4471
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
4472
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4473
- + (glEnable ? ' GL' : ' 2D');
4474
- overlayContext.fillText(text, mainCanvas.width - 3, 3);
4475
- overlayContext.fillStyle = '#fff';
4476
- overlayContext.fillText(text, mainCanvas.width - 2, 2);
4477
- drawCount = 0;
4478
- }
4479
- }
4480
- requestAnimationFrame(engineUpdate);
4481
- }
4482
- function updateCanvas() {
4483
- if (headlessMode)
4484
- return;
4485
- if (canvasFixedSize.x) {
4486
- // clear canvas and set fixed size
4487
- mainCanvas.width = canvasFixedSize.x;
4488
- mainCanvas.height = canvasFixedSize.y;
4489
- // fit to window by adding space on top or bottom if necessary
4490
- const aspect = innerWidth / innerHeight;
4491
- const fixedAspect = mainCanvas.width / mainCanvas.height;
4492
- (glCanvas || mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
4493
- (glCanvas || mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
4494
- }
4495
- else {
4496
- // clear canvas and set size to same as window
4497
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
4498
- mainCanvas.height = min(innerHeight, canvasMaxSize.y);
4499
- }
4500
- // clear overlay canvas and set size
4501
- overlayCanvas.width = mainCanvas.width;
4502
- overlayCanvas.height = mainCanvas.height;
4503
- // save canvas size
4504
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4505
- }
4506
- function startEngine() {
4507
- new Promise((resolve) => resolve(gameInit())).then(engineUpdate);
4508
- }
4509
- if (headlessMode) {
4510
- startEngine();
4511
- return;
4512
- }
4513
- // setup html
4514
- const styleRoot = 'margin:0;overflow:hidden;' + // fill the window
4515
- 'width:100vw;height:100vh;' + // fill the window
4516
- 'display:flex;' + // use flexbox
4517
- 'align-items:center;' + // horizontal center
4518
- 'justify-content:center;' + // vertical center
4519
- 'background:#000;' + // set background color
4520
- (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
4521
- 'user-select:none;' + // prevent hold to select
4522
- '-webkit-user-select:none;' + // compatibility for ios
4523
- (!touchInputEnable ? '' : // no touch css settings
4524
- 'touch-action:none;' + // prevent mobile pinch to resize
4525
- '-webkit-touch-callout:none'); // compatibility for ios
4526
- rootElement.style.cssText = styleRoot;
4527
- rootElement.appendChild(mainCanvas = document.createElement('canvas'));
4528
- mainContext = mainCanvas.getContext('2d');
4529
- // init stuff and start engine
4530
- inputInit();
4531
- audioInit();
4532
- debugInit();
4533
- glInit();
4534
- // create overlay canvas for hud to appear above gl canvas
4535
- rootElement.appendChild(overlayCanvas = document.createElement('canvas'));
4536
- overlayContext = overlayCanvas.getContext('2d');
4537
- // set canvas style
4538
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
4539
- mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
4540
- if (glCanvas)
4541
- glCanvas.style.cssText = styleCanvas;
4542
- updateCanvas();
4543
- // create promises for loading images
4544
- const promises = imageSources.map((src, textureIndex) => new Promise(resolve => {
4545
- const image = new Image;
4546
- image.crossOrigin = 'anonymous';
4547
- image.onerror = image.onload = () => {
4548
- textureInfos[textureIndex] = new TextureInfo(image);
4549
- resolve();
4550
- };
4551
- image.src = src;
4552
- }));
4553
- if (!imageSources.length) {
4554
- // no images to load
4555
- promises.push(new Promise(resolve => {
4556
- textureInfos[0] = new TextureInfo(new Image);
4557
- resolve();
4558
- }));
4559
- }
4560
- if (showSplashScreen) {
4561
- // draw splash screen
4562
- promises.push(new Promise(resolve => {
4563
- let t = 0;
4564
- console.log(`${engineName} Engine v${engineVersion}`);
4565
- updateSplash();
4566
- function updateSplash() {
4567
- clearInput();
4568
- drawEngineSplashScreen(t += .01);
4569
- t > 1 ? resolve() : setTimeout(updateSplash, 16);
4570
- }
4571
- }));
4572
- }
4573
- // load all of the images
4574
- Promise.all(promises).then(startEngine);
4575
- }
4576
- /** Update each engine object, remove destroyed objects, and update time
4577
- * @memberof Engine */
4578
- function engineObjectsUpdate() {
4579
- // get list of solid objects for physics optimization
4580
- engineObjectsCollide = engineObjects.filter(o => o.collideSolidObjects);
4581
- // recursive object update
4582
- function updateObject(o) {
4583
- if (!o.destroyed) {
4584
- o.update();
4585
- for (const child of o.children)
4586
- updateObject(child);
4587
- }
4588
- }
4589
- for (const o of engineObjects) {
4590
- // update top level objects
4591
- if (!o.parent) {
4592
- updateObject(o);
4593
- o.updateTransforms();
4594
- }
4595
- }
4596
- // remove destroyed objects
4597
- engineObjects = engineObjects.filter(o => !o.destroyed);
4598
- }
4599
- /** Destroy and remove all objects
4600
- * @memberof Engine */
4601
- function engineObjectsDestroy() {
4602
- for (const o of engineObjects)
4603
- o.parent || o.destroy();
4604
- engineObjects = engineObjects.filter(o => !o.destroyed);
4605
- }
4606
- /** Collects all object within a given area
4607
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
4608
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
4609
- * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
4610
- * @return {Array<EngineObject>} - List of collected objects
4611
- * @memberof Engine */
4612
- function engineObjectsCollect(pos, size, objects = engineObjects) {
4613
- const collectedObjects = [];
4614
- if (!pos) // all objects
4615
- {
4616
- for (const o of objects)
4617
- collectedObjects.push(o);
4618
- }
4619
- else if (size instanceof Vector2) // bounding box test
4620
- {
4621
- for (const o of objects)
4622
- isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
4623
- }
4624
- else // circle test
4625
- {
4626
- const sizeSquared = size * size;
4627
- for (const o of objects)
4628
- pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
4629
- }
4630
- return collectedObjects;
4631
- }
4632
- /** Triggers a callback for each object within a given area
4633
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
4634
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
4635
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
4636
- * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
4637
- * @memberof Engine */
4638
- function engineObjectsCallback(pos, size, callbackFunction, objects = engineObjects) { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
4639
- /** Return a list of objects intersecting a ray
4640
- * @param {Vector2} start
4641
- * @param {Vector2} end
4642
- * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
4643
- * @return {Array<EngineObject>} - List of objects hit
4644
- * @memberof Engine */
4645
- function engineObjectsRaycast(start, end, objects = engineObjects) {
4646
- const hitObjects = [];
4647
- for (const o of objects) {
4648
- if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size)) {
4649
- debugRaycast && debugRect(o.pos, o.size, '#f00');
4650
- hitObjects.push(o);
4651
- }
4652
- }
4653
- debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
4654
- return hitObjects;
4655
- }
4656
- ///////////////////////////////////////////////////////////////////////////////
4657
- // LittleJS splash screen and logo
4658
- function drawEngineSplashScreen(t) {
4659
- const x = overlayContext;
4660
- const w = overlayCanvas.width = innerWidth;
4661
- const h = overlayCanvas.height = innerHeight;
4662
- {
4663
- // background
4664
- const p3 = percent(t, 1, .8);
4665
- const p4 = percent(t, 0, .5);
4666
- const g = x.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.hypot(w, h) * .7);
4667
- g.addColorStop(0, hsl(0, 0, lerp(p4, 0, p3 / 2), p3).toString());
4668
- g.addColorStop(1, hsl(0, 0, 0, p3).toString());
4669
- x.save();
4670
- x.fillStyle = g;
4671
- x.fillRect(0, 0, w, h);
4672
- }
4673
- // draw LittleJS logo...
4674
- const rect = (X, Y, W, H, C) => {
4675
- x.beginPath();
4676
- x.rect(X, Y, W, C ? H * p : H);
4677
- x.fillStyle = C;
4678
- C ? x.fill() : x.stroke();
4679
- };
4680
- const line = (X, Y, Z, W) => {
4681
- x.beginPath();
4682
- x.lineTo(X, Y);
4683
- x.lineTo(Z, W);
4684
- x.stroke();
4685
- };
4686
- const circle = (X, Y, R, A = 0, B = 2 * PI, C, F) => {
4687
- const D = (A + B) / 2, E = p * (B - A) / 2;
4688
- x.beginPath();
4689
- F && x.lineTo(X, Y);
4690
- x.arc(X, Y, R, D - E, D + E);
4691
- x.fillStyle = C;
4692
- C ? x.fill() : x.stroke();
4693
- };
4694
- const color = (c = 0, l = 0) => hsl([.98, .3, .57, .14][c % 4] - 10, .8, [0, .3, .5, .8, .9][l]).toString();
4695
- const alpha = wave(1, 1, t);
4696
- const p = percent(alpha, .1, .5);
4697
- // setup
4698
- x.translate(w / 2, h / 2);
4699
- const size = min(6, min(w, h) / 99); // fit to screen
4700
- x.scale(size, size);
4701
- x.translate(-40, -35);
4702
- x.lineJoin = x.lineCap = 'round';
4703
- x.lineWidth = .1 + p * 1.9;
4704
- // drawing effect
4705
- const p2 = percent(alpha, .1, 1);
4706
- x.setLineDash([99 * p2, 99]);
4707
- // cab top
4708
- rect(7, 16, 18, -8, color(2, 2));
4709
- rect(7, 8, 18, 4, color(2, 3));
4710
- rect(25, 8, 8, 8, color(2, 1));
4711
- rect(25, 8, -18, 8);
4712
- rect(25, 8, 8, 8);
4713
- // cab
4714
- rect(25, 16, 7, 23, color());
4715
- rect(11, 39, 14, -23, color(1, 1));
4716
- rect(11, 16, 14, 18, color(1, 2));
4717
- rect(11, 16, 14, 8, color(1, 3));
4718
- rect(25, 16, -14, 24);
4719
- // cab window
4720
- rect(15, 29, 6, -9, color(2, 2));
4721
- circle(15, 21, 5, 0, PI / 2, color(2, 4), 1);
4722
- rect(21, 21, -6, 9);
4723
- // little stack
4724
- rect(37, 14, 9, 6, color(3, 2));
4725
- rect(37, 14, 4.5, 6, color(3, 3));
4726
- rect(37, 14, 9, 6);
4727
- // big stack
4728
- rect(50, 20, 10, -8, color(0, 1));
4729
- rect(50, 20, 6.5, -8, color(0, 2));
4730
- rect(50, 20, 3.5, -8, color(0, 3));
4731
- rect(50, 20, 10, -8);
4732
- circle(55, 2, 11.4, .5, PI - .5, color(3, 3));
4733
- circle(55, 2, 11.4, .5, PI / 2, color(3, 2), 1);
4734
- circle(55, 2, 11.4, .5, PI - .5);
4735
- rect(45, 7, 20, -7, color(0, 2));
4736
- rect(45, -1, 20, 4, color(0, 3));
4737
- rect(45, -1, 20, 8);
4738
- // engine
4739
- for (let i = 5; i--;) {
4740
- // stagger radius to fix slight seam
4741
- circle(60 - i * 6, 30, 9.9, 0, 2 * PI, color(i + 2, 3));
4742
- circle(60 - i * 6, 30, 10.0, -.5, PI + .5, color(i + 2, 2));
4743
- circle(60 - i * 6, 30, 10.1, .5, PI - .5, color(i + 2, 1));
4744
- }
4745
- // engine outline
4746
- circle(36, 30, 10, PI / 2, PI * 3 / 2);
4747
- circle(48, 30, 10, PI / 2, PI * 3 / 2);
4748
- circle(60, 30, 10);
4749
- line(36, 20, 60, 20);
4750
- // engine front light
4751
- circle(60, 30, 4, PI, 3 * PI, color(3, 2));
4752
- circle(60, 30, 4, PI, 2 * PI, color(3, 3));
4753
- circle(60, 30, 4, PI, 3 * PI);
4754
- // front brush
4755
- for (let i = 6; i--;) {
4756
- x.beginPath();
4757
- x.lineTo(53, 54);
4758
- x.lineTo(53, 40);
4759
- x.lineTo(53 + (1 + i * 2.9) * p, 40);
4760
- x.lineTo(53 + (4 + i * 3.5) * p, 54);
4761
- x.fillStyle = color(0, i % 2 + 2);
4762
- x.fill();
4763
- i % 2 && x.stroke();
4764
- }
4765
- // wheels
4766
- rect(6, 40, 5, 5);
4767
- rect(6, 40, 5, 5, color());
4768
- rect(15, 54, 38, -14, color());
4769
- for (let i = 3; i--;)
4770
- for (let j = 2; j--;) {
4771
- circle(15 * i + 15, 47, j ? 7 : 1, PI, 3 * PI, color(i, 3));
4772
- x.stroke();
4773
- circle(15 * i + 15, 47, j ? 7 : 1, 0, PI, color(i, 2));
4774
- x.stroke();
4775
- }
4776
- line(6, 40, 68, 40); // center
4777
- line(77, 54, 4, 54); // bottom
4778
- // draw engine name
4779
- const s = engineName;
4780
- x.font = '900 16px arial';
4781
- x.textAlign = 'center';
4782
- x.textBaseline = 'top';
4783
- x.lineWidth = .1 + p * 3.9;
4784
- let w2 = 0;
4785
- for (let i = 0; i < s.length; ++i)
4786
- w2 += x.measureText(s[i]).width;
4787
- for (let j = 2; j--;)
4788
- for (let i = 0, X = 41 - w2 / 2; i < s.length; ++i) {
4789
- x.fillStyle = color(i, 2);
4790
- const w = x.measureText(s[i]).width;
4791
- x[j ? 'strokeText' : 'fillText'](s[i], X + w / 2, 55.5, 17 * p);
4792
- X += w;
4793
- }
4794
- x.restore();
4795
- }
4796
- /**
4797
- * LittleJS Module Export
4798
- * - Export engine as a module
4799
- */
4800
- export {
4801
- // Engine
4802
- engineName, engineVersion, frameRate, timeDelta, engineObjects, frame, time, timeReal, paused, setPaused, engineInit, engineObjectsUpdate, engineObjectsDestroy, engineObjectsCollect, engineObjectsCallback, engineObjectsRaycast, engineAddPlugin,
4803
- // Globals
4804
- debug, debugOverlay, showWatermark,
4805
- // Debug
4806
- ASSERT, debugRect, debugPoly, debugCircle, debugPoint, debugLine, debugOverlap, debugText, debugClear, debugScreenshot, debugSaveCanvas, debugSaveText, debugSaveDataURL,
4807
- // Settings
4808
- cameraPos, cameraScale, canvasMaxSize, canvasFixedSize, canvasPixelated, tilesPixelated, fontDefault, showSplashScreen, headlessMode, tileSizeDefault, tileFixBleedScale, enablePhysicsSolver, objectDefaultMass, objectDefaultDamping, objectDefaultAngleDamping, objectDefaultElasticity, objectDefaultFriction, objectMaxSpeed, gravity, particleEmitRateScale, glEnable, glOverlay, gamepadsEnable, gamepadDirectionEmulateStick, inputWASDEmulateDirection, touchGamepadEnable, touchGamepadAnalog, touchGamepadSize, touchGamepadAlpha, vibrateEnable, soundEnable, soundVolume, soundDefaultRange, soundDefaultTaper, medalDisplayTime, medalDisplaySlideTime, medalDisplaySize, medalDisplayIconSize,
4809
- // Setters for globals
4810
- setCameraPos, setCameraScale, setCanvasMaxSize, setCanvasFixedSize, setCanvasPixelated, setTilesPixelated, setFontDefault, setShowSplashScreen, setHeadlessMode, setGlEnable, setGlOverlay, setTileSizeDefault, setTileFixBleedScale, setEnablePhysicsSolver, setObjectDefaultMass, setObjectDefaultDamping, setObjectDefaultAngleDamping, setObjectDefaultElasticity, setObjectDefaultFriction, setObjectMaxSpeed, setGravity, setParticleEmitRateScale, setTouchInputEnable, setGamepadsEnable, setGamepadDirectionEmulateStick, setInputWASDEmulateDirection, setTouchGamepadEnable, setTouchGamepadAnalog, setTouchGamepadSize, setTouchGamepadAlpha, setVibrateEnable, setSoundEnable, setSoundVolume, setSoundDefaultRange, setSoundDefaultTaper, setMedalDisplayTime, setMedalDisplaySlideTime, setMedalDisplaySize, setMedalDisplayIconSize, setMedalsPreventUnlock, setShowWatermark, setDebugKey,
4811
- // Utilities
4812
- PI, abs, min, max, sign, mod, clamp, percent, distanceWrap, lerpWrap, distanceAngle, lerpAngle, lerp, smoothStep, nearestPowerOfTwo, isOverlapping, isIntersecting, wave, formatTime,
4813
- // Random
4814
- rand, randInt, randSign, randInCircle, randVector, randColor,
4815
- // Utility Classes
4816
- RandomGenerator, Vector2, Color, Timer, vec2, rgb, hsl, isColor,
4817
- // Default Colors
4818
- WHITE, BLACK, GRAY, RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PURPLE, MAGENTA,
4819
- // Draw
4820
- textureInfos, tile, TileInfo, TextureInfo, mainCanvas, mainContext, overlayCanvas, overlayContext, mainCanvasSize, screenToWorld, worldToScreen, drawTile, drawRect, drawLine, drawPoly, drawEllipse, drawCircle, drawCanvas2D, drawText, drawTextOverlay, drawTextScreen, setBlendMode, combineCanvases, engineFontImage, FontImage, isFullscreen, toggleFullscreen, setCursor, getCameraSize,
4821
- // WebGL
4822
- glCanvas, glContext, glCompileShader, glCopyToContext, glCreateProgram, glCreateTexture, glDraw, glFlush, glSetTexture, glSetAntialias, glClearCanvas, glAntialias, glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive,
4823
- // Input
4824
- keyIsDown, keyWasPressed, keyWasReleased, keyDirection, clearInput, mouseIsDown, mouseWasPressed, mouseWasReleased, mousePos, mousePosScreen, mouseWheel, isUsingGamepad, preventDefaultInput, gamepadIsDown, gamepadWasPressed, gamepadWasReleased, gamepadStick, gamepadsUpdate, vibrate, vibrateStop, isTouchDevice,
4825
- // Audio
4826
- Sound, SoundWave, Music, playAudioFile, speak, speakStop, getNoteFrequency, audioContext, playSamples, zzfx,
4827
- // Base Object
4828
- EngineObject,
4829
- // Tiles
4830
- tileCollision, tileCollisionSize, initTileCollision, setTileCollisionData, getTileCollisionData, tileCollisionTest, tileCollisionRaycast, TileLayerData, TileLayer,
4831
- // Particles
4832
- ParticleEmitter, Particle,
4833
- // Medals
4834
- medals, medalsPreventUnlock, medalsInit, Medal, };