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