littlejsengine 1.4.0 → 1.4.6

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