littlejsengine 1.8.8 → 1.9.0

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 (47) hide show
  1. package/README.md +17 -17
  2. package/build/littlejs.d.ts +316 -254
  3. package/build/littlejs.esm.js +727 -676
  4. package/build/littlejs.esm.min.js +1 -1
  5. package/build/littlejs.js +726 -676
  6. package/build/littlejs.min.js +1 -1
  7. package/build/littlejs.release.js +661 -612
  8. package/examples/breakout/game.js +4 -4
  9. package/examples/breakout/index.html +3 -3
  10. package/examples/breakoutTutorial/index.html +2 -2
  11. package/examples/electron/game.js +1 -1
  12. package/examples/electron/index.html +2 -2
  13. package/examples/favicon.png +0 -0
  14. package/examples/js13k/index.html +13 -13
  15. package/examples/logo.png +0 -0
  16. package/examples/module/game.js +1 -1
  17. package/examples/module/index.html +1 -1
  18. package/examples/particles/index.html +1 -1
  19. package/examples/platformer/game.js +6 -6
  20. package/examples/platformer/gameCharacter.js +293 -0
  21. package/examples/platformer/gameEffects.js +8 -5
  22. package/examples/platformer/gameObjects.js +13 -13
  23. package/examples/platformer/gamePlayer.js +9 -293
  24. package/examples/platformer/index.html +7 -6
  25. package/examples/puzzle/game.js +3 -2
  26. package/examples/puzzle/index.html +2 -2
  27. package/examples/starter/game.js +3 -3
  28. package/examples/starter/index.html +13 -13
  29. package/examples/starter/tiles.png +0 -0
  30. package/examples/stress/index.html +34 -26
  31. package/examples/typescript/index.html +1 -1
  32. package/package.json +1 -1
  33. package/src/engine.js +50 -47
  34. package/src/engineAudio.js +126 -112
  35. package/src/engineDebug.js +66 -65
  36. package/src/engineDraw.js +47 -56
  37. package/src/engineExport.js +1 -0
  38. package/src/engineInput.js +57 -40
  39. package/src/engineMedals.js +32 -29
  40. package/src/engineObject.js +41 -26
  41. package/src/engineParticles.js +101 -75
  42. package/src/engineRelease.js +1 -1
  43. package/src/engineSettings.js +22 -22
  44. package/src/engineTileLayer.js +34 -33
  45. package/src/engineUtilities.js +44 -44
  46. package/src/engineWebGL.js +105 -126
  47. package/src/jsconfig.json +10 -0
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.8.8';
33
+ const engineVersion = '1.9.0';
34
34
 
35
35
  /** Frames per second to update objects
36
36
  * @type {Number}
@@ -71,14 +71,14 @@ let timeReal = 0;
71
71
 
72
72
  /** Is the game paused? Causes time and objects to not be updated
73
73
  * @type {Boolean}
74
- * @default 0
74
+ * @default false
75
75
  * @memberof Engine */
76
- let paused = 0;
76
+ let paused = false;
77
77
 
78
78
  /** Set if game is paused
79
- * @param {Boolean} paused
79
+ * @param {Boolean} isPaused
80
80
  * @memberof Engine */
81
- function setPaused(_paused) { paused = _paused; }
81
+ function setPaused(isPaused) { paused = isPaused; }
82
82
 
83
83
  // Frame time tracking
84
84
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
@@ -95,7 +95,7 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
95
95
  * @memberof Engine */
96
96
  function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
97
97
  {
98
- ASSERT(Array.isArray(imageSources)); // pass in images as array
98
+ ASSERT(Array.isArray(imageSources), 'pass in images as array');
99
99
 
100
100
  // internal update loop for engine
101
101
  function engineUpdate(frameTimeMS=0)
@@ -105,12 +105,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
105
105
  frameTimeLastMS = frameTimeMS;
106
106
  if (debug || showWatermark)
107
107
  averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
108
- const debugSpeedUp = debug && keyIsDown(107); // +
109
- const debugSpeedDown = debug && keyIsDown(109); // -
108
+ const debugSpeedUp = debug && keyIsDown('Equal'); // +
109
+ const debugSpeedDown = debug && keyIsDown('Minus'); // -
110
110
  if (debug) // +/- to speed/slow time
111
111
  frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
112
112
  timeReal += frameTimeDeltaMS / 1e3;
113
- frameTimeBufferMS += !paused * frameTimeDeltaMS;
113
+ frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
114
114
  if (!debugSpeedUp)
115
115
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
116
116
 
@@ -220,7 +220,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
220
220
  'user-select:none;' + // prevent mobile hold to select
221
221
  '-webkit-user-select:none;' + // compatibility for ios
222
222
  '-webkit-touch-callout:none'; // compatibility for ios
223
- document.body.style = styleBody;
223
+ document.body.style.cssText = styleBody;
224
224
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
225
225
  mainContext = mainCanvas.getContext('2d');
226
226
 
@@ -233,10 +233,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
233
233
  overlayContext = overlayCanvas.getContext('2d');
234
234
 
235
235
  // set canvas style
236
- const styleCanvas =
237
- 'position:absolute;' + // position
236
+ const styleCanvas = 'position:absolute;' + // position
238
237
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
239
- (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
238
+ (glCanvas||mainCanvas).style.cssText = mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
240
239
 
241
240
  // create promises for loading images
242
241
  const promises = imageSources.map((src, textureIndex)=>
@@ -256,13 +255,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
256
255
  showSplashScreen && promises.push(new Promise(resolve =>
257
256
  {
258
257
  let t = 0;
259
- console.log(`LittleJS Engine v${engineVersion}`);
258
+ console.log(`${engineName} Engine v${engineVersion}`);
260
259
  updateSplash();
261
260
  function updateSplash()
262
261
  {
263
262
  clearInput();
264
263
  drawEngineSplashScreen(t+=.01);
265
- t>1 ? resolve() : setTimeout(updateSplash,16);
264
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
266
265
  }
267
266
  }));
268
267
 
@@ -323,7 +322,7 @@ function engineObjectsDestroy()
323
322
 
324
323
  /** Triggers a callback for each object within a given area
325
324
  * @param {Vector2} [pos] - Center of test area
326
- * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
325
+ * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
327
326
  * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
328
327
  * @param {Array} [objects=engineObjects] - List of objects to check
329
328
  * @memberof Engine */
@@ -334,7 +333,7 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
334
333
  for (const o of objects)
335
334
  callbackFunction(o);
336
335
  }
337
- else if (size.x != undefined) // bounding box test
336
+ else if (typeof size === 'object') // bounding box test
338
337
  {
339
338
  for (const o of objects)
340
339
  isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
@@ -352,21 +351,23 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
352
351
 
353
352
  function drawEngineSplashScreen(t)
354
353
  {
355
- // background
356
- const grayscale = 0;
357
- const x = mainContext;
358
- const w = mainCanvas.width = innerWidth;
359
- const h = mainCanvas.height = innerHeight;
360
- const p3 = percent(t, 1, .8);
361
- const p4 = percent(t, 0, .5);
362
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
363
- g.addColorStop(0,hsl(0,0,lerp(p4,0,p3/2),p3));
364
- g.addColorStop(1,hsl(0,0,0,p3));
365
- x.save();
366
- x.fillStyle = g;
367
- x.fillRect(0,0,w,h);
368
-
369
- // logo - fade in and out
354
+ const x = overlayContext;
355
+ const w = overlayCanvas.width = innerWidth;
356
+ const h = overlayCanvas.height = innerHeight;
357
+
358
+ {
359
+ // background
360
+ const p3 = percent(t, 1, .8);
361
+ const p4 = percent(t, 0, .5);
362
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
363
+ g.addColorStop(0,hsl(0,0,lerp(p4,0,p3/2),p3).toString());
364
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
365
+ x.save();
366
+ x.fillStyle = g;
367
+ x.fillRect(0,0,w,h);
368
+ }
369
+
370
+ // draw LittleJS logo...
370
371
  const rect = (X, Y, W, H, C)=>
371
372
  {
372
373
  x.beginPath();
@@ -391,7 +392,7 @@ function drawEngineSplashScreen(t)
391
392
  C ? x.fill() : x.stroke();
392
393
  };
393
394
  const color = (c=0, l=0) =>
394
- hsl([.98,.3,.57,.14][c%4]-10,grayscale?0:.8,[0,.3,.5,.8,.9][l]);
395
+ hsl([.98,.3,.57,.14][c%4]-10,.8,[0,.3,.5,.8,.9][l]).toString();
395
396
  const alpha = wave(1,1,t);
396
397
  const p = percent(alpha, .1, .5);
397
398
 
@@ -401,7 +402,7 @@ function drawEngineSplashScreen(t)
401
402
  x.scale(size,size);
402
403
  x.translate(-40,-35);
403
404
  x.lineJoin = x.lineCap = 'round';
404
- x.lineWidth = 1+p;
405
+ x.lineWidth = .1 + p*1.9;
405
406
 
406
407
  // drawing effect
407
408
  const p2 = percent(alpha,.1,1);
@@ -426,7 +427,7 @@ function drawEngineSplashScreen(t)
426
427
 
427
428
  // little stack
428
429
  rect(37,14,9,6,color(3,2));
429
- rect(37,14,4,6,color(3,3));
430
+ rect(37,14,4.5,6,color(3,3));
430
431
  rect(37,14,9,6);
431
432
 
432
433
  // big stack
@@ -434,9 +435,9 @@ function drawEngineSplashScreen(t)
434
435
  rect(50,20,6,-10,color(0,2));
435
436
  rect(50,20,3,-10,color(0,3));
436
437
  rect(50,10,10,10);
437
- circle(55,2,11,.5,PI-.5,color(3,3));
438
- circle(55,2,11,.5,PI/2,color(3,2),1);
439
- circle(55,2,11,.5,PI-.5);
438
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
439
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
440
+ circle(55,2,11.4,.5,PI-.5);
440
441
  rect(45,7,20,-7,color(0,2));
441
442
  rect(45,0,20,3,color(0,3));
442
443
  rect(45,0,20,7);
@@ -470,12 +471,14 @@ function drawEngineSplashScreen(t)
470
471
  x.lineTo(53+(1+i*2.9)*p,40);
471
472
  x.lineTo(53+(4+i*3.5)*p,54);
472
473
  x.fillStyle = color(0,i%2+2);
473
- x.fill() || i%2 && x.stroke();
474
+ x.fill();
475
+ i%2 && x.stroke();
474
476
  }
475
477
 
476
478
  // wheels
477
- rect(5,40,9,6,color());
478
- rect(15,54,38,-14,color())
479
+ rect(6,40,5,5);
480
+ rect(6,40,5,5,color());
481
+ rect(15,54,38,-14,color());
479
482
  for (let i=3; i--;)
480
483
  for (let j=2; j--;)
481
484
  {
@@ -484,26 +487,26 @@ function drawEngineSplashScreen(t)
484
487
  circle(15*i+15,47,j?7:1,0,PI,color(i,2));
485
488
  x.stroke();
486
489
  }
487
- line(6,40,68,40) // center
488
- line(77,54,4,54) // bottom
490
+ line(6,40,68,40); // center
491
+ line(77,54,4,54); // bottom
489
492
 
490
- // text
493
+ // draw engine name
491
494
  const s = engineName;
492
495
  x.font = '900 16px arial';
493
496
  x.textAlign = 'center';
494
497
  x.textBaseline = 'top';
495
- x.lineWidth = 1+p*3
498
+ x.lineWidth = .1+p*3.9;
496
499
  let w2 = 0;
497
500
  for (let i=0; i<s.length; ++i)
498
501
  w2 += x.measureText(s[i]).width;
499
502
  for (let j=2; j--;)
500
503
  for (let i=0, X=41-w2/2; i<s.length; ++i)
501
504
  {
502
- x.fillStyle = color(i,grayscale?3:2);
505
+ x.fillStyle = color(i,2);
503
506
  const w = x.measureText(s[i]).width;
504
507
  x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
505
508
  X += w;
506
509
  }
507
-
510
+
508
511
  x.restore();
509
512
  }
@@ -54,13 +54,13 @@ class Sound
54
54
 
55
55
  /** Play the sound
56
56
  * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
57
- * @param {Number} [volume=1] - How much to scale volume by (in addition to range fade)
58
- * @param {Number} [pitch=1] - How much to scale pitch by (also adjusted by this.randomness)
59
- * @param {Number} [randomnessScale=1] - How much to scale randomness
60
- * @param {Boolean} [loop=0] - Should the sound loop
57
+ * @param {Number} [volume] - How much to scale volume by (in addition to range fade)
58
+ * @param {Number} [pitch] - How much to scale pitch by (also adjusted by this.randomness)
59
+ * @param {Number} [randomnessScale] - How much to scale randomness
60
+ * @param {Boolean} [loop] - Should the sound loop
61
61
  * @return {AudioBufferSourceNode} - The audio source node
62
62
  */
63
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=0)
63
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
64
64
  {
65
65
  if (!soundEnable || !this.sampleChannels) return;
66
66
 
@@ -93,8 +93,13 @@ class Sound
93
93
  {
94
94
  if (this.source)
95
95
  this.source.stop();
96
- this.source = 0;
96
+ this.source = undefined;
97
97
  }
98
+
99
+ /** Get source of most recent instance of this sound that was played
100
+ * @return {AudioBufferSourceNode}
101
+ */
102
+ getSource() { return this.source; }
98
103
 
99
104
  /** Play the sound as a note with a semitone offset
100
105
  * @param {Number} semitoneOffset - How many semitones to offset pitch
@@ -110,12 +115,7 @@ class Sound
110
115
  */
111
116
  getDuration()
112
117
  { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
113
-
114
- /** Check if the last instance of this sound is playing
115
- * @return {Boolean} - True if the sound is playing
116
- */
117
- isPlaying() { return this.source && !this.source.ended; }
118
-
118
+
119
119
  /** Check if sound is loading, for sounds fetched from a url
120
120
  * @return {Boolean} - True if sound is loading and not ready to play
121
121
  */
@@ -136,13 +136,13 @@ class SoundWave extends Sound
136
136
  {
137
137
  /** Create a sound object and cache the wave file for later use
138
138
  * @param {String} filename - Filename of audio file to load
139
- * @param {Number} [randomness=0] - How much to randomize frequency each time sound plays
139
+ * @param {Number} [randomness] - How much to randomize frequency each time sound plays
140
140
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
141
141
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
142
142
  */
143
143
  constructor(filename, randomness=0, range, taper)
144
144
  {
145
- super(0, range, taper);
145
+ super(undefined, range, taper);
146
146
  this.randomness = randomness;
147
147
 
148
148
  if (!soundEnable) return;
@@ -196,11 +196,11 @@ let soundDecoderContext; // audio context used only to decode audio files
196
196
  class Music extends Sound
197
197
  {
198
198
  /** Create a music object and cache the zzfx music samples for later use
199
- * @param {Array} zzfxMusic - Array of zzfx music parameters
199
+ * @param {[Array, Array, Array, Number]} zzfxMusic - Array of zzfx music parameters
200
200
  */
201
201
  constructor(zzfxMusic)
202
202
  {
203
- super();
203
+ super(undefined);
204
204
 
205
205
  if (!soundEnable) return;
206
206
  this.randomness = 0;
@@ -213,17 +213,17 @@ class Music extends Sound
213
213
  * @param {Boolean} [loop=1] - True if the music should loop
214
214
  * @return {AudioBufferSourceNode} - The audio source node
215
215
  */
216
- playMusic(volume, loop = 1)
217
- { return super.play(0, volume, 1, 1, loop); }
216
+ playMusic(volume, loop = false)
217
+ { return super.play(undefined, volume, 1, 1, loop); }
218
218
  }
219
219
 
220
220
  /** Play an mp3, ogg, or wav audio from a local file or url
221
221
  * @param {String} url - Location of sound file to play
222
- * @param {Number} [volume=1] - How much to scale volume by
223
- * @param {Boolean} [loop=1] - True if the music should loop
222
+ * @param {Number} [volume] - How much to scale volume by
223
+ * @param {Boolean} [loop] - True if the music should loop
224
224
  * @return {HTMLAudioElement} - The audio element for this sound
225
225
  * @memberof Audio */
226
- function playAudioFile(url, volume=1, loop=1)
226
+ function playAudioFile(url, volume=1, loop=false)
227
227
  {
228
228
  if (!soundEnable) return;
229
229
 
@@ -237,9 +237,9 @@ function playAudioFile(url, volume=1, loop=1)
237
237
  /** Speak text with passed in settings
238
238
  * @param {String} text - The text to speak
239
239
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
240
- * @param {Number} [volume=1] - How much to scale volume by
241
- * @param {Number} [rate=1] - How quickly to speak
242
- * @param {Number} [pitch=1] - How much to change the pitch by
240
+ * @param {Number} [volume] - How much to scale volume by
241
+ * @param {Number} [rate] - How quickly to speak
242
+ * @param {Number} [pitch] - How much to change the pitch by
243
243
  * @return {SpeechSynthesisUtterance} - The utterance that was spoken
244
244
  * @memberof Audio */
245
245
  function speak(text, language='', volume=1, rate=1, pitch=1)
@@ -266,7 +266,7 @@ function speakStop() {speechSynthesis && speechSynthesis.cancel();}
266
266
 
267
267
  /** Get frequency of a note on a musical scale
268
268
  * @param {Number} semitoneOffset - How many semitones away from the root note
269
- * @param {Number} [rootNoteFrequency=220] - Frequency at semitone offset 0
269
+ * @param {Number} [rootFrequency=220] - Frequency at semitone offset 0
270
270
  * @return {Number} - The frequency of the note
271
271
  * @memberof Audio */
272
272
  function getNoteFrequency(semitoneOffset, rootFrequency=220)
@@ -280,14 +280,14 @@ let audioContext;
280
280
 
281
281
  /** Play cached audio samples with given settings
282
282
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
283
- * @param {Number} [volume=1] - How much to scale volume by
284
- * @param {Number} [rate=1] - The playback rate to use
285
- * @param {Number} [pan=0] - How much to apply stereo panning
286
- * @param {Boolean} [loop=0] - True if the sound should loop when it reaches the end
283
+ * @param {Number} [volume] - How much to scale volume by
284
+ * @param {Number} [rate] - The playback rate to use
285
+ * @param {Number} [pan] - How much to apply stereo panning
286
+ * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
287
287
  * @param {Number} [sampleRate=44100] - Sample rate for the sound
288
288
  * @return {AudioBufferSourceNode} - The audio node of the sound played
289
289
  * @memberof Audio */
290
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0, sampleRate=zzfxR)
290
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
291
291
  {
292
292
  if (!soundEnable) return;
293
293
 
@@ -327,7 +327,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0, sampleRate
327
327
  }
328
328
 
329
329
  ///////////////////////////////////////////////////////////////////////////////
330
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.2.0 by Frank Force
330
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
331
331
 
332
332
  /** Generate and play a ZzFX sound
333
333
  *
@@ -343,26 +343,27 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
343
343
  const zzfxR = 44100;
344
344
 
345
345
  /** Generate samples for a ZzFX sound
346
- * @param {Number} [volume=1] - Volume scale (percent)
347
- * @param {Number} [randomness=.05] - How much to randomize frequency (percent Hz)
348
- * @param {Number} [frequency=220] - Frequency of sound (Hz)
349
- * @param {Number} [attack=0] - Attack time, how fast sound starts (seconds)
350
- * @param {Number} [sustain=0] - Sustain time, how long sound holds (seconds)
351
- * @param {Number} [release=.1] - Release time, how fast sound fades out (seconds)
352
- * @param {Number} [shape=0] - Shape of the sound wave
353
- * @param {Number} [shapeCurve=1] - Squarenes of wave (0=square, 1=normal, 2=pointy)
354
- * @param {Number} [slide=0] - How much to slide frequency (kHz/s)
355
- * @param {Number} [deltaSlide=0] - How much to change slide (kHz/s/s)
356
- * @param {Number} [pitchJump=0] - Frequency of pitch jump (Hz)
357
- * @param {Number} [pitchJumpTime=0] - Time of pitch jump (seconds)
358
- * @param {Number} [repeatTime=0] - Resets some parameters periodically (seconds)
359
- * @param {Number} [noise=0] - How much random noise to add (percent)
360
- * @param {Number} [modulation=0] - Frequency of modulation wave, negative flips phase (Hz)
361
- * @param {Number} [bitCrush=0] - Resamples at a lower frequency in (samples*100)
362
- * @param {Number} [delay=0] - Overlap sound with itself for reverb and flanger effects (seconds)
363
- * @param {Number} [sustainVolume=1] - Volume level for sustain (percent)
364
- * @param {Number} [decay=0] - Decay time, how long to reach sustain after attack (seconds)
365
- * @param {Number} [tremolo=0] - Trembling effect, rate controlled by repeat time (precent)
346
+ * @param {Number} [volume] - Volume scale (percent)
347
+ * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
348
+ * @param {Number} [frequency] - Frequency of sound (Hz)
349
+ * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
350
+ * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
351
+ * @param {Number} [release] - Release time, how fast sound fades out (seconds)
352
+ * @param {Number} [shape] - Shape of the sound wave
353
+ * @param {Number} [shapeCurve] - Squarenes of wave (0=square, 1=normal, 2=pointy)
354
+ * @param {Number} [slide] - How much to slide frequency (kHz/s)
355
+ * @param {Number} [deltaSlide] - How much to change slide (kHz/s/s)
356
+ * @param {Number} [pitchJump] - Frequency of pitch jump (Hz)
357
+ * @param {Number} [pitchJumpTime] - Time of pitch jump (seconds)
358
+ * @param {Number} [repeatTime] - Resets some parameters periodically (seconds)
359
+ * @param {Number} [noise] - How much random noise to add (percent)
360
+ * @param {Number} [modulation] - Frequency of modulation wave, negative flips phase (Hz)
361
+ * @param {Number} [bitCrush] - Resamples at a lower frequency in (samples*100)
362
+ * @param {Number} [delay] - Overlap sound with itself for reverb and flanger effects (seconds)
363
+ * @param {Number} [sustainVolume] - Volume level for sustain (percent)
364
+ * @param {Number} [decay] - Decay time, how long to reach sustain after attack (seconds)
365
+ * @param {Number} [tremolo] - Trembling effect, rate controlled by repeat time (precent)
366
+ * @param {Number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
366
367
  * @return {Array} - Array of audio samples
367
368
  * @memberof Audio
368
369
  */
@@ -372,78 +373,91 @@ function zzfxG
372
373
  volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
373
374
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
374
375
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
375
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0
376
+ bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
376
377
  )
377
378
  {
378
- // locals
379
- let PI2 = PI*2, startSlide = slide *= 500 * PI2 / zzfxR / zzfxR, b=[],
380
- startFrequency = frequency *= (1 + randomness*rand(-1,1)) * PI2 / zzfxR,
381
- t=0, tm=0, i=0, j=1, r=0, c=0, s=0, f, length
379
+ // init parameters
380
+ let PI2 = PI*2, sampleRate = zzfxR,
381
+ startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
382
+ startFrequency = frequency *=
383
+ rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
384
+ b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
385
+
386
+ // biquad LP/HP filter
387
+ quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
388
+ cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
389
+ a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
390
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
391
+ b1 = -(sign(filter) + cos) / a0, b2 = b0,
392
+ x2 = 0, x1 = 0, y2 = 0, y1 = 0;
382
393
 
383
394
  // scale by sample rate
384
- attack = attack * zzfxR + 9; // minimum attack to prevent pop
385
- decay *= zzfxR;
386
- sustain *= zzfxR;
387
- release *= zzfxR;
388
- delay *= zzfxR;
389
- deltaSlide *= 500 * PI2 / zzfxR**3;
390
- modulation *= PI2 / zzfxR;
391
- pitchJump *= PI2 / zzfxR;
392
- pitchJumpTime *= zzfxR;
393
- repeatTime = repeatTime * zzfxR | 0;
395
+ attack = attack * sampleRate + 9; // minimum attack to prevent pop
396
+ decay *= sampleRate;
397
+ sustain *= sampleRate;
398
+ release *= sampleRate;
399
+ delay *= sampleRate;
400
+ deltaSlide *= 500 * PI2 / sampleRate**3;
401
+ modulation *= PI2 / sampleRate;
402
+ pitchJump *= PI2 / sampleRate;
403
+ pitchJumpTime *= sampleRate;
404
+ repeatTime = repeatTime * sampleRate | 0;
405
+ volume *= soundVolume;
394
406
 
395
407
  // generate waveform
396
- for (length = attack + decay + sustain + release + delay | 0;
397
- i < length; b[i++] = s)
408
+ for(length = attack + decay + sustain + release + delay | 0;
409
+ i < length; b[i++] = s * volume) // sample
398
410
  {
399
- if (!(++c%(bitCrush*100|0))) // bit crush
411
+ if (!(++c%(bitCrush*100|0))) // bit crush
400
412
  {
401
- s = shape? shape>1? shape>2? shape>3? // wave shape
402
- Math.sin((t%PI2)**3) : // 4 noise
403
- max(min(Math.tan(t),1),-1): // 3 tan
404
- 1-(2*t/PI2%2+2)%2: // 2 saw
405
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
406
- Math.sin(t); // 0 sin
407
-
413
+ s = shape? shape>1? shape>2? shape>3? // wave shape
414
+ Math.sin(t**3) : // 4 noise
415
+ clamp(Math.tan(t),1,-1): // 3 tan
416
+ 1-(2*t/PI2%2+2)%2: // 2 saw
417
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
418
+ Math.sin(t); // 0 sin
419
+
408
420
  s = (repeatTime ?
409
421
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
410
422
  : 1) *
411
- sign(s)*(abs(s)**shapeCurve) * // curve 0=square, 2=pointy
412
- volume * soundVolume * ( // envelope
413
- i < attack ? i/attack : // attack
414
- i < attack + decay ? // decay
415
- 1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
416
- i < attack + decay + sustain ? // sustain
417
- sustainVolume : // sustain volume
418
- i < length - delay ? // release
419
- (length - i - delay)/release * // release falloff
420
- sustainVolume : // release volume
421
- 0); // post release
422
-
423
- s = delay ? s/2 + (delay > i ? 0 : // delay
424
- (i<length-delay? 1 : (length-i)/delay) * // release delay
425
- b[i-delay|0]/2) : s; // sample delay
423
+ sign(s)*(abs(s)**shapeCurve) * // curve
424
+ (i < attack ? i/attack : // attack
425
+ i < attack + decay ? // decay
426
+ 1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
427
+ i < attack + decay + sustain ? // sustain
428
+ sustainVolume : // sustain volume
429
+ i < length - delay ? // release
430
+ (length - i - delay)/release * // release falloff
431
+ sustainVolume : // release volume
432
+ 0); // post release
433
+
434
+ s = delay ? s/2 + (delay > i ? 0 : // delay
435
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
436
+ b[i-delay|0]/2/volume) : s; // sample delay
437
+
438
+ if (filter) // apply filter
439
+ s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
426
440
  }
427
441
 
428
- f = (frequency += slide += deltaSlide) * // frequency
429
- Math.cos(modulation*tm++); // modulation
430
- t += f - f*noise*(1 - (Math.sin(i)+1)*1e9%2); // noise
431
-
432
- if (j && ++j > pitchJumpTime) // pitch jump
433
- {
434
- frequency += pitchJump; // apply pitch jump
435
- startFrequency += pitchJump; // also apply to start
436
- j = 0; // reset pitch jump time
437
- }
438
-
439
- if (repeatTime && !(++r % repeatTime)) // repeat
440
- {
441
- frequency = startFrequency; // reset frequency
442
- slide = startSlide; // reset slide
443
- j ||= 1; // reset pitch jump time
442
+ f = (frequency += slide += deltaSlide) *// frequency
443
+ Math.cos(modulation*tm++); // modulation
444
+ t += f + f*noise*Math.sin(i**5); // noise
445
+
446
+ if (j && ++j > pitchJumpTime) // pitch jump
447
+ {
448
+ frequency += pitchJump; // apply pitch jump
449
+ startFrequency += pitchJump; // also apply to start
450
+ j = 0; // stop pitch jump time
451
+ }
452
+
453
+ if (repeatTime && !(++r % repeatTime)) // repeat
454
+ {
455
+ frequency = startFrequency; // reset frequency
456
+ slide = startSlide; // reset slide
457
+ j = j || 1; // reset pitch jump time
444
458
  }
445
459
  }
446
-
460
+
447
461
  return b;
448
462
  }
449
463
 
@@ -454,7 +468,7 @@ function zzfxG
454
468
  * @param {Array} instruments - Array of ZzFX sound paramaters
455
469
  * @param {Array} patterns - Array of pattern data
456
470
  * @param {Array} sequence - Array of pattern indexes
457
- * @param {Number} [BPM=125] - Playback speed of the song in BPM
471
+ * @param {Number} [BPM] - Playback speed of the song in BPM
458
472
  * @return {Array} - Left and right channel sample data
459
473
  * @memberof Audio */
460
474
  function zzfxM(instruments, patterns, sequence, BPM = 125)
@@ -493,10 +507,10 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
493
507
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
494
508
 
495
509
  // check if there are more channels
496
- hasMore ||= !!patterns[patternIndex][channelIndex];
510
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
497
511
 
498
512
  // get next offset, use the length of first channel
499
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - !notFirstBeat) * beatLength;
513
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
500
514
  // for each beat in pattern, plus one extra if end of sequence
501
515
  isSequenceEnd = sequenceIndex == sequence.length - 1;
502
516
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
@@ -512,7 +526,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
512
526
  for (j = 0; j < beatLength && notFirstBeat;
513
527
 
514
528
  // fade off attenuation at end of beat if stopping note, prevents clicking
515
- j++ > beatLength - 99 && stop ? attenuation += (attenuation < 1) / 99 : 0
529
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
516
530
  ) {
517
531
  // copy sample to stereo buffers with panning
518
532
  sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;