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.
- package/README.md +17 -17
- package/build/littlejs.d.ts +316 -254
- package/build/littlejs.esm.js +727 -676
- package/build/littlejs.esm.min.js +1 -1
- package/build/littlejs.js +726 -676
- package/build/littlejs.min.js +1 -1
- package/build/littlejs.release.js +661 -612
- package/examples/breakout/game.js +4 -4
- package/examples/breakout/index.html +3 -3
- package/examples/breakoutTutorial/index.html +2 -2
- package/examples/electron/game.js +1 -1
- package/examples/electron/index.html +2 -2
- package/examples/favicon.png +0 -0
- package/examples/js13k/index.html +13 -13
- package/examples/logo.png +0 -0
- package/examples/module/game.js +1 -1
- package/examples/module/index.html +1 -1
- package/examples/particles/index.html +1 -1
- package/examples/platformer/game.js +6 -6
- package/examples/platformer/gameCharacter.js +293 -0
- package/examples/platformer/gameEffects.js +8 -5
- package/examples/platformer/gameObjects.js +13 -13
- package/examples/platformer/gamePlayer.js +9 -293
- package/examples/platformer/index.html +7 -6
- package/examples/puzzle/game.js +3 -2
- package/examples/puzzle/index.html +2 -2
- package/examples/starter/game.js +3 -3
- package/examples/starter/index.html +13 -13
- package/examples/starter/tiles.png +0 -0
- package/examples/stress/index.html +34 -26
- package/examples/typescript/index.html +1 -1
- package/package.json +1 -1
- package/src/engine.js +50 -47
- package/src/engineAudio.js +126 -112
- package/src/engineDebug.js +66 -65
- package/src/engineDraw.js +47 -56
- package/src/engineExport.js +1 -0
- package/src/engineInput.js +57 -40
- package/src/engineMedals.js +32 -29
- package/src/engineObject.js +41 -26
- package/src/engineParticles.js +101 -75
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +22 -22
- package/src/engineTileLayer.js +34 -33
- package/src/engineUtilities.js +44 -44
- package/src/engineWebGL.js +105 -126
- 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.
|
|
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
|
|
74
|
+
* @default false
|
|
75
75
|
* @memberof Engine */
|
|
76
|
-
let paused =
|
|
76
|
+
let paused = false;
|
|
77
77
|
|
|
78
78
|
/** Set if game is paused
|
|
79
|
-
* @param {Boolean}
|
|
79
|
+
* @param {Boolean} isPaused
|
|
80
80
|
* @memberof Engine */
|
|
81
|
-
function setPaused(
|
|
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)
|
|
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(
|
|
109
|
-
const debugSpeedDown = debug && keyIsDown(
|
|
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 +=
|
|
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(
|
|
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]
|
|
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
|
|
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
|
-
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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()
|
|
474
|
+
x.fill();
|
|
475
|
+
i%2 && x.stroke();
|
|
474
476
|
}
|
|
475
477
|
|
|
476
478
|
// wheels
|
|
477
|
-
rect(
|
|
478
|
-
rect(
|
|
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
|
-
//
|
|
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,
|
|
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
|
}
|
package/src/engineAudio.js
CHANGED
|
@@ -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
|
|
58
|
-
* @param {Number} [pitch
|
|
59
|
-
* @param {Number} [randomnessScale
|
|
60
|
-
* @param {Boolean} [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=
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
217
|
-
{ return super.play(
|
|
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
|
|
223
|
-
* @param {Boolean} [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=
|
|
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
|
|
241
|
-
* @param {Number} [rate
|
|
242
|
-
* @param {Number} [pitch
|
|
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} [
|
|
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
|
|
284
|
-
* @param {Number} [rate
|
|
285
|
-
* @param {Number} [pan
|
|
286
|
-
* @param {Boolean} [loop
|
|
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=
|
|
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.
|
|
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
|
|
347
|
-
* @param {Number} [randomness
|
|
348
|
-
* @param {Number} [frequency
|
|
349
|
-
* @param {Number} [attack
|
|
350
|
-
* @param {Number} [sustain
|
|
351
|
-
* @param {Number} [release
|
|
352
|
-
* @param {Number} [shape
|
|
353
|
-
* @param {Number} [shapeCurve
|
|
354
|
-
* @param {Number} [slide
|
|
355
|
-
* @param {Number} [deltaSlide
|
|
356
|
-
* @param {Number} [pitchJump
|
|
357
|
-
* @param {Number} [pitchJumpTime
|
|
358
|
-
* @param {Number} [repeatTime
|
|
359
|
-
* @param {Number} [noise
|
|
360
|
-
* @param {Number} [modulation
|
|
361
|
-
* @param {Number} [bitCrush
|
|
362
|
-
* @param {Number} [delay
|
|
363
|
-
* @param {Number} [sustainVolume
|
|
364
|
-
* @param {Number} [decay
|
|
365
|
-
* @param {Number} [tremolo
|
|
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
|
-
//
|
|
379
|
-
let PI2 = PI*2,
|
|
380
|
-
|
|
381
|
-
|
|
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 *
|
|
385
|
-
decay *=
|
|
386
|
-
sustain *=
|
|
387
|
-
release *=
|
|
388
|
-
delay *=
|
|
389
|
-
deltaSlide *= 500 * PI2 /
|
|
390
|
-
modulation *= PI2 /
|
|
391
|
-
pitchJump *= PI2 /
|
|
392
|
-
pitchJumpTime *=
|
|
393
|
-
repeatTime = repeatTime *
|
|
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
|
|
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)))
|
|
411
|
+
if (!(++c%(bitCrush*100|0))) // bit crush
|
|
400
412
|
{
|
|
401
|
-
s = shape? shape>1? shape>2? shape>3?
|
|
402
|
-
Math.sin(
|
|
403
|
-
|
|
404
|
-
1-(2*t/PI2%2+2)%2:
|
|
405
|
-
1-4*abs(Math.round(t/PI2)-t/PI2):
|
|
406
|
-
Math.sin(t);
|
|
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) *
|
|
412
|
-
|
|
413
|
-
i < attack
|
|
414
|
-
i
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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)
|
|
429
|
-
Math.cos(modulation*tm++);
|
|
430
|
-
t += f
|
|
431
|
-
|
|
432
|
-
if (j && ++j > pitchJumpTime)
|
|
433
|
-
{
|
|
434
|
-
frequency += pitchJump;
|
|
435
|
-
startFrequency += pitchJump;
|
|
436
|
-
j = 0;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (repeatTime && !(++r % repeatTime))
|
|
440
|
-
{
|
|
441
|
-
frequency = startFrequency;
|
|
442
|
-
slide = startSlide;
|
|
443
|
-
j
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
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;
|