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