littlejsengine 1.6.6 → 1.6.92

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 (57) hide show
  1. package/README.md +51 -35
  2. package/build/littlejs.d.ts +71 -80
  3. package/build/littlejs.esm.js +162 -139
  4. package/build/littlejs.esm.min.js +1 -1
  5. package/build/littlejs.js +159 -129
  6. package/build/littlejs.min.js +1 -1
  7. package/build/littlejs.release.js +1942 -1907
  8. package/examples/breakout/game.js +5 -1
  9. package/examples/breakout/index.html +5 -5
  10. package/examples/breakoutTutorial/README.md +6 -6
  11. package/examples/breakoutTutorial/game.js +3 -0
  12. package/examples/breakoutTutorial/index.html +3 -3
  13. package/examples/electron/build.js +105 -0
  14. package/examples/electron/game.js +4 -2
  15. package/examples/electron/index.html +13 -2
  16. package/examples/electron/package.json +5 -3
  17. package/examples/empty/game.js +3 -1
  18. package/examples/empty/index.html +1 -1
  19. package/examples/js13k/build.bat +2 -0
  20. package/examples/js13k/build.js +110 -0
  21. package/examples/js13k/game.js +109 -0
  22. package/examples/js13k/index.html +18 -0
  23. package/examples/js13k/tiles.png +0 -0
  24. package/examples/module/game.js +9 -3
  25. package/examples/module/index.html +2 -2
  26. package/examples/particles/index.html +7 -13
  27. package/examples/platformer/game.js +5 -2
  28. package/examples/platformer/gameEffects.js +1 -2
  29. package/examples/platformer/gamePlayer.js +2 -2
  30. package/examples/platformer/index.html +8 -8
  31. package/examples/puzzle/game.js +5 -3
  32. package/examples/puzzle/index.html +4 -4
  33. package/examples/starter/build.bat +2 -78
  34. package/examples/starter/build.js +109 -0
  35. package/examples/starter/game.js +4 -1
  36. package/examples/starter/index.html +15 -18
  37. package/examples/stress/index.html +2 -4
  38. package/examples/typescript/build.bat +2 -14
  39. package/examples/typescript/build.js +31 -0
  40. package/examples/typescript/game.js +94 -89
  41. package/examples/typescript/game.ts +9 -3
  42. package/examples/typescript/index.html +2 -2
  43. package/package.json +2 -2
  44. package/src/engine.js +3 -3
  45. package/src/engineAudio.js +2 -2
  46. package/src/engineBuild.bat +2 -132
  47. package/src/engineBuild.js +143 -0
  48. package/src/engineDebug.js +4 -10
  49. package/src/engineDraw.js +12 -4
  50. package/src/engineExport.js +1 -8
  51. package/src/engineInput.js +1 -1
  52. package/src/engineObject.js +13 -11
  53. package/src/engineParticles.js +6 -6
  54. package/src/engineRelease.js +0 -1
  55. package/src/engineTileLayer.js +31 -18
  56. package/src/engineUtilities.js +74 -61
  57. package/examples/electron/build.bat +0 -52
@@ -8,7 +8,6 @@
8
8
  'use strict';
9
9
 
10
10
  let showWatermark = 0;
11
- let godMode = 0;
12
11
  let debugKeyCode = 0;
13
12
  const debug = 0;
14
13
  const debugOverlay = 0;
@@ -30,7 +29,7 @@ function debugLine (){}
30
29
  function debugAABB (){}
31
30
  function debugText (){}
32
31
  function debugClear (){}
33
- function debugSaveCanvas (){}
32
+ function debugSaveCanvas (){}
34
33
  /**
35
34
  * LittleJS Utility Classes and Functions
36
35
  * - General purpose math library
@@ -52,34 +51,34 @@ const PI = Math.PI;
52
51
  * @param {Number} value
53
52
  * @return {Number}
54
53
  * @memberof Utilities */
55
- function abs(a) { return a < 0 ? -a : a; }
54
+ function abs(value) { return Math.abs(value); }
56
55
 
57
56
  /** Returns lowest of two values passed in
58
57
  * @param {Number} valueA
59
58
  * @param {Number} valueB
60
59
  * @return {Number}
61
60
  * @memberof Utilities */
62
- function min(a, b) { return a < b ? a : b; }
61
+ function min(valueA, valueB) { return Math.min(valueA, valueB); }
63
62
 
64
63
  /** Returns highest of two values passed in
65
64
  * @param {Number} valueA
66
65
  * @param {Number} valueB
67
66
  * @return {Number}
68
67
  * @memberof Utilities */
69
- function max(a, b) { return a > b ? a : b; }
68
+ function max(valueA, valueB) { return Math.max(valueA, valueB); }
70
69
 
71
70
  /** Returns the sign of value passed in (also returns 1 if 0)
72
71
  * @param {Number} value
73
72
  * @return {Number}
74
73
  * @memberof Utilities */
75
- function sign(a) { return a < 0 ? -1 : 1; }
74
+ function sign(value) { return Math.sign(value); }
76
75
 
77
76
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
78
77
  * @param {Number} dividend
79
78
  * @param {Number} [divisor=1]
80
79
  * @return {Number}
81
80
  * @memberof Utilities */
82
- function mod(a, b=1) { return ((a % b) + b) % b; }
81
+ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
83
82
 
84
83
  /** Clamps the value beween max and min
85
84
  * @param {Number} value
@@ -87,8 +86,8 @@ function mod(a, b=1) { return ((a % b) + b) % b; }
87
86
  * @param {Number} [max=1]
88
87
  * @return {Number}
89
88
  * @memberof Utilities */
90
- function clamp(v, min=0, max=1)
91
- { return v < min ? min : v > max ? max : v; }
89
+ function clamp(value, min=0, max=1)
90
+ { return value < min ? min : value > max ? max : value; }
92
91
 
93
92
  /** Returns what percentage the value is between max and min
94
93
  * @param {Number} value
@@ -96,8 +95,8 @@ function clamp(v, min=0, max=1)
96
95
  * @param {Number} [max=1]
97
96
  * @return {Number}
98
97
  * @memberof Utilities */
99
- function percent(v, min=0, max=1)
100
- { return max-min ? clamp((v-min) / (max-min)) : 0; }
98
+ function percent(value, min=0, max=1)
99
+ { return max-min ? clamp((value-min) / (max-min)) : 0; }
101
100
 
102
101
  /** Linearly interpolates the percent value between max and min
103
102
  * @param {Number} percent
@@ -105,29 +104,32 @@ function percent(v, min=0, max=1)
105
104
  * @param {Number} [max=1]
106
105
  * @return {Number}
107
106
  * @memberof Utilities */
108
- function lerp(p, min=0, max=1){ return min + clamp(p) * (max-min); }
107
+ function lerp(percent, min=0, max=1){ return min + clamp(percent) * (max-min); }
109
108
 
110
109
  /** Applies smoothstep function to the percentage value
111
- * @param {Number} value
110
+ * @param {Number} percent
112
111
  * @return {Number}
113
112
  * @memberof Utilities */
114
- function smoothStep(p) { return p * p * (3 - 2 * p); }
113
+ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
115
114
 
116
115
  /** Returns the nearest power of two not less then the value
117
116
  * @param {Number} value
118
117
  * @return {Number}
119
118
  * @memberof Utilities */
120
- function nearestPowerOfTwo(v) { return 2**Math.ceil(Math.log2(v)); }
119
+ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
121
120
 
122
121
  /** Returns true if two axis aligned bounding boxes are overlapping
123
122
  * @param {Vector2} pointA - Center of box A
124
123
  * @param {Vector2} sizeA - Size of box A
125
124
  * @param {Vector2} pointB - Center of box B
126
- * @param {Vector2} [sizeB] - Size of box B
125
+ * @param {Vector2} sizeB - Size of box B
127
126
  * @return {Boolean} - True if overlapping
128
127
  * @memberof Utilities */
129
- function isOverlapping(pA, sA, pB, sB)
130
- { return abs(pA.x - pB.x)*2 < sA.x + sB.x && abs(pA.y - pB.y)*2 < sA.y + sB.y; }
128
+ function isOverlapping(pointA, sizeA, pointB, sizeB)
129
+ {
130
+ return abs(pointA.x - pointB.x)*2 < sizeA.x + sizeB.x
131
+ && abs(pointA.y - pointB.y)*2 < sizeA.y + sizeB.y;
132
+ }
131
133
 
132
134
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
133
135
  * @param {Number} [frequency=1] - Frequency of the wave in Hz
@@ -154,14 +156,14 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
154
156
  * @param {Number} [valueB=0]
155
157
  * @return {Number}
156
158
  * @memberof Random */
157
- function rand(a=1, b=0) { return b + (a-b)*Math.random(); }
159
+ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
158
160
 
159
161
  /** Returns a floored random value the two values passed in
160
162
  * @param {Number} [valueA=1]
161
163
  * @param {Number} [valueB=0]
162
164
  * @return {Number}
163
165
  * @memberof Random */
164
- function randInt(a=1, b=0) { return rand(a,b)|0; }
166
+ function randInt(valueA=1, valueB=0) { return Math.floor(rand(valueA,valueB)); }
165
167
 
166
168
  /** Randomly returns either -1 or 1
167
169
  * @return {Number}
@@ -188,8 +190,11 @@ function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length
188
190
  * @param {Boolean} [linear]
189
191
  * @return {Color}
190
192
  * @memberof Random */
191
- function randColor(cA = new Color, cB = new Color(0,0,0,1), linear)
192
- { return linear ? cA.lerp(cB, rand()) : new Color(rand(cA.r,cB.r),rand(cA.g,cB.g),rand(cA.b,cB.b),rand(cA.a,cB.a)); }
193
+ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear)
194
+ {
195
+ return linear ? colorA.lerp(colorB, rand()) :
196
+ new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
197
+ }
193
198
 
194
199
  /** Seed used by the randSeeded function
195
200
  * @type {Number}
@@ -207,13 +212,13 @@ function setRandSeed(seed) { randSeed = seed; }
207
212
  * @param {Number} [valueB=0]
208
213
  * @return {Number}
209
214
  * @memberof Random */
210
- function randSeeded(a=1, b=0)
215
+ function randSeeded(valueA=1, valueB=0)
211
216
  {
212
217
  // xorshift algorithm
213
218
  randSeed ^= randSeed << 13;
214
219
  randSeed ^= randSeed >>> 17;
215
220
  randSeed ^= randSeed << 5;
216
- return b + (a-b) * abs(randSeed % 1e9) / 1e9;
221
+ return valueB + (valueA-valueB) * abs(randSeed % 1e9) / 1e9;
217
222
  }
218
223
 
219
224
  ///////////////////////////////////////////////////////////////////////////////
@@ -235,7 +240,7 @@ function vec2(x=0, y)
235
240
 
236
241
  /**
237
242
  * Check if object is a valid Vector2
238
- * @param {Vector2} vector
243
+ * @param {Vector2} v
239
244
  * @return {Boolean}
240
245
  * @memberof Utilities
241
246
  */
@@ -268,27 +273,27 @@ class Vector2
268
273
  copy() { return new Vector2(this.x, this.y); }
269
274
 
270
275
  /** Returns a copy of this vector plus the vector passed in
271
- * @param {Vector2} vector
276
+ * @param {Vector2} v - other vector
272
277
  * @return {Vector2} */
273
278
  add(v) { ASSERT(isVector2(v)); return new Vector2(this.x + v.x, this.y + v.y); }
274
279
 
275
280
  /** Returns a copy of this vector minus the vector passed in
276
- * @param {Vector2} vector
281
+ * @param {Vector2} v - other vector
277
282
  * @return {Vector2} */
278
283
  subtract(v) { ASSERT(isVector2(v)); return new Vector2(this.x - v.x, this.y - v.y); }
279
284
 
280
285
  /** Returns a copy of this vector times the vector passed in
281
- * @param {Vector2} vector
286
+ * @param {Vector2} v - other vector
282
287
  * @return {Vector2} */
283
288
  multiply(v) { ASSERT(isVector2(v)); return new Vector2(this.x * v.x, this.y * v.y); }
284
289
 
285
290
  /** Returns a copy of this vector divided by the vector passed in
286
- * @param {Vector2} vector
291
+ * @param {Vector2} v - other vector
287
292
  * @return {Vector2} */
288
293
  divide(v) { ASSERT(isVector2(v)); return new Vector2(this.x / v.x, this.y / v.y); }
289
294
 
290
295
  /** Returns a copy of this vector scaled by the vector passed in
291
- * @param {Number} scale
296
+ * @param {Number} s - scale
292
297
  * @return {Vector2} */
293
298
  scale(s) { ASSERT(!isVector2(s)); return new Vector2(this.x * s, this.y * s); }
294
299
 
@@ -301,12 +306,12 @@ class Vector2
301
306
  lengthSquared() { return this.x**2 + this.y**2; }
302
307
 
303
308
  /** Returns the distance from this vector to vector passed in
304
- * @param {Vector2} vector
309
+ * @param {Vector2} v - other vector
305
310
  * @return {Number} */
306
311
  distance(v) { return this.distanceSquared(v)**.5; }
307
312
 
308
313
  /** Returns the distance squared from this vector to vector passed in
309
- * @param {Vector2} vector
314
+ * @param {Vector2} v - other vector
310
315
  * @return {Number} */
311
316
  distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
312
317
 
@@ -321,12 +326,12 @@ class Vector2
321
326
  clampLength(length=1) { const l = this.length(); return l > length ? this.scale(length/l) : this; }
322
327
 
323
328
  /** Returns the dot product of this and the vector passed in
324
- * @param {Vector2} vector
329
+ * @param {Vector2} v - other vector
325
330
  * @return {Number} */
326
331
  dot(v) { ASSERT(isVector2(v)); return this.x*v.x + this.y*v.y; }
327
332
 
328
333
  /** Returns the cross product of this and the vector passed in
329
- * @param {Vector2} vector
334
+ * @param {Vector2} v - other vector
330
335
  * @return {Number} */
331
336
  cross(v) { ASSERT(isVector2(v)); return this.x*v.y - this.y*v.x; }
332
337
 
@@ -338,12 +343,17 @@ class Vector2
338
343
  * @param {Number} [angle=0]
339
344
  * @param {Number} [length=1]
340
345
  * @return {Vector2} */
341
- setAngle(a=0, length=1) { this.x = length*Math.sin(a); this.y = length*Math.cos(a); return this; }
346
+ setAngle(angle=0, length=1)
347
+ { this.x = length*Math.sin(angle); this.y = length*Math.cos(angle); return this; }
342
348
 
343
349
  /** Returns copy of this vector rotated by the angle passed in
344
350
  * @param {Number} angle
345
351
  * @return {Vector2} */
346
- 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); }
352
+ rotate(angle)
353
+ {
354
+ const c = Math.cos(angle), s = Math.sin(angle);
355
+ return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
356
+ }
347
357
 
348
358
  /** Returns the integer direction of this vector, corrosponding to multiples of 90 degree rotation (0-3)
349
359
  * @return {Number} */
@@ -362,10 +372,11 @@ class Vector2
362
372
  area() { return abs(this.x * this.y); }
363
373
 
364
374
  /** Returns a new vector that is p percent between this and the vector passed in
365
- * @param {Vector2} vector
375
+ * @param {Vector2} v - other vector
366
376
  * @param {Number} percent
367
377
  * @return {Vector2} */
368
- lerp(v, p) { ASSERT(isVector2(v)); return this.add(v.subtract(this).scale(clamp(p))); }
378
+ lerp(v, percent)
379
+ { ASSERT(isVector2(v)); return this.add(v.subtract(this).scale(clamp(percent))); }
369
380
 
370
381
  /** Returns true if this vector is within the bounds of an array size passed in
371
382
  * @param {Vector2} arraySize
@@ -383,10 +394,10 @@ class Vector2
383
394
 
384
395
  /**
385
396
  * Create a color object with RGBA values
386
- * @param {Number} [r=1]
387
- * @param {Number} [g=1]
388
- * @param {Number} [b=1]
389
- * @param {Number} [a=1]
397
+ * @param {Number} [r=1] - red
398
+ * @param {Number} [g=1] - green
399
+ * @param {Number} [b=1] - blue
400
+ * @param {Number} [a=1] - alpha
390
401
  * @return {Color}
391
402
  * @memberof Utilities
392
403
  */
@@ -394,10 +405,10 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
394
405
 
395
406
  /**
396
407
  * Create a color object with HSLA values
397
- * @param {Number} [h=0]
398
- * @param {Number} [s=0]
399
- * @param {Number} [l=1]
400
- * @param {Number} [a=1]
408
+ * @param {Number} [h=0] - hue
409
+ * @param {Number} [s=0] - saturation
410
+ * @param {Number} [l=1] - lightness
411
+ * @param {Number} [a=1] - alpha
401
412
  * @return {Color}
402
413
  * @memberof Utilities
403
414
  */
@@ -414,11 +425,11 @@ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
414
425
  */
415
426
  class Color
416
427
  {
417
- /** Create a color with the components passed in, white by default
418
- * @param {Number} [red=1]
419
- * @param {Number} [green=1]
420
- * @param {Number} [blue=1]
421
- * @param {Number} [alpha=1] */
428
+ /** Create a color with the rgba components passed in, white by default
429
+ * @param {Number} [r=1] - red
430
+ * @param {Number} [g=1] - green
431
+ * @param {Number} [b=1] - blue
432
+ * @param {Number} [a=1] - alpha*/
422
433
  constructor(r=1, g=1, b=1, a=1)
423
434
  {
424
435
  /** @property {Number} - Red */
@@ -436,22 +447,22 @@ class Color
436
447
  copy() { return new Color(this.r, this.g, this.b, this.a); }
437
448
 
438
449
  /** Returns a copy of this color plus the color passed in
439
- * @param {Color} color
450
+ * @param {Color} c - other color
440
451
  * @return {Color} */
441
452
  add(c) { return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a); }
442
453
 
443
454
  /** Returns a copy of this color minus the color passed in
444
- * @param {Color} color
455
+ * @param {Color} c - other color
445
456
  * @return {Color} */
446
457
  subtract(c) { return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a); }
447
458
 
448
459
  /** Returns a copy of this color times the color passed in
449
- * @param {Color} color
460
+ * @param {Color} c - other color
450
461
  * @return {Color} */
451
462
  multiply(c) { return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a); }
452
463
 
453
464
  /** Returns a copy of this color divided by the color passed in
454
- * @param {Color} color
465
+ * @param {Color} c - other color
455
466
  * @return {Color} */
456
467
  divide(c) { return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a); }
457
468
 
@@ -459,23 +470,24 @@ class Color
459
470
  * @param {Number} scale
460
471
  * @param {Number} [alphaScale=scale]
461
472
  * @return {Color} */
462
- scale(s, a=s) { return new Color(this.r*s, this.g*s, this.b*s, this.a*a); }
473
+ scale(scale, alphaScale=scale)
474
+ { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
463
475
 
464
476
  /** Returns a copy of this color clamped to the valid range between 0 and 1
465
477
  * @return {Color} */
466
478
  clamp() { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }
467
479
 
468
480
  /** Returns a new color that is p percent between this and the color passed in
469
- * @param {Color} color
481
+ * @param {Color} c - other color
470
482
  * @param {Number} percent
471
483
  * @return {Color} */
472
- lerp(c, p) { return this.add(c.subtract(this).scale(clamp(p))); }
484
+ lerp(c, percent) { return this.add(c.subtract(this).scale(clamp(percent))); }
473
485
 
474
486
  /** Sets this color given a hue, saturation, lightness, and alpha
475
- * @param {Number} [hue=0]
476
- * @param {Number} [saturation=0]
477
- * @param {Number} [lightness=1]
478
- * @param {Number} [alpha=1]
487
+ * @param {Number} [h=0] - hue
488
+ * @param {Number} [s=0] - saturation
489
+ * @param {Number} [l=1] - lightness
490
+ * @param {Number} [a=1] - alpha
479
491
  * @return {Color} */
480
492
  setHSLA(h=0, s=0, l=1, a=1)
481
493
  {
@@ -616,12 +628,12 @@ class Timer
616
628
 
617
629
  /** Returns this timer expressed as a string
618
630
  * @return {String} */
619
- toString() { if (debug) { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); }}
631
+ toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
620
632
 
621
633
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
622
634
  * @return {Number} */
623
635
  valueOf() { return this.get(); }
624
- }
636
+ }
625
637
  /**
626
638
  * LittleJS Engine Settings
627
639
  * @namespace Settings
@@ -870,772 +882,456 @@ let medalDisplayIconSize = 50;
870
882
  * @type {Boolean}
871
883
  * @default 0
872
884
  * @memberof Settings */
873
- let medalsPreventUnlock;
885
+ let medalsPreventUnlock;
874
886
  /**
875
- * LittleJS - The Tiny JavaScript Game Engine That Can!
876
- * MIT License - Copyright 2021 Frank Force
877
- *
878
- * Engine Features
879
- * - Object oriented system with base class engine object
880
- * - Base class object handles update, physics, collision, rendering, etc
881
- * - Engine helper classes and functions like Vector2, Color, and Timer
882
- * - Super fast rendering system for tile sheets
883
- * - Sound effects audio with zzfx and music with zzfxm
884
- * - Input processing system with gamepad and touchscreen support
885
- * - Tile layer rendering and collision system
886
- * - Particle effect system
887
- * - Medal system tracks and displays achievements
888
- * - Debug tools and debug rendering system
889
- * - Post processing effects
890
- * - Call engineInit() to start it up!
891
- * @namespace Engine
887
+ * LittleJS Object System
892
888
  */
893
889
 
894
890
  'use strict';
895
891
 
896
- /** Name of engine
897
- * @type {String}
898
- * @default
899
- * @memberof Engine */
900
- const engineName = 'LittleJS';
901
-
902
- /** Version of engine
903
- * @type {String}
904
- * @default
905
- * @memberof Engine */
906
- const engineVersion = '1.6.6';
907
-
908
- /** Frames per second to update objects
909
- * @type {Number}
910
- * @default
911
- * @memberof Engine */
912
- const frameRate = 60;
892
+ /**
893
+ * LittleJS Object Base Object Class
894
+ * - Base object class used by the engine
895
+ * - Automatically adds self to object list
896
+ * - Will be updated and rendered each frame
897
+ * - Renders as a sprite from a tilesheet by default
898
+ * - Can have color and addtive color applied
899
+ * - 2d Physics and collision system
900
+ * - Sorted by renderOrder
901
+ * - Objects can have children attached
902
+ * - Parents are updated before children, and set child transform
903
+ * - Call destroy() to get rid of objects
904
+ *
905
+ * The physics system used by objects is simple and fast with some caveats...
906
+ * - Collision uses the axis aligned size, the object's rotation angle is only for rendering
907
+ * - Objects are guaranteed to not intersect tile collision from physics
908
+ * - If an object starts or is moved inside tile collision, it will not collide with that tile
909
+ * - Collision for objects can be set to be solid to block other objects
910
+ * - Objects may get pushed into overlapping other solid objects, if so they will push away
911
+ * - Solid objects are more performance intensive and should be used sparingly
912
+ * @example
913
+ * // create an engine object, normally you would first extend the class with your own
914
+ * const pos = vec2(2,3);
915
+ * const object = new EngineObject(pos);
916
+ */
917
+ class EngineObject
918
+ {
919
+ /** Create an engine object and adds it to the list of objects
920
+ * @param {Vector2} [position=Vector2()] - World space position of the object
921
+ * @param {Vector2} [size=Vector2(1,1)] - World space size of the object
922
+ * @param {Number} [tileIndex=-1] - Tile to use to render object (-1 is untextured)
923
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
924
+ * @param {Number} [angle=0] - Angle the object is rotated by
925
+ * @param {Color} [color=Color()] - Color to apply to tile when rendered
926
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
927
+ */
928
+ constructor(pos=vec2(), size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, angle=0, color, renderOrder=0)
929
+ {
930
+ // set passed in params
931
+ ASSERT(isVector2(pos) && isVector2(size)); // ensure pos and size are vec2s
913
932
 
914
- /** How many seconds each frame lasts, engine uses a fixed time step
915
- * @type {Number}
916
- * @default 1/60
917
- * @memberof Engine */
918
- const timeDelta = 1/frameRate;
933
+ /** @property {Vector2} - World space position of the object */
934
+ this.pos = pos.copy();
935
+ /** @property {Vector2} - World space width and height of the object */
936
+ this.size = size;
937
+ /** @property {Vector2} - Size of object used for drawing, uses size if not set */
938
+ this.drawSize;
939
+ /** @property {Number} - Tile to use to render object (-1 is untextured) */
940
+ this.tileIndex = tileIndex;
941
+ /** @property {Vector2} - Size of tile in source pixels */
942
+ this.tileSize = tileSize;
943
+ /** @property {Number} - Angle to rotate the object */
944
+ this.angle = angle;
945
+ /** @property {Color} - Color to apply when rendered */
946
+ this.color = color;
947
+ /** @property {Color} - Additive color to apply when rendered */
948
+ this.additiveColor;
919
949
 
920
- /** Array containing all engine objects
921
- * @type {Array}
922
- * @memberof Engine */
923
- let engineObjects = [];
950
+ // set object defaults
951
+ /** @property {Number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
952
+ this.mass = objectDefaultMass;
953
+ /** @property {Number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
954
+ this.damping = objectDefaultDamping;
955
+ /** @property {Number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
956
+ this.angleDamping = objectDefaultAngleDamping;
957
+ /** @property {Number} [elasticity=objectDefaultElasticity] - How bouncy the object is when colliding (0-1) */
958
+ this.elasticity = objectDefaultElasticity;
959
+ /** @property {Number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
960
+ this.friction = objectDefaultFriction;
961
+ /** @property {Number} [gravityScale=1] - How much to scale gravity by for this object */
962
+ this.gravityScale = 1;
963
+ /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
964
+ this.renderOrder = renderOrder;
965
+ /** @property {Vector2} [velocity=Vector2()] - Velocity of the object */
966
+ this.velocity = vec2();
967
+ /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
968
+ this.angleVelocity = 0;
924
969
 
925
- /** Array containing only objects that are set to collide with other objects this frame (for optimization)
926
- * @type {Array}
927
- * @memberof Engine */
928
- let engineObjectsCollide = [];
970
+ // init other internal object stuff
971
+ this.spawnTime = time;
972
+ this.children = [];
973
+ this.collideTiles = 1;
929
974
 
930
- /** Current update frame, used to calculate time
931
- * @type {Number}
932
- * @memberof Engine */
933
- let frame = 0;
975
+ // add to list of objects
976
+ engineObjects.push(this);
977
+ }
978
+
979
+ /** Update the object transform and physics, called automatically by engine once each frame */
980
+ update()
981
+ {
982
+ const parent = this.parent;
983
+ if (parent)
984
+ {
985
+ // copy parent pos/angle
986
+ this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
987
+ this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
988
+ return;
989
+ }
934
990
 
935
- /** Current engine time since start in seconds, derived from frame
936
- * @type {Number}
937
- * @memberof Engine */
938
- let time = 0;
991
+ // limit max speed to prevent missing collisions
992
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
993
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
939
994
 
940
- /** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
941
- * @type {Number}
942
- * @memberof Engine */
943
- let timeReal = 0;
995
+ // apply physics
996
+ const oldPos = this.pos.copy();
997
+ this.velocity.y += gravity * this.gravityScale;
998
+ this.pos.x += this.velocity.x *= this.damping;
999
+ this.pos.y += this.velocity.y *= this.damping;
1000
+ this.angle += this.angleVelocity *= this.angleDamping;
944
1001
 
945
- /** Is the game paused? Causes time and objects to not be updated
946
- * @type {Boolean}
947
- * @default 0
948
- * @memberof Engine */
949
- let paused = 0;
1002
+ // physics sanity checks
1003
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1004
+ ASSERT(this.damping >= 0 && this.damping <= 1);
950
1005
 
951
- /** Set if game is paused
952
- * @param {Boolean} paused
953
- * @memberof Engine */
954
- function setPaused(_paused) { paused = _paused; }
1006
+ if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1007
+ return;
955
1008
 
956
- ///////////////////////////////////////////////////////////////////////////////
1009
+ const wasMovingDown = this.velocity.y < 0;
1010
+ if (this.groundObject)
1011
+ {
1012
+ // apply friction in local space of ground object
1013
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1014
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1015
+ this.groundObject = 0;
1016
+ //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1017
+ }
957
1018
 
958
- /** Start up LittleJS engine with your callback functions
959
- * @param {Function} gameInit - Called once after the engine starts up, setup the game
960
- * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
961
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
962
- * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
963
- * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
964
- * @param {String} [tileImageSource] - Tile image to use, everything starts when the image is finished loading
965
- * @memberof Engine */
966
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource)
967
- {
968
- // init engine when tiles load or fail to load
969
- tileImage.onerror = tileImage.onload = ()=>
970
- {
971
- // save tile image info
972
- tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
973
- debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
1019
+ if (this.collideSolidObjects)
1020
+ {
1021
+ // check collisions against solid objects
1022
+ const epsilon = .001; // necessary to push slightly outside of the collision
1023
+ for (const o of engineObjectsCollide)
1024
+ {
1025
+ // non solid objects don't collide with eachother
1026
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
1027
+ continue;
974
1028
 
975
- // setup html
976
- const styleBody = 'margin:0;overflow:hidden;' + // fill the window
977
- 'background:#000;' + // set background color
978
- 'touch-action:none;' + // prevent mobile pinch to resize
979
- 'user-select:none;' + // prevent mobile hold to select
980
- '-webkit-user-select:none'; // compatibility for ios
981
- document.body.style = styleBody;
982
- document.body.appendChild(mainCanvas = document.createElement('canvas'));
983
- mainContext = mainCanvas.getContext('2d');
1029
+ // check collision
1030
+ if (!isOverlapping(this.pos, this.size, o.pos, o.size))
1031
+ continue;
984
1032
 
985
- // init stuff and start engine
986
- debugInit();
987
- glEnable && glInit();
1033
+ // notify objects of collision and check if should be resolved
1034
+ const collide1 = this.collideWithObject(o);
1035
+ const collide2 = o.collideWithObject(this);
1036
+ if (!collide1 || !collide2)
1037
+ continue;
988
1038
 
989
- // create overlay canvas for hud to appear above gl canvas
990
- document.body.appendChild(overlayCanvas = document.createElement('canvas'));
991
- overlayContext = overlayCanvas.getContext('2d');
1039
+ if (isOverlapping(oldPos, this.size, o.pos, o.size))
1040
+ {
1041
+ // if already was touching, try to push away
1042
+ const deltaPos = oldPos.subtract(o.pos);
1043
+ const length = deltaPos.length();
1044
+ const pushAwayAccel = .001; // push away if already overlapping
1045
+ const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1046
+ this.velocity = this.velocity.add(velocity);
1047
+ if (o.mass) // push away if not fixed
1048
+ o.velocity = o.velocity.subtract(velocity);
1049
+
1050
+ debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f00');
1051
+ continue;
1052
+ }
992
1053
 
993
- // set canvas style
994
- const styleCanvas = 'position:absolute;' +
995
- 'top:50%;left:50%;transform:translate(-50%,-50%);' + // center the canvas
996
- (canvasPixelated?'image-rendering:pixelated':''); // set pixelated rendering
997
- (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
998
-
999
- gameInit();
1000
- engineUpdate();
1001
- };
1054
+ // check for collision
1055
+ const sizeBoth = this.size.add(o.size);
1056
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
1057
+ const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1058
+ const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1059
+ const elasticity = max(this.elasticity, o.elasticity);
1060
+
1061
+ if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
1062
+ {
1063
+ // push outside object collision
1064
+ this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
1065
+ if (o.groundObject && wasMovingDown || !o.mass)
1066
+ {
1067
+ // set ground object if landed on something
1068
+ if (wasMovingDown)
1069
+ this.groundObject = o;
1002
1070
 
1003
- // frame time tracking
1004
- let frameTimeLastMS = 0, frameTimeBufferMS, averageFPS;
1071
+ // bounce if other object is fixed or grounded
1072
+ this.velocity.y *= -elasticity;
1073
+ }
1074
+ else if (o.mass)
1075
+ {
1076
+ // inelastic collision
1077
+ const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
1005
1078
 
1006
- // main update loop
1007
- function engineUpdate(frameTimeMS=0)
1008
- {
1009
- // update time keeping
1010
- let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
1011
- frameTimeLastMS = frameTimeMS;
1012
- if (debug || showWatermark)
1013
- averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
1014
- const debugSpeedUp = debug && keyIsDown(107); // +
1015
- const debugSpeedDown = debug && keyIsDown(109); // -
1016
- if (debug) // +/- to speed/slow time
1017
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
1018
- timeReal += frameTimeDeltaMS / 1e3;
1019
- frameTimeBufferMS += !paused * frameTimeDeltaMS;
1020
- if (!debugSpeedUp)
1021
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
1079
+ // elastic collision
1080
+ const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
1081
+ + o.velocity.y * 2 * o.mass / (this.mass + o.mass);
1082
+ const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
1083
+ + this.velocity.y * 2 * this.mass / (this.mass + o.mass);
1022
1084
 
1023
- if (canvasFixedSize.x)
1024
- {
1025
- // clear canvas and set fixed size
1026
- mainCanvas.width = canvasFixedSize.x;
1027
- mainCanvas.height = canvasFixedSize.y;
1028
-
1029
- // fit to window by adding space on top or bottom if necessary
1030
- const aspect = innerWidth / innerHeight;
1031
- const fixedAspect = mainCanvas.width / mainCanvas.height;
1032
- (glCanvas||mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
1033
- (glCanvas||mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
1034
- }
1035
- else
1036
- {
1037
- // clear canvas and set size to same as window
1038
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
1039
- mainCanvas.height = min(innerHeight, canvasMaxSize.y);
1040
- }
1041
-
1042
- // clear overlay canvas and set size
1043
- overlayCanvas.width = mainCanvas.width;
1044
- overlayCanvas.height = mainCanvas.height;
1085
+ // lerp betwen elastic or inelastic based on elasticity
1086
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1087
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1088
+ }
1089
+ }
1090
+ if (!smallStepUp && isBlockedX) // resolve x collision
1091
+ {
1092
+ // push outside collision
1093
+ this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
1094
+ if (o.mass)
1095
+ {
1096
+ // inelastic collision
1097
+ const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
1045
1098
 
1046
- // save canvas size
1047
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
1099
+ // elastic collision
1100
+ const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
1101
+ + o.velocity.x * 2 * o.mass / (this.mass + o.mass);
1102
+ const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
1103
+ + this.velocity.x * 2 * this.mass / (this.mass + o.mass);
1048
1104
 
1049
- if (paused)
1050
- {
1051
- // do post update even when paused
1052
- inputUpdate();
1053
- debugUpdate();
1054
- gameUpdatePost();
1055
- inputUpdatePost();
1105
+ // lerp betwen elastic or inelastic based on elasticity
1106
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1107
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1108
+ }
1109
+ else // bounce if other object is fixed
1110
+ this.velocity.x *= -elasticity;
1111
+ }
1112
+ debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
1113
+ }
1056
1114
  }
1057
- else
1115
+ if (this.collideTiles)
1058
1116
  {
1059
- // apply time delta smoothing, improves smoothness of framerate in some browsers
1060
- let deltaSmooth = 0;
1061
- if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
1062
- {
1063
- // force an update each frame if time is close enough (not just a fast refresh rate)
1064
- deltaSmooth = frameTimeBufferMS;
1065
- frameTimeBufferMS = 0;
1066
- }
1067
-
1068
- // update multiple frames if necessary in case of slow framerate
1069
- for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
1117
+ // check collision against tiles
1118
+ if (tileCollisionTest(this.pos, this.size, this))
1070
1119
  {
1071
- // update game and objects
1072
- inputUpdate();
1073
- gameUpdate();
1074
- engineObjectsUpdate();
1120
+ // if already was stuck in collision, don't do anything
1121
+ // this should not happen unless something starts in collision
1122
+ if (!tileCollisionTest(oldPos, this.size, this))
1123
+ {
1124
+ // test which side we bounced off (or both if a corner)
1125
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1126
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1127
+ if (isBlockedY || !isBlockedX)
1128
+ {
1129
+ // set if landed on ground
1130
+ this.groundObject = wasMovingDown;
1075
1131
 
1076
- // do post update
1077
- debugUpdate();
1078
- gameUpdatePost();
1079
- inputUpdatePost();
1080
- }
1132
+ // bounce velocity
1133
+ this.velocity.y *= -this.elasticity;
1081
1134
 
1082
- // add the time smoothing back in
1083
- frameTimeBufferMS += deltaSmooth;
1084
- }
1085
-
1086
- // render sort then render while removing destroyed objects
1087
- enginePreRender();
1088
- gameRender();
1089
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
1090
- for (const o of engineObjects)
1091
- o.destroyed || o.render();
1092
- gameRenderPost();
1093
- glRenderPostProcess();
1094
- medalsRender();
1095
- touchGamepadRender();
1096
- debugRender();
1097
- glEnable && glCopyToContext(mainContext);
1135
+ // adjust next velocity to settle on ground
1136
+ const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1137
+ if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1138
+ this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1098
1139
 
1099
- if (showWatermark)
1100
- {
1101
- // update fps
1102
- overlayContext.textAlign = 'right';
1103
- overlayContext.textBaseline = 'top';
1104
- overlayContext.font = '1em monospace';
1105
- overlayContext.fillStyle = '#000';
1106
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
1107
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
1108
- + (glEnable ? ' GL' : ' 2D') ;
1109
- overlayContext.fillText(text, mainCanvas.width-3, 3);
1110
- overlayContext.fillStyle = '#fff';
1111
- overlayContext.fillText(text, mainCanvas.width-2, 2);
1112
- drawCount = 0;
1140
+ // move to previous position
1141
+ this.pos.y = oldPos.y;
1142
+ }
1143
+ if (isBlockedX)
1144
+ {
1145
+ // move to previous position and bounce
1146
+ this.pos.x = oldPos.x;
1147
+ this.velocity.x *= -this.elasticity;
1148
+ }
1149
+ }
1150
+ }
1113
1151
  }
1114
-
1115
- requestAnimationFrame(engineUpdate);
1116
1152
  }
1153
+
1154
+ /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
1155
+ render()
1156
+ {
1157
+ // default object render
1158
+ drawTile(this.pos, this.drawSize || this.size, this.tileIndex, this.tileSize, this.color, this.angle, this.mirror, this.additiveColor);
1159
+ }
1160
+
1161
+ /** Destroy this object, destroy it's children, detach it's parent, and mark it for removal */
1162
+ destroy()
1163
+ {
1164
+ if (this.destroyed)
1165
+ return;
1166
+
1167
+ // disconnect from parent and destroy chidren
1168
+ this.destroyed = 1;
1169
+ this.parent && this.parent.removeChild(this);
1170
+ for (const child of this.children)
1171
+ child.destroy(child.parent = 0);
1172
+ }
1173
+
1174
+ /** Called to check if a tile collision should be resolved
1175
+ * @param {Number} tileData - the value of the tile at the position
1176
+ * @param {Vector2} pos - tile where the collision occured
1177
+ * @return {Boolean} - true if the collision should be resolved */
1178
+ collideWithTile(tileData, pos) { return tileData > 0; }
1179
+
1180
+ /** Called to check if a tile raycast hit
1181
+ * @param {Number} tileData - the value of the tile at the position
1182
+ * @param {Vector2} pos - tile where the raycast is
1183
+ * @return {Boolean} - true if the raycast should hit */
1184
+ collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1117
1185
 
1118
- // set tile image source to load the image and start the engine
1119
- tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
1120
- }
1121
-
1122
- // Called automatically by engine to setup render system
1123
- function enginePreRender()
1124
- {
1125
- // save canvas size
1126
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
1186
+ /** Called to check if a object collision should be resolved
1187
+ * @param {EngineObject} object - the object to test against
1188
+ * @return {Boolean} - true if the collision should be resolved
1189
+ */
1190
+ collideWithObject(object) { return 1; }
1127
1191
 
1128
- // disable smoothing for pixel art
1129
- mainContext.imageSmoothingEnabled = !canvasPixelated;
1192
+ /** How long since the object was created
1193
+ * @return {Number} */
1194
+ getAliveTime() { return time - this.spawnTime; }
1130
1195
 
1131
- // setup gl rendering if enabled
1132
- glEnable && glPreRender();
1133
- }
1196
+ /** Apply acceleration to this object (adjust velocity, not affected by mass)
1197
+ * @param {Vector2} acceleration */
1198
+ applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
1134
1199
 
1135
- /** Update each engine object, remove destroyed objects, and update time
1136
- * @memberof Engine */
1137
- function engineObjectsUpdate()
1138
- {
1139
- // get list of solid objects for physics optimzation
1140
- engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
1200
+ /** Apply force to this object (adjust velocity, affected by mass)
1201
+ * @param {Vector2} force */
1202
+ applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
1203
+
1204
+ /** Get the direction of the mirror
1205
+ * @return {Number} -1 if this.mirror is true, or 1 if not mirrored */
1206
+ getMirrorSign() { return this.mirror ? -1 : 1; }
1141
1207
 
1142
- // recursive object update
1143
- function updateObject(o)
1144
- {
1145
- if (!o.destroyed)
1146
- {
1147
- o.update();
1148
- for (const child of o.children)
1149
- updateObject(child);
1150
- }
1208
+ /** Attaches a child to this with a given local transform
1209
+ * @param {EngineObject} child
1210
+ * @param {Vector2} [localPos=Vector2()]
1211
+ * @param {Number} [localAngle=0] */
1212
+ addChild(child, localPos=vec2(), localAngle=0)
1213
+ {
1214
+ ASSERT(!child.parent && !this.children.includes(child));
1215
+ this.children.push(child);
1216
+ child.parent = this;
1217
+ child.localPos = localPos.copy();
1218
+ child.localAngle = localAngle;
1151
1219
  }
1152
- for (const o of engineObjects)
1153
- o.parent || updateObject(o);
1154
-
1155
- // remove destroyed objects
1156
- engineObjects = engineObjects.filter(o=>!o.destroyed);
1157
-
1158
- // increment frame and update time
1159
- time = ++frame / frameRate;
1160
- }
1161
-
1162
- /** Destroy and remove all objects
1163
- * @memberof Engine */
1164
- function engineObjectsDestroy()
1165
- {
1166
- for (const o of engineObjects)
1167
- o.parent || o.destroy();
1168
- engineObjects = engineObjects.filter(o=>!o.destroyed);
1169
- }
1170
1220
 
1171
- /** Triggers a callback for each object within a given area
1172
- * @param {Vector2} [pos] - Center of test area
1173
- * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
1174
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
1175
- * @param {Array} [objects=engineObjects] - List of objects to check
1176
- * @memberof Engine */
1177
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
1178
- {
1179
- if (!pos) // all objects
1221
+ /** Removes a child from this one
1222
+ * @param {EngineObject} child */
1223
+ removeChild(child)
1180
1224
  {
1181
- for (const o of objects)
1182
- callbackFunction(o);
1225
+ ASSERT(child.parent == this && this.children.includes(child));
1226
+ this.children.splice(this.children.indexOf(child), 1);
1227
+ child.parent = 0;
1183
1228
  }
1184
- else if (size.x != undefined) // bounding box test
1229
+
1230
+ /** Set how this object collides
1231
+ * @param {Boolean} [collideSolidObjects=1] - Does it collide with solid objects
1232
+ * @param {Boolean} [isSolid=1] - Does it collide with and block other objects (expensive in large numbers)
1233
+ * @param {Boolean} [collideTiles=1] - Does it collide with the tile collision */
1234
+ setCollision(collideSolidObjects=1, isSolid=1, collideTiles=1)
1185
1235
  {
1186
- for (const o of objects)
1187
- isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
1236
+ ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1237
+
1238
+ this.collideSolidObjects = collideSolidObjects;
1239
+ this.isSolid = isSolid;
1240
+ this.collideTiles = collideTiles;
1188
1241
  }
1189
- else // circle test
1242
+
1243
+ /** Returns string containg info about this object for debugging
1244
+ * @return {String} */
1245
+ toString()
1190
1246
  {
1191
- const sizeSquared = size*size;
1192
- for (const o of objects)
1193
- pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
1247
+ if (debug)
1248
+ {
1249
+ let text = 'type = ' + this.constructor.name;
1250
+ if (this.pos.x || this.pos.y)
1251
+ text += '\npos = ' + this.pos;
1252
+ if (this.velocity.x || this.velocity.y)
1253
+ text += '\nvelocity = ' + this.velocity;
1254
+ if (this.size.x || this.size.y)
1255
+ text += '\nsize = ' + this.size;
1256
+ if (this.angle)
1257
+ text += '\nangle = ' + this.angle.toFixed(3);
1258
+ if (this.color)
1259
+ text += '\ncolor = ' + this.color;
1260
+ return text;
1261
+ }
1194
1262
  }
1195
- }
1263
+ }
1196
1264
  /**
1197
- * LittleJS Object System
1265
+ * LittleJS Drawing System
1266
+ * - Hybrid with both Canvas2D and WebGL available
1267
+ * - Super fast tile sheet rendering with WebGL
1268
+ * - Can apply rotation, mirror, color and additive color
1269
+ * - Many useful utility functions
1270
+ *
1271
+ * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
1272
+ * There are 3 canvas/contexts available to draw to...
1273
+ * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
1274
+ * glCanvas - Used by the accelerated WebGL batch rendering system.
1275
+ * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1276
+ *
1277
+ * The WebGL rendering system is very fast with some caveats...
1278
+ * - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1279
+ * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1280
+ * - Group additive rendering together using renderOrder to mitigate this issue
1281
+ *
1282
+ * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
1283
+ * @namespace Draw
1198
1284
  */
1199
1285
 
1200
1286
  'use strict';
1201
1287
 
1202
- /**
1203
- * LittleJS Object Base Object Class
1204
- * - Base object class used by the engine
1205
- * - Automatically adds self to object list
1206
- * - Will be updated and rendered each frame
1207
- * - Renders as a sprite from a tilesheet by default
1208
- * - Can have color and addtive color applied
1209
- * - 2d Physics and collision system
1210
- * - Sorted by renderOrder
1211
- * - Objects can have children attached
1212
- * - Parents are updated before children, and set child transform
1213
- * - Call destroy() to get rid of objects
1214
- *
1215
- * The physics system used by objects is simple and fast with some caveats...
1216
- * - Collision uses the axis aligned size, the object's rotation angle is only for rendering
1217
- * - Objects are guaranteed to not intersect tile collision from physics
1218
- * - If an object starts or is moved inside tile collision, it will not collide with that tile
1219
- * - Collision for objects can be set to be solid to block other objects
1220
- * - Objects may get pushed into overlapping other solid objects, if so they will push away
1221
- * - Solid objects are more performance intensive and should be used sparingly
1222
- * @example
1223
- * // create an engine object, normally you would first extend the class with your own
1224
- * const pos = vec2(2,3);
1225
- * const object = new EngineObject(pos);
1226
- */
1227
- class EngineObject
1228
- {
1229
- /** Create an engine object and adds it to the list of objects
1230
- * @param {Vector2} [position=Vector2()] - World space position of the object
1231
- * @param {Vector2} [size=Vector2(1,1)] - World space size of the object
1232
- * @param {Number} [tileIndex=-1] - Tile to use to render object (-1 is untextured)
1233
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
1234
- * @param {Number} [angle=0] - Angle the object is rotated by
1235
- * @param {Color} [color=Color()] - Color to apply to tile when rendered
1236
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
1237
- */
1238
- constructor(pos=vec2(), size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, angle=0, color, renderOrder=0)
1239
- {
1240
- // set passed in params
1241
- ASSERT(isVector2(pos) && isVector2(size)); // ensure pos and size are vec2s
1242
-
1243
- /** @property {Vector2} - World space position of the object */
1244
- this.pos = pos.copy();
1245
- /** @property {Vector2} - World space width and height of the object */
1246
- this.size = size;
1247
- /** @property {Vector2} - Size of object used for drawing, uses size if not set */
1248
- this.drawSize;
1249
- /** @property {Number} - Tile to use to render object (-1 is untextured) */
1250
- this.tileIndex = tileIndex;
1251
- /** @property {Vector2} - Size of tile in source pixels */
1252
- this.tileSize = tileSize;
1253
- /** @property {Number} - Angle to rotate the object */
1254
- this.angle = angle;
1255
- /** @property {Color} - Color to apply when rendered */
1256
- this.color = color;
1257
- /** @property {Color} - Additive color to apply when rendered */
1258
- this.additiveColor;
1288
+ /** The primary 2D canvas visible to the user
1289
+ * @type {HTMLCanvasElement}
1290
+ * @memberof Draw */
1291
+ let mainCanvas;
1259
1292
 
1260
- // set object defaults
1261
- /** @property {Number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
1262
- this.mass = objectDefaultMass;
1263
- /** @property {Number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
1264
- this.damping = objectDefaultDamping;
1265
- /** @property {Number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
1266
- this.angleDamping = objectDefaultAngleDamping;
1267
- /** @property {Number} [elasticity=objectDefaultElasticity] - How bouncy the object is when colliding (0-1) */
1268
- this.elasticity = objectDefaultElasticity;
1269
- /** @property {Number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
1270
- this.friction = objectDefaultFriction;
1271
- /** @property {Number} [gravityScale=1] - How much to scale gravity by for this object */
1272
- this.gravityScale = 1;
1273
- /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1274
- this.renderOrder = renderOrder;
1275
- /** @property {Vector2} [velocity=Vector2()] - Velocity of the object */
1276
- this.velocity = new Vector2();
1277
- /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1278
- this.angleVelocity = 0;
1293
+ /** 2d context for mainCanvas
1294
+ * @type {CanvasRenderingContext2D}
1295
+ * @memberof Draw */
1296
+ let mainContext;
1279
1297
 
1280
- // init other internal object stuff
1281
- this.spawnTime = time;
1282
- this.children = [];
1283
- this.collideTiles = 1;
1298
+ /** A canvas that appears on top of everything the same size as mainCanvas
1299
+ * @type {HTMLCanvasElement}
1300
+ * @memberof Draw */
1301
+ let overlayCanvas;
1284
1302
 
1285
- // add to list of objects
1286
- engineObjects.push(this);
1287
- }
1288
-
1289
- /** Update the object transform and physics, called automatically by engine once each frame */
1290
- update()
1291
- {
1292
- const parent = this.parent;
1293
- if (parent)
1294
- {
1295
- // copy parent pos/angle
1296
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1297
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1298
- return;
1299
- }
1303
+ /** 2d context for overlayCanvas
1304
+ * @type {CanvasRenderingContext2D}
1305
+ * @memberof Draw */
1306
+ let overlayContext;
1300
1307
 
1301
- // limit max speed to prevent missing collisions
1302
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1303
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1308
+ /** The size of the main canvas (and other secondary canvases)
1309
+ * @type {Vector2}
1310
+ * @memberof Draw */
1311
+ let mainCanvasSize = vec2();
1304
1312
 
1305
- // apply physics
1306
- const oldPos = this.pos.copy();
1307
- this.velocity.y += gravity * this.gravityScale;
1308
- this.pos.x += this.velocity.x *= this.damping;
1309
- this.pos.y += this.velocity.y *= this.damping;
1310
- this.angle += this.angleVelocity *= this.angleDamping;
1313
+ /** Tile sheet for batch rendering system
1314
+ * @type {CanvasImageSource}
1315
+ * @memberof Draw */
1316
+ const tileImage = new Image;
1311
1317
 
1312
- // physics sanity checks
1313
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1314
- ASSERT(this.damping >= 0 && this.damping <= 1);
1318
+ // Engine internal variables not exposed to documentation
1319
+ let tileImageSize, tileImageFixBleed, drawCount;
1315
1320
 
1316
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1317
- return;
1318
-
1319
- const wasMovingDown = this.velocity.y < 0;
1320
- if (this.groundObject)
1321
- {
1322
- // apply friction in local space of ground object
1323
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1324
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1325
- this.groundObject = 0;
1326
- //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1327
- }
1328
-
1329
- if (this.collideSolidObjects)
1330
- {
1331
- // check collisions against solid objects
1332
- const epsilon = .001; // necessary to push slightly outside of the collision
1333
- for (const o of engineObjectsCollide)
1334
- {
1335
- // non solid objects don't collide with eachother
1336
- if (!this.isSolid & !o.isSolid || o.destroyed || o.parent || o == this)
1337
- continue;
1338
-
1339
- // check collision
1340
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
1341
- continue;
1342
-
1343
- // pass collision to objects
1344
- if (!this.collideWithObject(o) | !o.collideWithObject(this))
1345
- continue;
1346
-
1347
- if (isOverlapping(oldPos, this.size, o.pos, o.size))
1348
- {
1349
- // if already was touching, try to push away
1350
- const deltaPos = oldPos.subtract(o.pos);
1351
- const length = deltaPos.length();
1352
- const pushAwayAccel = .001; // push away if already overlapping
1353
- const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1354
- this.velocity = this.velocity.add(velocity);
1355
- if (o.mass) // push away if not fixed
1356
- o.velocity = o.velocity.subtract(velocity);
1357
-
1358
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f00');
1359
- continue;
1360
- }
1361
-
1362
- // check for collision
1363
- const sizeBoth = this.size.add(o.size);
1364
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
1365
- const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1366
- const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1367
- const elasticity = max(this.elasticity, o.elasticity);
1368
-
1369
- if (smallStepUp | isBlockedY | !isBlockedX) // resolve y collision
1370
- {
1371
- // push outside object collision
1372
- this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
1373
- if (o.groundObject && wasMovingDown || !o.mass)
1374
- {
1375
- // set ground object if landed on something
1376
- if (wasMovingDown)
1377
- this.groundObject = o;
1378
-
1379
- // bounce if other object is fixed or grounded
1380
- this.velocity.y *= -elasticity;
1381
- }
1382
- else if (o.mass)
1383
- {
1384
- // inelastic collision
1385
- const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
1386
-
1387
- // elastic collision
1388
- const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
1389
- + o.velocity.y * 2 * o.mass / (this.mass + o.mass);
1390
- const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
1391
- + this.velocity.y * 2 * this.mass / (this.mass + o.mass);
1392
-
1393
- // lerp betwen elastic or inelastic based on elasticity
1394
- this.velocity.y = lerp(elasticity, inelastic, elastic0);
1395
- o.velocity.y = lerp(elasticity, inelastic, elastic1);
1396
- }
1397
- }
1398
- if (!smallStepUp & isBlockedX) // resolve x collision
1399
- {
1400
- // push outside collision
1401
- this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
1402
- if (o.mass)
1403
- {
1404
- // inelastic collision
1405
- const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
1406
-
1407
- // elastic collision
1408
- const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
1409
- + o.velocity.x * 2 * o.mass / (this.mass + o.mass);
1410
- const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
1411
- + this.velocity.x * 2 * this.mass / (this.mass + o.mass);
1412
-
1413
- // lerp betwen elastic or inelastic based on elasticity
1414
- this.velocity.x = lerp(elasticity, inelastic, elastic0);
1415
- o.velocity.x = lerp(elasticity, inelastic, elastic1);
1416
- }
1417
- else // bounce if other object is fixed
1418
- this.velocity.x *= -elasticity;
1419
- }
1420
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
1421
- }
1422
- }
1423
- if (this.collideTiles)
1424
- {
1425
- // check collision against tiles
1426
- if (tileCollisionTest(this.pos, this.size, this))
1427
- {
1428
- // if already was stuck in collision, don't do anything
1429
- // this should not happen unless something starts in collision
1430
- if (!tileCollisionTest(oldPos, this.size, this))
1431
- {
1432
- // test which side we bounced off (or both if a corner)
1433
- const isBlockedY = tileCollisionTest(new Vector2(oldPos.x, this.pos.y), this.size, this);
1434
- const isBlockedX = tileCollisionTest(new Vector2(this.pos.x, oldPos.y), this.size, this);
1435
- if (isBlockedY | !isBlockedX)
1436
- {
1437
- // set if landed on ground
1438
- this.groundObject = wasMovingDown;
1439
-
1440
- // bounce velocity
1441
- this.velocity.y *= -this.elasticity;
1442
-
1443
- // adjust next velocity to settle on ground
1444
- const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1445
- if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1446
- this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1447
-
1448
- // move to previous position
1449
- this.pos.y = oldPos.y;
1450
- }
1451
- if (isBlockedX)
1452
- {
1453
- // move to previous position and bounce
1454
- this.pos.x = oldPos.x;
1455
- this.velocity.x *= -this.elasticity;
1456
- }
1457
- }
1458
- }
1459
- }
1460
- }
1461
-
1462
- /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
1463
- render()
1464
- {
1465
- // default object render
1466
- drawTile(this.pos, this.drawSize || this.size, this.tileIndex, this.tileSize, this.color, this.angle, this.mirror, this.additiveColor);
1467
- }
1468
-
1469
- /** Destroy this object, destroy it's children, detach it's parent, and mark it for removal */
1470
- destroy()
1471
- {
1472
- if (this.destroyed)
1473
- return;
1474
-
1475
- // disconnect from parent and destroy chidren
1476
- this.destroyed = 1;
1477
- this.parent && this.parent.removeChild(this);
1478
- for (const child of this.children)
1479
- child.destroy(child.parent = 0);
1480
- }
1481
-
1482
- /** Called to check if a tile collision should be resolved
1483
- * @param {Number} tileData - the value of the tile at the position
1484
- * @param {Vector2} pos - tile where the collision occured
1485
- * @return {Boolean} - true if the collision should be resolved */
1486
- collideWithTile(tileData, pos) { return tileData > 0; }
1487
-
1488
- /** Called to check if a tile raycast hit
1489
- * @param {Number} tileData - the value of the tile at the position
1490
- * @param {Vector2} pos - tile where the raycast is
1491
- * @return {Boolean} - true if the raycast should hit */
1492
- collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1493
-
1494
- /** Called to check if a object collision should be resolved
1495
- * @param {EngineObject} object - the object to test against
1496
- * @return {Boolean} - true if the collision should be resolved
1497
- */
1498
- collideWithObject(o) { return 1; }
1499
-
1500
- /** How long since the object was created
1501
- * @return {Number} */
1502
- getAliveTime() { return time - this.spawnTime; }
1503
-
1504
- /** Apply acceleration to this object (adjust velocity, not affected by mass)
1505
- * @param {Vector2} acceleration */
1506
- applyAcceleration(a) { if (this.mass) this.velocity = this.velocity.add(a); }
1507
-
1508
- /** Apply force to this object (adjust velocity, affected by mass)
1509
- * @param {Vector2} force */
1510
- applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
1511
-
1512
- /** Get the direction of the mirror
1513
- * @return {Number} -1 if this.mirror is true, or 1 if not mirrored */
1514
- getMirrorSign() { return this.mirror ? -1 : 1; }
1515
-
1516
- /** Attaches a child to this with a given local transform
1517
- * @param {EngineObject} child
1518
- * @param {Vector2} [localPos=Vector2()]
1519
- * @param {Number} [localAngle=0] */
1520
- addChild(child, localPos=vec2(), localAngle=0)
1521
- {
1522
- ASSERT(!child.parent && !this.children.includes(child));
1523
- this.children.push(child);
1524
- child.parent = this;
1525
- child.localPos = localPos.copy();
1526
- child.localAngle = localAngle;
1527
- }
1528
-
1529
- /** Removes a child from this one
1530
- * @param {EngineObject} child */
1531
- removeChild(child)
1532
- {
1533
- ASSERT(child.parent == this && this.children.includes(child));
1534
- this.children.splice(this.children.indexOf(child), 1);
1535
- child.parent = 0;
1536
- }
1537
-
1538
- /** Set how this object collides
1539
- * @param {Boolean} [collideSolidObjects=1] - Does it collide with solid objects
1540
- * @param {Boolean} [isSolid=1] - Does it collide with and block other objects (expensive in large numbers)
1541
- * @param {Boolean} [collideTiles=1] - Does it collide with the tile collision */
1542
- setCollision(collideSolidObjects=1, isSolid=1, collideTiles=1)
1543
- {
1544
- ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1545
-
1546
- this.collideSolidObjects = collideSolidObjects;
1547
- this.isSolid = isSolid;
1548
- this.collideTiles = collideTiles;
1549
- }
1550
-
1551
- /** Returns string containg info about this object for debugging
1552
- * @return {String} */
1553
- toString()
1554
- {
1555
- if (debug)
1556
- {
1557
- let text = 'type = ' + this.constructor.name;
1558
- if (this.pos.x || this.pos.y)
1559
- text += '\npos = ' + this.pos;
1560
- if (this.velocity.x || this.velocity.y)
1561
- text += '\nvelocity = ' + this.velocity;
1562
- if (this.size.x || this.size.y)
1563
- text += '\nsize = ' + this.size;
1564
- if (this.angle)
1565
- text += '\nangle = ' + this.angle.toFixed(3);
1566
- if (this.color)
1567
- text += '\ncolor = ' + this.color;
1568
- return text;
1569
- }
1570
- }
1571
- }
1572
- /**
1573
- * LittleJS Drawing System
1574
- * - Hybrid with both Canvas2D and WebGL available
1575
- * - Super fast tile sheet rendering with WebGL
1576
- * - Can apply rotation, mirror, color and additive color
1577
- * - Many useful utility functions
1578
- *
1579
- * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
1580
- * There are 3 canvas/contexts available to draw to...
1581
- * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
1582
- * glCanvas - Used by the accelerated WebGL batch rendering system.
1583
- * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1584
- *
1585
- * The WebGL rendering system is very fast with some caveats...
1586
- * - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1587
- * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1588
- * - Group additive rendering together using renderOrder to mitigate this issue
1589
- *
1590
- * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
1591
- * @namespace Draw
1592
- */
1593
-
1594
- 'use strict';
1595
-
1596
- /** The primary 2D canvas visible to the user
1597
- * @type {HTMLCanvasElement}
1598
- * @memberof Draw */
1599
- let mainCanvas;
1600
-
1601
- /** 2d context for mainCanvas
1602
- * @type {CanvasRenderingContext2D}
1603
- * @memberof Draw */
1604
- let mainContext;
1605
-
1606
- /** A canvas that appears on top of everything the same size as mainCanvas
1607
- * @type {HTMLCanvasElement}
1608
- * @memberof Draw */
1609
- let overlayCanvas;
1610
-
1611
- /** 2d context for overlayCanvas
1612
- * @type {CanvasRenderingContext2D}
1613
- * @memberof Draw */
1614
- let overlayContext;
1615
-
1616
- /** The size of the main canvas (and other secondary canvases)
1617
- * @type {Vector2}
1618
- * @memberof Draw */
1619
- let mainCanvasSize = vec2();
1620
-
1621
- /** Tile sheet for batch rendering system
1622
- * @type {CanvasImageSource}
1623
- * @memberof Draw */
1624
- const tileImage = new Image;
1625
-
1626
- // Engine internal variables not exposed to documentation
1627
- let tileImageSize, tileImageFixBleed, drawCount;
1628
-
1629
- /** Convert from screen to world space coordinates
1630
- * - if calling outside of render, you may need to manually set mainCanvasSize
1631
- * @param {Vector2} screenPos
1632
- * @return {Vector2}
1633
- * @memberof Draw */
1634
- function screenToWorld(screenPos)
1635
- {
1636
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1637
- return screenPos.add(vec2(.5)).subtract(mainCanvasSize.scale(.5)).multiply(vec2(1/cameraScale,-1/cameraScale)).add(cameraPos);
1638
- }
1321
+ /** Convert from screen to world space coordinates
1322
+ * - if calling outside of render, you may need to manually set mainCanvasSize
1323
+ * @param {Vector2} screenPos
1324
+ * @return {Vector2}
1325
+ * @memberof Draw */
1326
+ function screenToWorld(screenPos)
1327
+ {
1328
+ ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1329
+ return screenPos
1330
+ .add(vec2(.5))
1331
+ .subtract(mainCanvasSize.scale(.5))
1332
+ .multiply(vec2(1/cameraScale,-1/cameraScale))
1333
+ .add(cameraPos);
1334
+ }
1639
1335
 
1640
1336
  /** Convert from world to screen space coordinates
1641
1337
  * - if calling outside of render, you may need to manually set mainCanvasSize
@@ -1645,7 +1341,11 @@ function screenToWorld(screenPos)
1645
1341
  function worldToScreen(worldPos)
1646
1342
  {
1647
1343
  ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1648
- return worldPos.subtract(cameraPos).multiply(vec2(cameraScale,-cameraScale)).add(mainCanvasSize.scale(.5)).subtract(vec2(.5));
1344
+ return worldPos
1345
+ .subtract(cameraPos)
1346
+ .multiply(vec2(cameraScale,-cameraScale))
1347
+ .add(mainCanvasSize.scale(.5))
1348
+ .subtract(vec2(.5));
1649
1349
  }
1650
1350
 
1651
1351
  /** Draw textured tile centered in world space, with color applied if using WebGL
@@ -1659,8 +1359,8 @@ function worldToScreen(worldPos)
1659
1359
  * @param {Color} [additiveColor=Color(0,0,0,0)] - Additive color to be applied
1660
1360
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1661
1361
  * @memberof Draw */
1662
- function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle=0, mirror,
1663
- additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1362
+ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color,
1363
+ angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1664
1364
  {
1665
1365
  showWatermark && ++drawCount;
1666
1366
  if (glEnable && useWebGL)
@@ -1953,7 +1653,7 @@ function toggleFullscreen()
1953
1653
  else if (document.body.requestFullscreen)
1954
1654
  document.body.requestFullscreen();
1955
1655
  }
1956
-
1656
+
1957
1657
  /**
1958
1658
  * LittleJS Input System
1959
1659
  * - Tracks key down, pressed, and released
@@ -2387,7 +2087,7 @@ function touchGamepadRender()
2387
2087
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2388
2088
  for (let i=4; i--;)
2389
2089
  {
2390
- const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2090
+ const pos = rightCenter.add(vec2().setAngle(i*PI/2, touchGamepadSize/2));
2391
2091
  overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2392
2092
  overlayContext.beginPath();
2393
2093
  overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
@@ -2397,7 +2097,7 @@ function touchGamepadRender()
2397
2097
 
2398
2098
  // set canvas back to normal
2399
2099
  overlayContext.restore();
2400
- }
2100
+ }
2401
2101
  /**
2402
2102
  * LittleJS Audio System
2403
2103
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - Sound Effect Generator
@@ -2838,7 +2538,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2838
2538
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
2839
2539
 
2840
2540
  // check if there are more channels
2841
- hasMore |= !!patterns[patternIndex][channelIndex];
2541
+ hasMore ||= !!patterns[patternIndex][channelIndex];
2842
2542
 
2843
2543
  // get next offset, use the length of first channel
2844
2544
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - !notFirstBeat) * beatLength;
@@ -2851,7 +2551,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2851
2551
 
2852
2552
  // stop if end, different instrument or new note
2853
2553
  stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
2854
- instrument != (patternChannel[0] || 0) | note | 0;
2554
+ instrument != (patternChannel[0] || 0) || note;
2855
2555
 
2856
2556
  // fill buffer with samples for previous beat, most cpu intensive part
2857
2557
  for (j = 0; j < beatLength && notFirstBeat;
@@ -2895,7 +2595,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2895
2595
  }
2896
2596
 
2897
2597
  return [leftChannelBuffer, rightChannelBuffer];
2898
- }
2598
+ }
2899
2599
  /**
2900
2600
  * LittleJS Tile Layer System
2901
2601
  * - Caches arrays of tiles to off screen canvas for fast rendering
@@ -2965,1303 +2665,1638 @@ function tileCollisionTest(pos, size=vec2(), object)
2965
2665
  for (let x = minX; x < maxX; ++x)
2966
2666
  {
2967
2667
  const tileData = tileCollision[y*tileCollisionSize.x+x];
2968
- if (tileData && (!object || object.collideWithTile(tileData, new Vector2(x, y))))
2668
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
2969
2669
  return 1;
2970
2670
  }
2971
- }
2972
-
2973
- /** Return the center of tile if any that is hit (this does not return the exact hit point)
2974
- * @param {Vector2} posStart
2975
- * @param {Vector2} posEnd
2976
- * @param {EngineObject} [object]
2977
- * @return {Vector2}
2978
- * @memberof TileCollision */
2979
- function tileCollisionRaycast(posStart, posEnd, object)
2980
- {
2981
- // test if a ray collides with tiles from start to end
2982
- // todo: a way to get the exact hit point, it must still register as inside the hit tile
2983
- const posDelta = (posEnd = posEnd.floor()).subtract(posStart = posStart.floor());
2984
- const dx = abs(posDelta.x), dy = -abs(posDelta.y);
2985
- const sx = sign(posDelta.x), sy = sign(posDelta.y);
2671
+ }
2672
+
2673
+ /** Return the center of tile if any that is hit (does not return the exact intersection)
2674
+ * @param {Vector2} posStart
2675
+ * @param {Vector2} posEnd
2676
+ * @param {EngineObject} [object]
2677
+ * @return {Vector2}
2678
+ * @memberof TileCollision */
2679
+ function tileCollisionRaycast(posStart, posEnd, object)
2680
+ {
2681
+ // test if a ray collides with tiles from start to end
2682
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
2683
+ const delta = posEnd.subtract(posStart);
2684
+ const totalLength = delta.length();
2685
+ const normalizedDelta = delta.normalize();
2686
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
2687
+ const flooredPosStart = posStart.floor();
2688
+
2689
+ // setup iteration variables
2690
+ let pos = flooredPosStart;
2691
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
2692
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
2693
+
2694
+ while (1)
2695
+ {
2696
+ // check for tile collision
2697
+ const tileData = getTileCollisionData(pos);
2698
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
2699
+ {
2700
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
2701
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
2702
+ return pos.add(vec2(.5));
2703
+ }
2704
+
2705
+ // check if past the end
2706
+ if (xi > totalLength && yi > totalLength)
2707
+ break;
2708
+
2709
+ // get coordinates of the next tile to check
2710
+ if (xi > yi)
2711
+ pos.y += sign(delta.y), yi += unit.y;
2712
+ else
2713
+ pos.x += sign(delta.x), xi += unit.x;
2714
+ }
2715
+
2716
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
2717
+ }
2718
+
2719
+ ///////////////////////////////////////////////////////////////////////////////
2720
+ // Tile Layer Rendering System
2721
+
2722
+ /**
2723
+ * Tile layer data object stores info about how to render a tile
2724
+ * @example
2725
+ * // create tile layer data with tile index 0 and random orientation and color
2726
+ * const tileIndex = 0;
2727
+ * const direction = randInt(4)
2728
+ * const mirror = randInt(2);
2729
+ * const color = randColor();
2730
+ * const data = new TileLayerData(tileIndex, direction, mirror, color);
2731
+ */
2732
+ class TileLayerData
2733
+ {
2734
+ /** Create a tile layer data object, one for each tile in a TileLayer
2735
+ * @param {Number} [tile] - The tile to use, untextured if undefined
2736
+ * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
2737
+ * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
2738
+ * @param {Color} [color=Color()] - Color of the tile */
2739
+ constructor(tile, direction=0, mirror=0, color=new Color())
2740
+ {
2741
+ /** @property {Number} - The tile to use, untextured if undefined */
2742
+ this.tile = tile;
2743
+ /** @property {Number} - Integer direction of tile, in 90 degree increments */
2744
+ this.direction = direction;
2745
+ /** @property {Boolean} - If the tile should be mirrored along the x axis */
2746
+ this.mirror = mirror;
2747
+ /** @property {Color} - Color of the tile */
2748
+ this.color = color;
2749
+ }
2750
+
2751
+ /** Set this tile to clear, it will not be rendered */
2752
+ clear() { this.tile = this.direction = this.mirror = 0; color = new Color; }
2753
+ }
2754
+
2755
+ /**
2756
+ * Tile layer object - cached rendering system for tile layers
2757
+ * - Each Tile layer is rendered to an off screen canvas
2758
+ * - To allow dynamic modifications, layers are rendered using canvas 2d
2759
+ * - Some devices like mobile phones are limited to 4k texture resolution
2760
+ * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2761
+ * @extends EngineObject
2762
+ * @example
2763
+ * // create tile collision and visible tile layer
2764
+ * initTileCollision(vec2(200,100));
2765
+ * const tileLayer = new TileLayer();
2766
+ */
2767
+ class TileLayer extends EngineObject
2768
+ {
2769
+ /** Create a tile layer object
2770
+ * @param {Vector2} [position=Vector2()] - World space position
2771
+ * @param {Vector2} [size=tileCollisionSize] - World space size
2772
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2773
+ * @param {Vector2} [scale=Vector2(1,1)] - How much to scale this layer when rendered
2774
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2775
+ */
2776
+ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2777
+ {
2778
+ super(pos, size, -1, tileSize, 0, undefined, renderOrder);
2779
+
2780
+ /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
2781
+ this.canvas = document.createElement('canvas');
2782
+ /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
2783
+ this.context = this.canvas.getContext('2d');
2784
+ /** @property {Vector2} - How much to scale this layer when rendered */
2785
+ this.scale = scale;
2786
+ /** @property {Boolean} [isOverlay=0] - If true this layer will render to overlay canvas and appear above all objects */
2787
+ this.isOverlay;
2788
+
2789
+ // init tile data
2790
+ this.data = [];
2791
+ for (let j = this.size.area(); j--;)
2792
+ this.data.push(new TileLayerData);
2793
+ }
2794
+
2795
+ /** Set data at a given position in the array
2796
+ * @param {Vector2} position - Local position in array
2797
+ * @param {TileLayerData} data - Data to set
2798
+ * @param {Boolean} [redraw=0] - Force the tile to redraw if true */
2799
+ setData(layerPos, data, redraw)
2800
+ {
2801
+ if (layerPos.arrayCheck(this.size))
2802
+ {
2803
+ this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
2804
+ redraw && this.drawTileData(layerPos);
2805
+ }
2806
+ }
2807
+
2808
+ /** Get data at a given position in the array
2809
+ * @param {Vector2} layerPos - Local position in array
2810
+ * @return {TileLayerData} */
2811
+ getData(layerPos)
2812
+ { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
2813
+
2814
+ // Tile layers are not updated
2815
+ update() {}
2816
+
2817
+ // Render the tile layer, called automatically by the engine
2818
+ render()
2819
+ {
2820
+ ASSERT(mainContext != this.context); // must call redrawEnd() after drawing tiles
2821
+
2822
+ // flush and copy gl canvas because tile canvas does not use webgl
2823
+ glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
2824
+
2825
+ // draw the entire cached level onto the canvas
2826
+ const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
2827
+ (this.isOverlay ? overlayContext : mainContext).drawImage
2828
+ (
2829
+ this.canvas, pos.x, pos.y,
2830
+ cameraScale*this.size.x*this.scale.x, cameraScale*this.size.y*this.scale.y
2831
+ );
2832
+ }
2833
+
2834
+ /** Draw all the tile data to an offscreen canvas
2835
+ * - This may be slow in some browsers
2836
+ */
2837
+ redraw()
2838
+ {
2839
+ this.redrawStart(1);
2840
+ this.drawAllTileData();
2841
+ this.redrawEnd();
2842
+ }
2843
+
2844
+ /** Call to start the redraw process
2845
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2846
+ redrawStart(clear = 0)
2847
+ {
2848
+ // save current render settings
2849
+ this.savedRenderSettings = [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale];
2850
+
2851
+ // hack: use normal rendering system to render the tiles
2852
+ mainCanvas = this.canvas;
2853
+ mainContext = this.context;
2854
+ cameraPos = this.size.scale(.5);
2855
+ cameraScale = this.tileSize.x;
2856
+
2857
+ if (clear)
2858
+ {
2859
+ // clear and set size
2860
+ mainCanvas.width = this.size.x * this.tileSize.x;
2861
+ mainCanvas.height = this.size.y * this.tileSize.y;
2862
+ }
2863
+
2864
+ // begin a new render for the tile canvas
2865
+ enginePreRender();
2866
+ }
2867
+
2868
+ /** Call to end the redraw process */
2869
+ redrawEnd()
2870
+ {
2871
+ ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
2872
+ glEnable && glCopyToContext(mainContext, 1);
2873
+ //debugSaveCanvas(this.canvas);
2874
+
2875
+ // set stuff back to normal
2876
+ [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
2877
+ }
2878
+
2879
+ /** Draw the tile at a given position
2880
+ * @param {Vector2} layerPos */
2881
+ drawTileData(layerPos)
2882
+ {
2883
+ // first clear out where the tile was
2884
+ const pos = layerPos.floor().add(this.pos).add(vec2(.5));
2885
+ this.drawCanvas2D(pos, vec2(1), 0, 0, (context)=>context.clearRect(-.5, -.5, 1, 1));
2886
+
2887
+ // draw the tile if not undefined
2888
+ const d = this.getData(layerPos);
2889
+ if (d.tile != undefined)
2890
+ {
2891
+ ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
2892
+ drawTile(pos, vec2(1), d.tile, this.tileSize, d.color, d.direction*PI/2, d.mirror);
2893
+ }
2894
+ }
2895
+
2896
+ /** Draw all the tiles in this layer */
2897
+ drawAllTileData()
2898
+ {
2899
+ for (let x = this.size.x; x--;)
2900
+ for (let y = this.size.y; y--;)
2901
+ this.drawTileData(vec2(x,y));
2902
+ }
2903
+
2904
+ /** Draw directly to the 2D canvas in world space (bipass webgl)
2905
+ * @param {Vector2} pos
2906
+ * @param {Vector2} size
2907
+ * @param {Number} [angle=0]
2908
+ * @param {Boolean} [mirror=0]
2909
+ * @param {Function} drawFunction */
2910
+ drawCanvas2D(pos, size, angle=0, mirror, drawFunction)
2911
+ {
2912
+ const context = this.context;
2913
+ context.save();
2914
+ pos = pos.subtract(this.pos).multiply(this.tileSize);
2915
+ size = size.multiply(this.tileSize);
2916
+ context.translate(pos.x, this.canvas.height - pos.y);
2917
+ context.rotate(angle);
2918
+ context.scale(mirror ? -size.x : size.x, size.y);
2919
+ drawFunction(context);
2920
+ context.restore();
2921
+ }
2986
2922
 
2987
- for (let x = posStart.x, y = posStart.y, e = dx + dy;;)
2923
+ /** Draw a tile directly onto the layer canvas
2924
+ * @param {Vector2} pos
2925
+ * @param {Vector2} [size=Vector2(1,1)]
2926
+ * @param {Number} [tileIndex=-1]
2927
+ * @param {Vector2} [tileSize=tileSizeDefault]
2928
+ * @param {Color} [color=Color()]
2929
+ * @param {Number} [angle=0]
2930
+ * @param {Boolean} [mirror=0] */
2931
+ drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle, mirror)
2988
2932
  {
2989
- const tileData = getTileCollisionData(vec2(x,y));
2990
- if (tileData && (object ? object.collideWithTileRaycast(tileData, new Vector2(x, y)) : tileData > 0))
2933
+ this.drawCanvas2D(pos, size, angle, mirror, (context)=>
2991
2934
  {
2992
- debugRaycast && debugLine(posStart, posEnd, '#f00',.02, 1);
2993
- debugRaycast && debugPoint(new Vector2(x+.5, y+.5), '#ff0', 1);
2994
- return new Vector2(x+.5, y+.5);
2995
- }
2996
-
2997
- // update Bresenham line drawing algorithm
2998
- if (x == posEnd.x & y == posEnd.y) break;
2999
- const e2 = 2*e;
3000
- if (e2 >= dy) e += dy, x += sx;
3001
- if (e2 <= dx) e += dx, y += sy;
2935
+ if (tileIndex < 0)
2936
+ {
2937
+ // untextured
2938
+ context.fillStyle = color;
2939
+ context.fillRect(-.5, -.5, 1, 1);
2940
+ }
2941
+ else
2942
+ {
2943
+ const cols = tileImage.width/tileSize.x;
2944
+ context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
2945
+ context.drawImage(tileImage,
2946
+ (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.y,
2947
+ tileSize.x, tileSize.y, -.5, -.5, 1, 1);
2948
+ }
2949
+ });
3002
2950
  }
3003
- debugRaycast && debugLine(posStart, posEnd, '#00f',.02, 1);
3004
- }
3005
2951
 
3006
- ///////////////////////////////////////////////////////////////////////////////
3007
- // Tile Layer Rendering System
3008
-
3009
- /**
3010
- * Tile layer data object stores info about how to render a tile
3011
- * @example
3012
- * // create tile layer data with tile index 0 and random orientation and color
3013
- * const tileIndex = 0;
3014
- * const direction = randInt(4)
3015
- * const mirror = randInt(2);
3016
- * const color = randColor();
3017
- * const data = new TileLayerData(tileIndex, direction, mirror, color);
2952
+ /** Draw a rectangle directly onto the layer canvas
2953
+ * @param {Vector2} pos
2954
+ * @param {Vector2} [size=Vector2(1,1)]
2955
+ * @param {Color} [color=Color()]
2956
+ * @param {Number} [angle=0] */
2957
+ drawRect(pos, size, color, angle)
2958
+ { this.drawTile(pos, size, -1, 0, color, angle); }
2959
+ }
2960
+ /**
2961
+ * LittleJS Particle System
3018
2962
  */
3019
- class TileLayerData
3020
- {
3021
- /** Create a tile layer data object, one for each tile in a TileLayer
3022
- * @param {Number} [tile] - The tile to use, untextured if undefined
3023
- * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
3024
- * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
3025
- * @param {Color} [color=Color()] - Color of the tile */
3026
- constructor(tile, direction=0, mirror=0, color=new Color())
3027
- {
3028
- /** @property {Number} - The tile to use, untextured if undefined */
3029
- this.tile = tile;
3030
- /** @property {Number} - Integer direction of tile, in 90 degree increments */
3031
- this.direction = direction;
3032
- /** @property {Boolean} - If the tile should be mirrored along the x axis */
3033
- this.mirror = mirror;
3034
- /** @property {Color} - Color of the tile */
3035
- this.color = color;
3036
- }
3037
2963
 
3038
- /** Set this tile to clear, it will not be rendered */
3039
- clear() { this.tile = this.direction = this.mirror = 0; color = new Color; }
3040
- }
2964
+ 'use strict';
3041
2965
 
3042
2966
  /**
3043
- * Tile layer object - cached rendering system for tile layers
3044
- * - Each Tile layer is rendered to an off screen canvas
3045
- * - To allow dynamic modifications, layers are rendered using canvas 2d
3046
- * - Some devices like mobile phones are limited to 4k texture resolution
3047
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2967
+ * Particle Emitter - Spawns particles with the given settings
3048
2968
  * @extends EngineObject
3049
2969
  * @example
3050
- * // create tile collision and visible tile layer
3051
- * initTileCollision(vec2(200,100));
3052
- * const tileLayer = new TileLayer();
2970
+ * // create a particle emitter
2971
+ * let pos = vec2(2,3);
2972
+ * let particleEmiter = new ParticleEmitter
2973
+ * (
2974
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emiteCone
2975
+ * 0, vec2(16), // tileIndex, tileSize
2976
+ * new Color(1,1,1), new Color(0,0,0), // colorStartA, colorStartB
2977
+ * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
2978
+ * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
2979
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
2980
+ * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
2981
+ * );
3053
2982
  */
3054
- class TileLayer extends EngineObject
2983
+ class ParticleEmitter extends EngineObject
3055
2984
  {
3056
- /** Create a tile layer object
3057
- * @param {Vector2} [position=Vector2()] - World space position
3058
- * @param {Vector2} [size=tileCollisionSize] - World space size
3059
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
3060
- * @param {Vector2} [scale=Vector2(1,1)] - How much to scale this layer when rendered
3061
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
3062
- */
3063
- constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2985
+ /** Create a particle system with the given settings
2986
+ * @param {Vector2} position - World space position of the emitter
2987
+ * @param {Number} [angle=0] - Angle to emit the particles
2988
+ * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
2989
+ * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
2990
+ * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
2991
+ * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
2992
+ * @param {Number} [tileIndex=-1] - Index into tile sheet, if <0 no texture is applied
2993
+ * @param {Vector2} [tileSize=tileSizeDefault] - Tile size for particles
2994
+ * @param {Color} [colorStartA=Color()] - Color at start of life 1, randomized between start colors
2995
+ * @param {Color} [colorStartB=Color()] - Color at start of life 2, randomized between start colors
2996
+ * @param {Color} [colorEndA=Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
2997
+ * @param {Color} [colorEndB=Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
2998
+ * @param {Number} [particleTime=.5] - How long particles live
2999
+ * @param {Number} [sizeStart=.1] - How big are particles at start
3000
+ * @param {Number} [sizeEnd=1] - How big are particles at end
3001
+ * @param {Number} [speed=.1] - How fast are particles when spawned
3002
+ * @param {Number} [angleSpeed=.05] - How fast are particles rotating
3003
+ * @param {Number} [damping=1] - How much to dampen particle speed
3004
+ * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
3005
+ * @param {Number} [gravityScale=0] - How much does gravity effect particles
3006
+ * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
3007
+ * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
3008
+ * @param {Number} [randomness=.2] - Apply extra randomness percent
3009
+ * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3010
+ * @param {Boolean} [additive=0] - Should particles use addtive blend
3011
+ * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3012
+ * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3013
+ * @param {Boolean} [localSpace=0] - Should it be in local space of emitter (world space is default)
3014
+ */
3015
+ constructor
3016
+ (
3017
+ pos,
3018
+ angle,
3019
+ emitSize = 0,
3020
+ emitTime = 0,
3021
+ emitRate = 100,
3022
+ emitConeAngle = PI,
3023
+ tileIndex = -1,
3024
+ tileSize = tileSizeDefault,
3025
+ colorStartA = new Color,
3026
+ colorStartB = new Color,
3027
+ colorEndA = new Color(1,1,1,0),
3028
+ colorEndB = new Color(1,1,1,0),
3029
+ particleTime = .5,
3030
+ sizeStart = .1,
3031
+ sizeEnd = 1,
3032
+ speed = .1,
3033
+ angleSpeed = .05,
3034
+ damping = 1,
3035
+ angleDamping = 1,
3036
+ gravityScale = 0,
3037
+ particleConeAngle = PI,
3038
+ fadeRate = .1,
3039
+ randomness = .2,
3040
+ collideTiles,
3041
+ additive,
3042
+ randomColorLinear = 1,
3043
+ renderOrder = additive ? 1e9 : 0,
3044
+ localSpace
3045
+ )
3064
3046
  {
3065
- super(pos, size, -1, tileSize, 0, undefined, renderOrder);
3047
+ super(pos, vec2(), tileIndex, tileSize, angle, undefined, renderOrder);
3066
3048
 
3067
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3068
- this.canvas = document.createElement('canvas');
3069
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3070
- this.context = this.canvas.getContext('2d');
3071
- /** @property {Vector2} - How much to scale this layer when rendered */
3072
- this.scale = scale;
3073
- /** @property {Boolean} [isOverlay=0] - If true this layer will render to overlay canvas and appear above all objects */
3074
- this.isOverlay;
3049
+ // emitter settings
3050
+ /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
3051
+ this.emitSize = emitSize
3052
+ /** @property {Number} - How long to stay alive (0 is forever) */
3053
+ this.emitTime = emitTime;
3054
+ /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
3055
+ this.emitRate = emitRate;
3056
+ /** @property {Number} - Local angle to apply velocity to particles from emitter */
3057
+ this.emitConeAngle = emitConeAngle;
3075
3058
 
3076
- // init tile data
3077
- this.data = [];
3078
- for (let j = this.size.area(); j--;)
3079
- this.data.push(new TileLayerData);
3080
- }
3081
-
3082
- /** Set data at a given position in the array
3083
- * @param {Vector2} position - Local position in array
3084
- * @param {TileLayerData} data - Data to set
3085
- * @param {Boolean} [redraw=0] - Force the tile to redraw if true */
3086
- setData(layerPos, data, redraw)
3087
- {
3088
- if (layerPos.arrayCheck(this.size))
3089
- {
3090
- this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
3091
- redraw && this.drawTileData(layerPos);
3092
- }
3059
+ // color settings
3060
+ /** @property {Color} - Color at start of life 1, randomized between start colors */
3061
+ this.colorStartA = colorStartA;
3062
+ /** @property {Color} - Color at start of life 2, randomized between start colors */
3063
+ this.colorStartB = colorStartB;
3064
+ /** @property {Color} - Color at end of life 1, randomized between end colors */
3065
+ this.colorEndA = colorEndA;
3066
+ /** @property {Color} - Color at end of life 2, randomized between end colors */
3067
+ this.colorEndB = colorEndB;
3068
+ /** @property {Boolean} - Should color be randomized linearly or across each component */
3069
+ this.randomColorLinear = randomColorLinear;
3070
+
3071
+ // particle settings
3072
+ /** @property {Number} - How long particles live */
3073
+ this.particleTime = particleTime;
3074
+ /** @property {Number} - How big are particles at start */
3075
+ this.sizeStart = sizeStart;
3076
+ /** @property {Number} - How big are particles at end */
3077
+ this.sizeEnd = sizeEnd;
3078
+ /** @property {Number} - How fast are particles when spawned */
3079
+ this.speed = speed;
3080
+ /** @property {Number} - How fast are particles rotating */
3081
+ this.angleSpeed = angleSpeed;
3082
+ /** @property {Number} - How much to dampen particle speed */
3083
+ this.damping = damping;
3084
+ /** @property {Number} - How much to dampen particle angular speed */
3085
+ this.angleDamping = angleDamping;
3086
+ /** @property {Number} - How much does gravity effect particles */
3087
+ this.gravityScale = gravityScale;
3088
+ /** @property {Number} - Cone for start particle angle */
3089
+ this.particleConeAngle = particleConeAngle;
3090
+ /** @property {Number} - How quick to fade in particles at start/end in percent of life */
3091
+ this.fadeRate = fadeRate;
3092
+ /** @property {Number} - Apply extra randomness percent */
3093
+ this.randomness = randomness;
3094
+ /** @property {Number} - Do particles collide against tiles */
3095
+ this.collideTiles = collideTiles;
3096
+ /** @property {Number} - Should particles use addtive blend */
3097
+ this.additive = additive;
3098
+ /** @property {Boolean} - Should it be in local space of emitter */
3099
+ this.localSpace = localSpace;
3100
+ /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3101
+ this.trailScale = 0;
3102
+
3103
+ // internal variables
3104
+ this.emitTimeBuffer = 0;
3093
3105
  }
3094
3106
 
3095
- /** Get data at a given position in the array
3096
- * @param {Vector2} layerPos - Local position in array
3097
- * @return {TileLayerData} */
3098
- getData(layerPos)
3099
- { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
3100
-
3101
- // Tile layers are not updated
3102
- update() {}
3103
-
3104
- // Render the tile layer, called automatically by the engine
3105
- render()
3107
+ /** Update the emitter to spawn particles, called automatically by engine once each frame */
3108
+ update()
3106
3109
  {
3107
- ASSERT(mainContext != this.context); // must call redrawEnd() after drawing tiles
3110
+ // only do default update to apply parent transforms
3111
+ this.parent && super.update();
3108
3112
 
3109
- // flush and copy gl canvas because tile canvas does not use webgl
3110
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3111
-
3112
- // draw the entire cached level onto the canvas
3113
- const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
3114
- (this.isOverlay ? overlayContext : mainContext).drawImage
3115
- (
3116
- this.canvas, pos.x, pos.y,
3117
- cameraScale*this.size.x*this.scale.x, cameraScale*this.size.y*this.scale.y
3118
- );
3119
- }
3113
+ // update emitter
3114
+ if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3115
+ {
3116
+ // emit particles
3117
+ if (this.emitRate * particleEmitRateScale)
3118
+ {
3119
+ const rate = 1/this.emitRate/particleEmitRateScale;
3120
+ for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
3121
+ this.emitParticle();
3122
+ }
3123
+ }
3124
+ else
3125
+ this.destroy();
3120
3126
 
3121
- /** Draw all the tile data to an offscreen canvas
3122
- * - This may be slow in some browsers
3123
- */
3124
- redraw()
3125
- {
3126
- this.redrawStart(1);
3127
- this.drawAllTileData();
3128
- this.redrawEnd();
3127
+ debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
3129
3128
  }
3130
3129
 
3131
- /** Call to start the redraw process
3132
- * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
3133
- redrawStart(clear = 0)
3130
+ /** Spawn one particle
3131
+ * @return {Particle} */
3132
+ emitParticle()
3134
3133
  {
3135
- // save current render settings
3136
- this.savedRenderSettings = [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale];
3137
-
3138
- // hack: use normal rendering system to render the tiles
3139
- mainCanvas = this.canvas;
3140
- mainContext = this.context;
3141
- cameraPos = this.size.scale(.5);
3142
- cameraScale = this.tileSize.x;
3143
-
3144
- if (clear)
3134
+ // spawn a particle
3135
+ let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3136
+ vec2(rand(-.5,.5), rand(-.5,.5))
3137
+ .multiply(this.emitSize).rotate(this.angle) // box emitter
3138
+ : randInCircle(this.emitSize/2); // circle emitter
3139
+ let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3140
+ if (!this.localSpace)
3145
3141
  {
3146
- // clear and set size
3147
- mainCanvas.width = this.size.x * this.tileSize.x;
3148
- mainCanvas.height = this.size.y * this.tileSize.y;
3142
+ pos = this.pos.add(pos);
3143
+ angle += this.angle;
3149
3144
  }
3145
+
3146
+ const particle = new Particle(pos, this.tileIndex, this.tileSize, angle);
3150
3147
 
3151
- // begin a new render for the tile canvas
3152
- enginePreRender();
3153
- }
3148
+ // randomness scales each paremeter by a percentage
3149
+ const randomness = this.randomness;
3150
+ const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
3154
3151
 
3155
- /** Call to end the redraw process */
3156
- redrawEnd()
3157
- {
3158
- ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
3159
- glEnable && glCopyToContext(mainContext, 1);
3160
- //debugSaveCanvas(this.canvas);
3152
+ // randomize particle settings
3153
+ const particleTime = randomizeScale(this.particleTime);
3154
+ const sizeStart = randomizeScale(this.sizeStart);
3155
+ const sizeEnd = randomizeScale(this.sizeEnd);
3156
+ const speed = randomizeScale(this.speed);
3157
+ const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3158
+ const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3159
+ const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3160
+ const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3161
+ const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
3161
3162
 
3162
- // set stuff back to normal
3163
- [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
3164
- }
3163
+ // build particle settings
3164
+ particle.colorStart = colorStart;
3165
+ particle.colorEndDelta = colorEnd.subtract(colorStart);
3166
+ particle.velocity = vec2().setAngle(velocityAngle, speed);
3167
+ particle.angleVelocity = angleSpeed;
3168
+ particle.lifeTime = particleTime;
3169
+ particle.sizeStart = sizeStart;
3170
+ particle.sizeEndDelta = sizeEnd - sizeStart;
3171
+ particle.fadeRate = this.fadeRate;
3172
+ particle.damping = this.damping;
3173
+ particle.angleDamping = this.angleDamping;
3174
+ particle.elasticity = this.elasticity;
3175
+ particle.friction = this.friction;
3176
+ particle.gravityScale = this.gravityScale;
3177
+ particle.collideTiles = this.collideTiles;
3178
+ particle.additive = this.additive;
3179
+ particle.renderOrder = this.renderOrder;
3180
+ particle.trailScale = this.trailScale;
3181
+ particle.mirror = randInt(2);
3182
+ particle.localSpaceEmitter = this.localSpace && this;
3165
3183
 
3166
- /** Draw the tile at a given position
3167
- * @param {Vector2} layerPos */
3168
- drawTileData(layerPos)
3169
- {
3170
- // first clear out where the tile was
3171
- const pos = layerPos.floor().add(this.pos).add(vec2(.5));
3172
- this.drawCanvas2D(pos, vec2(1), 0, 0, (context)=>context.clearRect(-.5, -.5, 1, 1));
3184
+ // setup callbacks for particles
3185
+ particle.destroyCallback = this.particleDestroyCallback;
3186
+ this.particleCreateCallback && this.particleCreateCallback(particle);
3173
3187
 
3174
- // draw the tile if not undefined
3175
- const d = this.getData(layerPos);
3176
- if (d.tile != undefined)
3177
- {
3178
- ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
3179
- drawTile(pos, vec2(1), d.tile, this.tileSize, d.color, d.direction*PI/2, d.mirror);
3180
- }
3188
+ // return the newly created particle
3189
+ return particle;
3181
3190
  }
3182
3191
 
3183
- /** Draw all the tiles in this layer */
3184
- drawAllTileData()
3185
- {
3186
- for (let x = this.size.x; x--;)
3187
- for (let y = this.size.y; y--;)
3188
- this.drawTileData(vec2(x,y));
3189
- }
3192
+ // Particle emitters are not rendered, only the particles are
3193
+ render() {}
3194
+ }
3190
3195
 
3191
- /** Draw directly to the 2D canvas in world space (bipass webgl)
3192
- * @param {Vector2} pos
3193
- * @param {Vector2} size
3194
- * @param {Number} [angle=0]
3195
- * @param {Boolean} [mirror=0]
3196
- * @param {Function} drawFunction */
3197
- drawCanvas2D(pos, size, angle=0, mirror, drawFunction)
3198
- {
3199
- const context = this.context;
3200
- context.save();
3201
- pos = pos.subtract(this.pos).multiply(this.tileSize);
3202
- size = size.multiply(this.tileSize);
3203
- context.translate(pos.x, this.canvas.height - pos.y);
3204
- context.rotate(angle);
3205
- context.scale(mirror ? -size.x : size.x, size.y);
3206
- drawFunction(context);
3207
- context.restore();
3208
- }
3196
+ ///////////////////////////////////////////////////////////////////////////////
3197
+ /**
3198
+ * Particle Object - Created automatically by Particle Emitters
3199
+ * @extends EngineObject
3200
+ */
3201
+ class Particle extends EngineObject
3202
+ {
3203
+ /**
3204
+ * Create a particle with the given settings
3205
+ * @param {Vector2} position - World space position of the particle
3206
+ * @param {Number} [tileIndex=-1] - Tile to use to render, untextured if -1
3207
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
3208
+ * @param {Number} [angle=0] - Angle to rotate the particle
3209
+ */
3210
+ constructor(pos, tileIndex, tileSize, angle)
3211
+ { super(pos, vec2(), tileIndex, tileSize, angle); }
3209
3212
 
3210
- /** Draw a tile directly onto the layer canvas
3211
- * @param {Vector2} pos
3212
- * @param {Vector2} [size=Vector2(1,1)]
3213
- * @param {Number} [tileIndex=-1]
3214
- * @param {Vector2} [tileSize=tileSizeDefault]
3215
- * @param {Color} [color=Color()]
3216
- * @param {Number} [angle=0]
3217
- * @param {Boolean} [mirror=0] */
3218
- drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle, mirror)
3213
+ /** Render the particle, automatically called each frame, sorted by renderOrder */
3214
+ render()
3219
3215
  {
3220
- this.drawCanvas2D(pos, size, angle, mirror, (context)=>
3216
+ // modulate size and color
3217
+ const p = min((time - this.spawnTime) / this.lifeTime, 1);
3218
+ const radius = this.sizeStart + p * this.sizeEndDelta;
3219
+ const size = vec2(radius);
3220
+ const fadeRate = this.fadeRate/2;
3221
+ const color = new Color(
3222
+ this.colorStart.r + p * this.colorEndDelta.r,
3223
+ this.colorStart.g + p * this.colorEndDelta.g,
3224
+ this.colorStart.b + p * this.colorEndDelta.b,
3225
+ (this.colorStart.a + p * this.colorEndDelta.a) *
3226
+ (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
3227
+
3228
+ // draw the particle
3229
+ this.additive && setBlendMode(1);
3230
+
3231
+ let pos = this.pos, angle = this.angle;
3232
+ if (this.localSpaceEmitter)
3221
3233
  {
3222
- if (tileIndex < 0)
3223
- {
3224
- // untextured
3225
- context.fillStyle = color;
3226
- context.fillRect(-.5, -.5, 1, 1);
3227
- }
3228
- else
3229
- {
3230
- const cols = tileImage.width/tileSize.x;
3231
- context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
3232
- context.drawImage(tileImage,
3233
- (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.y,
3234
- tileSize.x, tileSize.y, -.5, -.5, 1, 1);
3235
- }
3236
- });
3237
- }
3234
+ // in local space of emitter
3235
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
3236
+ angle += this.localSpaceEmitter.angle;
3237
+ }
3238
+ if (this.trailScale)
3239
+ {
3240
+ // trail style particles
3241
+ let velocity = this.velocity;
3242
+ if (this.localSpaceEmitter)
3243
+ velocity = velocity.rotate(-this.localSpaceEmitter.angle);
3244
+ const speed = velocity.length();
3245
+ const direction = velocity.scale(1/speed);
3246
+ const trailLength = speed * this.trailScale;
3247
+ size.y = max(size.x, trailLength);
3248
+ angle = direction.angle();
3249
+ drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3250
+ }
3251
+ else
3252
+ drawTile(pos, size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3253
+ this.additive && setBlendMode();
3254
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
3238
3255
 
3239
- /** Draw a rectangle directly onto the layer canvas
3240
- * @param {Vector2} pos
3241
- * @param {Vector2} [size=Vector2(1,1)]
3242
- * @param {Color} [color=Color()]
3243
- * @param {Number} [angle=0] */
3244
- drawRect(pos, size, color, angle)
3245
- { this.drawTile(pos, size, -1, 0, color, angle); }
3246
- }
3256
+ if (p == 1)
3257
+ {
3258
+ // destroy particle when it's time runs out
3259
+ this.color = color;
3260
+ this.size = size;
3261
+ this.destroyCallback && this.destroyCallback(this);
3262
+ this.destroyed = 1;
3263
+ }
3264
+ }
3265
+ }
3247
3266
  /**
3248
- * LittleJS Particle System
3267
+ * LittleJS Medal System
3268
+ * - Tracks and displays medals
3269
+ * - Saves medals to local storage
3270
+ * - Newgrounds integration
3271
+ * @namespace Medals
3249
3272
  */
3250
3273
 
3251
3274
  'use strict';
3252
3275
 
3253
- /**
3254
- * Particle Emitter - Spawns particles with the given settings
3255
- * @extends EngineObject
3276
+ /** List of all medals
3277
+ * @type {Array}
3278
+ * @memberof Medals */
3279
+ const medals = [];
3280
+
3281
+ // Engine internal variables not exposed to documentation
3282
+ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3283
+
3284
+ ///////////////////////////////////////////////////////////////////////////////
3285
+
3286
+ /** Initialize medals with a save name used for storage
3287
+ * - Call this after creating all medals
3288
+ * - Checks if medals are unlocked
3289
+ * @param {String} saveName
3290
+ * @memberof Medals */
3291
+ function medalsInit(saveName)
3292
+ {
3293
+ // check if medals are unlocked
3294
+ medalsSaveName = saveName;
3295
+ debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
3296
+ }
3297
+
3298
+ /**
3299
+ * Medal Object - Tracks an unlockable medal
3256
3300
  * @example
3257
- * // create a particle emitter
3258
- * let pos = vec2(2,3);
3259
- * let particleEmiter = new ParticleEmitter
3260
- * (
3261
- * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emiteCone
3262
- * 0, vec2(16), // tileIndex, tileSize
3263
- * new Color(1,1,1), new Color(0,0,0), // colorStartA, colorStartB
3264
- * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
3265
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
3266
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
3267
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
3268
- * );
3301
+ * // create a medal
3302
+ * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
3303
+ *
3304
+ * // initialize medals
3305
+ * medalsInit('Example Game');
3306
+ *
3307
+ * // unlock the medal
3308
+ * medal_example.unlock();
3269
3309
  */
3270
- class ParticleEmitter extends EngineObject
3310
+ class Medal
3271
3311
  {
3272
- /** Create a particle system with the given settings
3273
- * @param {Vector2} position - World space position of the emitter
3274
- * @param {Number} [angle=0] - Angle to emit the particles
3275
- * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3276
- * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3277
- * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
3278
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3279
- * @param {Number} [tileIndex=-1] - Index into tile sheet, if <0 no texture is applied
3280
- * @param {Vector2} [tileSize=tileSizeDefault] - Tile size for particles
3281
- * @param {Color} [colorStartA=Color()] - Color at start of life 1, randomized between start colors
3282
- * @param {Color} [colorStartB=Color()] - Color at start of life 2, randomized between start colors
3283
- * @param {Color} [colorEndA=Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
3284
- * @param {Color} [colorEndB=Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
3285
- * @param {Number} [particleTime=.5] - How long particles live
3286
- * @param {Number} [sizeStart=.1] - How big are particles at start
3287
- * @param {Number} [sizeEnd=1] - How big are particles at end
3288
- * @param {Number} [speed=.1] - How fast are particles when spawned
3289
- * @param {Number} [angleSpeed=.05] - How fast are particles rotating
3290
- * @param {Number} [damping=1] - How much to dampen particle speed
3291
- * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
3292
- * @param {Number} [gravityScale=0] - How much does gravity effect particles
3293
- * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
3294
- * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
3295
- * @param {Number} [randomness=.2] - Apply extra randomness percent
3296
- * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3297
- * @param {Boolean} [additive=0] - Should particles use addtive blend
3298
- * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3299
- * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3300
- * @param {Boolean} [localSpace=0] - Should it be in local space of emitter (world space is default)
3312
+ /** Create an medal object and adds it to the list of medals
3313
+ * @param {Number} id - The unique identifier of the medal
3314
+ * @param {String} name - Name of the medal
3315
+ * @param {String} [description] - Description of the medal
3316
+ * @param {String} [icon='🏆'] - Icon for the medal
3317
+ * @param {String} [src] - Image location for the medal
3301
3318
  */
3302
- constructor
3303
- (
3304
- pos,
3305
- angle,
3306
- emitSize = 0,
3307
- emitTime = 0,
3308
- emitRate = 100,
3309
- emitConeAngle = PI,
3310
- tileIndex = -1,
3311
- tileSize = tileSizeDefault,
3312
- colorStartA = new Color,
3313
- colorStartB = new Color,
3314
- colorEndA = new Color(1,1,1,0),
3315
- colorEndB = new Color(1,1,1,0),
3316
- particleTime = .5,
3317
- sizeStart = .1,
3318
- sizeEnd = 1,
3319
- speed = .1,
3320
- angleSpeed = .05,
3321
- damping = 1,
3322
- angleDamping = 1,
3323
- gravityScale = 0,
3324
- particleConeAngle = PI,
3325
- fadeRate = .1,
3326
- randomness = .2,
3327
- collideTiles,
3328
- additive,
3329
- randomColorLinear = 1,
3330
- renderOrder = additive ? 1e9 : 0,
3331
- localSpace
3332
- )
3319
+ constructor(id, name, description='', icon='🏆', src)
3333
3320
  {
3334
- super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
3321
+ ASSERT(id >= 0 && !medals[id]);
3335
3322
 
3336
- // emitter settings
3337
- /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
3338
- this.emitSize = emitSize
3339
- /** @property {Number} - How long to stay alive (0 is forever) */
3340
- this.emitTime = emitTime;
3341
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
3342
- this.emitRate = emitRate;
3343
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
3344
- this.emitConeAngle = emitConeAngle;
3323
+ // save attributes and add to list of medals
3324
+ medals[this.id = id] = this;
3325
+ this.name = name;
3326
+ this.description = description;
3327
+ this.icon = icon;
3328
+ if (src)
3329
+ (this.image = new Image).src = src;
3330
+ }
3345
3331
 
3346
- // color settings
3347
- /** @property {Color} - Color at start of life 1, randomized between start colors */
3348
- this.colorStartA = colorStartA;
3349
- /** @property {Color} - Color at start of life 2, randomized between start colors */
3350
- this.colorStartB = colorStartB;
3351
- /** @property {Color} - Color at end of life 1, randomized between end colors */
3352
- this.colorEndA = colorEndA;
3353
- /** @property {Color} - Color at end of life 2, randomized between end colors */
3354
- this.colorEndB = colorEndB;
3355
- /** @property {Boolean} - Should color be randomized linearly or across each component */
3356
- this.randomColorLinear = randomColorLinear;
3332
+ /** Unlocks a medal if not already unlocked */
3333
+ unlock()
3334
+ {
3335
+ if (medalsPreventUnlock || this.unlocked)
3336
+ return;
3357
3337
 
3358
- // particle settings
3359
- /** @property {Number} - How long particles live */
3360
- this.particleTime = particleTime;
3361
- /** @property {Number} - How big are particles at start */
3362
- this.sizeStart = sizeStart;
3363
- /** @property {Number} - How big are particles at end */
3364
- this.sizeEnd = sizeEnd;
3365
- /** @property {Number} - How fast are particles when spawned */
3366
- this.speed = speed;
3367
- /** @property {Number} - How fast are particles rotating */
3368
- this.angleSpeed = angleSpeed;
3369
- /** @property {Number} - How much to dampen particle speed */
3370
- this.damping = damping;
3371
- /** @property {Number} - How much to dampen particle angular speed */
3372
- this.angleDamping = angleDamping;
3373
- /** @property {Number} - How much does gravity effect particles */
3374
- this.gravityScale = gravityScale;
3375
- /** @property {Number} - Cone for start particle angle */
3376
- this.particleConeAngle = particleConeAngle;
3377
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
3378
- this.fadeRate = fadeRate;
3379
- /** @property {Number} - Apply extra randomness percent */
3380
- this.randomness = randomness;
3381
- /** @property {Number} - Do particles collide against tiles */
3382
- this.collideTiles = collideTiles;
3383
- /** @property {Number} - Should particles use addtive blend */
3384
- this.additive = additive;
3385
- /** @property {Boolean} - Should it be in local space of emitter */
3386
- this.localSpace = localSpace;
3387
- /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3388
- this.trailScale = 0;
3338
+ // save the medal
3339
+ ASSERT(medalsSaveName); // save name must be set
3340
+ localStorage[this.storageKey()] = this.unlocked = 1;
3341
+ medalsDisplayQueue.push(this);
3342
+ newgrounds && newgrounds.unlockMedal(this.id);
3343
+ }
3344
+
3345
+ /** Render a medal
3346
+ * @param {Number} [hidePercent=0] - How much to slide the medal off screen
3347
+ */
3348
+ render(hidePercent=0)
3349
+ {
3350
+ const context = overlayContext;
3351
+ const width = min(medalDisplaySize.x, mainCanvas.width);
3352
+ const x = overlayCanvas.width - width;
3353
+ const y = -medalDisplaySize.y*hidePercent;
3354
+
3355
+ // draw containing rect and clip to that region
3356
+ context.save();
3357
+ context.beginPath();
3358
+ context.fillStyle = new Color(.9,.9,.9);
3359
+ context.strokeStyle = new Color(0,0,0);
3360
+ context.lineWidth = 3;
3361
+ context.fill(context.rect(x, y, width, medalDisplaySize.y));
3362
+ context.stroke();
3363
+ context.clip();
3364
+
3365
+ // draw the icon and text
3366
+ this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplaySize.y/2));
3367
+ const pos = vec2(x+medalDisplayIconSize+30, y+28);
3368
+ drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, 0, 'left');
3369
+ pos.y += 32;
3370
+ drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, 0, 'left');
3371
+ context.restore();
3372
+ }
3373
+
3374
+ /** Render the icon for a medal
3375
+ * @param {Number} x - Screen space X position
3376
+ * @param {Number} y - Screen space Y position
3377
+ * @param {Number} [size=medalDisplayIconSize] - Screen space size
3378
+ */
3379
+ renderIcon(pos, size=medalDisplayIconSize)
3380
+ {
3381
+ // draw the image or icon
3382
+ if (this.image)
3383
+ overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
3384
+ else
3385
+ drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
3386
+ }
3387
+
3388
+ // Get local storage key used by the medal
3389
+ storageKey() { return medalsSaveName + '_' + this.id; }
3390
+ }
3389
3391
 
3390
- // internal variables
3391
- this.emitTimeBuffer = 0;
3392
- }
3392
+ // engine automatically renders medals
3393
+ function medalsRender()
3394
+ {
3395
+ if (!medalsDisplayQueue.length)
3396
+ return;
3393
3397
 
3394
- /** Update the emitter to spawn particles, called automatically by engine once each frame */
3395
- update()
3398
+ // update first medal in queue
3399
+ const medal = medalsDisplayQueue[0];
3400
+ const time = timeReal - medalsDisplayTimeLast;
3401
+ if (!medalsDisplayTimeLast)
3402
+ medalsDisplayTimeLast = timeReal;
3403
+ else if (time > medalDisplayTime)
3404
+ medalsDisplayQueue.shift(medalsDisplayTimeLast = 0);
3405
+ else
3396
3406
  {
3397
- // only do default update to apply parent transforms
3398
- this.parent && super.update();
3407
+ // slide on/off medals
3408
+ const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
3409
+ const hidePercent =
3410
+ time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
3411
+ time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
3412
+ medal.render(hidePercent);
3413
+ }
3414
+ }
3399
3415
 
3400
- // update emitter
3401
- if (!this.emitTime | this.getAliveTime() <= this.emitTime)
3416
+ ///////////////////////////////////////////////////////////////////////////////
3417
+
3418
+ // global Newgrounds object
3419
+ let newgrounds;
3420
+
3421
+ /** This can used to enable Newgrounds functionality
3422
+ * @param {Number} app_id - The newgrounds App ID
3423
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64)
3424
+ * @memberof Medals */
3425
+ function newgroundsInit(app_id, cipher) { newgrounds = new Newgrounds(app_id, cipher); }
3426
+
3427
+ /**
3428
+ * Newgrounds API wrapper object
3429
+ * @example
3430
+ * // create a newgrounds object, replace the app id and cipher with your own
3431
+ * const app_id = '53123:1ZuSTQ9l';
3432
+ * const cipher = 'enF0vGH@Mj/FRASKL23Q==';
3433
+ * newgrounds = new Newgrounds(app_id, cipher);
3434
+ */
3435
+ class Newgrounds
3436
+ {
3437
+ /** Create a newgrounds object
3438
+ * @param {Number} app_id - The newgrounds App ID
3439
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64) */
3440
+ constructor(app_id, cipher)
3441
+ {
3442
+ ASSERT(!newgrounds && app_id);
3443
+
3444
+ this.app_id = app_id;
3445
+ this.cipher = cipher;
3446
+ this.host = location ? location.hostname : '';
3447
+
3448
+ // create an instance of CryptoJS for encrypted calls
3449
+ if (cipher)
3450
+ this.cryptoJS = this.CryptoJS();
3451
+
3452
+ // get session id from url search params
3453
+ const url = new URL(location.href);
3454
+ this.session_id = url.searchParams.get('ngio_session_id');
3455
+
3456
+ if (!this.session_id)
3457
+ return; // only use newgrounds when logged in
3458
+
3459
+ // get medals
3460
+ const medalsResult = this.call('Medal.getList');
3461
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
3462
+ debugMedals && console.log(this.medals);
3463
+ for (const newgroundsMedal of this.medals)
3402
3464
  {
3403
- // emit particles
3404
- if (this.emitRate * particleEmitRateScale)
3465
+ const medal = medals[newgroundsMedal['id']];
3466
+ if (medal)
3405
3467
  {
3406
- const rate = 1/this.emitRate/particleEmitRateScale;
3407
- for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
3408
- this.emitParticle();
3468
+ // copy newgrounds medal data
3469
+ medal.image = new Image;
3470
+ medal.image.src = newgroundsMedal['icon'];
3471
+ medal.name = newgroundsMedal['name'];
3472
+ medal.description = newgroundsMedal['description'];
3473
+ medal.unlocked = newgroundsMedal['unlocked'];
3474
+ medal.difficulty = newgroundsMedal['difficulty'];
3475
+ medal.value = newgroundsMedal['value'];
3476
+
3477
+ if (medal.value)
3478
+ medal.description = medal.description + ' (' + medal.value + ')';
3409
3479
  }
3410
3480
  }
3411
- else
3412
- this.destroy();
3481
+
3482
+ // get scoreboards
3483
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
3484
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
3485
+ debugMedals && console.log(this.scoreboards);
3413
3486
 
3414
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
3487
+ const keepAliveMS = 5 * 60 * 1e3;
3488
+ setInterval(()=>this.call('Gateway.ping', 0, 1), keepAliveMS);
3415
3489
  }
3416
3490
 
3417
- /** Spawn one particle
3418
- * @return {Particle} */
3419
- emitParticle()
3491
+ /** Send message to unlock a medal by id
3492
+ * @param {Number} id - The medal id */
3493
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, 1); }
3494
+
3495
+ /** Send message to post score
3496
+ * @param {Number} id - The scoreboard id
3497
+ * @param {Number} value - The score value */
3498
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3499
+
3500
+ /** Get scores from a scoreboard
3501
+ * @param {Number} id - The scoreboard id
3502
+ * @param {String} [user=0] - A user's id or name
3503
+ * @param {Number} [social=0] - If true, only social scores will be loaded
3504
+ * @param {Number} [skip=0] - Number of scores to skip before start
3505
+ * @param {Number} [limit=10] - Number of scores to include in the list
3506
+ * @return {Object} - The response JSON object
3507
+ */
3508
+ getScores(id, user=0, social=0, skip=0, limit=10)
3509
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3510
+
3511
+ /** Send message to log a view */
3512
+ logView() { return this.call('App.logView', {'host':this.host}, 1); }
3513
+
3514
+ /** Send a message to call a component of the Newgrounds API
3515
+ * @param {String} component - Name of the component
3516
+ * @param {Object} [parameters=0] - Parameters to use for call
3517
+ * @param {Boolean} [async=0] - If true, don't wait for response before continuing (avoid stall)
3518
+ * @return {Object} - The response JSON object
3519
+ */
3520
+ call(component, parameters=0, async=0)
3420
3521
  {
3421
- // spawn a particle
3422
- let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3423
- (new Vector2(rand(-.5,.5), rand(-.5,.5)))
3424
- .multiply(this.emitSize).rotate(this.angle) // box emitter
3425
- : randInCircle(this.emitSize/2); // circle emitter
3426
- let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3427
- if (!this.localSpace)
3522
+ const call = {'component':component, 'parameters':parameters};
3523
+ if (this.cipher)
3428
3524
  {
3429
- pos = this.pos.add(pos);
3430
- angle += this.angle;
3525
+ // encrypt using AES-128 Base64 with cryptoJS
3526
+ const cryptoJS = this.cryptoJS;
3527
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
3528
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
3529
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
3530
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
3531
+ call['parameters'] = 0;
3431
3532
  }
3432
-
3433
- const particle = new Particle(pos, this.tileIndex, this.tileSize, angle);
3434
3533
 
3435
- // randomness scales each paremeter by a percentage
3436
- const randomness = this.randomness;
3437
- const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
3534
+ // build the input object
3535
+ const input =
3536
+ {
3537
+ 'app_id': this.app_id,
3538
+ 'session_id': this.session_id,
3539
+ 'call': call
3540
+ };
3438
3541
 
3439
- // randomize particle settings
3440
- const particleTime = randomizeScale(this.particleTime);
3441
- const sizeStart = randomizeScale(this.sizeStart);
3442
- const sizeEnd = randomizeScale(this.sizeEnd);
3443
- const speed = randomizeScale(this.speed);
3444
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3445
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3446
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3447
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3448
- const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
3542
+ // build post data
3543
+ const formData = new FormData();
3544
+ formData.append('input', JSON.stringify(input));
3545
+
3546
+ // send post data
3547
+ const xmlHttp = new XMLHttpRequest();
3548
+ const url = 'https://newgrounds.io/gateway_v3.php';
3549
+ xmlHttp.open('POST', url, !debugMedals && async);
3550
+ xmlHttp.send(formData);
3551
+ debugMedals && console.log(xmlHttp.responseText);
3552
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
3553
+ }
3449
3554
 
3450
- // build particle settings
3451
- particle.colorStart = colorStart;
3452
- particle.colorEndDelta = colorEnd.subtract(colorStart);
3453
- particle.velocity = (new Vector2).setAngle(velocityAngle, speed);
3454
- particle.angleVelocity = angleSpeed;
3455
- particle.lifeTime = particleTime;
3456
- particle.sizeStart = sizeStart;
3457
- particle.sizeEndDelta = sizeEnd - sizeStart;
3458
- particle.fadeRate = this.fadeRate;
3459
- particle.damping = this.damping;
3460
- particle.angleDamping = this.angleDamping;
3461
- particle.elasticity = this.elasticity;
3462
- particle.friction = this.friction;
3463
- particle.gravityScale = this.gravityScale;
3464
- particle.collideTiles = this.collideTiles;
3465
- particle.additive = this.additive;
3466
- particle.renderOrder = this.renderOrder;
3467
- particle.trailScale = this.trailScale;
3468
- particle.mirror = randInt(2);
3469
- particle.localSpaceEmitter = this.localSpace && this;
3555
+ CryptoJS()
3556
+ {
3557
+ ///////////////////////////////////////////////////////////////////////////////
3558
+ // Crypto-JS - https://github.com/brix/crypto-js - MIT License
3559
+ //
3560
+ // [The MIT License (MIT)](http://opensource.org/licenses/MIT)
3561
+ //
3562
+ // Copyright (c) 2009-2013 Jeff Mott
3563
+ // Copyright (c) 2013-2016 Evan Vosberg
3564
+ //
3565
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
3566
+ // of this software and associated documentation files (the "Software"), to deal
3567
+ // in the Software without restriction, including without limitation the rights
3568
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3569
+ // copies of the Software, and to permit persons to whom the Software is
3570
+ // furnished to do so, subject to the following conditions:
3571
+ //
3572
+ // The above copyright notice and this permission notice shall be included in
3573
+ // all copies or substantial portions of the Software.
3574
+ //
3575
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3576
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3577
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3578
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3579
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3580
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
3581
+ // THE SOFTWARE.
3582
+ return eval(Function("[M='GBMGXz^oVYPPKKbB`agTXU|LxPc_ZBcMrZvCr~wyGfWrwk@ATqlqeTp^N?p{we}jIpEnB_sEr`l?YDkDhWhprc|Er|XETG?pTl`e}dIc[_N~}fzRycIfpW{HTolvoPB_FMe_eH~BTMx]yyOhv?biWPCGc]kABencBhgERHGf{OL`Dj`c^sh@canhy[secghiyotcdOWgO{tJIE^JtdGQRNSCrwKYciZOa]Y@tcRATYKzv|sXpboHcbCBf`}SKeXPFM|RiJsSNaIb]QPc[D]Jy_O^XkOVTZep`ONmntLL`Qz~UupHBX_Ia~WX]yTRJIxG`ioZ{fefLJFhdyYoyLPvqgH?b`[TMnTwwfzDXhfM?rKs^aFr|nyBdPmVHTtAjXoYUloEziWDCw_suyYT~lSMksI~ZNCS[Bex~j]Vz?kx`gdYSEMCsHpjbyxQvw|XxX_^nQYue{sBzVWQKYndtYQMWRef{bOHSfQhiNdtR{o?cUAHQAABThwHPT}F{VvFmgN`E@FiFYS`UJmpQNM`X|tPKHlccT}z}k{sACHL?Rt@MkWplxO`ASgh?hBsuuP|xD~LSH~KBlRs]t|l|_tQAroDRqWS^SEr[sYdPB}TAROtW{mIkE|dWOuLgLmJrucGLpebrAFKWjikTUzS|j}M}szasKOmrjy[?hpwnEfX[jGpLt@^v_eNwSQHNwtOtDgWD{rk|UgASs@mziIXrsHN_|hZuxXlPJOsA^^?QY^yGoCBx{ekLuZzRqQZdsNSx@ezDAn{XNj@fRXIwrDX?{ZQHwTEfu@GhxDOykqts|n{jOeZ@c`dvTY?e^]ATvWpb?SVyg]GC?SlzteilZJAL]mlhLjYZazY__qcVFYvt@|bIQnSno@OXyt]OulzkWqH`rYFWrwGs`v|~XeTsIssLrbmHZCYHiJrX}eEzSssH}]l]IhPQhPoQ}rCXLyhFIT[clhzYOvyHqigxmjz`phKUU^TPf[GRAIhNqSOdayFP@FmKmuIzMOeoqdpxyCOwCthcLq?n`L`tLIBboNn~uXeFcPE{C~mC`h]jUUUQe^`UqvzCutYCgct|SBrAeiYQW?X~KzCz}guXbsUw?pLsg@hDArw?KeJD[BN?GD@wgFWCiHq@Ypp_QKFixEKWqRp]oJFuVIEvjDcTFu~Zz]a{IcXhWuIdMQjJ]lwmGQ|]g~c]Hl]pl`Pd^?loIcsoNir_kikBYyg?NarXZEGYspt_vLBIoj}LI[uBFvm}tbqvC|xyR~a{kob|HlctZslTGtPDhBKsNsoZPuH`U`Fqg{gKnGSHVLJ^O`zmNgMn~{rsQuoymw^JY?iUBvw_~mMr|GrPHTERS[MiNpY[Mm{ggHpzRaJaoFomtdaQ_?xuTRm}@KjU~RtPsAdxa|uHmy}n^i||FVL[eQAPrWfLm^ndczgF~Nk~aplQvTUpHvnTya]kOenZlLAQIm{lPl@CCTchvCF[fI{^zPkeYZTiamoEcKmBMfZhk_j_~Fjp|wPVZlkh_nHu]@tP|hS@^G^PdsQ~f[RqgTDqezxNFcaO}HZhb|MMiNSYSAnQWCDJukT~e|OTgc}sf[cnr?fyzTa|EwEtRG|I~|IO}O]S|rp]CQ}}DWhSjC_|z|oY|FYl@WkCOoPuWuqr{fJu?Brs^_EBI[@_OCKs}?]O`jnDiXBvaIWhhMAQDNb{U`bqVR}oqVAvR@AZHEBY@depD]OLh`kf^UsHhzKT}CS}HQKy}Q~AeMydXPQztWSSzDnghULQgMAmbWIZ|lWWeEXrE^EeNoZApooEmrXe{NAnoDf`m}UNlRdqQ@jOc~HLOMWs]IDqJHYoMziEedGBPOxOb?[X`KxkFRg@`mgFYnP{hSaxwZfBQqTm}_?RSEaQga]w[vxc]hMne}VfSlqUeMo_iqmd`ilnJXnhdj^EEFifvZyxYFRf^VaqBhLyrGlk~qowqzHOBlOwtx?i{m~`n^G?Yxzxux}b{LSlx]dS~thO^lYE}bzKmUEzwW^{rPGhbEov[Plv??xtyKJshbG`KuO?hjBdS@Ru}iGpvFXJRrvOlrKN?`I_n_tplk}kgwSXuKylXbRQ]]?a|{xiT[li?k]CJpwy^o@ebyGQrPfF`aszGKp]baIx~H?ElETtFh]dz[OjGl@C?]VDhr}OE@V]wLTc[WErXacM{We`F|utKKjgllAxvsVYBZ@HcuMgLboFHVZmi}eIXAIFhS@A@FGRbjeoJWZ_NKd^oEH`qgy`q[Tq{x?LRP|GfBFFJV|fgZs`MLbpPYUdIV^]mD@FG]pYAT^A^RNCcXVrPsgk{jTrAIQPs_`mD}rOqAZA[}RETFz]WkXFTz_m{N@{W@_fPKZLT`@aIqf|L^Mb|crNqZ{BVsijzpGPEKQQZGlApDn`ruH}cvF|iXcNqK}cxe_U~HRnKV}sCYb`D~oGvwG[Ca|UaybXea~DdD~LiIbGRxJ_VGheI{ika}KC[OZJLn^IBkPrQj_EuoFwZ}DpoBRcK]Q}?EmTv~i_Tul{bky?Iit~tgS|o}JL_VYcCQdjeJ_MfaA`FgCgc[Ii|CBHwq~nbJeYTK{e`CNstKfTKPzw{jdhp|qsZyP_FcugxCFNpKitlR~vUrx^NrSVsSTaEgnxZTmKc`R|lGJeX}ccKLsQZQhsFkeFd|ckHIVTlGMg`~uPwuHRJS_CPuN_ogXe{Ba}dO_UBhuNXby|h?JlgBIqMKx^_u{molgL[W_iavNQuOq?ap]PGB`clAicnl@k~pA?MWHEZ{HuTLsCpOxxrKlBh]FyMjLdFl|nMIvTHyGAlPogqfZ?PlvlFJvYnDQd}R@uAhtJmDfe|iJqdkYr}r@mEjjIetDl_I`TELfoR|qTBu@Tic[BaXjP?dCS~MUK[HPRI}OUOwAaf|_}HZzrwXvbnNgltjTwkBE~MztTQhtRSWoQHajMoVyBBA`kdgK~h`o[J`dm~pm]tk@i`[F~F]DBlJKklrkR]SNw@{aG~Vhl`KINsQkOy?WhcqUMTGDOM_]bUjVd|Yh_KUCCgIJ|LDIGZCPls{RzbVWVLEhHvWBzKq|^N?DyJB|__aCUjoEgsARki}j@DQXS`RNU|DJ^a~d{sh_Iu{ONcUtSrGWW@cvUjefHHi}eSSGrNtO?cTPBShLqzwMVjWQQCCFB^culBjZHEK_{dO~Q`YhJYFn]jq~XSnG@[lQr]eKrjXpG~L^h~tDgEma^AUFThlaR{xyuP@[^VFwXSeUbVetufa@dX]CLyAnDV@Bs[DnpeghJw^?UIana}r_CKGDySoRudklbgio}kIDpA@McDoPK?iYcG?_zOmnWfJp}a[JLR[stXMo?_^Ng[whQlrDbrawZeSZ~SJstIObdDSfAA{MV}?gNunLOnbMv_~KFQUAjIMj^GkoGxuYtYbGDImEYiwEMyTpMxN_LSnSMdl{bg@dtAnAMvhDTBR_FxoQgANniRqxd`pWv@rFJ|mWNWmh[GMJz_Nq`BIN@KsjMPASXORcdHjf~rJfgZYe_uulzqM_KdPlMsuvU^YJuLtofPhGonVOQxCMuXliNvJIaoC?hSxcxKVVxWlNs^ENDvCtSmO~WxI[itnjs^RDvI@KqG}YekaSbTaB]ki]XM@[ZnDAP~@|BzLRgOzmjmPkRE@_sobkT|SszXK[rZN?F]Z_u}Yue^[BZgLtR}FHzWyxWEX^wXC]MJmiVbQuBzkgRcKGUhOvUc_bga|Tx`KEM`JWEgTpFYVeXLCm|mctZR@uKTDeUONPozBeIkrY`cz]]~WPGMUf`MNUGHDbxZuO{gmsKYkAGRPqjc|_FtblEOwy}dnwCHo]PJhN~JoteaJ?dmYZeB^Xd?X^pOKDbOMF@Ugg^hETLdhwlA}PL@_ur|o{VZosP?ntJ_kG][g{Zq`Tu]dzQlSWiKfnxDnk}KOzp~tdFstMobmy[oPYjyOtUzMWdjcNSUAjRuqhLS@AwB^{BFnqjCmmlk?jpn}TksS{KcKkDboXiwK]qMVjm~V`LgWhjS^nLGwfhAYrjDSBL_{cRus~{?xar_xqPlArrYFd?pHKdMEZzzjJpfC?Hv}mAuIDkyBxFpxhstTx`IO{rp}XGuQ]VtbHerlRc_LFGWK[XluFcNGUtDYMZny[M^nVKVeMllQI[xtvwQnXFlWYqxZZFp_|]^oWX[{pOMpxXxvkbyJA[DrPzwD|LW|QcV{Nw~U^dgguSpG]ClmO@j_TENIGjPWwgdVbHganhM?ema|dBaqla|WBd`poj~klxaasKxGG^xbWquAl~_lKWxUkDFagMnE{zHug{b`A~IYcQYBF_E}wiA}K@yxWHrZ{[d~|ARsYsjeNWzkMs~IOqqp[yzDE|WFrivsidTcnbHFRoW@XpAV`lv_zj?B~tPCppRjgbbDTALeFaOf?VcjnKTQMLyp{NwdylHCqmo?oelhjWuXj~}{fpuX`fra?GNkDiChYgVSh{R[BgF~eQa^WVz}ATI_CpY?g_diae]|ijH`TyNIF}|D_xpmBq_JpKih{Ba|sWzhnAoyraiDvk`h{qbBfsylBGmRH}DRPdryEsSaKS~tIaeF[s]I~xxHVrcNe@Jjxa@jlhZueLQqHh_]twVMqG_EGuwyab{nxOF?`HCle}nBZzlTQjkLmoXbXhOtBglFoMz?eqre`HiE@vNwBulglmQjj]DB@pPkPUgA^sjOAUNdSu_`oAzar?n?eMnw{{hYmslYi[TnlJD'",...']charCodeAtUinyxpf',"for(;e<10359;c[e++]=p-=128,A=A?p-A&&A:p==34&&p)for(p=1;p<128;y=f.map((n,x)=>(U=r[n]*2+1,U=Math.log(U/(h-U)),t-=a[x]*U,U/500)),t=~-h/(1+Math.exp(t))|1,i=o%h<t,o=o%h+(i?t:h-t)*(o>>17)-!i*t,f.map((n,x)=>(U=r[n]+=(i*h/2-r[n]<<13)/((C[n]+=C[n]<5)+1/20)>>13,a[x]+=y[x]*(i-t/h))),p=p*2+i)for(f='010202103203210431053105410642065206541'.split(t=0).map((n,x)=>(U=0,[...n].map((n,x)=>(U=U*997+(c[e-n]|0)|0)),h*32-1&U*997+p+!!A*129)*12+x);o<h*32;o=o*64|M.charCodeAt(d++)&63);for(C=String.fromCharCode(...c);r=/[\0-#?@\\\\~]/.exec(C);)with(C.split(r))C=join(shift());return C")([],[],1<<17,[0,0,0,0,0,0,0,0,0,0,0,0],new Uint16Array(51e6).fill(1<<15),new Uint8Array(51e6),0,0,0,0));
3583
+ // end of Crypto-JS
3584
+ ///////////////////////////////////////////////////////////////////////////////
3585
+ }
3586
+ }
3587
+ /**
3588
+ * LittleJS WebGL Interface
3589
+ * - All webgl used by the engine is wrapped up here
3590
+ * - For normal stuff you won't need to see or call anything in this file
3591
+ * - For advanced stuff there are helper functions to create shaders, textures, etc
3592
+ * - Can be disabled with glEnable to revert to 2D canvas rendering
3593
+ * - Batches sprite rendering on GPU for incredibly fast performance
3594
+ * - Sprite transform math is done in the shader where possible
3595
+ * @namespace WebGL
3596
+ */
3597
+
3598
+ 'use strict';
3599
+
3600
+ /** The WebGL canvas which appears above the main canvas and below the overlay canvas
3601
+ * @type {HTMLCanvasElement}
3602
+ * @memberof WebGL */
3603
+ let glCanvas;
3470
3604
 
3471
- // setup callbacks for particles
3472
- particle.destroyCallback = this.particleDestroyCallback;
3473
- this.particleCreateCallback && this.particleCreateCallback(particle);
3605
+ /** 2d context for glCanvas
3606
+ * @type {WebGLRenderingContext}
3607
+ * @memberof WebGL */
3608
+ let glContext;
3474
3609
 
3475
- // return the newly created particle
3476
- return particle;
3477
- }
3610
+ /** Main tile sheet texture automatically loaded by engine
3611
+ * @type {WebGLTexture}
3612
+ * @memberof WebGL */
3613
+ let glTileTexture;
3478
3614
 
3479
- // Particle emitters are not rendered, only the particles are
3480
- render() {}
3481
- }
3615
+ // WebGL internal variables not exposed to documentation
3616
+ let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3482
3617
 
3483
3618
  ///////////////////////////////////////////////////////////////////////////////
3484
- /**
3485
- * Particle Object - Created automatically by Particle Emitters
3486
- * @extends EngineObject
3487
- */
3488
- class Particle extends EngineObject
3619
+
3620
+ // Init WebGL, called automatically by the engine
3621
+ function glInit()
3489
3622
  {
3490
- /**
3491
- * Create a particle with the given settings
3492
- * @param {Vector2} position - World space position of the particle
3493
- * @param {Number} [tileIndex=-1] - Tile to use to render, untextured if -1
3494
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
3495
- * @param {Number} [angle=0] - Angle to rotate the particle
3496
- */
3497
- constructor(pos, tileIndex, tileSize, angle)
3498
- { super(pos, new Vector2, tileIndex, tileSize, angle); }
3623
+ // create the canvas and tile texture
3624
+ glCanvas = document.createElement('canvas');
3625
+ glContext = glCanvas.getContext('webgl', {antialias: false});
3626
+ glTileTexture = glCreateTexture(tileImage);
3499
3627
 
3500
- /** Render the particle, automatically called each frame, sorted by renderOrder */
3501
- render()
3502
- {
3503
- // modulate size and color
3504
- const p = min((time - this.spawnTime) / this.lifeTime, 1);
3505
- const radius = this.sizeStart + p * this.sizeEndDelta;
3506
- const size = new Vector2(radius, radius);
3507
- const fadeRate = this.fadeRate/2;
3508
- const color = new Color(
3509
- this.colorStart.r + p * this.colorEndDelta.r,
3510
- this.colorStart.g + p * this.colorEndDelta.g,
3511
- this.colorStart.b + p * this.colorEndDelta.b,
3512
- (this.colorStart.a + p * this.colorEndDelta.a) *
3513
- (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
3628
+ // some browsers are much faster without copying the gl buffer so we just overlay it instead
3629
+ glOverlay && document.body.appendChild(glCanvas);
3514
3630
 
3515
- // draw the particle
3516
- this.additive && setBlendMode(1);
3631
+ // setup vertex and fragment shaders
3632
+ glShader = glCreateProgram(
3633
+ 'precision highp float;'+ // use highp for better accuracy
3634
+ 'uniform mat4 m;'+ // transform matrix
3635
+ 'attribute vec2 p,t;'+ // position, uv
3636
+ 'attribute vec4 c,a;'+ // color, additiveColor
3637
+ 'varying vec4 v,d,e;'+ // return uv, color, additiveColor
3638
+ 'void main(){'+ // shader entry point
3639
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
3640
+ 'v=vec4(t,p);d=c;e=a;'+ // pass stuff to fragment shader
3641
+ '}' // end of shader
3642
+ ,
3643
+ 'precision highp float;'+ // use highp for better accuracy
3644
+ 'varying vec4 v,d,e;'+ // uv, color, additiveColor
3645
+ 'uniform sampler2D s;'+ // texture
3646
+ 'void main(){'+ // shader entry point
3647
+ 'gl_FragColor=texture2D(s,v.xy)*d+e;'+ // modulate texture by color plus additive
3648
+ '}' // end of shader
3649
+ );
3517
3650
 
3518
- let pos = this.pos, angle = this.angle;
3519
- if (this.localSpaceEmitter)
3520
- {
3521
- // in local space of emitter
3522
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
3523
- angle += this.localSpaceEmitter.angle;
3524
- }
3525
- if (this.trailScale)
3526
- {
3527
- // trail style particles
3528
- let velocity = this.velocity;
3529
- if (this.localSpaceEmitter)
3530
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
3531
- const speed = velocity.length();
3532
- const direction = velocity.scale(1/speed);
3533
- const trailLength = speed * this.trailScale;
3534
- size.y = max(size.x, trailLength);
3535
- angle = direction.angle();
3536
- drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3537
- }
3538
- else
3539
- drawTile(pos, size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3540
- this.additive && setBlendMode();
3541
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
3651
+ // init buffers
3652
+ const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
3653
+ glArrayBuffer = glContext.createBuffer();
3654
+ glPositionData = new Float32Array(vertexData);
3655
+ glColorData = new Uint32Array(vertexData);
3656
+ glBatchCount = 0;
3657
+ }
3542
3658
 
3543
- if (p == 1)
3544
- {
3545
- // destroy particle when it's time runs out
3546
- this.color = color;
3547
- this.size = size;
3548
- this.destroyCallback && this.destroyCallback(this);
3549
- this.destroyed = 1;
3550
- }
3551
- }
3659
+ /** Set the WebGl blend mode, normally you should call setBlendMode instead
3660
+ * @param {Boolean} [additive=0]
3661
+ * @memberof WebGL */
3662
+ function glSetBlendMode(additive)
3663
+ {
3664
+ // setup blending
3665
+ glAdditive = additive;
3552
3666
  }
3553
- /**
3554
- * LittleJS Medal System
3555
- * - Tracks and displays medals
3556
- * - Saves medals to local storage
3557
- * - Newgrounds integration
3558
- * @namespace Medals
3559
- */
3560
3667
 
3561
- 'use strict';
3668
+ /** Set the WebGl texture, not normally necessary unless multiple tile sheets are used
3669
+ * - This may also flush the gl buffer resulting in more draw calls and worse performance
3670
+ * @param {WebGLTexture} [texture=glTileTexture]
3671
+ * @memberof WebGL */
3672
+ function glSetTexture(texture=glTileTexture)
3673
+ {
3674
+ // must flush cache with the old texture to set a new one
3675
+ if (texture != glActiveTexture)
3676
+ glFlush();
3562
3677
 
3563
- /** List of all medals
3564
- * @type {Array}
3565
- * @memberof Medals */
3566
- const medals = [];
3678
+ glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = texture);
3679
+ }
3567
3680
 
3568
- // Engine internal variables not exposed to documentation
3569
- let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3681
+ /** Compile WebGL shader of the given type, will throw errors if in debug mode
3682
+ * @param {String} source
3683
+ * @param type
3684
+ * @return {WebGLShader}
3685
+ * @memberof WebGL */
3686
+ function glCompileShader(source, type)
3687
+ {
3688
+ // build the shader
3689
+ const shader = glContext.createShader(type);
3690
+ glContext.shaderSource(shader, source);
3691
+ glContext.compileShader(shader);
3570
3692
 
3571
- ///////////////////////////////////////////////////////////////////////////////
3693
+ // check for errors
3694
+ if (debug && !glContext.getShaderParameter(shader, gl_COMPILE_STATUS))
3695
+ throw glContext.getShaderInfoLog(shader);
3696
+ return shader;
3697
+ }
3572
3698
 
3573
- /** Initialize medals with a save name used for storage
3574
- * - Call this after creating all medals
3575
- * - Checks if medals are unlocked
3576
- * @param {String} saveName
3577
- * @memberof Medals */
3578
- function medalsInit(saveName)
3699
+ /** Create WebGL program with given shaders
3700
+ * @param {WebGLShader} vsSource
3701
+ * @param {WebGLShader} fsSource
3702
+ * @return {WebGLProgram}
3703
+ * @memberof WebGL */
3704
+ function glCreateProgram(vsSource, fsSource)
3579
3705
  {
3580
- // check if medals are unlocked
3581
- medalsSaveName = saveName;
3582
- debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
3706
+ // build the program
3707
+ const program = glContext.createProgram();
3708
+ glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
3709
+ glContext.attachShader(program, glCompileShader(fsSource, gl_FRAGMENT_SHADER));
3710
+ glContext.linkProgram(program);
3711
+
3712
+ // check for errors
3713
+ if (debug && !glContext.getProgramParameter(program, gl_LINK_STATUS))
3714
+ throw glContext.getProgramInfoLog(program);
3715
+ return program;
3583
3716
  }
3584
3717
 
3585
- /**
3586
- * Medal Object - Tracks an unlockable medal
3587
- * @example
3588
- * // create a medal
3589
- * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
3590
- *
3591
- * // initialize medals
3592
- * medalsInit('Example Game');
3593
- *
3594
- * // unlock the medal
3595
- * medal_example.unlock();
3596
- */
3597
- class Medal
3718
+ /** Create WebGL texture from an image and set the texture settings
3719
+ * @param {Image} image
3720
+ * @return {WebGLTexture}
3721
+ * @memberof WebGL */
3722
+ function glCreateTexture(image)
3598
3723
  {
3599
- /** Create an medal object and adds it to the list of medals
3600
- * @param {Number} id - The unique identifier of the medal
3601
- * @param {String} name - Name of the medal
3602
- * @param {String} [description] - Description of the medal
3603
- * @param {String} [icon='🏆'] - Icon for the medal
3604
- * @param {String} [src] - Image location for the medal
3605
- */
3606
- constructor(id, name, description='', icon='🏆', src)
3607
- {
3608
- ASSERT(id >= 0 && !medals[id]);
3724
+ // build the texture
3725
+ const texture = glContext.createTexture();
3726
+ glContext.bindTexture(gl_TEXTURE_2D, texture);
3727
+ image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3728
+
3729
+ // use point filtering for pixelated rendering
3730
+ const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
3731
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
3732
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
3733
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
3734
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
3735
+ return texture;
3736
+ }
3609
3737
 
3610
- // save attributes and add to list of medals
3611
- medals[this.id = id] = this;
3612
- this.name = name;
3613
- this.description = description;
3614
- this.icon = icon;
3615
- if (src)
3616
- (this.image = new Image).src = src;
3617
- }
3738
+ // called automatically by engine before render
3739
+ function glPreRender()
3740
+ {
3741
+ // clear and set to same size as main canvas
3742
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
3743
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3618
3744
 
3619
- /** Unlocks a medal if not already unlocked */
3620
- unlock()
3745
+ // set up the shader
3746
+ glContext.useProgram(glShader);
3747
+ glContext.activeTexture(gl_TEXTURE0);
3748
+ glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3749
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
3750
+ glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
3751
+ glSetBlendMode();
3752
+
3753
+ // set vertex attributes
3754
+ let offset = 0;
3755
+ const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3621
3756
  {
3622
- if (medalsPreventUnlock || this.unlocked)
3623
- return;
3624
-
3625
- // save the medal
3626
- ASSERT(medalsSaveName); // save name must be set
3627
- localStorage[this.storageKey()] = this.unlocked = 1;
3628
- medalsDisplayQueue.push(this);
3629
- newgrounds && newgrounds.unlockMedal(this.id);
3757
+ const location = glContext.getAttribLocation(glShader, name);
3758
+ glContext.enableVertexAttribArray(location);
3759
+ glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3760
+ offset += size*typeSize;
3630
3761
  }
3762
+ initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3763
+ initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3764
+ initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3765
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3631
3766
 
3632
- /** Render a medal
3633
- * @param {Number} [hidePercent=0] - How much to slide the medal off screen
3634
- */
3635
- render(hidePercent=0)
3636
- {
3637
- const context = overlayContext;
3638
- const width = min(medalDisplaySize.x, mainCanvas.width);
3639
- const x = overlayCanvas.width - width;
3640
- const y = -medalDisplaySize.y*hidePercent;
3767
+ // build the transform matrix
3768
+ const sx = 2 * cameraScale / mainCanvas.width;
3769
+ const sy = 2 * cameraScale / mainCanvas.height;
3770
+ glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), 0,
3771
+ new Float32Array([
3772
+ sx, 0, 0, 0,
3773
+ 0, sy, 0, 0,
3774
+ 1, 1, -1, 1,
3775
+ -1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
3776
+ ])
3777
+ );
3778
+ }
3641
3779
 
3642
- // draw containing rect and clip to that region
3643
- context.save();
3644
- context.beginPath();
3645
- context.fillStyle = new Color(.9,.9,.9);
3646
- context.strokeStyle = new Color(0,0,0);
3647
- context.lineWidth = 3;
3648
- context.fill(context.rect(x, y, width, medalDisplaySize.y));
3649
- context.stroke();
3650
- context.clip();
3780
+ /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
3781
+ * @memberof WebGL */
3782
+ function glFlush()
3783
+ {
3784
+ if (!glBatchCount) return;
3651
3785
 
3652
- // draw the icon and text
3653
- this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplaySize.y/2));
3654
- const pos = vec2(x+medalDisplayIconSize+30, y+28);
3655
- drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, 0, 'left');
3656
- pos.y += 32;
3657
- drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, 0, 'left');
3658
- context.restore();
3659
- }
3786
+ const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3787
+ glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
3788
+ glContext.enable(gl_BLEND);
3660
3789
 
3661
- /** Render the icon for a medal
3662
- * @param {Number} x - Screen space X position
3663
- * @param {Number} y - Screen space Y position
3664
- * @param {Number} [size=medalDisplayIconSize] - Screen space size
3665
- */
3666
- renderIcon(pos, size=medalDisplayIconSize)
3667
- {
3668
- // draw the image or icon
3669
- if (this.image)
3670
- overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
3671
- else
3672
- drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
3673
- }
3674
-
3675
- // Get local storage key used by the medal
3676
- storageKey() { return medalsSaveName + '_' + this.id; }
3790
+ // draw all the sprites in the batch and reset the buffer
3791
+ glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
3792
+ glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
3793
+ glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
3794
+ glBatchCount = 0;
3795
+ glBatchAdditive = glAdditive;
3677
3796
  }
3678
3797
 
3679
- // engine automatically renders medals
3680
- function medalsRender()
3798
+ /** Draw any sprites still in the buffer, copy to main canvas and clear
3799
+ * @param {CanvasRenderingContext2D} context
3800
+ * @param {Boolean} [forceDraw=0]
3801
+ * @memberof WebGL */
3802
+ function glCopyToContext(context, forceDraw)
3681
3803
  {
3682
- if (!medalsDisplayQueue.length)
3683
- return;
3804
+ if (!glBatchCount && !forceDraw) return;
3684
3805
 
3685
- // update first medal in queue
3686
- const medal = medalsDisplayQueue[0];
3687
- const time = timeReal - medalsDisplayTimeLast;
3688
- if (!medalsDisplayTimeLast)
3689
- medalsDisplayTimeLast = timeReal;
3690
- else if (time > medalDisplayTime)
3691
- medalsDisplayQueue.shift(medalsDisplayTimeLast = 0);
3692
- else
3806
+ glFlush();
3807
+
3808
+ // do not draw in overlay mode because the canvas is visible
3809
+ if (!glOverlay || forceDraw)
3810
+ context.drawImage(glCanvas, 0, 0);
3811
+ }
3812
+
3813
+ /** Add a sprite to the gl draw list, used by all gl draw functions
3814
+ * @param x
3815
+ * @param y
3816
+ * @param sizeX
3817
+ * @param sizeY
3818
+ * @param angle
3819
+ * @param uv0X
3820
+ * @param uv0Y
3821
+ * @param uv1X
3822
+ * @param uv1Y
3823
+ * @param rgba
3824
+ * @param [rgbaAdditive=0]
3825
+ * @memberof WebGL */
3826
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
3827
+ {
3828
+ // flush if there is no room for more verts or if different blend mode
3829
+ if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3830
+ glFlush();
3831
+
3832
+ // prepare to create the verts from size and angle
3833
+ const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3834
+ const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3835
+
3836
+ // setup 2 triangles to form a quad
3837
+ for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
3693
3838
  {
3694
- // slide on/off medals
3695
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
3696
- const hidePercent =
3697
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
3698
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
3699
- medal.render(hidePercent);
3839
+ const a = i-4&&i>1, b = i-5&&i-2&&i-1;
3840
+ glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
3841
+ glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
3842
+ glPositionData[offset++] = a ? uv0X : uv1X;
3843
+ glPositionData[offset++] = b ? uv0Y : uv1Y;
3844
+ glColorData[offset++] = rgba;
3845
+ glColorData[offset++] = rgbaAdditive;
3700
3846
  }
3701
3847
  }
3702
3848
 
3703
3849
  ///////////////////////////////////////////////////////////////////////////////
3850
+ // post processing - can be enabled to pass other canvases through a final shader
3704
3851
 
3705
- // global Newgrounds object
3706
- let newgrounds;
3707
-
3708
- /** This can used to enable Newgrounds functionality
3709
- * @param {Number} app_id - The newgrounds App ID
3710
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
3711
- * @memberof Medals */
3712
- function newgroundsInit(app_id, cipher) { newgrounds = new Newgrounds(app_id, cipher); }
3852
+ let glPostShader, glPostArrayBuffer, glPostTexture, glPostIncludeOverlay;
3713
3853
 
3714
- /**
3715
- * Newgrounds API wrapper object
3716
- * @example
3717
- * // create a newgrounds object, replace the app id and cipher with your own
3718
- * const app_id = '53123:1ZuSTQ9l';
3719
- * const cipher = 'enF0vGH@Mj/FRASKL23Q==';
3720
- * newgrounds = new Newgrounds(app_id, cipher);
3721
- */
3722
- class Newgrounds
3854
+ /** Set up a post processing shader
3855
+ * @param {String} shaderCode
3856
+ * @param {Boolean} includeOverlay
3857
+ * @memberof WebGL */
3858
+ function glInitPostProcess(shaderCode, includeOverlay)
3723
3859
  {
3724
- /** Create a newgrounds object
3725
- * @param {Number} app_id - The newgrounds App ID
3726
- * @param {String} [cipher] - The encryption Key (AES-128/Base64) */
3727
- constructor(app_id, cipher)
3728
- {
3729
- ASSERT(!newgrounds && app_id);
3860
+ ASSERT(!glPostShader); // can only have 1 post effects shader
3730
3861
 
3731
- this.app_id = app_id;
3732
- this.cipher = cipher;
3733
- this.host = location ? location.hostname : '';
3734
-
3735
- // create an instance of CryptoJS for encrypted calls
3736
- if (cipher)
3737
- this.cryptoJS = this.CryptoJS();
3862
+ if (!shaderCode) // default shader
3863
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture2D(iChannel0,p/iResolution.xy);}';
3738
3864
 
3739
- // get session id from url search params
3740
- const url = new URL(location.href);
3741
- this.session_id = url.searchParams.get('ngio_session_id');
3865
+ // create the shader
3866
+ glPostShader = glCreateProgram(
3867
+ 'precision highp float;'+ // use highp for better accuracy
3868
+ 'attribute vec2 p;'+ // position
3869
+ 'void main(){'+ // shader entry point
3870
+ 'gl_Position=vec4(p,1,1);'+ // set position
3871
+ '}' // end of shader
3872
+ ,
3873
+ 'precision highp float;'+ // use highp for better accuracy
3874
+ 'uniform sampler2D iChannel0;'+ // input texture
3875
+ 'uniform vec3 iResolution;'+ // size of output texture
3876
+ 'uniform float iTime;'+ // time passed
3877
+ '\n' + shaderCode + '\n'+ // insert custom shader code
3878
+ 'void main(){'+ // shader entry point
3879
+ 'mainImage(gl_FragColor,gl_FragCoord.xy);'+ // call post process function
3880
+ 'gl_FragColor.a=1.;'+ // always use full alpha
3881
+ '}' // end of shader
3882
+ );
3742
3883
 
3743
- if (!this.session_id)
3744
- return; // only use newgrounds when logged in
3884
+ // create buffer and texture
3885
+ glPostArrayBuffer = glContext.createBuffer();
3886
+ glPostTexture = glCreateTexture();
3887
+ glPostIncludeOverlay = includeOverlay;
3745
3888
 
3746
- // get medals
3747
- const medalsResult = this.call('Medal.getList');
3748
- this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
3749
- debugMedals && console.log(this.medals);
3750
- for (const newgroundsMedal of this.medals)
3751
- {
3752
- const medal = medals[newgroundsMedal['id']];
3753
- if (medal)
3754
- {
3755
- // copy newgrounds medal data
3756
- medal.image = new Image;
3757
- medal.image.src = newgroundsMedal['icon'];
3758
- medal.name = newgroundsMedal['name'];
3759
- medal.description = newgroundsMedal['description'];
3760
- medal.unlocked = newgroundsMedal['unlocked'];
3761
- medal.difficulty = newgroundsMedal['difficulty'];
3762
- medal.value = newgroundsMedal['value'];
3889
+ // hide the original 2d canvas
3890
+ mainCanvas.style.visibility = 'hidden';
3891
+ }
3763
3892
 
3764
- if (medal.value)
3765
- medal.description = medal.description + ' (' + medal.value + ')';
3766
- }
3767
- }
3893
+ // Render the post processing shader, called automatically by the engine
3894
+ function glRenderPostProcess()
3895
+ {
3896
+ if (!glPostShader)
3897
+ return;
3768
3898
 
3769
- // get scoreboards
3770
- const scoreboardResult = this.call('ScoreBoard.getBoards');
3771
- this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
3772
- debugMedals && console.log(this.scoreboards);
3773
-
3774
- const keepAliveMS = 5 * 60 * 1e3;
3775
- setInterval(()=>this.call('Gateway.ping', 0, 1), keepAliveMS);
3899
+ // prepare to render post process shader
3900
+ if (glEnable)
3901
+ {
3902
+ glFlush(); // clear out the buffer
3903
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
3776
3904
  }
3905
+ else // set viewport
3906
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
3777
3907
 
3778
- /** Send message to unlock a medal by id
3779
- * @param {Number} id - The medal id */
3780
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, 1); }
3908
+ if (glPostIncludeOverlay)
3909
+ {
3910
+ // copy overlay canvas so it will be included in post processing
3911
+ mainContext.drawImage(overlayCanvas, 0, 0);
3781
3912
 
3782
- /** Send message to post score
3783
- * @param {Number} id - The scoreboard id
3784
- * @param {Number} value - The score value */
3785
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3913
+ // clear overlay canvas
3914
+ overlayCanvas.width = mainCanvas.width;
3915
+ }
3786
3916
 
3787
- /** Get scores from a scoreboard
3788
- * @param {Number} id - The scoreboard id
3789
- * @param {String} [user=0] - A user's id or name
3790
- * @param {Number} [social=0] - If true, only social scores will be loaded
3791
- * @param {Number} [skip=0] - Number of scores to skip before start
3792
- * @param {Number} [limit=10] - Number of scores to include in the list
3793
- * @return {Object} - The response JSON object
3794
- */
3795
- getScores(id, user=0, social=0, skip=0, limit=10)
3796
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3917
+ // setup shader program to draw one triangle
3918
+ glContext.useProgram(glPostShader);
3919
+ glContext.disable(gl_BLEND);
3920
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glPostArrayBuffer);
3921
+ glContext.bufferData(gl_ARRAY_BUFFER, new Float32Array([-3,1,1,-3,1,1]), gl_STATIC_DRAW);
3922
+ glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, true);
3797
3923
 
3798
- /** Send message to log a view */
3799
- logView() { return this.call('App.logView', {'host':this.host}, 1); }
3924
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
3925
+ glContext.activeTexture(gl_TEXTURE0);
3926
+ glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
3927
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
3800
3928
 
3801
- /** Send a message to call a component of the Newgrounds API
3802
- * @param {String} component - Name of the component
3803
- * @param {Object} [parameters=0] - Parameters to use for call
3804
- * @param {Boolean} [async=0] - If true, don't wait for response before continuing (avoid stall)
3805
- * @return {Object} - The response JSON object
3806
- */
3807
- call(component, parameters=0, async=0)
3808
- {
3809
- const call = {'component':component, 'parameters':parameters};
3810
- if (this.cipher)
3811
- {
3812
- // encrypt using AES-128 Base64 with cryptoJS
3813
- const cryptoJS = this.cryptoJS;
3814
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
3815
- const iv = cryptoJS['lib']['WordArray']['random'](16);
3816
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
3817
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
3818
- call['parameters'] = 0;
3819
- }
3929
+ // set vertex position attribute
3930
+ const vertexByteStride = 8;
3931
+ const pLocation = glContext.getAttribLocation(glPostShader, 'p');
3932
+ glContext.enableVertexAttribArray(pLocation);
3933
+ glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, 0, vertexByteStride, 0);
3820
3934
 
3821
- // build the input object
3822
- const input =
3823
- {
3824
- 'app_id': this.app_id,
3825
- 'session_id': this.session_id,
3826
- 'call': call
3827
- };
3935
+ // set uniforms and draw
3936
+ const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
3937
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
3938
+ glContext.uniform1f(uniformLocation('iTime'), time);
3939
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
3940
+ glContext.drawArrays(gl_TRIANGLES, 0, 3);
3941
+ }
3828
3942
 
3829
- // build post data
3830
- const formData = new FormData();
3831
- formData.append('input', JSON.stringify(input));
3832
-
3833
- // send post data
3834
- const xmlHttp = new XMLHttpRequest();
3835
- const url = 'https://newgrounds.io/gateway_v3.php';
3836
- xmlHttp.open('POST', url, !debugMedals && async);
3837
- xmlHttp.send(formData);
3838
- debugMedals && console.log(xmlHttp.responseText);
3839
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
3840
- }
3943
+ ///////////////////////////////////////////////////////////////////////////////
3944
+ // store gl constants as integers so their name doesn't use space in minifed
3945
+ const
3946
+ gl_ONE = 1,
3947
+ gl_TRIANGLES = 4,
3948
+ gl_SRC_ALPHA = 770,
3949
+ gl_ONE_MINUS_SRC_ALPHA = 771,
3950
+ gl_BLEND = 3042,
3951
+ gl_TEXTURE_2D = 3553,
3952
+ gl_UNSIGNED_BYTE = 5121,
3953
+ gl_BYTE = 5120,
3954
+ gl_FLOAT = 5126,
3955
+ gl_RGBA = 6408,
3956
+ gl_NEAREST = 9728,
3957
+ gl_LINEAR = 9729,
3958
+ gl_TEXTURE_MAG_FILTER = 10240,
3959
+ gl_TEXTURE_MIN_FILTER = 10241,
3960
+ gl_TEXTURE_WRAP_S = 10242,
3961
+ gl_TEXTURE_WRAP_T = 10243,
3962
+ gl_COLOR_BUFFER_BIT = 16384,
3963
+ gl_CLAMP_TO_EDGE = 33071,
3964
+ gl_TEXTURE0 = 33984,
3965
+ gl_TEXTURE1 = 33985,
3966
+ gl_ARRAY_BUFFER = 34962,
3967
+ gl_STATIC_DRAW = 35044,
3968
+ gl_DYNAMIC_DRAW = 35048,
3969
+ gl_FRAGMENT_SHADER = 35632,
3970
+ gl_VERTEX_SHADER = 35633,
3971
+ gl_COMPILE_STATUS = 35713,
3972
+ gl_LINK_STATUS = 35714,
3973
+ gl_UNPACK_FLIP_Y_WEBGL = 37440,
3841
3974
 
3842
- CryptoJS()
3843
- {
3844
- ///////////////////////////////////////////////////////////////////////////////
3845
- // Crypto-JS - https://github.com/brix/crypto-js - MIT License
3846
- //
3847
- // [The MIT License (MIT)](http://opensource.org/licenses/MIT)
3848
- //
3849
- // Copyright (c) 2009-2013 Jeff Mott
3850
- // Copyright (c) 2013-2016 Evan Vosberg
3851
- //
3852
- // Permission is hereby granted, free of charge, to any person obtaining a copy
3853
- // of this software and associated documentation files (the "Software"), to deal
3854
- // in the Software without restriction, including without limitation the rights
3855
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3856
- // copies of the Software, and to permit persons to whom the Software is
3857
- // furnished to do so, subject to the following conditions:
3858
- //
3859
- // The above copyright notice and this permission notice shall be included in
3860
- // all copies or substantial portions of the Software.
3861
- //
3862
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3863
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3864
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3865
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3866
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3867
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
3868
- // THE SOFTWARE.
3869
- return eval(Function("[M='GBMGXz^oVYPPKKbB`agTXU|LxPc_ZBcMrZvCr~wyGfWrwk@ATqlqeTp^N?p{we}jIpEnB_sEr`l?YDkDhWhprc|Er|XETG?pTl`e}dIc[_N~}fzRycIfpW{HTolvoPB_FMe_eH~BTMx]yyOhv?biWPCGc]kABencBhgERHGf{OL`Dj`c^sh@canhy[secghiyotcdOWgO{tJIE^JtdGQRNSCrwKYciZOa]Y@tcRATYKzv|sXpboHcbCBf`}SKeXPFM|RiJsSNaIb]QPc[D]Jy_O^XkOVTZep`ONmntLL`Qz~UupHBX_Ia~WX]yTRJIxG`ioZ{fefLJFhdyYoyLPvqgH?b`[TMnTwwfzDXhfM?rKs^aFr|nyBdPmVHTtAjXoYUloEziWDCw_suyYT~lSMksI~ZNCS[Bex~j]Vz?kx`gdYSEMCsHpjbyxQvw|XxX_^nQYue{sBzVWQKYndtYQMWRef{bOHSfQhiNdtR{o?cUAHQAABThwHPT}F{VvFmgN`E@FiFYS`UJmpQNM`X|tPKHlccT}z}k{sACHL?Rt@MkWplxO`ASgh?hBsuuP|xD~LSH~KBlRs]t|l|_tQAroDRqWS^SEr[sYdPB}TAROtW{mIkE|dWOuLgLmJrucGLpebrAFKWjikTUzS|j}M}szasKOmrjy[?hpwnEfX[jGpLt@^v_eNwSQHNwtOtDgWD{rk|UgASs@mziIXrsHN_|hZuxXlPJOsA^^?QY^yGoCBx{ekLuZzRqQZdsNSx@ezDAn{XNj@fRXIwrDX?{ZQHwTEfu@GhxDOykqts|n{jOeZ@c`dvTY?e^]ATvWpb?SVyg]GC?SlzteilZJAL]mlhLjYZazY__qcVFYvt@|bIQnSno@OXyt]OulzkWqH`rYFWrwGs`v|~XeTsIssLrbmHZCYHiJrX}eEzSssH}]l]IhPQhPoQ}rCXLyhFIT[clhzYOvyHqigxmjz`phKUU^TPf[GRAIhNqSOdayFP@FmKmuIzMOeoqdpxyCOwCthcLq?n`L`tLIBboNn~uXeFcPE{C~mC`h]jUUUQe^`UqvzCutYCgct|SBrAeiYQW?X~KzCz}guXbsUw?pLsg@hDArw?KeJD[BN?GD@wgFWCiHq@Ypp_QKFixEKWqRp]oJFuVIEvjDcTFu~Zz]a{IcXhWuIdMQjJ]lwmGQ|]g~c]Hl]pl`Pd^?loIcsoNir_kikBYyg?NarXZEGYspt_vLBIoj}LI[uBFvm}tbqvC|xyR~a{kob|HlctZslTGtPDhBKsNsoZPuH`U`Fqg{gKnGSHVLJ^O`zmNgMn~{rsQuoymw^JY?iUBvw_~mMr|GrPHTERS[MiNpY[Mm{ggHpzRaJaoFomtdaQ_?xuTRm}@KjU~RtPsAdxa|uHmy}n^i||FVL[eQAPrWfLm^ndczgF~Nk~aplQvTUpHvnTya]kOenZlLAQIm{lPl@CCTchvCF[fI{^zPkeYZTiamoEcKmBMfZhk_j_~Fjp|wPVZlkh_nHu]@tP|hS@^G^PdsQ~f[RqgTDqezxNFcaO}HZhb|MMiNSYSAnQWCDJukT~e|OTgc}sf[cnr?fyzTa|EwEtRG|I~|IO}O]S|rp]CQ}}DWhSjC_|z|oY|FYl@WkCOoPuWuqr{fJu?Brs^_EBI[@_OCKs}?]O`jnDiXBvaIWhhMAQDNb{U`bqVR}oqVAvR@AZHEBY@depD]OLh`kf^UsHhzKT}CS}HQKy}Q~AeMydXPQztWSSzDnghULQgMAmbWIZ|lWWeEXrE^EeNoZApooEmrXe{NAnoDf`m}UNlRdqQ@jOc~HLOMWs]IDqJHYoMziEedGBPOxOb?[X`KxkFRg@`mgFYnP{hSaxwZfBQqTm}_?RSEaQga]w[vxc]hMne}VfSlqUeMo_iqmd`ilnJXnhdj^EEFifvZyxYFRf^VaqBhLyrGlk~qowqzHOBlOwtx?i{m~`n^G?Yxzxux}b{LSlx]dS~thO^lYE}bzKmUEzwW^{rPGhbEov[Plv??xtyKJshbG`KuO?hjBdS@Ru}iGpvFXJRrvOlrKN?`I_n_tplk}kgwSXuKylXbRQ]]?a|{xiT[li?k]CJpwy^o@ebyGQrPfF`aszGKp]baIx~H?ElETtFh]dz[OjGl@C?]VDhr}OE@V]wLTc[WErXacM{We`F|utKKjgllAxvsVYBZ@HcuMgLboFHVZmi}eIXAIFhS@A@FGRbjeoJWZ_NKd^oEH`qgy`q[Tq{x?LRP|GfBFFJV|fgZs`MLbpPYUdIV^]mD@FG]pYAT^A^RNCcXVrPsgk{jTrAIQPs_`mD}rOqAZA[}RETFz]WkXFTz_m{N@{W@_fPKZLT`@aIqf|L^Mb|crNqZ{BVsijzpGPEKQQZGlApDn`ruH}cvF|iXcNqK}cxe_U~HRnKV}sCYb`D~oGvwG[Ca|UaybXea~DdD~LiIbGRxJ_VGheI{ika}KC[OZJLn^IBkPrQj_EuoFwZ}DpoBRcK]Q}?EmTv~i_Tul{bky?Iit~tgS|o}JL_VYcCQdjeJ_MfaA`FgCgc[Ii|CBHwq~nbJeYTK{e`CNstKfTKPzw{jdhp|qsZyP_FcugxCFNpKitlR~vUrx^NrSVsSTaEgnxZTmKc`R|lGJeX}ccKLsQZQhsFkeFd|ckHIVTlGMg`~uPwuHRJS_CPuN_ogXe{Ba}dO_UBhuNXby|h?JlgBIqMKx^_u{molgL[W_iavNQuOq?ap]PGB`clAicnl@k~pA?MWHEZ{HuTLsCpOxxrKlBh]FyMjLdFl|nMIvTHyGAlPogqfZ?PlvlFJvYnDQd}R@uAhtJmDfe|iJqdkYr}r@mEjjIetDl_I`TELfoR|qTBu@Tic[BaXjP?dCS~MUK[HPRI}OUOwAaf|_}HZzrwXvbnNgltjTwkBE~MztTQhtRSWoQHajMoVyBBA`kdgK~h`o[J`dm~pm]tk@i`[F~F]DBlJKklrkR]SNw@{aG~Vhl`KINsQkOy?WhcqUMTGDOM_]bUjVd|Yh_KUCCgIJ|LDIGZCPls{RzbVWVLEhHvWBzKq|^N?DyJB|__aCUjoEgsARki}j@DQXS`RNU|DJ^a~d{sh_Iu{ONcUtSrGWW@cvUjefHHi}eSSGrNtO?cTPBShLqzwMVjWQQCCFB^culBjZHEK_{dO~Q`YhJYFn]jq~XSnG@[lQr]eKrjXpG~L^h~tDgEma^AUFThlaR{xyuP@[^VFwXSeUbVetufa@dX]CLyAnDV@Bs[DnpeghJw^?UIana}r_CKGDySoRudklbgio}kIDpA@McDoPK?iYcG?_zOmnWfJp}a[JLR[stXMo?_^Ng[whQlrDbrawZeSZ~SJstIObdDSfAA{MV}?gNunLOnbMv_~KFQUAjIMj^GkoGxuYtYbGDImEYiwEMyTpMxN_LSnSMdl{bg@dtAnAMvhDTBR_FxoQgANniRqxd`pWv@rFJ|mWNWmh[GMJz_Nq`BIN@KsjMPASXORcdHjf~rJfgZYe_uulzqM_KdPlMsuvU^YJuLtofPhGonVOQxCMuXliNvJIaoC?hSxcxKVVxWlNs^ENDvCtSmO~WxI[itnjs^RDvI@KqG}YekaSbTaB]ki]XM@[ZnDAP~@|BzLRgOzmjmPkRE@_sobkT|SszXK[rZN?F]Z_u}Yue^[BZgLtR}FHzWyxWEX^wXC]MJmiVbQuBzkgRcKGUhOvUc_bga|Tx`KEM`JWEgTpFYVeXLCm|mctZR@uKTDeUONPozBeIkrY`cz]]~WPGMUf`MNUGHDbxZuO{gmsKYkAGRPqjc|_FtblEOwy}dnwCHo]PJhN~JoteaJ?dmYZeB^Xd?X^pOKDbOMF@Ugg^hETLdhwlA}PL@_ur|o{VZosP?ntJ_kG][g{Zq`Tu]dzQlSWiKfnxDnk}KOzp~tdFstMobmy[oPYjyOtUzMWdjcNSUAjRuqhLS@AwB^{BFnqjCmmlk?jpn}TksS{KcKkDboXiwK]qMVjm~V`LgWhjS^nLGwfhAYrjDSBL_{cRus~{?xar_xqPlArrYFd?pHKdMEZzzjJpfC?Hv}mAuIDkyBxFpxhstTx`IO{rp}XGuQ]VtbHerlRc_LFGWK[XluFcNGUtDYMZny[M^nVKVeMllQI[xtvwQnXFlWYqxZZFp_|]^oWX[{pOMpxXxvkbyJA[DrPzwD|LW|QcV{Nw~U^dgguSpG]ClmO@j_TENIGjPWwgdVbHganhM?ema|dBaqla|WBd`poj~klxaasKxGG^xbWquAl~_lKWxUkDFagMnE{zHug{b`A~IYcQYBF_E}wiA}K@yxWHrZ{[d~|ARsYsjeNWzkMs~IOqqp[yzDE|WFrivsidTcnbHFRoW@XpAV`lv_zj?B~tPCppRjgbbDTALeFaOf?VcjnKTQMLyp{NwdylHCqmo?oelhjWuXj~}{fpuX`fra?GNkDiChYgVSh{R[BgF~eQa^WVz}ATI_CpY?g_diae]|ijH`TyNIF}|D_xpmBq_JpKih{Ba|sWzhnAoyraiDvk`h{qbBfsylBGmRH}DRPdryEsSaKS~tIaeF[s]I~xxHVrcNe@Jjxa@jlhZueLQqHh_]twVMqG_EGuwyab{nxOF?`HCle}nBZzlTQjkLmoXbXhOtBglFoMz?eqre`HiE@vNwBulglmQjj]DB@pPkPUgA^sjOAUNdSu_`oAzar?n?eMnw{{hYmslYi[TnlJD'",...']charCodeAtUinyxpf',"for(;e<10359;c[e++]=p-=128,A=A?p-A&&A:p==34&&p)for(p=1;p<128;y=f.map((n,x)=>(U=r[n]*2+1,U=Math.log(U/(h-U)),t-=a[x]*U,U/500)),t=~-h/(1+Math.exp(t))|1,i=o%h<t,o=o%h+(i?t:h-t)*(o>>17)-!i*t,f.map((n,x)=>(U=r[n]+=(i*h/2-r[n]<<13)/((C[n]+=C[n]<5)+1/20)>>13,a[x]+=y[x]*(i-t/h))),p=p*2+i)for(f='010202103203210431053105410642065206541'.split(t=0).map((n,x)=>(U=0,[...n].map((n,x)=>(U=U*997+(c[e-n]|0)|0)),h*32-1&U*997+p+!!A*129)*12+x);o<h*32;o=o*64|M.charCodeAt(d++)&63);for(C=String.fromCharCode(...c);r=/[\0-#?@\\\\~]/.exec(C);)with(C.split(r))C=join(shift());return C")([],[],1<<17,[0,0,0,0,0,0,0,0,0,0,0,0],new Uint16Array(51e6).fill(1<<15),new Uint8Array(51e6),0,0,0,0));
3870
- // end of Crypto-JS
3871
- ///////////////////////////////////////////////////////////////////////////////
3872
- }
3873
- }
3975
+ // constants for batch rendering
3976
+ gl_VERTICES_PER_QUAD = 6,
3977
+ gl_INDICIES_PER_VERT = 6,
3978
+ gl_MAX_BATCH = 1<<16,
3979
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
3980
+ gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;
3874
3981
  /**
3875
- * LittleJS WebGL Interface
3876
- * - All webgl used by the engine is wrapped up here
3877
- * - For normal stuff you won't need to see or call anything in this file
3878
- * - For advanced stuff there are helper functions to create shaders, textures, etc
3879
- * - Can be disabled with glEnable to revert to 2D canvas rendering
3880
- * - Batches sprite rendering on GPU for incredibly fast performance
3881
- * - Sprite transform math is done in the shader where possible
3882
- * @namespace WebGL
3982
+ * LittleJS - The Tiny JavaScript Game Engine That Can!
3983
+ * MIT License - Copyright 2021 Frank Force
3984
+ *
3985
+ * Engine Features
3986
+ * - Object oriented system with base class engine object
3987
+ * - Base class object handles update, physics, collision, rendering, etc
3988
+ * - Engine helper classes and functions like Vector2, Color, and Timer
3989
+ * - Super fast rendering system for tile sheets
3990
+ * - Sound effects audio with zzfx and music with zzfxm
3991
+ * - Input processing system with gamepad and touchscreen support
3992
+ * - Tile layer rendering and collision system
3993
+ * - Particle effect system
3994
+ * - Medal system tracks and displays achievements
3995
+ * - Debug tools and debug rendering system
3996
+ * - Post processing effects
3997
+ * - Call engineInit() to start it up!
3998
+ * @namespace Engine
3883
3999
  */
3884
4000
 
3885
4001
  'use strict';
3886
4002
 
3887
- /** The WebGL canvas which appears above the main canvas and below the overlay canvas
3888
- * @type {HTMLCanvasElement}
3889
- * @memberof WebGL */
3890
- let glCanvas;
4003
+ /** Name of engine
4004
+ * @type {String}
4005
+ * @default
4006
+ * @memberof Engine */
4007
+ const engineName = 'LittleJS';
3891
4008
 
3892
- /** 2d context for glCanvas
3893
- * @type {WebGLRenderingContext}
3894
- * @memberof WebGL */
3895
- let glContext;
4009
+ /** Version of engine
4010
+ * @type {String}
4011
+ * @default
4012
+ * @memberof Engine */
4013
+ const engineVersion = '1.6.92';
3896
4014
 
3897
- /** Main tile sheet texture automatically loaded by engine
3898
- * @type {WebGLTexture}
3899
- * @memberof WebGL */
3900
- let glTileTexture;
4015
+ /** Frames per second to update objects
4016
+ * @type {Number}
4017
+ * @default
4018
+ * @memberof Engine */
4019
+ const frameRate = 60;
3901
4020
 
3902
- // WebGL internal variables not exposed to documentation
3903
- let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
4021
+ /** How many seconds each frame lasts, engine uses a fixed time step
4022
+ * @type {Number}
4023
+ * @default 1/60
4024
+ * @memberof Engine */
4025
+ const timeDelta = 1/frameRate;
3904
4026
 
3905
- ///////////////////////////////////////////////////////////////////////////////
4027
+ /** Array containing all engine objects
4028
+ * @type {Array}
4029
+ * @memberof Engine */
4030
+ let engineObjects = [];
3906
4031
 
3907
- // Init WebGL, called automatically by the engine
3908
- function glInit()
3909
- {
3910
- // create the canvas and tile texture
3911
- glCanvas = document.createElement('canvas');
3912
- glContext = glCanvas.getContext('webgl', {antialias: false});
3913
- glTileTexture = glCreateTexture(tileImage);
4032
+ /** Array containing only objects that are set to collide with other objects this frame (for optimization)
4033
+ * @type {Array}
4034
+ * @memberof Engine */
4035
+ let engineObjectsCollide = [];
3914
4036
 
3915
- // some browsers are much faster without copying the gl buffer so we just overlay it instead
3916
- glOverlay && document.body.appendChild(glCanvas);
4037
+ /** Current update frame, used to calculate time
4038
+ * @type {Number}
4039
+ * @memberof Engine */
4040
+ let frame = 0;
3917
4041
 
3918
- // setup vertex and fragment shaders
3919
- glShader = glCreateProgram(
3920
- 'precision highp float;'+ // use highp for better accuracy
3921
- 'uniform mat4 m;'+ // transform matrix
3922
- 'attribute vec2 p,t;'+ // position, uv
3923
- 'attribute vec4 c,a;'+ // color, additiveColor
3924
- 'varying vec4 v,d,e;'+ // return uv, color, additiveColor
3925
- 'void main(){'+ // shader entry point
3926
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
3927
- 'v=vec4(t,p);d=c;e=a;'+ // pass stuff to fragment shader
3928
- '}' // end of shader
3929
- ,
3930
- 'precision highp float;'+ // use highp for better accuracy
3931
- 'varying vec4 v,d,e;'+ // uv, color, additiveColor
3932
- 'uniform sampler2D s;'+ // texture
3933
- 'void main(){'+ // shader entry point
3934
- 'gl_FragColor=texture2D(s,v.xy)*d+e;'+ // modulate texture by color plus additive
3935
- '}' // end of shader
3936
- );
4042
+ /** Current engine time since start in seconds, derived from frame
4043
+ * @type {Number}
4044
+ * @memberof Engine */
4045
+ let time = 0;
3937
4046
 
3938
- // init buffers
3939
- const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
3940
- glArrayBuffer = glContext.createBuffer();
3941
- glPositionData = new Float32Array(vertexData);
3942
- glColorData = new Uint32Array(vertexData);
3943
- glBatchCount = 0;
3944
- }
4047
+ /** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
4048
+ * @type {Number}
4049
+ * @memberof Engine */
4050
+ let timeReal = 0;
3945
4051
 
3946
- /** Set the WebGl blend mode, normally you should call setBlendMode instead
3947
- * @param {Boolean} [additive=0]
3948
- * @memberof WebGL */
3949
- function glSetBlendMode(additive)
3950
- {
3951
- // setup blending
3952
- glAdditive = additive;
3953
- }
4052
+ /** Is the game paused? Causes time and objects to not be updated
4053
+ * @type {Boolean}
4054
+ * @default 0
4055
+ * @memberof Engine */
4056
+ let paused = 0;
3954
4057
 
3955
- /** Set the WebGl texture, not normally necessary unless multiple tile sheets are used
3956
- * - This may also flush the gl buffer resulting in more draw calls and worse performance
3957
- * @param {WebGLTexture} [texture=glTileTexture]
3958
- * @memberof WebGL */
3959
- function glSetTexture(texture=glTileTexture)
3960
- {
3961
- // must flush cache with the old texture to set a new one
3962
- if (texture != glActiveTexture)
3963
- glFlush();
4058
+ /** Set if game is paused
4059
+ * @param {Boolean} paused
4060
+ * @memberof Engine */
4061
+ function setPaused(_paused) { paused = _paused; }
3964
4062
 
3965
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = texture);
3966
- }
4063
+ ///////////////////////////////////////////////////////////////////////////////
3967
4064
 
3968
- /** Compile WebGL shader of the given type, will throw errors if in debug mode
3969
- * @param {String} source
3970
- * @param type
3971
- * @return {WebGLShader}
3972
- * @memberof WebGL */
3973
- function glCompileShader(source, type)
4065
+ /** Start up LittleJS engine with your callback functions
4066
+ * @param {Function} gameInit - Called once after the engine starts up, setup the game
4067
+ * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
4068
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
4069
+ * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
4070
+ * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
4071
+ * @param {String} [tileImageSource] - Tile image to use, everything starts when the image is finished loading
4072
+ * @memberof Engine */
4073
+ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource)
3974
4074
  {
3975
- // build the shader
3976
- const shader = glContext.createShader(type);
3977
- glContext.shaderSource(shader, source);
3978
- glContext.compileShader(shader);
4075
+ // init engine when tiles load or fail to load
4076
+ tileImage.onerror = tileImage.onload = ()=>
4077
+ {
4078
+ // save tile image info
4079
+ tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
4080
+ debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
3979
4081
 
3980
- // check for errors
3981
- if (debug && !glContext.getShaderParameter(shader, gl_COMPILE_STATUS))
3982
- throw glContext.getShaderInfoLog(shader);
3983
- return shader;
3984
- }
4082
+ // setup html
4083
+ const styleBody = 'margin:0;overflow:hidden;' + // fill the window
4084
+ 'background:#000;' + // set background color
4085
+ 'touch-action:none;' + // prevent mobile pinch to resize
4086
+ 'user-select:none;' + // prevent mobile hold to select
4087
+ '-webkit-user-select:none'; // compatibility for ios
4088
+ document.body.style = styleBody;
4089
+ document.body.appendChild(mainCanvas = document.createElement('canvas'));
4090
+ mainContext = mainCanvas.getContext('2d');
3985
4091
 
3986
- /** Create WebGL program with given shaders
3987
- * @param {WebGLShader} vsSource
3988
- * @param {WebGLShader} fsSource
3989
- * @return {WebGLProgram}
3990
- * @memberof WebGL */
3991
- function glCreateProgram(vsSource, fsSource)
3992
- {
3993
- // build the program
3994
- const program = glContext.createProgram();
3995
- glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
3996
- glContext.attachShader(program, glCompileShader(fsSource, gl_FRAGMENT_SHADER));
3997
- glContext.linkProgram(program);
4092
+ // init stuff and start engine
4093
+ debugInit();
4094
+ glEnable && glInit();
3998
4095
 
3999
- // check for errors
4000
- if (debug && !glContext.getProgramParameter(program, gl_LINK_STATUS))
4001
- throw glContext.getProgramInfoLog(program);
4002
- return program;
4003
- }
4096
+ // create overlay canvas for hud to appear above gl canvas
4097
+ document.body.appendChild(overlayCanvas = document.createElement('canvas'));
4098
+ overlayContext = overlayCanvas.getContext('2d');
4004
4099
 
4005
- /** Create WebGL texture from an image and set the texture settings
4006
- * @param {Image} image
4007
- * @return {WebGLTexture}
4008
- * @memberof WebGL */
4009
- function glCreateTexture(image)
4010
- {
4011
- // build the texture
4012
- const texture = glContext.createTexture();
4013
- glContext.bindTexture(gl_TEXTURE_2D, texture);
4014
- image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
4100
+ // set canvas style
4101
+ const styleCanvas = 'position:absolute;' +
4102
+ 'top:50%;left:50%;transform:translate(-50%,-50%);' + // center the canvas
4103
+ (canvasPixelated?'image-rendering:pixelated':''); // set pixelated rendering
4104
+ (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
4015
4105
 
4016
- // use point filtering for pixelated rendering
4017
- const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4018
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4019
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4020
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
4021
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
4022
- return texture;
4023
- }
4106
+ gameInit();
4107
+ engineUpdate();
4108
+ };
4024
4109
 
4025
- // called automatically by engine before render
4026
- function glPreRender()
4027
- {
4028
- // clear and set to same size as main canvas
4029
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4030
- glContext.clear(gl_COLOR_BUFFER_BIT);
4110
+ // frame time tracking
4111
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS;
4031
4112
 
4032
- // set up the shader
4033
- glContext.useProgram(glShader);
4034
- glContext.activeTexture(gl_TEXTURE0);
4035
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
4036
- glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
4037
- glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
4038
- glSetBlendMode();
4039
-
4040
- // set vertex attributes
4041
- let offset = 0;
4042
- const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
4113
+ // main update loop
4114
+ function engineUpdate(frameTimeMS=0)
4043
4115
  {
4044
- const location = glContext.getAttribLocation(glShader, name);
4045
- glContext.enableVertexAttribArray(location);
4046
- glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
4047
- offset += size*typeSize;
4116
+ // update time keeping
4117
+ let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4118
+ frameTimeLastMS = frameTimeMS;
4119
+ if (debug || showWatermark)
4120
+ averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
4121
+ const debugSpeedUp = debug && keyIsDown(107); // +
4122
+ const debugSpeedDown = debug && keyIsDown(109); // -
4123
+ if (debug) // +/- to speed/slow time
4124
+ frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
4125
+ timeReal += frameTimeDeltaMS / 1e3;
4126
+ frameTimeBufferMS += !paused * frameTimeDeltaMS;
4127
+ if (!debugSpeedUp)
4128
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
4129
+
4130
+ if (canvasFixedSize.x)
4131
+ {
4132
+ // clear canvas and set fixed size
4133
+ mainCanvas.width = canvasFixedSize.x;
4134
+ mainCanvas.height = canvasFixedSize.y;
4135
+
4136
+ // fit to window by adding space on top or bottom if necessary
4137
+ const aspect = innerWidth / innerHeight;
4138
+ const fixedAspect = mainCanvas.width / mainCanvas.height;
4139
+ (glCanvas||mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
4140
+ (glCanvas||mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
4141
+ }
4142
+ else
4143
+ {
4144
+ // clear canvas and set size to same as window
4145
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
4146
+ mainCanvas.height = min(innerHeight, canvasMaxSize.y);
4147
+ }
4148
+
4149
+ // clear overlay canvas and set size
4150
+ overlayCanvas.width = mainCanvas.width;
4151
+ overlayCanvas.height = mainCanvas.height;
4152
+
4153
+ // save canvas size
4154
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4155
+
4156
+ if (paused)
4157
+ {
4158
+ // do post update even when paused
4159
+ inputUpdate();
4160
+ debugUpdate();
4161
+ gameUpdatePost();
4162
+ inputUpdatePost();
4163
+ }
4164
+ else
4165
+ {
4166
+ // apply time delta smoothing, improves smoothness of framerate in some browsers
4167
+ let deltaSmooth = 0;
4168
+ if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
4169
+ {
4170
+ // force an update each frame if time is close enough (not just a fast refresh rate)
4171
+ deltaSmooth = frameTimeBufferMS;
4172
+ frameTimeBufferMS = 0;
4173
+ }
4174
+
4175
+ // update multiple frames if necessary in case of slow framerate
4176
+ for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
4177
+ {
4178
+ // update game and objects
4179
+ inputUpdate();
4180
+ gameUpdate();
4181
+ engineObjectsUpdate();
4182
+
4183
+ // do post update
4184
+ debugUpdate();
4185
+ gameUpdatePost();
4186
+ inputUpdatePost();
4187
+ }
4188
+
4189
+ // add the time smoothing back in
4190
+ frameTimeBufferMS += deltaSmooth;
4191
+ }
4192
+
4193
+ // render sort then render while removing destroyed objects
4194
+ enginePreRender();
4195
+ gameRender();
4196
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4197
+ for (const o of engineObjects)
4198
+ o.destroyed || o.render();
4199
+ gameRenderPost();
4200
+ glRenderPostProcess();
4201
+ medalsRender();
4202
+ touchGamepadRender();
4203
+ debugRender();
4204
+ glEnable && glCopyToContext(mainContext);
4205
+
4206
+ if (showWatermark)
4207
+ {
4208
+ // update fps
4209
+ overlayContext.textAlign = 'right';
4210
+ overlayContext.textBaseline = 'top';
4211
+ overlayContext.font = '1em monospace';
4212
+ overlayContext.fillStyle = '#000';
4213
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
4214
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4215
+ + (glEnable ? ' GL' : ' 2D') ;
4216
+ overlayContext.fillText(text, mainCanvas.width-3, 3);
4217
+ overlayContext.fillStyle = '#fff';
4218
+ overlayContext.fillText(text, mainCanvas.width-2, 2);
4219
+ drawCount = 0;
4220
+ }
4221
+
4222
+ requestAnimationFrame(engineUpdate);
4048
4223
  }
4049
- initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
4050
- initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
4051
- initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
4052
- initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
4053
4224
 
4054
- // build the transform matrix
4055
- const sx = 2 * cameraScale / mainCanvas.width;
4056
- const sy = 2 * cameraScale / mainCanvas.height;
4057
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), 0,
4058
- new Float32Array([
4059
- sx, 0, 0, 0,
4060
- 0, sy, 0, 0,
4061
- 1, 1, -1, 1,
4062
- -1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
4063
- ])
4064
- );
4225
+ // set tile image source to load the image and start the engine
4226
+ tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
4065
4227
  }
4066
4228
 
4067
- /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
4068
- * @memberof WebGL */
4069
- function glFlush()
4229
+ // Called automatically by engine to setup render system
4230
+ function enginePreRender()
4070
4231
  {
4071
- if (!glBatchCount) return;
4072
-
4073
- const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
4074
- glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
4075
- glContext.enable(gl_BLEND);
4232
+ // save canvas size
4233
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4076
4234
 
4077
- // draw all the sprites in the batch and reset the buffer
4078
- glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
4079
- glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
4080
- glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
4081
- glBatchCount = 0;
4082
- glBatchAdditive = glAdditive;
4083
- }
4235
+ // disable smoothing for pixel art
4236
+ mainContext.imageSmoothingEnabled = !canvasPixelated;
4084
4237
 
4085
- /** Draw any sprites still in the buffer, copy to main canvas and clear
4086
- * @param {CanvasRenderingContext2D} context
4087
- * @param {Boolean} [forceDraw=0]
4088
- * @memberof WebGL */
4089
- function glCopyToContext(context, forceDraw)
4090
- {
4091
- if (!glBatchCount && !forceDraw) return;
4092
-
4093
- glFlush();
4094
-
4095
- // do not draw in overlay mode because the canvas is visible
4096
- if (!glOverlay || forceDraw)
4097
- context.drawImage(glCanvas, 0, 0);
4238
+ // setup gl rendering if enabled
4239
+ glEnable && glPreRender();
4098
4240
  }
4099
4241
 
4100
- /** Add a sprite to the gl draw list, used by all gl draw functions
4101
- * @param x
4102
- * @param y
4103
- * @param sizeX
4104
- * @param sizeY
4105
- * @param angle
4106
- * @param uv0X
4107
- * @param uv0Y
4108
- * @param uv1X
4109
- * @param uv1Y
4110
- * @param rgba
4111
- * @param [rgbaAdditive=0]
4112
- * @memberof WebGL */
4113
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4242
+ /** Update each engine object, remove destroyed objects, and update time
4243
+ * @memberof Engine */
4244
+ function engineObjectsUpdate()
4114
4245
  {
4115
- // flush if there is no room for more verts or if different blend mode
4116
- if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
4117
- glFlush();
4246
+ // get list of solid objects for physics optimzation
4247
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
4118
4248
 
4119
- // prepare to create the verts from size and angle
4120
- const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
4121
- const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
4122
-
4123
- // setup 2 triangles to form a quad
4124
- for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
4249
+ // recursive object update
4250
+ function updateObject(o)
4125
4251
  {
4126
- const a = i-4&&i>1, b = i-5&&i-2&&i-1;
4127
- glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
4128
- glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
4129
- glPositionData[offset++] = a ? uv0X : uv1X;
4130
- glPositionData[offset++] = b ? uv0Y : uv1Y;
4131
- glColorData[offset++] = rgba;
4132
- glColorData[offset++] = rgbaAdditive;
4252
+ if (!o.destroyed)
4253
+ {
4254
+ o.update();
4255
+ for (const child of o.children)
4256
+ updateObject(child);
4257
+ }
4133
4258
  }
4134
- }
4259
+ for (const o of engineObjects)
4260
+ o.parent || updateObject(o);
4135
4261
 
4136
- ///////////////////////////////////////////////////////////////////////////////
4137
- // post processing - can be enabled to pass other canvases through a final shader
4262
+ // remove destroyed objects
4263
+ engineObjects = engineObjects.filter(o=>!o.destroyed);
4138
4264
 
4139
- let glPostShader, glPostArrayBuffer, glPostTexture, glPostIncludeOverlay;
4265
+ // increment frame and update time
4266
+ time = ++frame / frameRate;
4267
+ }
4140
4268
 
4141
- /** Set up a post processing shader
4142
- * @param {String} shaderCode
4143
- * @param {Boolean} includeOverlay
4144
- * @memberof WebGL */
4145
- function glInitPostProcess(shaderCode, includeOverlay)
4269
+ /** Destroy and remove all objects
4270
+ * @memberof Engine */
4271
+ function engineObjectsDestroy()
4146
4272
  {
4147
- ASSERT(!glPostShader); // can only have 1 post effects shader
4148
-
4149
- if (!shaderCode) // default shader
4150
- shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture2D(iChannel0,p/iResolution.xy);}';
4151
-
4152
- // create the shader
4153
- glPostShader = glCreateProgram(
4154
- 'precision highp float;'+ // use highp for better accuracy
4155
- 'attribute vec2 p;'+ // position
4156
- 'void main(){'+ // shader entry point
4157
- 'gl_Position=vec4(p,1,1);'+ // set position
4158
- '}' // end of shader
4159
- ,
4160
- 'precision highp float;'+ // use highp for better accuracy
4161
- 'uniform sampler2D iChannel0;'+ // input texture
4162
- 'uniform vec3 iResolution;'+ // size of output texture
4163
- 'uniform float iTime;'+ // time passed
4164
- '\n' + shaderCode + '\n'+ // insert custom shader code
4165
- 'void main(){'+ // shader entry point
4166
- 'mainImage(gl_FragColor,gl_FragCoord.xy);'+ // call post process function
4167
- 'gl_FragColor.a=1.;'+ // always use full alpha
4168
- '}' // end of shader
4169
- );
4170
-
4171
- // create buffer and texture
4172
- glPostArrayBuffer = glContext.createBuffer();
4173
- glPostTexture = glCreateTexture();
4174
- glPostIncludeOverlay = includeOverlay;
4175
-
4176
- // hide the original 2d canvas
4177
- mainCanvas.style.visibility = 'hidden';
4273
+ for (const o of engineObjects)
4274
+ o.parent || o.destroy();
4275
+ engineObjects = engineObjects.filter(o=>!o.destroyed);
4178
4276
  }
4179
4277
 
4180
- // Render the post processing shader, called automatically by the engine
4181
- function glRenderPostProcess()
4278
+ /** Triggers a callback for each object within a given area
4279
+ * @param {Vector2} [pos] - Center of test area
4280
+ * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
4281
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
4282
+ * @param {Array} [objects=engineObjects] - List of objects to check
4283
+ * @memberof Engine */
4284
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
4182
4285
  {
4183
- if (!glPostShader)
4184
- return;
4185
-
4186
- // prepare to render post process shader
4187
- if (glEnable)
4286
+ if (!pos) // all objects
4188
4287
  {
4189
- glFlush(); // clear out the buffer
4190
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
4288
+ for (const o of objects)
4289
+ callbackFunction(o);
4191
4290
  }
4192
- else // set viewport
4193
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4194
-
4195
- if (glPostIncludeOverlay)
4291
+ else if (size.x != undefined) // bounding box test
4196
4292
  {
4197
- // copy overlay canvas so it will be included in post processing
4198
- mainContext.drawImage(overlayCanvas, 0, 0);
4199
-
4200
- // clear overlay canvas
4201
- overlayCanvas.width = mainCanvas.width;
4293
+ for (const o of objects)
4294
+ isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
4202
4295
  }
4203
-
4204
- // setup shader program to draw one triangle
4205
- glContext.useProgram(glPostShader);
4206
- glContext.disable(gl_BLEND);
4207
- glContext.bindBuffer(gl_ARRAY_BUFFER, glPostArrayBuffer);
4208
- glContext.bufferData(gl_ARRAY_BUFFER, new Float32Array([-3,1,1,-3,1,1]), gl_STATIC_DRAW);
4209
- glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, true);
4210
-
4211
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
4212
- glContext.activeTexture(gl_TEXTURE0);
4213
- glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
4214
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
4215
-
4216
- // set vertex position attribute
4217
- const vertexByteStride = 8;
4218
- const pLocation = glContext.getAttribLocation(glPostShader, 'p');
4219
- glContext.enableVertexAttribArray(pLocation);
4220
- glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, 0, vertexByteStride, 0);
4221
-
4222
- // set uniforms and draw
4223
- const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4224
- glContext.uniform1i(uniformLocation('iChannel0'), 0);
4225
- glContext.uniform1f(uniformLocation('iTime'), time);
4226
- glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4227
- glContext.drawArrays(gl_TRIANGLES, 0, 3);
4228
- }
4229
-
4230
- ///////////////////////////////////////////////////////////////////////////////
4231
- // store gl constants as integers so their name doesn't use space in minifed
4232
- const
4233
- gl_ONE = 1,
4234
- gl_TRIANGLES = 4,
4235
- gl_SRC_ALPHA = 770,
4236
- gl_ONE_MINUS_SRC_ALPHA = 771,
4237
- gl_BLEND = 3042,
4238
- gl_TEXTURE_2D = 3553,
4239
- gl_UNSIGNED_BYTE = 5121,
4240
- gl_BYTE = 5120,
4241
- gl_FLOAT = 5126,
4242
- gl_RGBA = 6408,
4243
- gl_NEAREST = 9728,
4244
- gl_LINEAR = 9729,
4245
- gl_TEXTURE_MAG_FILTER = 10240,
4246
- gl_TEXTURE_MIN_FILTER = 10241,
4247
- gl_TEXTURE_WRAP_S = 10242,
4248
- gl_TEXTURE_WRAP_T = 10243,
4249
- gl_COLOR_BUFFER_BIT = 16384,
4250
- gl_CLAMP_TO_EDGE = 33071,
4251
- gl_TEXTURE0 = 33984,
4252
- gl_TEXTURE1 = 33985,
4253
- gl_ARRAY_BUFFER = 34962,
4254
- gl_STATIC_DRAW = 35044,
4255
- gl_DYNAMIC_DRAW = 35048,
4256
- gl_FRAGMENT_SHADER = 35632,
4257
- gl_VERTEX_SHADER = 35633,
4258
- gl_COMPILE_STATUS = 35713,
4259
- gl_LINK_STATUS = 35714,
4260
- gl_UNPACK_FLIP_Y_WEBGL = 37440,
4261
-
4262
- // constants for batch rendering
4263
- gl_VERTICES_PER_QUAD = 6,
4264
- gl_INDICIES_PER_VERT = 6,
4265
- gl_MAX_BATCH = 1<<16,
4266
- gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
4267
- gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;
4296
+ else // circle test
4297
+ {
4298
+ const sizeSquared = size*size;
4299
+ for (const o of objects)
4300
+ pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
4301
+ }
4302
+ }