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