littlejsengine 1.4.0 → 1.4.6

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