littlejsengine 1.1.26 → 1.2.9

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/README.md +7 -9
  2. package/build.bat +1 -1
  3. package/engine/engine.all.js +369 -377
  4. package/engine/engine.all.min.js +1 -1
  5. package/engine/engine.all.release.js +338 -147
  6. package/engine/engine.js +7 -7
  7. package/engine/engineAudio.js +2 -2
  8. package/engine/engineBuild.bat +9 -1
  9. package/engine/engineDebug.js +32 -231
  10. package/engine/engineDraw.js +130 -20
  11. package/engine/engineFont.png +0 -0
  12. package/engine/engineInput.js +32 -9
  13. package/engine/engineMedals.js +7 -4
  14. package/engine/engineObject.js +30 -11
  15. package/engine/engineParticles.js +5 -3
  16. package/engine/engineSettings.js +5 -0
  17. package/engine/engineTileLayer.js +17 -14
  18. package/engine/engineUtilities.js +50 -18
  19. package/engine/engineWebGL.js +53 -59
  20. package/engine/index.d.ts +1644 -0
  21. package/examples/breakout/game.js +9 -7
  22. package/examples/breakout/gameObjects.js +3 -3
  23. package/examples/breakout/index.html +3 -3
  24. package/examples/particles/index.html +366 -0
  25. package/examples/particles/tiles.png +0 -0
  26. package/examples/platformer/game.js +5 -5
  27. package/examples/platformer/gameEffects.js +10 -10
  28. package/examples/platformer/gameLevel.js +3 -3
  29. package/examples/platformer/gameObjects.js +3 -3
  30. package/examples/platformer/gamePlayer.js +11 -6
  31. package/examples/platformer/index.html +6 -6
  32. package/examples/platformer/tiles.png +0 -0
  33. package/examples/puzzle/game.js +3 -3
  34. package/examples/puzzle/index.html +2 -2
  35. package/examples/screenshot.jpg +0 -0
  36. package/examples/stress/index.html +4 -4
  37. package/game.js +4 -7
  38. package/index.html +13 -13
  39. package/package.json +8 -1
  40. package/engine/engineBuildSetup.bat +0 -6
@@ -7,6 +7,7 @@
7
7
  * LittleJS Debug System
8
8
  * <br> - Press ~ to show debug overlay with mouse pick
9
9
  * <br> - Number keys toggle debug functions
10
+ * <br> - +/- apply time scale
10
11
  * <br> - Debug primitive rendering
11
12
  * <br> - Save a 2d canvas as an image
12
13
  * @namespace Debug
@@ -159,27 +160,27 @@ const debugUpdate = ()=>
159
160
  {
160
161
  if (!debug)
161
162
  return;
162
-
163
+
163
164
  if (keyWasPressed(192)) // ~
164
165
  debugOverlay = !debugOverlay;
165
166
  if (debugOverlay)
166
167
  {
168
+ if (keyWasPressed(48)) // 0
169
+ showWatermark = !showWatermark;
167
170
  if (keyWasPressed(49)) // 1
168
171
  debugPhysics = !debugPhysics, debugParticles = 0;
169
172
  if (keyWasPressed(50)) // 2
170
173
  debugParticles = !debugParticles, debugPhysics = 0;
171
174
  if (keyWasPressed(51)) // 3
175
+ debugGamepads = !debugGamepads;
176
+ if (keyWasPressed(52)) // 4
172
177
  godMode = !godMode;
173
178
  if (keyWasPressed(53)) // 5
174
179
  debugTakeScreenshot = 1;
175
180
  //if (keyWasPressed(54)) // 6
176
- // debugToggleParticleEditor();
177
- if (keyWasPressed(55)) // 7
178
- debugGamepads = !debugGamepads;
181
+ //if (keyWasPressed(55)) // 7
179
182
  //if (keyWasPressed(56)) // 8
180
183
  //if (keyWasPressed(57)) // 9
181
- if (keyWasPressed(48)) // 0
182
- showWatermark = !showWatermark;
183
184
  }
184
185
  }
185
186
 
@@ -200,21 +201,20 @@ const debugRender = ()=>
200
201
 
201
202
  if (debugGamepads && gamepadsEnable && navigator.getGamepads)
202
203
  {
203
- // poll gamepads
204
+ // gamepad debug display
204
205
  const gamepads = navigator.getGamepads();
205
206
  for (let i = gamepads.length; i--;)
206
207
  {
207
208
  const gamepad = gamepads[i];
208
209
  if (gamepad)
209
210
  {
210
- // gamepad debug display
211
211
  const stickScale = 1;
212
212
  const buttonScale = .2;
213
213
  const centerPos = cameraPos;
214
214
  const sticks = stickData[i];
215
215
  for (let j = sticks.length; j--;)
216
216
  {
217
- const drawPos = centerPos.add(vec2(j*stickScale*2,i*stickScale*3));
217
+ const drawPos = centerPos.add(vec2(j*stickScale*2, i*stickScale*3));
218
218
  const stickPos = drawPos.add(sticks[j].scale(stickScale));
219
219
  debugCircle(drawPos, stickScale, '#fff7',0,1);
220
220
  debugLine(drawPos, stickPos, '#f00');
@@ -251,16 +251,14 @@ const debugRender = ()=>
251
251
  bestObject = o
252
252
  }
253
253
 
254
- size.x = max(size.x, .2);
255
- size.y = max(size.y, .2);
256
-
254
+ // show object info
257
255
  const color = new Color(
258
256
  o.collideTiles?1:0,
259
257
  o.collideSolidObjects?1:0,
260
258
  o.isSolid?1:0,
261
259
  o.parent ? .2 : .5);
262
-
263
- // show object info
260
+ size.x = max(size.x, .2);
261
+ size.y = max(size.y, .2);
264
262
  drawRect(o.pos, size, color);
265
263
  drawRect(o.pos, size.scale(.8), o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8));
266
264
  o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5));
@@ -275,17 +273,11 @@ const debugRender = ()=>
275
273
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5));
276
274
  drawLine(mousePos, bestObject.pos, .1, !raycastHitPos ? new Color(0,1,0,.5) : new Color(1,0,0,.5));
277
275
 
278
- let pos = mousePos.copy(), height = vec2(0,.5);
279
- const printVec2 = (v)=> '(' + (v.x>0?' ':'') + (v.x).toFixed(2) + ',' + (v.y>0?' ':'') + (v.y).toFixed(2) + ')';
280
- const args = [.5, new Color, .05, undefined, undefined, 'monospace'];
281
-
282
- drawText('pos = ' + printVec2(bestObject.pos)
283
- + (bestObject.angle>0?' ':' ') + (bestObject.angle*180/PI).toFixed(1) + '°',
284
- pos = pos.add(height), ...args);
285
- drawText('vel = ' + printVec2(bestObject.velocity), pos = pos.add(height), ...args);
286
- drawText('size = ' + printVec2(bestObject.size), pos = pos.add(height), ...args);
287
- drawText('type = ' + ( bestObject.constructor.name), pos = mousePos.subtract(height), ...args);
288
- drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height.scale(2)), ...args);
276
+ const debugText = 'mouse pos = ' + mousePos +
277
+ '\nmouse collision = ' + getTileCollisionData(mousePos) +
278
+ '\n\n--- object info ---\n' +
279
+ bestObject.toString();
280
+ drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, undefined, 'monospace');
289
281
  mainContext = saveContext;
290
282
  }
291
283
 
@@ -293,16 +285,16 @@ const debugRender = ()=>
293
285
  }
294
286
 
295
287
  {
296
- // render debug rects
297
- overlayContext.lineWidth = 1;
288
+ // draw debug primitives
289
+ overlayContext.lineWidth = 2;
298
290
  const pointSize = debugPointSize * cameraScale;
299
291
  debugPrimitives.forEach(p=>
300
292
  {
293
+ overlayContext.save();
294
+
301
295
  // create canvas transform from world space to screen space
302
296
  const pos = worldToScreen(p.pos);
303
297
 
304
- overlayContext.save();
305
- overlayContext.lineWidth = 2;
306
298
  overlayContext.translate(pos.x|0, pos.y|0);
307
299
  overlayContext.rotate(p.angle);
308
300
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
@@ -335,15 +327,17 @@ const debugRender = ()=>
335
327
  p.fill && overlayContext.fill();
336
328
  overlayContext.stroke();
337
329
  }
338
-
330
+
339
331
  overlayContext.restore();
340
332
  });
341
333
 
342
- overlayContext.fillStyle = overlayContext.strokeStyle = '#fff';
334
+ // remove expired pritives
335
+ debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
343
336
  }
344
337
 
345
338
  {
346
- let x = 9, y = -20, h = 30;
339
+ // draw debug overlay
340
+ overlayContext.save();
347
341
  overlayContext.fillStyle = '#fff';
348
342
  overlayContext.textAlign = 'left';
349
343
  overlayContext.textBaseline = 'top';
@@ -351,6 +345,7 @@ const debugRender = ()=>
351
345
  overlayContext.shadowColor = '#000';
352
346
  overlayContext.shadowBlur = 9;
353
347
 
348
+ let x = 9, y = -20, h = 30;
354
349
  if (debugOverlay)
355
350
  {
356
351
  overlayContext.fillText(engineName, x, y += h);
@@ -363,14 +358,12 @@ const debugRender = ()=>
363
358
  overlayContext.fillText('1: Debug Physics', x, y += h);
364
359
  overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
365
360
  overlayContext.fillText('2: Debug Particles', x, y += h);
361
+ overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
362
+ overlayContext.fillText('3: Debug Gamepads', x, y += h);
366
363
  overlayContext.fillStyle = godMode ? '#f00' : '#fff';
367
- overlayContext.fillText('3: God Mode', x, y += h);
364
+ overlayContext.fillText('4: God Mode', x, y += h);
368
365
  overlayContext.fillStyle = '#fff';
369
366
  overlayContext.fillText('5: Save Screenshot', x, y += h);
370
- //overlayContext.fillStyle = debugParticleEditor ? '#f00' : '#fff';
371
- //overlayContext.fillText('6: Particle Editor', x, y += h);
372
- overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
373
- overlayContext.fillText('7: Debug Gamepads', x, y += h);
374
367
 
375
368
  let keysPressed = '';
376
369
  for(const i in inputData[0])
@@ -388,7 +381,6 @@ const debugRender = ()=>
388
381
  buttonsPressed += i + ' ' ;
389
382
  }
390
383
  buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
391
-
392
384
  }
393
385
  else
394
386
  {
@@ -398,200 +390,9 @@ const debugRender = ()=>
398
390
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
399
391
  }
400
392
 
401
- overlayContext.shadowBlur = 0;
393
+ overlayContext.restore();
402
394
  }
403
-
404
- debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
405
395
  }
406
-
407
- ///////////////////////////////////////////////////////////////////////////////
408
- // particle system editor (work in progress)
409
- let debugParticleEditor = 0, debugParticleSystem, debugParticleSystemDiv, particleSystemCode;
410
-
411
- const debugToggleParticleEditor = ()=>
412
- {
413
- debugParticleEditor = !debugParticleEditor;
414
-
415
- if (debugParticleEditor)
416
- {
417
- if (!debugParticleSystem || debugParticleSystem.destroyed)
418
- debugParticleSystem = new ParticleEmitter(cameraPos);
419
- }
420
- else if (debugParticleSystem && !debugParticleSystem.destroyed)
421
- debugParticleSystem.destroy();
422
-
423
-
424
- const colorToHex = (color)=>
425
- {
426
- const componentToHex = (c)=>
427
- {
428
- const hex = (c*255|0).toString(16);
429
- return hex.length == 1 ? '0' + hex : hex;
430
- }
431
-
432
- return '#' + componentToHex(color.r) + componentToHex(color.g) + componentToHex(color.b);
433
- }
434
- const hexToColor = (hex)=>
435
- {
436
- return new Color(
437
- parseInt(hex.substr(1,2), 16)/255,
438
- parseInt(hex.substr(3,2), 16)/255,
439
- parseInt(hex.substr(5,2), 16)/255)
440
- }
441
-
442
- if (!debugParticleSystemDiv)
443
- {
444
- const div = debugParticleSystemDiv = document.createElement('div');
445
- div.innerHTML = '<big><b>Particle Editor';
446
- div.style = 'position:absolute;top:10;left:10;color:#fff';
447
- document.body.appendChild(div);
448
-
449
- for ( const setting of debugParticleSettings)
450
- {
451
- const input = setting[2] = document.createElement('input');
452
- const name = setting[0];
453
- const type = setting[1];
454
- if (type)
455
- {
456
- if (type == 'color')
457
- {
458
- input.type = type;
459
- const color = debugParticleSystem[name];
460
- input.value = colorToHex(color);
461
- }
462
- else if (type == 'alpha' && name == 'colorStartAlpha')
463
- input.value = debugParticleSystem.colorStartA.a;
464
- else if (type == 'alpha' && name == 'colorEndAlpha')
465
- input.value = debugParticleSystem.colorEndA.a;
466
- else if (name == 'tileSizeX')
467
- input.value = debugParticleSystem.tileSize.x;
468
- else if (name == 'tileSizeY')
469
- input.value = debugParticleSystem.tileSize.y;
470
- }
471
- else
472
- input.value = debugParticleSystem[name] || '0';
473
-
474
- input.oninput = (e)=>
475
- {
476
- const inputFloat = parseFloat(input.value) || 0;
477
- if (type)
478
- {
479
- if (type == 'color')
480
- {
481
- const color = hexToColor(input.value);
482
- debugParticleSystem[name].r = color.r;
483
- debugParticleSystem[name].g = color.g;
484
- debugParticleSystem[name].b = color.b;
485
- }
486
- else if (type == 'alpha' && name == 'colorStartAlpha')
487
- {
488
- debugParticleSystem.colorStartA.a = clamp(inputFloat);
489
- debugParticleSystem.colorStartB.a = clamp(inputFloat);
490
- }
491
- else if (type == 'alpha' && name == 'colorEndAlpha')
492
- {
493
- debugParticleSystem.colorEndA.a = clamp(inputFloat);
494
- debugParticleSystem.colorEndB.a = clamp(inputFloat);
495
- }
496
- else if (name == 'tileSizeX')
497
- {
498
- debugParticleSystem.tileSize = vec2(parseInt(input.value), debugParticleSystem.tileSize.y);
499
- }
500
- else if (name == 'tileSizeY')
501
- {
502
- debugParticleSystem.tileSize.y = vec2(debugParticleSystem.tileSize.x, parseInt(input.value));
503
- }
504
- }
505
- else
506
- debugParticleSystem[name] = inputFloat;
507
-
508
- updateCode();
509
- }
510
- div.appendChild(document.createElement('br'));
511
- div.appendChild(input);
512
- div.appendChild(document.createTextNode(' ' + name));
513
- }
514
-
515
- div.appendChild(document.createElement('br'));
516
- div.appendChild(document.createElement('br'));
517
- div.appendChild(particleSystemCode = document.createElement('input'));
518
- particleSystemCode.disabled = true;
519
- div.appendChild(document.createTextNode(' code'));
520
-
521
- div.appendChild(document.createElement('br'));
522
- const button = document.createElement('button')
523
- div.appendChild(button);
524
- button.innerHTML = 'Copy To Clipboard';
525
-
526
- button.onclick = (e)=> navigator.clipboard.writeText(particleSystemCode.value);
527
-
528
- const updateCode = ()=>
529
- {
530
- let code = '';
531
- let count = 0;
532
- for ( const setting of debugParticleSettings)
533
- {
534
- const name = setting[0];
535
- const type = setting[1];
536
- let value;
537
- if (name == 'tileSizeX' || type == 'alpha')
538
- continue;
539
-
540
- if (count++)
541
- code += ', ';
542
-
543
- if (name == 'tileSizeY')
544
- {
545
- value = `vec2(${debugParticleSystem.tileSize.x},${debugParticleSystem.tileSize.y})`;
546
- }
547
- else if (type == 'color')
548
- {
549
- const c = debugParticleSystem[name];
550
- value = `new Color(${c.r},${c.g},${c.b},${c.a})`;
551
- }
552
- else
553
- value = debugParticleSystem[name];
554
- code += value;
555
- }
556
-
557
- particleSystemCode.value = '...[' + code + ']';
558
- }
559
- updateCode();
560
- }
561
- debugParticleSystemDiv.style.display = debugParticleEditor ? '' : 'none'
562
- }
563
-
564
- const debugParticleSettings =
565
- [
566
- ['emitSize'],
567
- ['emitTime'],
568
- ['emitRate'],
569
- ['emitConeAngle'],
570
- ['tileIndex'],
571
- ['tileSizeX', 'tileSize'],
572
- ['tileSizeY', 'tileSize'],
573
- ['colorStartA', 'color'],
574
- ['colorStartB', 'color'],
575
- ['colorStartAlpha', 'alpha'],
576
- ['colorEndA', 'color'],
577
- ['colorEndB', 'color'],
578
- ['colorEndAlpha', 'alpha'],
579
- ['particleTime'],
580
- ['sizeStart'],
581
- ['sizeEnd'],
582
- ['speed'],
583
- ['angleSpeed'],
584
- ['damping'],
585
- ['angleDamping'],
586
- ['gravityScale'],
587
- ['particleConeAngle'],
588
- ['fadeRate'],
589
- ['randomness'],
590
- ['collideTiles'],
591
- ['additive'],
592
- ['randomColorComponents'],
593
- ['renderOrder'],
594
- ];
595
396
  /**
596
397
  * LittleJS Utility Classes and Functions
597
398
  * <br> - General purpose math library
@@ -643,27 +444,27 @@ const mod = (a, b=1)=> ((a % b) + b) % b;
643
444
 
644
445
  /** Clamps the value beween max and min
645
446
  * @param {Number} value
646
- * @param {Number} [max=1]
647
447
  * @param {Number} [min=0]
448
+ * @param {Number} [max=1]
648
449
  * @return {Number}
649
450
  * @memberof Utilities */
650
- const clamp = (v, max=1, min=0)=> (ASSERT(max > min), v < min ? min : v > max ? max : v);
451
+ const clamp = (v, min=0, max=1)=> v < min ? min : v > max ? max : v;
651
452
 
652
453
  /** Returns what percentage the value is between max and min
653
454
  * @param {Number} value
654
- * @param {Number} [max=1]
655
455
  * @param {Number} [min=0]
456
+ * @param {Number} [max=1]
656
457
  * @return {Number}
657
458
  * @memberof Utilities */
658
- const percent = (v, max=1, min=0)=> max-min ? clamp((v-min) / (max-min)) : 0;
459
+ const percent = (v, min=0, max=1)=> max-min ? clamp((v-min) / (max-min)) : 0;
659
460
 
660
461
  /** Linearly interpolates the percent value between max and min
661
462
  * @param {Number} percent
662
- * @param {Number} [max=1]
663
463
  * @param {Number} [min=0]
464
+ * @param {Number} [max=1]
664
465
  * @return {Number}
665
466
  * @memberof Utilities */
666
- const lerp = (p, max=1, min=0)=> min + clamp(p) * (max-min);
467
+ const lerp = (p, min=0, max=1)=> min + clamp(p) * (max-min);
667
468
 
668
469
  /** Applies smoothstep function to the percentage value
669
470
  * @param {Number} value
@@ -678,11 +479,11 @@ const smoothStep = (p)=> p * p * (3 - 2 * p);
678
479
  const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
679
480
 
680
481
  /** Returns true if two axis aligned bounding boxes are overlapping
681
- * @param {Vector2} pointA - Center of box A
682
- * @param {Vector2} sizeA - Size of box A
683
- * @param {Vector2} pointB - Center of box B
684
- * @param {Vector2} sizeB - Size of box B
685
- * @return {Boolean} - True if overlapping
482
+ * @param {Vector2} pointA - Center of box A
483
+ * @param {Vector2} sizeA - Size of box A
484
+ * @param {Vector2} pointB - Center of box B
485
+ * @param {Vector2} [sizeB] - Size of box B
486
+ * @return {Boolean} - True if overlapping
686
487
  * @memberof Utilities */
687
488
  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;
688
489
 
@@ -905,7 +706,13 @@ class Vector2
905
706
  /** Returns true if this vector is within the bounds of an array size passed in
906
707
  * @param {Vector2} arraySize
907
708
  * @return {Boolean} */
908
- arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
709
+ arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
710
+
711
+ /** Returns this vector expressed as a string
712
+ * @param {float} digits - precision to display
713
+ * @return {String} */
714
+ toString(digits=3)
715
+ { return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`; }
909
716
  }
910
717
 
911
718
  ///////////////////////////////////////////////////////////////////////////////
@@ -976,7 +783,7 @@ class Color
976
783
  * @return {Color} */
977
784
  lerp(c, p) { return this.add(c.subtract(this).scale(clamp(p))); }
978
785
 
979
- /** Sets this color given a hue, saturation, lightness , and alpha
786
+ /** Sets this color given a hue, saturation, lightness, and alpha
980
787
  * @param {Number} [hue=0]
981
788
  * @param {Number} [saturation=0]
982
789
  * @param {Number} [lightness=1]
@@ -1012,21 +819,43 @@ class Color
1012
819
  ).clamp();
1013
820
  }
1014
821
 
1015
- /** Returns this color expressed as an rgba string
822
+ /** Returns this color expressed as an CSS color value
1016
823
  * @return {String} */
1017
- rgba()
824
+ toString()
1018
825
  {
1019
826
  ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1 && this.a>=0 && this.a<=1);
1020
827
  return `rgb(${this.r*255|0},${this.g*255|0},${this.b*255|0},${this.a})`;
1021
828
  }
1022
829
 
1023
- /** Returns this color expressed as 32 bit integer value
830
+ /** Returns this color expressed as 32 bit integer RGBA value
1024
831
  * @return {Number} */
1025
832
  rgbaInt()
1026
833
  {
1027
834
  ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1 && this.a>=0 && this.a<=1);
1028
835
  return (this.r*255|0) + (this.g*255<<8) + (this.b*255<<16) + (this.a*255<<24);
1029
836
  }
837
+
838
+ /** Returns this color expressed as a hex code
839
+ * @return {String} */
840
+ hex()
841
+ {
842
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
843
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b);
844
+ }
845
+
846
+ /** Set this color from a hex code
847
+ * @param {String} hex - html hex code
848
+ * @return {Color} */
849
+ setHex(hex)
850
+ {
851
+ const fromHex = (a)=> parseInt(hex.slice(a,a+2), 16)/255;
852
+ this.r = fromHex(1);
853
+ this.g = fromHex(3),
854
+ this.b = fromHex(5);
855
+ this.a = 1;
856
+ ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1);
857
+ return this;
858
+ }
1030
859
  }
1031
860
 
1032
861
  ///////////////////////////////////////////////////////////////////////////////
@@ -1063,7 +892,7 @@ class Timer
1063
892
 
1064
893
  /** Returns true if set and elapsed
1065
894
  * @return {Boolean} */
1066
- elapsed() { return time > this.time; }
895
+ elapsed() { return time > this.time; }
1067
896
 
1068
897
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1069
898
  * @return {Number} */
@@ -1071,7 +900,11 @@ class Timer
1071
900
 
1072
901
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1073
902
  * @return {Number} */
1074
- getPercent() { return this.isSet()? percent(this.time - time, 0, this.setTime) : 0; }
903
+ getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
904
+
905
+ /** Returns this timer expressed as a string
906
+ * @return {String} */
907
+ toString() { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); }
1075
908
  }
1076
909
  /**
1077
910
  * LittleJS Engine Settings
@@ -1220,6 +1053,11 @@ let inputWASDEmulateDirection = 1;
1220
1053
  * @memberof Settings */
1221
1054
  let touchGamepadEnable = 0;
1222
1055
 
1056
+ /** True if touch gamepad should be analog stick or false to use if 8 way dpad
1057
+ * @default
1058
+ * @memberof Settings */
1059
+ let touchGamepadAnalog = 1;
1060
+
1223
1061
  /** Size of virutal gamepad for touch devices in pixels
1224
1062
  * @default
1225
1063
  * @memberof Settings */
@@ -1319,11 +1157,11 @@ let medalDisplayIconSize = 50;
1319
1157
  class EngineObject
1320
1158
  {
1321
1159
  /** Create an engine object and adds it to the list of objects
1322
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1160
+ * @param {Vector2} [position=new Vector2()] - World space position of the object
1323
1161
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1324
1162
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1325
1163
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
1326
- * @param {Number} [angle=0] - Angle to rotate the object
1164
+ * @param {Number} [angle=0] - Angle the object is rotated by
1327
1165
  * @param {Color} [color] - Color to apply to tile when rendered
1328
1166
  * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
1329
1167
  */
@@ -1364,12 +1202,15 @@ class EngineObject
1364
1202
  this.gravityScale = 1;
1365
1203
  /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1366
1204
  this.renderOrder = renderOrder;
1205
+ /** @property {Vector2} [velocity=new Vector2()] - Velocity of the object */
1206
+ this.velocity = new Vector2();
1207
+ /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1208
+ this.angleVelocity = 0;
1367
1209
 
1368
1210
  // init other internal object stuff
1369
1211
  this.spawnTime = time;
1370
- this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1371
- this.collideTiles = 1;
1372
1212
  this.children = [];
1213
+ this.collideTiles = 1;
1373
1214
 
1374
1215
  // add to list of objects
1375
1216
  engineObjects.push(this);
@@ -1388,8 +1229,8 @@ class EngineObject
1388
1229
  }
1389
1230
 
1390
1231
  // limit max speed to prevent missing collisions
1391
- this.velocity.x = clamp(this.velocity.x, objectMaxSpeed, -objectMaxSpeed);
1392
- this.velocity.y = clamp(this.velocity.y, objectMaxSpeed, -objectMaxSpeed);
1232
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1233
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1393
1234
 
1394
1235
  // apply physics
1395
1236
  const oldPos = this.pos.copy();
@@ -1479,8 +1320,8 @@ class EngineObject
1479
1320
 
1480
1321
  // lerp betwen elastic or inelastic based on elasticity
1481
1322
  const elasticity = max(this.elasticity, o.elasticity);
1482
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1483
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1323
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1324
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1484
1325
  }
1485
1326
  }
1486
1327
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
@@ -1500,8 +1341,8 @@ class EngineObject
1500
1341
 
1501
1342
  // lerp betwen elastic or inelastic based on elasticity
1502
1343
  const elasticity = max(this.elasticity, o.elasticity);
1503
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1504
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1344
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1345
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1505
1346
  }
1506
1347
  else // bounce if other object is fixed
1507
1348
  this.velocity.x *= -this.elasticity;
@@ -1531,7 +1372,7 @@ class EngineObject
1531
1372
 
1532
1373
  // adjust next velocity to settle on ground
1533
1374
  const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1534
- if (o < 0 && o > -1 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1375
+ if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1535
1376
  this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1536
1377
 
1537
1378
  // move to previous position
@@ -1636,6 +1477,22 @@ class EngineObject
1636
1477
  this.isSolid = isSolid;
1637
1478
  this.collideTiles = collideTiles;
1638
1479
  }
1480
+
1481
+ toString()
1482
+ {
1483
+ let text = 'type = ' + this.constructor.name;
1484
+ if (this.pos.x || this.pos.y)
1485
+ text += '\npos = ' + this.pos;
1486
+ if (this.velocity.x || this.velocity.y)
1487
+ text += '\nvelocity = ' + this.velocity;
1488
+ if (this.size.x || this.size.y)
1489
+ text += '\nsize = ' + this.size;
1490
+ if (this.angle)
1491
+ text += '\nangle = ' + this.angle.toFixed(3);
1492
+ if (this.color)
1493
+ text += '\ncolor = ' + this.color;
1494
+ return text;
1495
+ }
1639
1496
  }
1640
1497
  /**
1641
1498
  * LittleJS Drawing System
@@ -1749,7 +1606,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1749
1606
  if (tileIndex < 0)
1750
1607
  {
1751
1608
  // if negative tile index, force untextured
1752
- context.fillStyle = color.rgba();
1609
+ context.fillStyle = color;
1753
1610
  context.fillRect(-.5, -.5, 1, 1);
1754
1611
  }
1755
1612
  else
@@ -1842,7 +1699,20 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1842
1699
  context.restore();
1843
1700
  }
1844
1701
 
1845
- /** Draw text on overlay canvas in world space
1702
+ /** Enable normal or additive blend mode
1703
+ * @param {Boolean} [additive=0]
1704
+ * @param {Boolean} [useWebGL=glEnable]
1705
+ * @memberof Draw */
1706
+ function setBlendMode(additive, useWebGL=glEnable)
1707
+ {
1708
+ if (glEnable && useWebGL)
1709
+ glSetBlendMode(additive);
1710
+ else
1711
+ mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1712
+ }
1713
+
1714
+ /** Draw text on overlay canvas in screen space
1715
+ * Automatically splits new lines into rows
1846
1716
  * @param {String} text
1847
1717
  * @param {Vector2} pos
1848
1718
  * @param {Number} [size=1]
@@ -1851,32 +1721,129 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1851
1721
  * @param {Color} [lineColor=new Color(0,0,0)]
1852
1722
  * @param {String} [textAlign='center']
1853
1723
  * @memberof Draw */
1854
- function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1724
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1855
1725
  {
1856
- pos = worldToScreen(pos);
1857
- overlayContext.font = size*cameraScale + 'px '+ font;
1726
+ overlayContext.fillStyle = color;
1727
+ overlayContext.lineWidth = lineWidth *= cameraScale;
1728
+ overlayContext.strokeStyle = lineColor;
1858
1729
  overlayContext.textAlign = textAlign;
1730
+ overlayContext.font = size + 'px '+ font;
1859
1731
  overlayContext.textBaseline = 'middle';
1860
- if (lineWidth)
1732
+
1733
+ pos = pos.copy();
1734
+ text.split('\n').forEach(line=>
1861
1735
  {
1862
- overlayContext.lineWidth = lineWidth*cameraScale;
1863
- overlayContext.strokeStyle = lineColor.rgba();
1864
- overlayContext.strokeText(text, pos.x, pos.y);
1865
- }
1866
- overlayContext.fillStyle = color.rgba();
1867
- overlayContext.fillText(text, pos.x, pos.y);
1736
+ lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
1737
+ overlayContext.fillText(line, pos.x, pos.y);
1738
+ pos.y += size;
1739
+ });
1868
1740
  }
1869
1741
 
1870
- /** Enable normal or additive blend mode
1871
- * @param {Boolean} [additive=0]
1872
- * @param {Boolean} [useWebGL=glEnable]
1742
+ /** Draw text on overlay canvas in world space
1743
+ * Automatically splits new lines into rows
1744
+ * @param {String} text
1745
+ * @param {Vector2} pos
1746
+ * @param {Number} [size=1]
1747
+ * @param {Color} [color=new Color(1,1,1)]
1748
+ * @param {Number} [lineWidth=0]
1749
+ * @param {Color} [lineColor=new Color(0,0,0)]
1750
+ * @param {String} [textAlign='center']
1873
1751
  * @memberof Draw */
1874
- function setBlendMode(additive, useWebGL=glEnable)
1752
+ function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
1875
1753
  {
1876
- if (glEnable && useWebGL)
1877
- glSetBlendMode(additive);
1878
- else
1879
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1754
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth, lineColor, textAlign, font);
1755
+ }
1756
+
1757
+ ///////////////////////////////////////////////////////////////////////////////
1758
+
1759
+ /**
1760
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
1761
+ * <br> - 96 characters (from space to tilde) are stored in an image
1762
+ * <br> - Uses a default 8x8 font if none is supplied
1763
+ * <br> - You can also use fonts from the main tile sheet
1764
+ * @example
1765
+ * // use built in font
1766
+ * const font = new ImageFont;
1767
+ *
1768
+ * // draw text
1769
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1770
+ */
1771
+
1772
+ let engineFontImage;
1773
+
1774
+ class FontImage
1775
+ {
1776
+ /** Create an image font
1777
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1778
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1779
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1780
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
1781
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1782
+ */
1783
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1784
+ {
1785
+ if (!image && !engineFontImage)
1786
+ {
1787
+ // load default font image
1788
+ engineFontImage = new Image();
1789
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1790
+ }
1791
+
1792
+ this.image = image || engineFontImage;
1793
+ this.tileSize = tileSize;
1794
+ this.paddingSize = paddingSize;
1795
+ this.startTileIndex = startTileIndex;
1796
+ this.context = context;
1797
+ }
1798
+
1799
+ /** Draw text in screen space using the image font
1800
+ * @param {String} text
1801
+ * @param {Vector2} pos
1802
+ * @param {Number} [scale=4]
1803
+ * @param {Boolean} [center]
1804
+ */
1805
+ drawTextScreen(text, pos, scale=4, center)
1806
+ {
1807
+ const context = this.context;
1808
+ context.save();
1809
+ context.imageSmoothingEnabled = !cavasPixelated;
1810
+
1811
+ const size = this.tileSize;
1812
+ const drawSize = size.add(this.paddingSize).scale(scale);
1813
+ const cols = this.image.width / this.tileSize.x |0;
1814
+ text.split('\n').forEach((line, i)=>
1815
+ {
1816
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
1817
+ for(let j=line.length; j--;)
1818
+ {
1819
+ // draw each character
1820
+ let charCode = line[j].charCodeAt();
1821
+ if (charCode < 32 || charCode > 127)
1822
+ charCode = 127; // unknown character
1823
+
1824
+ // get the character source location and draw it
1825
+ const tile = this.startTileIndex + charCode - 32;
1826
+ const x = tile % cols;
1827
+ const y = tile / cols |0;
1828
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
1829
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
1830
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
1831
+ }
1832
+ });
1833
+
1834
+ context.restore();
1835
+ }
1836
+
1837
+ /** Draw text in world space using the image font
1838
+ * @param {String} text
1839
+ * @param {Vector2} pos
1840
+ * @param {Number} [scale=.25]
1841
+ * @param {Boolean} [center]
1842
+ */
1843
+ drawText(text, pos, scale=1, center)
1844
+ {
1845
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1846
+ }
1880
1847
  }
1881
1848
 
1882
1849
  ///////////////////////////////////////////////////////////////////////////////
@@ -2067,7 +2034,7 @@ const mouseToScreen = (mousePos)=>
2067
2034
 
2068
2035
  const rect = mainCanvas.getBoundingClientRect();
2069
2036
  return mainCanvasSize.multiply(
2070
- vec2(percent(mousePos.x, rect.right, rect.left), percent(mousePos.y, rect.bottom, rect.top)));
2037
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
2071
2038
  }
2072
2039
 
2073
2040
  ///////////////////////////////////////////////////////////////////////////////
@@ -2108,8 +2075,8 @@ function gamepadsUpdate()
2108
2075
  // read clamp dead zone of analog sticks
2109
2076
  const deadZone = .3, deadZoneMax = .8;
2110
2077
  const applyDeadZone = (v)=>
2111
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
2112
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
2078
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
2079
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
2113
2080
 
2114
2081
  // read analog sticks
2115
2082
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -2235,7 +2202,14 @@ function touchGamepadCreate()
2235
2202
  if (touchPos.distance(stickCenter) < touchGamepadSize)
2236
2203
  {
2237
2204
  // virtual analog stick
2238
- touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2205
+ if (touchGamepadAnalog)
2206
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2207
+ else
2208
+ {
2209
+ // 8 way dpad
2210
+ const angle = touchPos.subtract(stickCenter).angle();
2211
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2212
+ }
2239
2213
  }
2240
2214
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2241
2215
  {
@@ -2259,7 +2233,7 @@ function touchGamepadRender()
2259
2233
  return;
2260
2234
 
2261
2235
  // fade off when not touching or paused
2262
- const alpha = percent(touchGamepadTimer.get(), 3, 4);
2236
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2263
2237
  if (!alpha || paused)
2264
2238
  return;
2265
2239
 
@@ -2270,12 +2244,28 @@ function touchGamepadRender()
2270
2244
  overlayContext.lineWidth = 3;
2271
2245
 
2272
2246
  // draw left analog stick
2273
- overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
2247
+ overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2274
2248
  overlayContext.beginPath();
2275
- overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
2276
- overlayContext.fill();
2277
- overlayContext.stroke();
2278
2249
 
2250
+ const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2251
+ if (touchGamepadAnalog)
2252
+ {
2253
+ overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2254
+ overlayContext.fill();
2255
+ overlayContext.stroke();
2256
+ }
2257
+ else // draw cross shaped gamepad
2258
+ {
2259
+ for(let i=10; i--;)
2260
+ {
2261
+ const angle = i*PI/4;
2262
+ overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2263
+ i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2264
+ i==1 && overlayContext.fill();
2265
+ }
2266
+ overlayContext.stroke();
2267
+ }
2268
+
2279
2269
  // draw right face buttons
2280
2270
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2281
2271
  for (let i=4; i--;)
@@ -2362,7 +2352,7 @@ class Sound
2362
2352
  return; // out of range
2363
2353
 
2364
2354
  // attenuate volume by distance
2365
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2355
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
2366
2356
  }
2367
2357
 
2368
2358
  // get pan from screen space coords
@@ -2544,7 +2534,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2544
2534
  // connect source to gain
2545
2535
  (
2546
2536
  window.StereoPannerNode ? // create pan node if possible
2547
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
2537
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2548
2538
  : source
2549
2539
  )
2550
2540
  .connect(gainNode);
@@ -2916,14 +2906,14 @@ class TileLayerData
2916
2906
  */
2917
2907
  class TileLayer extends EngineObject
2918
2908
  {
2919
- /** Create a tile layer object
2920
- * @param {Vector2} [position=new Vector2(0,0)] - World space position
2921
- * @param {Vector2} [size=objectDefaultSize] - World space size
2922
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2923
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2924
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2925
- */
2926
- constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2909
+ /** Create a tile layer object
2910
+ * @param {Vector2} [position=new Vector2()] - World space position
2911
+ * @param {Vector2} [size=tileCollisionSize] - World space size
2912
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2913
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2914
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2915
+ */
2916
+ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2927
2917
  {
2928
2918
  super(pos, size, -1, tileSize, 0, undefined, renderOrder);
2929
2919
 
@@ -2981,17 +2971,19 @@ class TileLayer extends EngineObject
2981
2971
  );
2982
2972
  }
2983
2973
 
2984
- /** Draw all the tile data to an offscreen canvas */
2974
+ /** Draw all the tile data to an offscreen canvas
2975
+ * - This may be slow in some browsers
2976
+ */
2985
2977
  redraw()
2986
2978
  {
2987
- this.redrawStart();
2979
+ this.redrawStart(1);
2988
2980
  this.drawAllTileData();
2989
2981
  this.redrawEnd();
2990
2982
  }
2991
2983
 
2992
2984
  /** Call to start the redraw process
2993
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2994
- redrawStart(clear = 1)
2985
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2986
+ redrawStart(clear = 0)
2995
2987
  {
2996
2988
  if (clear)
2997
2989
  {
@@ -3081,7 +3073,7 @@ class TileLayer extends EngineObject
3081
3073
  if (tileIndex < 0)
3082
3074
  {
3083
3075
  // untextured
3084
- context.fillStyle = color.rgba();
3076
+ context.fillStyle = color;
3085
3077
  context.fillRect(-.5, -.5, 1, 1);
3086
3078
  }
3087
3079
  else
@@ -3100,7 +3092,8 @@ class TileLayer extends EngineObject
3100
3092
  * @param {Vector2} [size=new Vector2(1,1)]
3101
3093
  * @param {Color} [color=new Color(1,1,1)]
3102
3094
  * @param {Number} [angle=0] */
3103
- drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle); }
3095
+ drawRect(pos, size, color, angle)
3096
+ { this.drawTile(pos, size, -1, 0, color, angle); }
3104
3097
  }
3105
3098
  /*
3106
3099
  LittleJS Particle System
@@ -3119,9 +3112,9 @@ class TileLayer extends EngineObject
3119
3112
  * let pos = vec2(2,3);
3120
3113
  * let particleEmiter = new ParticleEmitter
3121
3114
  * (
3122
- * pos, 1, 0, 500, PI, // pos, emitSize, emitTime, emitRate, emiteCone
3115
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emiteCone
3123
3116
  * 0, vec2(16), // tileIndex, tileSize
3124
- * new Color, new Color(0,0,0), // colorStartA, colorStartB
3117
+ * new Color(1,1,1), new Color(0,0,0), // colorStartA, colorStartB
3125
3118
  * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
3126
3119
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
3127
3120
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
@@ -3132,6 +3125,7 @@ class ParticleEmitter extends EngineObject
3132
3125
  {
3133
3126
  /** Create a particle system with the given settings
3134
3127
  * @param {Vector2} position - World space position of the emitter
3128
+ * @param {Number} [angle=0] - Angle to emit the particles
3135
3129
  * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3136
3130
  * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3137
3131
  * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
@@ -3161,6 +3155,7 @@ class ParticleEmitter extends EngineObject
3161
3155
  constructor
3162
3156
  (
3163
3157
  pos,
3158
+ angle,
3164
3159
  emitSize = 0,
3165
3160
  emitTime = 0,
3166
3161
  emitRate = 100,
@@ -3188,7 +3183,7 @@ class ParticleEmitter extends EngineObject
3188
3183
  renderOrder = additive ? 1e9 : 0
3189
3184
  )
3190
3185
  {
3191
- super(pos, new Vector2, tileIndex, tileSize, 0, undefined, renderOrder);
3186
+ super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
3192
3187
 
3193
3188
  // emitter settings
3194
3189
  /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
@@ -3421,7 +3416,7 @@ function medalsInit(saveName)
3421
3416
  {
3422
3417
  // check if medals are unlocked
3423
3418
  medalsSaveName = saveName;
3424
- debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3419
+ debugMedals || medals.forEach(medal=> localStorage[medal.storageKey()]);
3425
3420
  }
3426
3421
 
3427
3422
  /**
@@ -3610,6 +3605,9 @@ class Newgrounds
3610
3605
  const scoreboardResult = this.call('ScoreBoard.getBoards');
3611
3606
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
3612
3607
  debugMedals && console.log(this.scoreboards);
3608
+
3609
+ const keepAliveMS = 5 * 60 * 1e3;
3610
+ setInterval(()=>this.call('Gateway.ping', 0, 1), keepAliveMS);
3613
3611
  }
3614
3612
 
3615
3613
  /** Send message to unlock a medal by id
@@ -3621,9 +3619,6 @@ class Newgrounds
3621
3619
  * @param {Number} value - The score value */
3622
3620
  postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3623
3621
 
3624
- /** Send message to log a view */
3625
- logView() { return this.call('App.logView', {'host':this.host}, 1); }
3626
-
3627
3622
  /** Get scores from a scoreboard
3628
3623
  * @param {Number} id - The scoreboard id
3629
3624
  * @param {String} [user=0] - A user's id or name
@@ -3635,6 +3630,9 @@ class Newgrounds
3635
3630
  getScores(id, user=0, social=0, skip=0, limit=10)
3636
3631
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3637
3632
 
3633
+ /** Send message to log a view */
3634
+ logView() { return this.call('App.logView', {'host':this.host}, 1); }
3635
+
3638
3636
  /** Send a message to call a component of the Newgrounds API
3639
3637
  * @param {String} component - Name of the component
3640
3638
  * @param {Object} [parameters=0] - Parameters to use for call
@@ -3722,8 +3720,8 @@ function glInit()
3722
3720
 
3723
3721
  // create the canvas and tile texture
3724
3722
  glCanvas = document.createElement('canvas');
3725
- glContext = glCanvas.getContext('webgl');
3726
- glCanvas.style = canvaStyle;
3723
+ glContext = glCanvas.getContext('webgl', {antialias: false});
3724
+ glCanvas.style = styleCanvas;
3727
3725
  glTileTexture = glCreateTexture(tileImage);
3728
3726
 
3729
3727
  // some browsers are much faster without copying the gl buffer so we just overlay it instead
@@ -3731,25 +3729,24 @@ function glInit()
3731
3729
 
3732
3730
  // setup vertex and fragment shaders
3733
3731
  glShader = glCreateProgram(
3734
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3735
- 'uniform mat4 m;'+ // transform matrix
3736
- 'attribute float a;'+ // angle
3737
- 'attribute vec2 p,s,t;'+ // position, size, uv
3738
- 'attribute vec4 c,b;'+ // color, additiveColor
3739
- 'varying vec2 v;'+ // return uv
3740
- 'varying vec4 d,e;'+ // return color, additiveColor
3741
- 'void main(){'+ // shader entry point
3742
- 'gl_Position=m*vec4((s*cos(a)-vec2(-s.y,s)*sin(a))*.5+p,1,1);'+// transform position
3743
- 'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3744
- '}' // end of shader
3732
+ 'precision highp float;'+ // use highp for better accuracy
3733
+ 'uniform mat4 m;'+ // transform matrix
3734
+ 'attribute vec2 p,t;'+ // position, uv
3735
+ 'attribute vec4 c,a;'+ // color, additiveColor
3736
+ 'varying vec2 v;'+ // return uv
3737
+ 'varying vec4 d,e;'+ // return color, additiveColor
3738
+ 'void main(){'+ // shader entry point
3739
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
3740
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3741
+ '}' // end of shader
3745
3742
  ,
3746
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3747
- 'varying vec2 v;'+ // uv
3748
- 'varying vec4 d,e;'+ // color, additiveColor
3749
- 'uniform sampler2D j;'+ // texture
3750
- 'void main(){'+ // shader entry point
3751
- 'gl_FragColor=texture2D(j,v)*d+e;'+ // modulate texture by color plus additive
3752
- '}' // end of shader
3743
+ 'precision highp float;'+ // use highp for better accuracy
3744
+ 'varying vec2 v;'+ // uv
3745
+ 'varying vec4 d,e;'+ // color, additiveColor
3746
+ 'uniform sampler2D s;'+ // texture
3747
+ 'void main(){'+ // shader entry point
3748
+ 'gl_FragColor=texture2D(s,v)*d+e;'+ // modulate texture by color plus additive
3749
+ '}' // end of shader
3753
3750
  );
3754
3751
 
3755
3752
  // init buffers
@@ -3767,12 +3764,10 @@ function glInit()
3767
3764
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3768
3765
  offset += size*typeSize;
3769
3766
  }
3770
- initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3771
3767
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3772
- initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
3773
3768
  initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3774
3769
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3775
- initVertexAttribArray('b', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3770
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3776
3771
  }
3777
3772
 
3778
3773
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3887,6 +3882,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3887
3882
 
3888
3883
  // clear and set to same size as main canvas
3889
3884
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3885
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3890
3886
 
3891
3887
  // set up the shader
3892
3888
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3933,12 +3929,10 @@ function glCopyToContext(context, forceDraw)
3933
3929
  if (!glEnable || !glBatchCount) return;
3934
3930
 
3935
3931
  glFlush();
3932
+
3933
+ // do not draw in overlay mode because the canvas is visible
3936
3934
  if (!glOverlay || forceDraw)
3937
- {
3938
- // do not draw/clear in overlay mode because the canvas is visible
3939
3935
  context.drawImage(glCanvas, 0, 0);
3940
- glContext.clear(gl_COLOR_BUFFER_BIT);
3941
- }
3942
3936
  }
3943
3937
 
3944
3938
  /** Add a sprite to the gl draw list, used by all gl draw functions
@@ -3961,51 +3955,49 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3961
3955
  // flush if there is no room for more verts or if different blend mode
3962
3956
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3963
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;
3964
3962
 
3965
3963
  // setup 2 triangles to form a quad
3966
3964
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3967
3965
 
3968
3966
  // vertex 0
3969
- glPositionData[offset++] = angle;
3970
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3971
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3972
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3973
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
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;
3974
3971
 
3975
3972
  // vertex 1
3976
- glPositionData[offset++] = angle;
3977
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3978
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3979
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3980
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
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;
3981
3977
 
3982
3978
  // vertex 2
3983
- glPositionData[offset++] = angle;
3984
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3985
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = sizeY;
3986
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3987
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
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;
3988
3983
 
3989
3984
  // vertex 0
3990
- glPositionData[offset++] = angle;
3991
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3992
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3993
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3994
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
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;
3995
3989
 
3996
3990
  // vertex 3
3997
- glPositionData[offset++] = angle;
3998
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3999
- glPositionData[offset++] = sizeX; glPositionData[offset++] = -sizeY;
4000
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
4001
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
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;
4002
3995
 
4003
3996
  // vertex 1
4004
- glPositionData[offset++] = angle;
4005
- glPositionData[offset++] = x; glPositionData[offset++] = y;
4006
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
4007
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
4008
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
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;
4009
4001
  }
4010
4002
 
4011
4003
  ///////////////////////////////////////////////////////////////////////////////
@@ -4037,9 +4029,9 @@ gl_LINK_STATUS = 35714,
4037
4029
 
4038
4030
  // constants for batch rendering
4039
4031
  gl_VERTICES_PER_QUAD = 6,
4040
- gl_INDICIES_PER_VERT = 9,
4032
+ gl_INDICIES_PER_VERT = 6,
4041
4033
  gl_MAX_BATCH = 1<<16,
4042
- gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char * 4) * 2
4034
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
4043
4035
  /*
4044
4036
  LittleJS - The Tiny JavaScript Game Engine That Can!
4045
4037
  MIT License - Copyright 2021 Frank Force
@@ -4064,7 +4056,7 @@ gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char *
4064
4056
  const engineName = 'LittleJS';
4065
4057
 
4066
4058
  /** Version of engine */
4067
- const engineVersion = '1.1.26';
4059
+ const engineVersion = '1.2.8';
4068
4060
 
4069
4061
  /** Frames per second to update objects
4070
4062
  * @default */
@@ -4101,7 +4093,7 @@ let averageFPS, drawCount;
4101
4093
  // css text used for elements created by engine
4102
4094
  const styleBody = 'margin:0;overflow:hidden;background:#000' +
4103
4095
  ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4104
- const canvaStyle = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4096
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4105
4097
 
4106
4098
  ///////////////////////////////////////////////////////////////////////////////
4107
4099
 
@@ -4126,7 +4118,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4126
4118
  document.body.style = styleBody;
4127
4119
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
4128
4120
  mainContext = mainCanvas.getContext('2d');
4129
- mainCanvas.style = canvaStyle;
4121
+ mainCanvas.style = styleCanvas;
4130
4122
 
4131
4123
  // init stuff and start engine
4132
4124
  debugInit();
@@ -4135,7 +4127,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4135
4127
  // create overlay canvas for hud to appear above gl canvas
4136
4128
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
4137
4129
  overlayContext = overlayCanvas.getContext('2d');
4138
- overlayCanvas.style = canvaStyle;
4130
+ overlayCanvas.style = styleCanvas;
4139
4131
 
4140
4132
  gameInit();
4141
4133
  touchGamepadCreate();
@@ -4145,13 +4137,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4145
4137
  // main update loop
4146
4138
  const engineUpdate = (frameTimeMS=0)=>
4147
4139
  {
4148
- requestAnimationFrame(engineUpdate);
4149
-
4150
4140
  // update time keeping
4151
4141
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4152
4142
  frameTimeLastMS = frameTimeMS;
4153
4143
  if (debug || showWatermark)
4154
- averageFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), averageFPS || 0);
4144
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
4155
4145
  if (debug)
4156
4146
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
4157
4147
  timeReal += frameTimeDeltaMS / 1e3;
@@ -4245,6 +4235,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4245
4235
  overlayContext.fillText(text, mainCanvas.width-2, 2);
4246
4236
  drawCount = 0;
4247
4237
  }
4238
+
4239
+ requestAnimationFrame(engineUpdate);
4248
4240
  }
4249
4241
 
4250
4242
  // set tile image source to load the image and start the engine