littlejsengine 1.17.1 → 1.17.10

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 (49) hide show
  1. package/LICENSE +10 -10
  2. package/dist/littlejs.d.ts +254 -171
  3. package/dist/littlejs.esm.js +892 -628
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +889 -626
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +885 -622
  8. package/examples/box2d/gameObjects.js +3 -3
  9. package/examples/electron/index.html +2 -2
  10. package/examples/index.html +10 -9
  11. package/examples/logo.png +0 -0
  12. package/examples/logo2.png +0 -0
  13. package/examples/module/build.bat +7 -0
  14. package/examples/module/build.js +121 -0
  15. package/examples/particles/index.html +1 -1
  16. package/examples/platformer/game.js +1 -1
  17. package/examples/platformer/gameEffects.js +5 -10
  18. package/examples/platformer/gameLevel.js +3 -3
  19. package/examples/puzzle/game.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/box2dPool.js +2 -2
  22. package/examples/shorts/box2dTileLayer.js +48 -0
  23. package/examples/shorts/colors.js +1 -1
  24. package/examples/shorts/fps.js +1 -1
  25. package/examples/shorts/music.js +5 -4
  26. package/examples/shorts/musicPlayer.js +4 -3
  27. package/examples/shorts/parallax.js +2 -2
  28. package/examples/shorts/tiltedView.js +1 -1
  29. package/examples/starter/index.html +2 -2
  30. package/package.json +5 -5
  31. package/plugins/box2d.js +166 -63
  32. package/plugins/pluginExport.js +1 -1
  33. package/plugins/postProcess.js +1 -1
  34. package/plugins/uiSystem.js +33 -20
  35. package/plugins/zzfxm.js +2 -2
  36. package/reference.md +9 -8
  37. package/src/engine.js +111 -134
  38. package/src/engineAudio.js +44 -58
  39. package/src/engineDebug.js +4 -4
  40. package/src/engineDraw.js +7 -4
  41. package/src/engineExport.js +2 -1
  42. package/src/engineInput.js +5 -5
  43. package/src/engineMath.js +9 -9
  44. package/src/engineObject.js +25 -25
  45. package/src/engineParticles.js +241 -118
  46. package/src/engineTileLayer.js +117 -88
  47. package/src/engineUtilities.js +105 -78
  48. package/src/engineWebGL.js +2 -2
  49. package/plugins/Box2D_License.txt +0 -17
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.17.1';
33
+ const engineVersion = '1.17.10';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -85,8 +85,9 @@ function getPaused() { return paused; }
85
85
  * @memberof Engine */
86
86
  function setPaused(isPaused=true) { paused = isPaused; }
87
87
 
88
- // Frame time tracking
88
+ // Engine internal variables
89
89
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
90
+ let showEngineVersion = true;
90
91
 
91
92
  ///////////////////////////////////////////////////////////////////////////////
92
93
  // plugin hooks
@@ -150,16 +151,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
150
151
  * @example
151
152
  * // Basic engine startup
152
153
  * engineInit(
153
- * () => { LOG('Game initialized!'); }, // gameInit
154
- * () => { updateGameLogic(); }, // gameUpdate
155
- * () => { updateUI(); }, // gameUpdatePost
156
- * () => { drawBackground(); }, // gameRender
157
- * () => { drawHUD(); }, // gameRenderPost
154
+ * ()=> { LOG('Game initialized!'); }, // gameInit
155
+ * ()=> { updateGameLogic(); }, // gameUpdate
156
+ * ()=> { updateUI(); }, // gameUpdatePost
157
+ * ()=> { drawBackground(); }, // gameRender
158
+ * ()=> { drawHUD(); }, // gameRenderPost
158
159
  * ['tiles.png', 'tilesLevel.png'] // images to load
159
160
  * );
160
161
  * @memberof Engine */
161
162
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
162
163
  {
164
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
163
165
  ASSERT(!mainContext, 'engine already initialized');
164
166
  ASSERT(isArray(imageSources), 'pass in images as array');
165
167
 
@@ -428,7 +430,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
428
430
  promises.push(new Promise(resolve =>
429
431
  {
430
432
  let t = 0;
431
- console.log(`${engineName} Engine v${engineVersion}`);
432
433
  updateSplash();
433
434
  function updateSplash()
434
435
  {
@@ -460,13 +461,13 @@ function engineObjectsUpdate()
460
461
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
461
462
 
462
463
  // recursive object update
463
- function updateObject(o)
464
+ function updateChildObject(o)
464
465
  {
465
466
  if (o.destroyed) return;
466
467
 
467
468
  o.update();
468
469
  for (const child of o.children)
469
- updateObject(child);
470
+ updateChildObject(child);
470
471
  }
471
472
  for (const o of engineObjects)
472
473
  {
@@ -476,7 +477,7 @@ function engineObjectsUpdate()
476
477
  o.update();
477
478
  o.updatePhysics();
478
479
  for (const child of o.children)
479
- updateObject(child);
480
+ updateChildObject(child);
480
481
  o.updateTransforms();
481
482
  }
482
483
 
@@ -485,11 +486,14 @@ function engineObjectsUpdate()
485
486
  }
486
487
 
487
488
  /** Destroy and remove all objects
489
+ * - This can be used to clear out all objects when restarting a level
490
+ * - Objects can override their destroy function to do cleanup or stick around
491
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
488
492
  * @memberof Engine */
489
- function engineObjectsDestroy()
493
+ function engineObjectsDestroy(immediate=true)
490
494
  {
491
495
  for (const o of engineObjects)
492
- o.parent || o.destroy();
496
+ o.parent || o.destroy(immediate);
493
497
  engineObjects = engineObjects.filter(o=>!o.destroyed);
494
498
  }
495
499
 
@@ -564,164 +568,137 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
564
568
  ///////////////////////////////////////////////////////////////////////////////
565
569
  function drawEngineLogo(t)
566
570
  {
571
+ const blackAndWhite = 0;
572
+ const showName = 1;
573
+
567
574
  // LittleJS Logo and Splash Screen
568
575
  const x = mainContext;
569
576
  const w = mainCanvas.width = innerWidth;
570
577
  const h = mainCanvas.height = innerHeight;
571
-
572
578
  {
573
579
  // background
574
580
  const p3 = percent(t, 1, .8);
575
581
  const p4 = percent(t, 0, .5);
576
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
582
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
577
583
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
578
584
  g.addColorStop(1,hsl(0,0,0,p3).toString());
579
585
  x.save();
580
586
  x.fillStyle = g;
581
587
  x.fillRect(0,0,w,h);
582
588
  }
583
-
584
- // draw LittleJS logo...
585
- const rect = (X, Y, W, H, C)=>
589
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
590
+ {
591
+ if (C >= 0)
592
+ {
593
+ if (blackAndWhite)
594
+ x.fillStyle = '#fff';
595
+ else
596
+ {
597
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
598
+ g.addColorStop(0,color(C,2));
599
+ g.addColorStop(1,color(C,1));
600
+ }
601
+ }
602
+ else
603
+ x.fillStyle = '#000';
604
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
605
+ }
606
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
586
607
  {
587
608
  x.beginPath();
588
- x.rect(X,Y,W,C?H*p:H);
589
- x.fillStyle = C;
590
- C ? x.fill() : x.stroke();
591
- };
592
- const line = (X, Y, Z, W)=>
609
+ x.arc(X,Y,R,p*A,p*B);
610
+ gradient(X,Y-R,X,Y+R,C,S);
611
+ }
612
+ const rect = (X,Y,W,H,C)=>
593
613
  {
594
614
  x.beginPath();
595
- x.lineTo(X,Y);
596
- x.lineTo(Z,W);
597
- x.stroke();
598
- };
599
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
615
+ x.rect(X,Y,W,H*p);
616
+ gradient(X,Y+H,X+W,Y,C);
617
+ }
618
+ const poly = (points,C,Y,H)=>
600
619
  {
601
- const D = (A+B)/2, E = p*(B-A)/2;
602
620
  x.beginPath();
603
- F && x.lineTo(X,Y);
604
- x.arc(X,Y,R,D-E,D+E);
605
- x.fillStyle = C;
606
- C ? x.fill() : x.stroke();
607
- };
608
- const color = (c=0, l=0) =>
609
- hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
621
+ for (const p of points)
622
+ x.lineTo(p.x, p.y);
623
+ x.closePath();
624
+ gradient(0, Y, 0, Y+H,C);
625
+ }
626
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
627
+
628
+ // center and fit tos screen
610
629
  const alpha = wave(1,1,t);
611
630
  const p = percent(alpha, .1, .5);
612
-
613
- // setup
631
+ const size = min(6, min(w,h)/99);
614
632
  x.translate(w/2,h/2);
615
- const size = min(6, min(w,h)/99); // fit to screen
616
633
  x.scale(size,size);
617
634
  x.translate(-40,-35);
635
+ p < 1 && x.setLineDash([99*p,99]);
618
636
  x.lineJoin = x.lineCap = 'round';
619
637
  x.lineWidth = .1 + p*1.9;
638
+ //x.strokeStyle='#fff7';
639
+
640
+ if (showName)
641
+ {
642
+ // engine name text
643
+ const Y = 54;
644
+ const s = 'LittleJS';
645
+ x.font = '900 15.5px arial';
646
+ x.lineWidth = .1+p*3.9;
647
+ x.textAlign = 'center';
648
+ x.textBaseline = 'top';
649
+ rect(11,Y+2,59,8*p,-1);
650
+ x.beginPath();
651
+
652
+ let w2 = 0;
653
+ for (let i=0;i<s.length;++i)
654
+ w2 += x.measureText(s[i]).width;
655
+ for (let j=2;j--;)
656
+ for (let i=0,X=40-w2/2;i<s.length;++i)
657
+ {
658
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
659
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
660
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
661
+ X += w;
662
+ }
663
+
664
+ x.lineWidth = .1 + p*1.9;
665
+ rect(3,Y,73,0); // bottom
666
+ }
620
667
 
621
- // drawing effect
622
- const p2 = percent(alpha,.1,1);
623
- x.setLineDash([99*p2,99]);
624
-
625
- // cab top
626
- rect(7,16,18,-8,color(2,2));
627
- rect(7,8,18,4,color(2,3));
628
- rect(25,8,8,8,color(2,1));
629
- rect(25,8,-18,8);
630
- rect(25,8,8,8);
631
-
632
- // cab
633
- rect(25,16,7,24,color());
634
- rect(11,39,14,-23,color(1,1));
635
- rect(11,16,14,18,color(1,2));
636
- rect(11,16,14,8,color(1,3));
637
- rect(25,16,-14,24);
638
-
639
- // cab window
640
- rect(15,29,6,-9,color(2,2));
641
- circle(15,21,5,0,PI/2,color(2,4),1);
642
- rect(21,21,-6,9);
643
-
644
- // little stack
645
- rect(37,14,9,6,color(3,2));
646
- rect(37,14,4.5,6,color(3,3));
647
- rect(37,14,9,6);
668
+ rect(7,15,26,-7,0); // cab top
669
+ rect(25,15,8,25,-1); // cab front
670
+ rect(10,40,15,-25,1); // cab back
671
+ rect(14,21,7,9,2); // cab window
672
+ rect(38,15,6,6,2); // little stack
648
673
 
649
674
  // big stack
650
- rect(50,20,10,-8,color(0,1));
651
- rect(50,20,6.5,-8,color(0,2));
652
- rect(50,20,3.5,-8,color(0,3));
653
- rect(50,20,10,-8);
654
- circle(55,2,11.4,.5,PI-.5,color(3,3));
655
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
656
- circle(55,2,11.4,.5,PI-.5);
657
- rect(45,7,20,-7,color(0,2));
658
- rect(45,-1,20,4,color(0,3));
659
- rect(45,-1,20,8);
675
+ rect(49,20,10,-6,0);
676
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
677
+ poly(stackPoints,2,8,6*p);
678
+ rect(44,8,20,-7,0);
660
679
 
661
680
  // engine
662
- for (let i=5; i--;)
663
- {
664
- // stagger radius to fix slight seam
665
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
666
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
667
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
668
- }
681
+ for (let i=5;i--;) circle(59-i*6,30,10,0,7,1,0);
682
+ circle(59,30,4,0,7,2); // light
669
683
 
670
684
  // engine outline
671
- circle(36,30,10,PI/2,PI*3/2);
672
- circle(48,30,10,PI/2,PI*3/2);
673
- circle(60,30,10);
674
- line(36,20,60,20);
675
-
676
- // engine front light
677
- circle(60,30,4,PI,3*PI,color(3,2));
678
- circle(60,30,4,PI,2*PI,color(3,3));
679
- circle(60,30,4,PI,3*PI);
680
-
681
- // front brush
682
- for (let i=6; i--;)
683
- {
684
- x.beginPath();
685
- x.lineTo(53,54);
686
- x.lineTo(53,40);
687
- x.lineTo(53+(1+i*2.9)*p,40);
688
- x.lineTo(53+(4+i*3.5)*p,54);
689
- x.fillStyle = color(0,i%2+2);
690
- x.fill();
691
- i%2 && x.stroke();
692
- }
685
+ rect(35,20,24,0); // top
686
+ circle(59,30,10); // front
687
+ circle(47,30,10,PI/2,PI*3/2); // middle
688
+ circle(35,30,10,PI/2,PI*3/2); // back
689
+ rect(7,40,13,7,-1); // bottom back
690
+ rect(17,40,43,14,-1); // bottom center
693
691
 
694
692
  // wheels
695
- rect(6,40,5,5);
696
- rect(6,40,5,5,color());
697
- rect(15,54,38,-14,color());
698
- for (let i=3; i--;)
699
- for (let j=2; j--;)
700
- {
701
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
702
- x.stroke();
703
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
704
- x.stroke();
705
- }
706
- line(6,40,68,40); // center
707
- line(77,54,4,54); // bottom
708
-
709
- // draw engine name
710
- const s = engineName;
711
- x.font = '900 16px arial';
712
- x.textAlign = 'center';
713
- x.textBaseline = 'top';
714
- x.lineWidth = .1+p*3.9;
715
- let w2 = 0;
716
- for (let i=0; i<s.length; ++i)
717
- w2 += x.measureText(s[i]).width;
718
- for (let j=2; j--;)
719
- for (let i=0, X=41-w2/2; i<s.length; ++i)
693
+ for (let i=3;i--;) for (let j=2;j--;) circle(15*i+17,47,j?7:1,0,7,2);
694
+
695
+ // cowcatcher
696
+ for (let i=2;i--;)
720
697
  {
721
- x.fillStyle = color(i,2);
722
- const w = x.measureText(s[i]).width;
723
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
724
- X += w;
698
+ let w=6, s=7, o=53+w*p*i
699
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
700
+ poly(points,0,40,14);
725
701
  }
702
+
726
703
  x.restore();
727
704
  }
@@ -44,29 +44,46 @@ function audioInit()
44
44
  ///////////////////////////////////////////////////////////////////////////////
45
45
 
46
46
  /**
47
- * Sound Object - Stores a sound for later use and can be played positionally
47
+ * Sound Object - Stores a sound for later
48
+ * - this can be used to load and play wave, mp3, and ogg files
49
+ * - it can also create sounds using the ZzFX sound generator
50
+ * - can attenuate and apply stereo panning to sounds
51
+ * - sound instance control with pause/resume capability
48
52
  *
49
53
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
50
54
  * @memberof Audio
51
55
  * @example
52
- * // create a sound
56
+ * // load an audio asset file
57
+ * const sound_example = new Sound('sound.mp3');
58
+ *
59
+ * // create a zzfx sound
53
60
  * const sound_example = new Sound([.5,.5]);
54
61
  *
55
- * // play the sound
62
+ * // play a sound
56
63
  * sound_example.play();
57
64
  */
58
65
  class Sound
59
66
  {
60
- /** Create a sound object and cache the zzfx samples for later use
61
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
67
+ /**
68
+ * @callback SoundLoadCallback - Function called when sound is loaded
69
+ * @param {Sound} sound
70
+ * @memberof Audio
71
+ */
72
+
73
+ /** Create a sound object and cache the audio for later use
74
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
75
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
62
76
  * @param {number} [range=soundDefaultRange] - World space max range of sound
63
77
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
78
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
64
79
  */
65
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
80
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
66
81
  {
67
82
  if (!soundEnable || headlessMode) return;
68
83
 
69
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
84
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
85
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
86
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
70
87
  ASSERT(isNumber(range), 'range must be a number');
71
88
  ASSERT(isNumber(taper), 'taper must be a number');
72
89
 
@@ -75,23 +92,35 @@ class Sound
75
92
  /** @property {number} - At what percentage of range should it start tapering */
76
93
  this.taper = taper;
77
94
  /** @property {number} - How much to randomize frequency each time sound plays */
78
- this.randomness = 0;
95
+ this.randomness = randomness ?? 0;
79
96
  /** @property {number} - Sample rate for this sound */
80
97
  this.sampleRate = audioDefaultSampleRate;
81
98
  /** @property {number} - Percentage of this sound currently loaded */
82
99
  this.loadedPercent = 0;
100
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
101
+ this.onloadCallback = onloadCallback;
83
102
 
84
- // generate zzfx sound now for fast playback
85
- if (zzfxSound)
103
+ if (Array.isArray(asset))
86
104
  {
105
+ // generate zzfx sound
106
+ const zzfxSound = asset;
107
+
87
108
  // remove randomness so it can be applied on playback
88
- const randomnessIndex = 1, defaultRandomness = .05;
109
+ const defaultRandomness = randomness ?? .05;
110
+ const randomnessIndex = 1;
89
111
  this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
90
112
  zzfxSound[randomnessIndex] = 0;
91
113
 
92
114
  // generate the zzfx samples
93
115
  this.sampleChannels = [zzfxG(...zzfxSound)];
94
116
  this.loadedPercent = 1;
117
+ onloadCallback?.(this);
118
+ }
119
+ else if (typeof asset === 'string')
120
+ {
121
+ // load the audio file
122
+ const filename = asset;
123
+ this.loadSound(filename);
95
124
  }
96
125
  }
97
126
 
@@ -143,7 +172,7 @@ class Sound
143
172
  * @param {number} [volume] - Volume to play the music at
144
173
  * @param {boolean} [loop] - Should the music loop?
145
174
  * @param {boolean} [paused] - Should the music start paused
146
- * @return {SoundInstance} - The audio source node
175
+ * @return {SoundInstance} - The sound instance
147
176
  */
148
177
  playMusic(volume=1, loop=true, paused=false)
149
178
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -153,7 +182,7 @@ class Sound
153
182
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
154
183
  * @param {Vector2} [pos] - World space position to play the sound if any
155
184
  * @param {number} [volume=1] - How much to scale volume by
156
- * @return {SoundInstance} - The audio source node
185
+ * @return {SoundInstance} - The sound instance
157
186
  */
158
187
  playNote(semitoneOffset=0, pos, volume)
159
188
  {
@@ -166,57 +195,14 @@ class Sound
166
195
  * @return {number} - How long the sound is in seconds (undefined if loading)
167
196
  */
168
197
  getDuration()
169
- { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
198
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
170
199
 
171
200
  /** Check if sound is loaded, for sounds fetched from a url
172
201
  * @return {boolean} - True if sound is loaded and ready to play
173
202
  */
174
203
  isLoaded() { return this.loadedPercent === 1; }
175
- }
176
-
177
- ///////////////////////////////////////////////////////////////////////////////
178
-
179
- /**
180
- * Sound Wave Object - Loads and stores an audio file for later use
181
- * - this can be used to load and play wave, mp3, and ogg files
182
- * @extends Sound
183
- * @memberof Audio
184
- * @example
185
- * // load an audio asset file
186
- * const sound_example = new SoundWave('sound.mp3');
187
- *
188
- * // play the sound
189
- * sound_example.play();
190
- */
191
- class SoundWave extends Sound
192
- {
193
- /**
194
- * @callback SoundLoadCallback - Function called when sound is loaded
195
- * @param {SoundWave} sound
196
- * @memberof Audio
197
- */
198
204
 
199
- /** Create a sound object and cache the wave file for later use
200
- * @param {string} filename - Filename of audio file to load
201
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
202
- * @param {number} [range=soundDefaultRange] - World space max range of sound
203
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
204
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
205
- */
206
- constructor(filename, randomness=0, range, taper, onloadCallback)
207
- {
208
- super(undefined, range, taper);
209
- if (!soundEnable || headlessMode) return;
210
- ASSERT(!filename || isString(filename), 'filename must be a string');
211
- ASSERT(isNumber(randomness), 'randomness must be a number');
212
-
213
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
214
- this.onloadCallback = onloadCallback;
215
- this.randomness = randomness;
216
- filename && this.loadSound(filename);
217
- }
218
-
219
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
205
+ /** Loads a sound from a URL and decodes it into sample data.
220
206
  * @param {string} filename
221
207
  * @return {Promise<void>} */
222
208
  async loadSound(filename)
@@ -49,7 +49,7 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
49
49
  /** Asserts if the expression is false, does nothing in release builds
50
50
  * Halts execution if the assert fails and throws an error
51
51
  * @param {boolean} assert
52
- * @param {...Object} [output] - error message output
52
+ * @param {...Object} output - error message output
53
53
  * @memberof Debug */
54
54
  function ASSERT(assert, ...output)
55
55
  {
@@ -59,7 +59,7 @@ function ASSERT(assert, ...output)
59
59
  }
60
60
 
61
61
  /** Log to console if debug is enabled, does nothing in release builds
62
- * @param {...Object} [output] - message output
62
+ * @param {...Object} output - message output
63
63
  * @memberof Debug */
64
64
  function LOG(...output) { console.log(...output); }
65
65
 
@@ -706,8 +706,8 @@ function debugProtectConstant(obj)
706
706
  props.forEach(prop =>
707
707
  {
708
708
  Object.defineProperty(obj, prop, {
709
- get: () => values[prop],
710
- set: (value) =>
709
+ get: ()=> values[prop],
710
+ set: (value)=>
711
711
  {
712
712
  ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
713
713
  },
package/src/engineDraw.js CHANGED
@@ -190,7 +190,7 @@ class TileInfo
190
190
  }
191
191
 
192
192
  /**
193
- * Returns a tile info for an index using this tile as refrence
193
+ * Returns a tile info for an index using this tile as reference
194
194
  * @param {Vector2|number} [index=0]
195
195
  * @return {TileInfo}
196
196
  */
@@ -258,7 +258,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
258
258
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
259
259
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
260
260
 
261
- const textureInfo = tileInfo && tileInfo.textureInfo;
261
+ const textureInfo = tileInfo?.textureInfo;
262
262
  const bleed = tileInfo?.bleed ?? 0;
263
263
  if (useWebGL && glEnable)
264
264
  {
@@ -564,9 +564,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
564
564
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
565
565
  ASSERT(isNumber(angle), 'angle must be a number');
566
566
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
567
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
567
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
568
568
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
569
569
 
570
+ // clamp line width to prevent artifacts
571
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
572
+
570
573
  if (useWebGL && glEnable)
571
574
  {
572
575
  // draw as a regular polygon
@@ -1017,7 +1020,7 @@ let engineFontImage;
1017
1020
  class FontImage
1018
1021
  {
1019
1022
  /** Create an image font
1020
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
1023
+ * @param {TileInfo} tileInfo - Tile info of first character in font
1021
1024
  */
1022
1025
  constructor(tileInfo)
1023
1026
  {
@@ -177,6 +177,8 @@ export
177
177
  saveCanvas,
178
178
  saveDataURL,
179
179
  shareURL,
180
+ readSaveData,
181
+ writeSaveData,
180
182
 
181
183
  // Random
182
184
  rand,
@@ -315,7 +317,6 @@ export
315
317
  audioMasterGain,
316
318
  audioDefaultSampleRate,
317
319
  Sound,
318
- SoundWave,
319
320
  SoundInstance,
320
321
  speak,
321
322
  speakStop,
@@ -408,9 +408,9 @@ function inputInit()
408
408
  function touchInputInit()
409
409
  {
410
410
  // add non passive touch event listeners
411
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
412
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
413
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
411
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
412
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
413
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
414
414
 
415
415
  // handle all touch events the same way
416
416
  let wasTouching;
@@ -616,7 +616,7 @@ function inputUpdate()
616
616
  // poll gamepads
617
617
  const maxGamepads = 8;
618
618
  const gamepads = navigator.getGamepads();
619
- const gamepadCount = min(maxGamepads, gamepads.length)
619
+ const gamepadCount = min(maxGamepads, gamepads.length);
620
620
  for (let i=0; i<gamepadCount; ++i)
621
621
  {
622
622
  // get or create gamepad data
@@ -769,7 +769,7 @@ function inputRender()
769
769
  }
770
770
  }
771
771
 
772
- // center position for right tocuh pad face buttons
772
+ // center position for right touch pad face buttons
773
773
  function touchGamepadButtonCenter()
774
774
  {
775
775
  const center = mainCanvasSize.subtract(vec2(touchGamepadSize));