littlejsengine 1.11.13 → 1.11.17

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 (51) hide show
  1. package/dist/littlejs.d.ts +1306 -66
  2. package/dist/littlejs.esm.js +3097 -269
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +253 -265
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +155 -260
  7. package/examples/box2d/game.js +7 -10
  8. package/examples/box2d/gameObjects.js +33 -33
  9. package/examples/box2d/index.html +6 -6
  10. package/examples/box2d/scenes.js +28 -28
  11. package/examples/breakout/game.js +1 -4
  12. package/examples/breakout/index.html +4 -4
  13. package/examples/breakoutTutorial/index.html +2 -2
  14. package/examples/htmlMenu/game.js +3 -6
  15. package/examples/htmlMenu/index.html +2 -2
  16. package/examples/module/index.html +1 -1
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +0 -3
  19. package/examples/platformer/index.html +8 -8
  20. package/examples/puzzle/game.js +0 -3
  21. package/examples/puzzle/index.html +2 -2
  22. package/examples/shorts/base.html +1 -1
  23. package/examples/starter/build.js +4 -4
  24. package/examples/starter/index.html +13 -13
  25. package/examples/stress/index.html +3 -2
  26. package/examples/uiSystem/game.js +1 -7
  27. package/examples/uiSystem/index.html +3 -3
  28. package/package.json +3 -3
  29. package/plugins/box2d.js +1552 -640
  30. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  31. package/plugins/newgrounds.js +7 -8
  32. package/plugins/pluginExport.js +51 -0
  33. package/plugins/postProcess.js +94 -93
  34. package/plugins/uiSystem.js +145 -161
  35. package/plugins/zzfxm.js +163 -0
  36. package/src/engine.js +15 -20
  37. package/src/engineAudio.js +74 -204
  38. package/src/engineBuild.js +21 -3
  39. package/src/engineDebug.js +104 -6
  40. package/src/engineDraw.js +27 -14
  41. package/src/engineExport.js +12 -4
  42. package/src/engineParticles.js +6 -1
  43. package/src/engineRelease.js +6 -1
  44. package/src/engineSettings.js +2 -2
  45. package/src/engineUtilities.js +5 -11
  46. package/src/engineWebGL.js +19 -7
  47. package/examples/starter/build/index.html +0 -2
  48. package/examples/starter/build/index.js +0 -1
  49. package/examples/starter/build/tiles.png +0 -0
  50. package/examples/starter/game.zip +0 -0
  51. /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
@@ -37,7 +37,12 @@ function debugClear (){}
37
37
  function debugScreenshot (){}
38
38
  function debugSaveCanvas (){}
39
39
  function debugSaveText (){}
40
- function debugSaveDataURL(){}
40
+ function debugSaveDataURL(){}
41
+ function debugShowErrors(){}
42
+ function debugVideoCaptureIsActive(){ return false; }
43
+ function debugVideoCaptureStart (){}
44
+ function debugVideoCaptureStop (){}
45
+ function debugVideoCaptureUpdate(){}
41
46
  /**
42
47
  * LittleJS Utility Classes and Functions
43
48
  * - General purpose math library
@@ -131,7 +136,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
131
136
  * @returns {number}
132
137
  * @memberof Utilities */
133
138
  function lerpWrap(percent, valueA, valueB, wrapSize=1)
134
- { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
139
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
135
140
 
136
141
  /** Returns signed wrapped distance between the two angles passed in
137
142
  * @param {number} angleA
@@ -329,23 +334,17 @@ class RandomGenerator
329
334
  ///////////////////////////////////////////////////////////////////////////////
330
335
 
331
336
  /**
332
- * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
333
- * @param {Vector2|number} [x]
334
- * @param {number} [y]
337
+ * Create a 2d vector, can take 1 or 2 scalar values
338
+ * @param {number} [x]
339
+ * @param {number} [y] - if y is undefined, x is used for both
335
340
  * @return {Vector2}
336
341
  * @example
337
342
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
338
- * let b = vec2(a); // copy a into b
339
343
  * a = vec2(5); // set a to (5, 5)
340
344
  * b = vec2(); // set b to (0, 0)
341
345
  * @memberof Utilities
342
346
  */
343
- function vec2(x=0, y)
344
- {
345
- return typeof x == 'number' ?
346
- new Vector2(x, y == undefined? x : y) :
347
- new Vector2(x.x, x.y);
348
- }
347
+ function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
349
348
 
350
349
  /**
351
350
  * Check if object is a valid Vector2
@@ -1409,8 +1408,8 @@ function setSoundEnable(enable) { soundEnable = enable; }
1409
1408
  function setSoundVolume(volume)
1410
1409
  {
1411
1410
  soundVolume = volume;
1412
- if (soundEnable && !headlessMode && audioGainNode)
1413
- audioGainNode.gain.value = volume; // update gain immediately
1411
+ if (soundEnable && !headlessMode && audioMasterGain)
1412
+ audioMasterGain.gain.value = volume; // update gain immediately
1414
1413
  }
1415
1414
 
1416
1415
  /** Set default range where sound no longer plays
@@ -2076,6 +2075,8 @@ class TextureInfo
2076
2075
  this.image = image;
2077
2076
  /** @property {Vector2} - size of the image */
2078
2077
  this.size = vec2(image.width, image.height);
2078
+ /** @property {Vector2} - inverse of the size, cached for rendering */
2079
+ this.sizeInverse = vec2(1/image.width, 1/image.height);
2079
2080
  /** @property {WebGLTexture} - webgl texture */
2080
2081
  this.glTexture = glEnable && glCreateTexture(image);
2081
2082
  }
@@ -2121,19 +2122,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2121
2122
  * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2122
2123
  * @param {number} [angle] - Angle to rotate by
2123
2124
  * @param {boolean} [mirror] - If true image is flipped along the Y axis
2124
- * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2125
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2125
2126
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2126
2127
  * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2127
2128
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2128
2129
  * @memberof Draw */
2129
2130
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2130
- angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
2131
+ angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2131
2132
  {
2132
2133
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2133
2134
  ASSERT(typeof tileInfo !== 'number' || !tileInfo,
2134
2135
  'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
2135
2136
  ASSERT(isVector2(pos) && isVector2(size));
2136
- ASSERT(isColor(color) && isColor(additiveColor));
2137
+ ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
2137
2138
 
2138
2139
  const textureInfo = tileInfo && tileInfo.getTextureInfo();
2139
2140
  if (useWebGL)
@@ -2144,21 +2145,30 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2144
2145
  pos = screenToWorld(pos);
2145
2146
  size = size.scale(1/cameraScale);
2146
2147
  }
2147
-
2148
2148
  if (textureInfo)
2149
2149
  {
2150
2150
  // calculate uvs and render
2151
- const sizeInverse = vec2(1).divide(textureInfo.size);
2151
+ const sizeInverse = textureInfo.sizeInverse;
2152
2152
  const x = tileInfo.pos.x * sizeInverse.x;
2153
2153
  const y = tileInfo.pos.y * sizeInverse.y;
2154
2154
  const w = tileInfo.size.x * sizeInverse.x;
2155
2155
  const h = tileInfo.size.y * sizeInverse.y;
2156
- const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2157
2156
  glSetTexture(textureInfo.glTexture);
2158
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2159
- x + tileImageFixBleed.x, y + tileImageFixBleed.y,
2160
- x - tileImageFixBleed.x + w, y - tileImageFixBleed.y + h,
2161
- color.rgbaInt(), additiveColor.rgbaInt());
2157
+ if (tileFixBleedScale)
2158
+ {
2159
+ const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2160
+ const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2161
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2162
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2163
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2164
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2165
+ }
2166
+ else
2167
+ {
2168
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2169
+ x, y, x + w, y + h,
2170
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2171
+ }
2162
2172
  }
2163
2173
  else
2164
2174
  {
@@ -2370,16 +2380,15 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
2370
2380
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
2371
2381
  {
2372
2382
  context.fillStyle = color.toString();
2373
- context.lineWidth = lineWidth;
2374
2383
  context.strokeStyle = lineColor.toString();
2384
+ context.lineWidth = lineWidth;
2375
2385
  context.textAlign = textAlign;
2376
2386
  context.font = size + 'px '+ font;
2377
2387
  context.textBaseline = 'middle';
2378
2388
  context.lineJoin = 'round';
2379
2389
 
2380
- pos = pos.copy();
2381
-
2382
2390
  const lines = (text+'').split('\n');
2391
+ pos = pos.copy();
2383
2392
  pos.y -= (lines.length-1) * size/2; // center text vertically
2384
2393
  lines.forEach(line=>
2385
2394
  {
@@ -2449,7 +2458,10 @@ class FontImage
2449
2458
  {
2450
2459
  // load default font image
2451
2460
  if (!engineFontImage)
2452
- (engineFontImage = new Image).src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2461
+ {
2462
+ engineFontImage = new Image;
2463
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2464
+ }
2453
2465
 
2454
2466
  this.image = image || engineFontImage;
2455
2467
  this.tileSize = tileSize;
@@ -3080,16 +3092,15 @@ let audioContext = new AudioContext;
3080
3092
  /** Master gain node for all audio to pass through
3081
3093
  * @type {GainNode}
3082
3094
  * @memberof Audio */
3083
- let audioGainNode;
3095
+ let audioMasterGain;
3084
3096
 
3085
3097
  function audioInit()
3086
3098
  {
3087
3099
  if (!soundEnable || headlessMode) return;
3088
3100
 
3089
- // (createGain is more widely supported then GainNode constructor)
3090
- audioGainNode = audioContext.createGain();
3091
- audioGainNode.connect(audioContext.destination);
3092
- audioGainNode.gain.value = soundVolume; // set starting value
3101
+ audioMasterGain = audioContext.createGain();
3102
+ audioMasterGain.connect(audioContext.destination);
3103
+ audioMasterGain.gain.value = soundVolume; // set starting value
3093
3104
  }
3094
3105
 
3095
3106
  ///////////////////////////////////////////////////////////////////////////////
@@ -3218,6 +3229,8 @@ class Sound
3218
3229
  isLoading() { return !this.sampleChannels; }
3219
3230
  }
3220
3231
 
3232
+ ///////////////////////////////////////////////////////////////////////////////
3233
+
3221
3234
  /**
3222
3235
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
3223
3236
  * - this can be used to play wave, mp3, and ogg files
@@ -3269,59 +3282,7 @@ function playAudioFile(filename, volume=1, loop=false)
3269
3282
  return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3270
3283
  }
3271
3284
 
3272
- /**
3273
- * Music Object - Stores a zzfx music track for later use
3274
- *
3275
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
3276
- * @example
3277
- * // create some music
3278
- * const music_example = new Music(
3279
- * [
3280
- * [ // instruments
3281
- * [,0,400] // simple note
3282
- * ],
3283
- * [ // patterns
3284
- * [ // pattern 1
3285
- * [ // channel 0
3286
- * 0, -1, // instrument 0, left speaker
3287
- * 1, 0, 9, 1 // channel notes
3288
- * ],
3289
- * [ // channel 1
3290
- * 0, 1, // instrument 0, right speaker
3291
- * 0, 12, 17, -1 // channel notes
3292
- * ]
3293
- * ],
3294
- * ],
3295
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
3296
- * 90 // BPM
3297
- * ]);
3298
- *
3299
- * // play the music
3300
- * music_example.play();
3301
- */
3302
- class Music extends Sound
3303
- {
3304
- /** Create a music object and cache the zzfx music samples for later use
3305
- * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
3306
- */
3307
- constructor(zzfxMusic)
3308
- {
3309
- super(undefined);
3310
-
3311
- if (!soundEnable || headlessMode) return;
3312
- this.randomness = 0;
3313
- this.sampleChannels = zzfxM(...zzfxMusic);
3314
- this.sampleRate = zzfxR;
3315
- }
3316
-
3317
- /** Play the music
3318
- * @param {number} [volume=1] - How much to scale volume by
3319
- * @param {boolean} [loop] - True if the music should loop
3320
- * @return {AudioBufferSourceNode} - The audio source node
3321
- */
3322
- playMusic(volume, loop=false)
3323
- { return super.play(undefined, volume, 1, 1, loop); }
3324
- }
3285
+ ///////////////////////////////////////////////////////////////////////////////
3325
3286
 
3326
3287
  /** Speak text with passed in settings
3327
3288
  * @param {string} text - The text to speak
@@ -3393,7 +3354,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3393
3354
  // create and connect gain node
3394
3355
  gainNode = gainNode || audioContext.createGain();
3395
3356
  gainNode.gain.value = volume;
3396
- gainNode.connect(audioGainNode);
3357
+ gainNode.connect(audioMasterGain);
3397
3358
 
3398
3359
  // connect source to stereo panner and gain
3399
3360
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
@@ -3413,7 +3374,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3413
3374
  }
3414
3375
 
3415
3376
  ///////////////////////////////////////////////////////////////////////////////
3416
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
3377
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
3417
3378
 
3418
3379
  /** Generate and play a ZzFX sound
3419
3380
  *
@@ -3455,21 +3416,45 @@ const zzfxR = 44100;
3455
3416
  */
3456
3417
  function zzfxG
3457
3418
  (
3458
- // parameters
3459
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3460
- release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3461
- pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3462
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3419
+ volume = 1,
3420
+ randomness = .05,
3421
+ frequency = 220,
3422
+ attack = 0,
3423
+ sustain = 0,
3424
+ release = .1,
3425
+ shape = 0,
3426
+ shapeCurve = 1,
3427
+ slide = 0,
3428
+ deltaSlide = 0,
3429
+ pitchJump = 0,
3430
+ pitchJumpTime = 0,
3431
+ repeatTime = 0,
3432
+ noise = 0,
3433
+ modulation = 0,
3434
+ bitCrush = 0,
3435
+ delay = 0,
3436
+ sustainVolume = 1,
3437
+ decay = 0,
3438
+ tremolo = 0,
3439
+ filter = 0
3463
3440
  )
3464
3441
  {
3465
- // LJS Note: ZZFX modded so randomness is handled by Sound class
3466
-
3467
3442
  // init parameters
3468
- let PI2 = PI*2, sampleRate = zzfxR,
3443
+ let sampleRate = zzfxR,
3444
+ PI2 = PI*2,
3469
3445
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3470
3446
  startFrequency = frequency *=
3471
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3472
- b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3447
+ (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
3448
+ modOffset = 0, // modulation offset
3449
+ repeat = 0, // repeat offset
3450
+ crush = 0, // bit crush offset
3451
+ jump = 1, // pitch jump timer
3452
+ length, // sample length
3453
+ b = [], // sample buffer
3454
+ t = 0, // sample time
3455
+ i = 0, // sample index
3456
+ s = 0, // sample value
3457
+ f, // wave frequency
3473
3458
 
3474
3459
  // biquad LP/HP filter
3475
3460
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
@@ -3479,35 +3464,37 @@ function zzfxG
3479
3464
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
3480
3465
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3481
3466
 
3482
- // scale by sample rate
3483
- attack = attack * sampleRate + 9; // minimum attack to prevent pop
3484
- decay *= sampleRate;
3485
- sustain *= sampleRate;
3486
- release *= sampleRate;
3487
- delay *= sampleRate;
3488
- deltaSlide *= 500 * PI2 / sampleRate**3;
3489
- modulation *= PI2 / sampleRate;
3490
- pitchJump *= PI2 / sampleRate;
3491
- pitchJumpTime *= sampleRate;
3492
- repeatTime = repeatTime * sampleRate | 0;
3467
+ // scale by sample rate
3468
+ const minAttack = 9; // prevent pop if attack is 0
3469
+ attack = attack * sampleRate || minAttack;
3470
+ decay *= sampleRate;
3471
+ sustain *= sampleRate;
3472
+ release *= sampleRate;
3473
+ delay *= sampleRate;
3474
+ deltaSlide *= 500 * PI2 / sampleRate**3;
3475
+ modulation *= PI2 / sampleRate;
3476
+ pitchJump *= PI2 / sampleRate;
3477
+ pitchJumpTime *= sampleRate;
3478
+ repeatTime = repeatTime * sampleRate | 0;
3493
3479
 
3494
3480
  // generate waveform
3495
3481
  for(length = attack + decay + sustain + release + delay | 0;
3496
- i < length; b[i++] = s * volume) // sample
3482
+ i < length; b[i++] = s * volume) // sample
3497
3483
  {
3498
- if (!(++c%(bitCrush*100|0))) // bit crush
3484
+ if (!(++crush%(bitCrush*100|0))) // bit crush
3499
3485
  {
3500
- s = shape? shape>1? shape>2? shape>3? // wave shape
3501
- Math.sin(t**3) : // 4 noise
3502
- clamp(Math.tan(t),1,-1): // 3 tan
3503
- 1-(2*t/PI2%2+2)%2: // 2 saw
3504
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3505
- Math.sin(t); // 0 sin
3486
+ s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
3487
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
3488
+ Math.sin(t**3) : // 4 noise
3489
+ Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
3490
+ 1-(2*t/PI2%2+2)%2: // 2 saw
3491
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3492
+ Math.sin(t); // 0 sin
3506
3493
 
3507
3494
  s = (repeatTime ?
3508
3495
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
3509
3496
  : 1) *
3510
- sign(s)*(abs(s)**shapeCurve) * // curve
3497
+ (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
3511
3498
  (i < attack ? i/attack : // attack
3512
3499
  i < attack + decay ? // decay
3513
3500
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -3522,136 +3509,32 @@ function zzfxG
3522
3509
  (i<length-delay? 1 : (length-i)/delay) * // release delay
3523
3510
  b[i-delay|0]/2/volume) : s; // sample delay
3524
3511
 
3525
- if (filter) // apply filter
3512
+ if (filter) // apply filter
3526
3513
  s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
3527
3514
  }
3528
3515
 
3529
3516
  f = (frequency += slide += deltaSlide) *// frequency
3530
- Math.cos(modulation*tm++); // modulation
3517
+ Math.cos(modulation*modOffset++); // modulation
3531
3518
  t += f + f*noise*Math.sin(i**5); // noise
3532
3519
 
3533
- if (j && ++j > pitchJumpTime) // pitch jump
3520
+ if (jump && ++jump > pitchJumpTime) // pitch jump
3534
3521
  {
3535
3522
  frequency += pitchJump; // apply pitch jump
3536
3523
  startFrequency += pitchJump; // also apply to start
3537
- j = 0; // stop pitch jump time
3524
+ jump = 0; // stop pitch jump time
3538
3525
  }
3539
3526
 
3540
- if (repeatTime && !(++r % repeatTime)) // repeat
3527
+ if (repeatTime && !(++repeat % repeatTime)) // repeat
3541
3528
  {
3542
- frequency = startFrequency; // reset frequency
3543
- slide = startSlide; // reset slide
3544
- j = j || 1; // reset pitch jump time
3529
+ frequency = startFrequency; // reset frequency
3530
+ slide = startSlide; // reset slide
3531
+ jump ||= 1; // reset pitch jump time
3545
3532
  }
3546
3533
  }
3547
3534
 
3548
- return b;
3535
+ return b; // return sample buffer
3549
3536
  }
3550
-
3551
- ///////////////////////////////////////////////////////////////////////////////
3552
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
3553
-
3554
- /** Generate samples for a ZzFM song with given parameters
3555
- * @param {Array} instruments - Array of ZzFX sound parameters
3556
- * @param {Array} patterns - Array of pattern data
3557
- * @param {Array} sequence - Array of pattern indexes
3558
- * @param {number} [BPM] - Playback speed of the song in BPM
3559
- * @return {Array} - Left and right channel sample data
3560
- * @memberof Audio */
3561
- function zzfxM(instruments, patterns, sequence, BPM = 125)
3562
- {
3563
- let i, j, k;
3564
- let instrumentParameters;
3565
- let note;
3566
- let sample;
3567
- let patternChannel;
3568
- let notFirstBeat;
3569
- let stop;
3570
- let instrument;
3571
- let attenuation;
3572
- let outSampleOffset;
3573
- let isSequenceEnd;
3574
- let sampleOffset = 0;
3575
- let nextSampleOffset;
3576
- let sampleBuffer = [];
3577
- let leftChannelBuffer = [];
3578
- let rightChannelBuffer = [];
3579
- let channelIndex = 0;
3580
- let panning = 0;
3581
- let hasMore = 1;
3582
- let sampleCache = {};
3583
- let beatLength = zzfxR / BPM * 60 >> 2;
3584
-
3585
- // for each channel in order until there are no more
3586
- for (; hasMore; channelIndex++) {
3587
-
3588
- // reset current values
3589
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
3590
-
3591
- // for each pattern in sequence
3592
- sequence.forEach((patternIndex, sequenceIndex) => {
3593
- // get pattern for current channel, use empty 1 note pattern if none found
3594
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
3595
-
3596
- // check if there are more channels
3597
- hasMore |= patterns[patternIndex][channelIndex]&&1;
3598
-
3599
- // get next offset, use the length of first channel
3600
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
3601
- // for each beat in pattern, plus one extra if end of sequence
3602
- isSequenceEnd = sequenceIndex == sequence.length - 1;
3603
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
3604
-
3605
- // <channel-note>
3606
- note = patternChannel[i];
3607
-
3608
- // stop if end, different instrument or new note
3609
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
3610
- instrument != (patternChannel[0] || 0) || note | 0;
3611
-
3612
- // fill buffer with samples for previous beat, most cpu intensive part
3613
- for (j = 0; j < beatLength && notFirstBeat;
3614
-
3615
- // fade off attenuation at end of beat if stopping note, prevents clicking
3616
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
3617
- ) {
3618
- // copy sample to stereo buffers with panning
3619
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
3620
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
3621
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
3622
- }
3623
-
3624
- // set up for next note
3625
- if (note) {
3626
- // set attenuation
3627
- attenuation = note % 1;
3628
- panning = patternChannel[1] || 0;
3629
- if (note |= 0) {
3630
- // get cached sample
3631
- sampleBuffer = sampleCache[
3632
- [
3633
- instrument = patternChannel[sampleOffset = 0] || 0,
3634
- note
3635
- ]
3636
- ] = sampleCache[[instrument, note]] || (
3637
- // add sample to cache
3638
- instrumentParameters = [...instruments[instrument]],
3639
- instrumentParameters[2] *= 2 ** ((note - 12) / 12),
3640
-
3641
- // allow negative values to stop notes
3642
- note > 0 ? zzfxG(...instrumentParameters) : []
3643
- );
3644
- }
3645
- }
3646
- }
3647
-
3648
- // update the sample offset
3649
- outSampleOffset = nextSampleOffset;
3650
- });
3651
- }
3652
-
3653
- return [leftChannelBuffer, rightChannelBuffer];
3654
- }
3537
+
3655
3538
  /**
3656
3539
  * LittleJS Tile Layer System
3657
3540
  * - Caches arrays of tiles to off screen canvas for fast rendering
@@ -4208,7 +4091,12 @@ class ParticleEmitter extends EngineObject
4208
4091
  else
4209
4092
  this.destroy();
4210
4093
 
4211
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
4094
+ if (debugParticles)
4095
+ {
4096
+ // show emitter bounds
4097
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
4098
+ debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
4099
+ }
4212
4100
  }
4213
4101
 
4214
4102
  /** Spawn one particle
@@ -4670,12 +4558,12 @@ function glPreRender()
4670
4558
 
4671
4559
  // set vertex attributes
4672
4560
  let offset = glAdditive = glBatchAdditive = 0;
4673
- let initVertexAttribArray = (name, type, typeSize, size)=>
4561
+ const initVertexAttribArray = (name, type, typeSize, size)=>
4674
4562
  {
4675
4563
  const location = glContext.getAttribLocation(glShader, name);
4676
4564
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
4677
4565
  const divisor = typeSize && 1; // only if not geometry
4678
- const normalize = typeSize==1; // only if color
4566
+ const normalize = typeSize == 1; // only if color
4679
4567
  glContext.enableVertexAttribArray(location);
4680
4568
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
4681
4569
  glContext.vertexAttribDivisor(location, divisor);
@@ -4774,14 +4662,14 @@ function glCreateTexture(image)
4774
4662
  const texture = glContext.createTexture();
4775
4663
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
4776
4664
  if (image && image.width)
4777
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4665
+ glSetTextureData(texture, image);
4778
4666
  else
4779
4667
  {
4780
4668
  // create a white texture
4781
4669
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
4782
4670
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
4783
4671
  }
4784
-
4672
+
4785
4673
  // use point filtering for pixelated rendering
4786
4674
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
4787
4675
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -4789,6 +4677,18 @@ function glCreateTexture(image)
4789
4677
  return texture;
4790
4678
  }
4791
4679
 
4680
+ /** Set WebGL texture data from an image
4681
+ * @param {WebGLTexture} texture
4682
+ * @param {HTMLImageElement} image
4683
+ * @memberof WebGL */
4684
+ function glSetTextureData(texture, image)
4685
+ {
4686
+ // build the texture
4687
+ ASSERT(!!image && image.width > 0, 'Invalid image data.');
4688
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
4689
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4690
+ }
4691
+
4792
4692
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
4793
4693
  * @memberof WebGL */
4794
4694
  function glFlush()
@@ -4842,10 +4742,10 @@ function glSetAntialias(antialias=true)
4842
4742
  * @param {Number} uv0Y
4843
4743
  * @param {Number} uv1X
4844
4744
  * @param {Number} uv1Y
4845
- * @param {Number} rgba
4846
- * @param {Number} [rgbaAdditive=0]
4745
+ * @param {Number} [rgba=-1] - white is -1
4746
+ * @param {Number} [rgbaAdditive=0] - black is 0
4847
4747
  * @memberof WebGL */
4848
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4748
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgbaAdditive=0)
4849
4749
  {
4850
4750
  ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
4851
4751
 
@@ -4898,7 +4798,7 @@ const engineName = 'LittleJS';
4898
4798
  * @type {string}
4899
4799
  * @default
4900
4800
  * @memberof Engine */
4901
- const engineVersion = '1.11.13';
4801
+ const engineVersion = '1.11.17';
4902
4802
 
4903
4803
  /** Frames per second to update
4904
4804
  * @type {number}
@@ -4986,16 +4886,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4986
4886
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
4987
4887
 
4988
4888
  // allow passing in empty functions
4989
- if (!gameInit)
4990
- gameInit = ()=>{};
4991
- if (!gameUpdate)
4992
- gameUpdate = ()=>{};
4993
- if (!gameUpdatePost)
4994
- gameUpdatePost = ()=>{};
4995
- if (!gameRender)
4996
- gameRender = ()=>{};
4997
- if (!gameRenderPost)
4998
- gameRenderPost = ()=>{};
4889
+ gameInit ||= ()=>{};
4890
+ gameUpdate ||= ()=>{};
4891
+ gameUpdatePost ||= ()=>{};
4892
+ gameRender ||= ()=>{};
4893
+ gameRenderPost ||= ()=>{};
4999
4894
 
5000
4895
  // Called automatically by engine to setup render system
5001
4896
  function enginePreRender()
@@ -5022,11 +4917,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5022
4917
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
5023
4918
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
5024
4919
  if (debug) // +/- to speed/slow time
5025
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
4920
+ frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
5026
4921
  timeReal += frameTimeDeltaMS / 1e3;
5027
4922
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5028
4923
  if (!debugSpeedUp)
5029
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
4924
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
4925
+ if (debug && debugVideoCaptureIsActive())
4926
+ frameTimeBufferMS = 0; // disable time smoothing when capturing video
5030
4927
 
5031
4928
  updateCanvas();
5032
4929
 
@@ -5105,6 +5002,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5105
5002
  }
5106
5003
  }
5107
5004
 
5005
+ debugVideoCaptureUpdate();
5108
5006
  requestAnimationFrame(engineUpdate);
5109
5007
  }
5110
5008
 
@@ -5152,11 +5050,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5152
5050
 
5153
5051
  // setup html
5154
5052
  const styleRoot =
5155
- 'margin:0;overflow:hidden;' + // fill the window
5156
- 'width:100vw;height:100vh;' + // fill the window
5157
- 'display:flex;' + // use flexbox
5158
- 'align-items:center;' + // horizontal center
5159
- 'justify-content:center;' + // vertical center
5053
+ 'margin:0;' + // fill the window
5160
5054
  'background:#000;' + // set background color
5161
5055
  (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5162
5056
  'user-select:none;' + // prevent hold to select
@@ -5179,7 +5073,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5179
5073
  overlayContext = overlayCanvas.getContext('2d');
5180
5074
 
5181
5075
  // set canvas style
5182
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
5076
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
5077
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
5183
5078
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5184
5079
  if (glCanvas)
5185
5080
  glCanvas.style.cssText = styleCanvas;
@@ -5190,12 +5085,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5190
5085
  new Promise(resolve =>
5191
5086
  {
5192
5087
  const image = new Image;
5193
- image.crossOrigin = 'anonymous';
5194
5088
  image.onerror = image.onload = ()=>
5195
5089
  {
5196
5090
  textureInfos[textureIndex] = new TextureInfo(image);
5197
5091
  resolve();
5198
5092
  }
5093
+ image.crossOrigin = 'anonymous';
5199
5094
  image.src = src;
5200
5095
  })
5201
5096
  );